feat(informed-decision): HTTP -> HTTPS redirect for decisions.coulomb.social
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

The main Ingress is websecure-only per the forgejo pattern, so port 80 was
unrouted for this host and plain http:// returned Traefik's default 404. Adds a
redirectScheme Middleware plus a web-entrypoint Ingress, following the
reuse-surface pattern.

Pins router.priority: "1" on the redirect. cert-manager solves HTTP-01 by
creating a solver Ingress on this same host and entrypoint, so a catch-all "/"
redirect competes with it directly. Traefik would normally settle that by rule
length, but reuse-surface's 2026-07-07 report is exactly a case of a specific
rule losing to a catch-all when precedence was left implicit -- and here the
symptom would not be a visible 404 but a silently failed renewal ~60 days out,
surfacing as an expired certificate on the origin backing an OIDC redirect URI.

Verified by probe twice: with a solver-shaped Ingress present the challenge path
returns 200 (solver wins) while "/" still redirects; with it absent the redirect
correctly catches both. Probe was throwaway and is not committed; the runbook
carries the table to recreate it.

GET returns 301 and HEAD 308. That split is Traefik's own behaviour on this
cluster, not a defect here -- reuse.coulomb.social does the same.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJbh7o7UWF4tQ5jxygnNGu

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2072522@bnt-lap001
Assistant-Session: 46173adf-7302-4ede-99d6-963b61359928
This commit is contained in:
tegwick 2026-09-10 16:57:28 +02:00
parent a6a043a318
commit 8d3e4a62b6
2 changed files with 113 additions and 5 deletions

View file

@ -38,6 +38,7 @@ placeholder first and the real surface later.
| --- | --- |
| `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
@ -78,6 +79,7 @@ Applied with operator approval. Evidence:
| 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
@ -93,6 +95,7 @@ to `key-cape` and close `KEY-WP-0013-T02`.**
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
@ -109,8 +112,51 @@ mismatch.
## HTTP → HTTPS
The Ingress is `websecure`-only, following the `forgejo` and `coulomb-social`
pattern, so port 80 stays free for cert-manager HTTP-01 solvers. Plain
`http://decisions.coulomb.social/` will not redirect. `reuse-surface` adds a
separate `-http-redirect` Ingress for this; add one here if a bare-host redirect
is wanted. It is not required for the OIDC flow, which is always `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 3060s 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.