docs: contain credential rotation guidance #26

Merged
eding merged 1 commit from docs/credential-containment into main 2026-09-05 21:17:19 +02:00
3 changed files with 69 additions and 61 deletions

View file

@ -1,2 +1,11 @@
edingrech # Credential onboarding
dckr_pat_bfKKDH4g3qUxchs9UMLxFx2oTiU
Do not put credential values in this file, Git, generated configuration, command arguments, terminal history, or logs.
1. Obtain written authorization for `<credential-purpose>`, `<consumer-inventory>`, `<approved-scope>`, and `<rotation-window>`.
2. Have the authorized operator enter `<credential-value>` only through the approved secure prompt or standard input boundary.
3. Store it only in the configured SOPS-encrypted operational-secrets file, then reseed and verify OpenBao before changing consumers.
4. Keep the previous credential active only for the approved overlap window; revoke it only after every consumer check succeeds.
5. Record `<credential-identifier>`, `<timestamp>`, `<operator>`, and `<status>` without recording any credential value.
See [docs/secrets.md](docs/secrets.md) and [docs/runbooks/credential-rotation.md](docs/runbooks/credential-rotation.md).

View file

@ -0,0 +1,43 @@
# Credential rotation runbook
## Authorization and inputs
Before any live action, obtain approval for the exact `<credential-identifier>`, `<credential-purpose>`, `<approved-scope>`, `<consumer-inventory>`, `<overlap-window>`, `<maintenance-window>`, and `<rollback-owner>`. The authorized operator must have access to the approved configuration, SOPS identity, recovery identity and encrypted recovery bundle, OpenBao recovery recipient, and the required Forgejo, Flux, webhook, Tekton, registry, and cluster permissions. Credential values must be entered only through an approved secure prompt or standard input boundary; never provide them to an agent or place them in an argument, file, log, or evidence record.
## Consumer inventory template
| Consumer | Logical secret path | Credential identifier | Owner | Validation | Status |
| --- | --- | --- | --- | --- | --- |
| `<consumer>` | `<encrypted-logical-path>` | `<credential-identifier>` | `<owner>` | `<sanitized-check>` | `<pending>` |
Include Forgejo API access, Flux source authentication, webhook authorization, Tekton pipeline consumers, and registry pull/push consumers when applicable. Stop for an unlisted or ambiguous consumer; do not guess its credential source.
## Ordered rotation
1. Confirm the old credential remains valid for the approved overlap window and capture the sanitized baseline status.
2. Create the replacement with the approved minimum scope. Do not revoke or replace the old credential yet.
3. Put the replacement only in `sops.operationalSecretsPath`; encrypt it with `sops.ageKeyPath`. Keep the recovery identity and encrypted recovery bundle in their configured local paths. Do not write plaintext configuration or generated files.
4. Reseed OpenBao from the encrypted operational-secrets file. Verify OpenBao is initialized, unsealed, and ready, then verify External Secrets has refreshed the intended target Secret without reading or printing its data.
5. Validate each inventoried consumer: Forgejo authenticated operation, Flux source authentication and reconciliation readiness, webhook delivery authorization, Tekton trigger and PipelineRun behavior, and registry pull/push behavior. Record only identifiers, timestamps, and pass/fail status.
6. If every validation passes during the overlap window, revoke the old credential through its owning system and repeat the affected consumer checks.
## Rollback
If OpenBao reseed, readiness, External Secrets refresh, or any consumer validation fails, stop before revocation. Restore the previously encrypted operational-secret version, reseed OpenBao, verify readiness and all affected consumers, and keep the old credential active. Escalate if the previous encrypted version or recovery material is unavailable; do not reconstruct values from logs or configuration.
## Sanitized evidence template
| Field | Record |
| --- | --- |
| Rotation ID | `<rotation-id>` |
| Credential identifier | `<credential-identifier>` |
| Scope approval | `<approval-reference>` |
| Operator | `<operator-id>` |
| Started / completed | `<timestamp>` / `<timestamp>` |
| OpenBao initialized / unsealed / ready | `<status>` / `<status>` / `<status>` |
| External Secrets target refresh | `<target-identifier>: <status>` |
| Forgejo / Flux / webhook / Tekton / registry | `<status>` / `<status>` / `<status>` / `<status>` / `<status>` |
| Old credential revocation | `<not-attempted|completed|rolled-back>` |
| Follow-up | `<sanitized-reference>` |
Never include values, encoded values, headers, token fragments, private keys, recovery shares, or decrypted manifest content in the evidence.

View file

@ -1,66 +1,22 @@
# Bootstrap Secret Inputs # Secrets and recovery material
`bootstrap` uses the Flux age identity in ignored `.age/` storage to encrypt ## Storage contract
the configured Democratic CSI Secret directly into the generated cluster
repository. Its TrueNAS API key is never printed or committed in plaintext.
`operational-secrets.sops.yaml` is decrypted only in MaidnCLI memory after - `sops.operationalSecretsPath` defaults to `<workspace>/operational-secrets.sops.yaml`. It is a SOPS-encrypted input containing operational secrets, keyed by logical OpenBao paths. It is never plaintext Git content.
OpenBao is initialized. It is not copied to the cluster repository. Its schema - `sops.ageKeyPath` defaults to `<workspace>/.age/key.txt`. This Flux SOPS identity is private local material; do not commit, print, or copy it.
is: - `sops.recoveryIdentityPath` defaults to `<workspace>/.age/recovery-key.txt`. This separate OpenBao-recovery identity is private local material; do not commit, print, or copy it.
- `sops.recoveryBundlePath` defaults to `<workspace>/.recovery/openbao-recovery.age`. This bundle is encrypted to `sops.recoveryRecipient`; it contains OpenBao recovery material and is never plaintext Git content.
- During generated Flux repository reconciliation, recovery material is rendered only into `base/openbao/unseal.sops.yaml`, encrypted with the Flux SOPS identity. This is the narrowly scoped encrypted recovery path for the `openbao-unseal` controller; no plaintext recovery material belongs in a workload, config, log, or command argument.
```yaml ## Bootstrap gates
secrets:
cicd/forgejo:
username: encrypted-value
token: encrypted-value
cicd/forgejo-registry:
dockerconfigjson: encrypted-value
cicd/forgejo-webhook:
authorization: encrypted-value
platform/pihole:
server: encrypted-value
password: encrypted-value
platform/cloudflare:
api-token: encrypted-value
platform/cloudflare-tunnel:
credentials: encrypted-value
config: encrypted-value
```
Keys are written to OpenBao KV v2 under `secret/<path>`. Additional paths are `bootstrap` resolves and validates the configuration before reconciliation. Any bootstrap requires a readable SOPS age identity. `talos.autoBootstrapFlux: true` additionally requires an existing encrypted operational-secrets file and a nonempty recovery recipient. A non-webhook bootstrap additionally requires configured, existing recovery-identity and recovery-bundle files.
allowed when they use lowercase path characters and scalar property names.
Set all `democraticCsi` settings in the bootstrap configuration or provide The webhook-only path requires a complete delivery contract, an approved configuration, and a readable SOPS age identity. It must reseed OpenBao and observe the refreshed `forgejo-webhook` target Secret plus the EventListener and Pipeline before Forgejo is changed. The registry-token and webhook-authorization flows also require an explicit configuration; they are live credential operations and are not offline-safe.
them through the interactive wizard. The CLI writes those values only to
`base/democratic-csi/secret.sops.yaml` in the generated cluster repository.
For an existing configuration, run `bootstrap --config <path>
--prompt-democratic-csi` to enter the settings with the API key masked.
For a new operational-secret input, run `bootstrap --config <path> ## Rules
--prompt-operational-secrets`. It derives Forgejo Git and registry credentials
from the configured Forgejo account, prompts for the Pi-hole server and masked
password and Cloudflare API token, and generates the webhook authorization
value. The Cloudflare token issues the Gateway certificate and manages explicit
Tunnel CNAME records; it is not used by ExternalDNS.
Import the user-approved local credential file before bootstrap validates the - Credential values are accepted only at an approved secure input boundary and stored only in encrypted operational or recovery material.
tunnel state: - Never pass credential values in CLI arguments, URLs, logs, Git commits, generated config, tickets, or evidence.
- Do not revoke a previous credential until OpenBao, External Secrets, and every listed consumer have passed validation.
```powershell - Use the sanitized procedure in [runbooks/credential-rotation.md](runbooks/credential-rotation.md) for any live rotation.
go run . cloudflare-tunnel import --config <private-bootstrap-config> --credentials-file <local-credentials-json>
```
The import command reads the file only in memory, validates its credential
shape, stores only the credentials JSON and terminal-404 local config shown
above, and seeds OpenBao. It never saves a Tunnel run token.
The template maps these simple OpenBao properties to the `credentials.json` and
`config.yml` Kubernetes filenames.
`cicd/forgejo-webhook.authorization` is required for delivery bootstrap. The
CLI supplies it as the Forgejo webhook Authorization header and Tekton compares
that header against the ExternalSecret-derived `forgejo-webhook` Secret.
Run `bootstrap --config <path> --initialize-openbao-recovery` to create and
save a separate recovery age identity for `openbao-recovery.age`. The Flux SOPS
age identity is installed in `flux-system`; it must not encrypt OpenBao
recovery material.