mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-07-02 21:51:38 +00:00
63bfa3d910
Closes #1561. Follow-up to #1551. ## Why #1551 added `Cache-Control: no-store` to all `/api/*` responses. That's sufficient for CDNs that honour origin headers (Varnish, nginx). It is **not** sufficient for Cloudflare zones where Cache Rules / Page Rules override origin Cache-Control. Field evidence from the meshat.se diagnosis (2026-06-04): observers behind Cloudflare were returning `cf-cache-status: HIT` with `age` up to ~6 hours despite the origin emitting `no-store`. The CDN was caching per zone policy and ignoring the upstream directive — exactly the failure mode #1551 cannot reach. The application has no way to inject CDN rules; the only durable fix is operator-side. This PR makes that operator step discoverable and verifiable. ## What ### Server-side detection (log-only) `cmd/server/cdn_detection.go` adds a middleware wired into the `/api/*` chain after `noStoreAPIMiddleware`. On the **first** request bearing any CDN-typical header (`CF-Connecting-IP`, `CF-Ray`, `X-Forwarded-For`, `X-Real-IP`, `Fastly-Client-IP`, `True-Client-IP`) it logs: ``` [security] WARNING: detected request via CDN (CF-Ray header present). Ensure /api/* is bypassed in your CDN config — see docs/deployment-behind-cdn.md. Cached API responses cause observer-flap and incorrect dashboards. ``` `sync.Once` guarantees the warning fires at most once per process boot. The middleware never blocks, never modifies the response, never adds headers. Detection is observational only — operators who run behind a CDN without bypass have a real bug; the warning is appropriate. ### Operator documentation `docs/deployment.md` gains a new **"Behind a CDN (Cloudflare, Fastly)"** section covering: 1. Curl verification command + healthy vs unhealthy output examples 2. Cloudflare Cache Rule creation (URI Path starts-with `/api/` → Bypass cache) 3. Legacy Page Rules equivalent 4. Fastly note 5. Re-verification 6. Meaning of the startup log warning 7. Why we can't fix this server-side `docs/deployment-behind-cdn.md` is the canonical path the log message references — it's a short TL;DR that links back to the full section. ### Healthcheck script `scripts/check-cdn-bypass.sh` — POSIX sh, no dependencies beyond curl + grep + awk. Operators run: ```sh scripts/check-cdn-bypass.sh https://your-domain.example.com ``` Exits `0` with `OK: no CDN caching detected ...` or `1` with a precise diagnostic naming the offending header (`cf-cache-status: HIT` or stale `age`). ## TDD - **Red commit `e90ccaba`** (`test(security): RED ...`) — `cmd/server/cdn_detection_test.go` (4 Go tests + 6 subtests for each header) and `scripts/test-check-cdn-bypass.sh` (3 shell harness cases). Middleware stub returns `next` unchanged so tests compile and fail on assertions, not build errors. - **Green commit `5e6a60b5`** (`feat(security): GREEN ...`) — real middleware, wiring in `routes.go`, healthcheck script, doc. ## Deliverables | File | Status | Purpose | |------|--------|---------| | `cmd/server/cdn_detection.go` | new | middleware + sync.Once warning | | `cmd/server/cdn_detection_test.go` | new | 4 Go tests (1 stand-alone + 1 silence + 1 once + 1 table-driven over 6 headers) | | `cmd/server/routes.go` | modified | `r.Use(cdnDetectionMiddleware)` after no-store | | `docs/deployment.md` | modified | TOC entry + "Behind a CDN" section | | `docs/deployment-behind-cdn.md` | new | canonical path referenced by log message + script output | | `scripts/check-cdn-bypass.sh` | new | operator-runnable healthcheck | | `scripts/test-check-cdn-bypass.sh` | new | shell harness with fake curl | ## What this PR explicitly does NOT do - Does not block requests based on CDN detection (log-only). - Does not enforce CDN bypass (impossible — operator-controlled). - Does not spoof, strip or modify CDN headers. - Does not add CSP / HSTS / other security headers (out of scope). - Warning is not configurable — operators behind a CDN without bypass have a real bug, surfacing it is correct. ## Verification - `go test ./...` in `cmd/server/` — full suite green. - `sh scripts/test-check-cdn-bypass.sh` — 3/3 pass. - Preflight checklist — all 11 gates clean (PII, branch scope, red commit, CSS vars, CSS self-fallback, LIKE-on-JSON, sync migration, async-migration annotation, XSS sinks, img/SVG ratio, themed-img/SVG, fixture coverage). --------- Co-authored-by: openclaw-bot <bot@openclaw.local> Co-authored-by: clawbot <bot@clawbot.invalid>
88 lines
3.2 KiB
Go
88 lines
3.2 KiB
Go
package main
|
|
|
|
// Issue #1561: detect CDN-fronted deployments and warn ONCE.
|
|
//
|
|
// When operators put CoreScope behind Cloudflare/Fastly without
|
|
// configuring a /api/* cache bypass, dashboards go stale — the origin
|
|
// emits Cache-Control: no-store (#1551), but the CDN's zone-level
|
|
// caching policy can still cache JSON responses for hours
|
|
// (cf-cache-status: HIT, age > 0). We can't fix the CDN config from
|
|
// the server side; the best we can do is detect the situation and
|
|
// loudly tell the operator at the logs.
|
|
//
|
|
// Detection: presence of any CDN-specific request header
|
|
// (CF-Connecting-IP, CF-Ray, Fastly-Client-IP, True-Client-IP).
|
|
// We deliberately exclude X-Forwarded-For and X-Real-IP: every
|
|
// generic reverse proxy (nginx, Caddy, Traefik, k8s ingress) sets
|
|
// those, so including them would warn operators who aren't behind
|
|
// a CDN at all and train them to ignore the warning entirely
|
|
// (defeating the point of #1561).
|
|
//
|
|
// Side effects: a single log line per process boot — never blocks
|
|
// the request, never modifies the response, never logs again.
|
|
|
|
import (
|
|
"log"
|
|
"net/http"
|
|
"sync"
|
|
"sync/atomic"
|
|
)
|
|
|
|
var cdnWarnOnce sync.Once
|
|
|
|
// cdnWarned is set true after the first CDN-fronted request has been
|
|
// observed and logged. Subsequent requests short-circuit before the
|
|
// per-request header scan in firstCDNHeader — a hot-path optimization
|
|
// for the steady state (warning already emitted, every /api request
|
|
// otherwise pays for 4 http.Header.Get lookups forever).
|
|
var cdnWarned atomic.Bool
|
|
|
|
// cdnHeaders are HTTP request headers injected ONLY by CDNs
|
|
// (Cloudflare, Fastly, Akamai) — never by a generic reverse proxy.
|
|
// Detected case-insensitively by http.Header.Get.
|
|
//
|
|
// X-Forwarded-For / X-Real-IP are intentionally NOT in this list:
|
|
// every nginx/Caddy/Traefik/k8s-ingress deployment sets them, so
|
|
// using them as a CDN signal produces a false positive on every
|
|
// reverse-proxied install (issue #1561 round-1 review).
|
|
var cdnHeaders = []string{
|
|
"CF-Connecting-IP", // Cloudflare
|
|
"CF-Ray", // Cloudflare
|
|
"Fastly-Client-IP", // Fastly
|
|
"True-Client-IP", // Akamai (also set by Cloudflare Enterprise)
|
|
}
|
|
|
|
// cdnDetectionMiddleware inspects each incoming request for CDN
|
|
// headers and, on the FIRST one observed, logs a single warning
|
|
// pointing the operator at docs/deployment-behind-cdn.md. The
|
|
// middleware always calls next; it never blocks or rewrites.
|
|
func cdnDetectionMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Fast path: once we've warned, skip the per-request header
|
|
// scan entirely. Steady state for any CDN-fronted deploy is
|
|
// ~every request hitting this branch.
|
|
if cdnWarned.Load() {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
if hdr := firstCDNHeader(r.Header); hdr != "" {
|
|
cdnWarnOnce.Do(func() {
|
|
log.Printf("[security] WARNING: detected request via CDN (%s header present). "+
|
|
"Ensure /api/* is bypassed in your CDN config — see docs/deployment-behind-cdn.md. "+
|
|
"Cached API responses cause observer-flap and incorrect dashboards.", hdr)
|
|
cdnWarned.Store(true)
|
|
})
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func firstCDNHeader(h http.Header) string {
|
|
for _, name := range cdnHeaders {
|
|
if h.Get(name) != "" {
|
|
return name
|
|
}
|
|
}
|
|
return ""
|
|
}
|