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>
6.1 KiB
| id | type | title | domain | repo | status | owner | topic_slug | flavor | created | updated | related | origin | origin_ref | ||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| CUST-WP-0073 | workplan | Agents cannot read secret values: separate agent and admin credentials | infotech | the-custodian | proposed | claude-code | custodian | implementation | 2026-09-24 | 2026-09-24 |
|
incident | 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
kubectlandssh railiance01both authenticate assystem:admininsystem:masters, which bypasses authorization. No RBAC rule can restrict it. - k3s runs with
--write-kubeconfig-mode=644, so/etc/rancher/k3s/k3s.yamlis world-readable, and anything running astegwickis 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
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-inviewrole plus the specific write verbs agents need (for example patch and rollout restart on Deployments, ConfigMap updates). Nosecretsverbs at all, sincelistandwatchreturn data too. Nopods/exec,pods/attach,pods/portforwardornodes/proxy. Nohelm, 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/atmSSH principals. Mappingagtto 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
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-modeto600in the k3s install config. Attended admin use becomessudo k3s kubectl. - Workstation:
~/.kube/configpoints 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 -Aandcan-i create pods/exec -Aboth answerno, andkubectl auth whoamishows nosystem:masters. Record the output as evidence.
Reject last-applied annotations on Secrets
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 carryingkubectl.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 applyof a test Secret in a scratch namespace is refused.
Fleet guidance and harness guards
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 --rawand kubeconfig reads, and asks before exec/attach/cp/debug and--raw. Thekubectl * 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 anecho. 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 getandvault readare still pre-approved and print secret values the same way.
Rotate what was exposed
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.