v0.312.0: every public name answers only over https; HSTS one day (R-1033, P1)
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:
2026-10-14 22:39:58 +02:00
parent 9b95e8e09b
commit 8bc7ec54cd
7 changed files with 158 additions and 25 deletions
+44 -1
View File
@@ -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)
}
}