key-cape/docs/upstream-issuer-proof.md
tegwick 41f6916a5d docs(identity): record verified live upstream issuer and completed cleanup
Assistant: codex
Assistant-Model: gpt-5.6-luna
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
2026-09-08 23:50:54 +02:00

7.1 KiB

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. 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:

    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.