v0.312.0: every public name answers only over https; HSTS one day (R-1033, P1)
gates / gates (push) Successful in 48s
gates / gates (push) Successful in 48s
forwarded.yml: felhom-forwarded is now a chain [felhom-https-only (redirectScheme https, 301 when the trusted tunnel says X-Forwarded-Proto: http), felhom-forwarded-headers (the clean-up + HSTS 86400, no subdomains, no preload)] plus a priority-1 catch-all router for unknown names over http. traefik.yml unchanged; hot-loaded. Tests red-proofed. Evidence: felhom.eu documentation/audits/https-only-2026-10-14/.
This commit is contained in:
@@ -1,3 +1,24 @@
|
||||
## v0.312.0 — every public name answers only over https; HSTS one day (R-1033, P1) (2026-10-14)
|
||||
|
||||
**MinAgent: 0.155.0** (unchanged — traefik's dynamic file only). **Hub:** none. **Catalog:** none. Design: felhom.eu
|
||||
`09` §3 decision 244 (the operator made R-1033 P1) and the decision taken unattended that night (traefik, not
|
||||
Cloudflare: the box keys lack „Zone Settings"); evidence felhom.eu `documentation/audits/https-only-2026-10-14/`.
|
||||
|
||||
- **`dynamic/forwarded.yml` (`infra.RenderForwardedHeaders`):** `felhom-forwarded`, the middleware traefik's websecure
|
||||
entrypoint already names, is now a CHAIN — `felhom-https-only` (redirectScheme https, permanent) then
|
||||
`felhom-forwarded-headers` (the unchanged header clean-up + `Strict-Transport-Security: max-age=86400`, no subdomains,
|
||||
no preload). A visitor who typed `http://` reaches Cloudflare over plain http; the tunnel delivers to :443 and says
|
||||
so in `X-Forwarded-Proto: http`, which traefik keeps only from the trusted tunnel address — so the visitor gets a 301
|
||||
to the same https URL (path and query kept) BEFORE any app sees the request. An https visitor and a LAN visitor are
|
||||
unchanged; a forged `X-Forwarded-Proto` from any other peer is ignored. Measured with traefik v3.7.13 on the bench.
|
||||
- **`felhom-https-only-catchall`** (router, priority 1, `Header(X-Forwarded-Proto, http)`, `noop@internal`): a name no
|
||||
app route matches is redirected over http too, while the same unknown name over https stays traefik's 404.
|
||||
- traefik.yml is UNCHANGED (the entrypoint still names `felhom-forwarded@file`): the dynamic file is hot-loaded, no
|
||||
traefik restart. The existing bring-up rewrites a drifted file on the next health tick (`ensureForwardedHeaders`).
|
||||
- Tests: `TestRenderForwardedHeaders_HTTPSOnly` (red-proofed: the redirect removed from the chain → fails; HSTS a year →
|
||||
fails), `TestEnsureForwardedHeaders_R1033_RewritesAnOldFile` (red-proofed: rewrite only when missing → fails);
|
||||
`TestRenderForwardedHeaders_RemovesClientWritableHeaders` reads the headers through the chain.
|
||||
|
||||
## v0.311.0 — after a power cut, an app repairs the files the cut left empty; the household sees lost photos (R-1007) (2026-10-14)
|
||||
|
||||
**MinAgent: 0.155.0** (unchanged — the crash fact is the existing `GET /host/crash-guard`). **Hub:** v0.150.0 registers
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
|
||||
Last updated: 2026-10-04 (v0.293.0 — the Docker-socket self-heal, R-860)
|
||||
|
||||
> **2026-10-14 (night) — v0.312.0, https only (R-1033, P1; `09` §3 decision 244).** DECIDED BY CC UNATTENDED — operator
|
||||
> may reverse: the redirect lives in traefik on the box (`felhom-https-only` in the `felhom-forwarded` chain), not in
|
||||
> Cloudflare, because every box's `cf_api_token` lacks „Zone Settings" (403, measured on all three zones). Cloudflare's
|
||||
> „Always Use HTTPS" stays the operator's defence-in-depth option once the keys carry the permission. HSTS one day.
|
||||
|
||||
> **2026-10-08 (day) — UNRELEASED on main, ships with tomorrow's release (no release today: kernel night 8→9, `09` §3
|
||||
> decision 178).** R-899 (decision 177): a daytime press never cancels the night's whole-guest backup —
|
||||
> `internal/quiesce/nightowed.go` ledger, owed tiers never fire the valve; a press reaches the agent as `trigger=manual`.
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
# REPORT — v0.311.0: after a power cut, an app repairs the files the cut left empty (R-1007, 2026-10-14)
|
||||
# REPORT — v0.312.0: every public name answers only over https (R-1033, P1, 2026-10-14 night)
|
||||
|
||||
Baseline `acbbf4b` (0.310.0). Code `8d862e4`, security fix `3b85a0c` (before the build). Image
|
||||
`felhom-controller:0.311.0` (sha256 `2a5550da…`). Design: felhom.eu `09` §3 decision 236; evidence
|
||||
felhom.eu `documentation/audits/immich-repair-2026-10-14/`.
|
||||
Baseline `9b95e8e` (0.311.0). Design: felhom.eu `09` §3 decision 244 + the unattended decision recorded there (traefik on
|
||||
the box, because the box keys cannot change Cloudflare zone settings). Evidence: felhom.eu
|
||||
`documentation/audits/https-only-2026-10-14/`.
|
||||
|
||||
- New template block `after_unclean_boot`; `internal/aftercrash` runs it once per start after an UNCLEAN host boot
|
||||
(crashboot's verdict, after the 15-min crash grace). The household's lost-files note on the app page; the operator
|
||||
event `app_repaired_after_crash` (counts only). MinAgent 0.155.0 unchanged.
|
||||
- Tests: 15 guards red-proofed (each removed in turn convicted its test). Full suite and every controller gate green.
|
||||
- **Security review of `8d862e4`:** compose's shared runner logs a failed command's stdout and puts stderr in the error,
|
||||
which reached the event's `problem`. Fixed: a quiet runner, exit code only (`TestAUB_AFailingCommandLeaksNoOutput`).
|
||||
- **Live (demo-hp 9201, a real power cut mid-upload):** 41 of 41 broken photos repaired in 10 s; 120 controls untouched.
|
||||
- Open: R-1020 — the returned duration is 0 (the record's is right). Delivery to the N100 and Tester 1 waits on golden
|
||||
0.311.0 + the global floor.
|
||||
- `forwarded.yml`: `felhom-forwarded` = chain [`felhom-https-only` (redirectScheme https, 301), `felhom-forwarded-headers`
|
||||
(clean-up + HSTS 86400)]; catch-all router for unknown names over http. traefik.yml unchanged, no traefik restart.
|
||||
- Tests red-proofed (three mutations, each seen failing). Green gate: build, vet, test all OK.
|
||||
- Delivered by hand to Tester 1, demo-hp 9201 and demo-felhom 9201 for the proof (Tier 0). **Not vouched, floor not
|
||||
moved** — the operator's morning step.
|
||||
|
||||
@@ -1005,7 +1005,7 @@ The controller stands up its own base stack — **traefik** (reverse proxy), **c
|
||||
|
||||
cloudflared is only deployed when a tunnel token is configured. **Triggers**: a first-boot goroutine (after stack init) and an unconditional call on every `system-health` tick (self-heal — cheap when healthy thanks to the idempotency). `monitor.EffectiveProtected` mirrors the cloudflared condition so a LAN-only node (no tunnel token) doesn't report a perpetual "protected container not running" FAIL.
|
||||
|
||||
**The box tells visitors apart (v0.286.0, R-753, `09` §3 decision 63).** cloudflared sits ALONE on the `felhom-tunnel` network (`172.16.253.0/29`, docker's own allocation confined to `.4/30`) at the fixed address `172.16.253.2`; traefik joins it at `.3`. traefik's `websecure` entrypoint trusts forwarded headers from `172.16.253.2/32` only, so an app behind the tunnel receives `X-Forwarded-For: <client-written…>, <real visitor>, 172.16.253.2` and a LAN visitor's own address; every other peer's chain is dropped as before. Every websecure request passes the entrypoint middleware `felhom-forwarded@file` (`dynamic/forwarded.yml`, `RenderForwardedHeaders`), which removes the headers a client could write a host, path or address into (`X-Forwarded-Host/-Uri/-Method/-Prefix`, `Forwarded`, `True-Client-Ip`, `X-Client-Ip`, …) and fixes `X-Forwarded-Port: 443` — measured: Cloudflare passes a client's `X-Forwarded-Host`/`-Port` and appends to a client's `X-Forwarded-For`, and strips a client's `X-Real-IP`. **Readers take the visitor from the RIGHT, never the leftmost.** The controller's own rule is `internal/web/clientaddr.go` (`clientIP`): believed only when the TCP peer is traefik (docker DNS); the rightmost entry is the hop traefik saw; that hop being the tunnel address → `CF-Connecting-IP` (Cloudflare's edge refuses a client-sent one). Catalog apps that read the LEFTMOST entry carry a router middleware that removes the chain (`<router>-xff`). If the network cannot be made, traefik keeps trusting nobody and cloudflared stays on `traefik-public`. Design + measurements: `felhom.eu/documentation/audits/visitors-2026-10-01/A/`.
|
||||
**The box tells visitors apart (v0.286.0, R-753, `09` §3 decision 63).** cloudflared sits ALONE on the `felhom-tunnel` network (`172.16.253.0/29`, docker's own allocation confined to `.4/30`) at the fixed address `172.16.253.2`; traefik joins it at `.3`. traefik's `websecure` entrypoint trusts forwarded headers from `172.16.253.2/32` only, so an app behind the tunnel receives `X-Forwarded-For: <client-written…>, <real visitor>, 172.16.253.2` and a LAN visitor's own address; every other peer's chain is dropped as before. Every websecure request passes the entrypoint middleware `felhom-forwarded@file` (`dynamic/forwarded.yml`, `RenderForwardedHeaders`), which removes the headers a client could write a host, path or address into (`X-Forwarded-Host/-Uri/-Method/-Prefix`, `Forwarded`, `True-Client-Ip`, `X-Client-Ip`, …) and fixes `X-Forwarded-Port: 443` — measured: Cloudflare passes a client's `X-Forwarded-Host`/`-Port` and appends to a client's `X-Forwarded-For`, and strips a client's `X-Real-IP`. **Readers take the visitor from the RIGHT, never the leftmost.** The controller's own rule is `internal/web/clientaddr.go` (`clientIP`): believed only when the TCP peer is traefik (docker DNS); the rightmost entry is the hop traefik saw; that hop being the tunnel address → `CF-Connecting-IP` (Cloudflare's edge refuses a client-sent one). Catalog apps that read the LEFTMOST entry carry a router middleware that removes the chain (`<router>-xff`). **https only (v0.312.0, R-1033, P1):** `felhom-forwarded` is a chain — `felhom-https-only` (301 to the same https URL when the trusted tunnel says `X-Forwarded-Proto: http`) then `felhom-forwarded-headers` (the clean-up + HSTS one day, no subdomains, no preload); the router `felhom-https-only-catchall` (priority 1, only `X-Forwarded-Proto: http`) redirects names no app route matches. An https or LAN visitor is unchanged. If the network cannot be made, traefik keeps trusting nobody and cloudflared stays on `traefik-public`. Design + measurements: `felhom.eu/documentation/audits/visitors-2026-10-01/A/`.
|
||||
|
||||
**Backend transports — self-signed HTTPS backends (`ensureServersTransports` → `RenderServersTransports`, v0.83.0).** Traefik talks **HTTP** to app backends by default, which is correct for every catalog app that serves plain HTTP. The exception is an app that serves its **own self-signed TLS** on the internal docker bridge (the first is Crafty, HTTPS-only on `:8443`): Traefik must speak `https` to it *and* skip verifying a per-container self-signed cert (no CA to verify against; the hop never leaves the host). `insecureSkipVerify` is **not settable via Docker labels** in traefik v3 — it must live in static/file config — so `EnsureBaseStack` writes a file-provider dynamic file `dynamic/serverstransports.yml` defining a **named** transport `insecure-skip-verify` (write-if-changed; hot-loaded by the file watcher). An app **opts in per-service** via two catalog labels — `loadbalancer.server.scheme=https` + `loadbalancer.serverstransport=insecure-skip-verify@file` (the `@file` suffix is the cross-provider reference). Backend verification stays the default (ON) for every other service — there is deliberately **no global `insecureSkipVerify`** in `traefik.yml`. This write runs **outside** `ensureTraefik` (which early-returns when traefik is already up) so an established node still materializes the file on a self-heal tick.
|
||||
|
||||
|
||||
@@ -88,6 +88,16 @@ const (
|
||||
TunnelIPRange = "172.16.253.4/30" // where docker may put anything else: never .2 or .3
|
||||
// ForwardedMiddleware is the entrypoint middleware every websecure request passes (RenderForwardedHeaders).
|
||||
ForwardedMiddleware = "felhom-forwarded"
|
||||
// R-1033 (P1, `09` §3 decision 244): the parts of the ForwardedMiddleware CHAIN. HTTPSOnlyMiddleware sends a visitor
|
||||
// who reached Cloudflare over plain http (the tunnel's trusted X-Forwarded-Proto: http) to the same https URL;
|
||||
// ForwardedHeadersMiddleware is the header clean-up plus HSTS. HTTPSOnlyCatchAllRouter does the same for a name no
|
||||
// app route matches (an entrypoint middleware runs only on a matched router).
|
||||
ForwardedHeadersMiddleware = "felhom-forwarded-headers"
|
||||
HTTPSOnlyMiddleware = "felhom-https-only"
|
||||
HTTPSOnlyCatchAllRouter = "felhom-https-only-catchall"
|
||||
// HSTSMaxAgeSeconds is ONE DAY on purpose (decision 244 brief): no includeSubDomains, no preload — a longer value is
|
||||
// the operator's decision (register row), because a browser keeps it even if the box is ever served without https.
|
||||
HSTSMaxAgeSeconds = 86400
|
||||
)
|
||||
|
||||
// TraefikData is the per-customer input for the traefik stack. ACMEEmail empty → no Let's Encrypt
|
||||
@@ -309,23 +319,49 @@ http:
|
||||
}
|
||||
|
||||
// RenderForwardedHeaders returns the dynamic file defining the entrypoint middleware every websecure request passes
|
||||
// (traefik.yml names it). Once traefik trusts the tunnel's address it KEEPS the forwarded headers that hop carries,
|
||||
// and Cloudflare passes a client's own X-Forwarded-Host and X-Forwarded-Port through unchanged (measured,
|
||||
// audits/visitors-2026-10-01/A/M2) — so this removes every header in which a client could write a host, a path or an
|
||||
// address, and fixes the port: both paths reach traefik on 443. X-Forwarded-For stays (traefik appends the hop it saw;
|
||||
// readers take it from the RIGHT), X-Real-Ip stays (traefik's peer — Cloudflare strips a client's, measured M2),
|
||||
// CF-Connecting-IP stays (the controller believes it only when the hop is the tunnel). A request's Host header still
|
||||
// says which app it is for, so an app that falls back from X-Forwarded-Host to Host gets the same name.
|
||||
// Static — no per-customer input. Pinned by TestRenderForwardedHeaders_RemovesClientWritableHeaders.
|
||||
// (traefik.yml names it). Since R-1033 (P1, controller v0.312.0) ForwardedMiddleware is a CHAIN of two:
|
||||
//
|
||||
// 1. HTTPSOnlyMiddleware — every public name answers only over https. A visitor who typed http:// reaches Cloudflare
|
||||
// over plain http; the tunnel still delivers to traefik's :443 and says so in X-Forwarded-Proto: http, which traefik
|
||||
// keeps because the hop is the trusted tunnel address (any other peer's X-Forwarded-Proto is replaced by traefik's
|
||||
// own — measured on the bench, audits/https-only-2026-10-14/1A-6). redirectScheme honours that header: 301 to the
|
||||
// same https URL, path and query kept; an https visitor (X-Forwarded-Proto: https) and a LAN visitor are unchanged.
|
||||
// The redirect happens before any app sees the request, so a login page is never served — and a password never
|
||||
// sent — over http. The Cloudflare-side switch („Always Use HTTPS") needs a zone permission the box keys lack.
|
||||
// 2. ForwardedHeadersMiddleware — the clean-up: once traefik trusts the tunnel's address it KEEPS the forwarded headers
|
||||
// that hop carries, and Cloudflare passes a client's own X-Forwarded-Host and X-Forwarded-Port through unchanged
|
||||
// (measured, audits/visitors-2026-10-01/A/M2) — so this removes every header in which a client could write a host, a
|
||||
// path or an address, and fixes the port: both paths reach traefik on 443. X-Forwarded-For stays (traefik appends the
|
||||
// hop it saw; readers take it from the RIGHT), X-Real-Ip stays (traefik's peer — Cloudflare strips a client's,
|
||||
// measured M2), CF-Connecting-IP stays (the controller believes it only when the hop is the tunnel). A request's
|
||||
// Host header still says which app it is for. It also sets HSTS for HSTSMaxAgeSeconds, no subdomains, no preload.
|
||||
//
|
||||
// HTTPSOnlyCatchAllRouter (priority 1, only X-Forwarded-Proto: http) redirects a name no app route matches, so an
|
||||
// unknown name over http is a redirect too while an unknown name over https stays traefik's 404.
|
||||
// Static — no per-customer input. Pinned by TestRenderForwardedHeaders_RemovesClientWritableHeaders and
|
||||
// TestRenderForwardedHeaders_HTTPSOnly (both read the chain, so removing either half fails a test).
|
||||
func RenderForwardedHeaders() string {
|
||||
return `# Traefik dynamic config — the forwarded-header clean-up every websecure request passes. Managed by felhom-controller.
|
||||
# WARNING: auto-generated at base-infra bring-up. Manual edits are overwritten. traefik.yml names this middleware on its
|
||||
return `# Traefik dynamic config — the chain every websecure request passes: https only (R-1033), then the forwarded-header
|
||||
# clean-up + HSTS. Managed by felhom-controller.
|
||||
# WARNING: auto-generated at base-infra bring-up. Manual edits are overwritten. traefik.yml names ` + ForwardedMiddleware + ` on its
|
||||
# websecure entrypoint, so a missing file would break every route: it is written before traefik.yml.
|
||||
# An empty value REMOVES the header (traefik headers middleware).
|
||||
http:
|
||||
middlewares:
|
||||
` + ForwardedMiddleware + `:
|
||||
chain:
|
||||
middlewares:
|
||||
- ` + HTTPSOnlyMiddleware + `
|
||||
- ` + ForwardedHeadersMiddleware + `
|
||||
` + HTTPSOnlyMiddleware + `:
|
||||
redirectScheme:
|
||||
scheme: https
|
||||
permanent: true
|
||||
` + ForwardedHeadersMiddleware + `:
|
||||
headers:
|
||||
stsSeconds: ` + fmt.Sprint(HSTSMaxAgeSeconds) + `
|
||||
stsIncludeSubdomains: false
|
||||
stsPreload: false
|
||||
customRequestHeaders:
|
||||
X-Forwarded-Port: "443"
|
||||
X-Forwarded-Host: ""
|
||||
@@ -340,6 +376,14 @@ http:
|
||||
X-Cluster-Client-Ip: ""
|
||||
Client-Ip: ""
|
||||
X-Original-Forwarded-For: ""
|
||||
routers:
|
||||
` + HTTPSOnlyCatchAllRouter + `:
|
||||
rule: "Header(` + "`X-Forwarded-Proto`, `http`" + `)"
|
||||
priority: 1
|
||||
entryPoints:
|
||||
- websecure
|
||||
service: noop@internal
|
||||
tls: {}
|
||||
`
|
||||
}
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ func TestRenderForwardedHeaders_RemovesClientWritableHeaders(t *testing.T) {
|
||||
if err := yaml.Unmarshal([]byte(RenderForwardedHeaders()), &doc); err != nil {
|
||||
t.Fatalf("not YAML: %v", err)
|
||||
}
|
||||
h := doc["http"].(map[string]any)["middlewares"].(map[string]any)[ForwardedMiddleware].(map[string]any)["headers"].(map[string]any)["customRequestHeaders"].(map[string]any)
|
||||
h := doc["http"].(map[string]any)["middlewares"].(map[string]any)[ForwardedHeadersMiddleware].(map[string]any)["headers"].(map[string]any)["customRequestHeaders"].(map[string]any)
|
||||
for _, name := range []string{"X-Forwarded-Host", "X-Forwarded-Uri", "X-Forwarded-Method", "X-Forwarded-Prefix",
|
||||
"X-Forwarded-Tls-Client-Cert", "X-Forwarded-Tls-Client-Cert-Info", "Forwarded", "True-Client-Ip", "X-Client-Ip"} {
|
||||
if v, ok := h[name]; !ok || v != "" {
|
||||
@@ -135,3 +135,46 @@ func TestRenderForwardedHeaders_RemovesClientWritableHeaders(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// R-1033 (P1): every websecure request passes the https-only redirect FIRST, then the clean-up + HSTS — read through the
|
||||
// chain the entrypoint names, so a chain that drops either half (or names a middleware that is not defined) fails here.
|
||||
// The CONSEQUENCE (an http visitor gets 301 to the same https URL, an https or LAN visitor is untouched, a forged header
|
||||
// from an untrusted peer is ignored) was measured with traefik v3.7.13 on the bench: audits/https-only-2026-10-14/1A-6.
|
||||
func TestRenderForwardedHeaders_HTTPSOnly(t *testing.T) {
|
||||
var doc map[string]any
|
||||
if err := yaml.Unmarshal([]byte(RenderForwardedHeaders()), &doc); err != nil {
|
||||
t.Fatalf("not YAML: %v", err)
|
||||
}
|
||||
mws := doc["http"].(map[string]any)["middlewares"].(map[string]any)
|
||||
chain, ok := mws[ForwardedMiddleware].(map[string]any)["chain"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("%s must be a chain, got %v", ForwardedMiddleware, mws[ForwardedMiddleware])
|
||||
}
|
||||
parts, _ := chain["middlewares"].([]any)
|
||||
if len(parts) != 2 || parts[0] != HTTPSOnlyMiddleware || parts[1] != ForwardedHeadersMiddleware {
|
||||
t.Fatalf("the chain must be [%s, %s] in that order, got %v", HTTPSOnlyMiddleware, ForwardedHeadersMiddleware, parts)
|
||||
}
|
||||
for _, p := range parts {
|
||||
if _, ok := mws[p.(string)]; !ok {
|
||||
t.Errorf("the chain names %v but the file does not define it — every route on the box would break", p)
|
||||
}
|
||||
}
|
||||
rs, ok := mws[HTTPSOnlyMiddleware].(map[string]any)["redirectScheme"].(map[string]any)
|
||||
if !ok || rs["scheme"] != "https" || rs["permanent"] != true {
|
||||
t.Errorf("%s must be redirectScheme{scheme: https, permanent: true}, got %v", HTTPSOnlyMiddleware, mws[HTTPSOnlyMiddleware])
|
||||
}
|
||||
h := mws[ForwardedHeadersMiddleware].(map[string]any)["headers"].(map[string]any)
|
||||
if h["stsSeconds"] != HSTSMaxAgeSeconds || HSTSMaxAgeSeconds != 86400 {
|
||||
t.Errorf("HSTS must be one day (86400) until the operator rules otherwise, got %v", h["stsSeconds"])
|
||||
}
|
||||
if h["stsIncludeSubdomains"] != false || h["stsPreload"] != false {
|
||||
t.Errorf("HSTS must not include subdomains nor preload, got %v / %v", h["stsIncludeSubdomains"], h["stsPreload"])
|
||||
}
|
||||
r, ok := doc["http"].(map[string]any)["routers"].(map[string]any)[HTTPSOnlyCatchAllRouter].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("the catch-all router for unknown names over http is missing")
|
||||
}
|
||||
if r["rule"] != "Header(`X-Forwarded-Proto`, `http`)" || r["priority"] != 1 || r["service"] != "noop@internal" {
|
||||
t.Errorf("catch-all must match ONLY X-Forwarded-Proto: http, at priority 1, to noop@internal; got %v", r)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,3 +302,27 @@ func TestRenderCloudflared_HasAReadinessHealthcheck(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// R-1033 (P1): a box that carries the pre-v0.312.0 forwarded.yml (headers only, no https-only redirect), or a hand-edited
|
||||
// one, gets the current file back on the next bring-up — so a released template reaches every box and drift does not
|
||||
// survive a tick. The file is hot-loaded by traefik's file provider (watch: true): no traefik restart.
|
||||
func TestEnsureForwardedHeaders_R1033_RewritesAnOldFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dyn := filepath.Join(dir, "dynamic")
|
||||
if err := os.MkdirAll(dyn, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
old := "http:\n middlewares:\n " + infra.ForwardedMiddleware + ":\n headers:\n customRequestHeaders:\n X-Forwarded-Port: \"443\"\n"
|
||||
touch(t, filepath.Join(dyn, "forwarded.yml"), old)
|
||||
m := &Manager{logger: log.New(io.Discard, "", 0)}
|
||||
if err := m.ensureForwardedHeaders(dir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, _ := os.ReadFile(filepath.Join(dyn, "forwarded.yml"))
|
||||
if string(got) != infra.RenderForwardedHeaders() {
|
||||
t.Fatalf("an old forwarded.yml must be replaced by the current render (https-only chain); got:\n%s", got)
|
||||
}
|
||||
if !strings.Contains(string(got), infra.HTTPSOnlyMiddleware) {
|
||||
t.Fatal("the rewritten file must carry the https-only redirect")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user