key-rotation-controller / image (push) Failing after 6s
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.
123 lines
5.0 KiB
YAML
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}
|