--- id: NK-WP-0004 type: workplan title: "Credential Management Foundation" domain: netkingdom repo: net-kingdom status: done owner: custodian topic_slug: netkingdom created: "2026-03-20" updated: "2026-03-20" state_hub_workstream_id: "d9cf7c4b-886b-4cd1-ad7b-99c4e1929c9e" --- # Credential Management Foundation ## Goal Make credential management a first-class, reliable foundation rather than a manual side-task. By the end of this workplan an operator can: 1. Run `make creds-init` to set up the full SOPS + age + KeePassXC workflow 2. Run `make creds-generate` to produce all service secrets and be guided on KeePassXC entry 3. Run `make creds-apply` to inject secrets into the cluster in the correct order 4. Run `make creds-status` to see what is generated, applied, and verified 5. Invoke `/creds-bootstrap` in Claude Code for guided assistance through the bootstrap process This workplan is a **pre-condition for NK-WP-0003** (cluster deployment). NK-WP-0003-T01 is blocked until this workplan is complete. ## Problem Current state: - `gen-secrets.sh` and `pack-bundle.sh` exist but are run manually, in isolation, with no orchestration - The five `create-secrets.sh` scripts must be run in a specific order (postgres → lldap → authelia → privacyidea → keycape) but this is undocumented and unenforced - Shared secrets (LLDAP_LDAP_USER_PASS, PI_DB_PASSWORD) are referenced across component scripts but there is no enforcement that source exists before consumer runs - No git pre-commit hook — plaintext secrets can accidentally be committed - No `.sops.yaml` — net-kingdom is not SOPS-enabled, unlike railiance-infra - No credential state file — no way to know which secrets are generated, which are applied, which are verified, without manual cluster inspection - The enckey-bootstrap.sh step is time-sensitive (must run while the privacyIDEA pod is live) but nothing flags this or sequences it - Operator must hold all of this in their head ## Architecture ``` Operator │ ├── make creds-init # one-time: age key check, .sops.yaml, git hook ├── make creds-generate # run gen-secrets.sh → guided KeePassXC entry ├── make creds-bundle # age-encrypt ops bundle → offsite ├── make creds-apply # run all create-secrets.sh in correct order ├── make creds-verify # check all K8s secrets exist with expected keys ├── make creds-status # show credential state file └── make creds-rotate SECRET= # guided rotation for one secret Claude Code skill: /creds-bootstrap └── guided session for first-time bootstrap (reads credential state, knows what's done, provides KeePassXC entry instructions, warns about time-sensitive steps like enckey-bootstrap) ``` ## Dependency on canon standard All design decisions in this workplan follow `canon/standards/credential-management_v0.1.md`. The KeePassXC group structure, phase model, SOPS policy, and prohibited patterns defined there are normative. This workplan implements them. ## Tasks ### T01 — SOPS integration ```task id: NK-WP-0004-T01 status: done priority: high state_hub_task_id: "2340f2a3-9c11-44a8-b264-41d75b6dbc3e" ``` Add SOPS encryption infrastructure to net-kingdom, aligned with railiance-infra (same age key, same approach). **Steps:** 1. Verify the operator age key exists: ```bash ls ~/.config/sops/age/key.txt || age-keygen -o ~/.config/sops/age/key.txt ``` The public key (`age1aq8twfd78wvpra0had8cezcnj96tj4q0068edrz5jez8d6xwmflqdepsh4` for the primary operator) is already in railiance-infra. Reuse the same keypair — one age key per operator across all repos. 2. Create `keys/age.pub` at the repo root: ``` age1aq8twfd78wvpra0had8cezcnj96tj4q0068edrz5jez8d6xwmflqdepsh4 ``` 3. Create `.sops.yaml` at the repo root: ```yaml creation_rules: - path_regex: secrets/.*$ key_groups: - age: - age1aq8twfd78wvpra0had8cezcnj96tj4q0068edrz5jez8d6xwmflqdepsh4 ``` 4. Add `secrets/` to `.gitignore` (plaintext secrets MUST NOT enter git). SOPS-encrypted files (`.sops.yaml` extension) may be committed. 5. Create `.githooks/pre-commit` mirroring railiance-infra: - Blocks any commit that includes a file under `secrets/` lacking `sops:` or `"sops":` marker (i.e. plaintext) - Also blocks any file named `*.env` outside of `sso-mfa/bootstrap/` being committed 6. `make hooks` target to enable the hook: ```makefile hooks: git config core.hooksPath .githooks ``` ### T02 — Makefile: SOPS targets ```task id: NK-WP-0004-T02 status: done priority: high state_hub_task_id: "f6ad469c-e1d3-4253-b855-e0554e43f612" ``` Create the top-level `Makefile` for net-kingdom. Port SOPS targets from railiance-infra and add net-kingdom-specific targets. **Targets to implement:** ```makefile ## One-time setup sops-setup: # Copy age key to ~/.config/sops/age/keys.txt hooks: # Enable git pre-commit hook ## SOPS operations sops-edit: # sops sops-encrypt: # sops --encrypt --in-place $(FILE) sops-decrypt: # sops -d $(FILE) (stdout only, never write plaintext to disk) sops-rotate: # sops --rotate --in-place $(FILE) (after adding new recipient) check-secrets: # fail if any secrets/ file is not SOPS-encrypted ## Credential lifecycle creds-init: # prerequisite check + sops-setup + hooks creds-generate: # run gen-secrets.sh + print KeePassXC entry guide creds-bundle: # run pack-bundle.sh with operator age public key creds-apply: # run all create-secrets.sh in dependency order creds-verify: # check all expected K8s secrets exist creds-status: # print credential state file ## Single-secret rotation creds-rotate: # guided rotation for SECRET= (generate → KeePassXC → apply → verify) ``` ### T03 — Credential orchestrator: `creds-apply` ordering ```task id: NK-WP-0004-T03 status: done priority: high state_hub_task_id: "4b386b92-8db9-440c-b116-52dbb2bd68cb" ``` The `creds-apply` Makefile target must run `create-secrets.sh` scripts in the correct dependency order, with prerequisite checks at each step. **Dependency graph:** ``` postgres/create-secrets.sh (no dependencies) │ lldap/create-secrets.sh (needs: lldap/secrets.env) │ ├── authelia/create-secrets.sh (needs: lldap/secrets.env → LLDAP_LDAP_USER_PASS) │ └── keycape/create-secrets.sh (needs: lldap/secrets.env + PI_ADMIN_TOKEN) └── PI_ADMIN_TOKEN available only after T04 privacyidea/create-secrets.sh (needs: privacyidea/secrets.env) │ └── enckey-bootstrap.sh ← TIME-SENSITIVE: must run while pod is live ``` **Implementation:** Create `sso-mfa/bootstrap/creds-apply.sh` that: 1. Checks `KUBECONFIG` is set and cluster is reachable 2. Checks each `secrets//secrets.env` exists before sourcing it 3. Runs scripts in order: postgres → lldap → authelia → privacyidea 4. Explicitly skips keycape (requires PI_ADMIN_TOKEN from post-T04 bootstrap) 5. Prints the keycape step as a manual reminder with the exact command 6. On success, updates `sso-mfa/bootstrap/creds-state.yaml` ### T04 — Credential state file ```task id: NK-WP-0004-T04 status: done priority: high state_hub_task_id: "5bc125a7-ae42-40a3-864c-c356e5fc122d" ``` Create `sso-mfa/bootstrap/creds-state.yaml` — a tracked file (safe to commit, contains no secrets) that records what has been done: ```yaml # Credential state — net-kingdom SSO/MFA stack # This file is safe to commit. It contains no secrets. # Updated automatically by make creds-* targets. generated_at: null # ISO datetime from last gen-secrets.sh run bundle_at: null # ISO datetime from last pack-bundle.sh run keepass_confirmed: false # Manually set to true after KeePassXC entry secrets_applied: postgres: false lldap: false authelia: false privacyidea: false keycape: false # Requires PI_ADMIN_TOKEN (post privacyIDEA T04) enckey_bootstrapped: false # Set after enckey-bootstrap.sh runs pi_admin_created: false # Set after bootstrap-admin.sh runs ``` The `make creds-status` target reads this file and prints a human-readable status table. The `make creds-verify` target checks actual K8s secret existence and updates `secrets_applied` accordingly. `keepass_confirmed` is the only field that requires manual operator intervention to set to `true` — it represents the irreducibly human step in the bootstrap process. ### T05 — git pre-commit hook + `check-secrets` gate ```task id: NK-WP-0004-T05 status: done priority: high state_hub_task_id: "d8ea8fbf-ae89-4675-afba-958187ca37f1" ``` Implement `.githooks/pre-commit` that prevents plaintext secrets from entering git. Port from railiance-infra with net-kingdom-specific additions: **Blocks:** - Any file under `secrets/` without a SOPS marker - Any file matching `*.env` outside of `sso-mfa/bootstrap/` - Any file containing any of these patterns: `PI_SECRET_KEY=`, `PI_PEPPER=`, `LLDAP_JWT_SECRET=`, `AUTHELIA_`, `BREAKGLASS_PASSWORD=` **Warning only (does not block):** - Files matching `*-bundle*.tar.age` being committed (large encrypted artifacts belong offsite, not in git) Add `make hooks-test` target that verifies the hook blocks plaintext (mirrors railiance-infra pattern). ### T06 — Claude Code skill: `/creds-bootstrap` ```task id: NK-WP-0004-T06 status: done priority: medium state_hub_task_id: "b9ecbd3f-17f0-4c1d-97e5-84bfbb43d360" ``` Create `~/.claude/commands/creds-bootstrap.md` — a Claude Code skill that provides guided assistance during the credential bootstrap process. **When to use it:** First-time bootstrap or onboarding a new operator. The skill reads `sso-mfa/bootstrap/creds-state.yaml` and provides contextual guidance based on what has been done. **Skill behavior:** 1. Read `creds-state.yaml` to determine current state 2. Identify the next required step (first `false` in dependency order) 3. For KeePassXC entry steps: display the exact group path and field names to enter, with values sourced from `secrets/` env files (if present) 4. For time-sensitive steps (enckey-bootstrap): print a prominent warning with the exact command and timing constraint 5. For verification steps: run `make creds-verify` and interpret results 6. After each confirmed step: prompt operator to update `creds-state.yaml` or do it automatically when the state can be derived from cluster state **Skill definition file structure:** ```yaml --- description: "Guide through net-kingdom credential bootstrap. Reads creds-state.yaml and provides step-by-step KeePassXC entry instructions, timing warnings, and verification." argument-hint: "[--repo-path /path/to/net-kingdom]" allowed-tools: - Read - Bash(make creds-status:*) - Bash(make creds-verify:*) - Bash(kubectl get secret:*) --- ``` **Note:** The skill does NOT automate KeePassXC entry (that remains a human step). It provides the information an operator needs to do it correctly and verifies the result afterwards. ### T07 — Secret rotation runbook ```task id: NK-WP-0004-T07 status: done priority: medium state_hub_task_id: "e27762d9-aa6a-4a7e-9c34-f8c546797548" ``` Document and automate the rotation procedure for each secret type. Different secrets have different rotation complexity: | Secret | Rotation impact | Procedure | |--------|----------------|-----------| | PI_SECRET_KEY | Flask session reset — all users logged out | Stop pod, rotate, restart | | PI_PEPPER | Cannot rotate without re-hashing all passwords | Treat as permanent | | PI_DB_PASSWORD | DB + K8s Secret must be rotated atomically | pg GRANT + Secret update | | LLDAP_JWT_SECRET | All LLDAP sessions invalidated | Rotate Secret, restart pod | | LLDAP_LDAP_USER_PASS | Must update LLDAP + Authelia + KeyCape atomically | 3-step coordinated | | AUTHELIA_SESSION_SECRET | All Authelia sessions invalidated | Rotate, restart | | AUTHELIA_KEYCAPE_CLIENT_SECRET | Must update Authelia (bcrypt) + KeyCape simultaneously | Coordinated 2-step | | KeyCape RSA signing key | All issued tokens immediately invalidated | Brief auth outage | | PI_ENCFILE | Cannot rotate — replace and re-enroll all tokens | Major operation | | BREAKGLASS_PASSWORD | Low impact, rotate freely | Simple update | Implement `make creds-rotate SECRET=` that: 1. Validates the secret name is known 2. Prints the rotation impact and required coordination steps 3. Generates a new value (same entropy as original) 4. Guides through the atomic update sequence for that secret 5. Updates `creds-state.yaml` and ops bundle after rotation ## Done criteria - [ ] `make creds-init` runs cleanly on a fresh workstation (age key check + setup) - [ ] `make creds-generate` produces all secrets and prints KeePassXC entry guide - [ ] `make creds-bundle` produces an age-encrypted ops bundle - [ ] `make creds-apply` runs all `create-secrets.sh` scripts in dependency order - [ ] `make creds-verify` accurately reflects K8s secret state - [ ] `make creds-status` shows a readable state table from `creds-state.yaml` - [ ] `make hooks-test` confirms pre-commit hook blocks plaintext commits - [ ] `/creds-bootstrap` skill loads, reads state, and provides correct next step - [ ] NK-WP-0003-T01 can be marked done by referencing this workplan as complete