diff --git a/docs/evidence/upstream-issuer-probe.json b/docs/evidence/upstream-issuer-probe.json new file mode 100644 index 0000000..88d98b8 --- /dev/null +++ b/docs/evidence/upstream-issuer-probe.json @@ -0,0 +1,63 @@ +{ + "observed_at": "2026-09-08T21:17:50.880337+00:00", + "image": { + "image": "forgejo.coulomb.social/coulomb/key-cape@sha256:0c85ed377cae7ae6ca5b5c56b1a52930e706b3cb78009747e42f551e869a22e4", + "source_commit": "6f33abddcff6cbc348ced862057973cdcc4f78ec", + "published_tag": "main-6f33abd", + "packaged_command_proven": true, + "container_uid": 65534, + "container_network": "none", + "container_readonly": true, + "fixture_lifetime_seconds": 1, + "expected_exit": 1, + "observed_exit": 1, + "idle_receipt": { + "downstream_credential_issued": false, + "failure": "probe_deadline", + "schema": "keycape.upstream-issuer-proof.v1", + "status": "failed", + "tokens_retained": false + } + }, + "validation": { + "go_test_race_all": "passed", + "go_vet_all": "passed", + "probe_tests_after_cli_fixture": "passed", + "python_renderer_and_receipt_tests": 5, + "traefik_route": { + "traefik": "3.6.7", + "exact_route_cases_passed": 7, + "ordinary_login_callbacks_preserved": true, + "live_cluster_changed": false + }, + "server_dry_run": { + "accepted": true, + "resources": [ + "Job", + "Service", + "IngressRoute", + "NetworkPolicy/probe", + "NetworkPolicy/Authelia" + ], + "image_digest": "sha256:0c85ed377cae7ae6ca5b5c56b1a52930e706b3cb78009747e42f551e869a22e4", + "job_owner_uid": "fixture only; must bind the actual UID at execution" + } + }, + "deployment_metadata": { + "deployment": { + "name": "keycape", + "metadata": "99ddd83c-cb3f-4847-bcf8-35f1aa87627f 55113259 29" + }, + "secret": { + "name": "keycape-config", + "metadata": "2e94519d-1550-41c7-9701-2efe47fe1fd3 51346058" + }, + "production_image": "forgejo.coulomb.social/coulomb/key-cape:main-153258b", + "probe_jobs_before": [] + }, + "live_upstream_issuer_proved": false, + "production_mutated": false, + "credential_values_extracted": false, + "custody_activated": false, + "required_next_return": "Deployment-owner admission for the temporary config-reading probe; attended signed issuer receipt and cleanup, then contained issuer pin and named CCR reviews" +} diff --git a/docs/upstream-issuer-proof.md b/docs/upstream-issuer-proof.md new file mode 100644 index 0000000..e5a8c47 --- /dev/null +++ b/docs/upstream-issuer-proof.md @@ -0,0 +1,113 @@ +# One-shot upstream issuer proof + +Prepared for KEY-WP-0013-T02/T07 on 2026-09-08. This packet needs +NetKingdom/platform deployment-owner admission before its temporary workload +uses the existing KeyCape configuration. It does not approve CCR-2026-0017/0018. + +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 --output ` 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/ --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/). diff --git a/workplans/KEY-WP-0013-approval-engine-resource-audience.md b/workplans/KEY-WP-0013-approval-engine-resource-audience.md index bcc271d..65c572e 100644 --- a/workplans/KEY-WP-0013-approval-engine-resource-audience.md +++ b/workplans/KEY-WP-0013-approval-engine-resource-audience.md @@ -219,7 +219,7 @@ T06 completion: full Go suite and vet passed; published code `dcebd46` and pulle ```task id: KEY-WP-0013-T07 -status: progress +status: done priority: high assignee: the-custodian ``` @@ -243,3 +243,12 @@ Source tests, image and deployment packet close this preparation task. The attended live receipt and issuer pin remain in T02, alongside the named CCR reviews and custody/rollout acceptance. Preparing the diagnostic does not approve CCR-2026-0017/0018 or close any live factory gate. + +T07 preparation complete: source `6f33abd`, immutable image +`sha256:0c85ed377cae7ae6ca5b5c56b1a52930e706b3cb78009747e42f551e869a22e4`. +Complete Go race suite and vet passed; five renderer/collector checks, seven +real Traefik 3.6.7 route cases and Railiance server dry-run passed. The packaged +command ran as UID 65534 with no network and a read-only filesystem and produced +the expected bounded idle-timeout receipt. See `docs/upstream-issuer-proof.md` +and `docs/evidence/upstream-issuer-probe.json`. T02 retains the live proof and +configuration-owner admission; no production resource or custody value changed.