feat(iam): define a KeyRotation for every consumer except poimen-memory
Eight objects cover grafana, argocd, comfyui, minio, paperless, immich, forgejo and api-gw. Each names its ZITADEL app, the Secret it writes, the consumer to roll and whether the consumer reloads hot or needs a restart. All are suspended: the controller image does not exist yet, and an unsuspended object would claim an intent that no process acts on. The README records the retirement target for the 34 .enc.yaml files: six root-of-trust secrets that cannot rotate themselves survive, everything else becomes a KeyRotation and leaves git.
This commit was merged in pull request #84.
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
// Package api declares the KeyRotation custom resource.
|
||||
//
|
||||
// One resource describes one credential and how to rotate it. The controller
|
||||
// never stores the credential in git: it writes a Kubernetes Secret and records
|
||||
// only the credential's identifier in status.
|
||||
package api
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
// Group is the API group for homelab platform resources.
|
||||
const Group = "iam.homelab.riotpiao.com"
|
||||
|
||||
// Version is the served version.
|
||||
const Version = "v1"
|
||||
|
||||
// Provider names. Each provider uses one external API to create and retire
|
||||
// credentials, and each declares whether a rotation can overlap.
|
||||
const (
|
||||
// ProviderZitadelMachineKey creates a machine key while the previous key
|
||||
// stays valid. True zero downtime.
|
||||
ProviderZitadelMachineKey = "zitadel-machine-key"
|
||||
// ProviderZitadelOIDCClient regenerates an app client secret. ZITADEL keeps
|
||||
// one secret per app, so the old one dies the moment the new one exists.
|
||||
ProviderZitadelOIDCClient = "zitadel-oidc-client"
|
||||
)
|
||||
|
||||
// Strategy names.
|
||||
const (
|
||||
// StrategyOverlap keeps the old credential valid until the new one is proven.
|
||||
StrategyOverlap = "overlap"
|
||||
// StrategyAtomic replaces the credential in place. A short window exists.
|
||||
StrategyAtomic = "atomic"
|
||||
)
|
||||
|
||||
// Reload names tell the controller how the consumer picks up a new value.
|
||||
const (
|
||||
// ReloadHot means the consumer watches the mounted file.
|
||||
ReloadHot = "hot"
|
||||
// ReloadRestart means the consumer reads the value at start, so it must roll.
|
||||
ReloadRestart = "restart"
|
||||
)
|
||||
|
||||
// Phase names for status.
|
||||
const (
|
||||
PhasePending = "Pending"
|
||||
PhasePreparing = "Preparing"
|
||||
PhasePropagating = "Propagating"
|
||||
PhaseVerifying = "Verifying"
|
||||
PhaseCompleting = "Completing"
|
||||
PhaseSucceeded = "Succeeded"
|
||||
PhaseFailed = "Failed"
|
||||
PhaseSuspended = "Suspended"
|
||||
)
|
||||
|
||||
// ConsumerRef names a workload that reads the rotated credential.
|
||||
type ConsumerRef struct {
|
||||
// Kind is Deployment or StatefulSet.
|
||||
Kind string `json:"kind"`
|
||||
// Name is the workload name.
|
||||
Name string `json:"name"`
|
||||
// Namespace is where the workload runs.
|
||||
Namespace string `json:"namespace"`
|
||||
}
|
||||
|
||||
// RotationTarget is the credential's source and destination.
|
||||
type RotationTarget struct {
|
||||
// ZitadelProjectID is the project that owns the app or service user.
|
||||
ZitadelProjectID string `json:"zitadelProjectID,omitempty"`
|
||||
// ZitadelAppID is the OIDC app id, for the client-secret provider.
|
||||
ZitadelAppID string `json:"zitadelAppID,omitempty"`
|
||||
// MachineUserID is the service user id, for the machine-key provider.
|
||||
MachineUserID string `json:"machineUserID,omitempty"`
|
||||
// SecretName is the Secret the controller writes.
|
||||
SecretName string `json:"secretName"`
|
||||
// SecretNamespace is where that Secret lives.
|
||||
SecretNamespace string `json:"secretNamespace"`
|
||||
// SecretKeys maps a logical field name to the Secret key that holds it.
|
||||
// For a machine key: key -> key, key-id -> key-id.
|
||||
SecretKeys map[string]string `json:"secretKeys,omitempty"`
|
||||
// Consumers are the workloads that must pick the new value up.
|
||||
Consumers []ConsumerRef `json:"consumers,omitempty"`
|
||||
// Reload is hot or restart. Empty means restart, the safe default.
|
||||
Reload string `json:"reload,omitempty"`
|
||||
}
|
||||
|
||||
// VerifySpec is the proof the controller demands before it retires the old
|
||||
// credential.
|
||||
type VerifySpec struct {
|
||||
// Introspect checks client_id and client_secret against the introspection
|
||||
// endpoint.
|
||||
Introspect bool `json:"introspect,omitempty"`
|
||||
// TokenExchange exchanges a token with the new machine key.
|
||||
TokenExchange bool `json:"tokenExchange,omitempty"`
|
||||
// AudienceProject, when set, is added to the token request as
|
||||
// urn:zitadel:iam:org:project:id:<id>:aud.
|
||||
AudienceProject string `json:"audienceProject,omitempty"`
|
||||
}
|
||||
|
||||
// KeyRotationSpec is the desired state.
|
||||
type KeyRotationSpec struct {
|
||||
// Provider selects the external API.
|
||||
Provider string `json:"provider"`
|
||||
// Strategy is overlap or atomic.
|
||||
Strategy string `json:"strategy"`
|
||||
// IntervalHours is how often to rotate. Minimum 24.
|
||||
IntervalHours int32 `json:"intervalHours"`
|
||||
// Suspend stops automatic rotation without deleting the resource.
|
||||
Suspend bool `json:"suspend,omitempty"`
|
||||
// Target names the credential and its consumers.
|
||||
Target RotationTarget `json:"target"`
|
||||
// Verify names the checks that must pass.
|
||||
Verify VerifySpec `json:"verify,omitempty"`
|
||||
}
|
||||
|
||||
// KeyRotationStatus is the observed state.
|
||||
type KeyRotationStatus struct {
|
||||
Phase string `json:"phase,omitempty"`
|
||||
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
|
||||
LastRotation *metav1.Time `json:"lastRotation,omitempty"`
|
||||
NextRotation *metav1.Time `json:"nextRotation,omitempty"`
|
||||
ActiveCredentialID string `json:"activeCredentialID,omitempty"`
|
||||
PreviousCredentialID string `json:"previousCredentialID,omitempty"`
|
||||
LastError string `json:"lastError,omitempty"`
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
}
|
||||
|
||||
// KeyRotation is one credential under management.
|
||||
type KeyRotation struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec KeyRotationSpec `json:"spec,omitempty"`
|
||||
Status KeyRotationStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// KeyRotationList is a list of KeyRotation.
|
||||
type KeyRotationList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []KeyRotation `json:"items"`
|
||||
}
|
||||
|
||||
// SchemeGroupVersion is the group and version for this type.
|
||||
var SchemeGroupVersion = schema.GroupVersion{Group: Group, Version: Version}
|
||||
|
||||
// SchemeBuilder registers the types with a scheme.
|
||||
var SchemeBuilder = runtime.NewSchemeBuilder(func(s *runtime.Scheme) error {
|
||||
s.AddKnownTypes(SchemeGroupVersion, &KeyRotation{}, &KeyRotationList{})
|
||||
metav1.AddToGroupVersion(s, SchemeGroupVersion)
|
||||
return nil
|
||||
})
|
||||
|
||||
// AddToScheme registers this package's types.
|
||||
var AddToScheme = SchemeBuilder.AddToScheme
|
||||
|
||||
// DeepCopyInto copies the receiver into out.
|
||||
func (in *KeyRotation) DeepCopyInto(out *KeyRotation) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy makes a deep copy.
|
||||
func (in *KeyRotation) DeepCopy() *KeyRotation {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(KeyRotation)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject satisfies runtime.Object.
|
||||
func (in *KeyRotation) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto copies the receiver into out.
|
||||
func (in *KeyRotationSpec) DeepCopyInto(out *KeyRotationSpec) {
|
||||
*out = *in
|
||||
in.Target.DeepCopyInto(&out.Target)
|
||||
}
|
||||
|
||||
// DeepCopyInto copies the receiver into out.
|
||||
func (in *RotationTarget) DeepCopyInto(out *RotationTarget) {
|
||||
*out = *in
|
||||
if in.SecretKeys != nil {
|
||||
out.SecretKeys = make(map[string]string, len(in.SecretKeys))
|
||||
for k, v := range in.SecretKeys {
|
||||
out.SecretKeys[k] = v
|
||||
}
|
||||
}
|
||||
if in.Consumers != nil {
|
||||
out.Consumers = make([]ConsumerRef, len(in.Consumers))
|
||||
copy(out.Consumers, in.Consumers)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopyInto copies the receiver into out.
|
||||
func (in *KeyRotationStatus) DeepCopyInto(out *KeyRotationStatus) {
|
||||
*out = *in
|
||||
if in.LastRotation != nil {
|
||||
t := *in.LastRotation
|
||||
out.LastRotation = &t
|
||||
}
|
||||
if in.NextRotation != nil {
|
||||
t := *in.NextRotation
|
||||
out.NextRotation = &t
|
||||
}
|
||||
if in.Conditions != nil {
|
||||
out.Conditions = make([]metav1.Condition, len(in.Conditions))
|
||||
copy(out.Conditions, in.Conditions)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopyInto copies the receiver into out.
|
||||
func (in *KeyRotationList) DeepCopyInto(out *KeyRotationList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
out.Items = make([]KeyRotation, len(in.Items))
|
||||
for i := range in.Items {
|
||||
in.Items[i].DeepCopyInto(&out.Items[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy makes a deep copy.
|
||||
func (in *KeyRotationList) DeepCopy() *KeyRotationList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(KeyRotationList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject satisfies runtime.Object.
|
||||
func (in *KeyRotationList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user