- canon/standards/credential-management_v0.1.md: single root-of-trust credential hierarchy standard - canon/standards/federated-organization-standard_v1.0.md: FOS reference architecture (VSM-based) - wiki/BigPictureGuidance.md: integration guidance for OAS + FOS orthogonal layers - workplans/CUST-WP-0025-fos-hub-bootstrap.md: 4-phase plan (identity, hub-core extraction, ops-hub, fin-hub) - state-hub/Makefile: treat exit 2 (warnings-only) as success in check-consistency targets Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
10 KiB
title, version, status, domain, scope, created
| title | version | status | domain | scope | created |
|---|---|---|---|---|---|
| Credential Management Standard | 0.1 | Draft Standard | custodian | all-domains | 2026-03-20 |
Credential Management Standard
Version: 0.1 Status: Draft Standard Scope: All domains and repositories in the federated organization
1. Purpose
This standard defines how credentials, secrets, and key material are managed across all systems — from a developer workstation with no infrastructure, to a fully operational Kubernetes cluster.
The core principle is a single root of trust: one operator keypair anchors all credential storage and encryption. Every secret can be traced back to that root. No secret lives outside this hierarchy.
2. Trust Hierarchy
Operator passphrase (human memory only — never stored anywhere)
│
└── age keypair (~/.config/sops/age/key.txt — one per operator)
│
├── SOPS encryption (GitOps secrets in all repos)
│ └── secrets/**/*.sops.yaml — encrypted at rest in git
│
├── Ops bundle (age-encrypted tar — offsite backup)
│ └── ops-bundle-<date>.tar.age
│ └── all service secrets at point-in-time
│
└── KeePassXC (pre-cluster primary credential store)
│ └── master password = operator passphrase (or derived)
│
├── Infrastructure credentials
│ ├── SSH keys (server access)
│ ├── API tokens (Gitea, HostEurope, Hetzner)
│ └── Cloud credentials
│
├── Service secrets (per-domain groups)
│ ├── net-kingdom/privacyidea/
│ ├── net-kingdom/lldap/
│ ├── net-kingdom/authelia/
│ ├── net-kingdom/keycape/
│ └── railiance/postgres/
│
└── Vault root token (in-cluster phase, stored here)
└── HashiCorp Vault
└── External Secrets Operator (ESO)
└── K8s Secrets → pods
3. Phases
Phase 0 — Pre-cluster (bootstrap)
Used when: No Kubernetes cluster is available. Local development, initial server provisioning, CI bootstrap.
Tools: age keypair + KeePassXC + ops bundle
Flow:
- Generate service secrets with a
gen-secrets.shscript - Copy each secret manually into KeePassXC (under the appropriate group)
- Encrypt a point-in-time ops bundle:
pack-bundle.sh <secrets-dir> <age-pub-key> - Store the ops bundle offsite (separate physical location from KeePassXC)
- Shred the plaintext secrets directory:
find secrets/ -type f -exec shred -u {} \; - When deploying to k8s, read each secret from KeePassXC and inject via
create-secrets.shscripts that produce K8s Secrets
Invariant: Plaintext secrets MUST NOT persist on disk after being stored in KeePassXC. The only durable forms are: KeePassXC + ops bundle.
Phase 1 — GitOps secrets (SOPS)
Used when: Secrets need to live alongside infrastructure code in git. All repos with infrastructure manifests use this pattern.
Tools: SOPS + age
Configuration (.sops.yaml in repo root):
creation_rules:
- path_regex: secrets/.*$
age: >-
<operator-age-public-key>
- path_regex: .*\.sops\.yaml$
age: >-
<operator-age-public-key>
Multi-operator: When a second operator joins, add their age public key
as an additional recipient and re-encrypt all secrets with sops updatekeys.
Both keys can decrypt independently — no single point of failure.
Invariant: The age private key is NEVER committed to git. The public
key is committed (in .sops.yaml and keys/age.pub). Encrypted values
in git are safe to store and review.
Phase 2 — In-cluster (HashiCorp Vault)
Used when: Kubernetes cluster is operational and stable.
Tools: HashiCorp Vault + External Secrets Operator (ESO)
Why ESO over Vault Agent Injector: ESO produces standard K8s Secrets, which are compatible with plain Helm charts and do not require pod annotation changes. Decision D4 (net-kingdom DECISIONS.md).
Flow:
- Bootstrap Vault with the root token stored in KeePassXC
- Enable Kubernetes auth method (
vault auth enable kubernetes) - Create per-service policies with least-privilege access
- Migrate each service secret from KeePassXC into Vault
- Deploy ESO
SecretStorepointing to Vault - Replace
create-secrets.shcalls withExternalSecretmanifests - Vault reconciles secrets into K8s Secrets automatically
KeePassXC post-cluster: Remains the source of truth for:
- The Vault root/unseal keys (emergency only)
- Dev/sandbox systems that do not connect to in-cluster Vault
- New secrets before they are migrated into Vault
4. KeePassXC Group Structure
All service secrets are organized under a standardized group hierarchy:
KeePassXC root
├── Infrastructure
│ ├── SSH Keys
│ │ └── <hostname> (private key as attachment, public key as note)
│ ├── API Tokens
│ │ ├── gitea-admin
│ │ ├── hosteurope-api
│ │ └── hetzner-api
│ └── Cloud Credentials
│ └── <provider>
│
├── net-kingdom
│ ├── privacyidea
│ │ ├── PI_SECRET_KEY
│ │ ├── PI_PEPPER
│ │ ├── PI_DB_PASSWORD
│ │ ├── pi-admin (password + totp-seed)
│ │ ├── trigger-admin (password + API token)
│ │ └── enckey (attachment: enckey file + audit keypair)
│ ├── lldap
│ │ ├── LLDAP_JWT_SECRET
│ │ └── LLDAP_LDAP_USER_PASS
│ ├── authelia
│ │ ├── AUTHELIA_JWT_SECRET
│ │ ├── AUTHELIA_SESSION_SECRET
│ │ ├── AUTHELIA_STORAGE_ENCRYPTION_KEY
│ │ ├── AUTHELIA_OIDC_HMAC_SECRET
│ │ └── AUTHELIA_KEYCAPE_CLIENT_SECRET
│ └── keycape
│ ├── RSA signing key (attachment: private + public PEM)
│ └── PI_ADMIN_TOKEN
│
├── railiance
│ ├── postgres
│ │ └── PG_ROOT_PASSWORD
│ └── sops-age
│ └── age private key (attachment: key.txt)
│
└── vault
├── root-token
└── unseal-keys (attachment: unseal-keys.txt, gpg-encrypted)
5. Age Keypair Management
One keypair per operator. The same key is used for:
- SOPS encryption across all repos
- Ops bundle encryption
Generate:
age-keygen -o ~/.config/sops/age/key.txt
# output: Public key: age1...
Add to repos: Copy the public key into .sops.yaml of each repo and
into keys/age.pub. Commit both.
Back up: The private key file MUST be stored in KeePassXC as an
attachment under railiance/sops-age/age private key. The KeePassXC
database is the disaster recovery path for the age private key.
Rotation: If the private key is compromised, generate a new keypair,
add the new public key to all repos, re-encrypt all secrets with
sops updatekeys, then revoke the old key from all .sops.yaml files.
6. Ops Bundle
The ops bundle is a point-in-time snapshot of all service secrets, encrypted with age and stored offsite.
Create:
bash gen-secrets.sh ./secrets # generates all secrets as env files
# ... enter each into KeePassXC ...
bash pack-bundle.sh ./secrets <age-pub-key> # → ops-bundle-<date>.tar.age
find secrets/ -type f -exec shred -u {} \; # shred plaintext
Restore:
age -d -i ~/.config/sops/age/key.txt -o secrets.tar ops-bundle-<date>.tar.age
tar xf secrets.tar
# re-run create-secrets.sh scripts from restored env files
Frequency: Create a new ops bundle:
- Before any major cluster operation (migration, upgrade, rekey)
- After adding or rotating any service secret
- At least once per quarter
7. Prohibited Patterns
These are hard violations regardless of context:
| Pattern | Why prohibited |
|---|---|
| Plaintext secrets committed to git | Unrecoverable leak |
| Secrets in environment variables in shell history | ~/.bash_history exposure |
| Sharing secrets via chat, email, or issue trackers | Uncontrolled propagation |
| Using the same password for multiple services | Single-point compromise |
| Storing age private key only on a single machine | Catastrophic loss on disk failure |
| Hardcoded secrets in application code or Helm values | Accidental publishing |
8. Multi-operator Extension
When a second operator needs access:
- They generate their own age keypair (
age-keygen) - Share only the public key (never the private key)
- Primary operator adds it to
.sops.yamlin all repos - Primary operator runs
sops updatekeys <file>on all encrypted files - Both operators can now encrypt and decrypt independently
- Share KeePassXC database via an encrypted channel (never plaintext) — the other operator opens it with their own master password after import
9. Vault Migration Checklist
When the cluster is stable enough to operate Vault:
- Deploy Vault via Helm with HA mode (3 replicas minimum)
- Store root token and unseal keys in KeePassXC (vault/ group)
- Enable Kubernetes auth method
- Create per-service Vault policies (least privilege)
- Deploy ESO
ClusterSecretStorepointing to Vault - For each service: create
ExternalSecretmanifest, verify K8s Secret reconciles correctly, then delete the manually-created K8s Secret - Verify ESO auto-rotation works (reduce TTL to 1h, confirm rotation)
- Remove
create-secrets.shscripts from deployment runbooks - Update this standard to Phase 2 operational status
10. Summary
| Situation | Tool | Source of truth |
|---|---|---|
| No cluster, local dev | KeePassXC + create-secrets.sh | KeePassXC |
| GitOps secrets in repo | SOPS + age | Git (ciphertext) |
| Cluster operational | Vault + ESO | Vault (KeePassXC holds root) |
| Disaster recovery | Ops bundle (age) | Offsite encrypted archive |
| Multi-operator | SOPS multi-recipient | Each operator's age keypair |