feat(informed-decision): HTTP -> HTTPS redirect for decisions.coulomb.social
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:
parent
a6a043a318
commit
8d3e4a62b6
2 changed files with 113 additions and 5 deletions
|
|
@ -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 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.
|
||||
|
|
|
|||
62
manifests/informed-decision-http-redirect.yaml
Normal file
62
manifests/informed-decision-http-redirect.yaml
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# HTTP -> HTTPS redirect for decisions.coulomb.social.
|
||||
#
|
||||
# manifests/informed-decision-ingress.yaml is websecure-only (forgejo pattern),
|
||||
# which leaves port 80 unrouted for this host: plain http:// returned Traefik's
|
||||
# default 404 rather than redirecting. This adds the redirect, following the
|
||||
# reuse-surface pattern (reuse/reuse-surface-http-redirect + a redirectScheme
|
||||
# Middleware).
|
||||
#
|
||||
# ROUTER PRIORITY IS LOAD-BEARING. 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 the
|
||||
# same entrypoint competes with it. Traefik would normally resolve that by rule
|
||||
# length -- the solver's Path rule is far longer than PathPrefix(`/`) -- but
|
||||
# reuse-surface reported on 2026-07-07 that relying on implicit precedence is
|
||||
# exactly how a specific rule ends up swallowed by a catch-all, and there the
|
||||
# symptom was a 404 someone noticed. Here the symptom would be a silently failed
|
||||
# renewal roughly 60 days out, i.e. an expired certificate on the origin backing
|
||||
# an OIDC redirect URI.
|
||||
#
|
||||
# So priority is pinned explicitly and low. Traefik gives the highest-priority
|
||||
# matching router the request; any cert-manager solver (which uses the default
|
||||
# computed priority, an order of magnitude larger) outbids this one. Verified by
|
||||
# probe on 2026-09-10 -- see docs/informed-decision-origin.md.
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: informed-decision-redirect-https
|
||||
namespace: informed-decision
|
||||
labels:
|
||||
app.kubernetes.io/name: informed-decision
|
||||
app.kubernetes.io/component: redirect
|
||||
spec:
|
||||
redirectScheme:
|
||||
scheme: https
|
||||
permanent: true
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: informed-decision-http-redirect
|
||||
namespace: informed-decision
|
||||
labels:
|
||||
app.kubernetes.io/name: informed-decision
|
||||
app.kubernetes.io/component: redirect
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: web
|
||||
traefik.ingress.kubernetes.io/router.middlewares: informed-decision-informed-decision-redirect-https@kubernetescrd
|
||||
# Deliberately low: ACME solver routers must win on this entrypoint.
|
||||
traefik.ingress.kubernetes.io/router.priority: "1"
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
rules:
|
||||
- host: decisions.coulomb.social
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: informed-decision
|
||||
port:
|
||||
number: 80
|
||||
Loading…
Add table
Add a link
Reference in a new issue