key-cape/docs/upstream-issuer-proof.md

116 lines
7.1 KiB
Markdown
Raw Normal View History

# One-shot upstream issuer proof
Executed with explicit user admission on 2026-09-08: the verified signed issuer
is **`https://auth.coulomb.social`**. The Job exited successfully and all temporary
resources were removed. [Live receipt](evidence/2026-09-08-upstream-issuer-live-proof.json).
This completed run did not approve CCR-2026-0017/0018 or change the normal config.
The procedure below is retained for an admitted recheck after relevant changes.
The missing evidence is the actual signed Authelia ID token produced by the
existing confidential `keycape` registration and server-side token exchange.
The old production KeyCape image cannot emit that evidence. A downstream
OpenBao login or public discovery cannot substitute for it.
The diagnostic uses the same configuration and registered callback URI but a
fresh random state, browser cookie and OIDC nonce. A temporary Traefik route
matches only its start path or `/authorize/callback` with that exact state.
Ordinary callbacks continue to use the existing KeyCape service. The probe
verifies the signature against the configured upstream's keys, client audience,
validity window, nonce and a finite reviewed issuer set. Only the resulting
issuer and verification booleans leave the adapter; no subject, token or code
is returned, logged or retained. No downstream application credential is issued.
## Concrete deployment boundary
`scripts/render-upstream-issuer-probe.py` renders five temporary resources in
`sso`: one Job, one Service, one IngressRoute and two NetworkPolicies. The Job
has a ten-minute active deadline, no retry, and a five-minute completed-Job TTL.
Every child must have an owner reference to the **actual created Job UID** so
TTL collection removes the route, service and policies as well as the pod.
The probe projects only `keycape-config/config.yaml`; it does not mount the
signing key or a Kubernetes API token. The existing YAML nevertheless contains
credential data for other adapters. The diagnostic decodes only `authelia` and
never starts those adapters. Admission must explicitly cover this temporary
read of the existing configuration; the local agent never extracts its values.
The Job runs as UID/GID 65534, drops capabilities and has a read-only filesystem.
Its unique pod labels cannot join the production KeyCape Service selector.
The temporary policies permit Traefik-to-probe on TCP 8081 and
probe-to-Authelia on TCP 9091, plus DNS. Existing namespace-wide DNS allowances
remain additive. The narrow Authelia ingress allowance names only the probe's
unique selector. The route reuses `kc-tls`, rate limiting and HSTS; its access
logs and tracing are disabled so callback parameters are not added to telemetry.
The issuer allowlist is the observed public and split-horizon candidate set:
`https://auth.coulomb.social`, `http://auth.coulomb.social`,
`http://authelia.sso.svc.cluster.local:9091`. It does not supply key-fetch URLs:
keys still come from the deployment's configured Authelia/JWKS path. A parsed
issuer merely selects a reviewed candidate; full independent verification must
pass before the issuer is emitted. An issuer outside that set fails without
printing its value. This diagnostic does not change the production verifier or
make these three alternatives permanent accepted production issuers.
## Attended execution and cleanup
Use the immutable diagnostic image in `docs/evidence/upstream-issuer-probe.json`.
The approval-client rollout candidate remains its independently pinned image.
Run from the published KeyCape source checkout. Keep each rendered packet in a
new private directory outside Git. No step below requests a credential value.
1. Recheck the live KeyCape image/Deployment generation, the config Secret's UID
and resourceVersion (metadata only), the existing Ingress/backend and the
Authelia/Traefik selectors. Confirm no other issuer probe is active. Render a
fresh packet with `--image <digest> --output <new-directory>` and run a server
dry-run of its `job.json` and `children.json`. Do not create unbound children.
2. After the deployment owner's admission, create **only** `job.json` with
`kubectl create -f`. Read that Job's UID. Re-render into a second new directory
with the same `--image` and `--state` from `packet.json`, plus `--job-uid`.
Create the second packet's `children.json`. Do not recreate its Job. Record
object UIDs and image/config metadata, without Secret data.
3. Wait for the probe Pod to become Ready; do not open the start URL before the
exact route has converged. Open `packet.json`'s `start_url` in the attended
browser, using the approved browser launcher. Complete the normal upstream
login. The resulting page reports only completion or failure. This proof
does not complete downstream KeyCape MFA or an application login.
4. Immediately collect the Job's bounded output through
`scripts/collect-upstream-issuer-proof.py`, never by printing raw logs:
```bash
kubectl -n sso logs job/<exact-job-name> --tail=4 --limit-bytes=8192 | \
python3 scripts/collect-upstream-issuer-proof.py
```
A successful receipt must also match a successful Job/container exit and the
exact image/config metadata. The collector refuses extra fields, unknown
issuers, missing verification checks and arbitrary log text.
5. Delete only this exact Job and its four owned children; verify all five and
the Pod are absent. On any failure, remove these same temporary resources.
TTL ownership is the unattended fallback, not a reason to skip attended
cleanup. Do not delete a pre-existing object after a name-collision refusal.
No normal Deployment, Service, Ingress or Secret needs a rollback.
6. Return the signed issuer receipt and cleanup metadata to KEY-WP-0013-T02,
RPF-WP-0035-T05 and HFACT-WP-0001-T03. Pin the one verified issuer through the
configuration owner's contained change path. Recheck existing human login
before and after the separately reviewed client/configuration rollout.
The native founder-attended OpenBao lane remains the authority for subsequent
custody writes. This Job neither reads OpenBao data nor creates policy/role/KV
records. Named CCR reviews, verifier/client-side custody, existing-human-login
regression and the compatible KeyCape rollout remain distinct acceptance steps.
## Validation
Complete Go suite with race detection and `go vet ./...` passed. The probe tests
cover genuine public/internal issuer alternatives, forged signatures, wrong
issuer/audience/nonce, expiry, key and token endpoint failures, oversized bodies,
state/cookie binding, replay, idle timeout and output nondisclosure. Five Python
checks cover immutable images, exact callback routes, owner references, projected
files and receipt validation.
A local Traefik **3.6.7** check exercised seven real routing cases: ordinary
callbacks remain on their normal backend; only the exact proof start/callback
reaches the probe. Railiance's API accepted the five resource shapes in a server
dry-run. These are preparation checks, not live signed-token issuer evidence.
The exact state matcher follows [Traefik's routing reference](https://doc.traefik.io/traefik/v3.6/reference/routing-configuration/http/routing/rules-and-priority/).