Files
homelab/k8s/apps/key-rotation-controller/crd.yaml
T
rock c5058dadc6
key-rotation-controller / image (push) Failing after 6s
fix(iam): merge into the consumer Secret, and retry with a backoff
Two defects that a real rotation exposed:

  merge      writeSecret replaced the whole Secret map, so every key a consumer
             keeps beside the credential disappeared. paperless lost
             SOCIALACCOUNT_PROVIDERS_JSON and immich lost config.json; both pods then
             failed to start while the rotation reported success.
  backoff    a Failed attempt returned 0, so the controller retried in a tight loop.
             It now waits two minutes and then retries.

All eight objects carry both identifiers ZITADEL needs: the app resource id for the
secret endpoint and the client id for introspection.

Result: seven rotations reach Succeeded with every consumer ready (grafana, comfyui,
argocd, forgejo, paperless, immich, minio). api-gw stays suspended because no workload
exists for it.
2026-10-27 19:59:23 -07:00

123 lines
5.0 KiB
YAML

# KeyRotation declares one credential and how to rotate it.
#
# The resource never holds a secret value. It names where the value goes and how
# to prove the new value works. The controller writes the Secret.
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: keyrotations.iam.homelab.riotpiao.com
spec:
group: iam.homelab.riotpiao.com
scope: Namespaced
names:
kind: KeyRotation
plural: keyrotations
singular: keyrotation
shortNames: [kr]
versions:
- name: v1
served: true
storage: true
additionalPrinterColumns:
- {name: Provider, type: string, jsonPath: .spec.provider}
- {name: Strategy, type: string, jsonPath: .spec.strategy}
- {name: Phase, type: string, jsonPath: .status.phase}
- {name: Last, type: date, jsonPath: .status.lastRotation}
- {name: Next, type: date, jsonPath: .status.nextRotation}
subresources:
status: {}
schema:
openAPIV3Schema:
type: object
required: [spec]
properties:
spec:
type: object
required: [provider, strategy, intervalHours, target]
properties:
provider:
type: string
enum: [zitadel-machine-key, zitadel-oidc-client]
description: >-
Which external API creates the credential.
strategy:
type: string
enum: [overlap, atomic]
description: >-
overlap keeps the old credential valid until the new one is
proven. atomic replaces it in place, because the provider
allows only one value.
intervalHours:
type: integer
minimum: 1
description: How often to rotate.
suspend:
type: boolean
description: Stop automatic rotation without deleting the resource.
target:
type: object
required: [secretName, secretNamespace]
properties:
zitadelProjectID: {type: string}
zitadelAppID: {type: string}
zitadelClientID: {type: string}
machineUserID: {type: string}
secretName: {type: string}
secretNamespace: {type: string}
secretKeys:
type: object
additionalProperties: {type: string}
description: >-
Logical field name to Secret key. Unmapped fields keep
their logical name.
reload:
type: string
enum: [hot, restart]
description: >-
hot means the consumer watches the file. restart rolls the
consumer so it reads the new value at start.
consumers:
type: array
items:
type: object
required: [kind, name, namespace]
properties:
kind: {type: string, enum: [Deployment, StatefulSet]}
name: {type: string}
namespace: {type: string}
verify:
type: object
properties:
introspect:
type: boolean
description: Check client id and secret against introspection.
tokenExchange:
type: boolean
description: Exchange a token with the new machine key.
audienceProject:
type: string
description: Project id added to the token audience scope.
status:
type: object
properties:
phase:
type: string
enum: [Pending, Preparing, Propagating, Verifying, Completing, Succeeded, Failed, Suspended]
observedGeneration: {type: integer, format: int64}
lastRotation: {type: string, format: date-time}
nextRotation: {type: string, format: date-time}
activeCredentialID: {type: string}
previousCredentialID: {type: string}
lastError: {type: string}
conditions:
type: array
items:
type: object
required: [type, status, reason, lastTransitionTime]
properties:
type: {type: string}
status: {type: string}
reason: {type: string}
message: {type: string}
lastTransitionTime: {type: string, format: date-time}