net-kingdom/sso-mfa/bootstrap
tegwick 4cf26a9bc8
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
fix: make custody input explicit and remove partial decryptions
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a12d13-27f2-7163-b8c0-f682af02772c
2026-10-12 10:53:30 +02:00
..
secrets.enc chore(creds): encrypted secrets [agent NK-WP-0005] 2026-03-21 12:09:53 +00:00
creds-apply.sh feat(creds): implement NK-WP-0004 Credential Management Foundation 2026-03-20 23:39:35 +00:00
creds-bootstrap-agent.sh fix: separate bootstrap encryption from private root custody 2026-10-12 09:41:08 +02:00
creds-rotate.sh feat(creds): NK-WP-0005 — agent-driven credential bootstrap 2026-03-21 08:38:52 +00:00
creds-state.yaml NET-WP-0020-T02: wire OpenBao init/unseal as Phase 7b in creds-bootstrap-agent (operator-reviewed) 2026-07-02 13:32:58 +02:00
creds-status.sh fix: separate bootstrap encryption from private root custody 2026-10-12 09:41:08 +02:00
creds-verify.sh fix(creds-bootstrap): harden agent bootstrap for non-interactive execution 2026-03-21 12:11:13 +00:00
decrypt-secrets.sh fix: make custody input explicit and remove partial decryptions 2026-10-12 10:53:30 +02:00
emergency-bundle.sh fix: separate bootstrap encryption from private root custody 2026-10-12 09:41:08 +02:00
encrypt-secrets.sh fix: make custody input explicit and remove partial decryptions 2026-10-12 10:53:30 +02:00
gen-secrets.sh fix: separate bootstrap encryption from private root custody 2026-10-12 09:41:08 +02:00
openbao-init-unseal.sh NET-WP-0020 finished: attended-ceremony + auto-unseal-transit profiles, greenfield init/unseal proof 2026-07-02 22:08:33 +02:00
pack-bundle.sh feat(sso-mfa): Phase 0a bootstrap tooling (NK-WP-0001-T01) 2026-03-02 09:01:50 +01:00
README.md fix: separate bootstrap encryption from private root custody 2026-10-12 09:41:08 +02:00
sops-custody-unlock.sh fix: make custody input explicit and remove partial decryptions 2026-10-12 10:53:30 +02:00

Phase 0a — Pre-cluster Secret Bootstrap

This directory contains tooling for the KeePassXC bootstrap phase (T01 Phase 0a). No secrets are stored here — only scripts and documentation.

Why KeePassXC first?

The single-credential bootstrap principle (Decision D1): one master password unlocks the KeePassXC vault; all other credentials are generated inside it. KeePassXC is the pre-cluster source of truth. After the K3s cluster is running, secrets migrate into HashiCorp Vault (T01 Phase 0b) and KeePassXC becomes the break-glass / dev-local backup.

KeePassXC database structure

Create a new .kdbx database. Use a strong master password stored in your personal password manager (Bitwarden, 1Password, etc.).

Recommended group structure:

net-kingdom/
├── privacyIDEA/
│   ├── pi-admin          (username: pi-admin, password: PI_ADMIN_PASSWORD)
│   ├── database          (username: privacyidea, password: PI_DB_PASSWORD)
│   ├── SECRET_KEY        (password field only — PI_SECRET_KEY value)
│   ├── PI_PEPPER         (password field only — PI_PEPPER value)
│   └── PI_ENCFILE        (binary attachment: pi.enc — generate after deploy)
├── PostgreSQL/
│   ├── postgres root     (username: postgres, password: PG_ROOT_PASSWORD)
│   ├── keycloak user     (username: keycloak, password: PG_KEYCLOAK_PASSWORD)
│   └── privacyidea user  (username: privacyidea — same password as PI_DB_PASSWORD)
├── Keycloak/
│   ├── admin             (username: admin, password: KC_ADMIN_PASSWORD)
│   └── database          (username: keycloak — same password as PG_KEYCLOAK_PASSWORD)
├── Break-glass/
│   ├── break-glass       (username: break-glass, password: BREAKGLASS_PASSWORD)
│   └── recovery-otp      (TOTP seed — enroll manually after Keycloak is up)
└── Vault/
    └── (populated in Phase 0b after Vault is deployed)

Workflow

Step 1 — Generate secrets

chmod +x gen-secrets.sh
./gen-secrets.sh ./secrets

This writes .env files to ./secrets/ (gitignored). Inspect each file, then copy each value into the appropriate KeePassXC entry.

Step 2 — Create the age encryption key (one-time)

age-keygen -o ~/net-kingdom-ops-bundle.key

The public key prints to stdout. The private key is in the .key file. Store the .key file somewhere safe (NOT in this repo; NOT in the secrets/ dir).

Step 3 — Create the encrypted ops bundle

chmod +x pack-bundle.sh
./pack-bundle.sh ./secrets "age1..." ops-bundle.tar.age

Store ops-bundle.tar.age offsite (cloud storage, external drive, separate location).

Step 4 — Shred the generated files

find ./secrets -type f -exec shred -u {} \;
rm -rf ./secrets

Step 5 — PI_ENCFILE (after privacyIDEA container is running — T04)

# Generate the encryption key inside the running container:
kubectl exec -n mfa <pi-pod-name> -- pi-manage create_enckey

# Extract it:
kubectl cp -n mfa <pi-pod-name>:/etc/privacyidea/enckey ./pi.enc

# Store as a binary attachment in KeePassXC → net-kingdom/privacyIDEA/PI_ENCFILE

# Create the K8s Secret:
kubectl create secret generic privacyidea-enckey \
    --from-file=PI_ENCFILE=./pi.enc \
    --namespace mfa

# Shred the local copy:
shred -u ./pi.enc

Notes on secret reuse

Some secrets appear in multiple components — this is intentional to avoid drift. When adding to KeePassXC, note the cross-references rather than duplicating the value:

  • PI_DB_PASSWORD == PostgreSQL privacyidea user password
  • KC_DB_PASSWORD == PostgreSQL keycloak user password

Use KeePassXC references ({REF:P@T:UUID}) to avoid maintaining two copies.

Phase 0b — HashiCorp Vault (after T02, once K3s is running)

See ../vault/ (created in T01 Phase 0b) for:

  • Vault Helm chart values
  • ESO (External Secrets Operator) configuration
  • Vault secret path layout
  • Migration procedure: KeePassXC → Vault

Current bootstrap and recovery handoff

make creds-agent-init is now attended and public-recipient based. Register keys/age.pub from independently established custody first. No root private key is generated or discovered by bootstrap. --dry-run is a non-mutating plan. make creds-emergency-reprint prints only the non-secret recovery guide.

For a resume that needs decryption, use the custody unlock helper below. After apply, the operator must store and independently verify the fresh encrypted ops bundle before acknowledging its handoff. Source tests use disposable keys and a simulated cluster; native acceptance remains NK-WP-0049.

Custody Age Key Unlock

For SOPS-backed drills and incident work, do not copy the custodian age private key into a permanent workstation path. Use the custody helper from the repo root:

make sops-custody-run COMMAND='make -C /home/worsch/inter-hub recovery-drill'

The helper prompts for the AGE-SECRET-KEY-1... line from the password safe or offline custody packet, validates it against keys/age.pub, writes a temporary SOPS_AGE_KEY_FILE, runs the command, and removes the temporary file on exit.

For multi-step recovery work:

make sops-custody-shell

Exit the shell when finished so the temporary key file is removed.