railiance-apps/docs/informed-decision-origin.md
tegwick 0da83472c4
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 5s
Default railiance01 kubeconfig to config-railiance01
config-hosteurope names port 16443, but the k3s-api-railiance01
ops-bridge tunnel listens on 16444, so every default target failed
with connection refused. Switch Makefile, tools and docs to
~/.kube/config-railiance01 and fix the restore recipe to rewrite the
API address to the tunnel port. The node-side hostPath in STATE.md is
unrelated and unchanged.

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1031639@bnt-lap001
Assistant-Session: e5073fee-f4ae-4fc3-a75a-6c1fc83e0576
2026-10-17 11:46:40 +02:00

9.2 KiB
Raw Blame History

Current origin ownership — 2026-10-04

The founder approved INFD-WP-0007 lifecycle cutover. Runtime Deployment, Service, PVC, config and workload routes are owned by informed-decision deploy/lifecycle through a pinned manual ArgoCD application. This repository retains Namespace, Ingress, HTTPS redirect and certificate. The obsolete placeholder deployment definition is removed so reapplying the origin cannot revert the service.

Use railiance01 (92.205.62.239), not the retired config-hosteurope tunnel. Runtime recovery follows informed-decision/deploy/lifecycle/README.md; never reintroduce the placeholder or downgrade a schema-5 database with an older image. Existing Ingress remains one Prefix route to informed-decision:80.

The record below is founding origin history, not the current recovery procedure.

decisions.coulomb.social — origin for the informed-decision approver surface

Status: applied and live. https://decisions.coulomb.social/ answers 200 over a valid Let's Encrypt certificate as of 2026-09-10 14:32 UTC. Last reviewed: 2026-09-10

Why this host exists before the app does

informed-decision owns the browser-facing approver UI that approval-engine deliberately does not contain. Its workplan task INFD-WP-0001-T07 must publish two strings to key-cape — an OIDC client_id and a full callback URI — and cannot do so against a host that does not answer.

Correction, 2026-09-11. An earlier version of this document said the origin unblocks key-cape's KEY-WP-0013-T02, "blocked since 2026-09-08". That is wrong twice over, and the error came from repeating informed-decision's workplan prose of 2026-09-09 without checking the source repo. KEY-WP-0013-T02 is done; so is T05, the task that actually carried the human registration. key-cape published the contract shape from its side — including the approval:read scope informed-decision asked for and a fix to the assurance object's at field — and closed a follow-up (KEY-WP-0030) that informed-decision prompted. Nothing in key-cape is waiting on this host. What remains is informed-decision's own T07, whose acceptance requires the real origin regardless.

Both strings are now fixed except for the host:

Field Value
client_id informed-decision-approver
Redirect URI https://decisions.coulomb.social/auth/callback
Flow authorization code + S256 PKCE, public client
Scopes openid, approval:read, approval:approve

Redirect URIs match byte-exactly at /authorize. A registration pointing at a host that does not answer fails closed at first login and presents as a rejected approval rather than as a registration defect — which is exactly the failure approval-engine avoided by refusing to invent these strings. So the origin has to be real before the registration is submitted, and that is S5 work here rather than in informed-decision.

The approver UI itself is INFD-WP-0001-T08, still gated on approval-engine APPROVAL-WP-0002-T01 and on intake INFD-IN-0003 (the evidence copy must reach audit-core independently of this component). This host therefore ships a placeholder first and the real surface later.

What is in the repo

File Contents
manifests/informed-decision-origin.yaml Namespace, placeholder nginx ConfigMap/Deployment/Service on informed-decision:80
manifests/informed-decision-ingress.yaml Traefik Ingress + letsencrypt-prod certificate for decisions.coulomb.social
manifests/informed-decision-http-redirect.yaml redirectScheme Middleware + web-entrypoint Ingress, HTTP → HTTPS

The placeholder is nginxinc/nginx-unprivileged, read-only root filesystem, non-root, noindex. When the real surface lands it takes over the same Service name and the origin file shrinks to the Namespace.

Deliberate single path rule

The Ingress carries exactly one rule: / Prefix to one backend.

reuse-surface reported on 2026-07-07 that an Ingress declaring an Exact /health rule alongside a catch-all / Prefix rule on the same host had the exact match swallowed by the catch-all — the public health URL returned 404 from the landing container while the pod was 1/1 Ready and its own probes passed. Splitting /auth, /api and / across backends here would reproduce that on a host whose whole purpose is an exact-match redirect URI. If a second backend ever becomes necessary, set traefik.ingress.kubernetes.io/router.priority explicitly rather than relying on rule order.

Preconditions verified 2026-09-10

  • DNS decisions.coulomb.social → 92.205.62.239 (same A record as reuse).
  • letsencrypt-prod ClusterIssuer Ready=True.
  • No existing Ingress claims decisions.coulomb.social.
  • Server-side dry-run of all five objects against the live API is clean. The namespaced four were validated against an existing namespace, since a server dry-run cannot create the new one first; see DRY_RUN_CREATE_NAMESPACES in tools/k8s-server-dry-run.sh.

Deployed 2026-09-10

Applied with operator approval. Evidence:

Check Result
Placeholder pod 1/1 Running, /healthz → ok in-pod
Certificate informed-decision-tls Ready=True, ACME order valid
Issuer / subject CN=decisions.coulomb.social, Let's Encrypt YR2
Validity 2026-09-10 → 2026-12-09 (cert-manager renews)
GET https://decisions.coulomb.social/ HTTP/2 200, chain verify 0
GET https://decisions.coulomb.social/auth/callback 200 — the exact redirect URI resolves
GET http://decisions.coulomb.social/ 301/308 → HTTPS (added 2026-09-10)

/auth/callback currently returns the placeholder page via the SPA try_files fallback. That is the correct behaviour for now: the origin answers, which is what the registration needs. The real surface will handle the path when INFD-WP-0001-T08 ships.

informed-decision can now submit docs/keycape-client-registration.md to key-cape. That closes its own INFD-WP-0001-T07, not a key-cape task — see the correction above.

Redeploy / recovery

export KUBECONFIG=$HOME/.kube/config-railiance01
kubectl apply -f manifests/informed-decision-origin.yaml
kubectl apply -f manifests/informed-decision-ingress.yaml
kubectl apply -f manifests/informed-decision-http-redirect.yaml
kubectl -n informed-decision get pods,svc,ingress
kubectl -n informed-decision get certificate informed-decision-tls -w
curl -sSI https://decisions.coulomb.social/ | head -1

That curl succeeded on 2026-09-10, which is the gate informed-decision was waiting on.

Note on the kubeconfig: use ~/.kube/config-railiance01, which targets the k3s-api-railiance01 ops-bridge tunnel on 16444. The retired ~/.kube/config-hosteurope names port 16443; bridge check reports the tunnel healthy either way, so it does not surface that mismatch.

HTTP → HTTPS

manifests/informed-decision-http-redirect.yaml adds a redirectScheme Middleware plus a web-entrypoint Ingress, following the reuse-surface pattern. Applied 2026-09-10.

http://decisions.coulomb.social/ now returns 308 to HEAD and 301 to GET. That split is not a defect in this configuration — reuse.coulomb.social, which uses the identical Middleware, behaves the same way. It is Traefik's own behaviour on this cluster.

The main Ingress stays websecure-only; this is a second router on port 80 rather than a change to the first.

Router priority is load-bearing here

cert-manager solves HTTP-01 by creating a temporary solver Ingress on this same host and this same web entrypoint, serving /.well-known/acme-challenge/<token>. A catch-all / redirect on that entrypoint competes with it directly.

Traefik would normally settle this by rule length — the solver's Path rule is much longer than PathPrefix(/) — but reuse-surface's 2026-07-07 report is precisely a case of a specific rule losing to a catch-all when precedence was left implicit. There the symptom was a 404 someone noticed within the day. Here it would be a silently failed renewal about 60 days out, surfacing as an expired certificate on the origin backing an OIDC redirect URI — the kind of failure that presents as a broken login rather than as an expired cert.

So the redirect router pins traefik.ingress.kubernetes.io/router.priority: "1". Any solver router, using the default computed priority, outbids it.

Verified by probe, twice, on 2026-09-10. A throwaway Ingress mimicking the solver shape (web entrypoint, Exact acme-challenge path, default priority) was applied and the challenge path was polled:

Condition /.well-known/acme-challenge/<token> /
No solver present 301 (redirect catches it — correct) 301
Solver present 200 (solver wins) 301
Solver removed 301 301

Traefik takes 30–60s to converge on Ingress changes, so the flip is not instant; poll rather than reading a single response. The probe was deleted after each run and is not in the repo — recreate it from this table if the redirect or the priority annotation is ever changed.

The next real renewal is due around 2026-11-09. Confirm the certificate's notAfter advances then; that is the only test that exercises the real solver rather than a probe of its shape.