Adopt the agent environment orientation for KeyCape's live objects.

Write Secrets with kubectl replace in the rotation script, since apply copies
the data into the last-applied annotation. Record in operations.md the
live-change rules that apply here and the open finding: four live Secrets
carry that annotation, and removing it waits for the founder's go-ahead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 352750@bnt-lap001
Assistant-Session: de41ef1c-2113-4dd2-9b92-f318ffa7f98b
This commit is contained in:
tegwick 2026-09-23 22:07:23 +02:00
parent 012e695947
commit 51e541c9a1
3 changed files with 50 additions and 4 deletions

View file

@ -3,6 +3,10 @@
Dev Hub (State Hub API): http://127.0.0.1:8000
MCP server name in `~/.claude.json`: `dev-hub`
**Before production, credential or GitOps work** read
`~/the-custodian/docs/agent-environment-orientation.md`; KeyCape specifics are
in `docs/operations.md`, "Before any live change".
**Step 1 — Orient**
Read the offline-safe brief first — it works without a live hub connection:

View file

@ -125,12 +125,42 @@ curl -s -H "Host: auth.coulomb.social" \
| jq -r .issuer
```
## Before any live change
The estate-wide rules are in
`the-custodian/docs/agent-environment-orientation.md` (revision 2026-09-21);
read it first. The parts that apply to KeyCape's objects in `sso` and `mfa`:
- The founder's go-ahead comes first, recorded as
`ADMINISTER @ realm:kubernetes/railiance01`, `activation=APPROVED`. Run changes
as `ssh railiance01 '…'`. A harness block is a stop signal, not something to
route around.
- Check CPU headroom before a restart or rollout
(`kubectl describe node | grep -A4 "Allocated resources"`). A rolling update
starts the new pod before stopping the old one.
- Diff first: client-side `kubectl diff`, then `--dry-run=server`.
- **Never `kubectl apply` a Secret, and never read one's metadata.** `apply`
copies the whole Secret into the `last-applied-configuration` annotation, so
even a metadata read prints the signing key and client secrets. Write Secrets
with `kubectl replace`. To test for the annotation without printing it:
`kubectl get secret <n> -n sso -o go-template='{{ if index .metadata.annotations "kubectl.kubernetes.io/last-applied-configuration" }}HAS-ANNOTATION{{ else }}clean{{ end }}'`.
**Open finding, 2026-09-23:** `sso/keycape-config`, `sso/authelia-secrets`,
`sso/lldap-secrets` and `mfa/privacyidea-config` all carry that annotation. The
earlier rotation script wrote them with `apply`; it now uses `replace`. Anyone
who can `get` these Secrets could read the data anyway, so this is not a new
reader. What it breaks is the assumption that a metadata read is safe. The fix
is a live change and waits for the founder's go-ahead:
`kubectl annotate secret <n> -n <ns> kubectl.kubernetes.io/last-applied-configuration-`
for each of the four. It prints no value and leaves `.data` untouched, so pods
need no restart. Re-check with the template above afterwards.
## Unreleased fail-closed startup changes (read before the next rollout)
The deployed image at the time of writing is
`sha256:7ff54c54e63ee172ae9e6e7fd2da96e427352f712343d74626ee6fe0f6f82611`, built
from `dcebd46`. Two changes on `main` postdate it, both affect startup or
issuance, and **both fail closed**, so they land together on the next rollout and
from `dcebd46`. The changes below on `main` postdate it, all affect startup or
issuance, and **all fail closed**, so they land together on the next rollout and
a mistake in either presents as a refusal rather than as a warning.
**1. Browser clients reject service-identity fields (`74b35b6`, KEY-WP-0028).**
@ -155,7 +185,15 @@ grant today, so nothing changes on rollout; the refusal only becomes reachable
once the approver client is registered. See
[the tenant contract](tenant-claim-contract.md).
Neither has been exercised against a running issuer. The honest proof for both is
**3. KeyCape enforces login freshness itself (`11ce29a`, KEY-WP-0033).**
`prompt=login` is no longer forwarded to Authelia, because 4.38 refuses it for
every real login. `prompt=login` and `max_age=0` go upstream as `max_age=10`,
and the callback refuses any upstream `auth_time` that misses the requirement,
or is absent (`stale_upstream_authentication`). This changes nothing for clients
that do not ask for freshness. The proof is a completed fresh-login journey,
not a redirect check.
None of these has been exercised against a running issuer. The honest proof for both is
a live boot with the new binary, which belongs to the attended rollout window and
not to a session running against production on its own.

View file

@ -283,8 +283,12 @@ sys.stdout.write(updated)
kubectl create configmap authelia-config -n sso --from-file=configuration.yml="$work_dir/authelia-configuration.updated.yml" --dry-run=client -o yaml |
kubectl apply -f - >/dev/null
# replace, not apply: apply copies the full Secret data into the
# last-applied-configuration annotation, where any metadata read prints it.
# replace writes only the manifest, which also drops an annotation left by an
# earlier apply.
kubectl create secret generic keycape-config -n sso --from-file=config.yaml="$work_dir/config.yaml" --from-file=key.pem="$work_dir/key.pem" --dry-run=client -o yaml |
kubectl apply -f - >/dev/null
kubectl replace -f - >/dev/null
kubectl rollout restart deployment/authelia deployment/keycape -n sso >/dev/null
kubectl rollout status deployment/authelia -n sso --timeout=180s