net-kingdom/docs/security-bootstrap-age-custody.md
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

7.5 KiB

Bootstrap Age Custody

Status: draft control-surface contract Date: 2026-05-24

Purpose

The custodian age keypair is the bootstrap envelope key for NetKingdom security setup. It is not a login account and it is not the runtime secret manager.

The public key may be stored in the control surface and used by automation to encrypt generated bootstrap bundles. The private key is supplied only during an explicit unlock/apply or recovery ceremony.

Minimal Secret Model

The minimal human-held secret set is:

Secret Home Notes
Custodian age private key Password safe or offline custody packet Used to decrypt bootstrap bundles for an attended apply/recovery ceremony
Platform-root password Password safe or offline custody packet Dedicated custody identity, not day-to-day account
Platform-root OTP/recovery Authenticator plus offline recovery Not stored in Git, State Hub, or bootstrap metadata
OpenBao recovery/unseal material OpenBao custody packet Created only after custody approval

Everything else should be generated, encrypted to the custodian public key, applied to the cluster, then shredded or rotated into OpenBao once OpenBao is available.

Trial Versus Custody Mode

Trial mode uses throwaway values to document the steps, UI, failure modes, and operator instructions. Nothing created in trial mode should be trusted as live security material.

Custody mode uses real generated secrets. The expected lifecycle is:

  1. Register the custodian public age recipient in the control surface.
  2. Generate or recover bootstrap secrets.
  3. Encrypt the bundle to the custodian public key.
  4. Apply secrets to the cluster during an attended ceremony.
  5. Shred plaintext sso-mfa/bootstrap/secrets/.
  6. Record only non-secret fingerprints, timestamps, and state transitions.

Control Surface Rules

The NetKingdom control surface may store:

  • custodian public age recipient;
  • public-key fingerprint;
  • private-key location reference, such as a password-safe entry label;
  • encrypted bundle path and file count;
  • whether plaintext bootstrap secrets are currently present; and
  • non-secret apply/verification status.

It must not store:

  • AGE-SECRET-KEY-1... private key material;
  • generated passwords;
  • OTP seeds;
  • OpenBao root tokens or unseal shares;
  • decrypted bootstrap files; or
  • screenshots of secret output.

Current Tooling Alignment

sso-mfa/bootstrap/encrypt-secrets.sh should accept an age public recipient directly. Encryption does not require the private key.

sso-mfa/bootstrap/decrypt-secrets.sh requires the private key path and should be used only in an explicit unlock/apply ceremony. After apply, plaintext files must be shredded.

Custody Unlock Helper

Use sso-mfa/bootstrap/sops-custody-unlock.sh when an operator needs to run a SOPS-backed drill, recovery, or lockdown command but the private key is only in the password safe or offline custody packet.

The helper disables shell tracing before handling input and refuses conflicting input modes. Noninteractive callers must select --from-stdin or --from-file explicitly.

The helper:

  • reads the private age key from one explicit source: hidden prompt, stdin, or a source file;
  • derives the public age recipient with age-keygen -y;
  • refuses to continue unless it matches the expected recipient;
  • writes a 0600 temporary SOPS_AGE_KEY_FILE;
  • runs the requested command or opens a temporary custody shell; and
  • shreds/removes the temporary key file when the command or shell exits.

It must not be used to install the private key permanently on a workstation or server. The source of truth remains the password safe/offline custody packet.

Validate Custody Material

From the NetKingdom repo, validate the supplied private key against keys/age.pub without keeping it on disk:

make sops-custody-check

Run A One-Shot Recovery Drill

For inter-hub:

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 the registered public recipient, sets SOPS_AGE_KEY_FILE for the command, and removes the temporary key after the drill exits.

If using a password-manager CLI, pipe only the private key field:

op read 'op://Platform/NetKingdom custodian age key/private-key' \
  | sso-mfa/bootstrap/sops-custody-unlock.sh \
      --from-stdin \
      -- make -C /home/worsch/inter-hub recovery-drill

Open An Incident Shell

For multi-step recovery or lockdown work:

make sops-custody-shell

Run the required SOPS, recovery, or lockdown commands inside that shell. Exit the shell to shred the temporary key file.

Bootstrap implementation correction — 2026-10-12

The compatibility creds-agent-init command now encrypts using keys/age.pub. It does not generate or discover private keys. Live bootstrap requires the operator terminal; --dry-run is a non-mutating plan. For an encrypted resume, use the explicit custody helper with the independently held matching key. The bootstrap validates that temporary key before contacting the cluster.

The former emergency-bundle export is a non-secret guide. The current encrypted ops bundle is handed off by location and SHA256, and a fresh operator acknowledgment is required before plaintext cleanup/completion. Old delivery flags retain their historical meaning and do not acknowledge a new artifact. Generation no longer logs value prefixes and creates private files even under a permissive caller umask. These controls do not enforce agent runtime isolation.

Recovery verification uses an owner-approved isolated workspace and target. With the helper's temporary key, decrypt the exact bundle to a private file, verify its expected contents without printing values, then remove decrypted files and retain a non-secret receipt. Acknowledgment of a guide is not that verification. No live root or password was read or rotated during the source correction. NK-WP-0049 tracks remaining native acceptance and other legacy custody callers.

Direct helper correction — 2026-10-12

encrypt-secrets.sh defaults to the registered keys/age.pub. It accepts a public recipient or public-recipient file and rejects private-key files instead of loading them to derive metadata. decrypt-secrets.sh accepts an explicit key argument or the custody helper's NETKINGDOM_CUSTODY_AGE_KEY_FILE; it no longer searches HOME. Successful decryption publishes a new private output directory only after every input decrypts. Failed/empty input removes partial staging; existing files, directories and dangling symlinks are never replaced.

The custody helper disables shell tracing, rejects repeated/mixed input modes, does not echo unknown arguments and clears competing inline/key-command SOPS authentication before the child starts. HUP/INT/TERM now retain failure status and EXIT cleanup rather than running cleanup and then continuing. Bash may defer its trap while a foreground child runs; this is not a process-group supervisor, SIGKILL cleanup guarantee or a runtime authority boundary. Temporary key cleanup on command return does not certify that an arbitrary child retained no copies.

Remaining concrete caller defects stay in NK-WP-0049-T02: Make sops-setup / creds-init still install or require permanent HOME keys; Make sops-decrypt still emits plaintext; legacy rotation and privacyIDEA bootstrap helpers contain secret-bearing output. Do not use those legacy paths as the recovery procedure or treat these narrower helper repairs as closure of the whole custody surface.