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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user