# 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 close `KEY-WP-0013-T02`, which has been blocked on them since 2026-09-08. 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` is unblocked to submit `docs/keycape-client-registration.md` to `key-cape` and close `KEY-WP-0013-T02`.** ## Redeploy / recovery ```bash export KUBECONFIG=$HOME/.kube/config-hosteurope 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:** `~/.kube/config-hosteurope` names port `16443`, but the `k3s-api-railiance01` ops-bridge tunnel currently listens on `16444`. Override with `--server https://127.0.0.1:16444` or fix the kubeconfig; `bridge check` reports the tunnel healthy either way, so it does not surface the 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/`. 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/` | `/` | | --- | --- | --- | | 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.