Propose CUST-WP-0073: agents cannot read secret values.
Separate agent and admin cluster credentials after the 2026-09-23 key-cape exposure, reject last-applied annotations by admission policy, and record the deferred rotation with its triggers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e6baac329f
commit
8b0a8ad11d
1 changed files with 161 additions and 0 deletions
161
workplans/CUST-WP-0073-agent-credential-separation.md
Normal file
161
workplans/CUST-WP-0073-agent-credential-separation.md
Normal file
|
|
@ -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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue