net-kingdom/tools/rotation-lanes/lanes.yaml
tegwick 666160a390
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
NK-WP-0055-T15: credential rotation-lane register with coverage check
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 308701@bnt-lap001
Assistant-Session: 5548e447-444c-4dcc-bed1-05c16eebfe56
2026-10-28 11:01:08 +01:00

241 lines
14 KiB
YAML

# Credential rotation lanes (NK-WP-0055-T15). Value-free: names, paths and procedures only.
#
# One lane is one way a credential (or a family of credentials rotated the same way) is
# replaced. `check.py` fails if a credential named in the service-identity declarations,
# Credential Management v0.2 section 6 or the crisis custody list has no lane here.
#
# state: admitted (owner lane accepted and runnable) | designed (procedure written, not admitted)
# | deferred (a founder decision holds it) | none (no rotation; reason given)
# blast_radius.level: minimal | low | medium | high
schema: netkingdom.rotation-lanes.v0.1
reviewed: "2026-10-28"
lanes:
# --- service credentials ---------------------------------------------------------------
- id: lldap-directory-bind
class: service
credentials: [LLDAP_LDAP_USER_PASS]
identities: [lldap/admin]
path: "Three-way coordinated change: LLDAP admin password, Authelia and KeyCape bind config, then restart both (Credential Management section 6)"
blast_radius: {level: high, affects: [lldap, authelia, keycape], effect: "SSO and token issuance fail until both consumers carry the new password"}
verification: "Authelia login and a KeyCape authorization-code flow succeed; an LDAP bind with the old password is refused"
owner_lane: [NK-WP-0054-T05, NK-WP-0055-T06]
state: deferred
note: "Bind split (svc-* accounts) replaces this three-way lane; rotation deferral 1591daa1 holds it"
- id: lldap-service-binds
class: service
credentials: []
identities: [lldap/svc-authelia, lldap/svc-keycape, lldap/svc-privacyidea, lldap/svc-provisioner]
path: "Per consumer: new bind password in LLDAP, delivered Secret updated, that consumer alone restarted"
blast_radius: {level: low, affects: [one consumer], effect: "Only the rotated consumer loses directory access"}
verification: "The consumer's own login or resolver check passes; the old password's bind is refused"
owner_lane: [NK-WP-0054-T05]
state: designed
note: "Accounts are planned; the lane becomes real with the bind split"
- id: lldap-super-admin
class: service
credentials: []
identities: [lldap/ld-admin]
path: "To be decided with the account (NK-WP-0054-T06)"
blast_radius: {level: high, affects: [lldap], effect: "Directory administration"}
verification: "Founder login to the LLDAP UI with the new password; old password refused"
owner_lane: [NK-WP-0054-T06]
state: designed
- id: identity-provisioner-token
class: service
credentials: []
identities: [identity-provisioner/identity-provisioner-token]
path: "Owner rotation of Kubernetes Secret identity-provisioner-token, user-engine and provisioner restarted together"
blast_radius: {level: low, affects: [user-engine, identity-provisioner], effect: "User onboarding pauses during the swap"}
verification: "A synthetic onboarding call from user-engine is accepted; a call with the old token is refused"
owner_lane: [NK-WP-0055-T06]
state: deferred
- id: session-and-signing-secrets
class: service
credentials: [PI_SECRET_KEY, LLDAP_JWT_SECRET, AUTHELIA_SESSION_SECRET]
identities: []
path: "Replace the value in its Secret and restart the one service (Credential Management section 6)"
blast_radius: {level: low, affects: [privacyidea, lldap, authelia], effect: "Existing sessions of that service end; users sign in again"}
verification: "Fresh login to the service succeeds after restart"
owner_lane: [NK-WP-0055-T06]
state: deferred
- id: keycape-signing-key
class: service
credentials: [KEYCAPE_RSA_KEY]
identities: []
path: "New signing key published in JWKS, then switched; KeyCape has a single key kid=key-1 and no rotation state today"
blast_radius: {level: high, affects: [keycape, every relying party], effect: "All issued tokens become invalid; brief authentication outage"}
verification: "JWKS serves the new kid; a fresh token validates at OpenBao and access-engine; a token signed by the old key is refused"
owner_lane: [NK-WP-0055-T06]
state: deferred
note: "Overlap rotation (two keys in JWKS) is not implemented; until it is, rotation is an outage window"
- id: privacyidea-pepper
class: service
credentials: [PI_PEPPER]
identities: []
path: "No rotation: re-hashing every privacyIDEA user password would be required"
blast_radius: {level: high, affects: [privacyidea], effect: "All privacyIDEA-local passwords"}
verification: "Not applicable; custody is proven by the restore drill decrypting token material"
owner_lane: [NK-WP-0053-T02]
state: none
- id: privacyidea-resolver-bind
class: service
credentials: []
identities: [privacyidea/keycape-factor-reader]
path: "ESO renews the reader token under policy keycape-factor-renewer"
blast_radius: {level: low, affects: [keycape], effect: "Factor checks fail closed if renewal stops"}
verification: "KeyCape MFA step passes after renewal; ESO status Ready with a fresh refresh time"
owner_lane: [RPF-WP-0035]
state: admitted
- id: privacyidea-legacy-trigger-admin
class: service
credentials: []
identities: [privacyidea/trigger-admin]
path: "Revoke rather than rotate; revocation needs its own approval"
blast_radius: {level: medium, affects: [privacyidea], effect: "Whatever still triggers challenges with it"}
verification: "Token listed as revoked in privacyIDEA; no consumer reports failures"
owner_lane: [NK-WP-0054-T06]
state: none
# --- database credentials --------------------------------------------------------------
- id: privacyidea-database
class: database
credentials: [PI_DB_PASSWORD]
identities: [postgresql/privacyidea]
path: "Atomic update of the PostgreSQL role password and the privacyIDEA pod Secret (Credential Management section 6)"
blast_radius: {level: medium, affects: [privacyidea, postgresql], effect: "MFA unavailable between the two changes"}
verification: "privacyIDEA health and one OTP validation succeed; a connection with the old password is refused"
owner_lane: [NK-WP-0055-T06]
state: deferred
# --- client secrets --------------------------------------------------------------------
- id: authelia-keycape-client
class: client-secret
credentials: [AUTHELIA_KEYCAPE_CLIENT_SECRET]
identities: [authelia/keycape]
path: "Two-way atomic: Authelia verifier and KeyCape upstream secret together (Credential Management section 6)"
blast_radius: {level: medium, affects: [authelia, keycape], effect: "KeyCape cannot complete upstream login until both carry the new secret"}
verification: "A KeyCape login through Authelia succeeds; the old secret is refused at Authelia's token endpoint"
owner_lane: [NK-WP-0055-T06]
state: deferred
- id: keycape-registered-clients
class: client-secret
credentials: []
identities: [keycape/secrets-engine-openbao, keycape/secrets-engine-approval, keycape/secrets-engine-requester,
keycape/approval-engine-operator, keycape/informed-decision-sitting-requester, keycape/rapp-qonto]
path: "Mint in custody, then deliver through the credential broker; KeyCape and the consumer read it at start"
blast_radius: {level: low, affects: [keycape, the one consuming service], effect: "Only that client's token requests fail during the swap"}
verification: "client_credentials token issued for the consumer with its registered scopes; old secret refused"
owner_lane: [RPF-WP-0035, RPF-WP-0055-T01]
state: designed
note: "First-generation custody admitted in RPF-WP-0035; coordinated rotation is RPF-WP-0055-T01 (backlog)"
# --- OpenBao-issued credentials --------------------------------------------------------
- id: openbao-federated-roles
class: openbao-issued
credentials: []
identities: [openbao/netkingdom-platform-admin, openbao/credential-broker-keycape-client-secret-generate,
openbao/fluid-telegram-operators, openbao/secrets-engine-openbao]
path: "Nothing to rotate: tokens are short-lived from OIDC/JWT login; role and policy change by reviewed change"
blast_radius: {level: low, affects: [openbao], effect: "A role change affects only new logins"}
verification: "Role readback matches the committed role file (service-identity metadata exporter, RPF-WP-0067)"
owner_lane: [RPF-WP-0035]
state: admitted
- id: openbao-approle-secret-ids
class: openbao-issued
credentials: []
identities: [openbao/coding-agent]
path: "AppRole secret-id reissued by railiance-platform; JWT role is federated"
blast_radius: {level: low, affects: [coding agent], effect: "Agent loses OpenBao access until the new secret-id is delivered"}
verification: "Login with the new secret-id succeeds; the old secret-id accessor is destroyed"
owner_lane: [RPF-WP-0035]
state: designed
- id: openbao-workload-secrets
class: openbao-issued
credentials: []
identities: []
path: "KV values replaced through the platform operator-write CCR lane; ESO delivers; prefer leases where the engine supports them"
blast_radius: {level: low, affects: [the one workload], effect: "That workload until ESO refreshes"}
verification: "ExternalSecret Ready with a fresh sync; workload health passes"
owner_lane: [RPF-WP-0035]
state: admitted
- id: audit-core-sender-bearers
class: openbao-issued
credentials: []
identities: []
path: "Per-sender bearer in platform/workloads/audit-core/senders, delivered by ESO as a mounted file"
blast_radius: {level: low, affects: [one audit sender], effect: "That sender's events queue until the new bearer is delivered"}
verification: "Sender's next POST /v1/events is accepted; audit-core refuses the old bearer"
owner_lane: [RPF-WP-0035]
state: admitted
# --- break-glass material --------------------------------------------------------------
- id: backup-age-key
class: break-glass
credentials: []
crisis_items: ["1"]
path: "New age key, re-encrypt the backup stream and the break-glass copy, prove a restore with the new key before destroying the old one"
blast_radius: {level: high, affects: [identity backup, break-glass copy], effect: "Every past backup stays readable only with the old key"}
verification: "Isolated restore drill (NK-WP-0053) decrypts a generation made with the new key"
owner_lane: [NK-WP-0053-T02]
state: designed
- id: custodian-age-key
class: break-glass
credentials: []
crisis_items: ["1b"]
path: "Credential Management section 6 'Age key rotation': new key, make sops-rotate, commit, deliver a new bundle before revoking the old key"
blast_radius: {level: high, affects: [SOPS custody files, bootstrap bundles], effect: "Repository custody files unreadable until re-encrypted"}
verification: "make sops-custody-check passes with the new key; make check-secrets passes"
owner_lane: [NK-WP-0055-T06]
state: deferred
- id: storage-share-backup-account
class: break-glass
credentials: []
crisis_items: ["2"]
path: "Provider app-password reissue by the founder; old app password revoked at the provider"
blast_radius: {level: low, affects: [offsite retrieval], effect: "Retrieval of the backup ciphertext"}
verification: "Independent retrieval (retrieve-identity.py) downloads the newest generation with the new password"
owner_lane: [NK-WP-0055-T06]
state: deferred
- id: openbao-unseal-and-root
class: break-glass
credentials: []
crisis_items: ["3"]
path: "Rekey of unseal shares by ceremony; the initial root token is revoked at production entry and later roots come from generate-root"
blast_radius: {level: high, affects: [openbao], effect: "OpenBao cannot be unsealed if the new shares are lost"}
verification: "Restore drill unseals with the new shares; root revoke receipt shows revoke-self 204 then lookup-self 403"
owner_lane: [NK-WP-0055-T16, NK-WP-0055-T06]
state: designed
- id: king-credential
class: break-glass
credentials: [BREAKGLASS_PASSWORD]
identities: [platform/platform-root]
crisis_items: ["4"]
path: "Explicit maintenance window; new password and factors into the independent password safe (Credential Management section 6)"
blast_radius: {level: minimal, affects: [break-glass login], effect: "None while unused"}
verification: "King-path drill passes with the new material"
owner_lane: [NK-WP-0055-T06]
state: deferred
- id: founder-admission-signing-key
class: break-glass
credentials: []
crisis_items: ["5"]
path: "New ed25519 key, allowed_signers updated in the restore admission config, old key removed"
blast_radius: {level: medium, affects: [restore and drill admissions], effect: "No new restore attempt can be admitted until the new key is trusted"}
verification: "A drill admission signed with the new key is accepted; one signed with the old key is refused"
owner_lane: [NK-WP-0055-T06]
state: designed
- id: personal-admin-credentials
class: break-glass
credentials: []
identities: [privacyidea/pi-admin, platform/tegwick]
crisis_items: ["6", "7"]
path: "Owner-managed personal credentials: password change in the service, OTP re-enrolment, password safe updated"
blast_radius: {level: low, affects: [the one person's access], effect: "Only that login"}
verification: "Login with the new credential succeeds; the crisis list's last-proven date is updated"
owner_lane: [NK-WP-0054-T07]
state: admitted
- id: operator-cluster-access
class: break-glass
credentials: []
crisis_items: ["9"]
path: "Replace the static SSH key with a warden certificate lane and reissue the admin kubeconfig client certificate"
blast_radius: {level: medium, affects: [production host and cluster access], effect: "Founder loses direct production access if both lapse"}
verification: "bridge check k3s-api-railiance01 and a read-only kubectl get nodes succeed with the new material"
owner_lane: [NK-WP-0055-T06]
state: deferred