diff --git a/workplans/CUST-WP-0073-agent-credential-separation.md b/workplans/CUST-WP-0073-agent-credential-separation.md new file mode 100644 index 0000000..519e082 --- /dev/null +++ b/workplans/CUST-WP-0073-agent-credential-separation.md @@ -0,0 +1,161 @@ +--- +id: CUST-WP-0073 +type: workplan +title: "Agents cannot read secret values: separate agent and admin credentials" +domain: infotech +repo: the-custodian +status: proposed +owner: claude-code +topic_slug: custodian +flavor: implementation +created: "2026-09-24" +updated: "2026-09-24" +related: + - KEY-WP-0033 + - RPF-WP-0044 +origin: incident +origin_ref: key-cape/docs/operations.md#before-any-live-change +--- + +# Agents cannot read secret values: separate agent and admin credentials + +## Why + +On 2026-09-23 a key-cape agent session meant to read only Secret metadata. +The go-template failed on an absent field, and kubectl printed +`sso/keycape-config` in full as debugging output: the signing key, the LLDAP +bind password and the Authelia client secret. Three more Secrets in `sso`/`mfa` +were probably printed as well. + +The root cause is the credentials, not the command: + +- Workstation `kubectl` and `ssh railiance01` both authenticate as + `system:admin` in `system:masters`, which bypasses authorization. No RBAC + rule can restrict it. +- k3s runs with `--write-kubeconfig-mode=644`, so `/etc/rancher/k3s/k3s.yaml` + is world-readable, and anything running as `tegwick` is cluster-admin. +- The paths that leak a value are open-ended: template error dumps, + `-o yaml`/`describe`, `helm get`, `kubectl logs`, `exec … env`, ConfigMaps + holding credentials, last-applied annotations, and the kubeconfig file itself. + A command denylist chases them one by one, and it only binds the harness that + enforces it. + +**Goal:** the identity an agent uses cannot read a secret value by any command. +Then a leak needs a human's attended credential, not a mistake. + +**Scope:** builder mode, founder decision 2026-09-24. Rotating the exposed +Secrets is deferred, not dropped (T05). This plan removes the problem class. + +## Decide the identity model + +```task +id: CUST-WP-0073-T01 +status: todo +priority: high +``` + +Decide, with railiance-platform and ops-warden: + +- **Agent identity:** a dedicated kube identity outside `system:masters`, + bound to the built-in `view` role plus the specific write verbs agents need + (for example patch and rollout restart on Deployments, ConfigMap updates). + No `secrets` verbs at all, since `list` and `watch` return data too. No + `pods/exec`, `pods/attach`, `pods/portforward` or `nodes/proxy`. No `helm`, + which stores its releases in Secrets. +- **Admin identity:** stays `system:admin`, reachable only by an attended step, + never readable from the agent's Unix account. +- **Where the agent credential lives** on the workstation and on railiance01. + ops-warden already distinguishes `adm`/`agt`/`atm` SSH principals. Mapping + `agt` to a restricted account on railiance01 is the obvious candidate; how + warden provisions those principals is still to be verified. +- **Paths that stay attended:** Secret writes, helm releases and break-glass. + +Output: a decision record in the-custodian, resolved by the founder +(`GOVERN @ estate`). + +## Build and hand out the agent identity + +```task +id: CUST-WP-0073-T02 +status: todo +priority: high +``` + +Owner: railiance-platform (RBAC), railiance-enablement (k3s install), +ops-warden (principal mapping). + +- Create the ServiceAccount or client certificate, the ClusterRole and the + binding in git, applied by the owner's documented path. +- Set `write-kubeconfig-mode` to `600` in the k3s install config. Attended admin + use becomes `sudo k3s kubectl`. +- Workstation: `~/.kube/config` points at the agent kubeconfig. The admin + kubeconfig moves out of the agent's reach (another account, or short-lived + issuance through an attended login). +- Proof: as the agent identity, `kubectl auth can-i get secrets -A` and + `can-i create pods/exec -A` both answer `no`, and `kubectl auth whoami` + shows no `system:masters`. Record the output as evidence. + +## Reject last-applied annotations on Secrets + +```task +id: CUST-WP-0073-T03 +status: todo +priority: medium +``` + +Owner: railiance-platform. + +- Add a `ValidatingAdmissionPolicy` (v1.35 is available) with its binding. It + rejects any Secret carrying `kubectl.kubernetes.io/last-applied-configuration`. +- Strip the annotation from existing Secrets first, cluster-wide, using the + presence-check template, or the policy blocks their next update. Known: + `sso/keycape-config`, `sso/authelia-secrets`, `sso/lldap-secrets`, + `mfa/privacyidea-config`. +- Proof: a client-side `kubectl apply` of a test Secret in a scratch namespace + is refused. + +## Fleet guidance and harness guards + +```task +id: CUST-WP-0073-T04 +status: todo +priority: medium +``` + +- Orientation doc §6: add the template-error dump; state that no template or + jsonpath may run against a Secret except the tested presence check; point to + the agent identity once T02 lands. Announce it as a standing notice that + supersedes the 2026-09-21 one. +- Claude Code guard, **done on the workstation 2026-09-24**: + `~/.claude/hooks/guard-secret-reads.py` (PreToolUse on Bash). It denies Secret + reads, `helm get`, `config view --raw` and kubeconfig reads, and asks before + exec/attach/cp/debug and `--raw`. The `kubectl * get secret *` allow rule and + the expired 2026-07-07 temporary elevation are removed. Known false positive: + any command naming both kubectl and "secret", including an `echo`. That is + acceptable for a stopgap. +- Offer the same guard to the Codex and Grok harnesses, or record that they have + none. Until T02 lands, those agents are protected by instructions only. +- Open question for the founder: `Bash(bao read *)`, `vault kv get` and + `vault read` are still pre-approved and print secret values the same way. + +## Rotate what was exposed + +```task +id: CUST-WP-0073-T05 +status: wait +priority: low +``` + +Deferred by founder decision on 2026-09-24 (builder mode). Rotate +`sso/keycape-config` (signing key, LLDAP bind password, Authelia client secret), +`sso/authelia-secrets`, `sso/lldap-secrets` and `mfa/privacyidea-config` +together, with `replace` rather than `apply`, in one attended window. + +Reopen on the first of: + +- the first production workload or customer data passing through KeyCape +- any sign the 2026-09-23 transcript left the workstation beyond the model + provider +- a planned key rotation + +The passage of time alone reopens nothing.