CRITICAL: RECON backends (8420, 8440) accept direct LAN/Tailscale connections and trust X-Authentik-Username header unconditionally. Verified exploitation: contacts read, API keys added via spoofed header. Root cause: No firewall on RECON VM, services bind 0.0.0.0. Caddy forward_auth is NOT bypassed - direct backend access is the vector. P0 remediation: Firewall RECON to accept only from CT 101. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
26 KiB
Public Navi Frontend with Selective Backend Auth
Status: Design — implementation deferred to dedicated Phase 3 module Owner: Matt Last updated: 2026-04-25
1. Principle
A request to navi.echo6.co triggers an external paid API call (Google Places, TomTom, Gemini, or any future paid integration) ONLY IF the request carries a valid Authentik session. No session = no external call, ever. This is a hard gate, not a soft preference.
Concretely:
- Local stack services (Photon, Nominatim, Valhalla, Overture Postgres, PAD-US, OSM wiki rewriter, contour/hillshade/public-lands tiles) can serve unauthenticated users, subject to rate limiting.
- External paid services (Google Places, TomTom, Gemini, anything that costs real money per call) are authenticated-only, no exceptions, no fallbacks, no "low-cost" carve-outs.
- User-state-tied features (contacts, address book, anything keyed to
X-Authentik-Username) are authenticated-only. - Backend administration (API key management, RECON pipeline controls, settings, scrapers) lives behind
recon.echo6.coand is fully Authentik-gated. Never exposed vianavi.echo6.co.
The migration is gated on verified protection of paid APIs, not just architecture that's "designed to gate."
2. Two Frontends, Two Auth Postures
| Domain | Posture | Scope |
|---|---|---|
navi.echo6.co |
Public + selective backend auth | End-user navigation. Browse map, search, route, view places. Authenticated for personal data + paid enrichment. Migration target. |
recon.echo6.co |
Fully Authentik-gated, admin-only | Backend administration: API keys, RECON dashboard, Nav-I dashboard, settings, scrapers, pipeline controls. Unchanged from today, NOT part of this migration. |
Hard separation: No path on navi.echo6.co reaches admin endpoints. No /api/keys, /api/settings, or backend configuration is exposed via Navi's reverse proxy. The two domains share a RECON backend but expose different surface areas.
3. Endpoint Inventory and Classification
Four categories:
- PUBLIC — local stack data, no external calls, no per-user state. Public access with rate limiting.
- AUTHED-EXTERNAL — exists solely to proxy a paid external API. Caddy forward-auth gated. No unauthenticated fallback.
- AUTHED-USER — per-user state. Caddy forward-auth gated.
- MIXED — returns local data freely; optional enrichment from paid API only when authenticated.
Classified routes
| Route | Category | Notes |
|---|---|---|
/ (frontend HTML/JS/CSS/assets) |
PUBLIC | Static frontend |
/tiles/* (PMTiles archives) |
PUBLIC | Basemap, hillshade, contours, public lands |
/api/photon/* |
PUBLIC | Local geocoder |
/api/nominatim/* |
PUBLIC | Local reverse geocoder |
/api/valhalla/* |
PUBLIC | Local routing |
/api/overture/* |
PUBLIC | Local POI Postgres |
/api/padus/* |
PUBLIC | Local public lands data |
/api/wiki/* |
PUBLIC | Local cache + OSM wiki rewriter |
/api/kiwix/* |
PUBLIC | Local ZIM archives (when wired) |
/api/place |
MIXED | Local data unauthenticated; Google Places enrichment authed-only |
/api/config |
PUBLIC | Feature flags. May suppress authed-only flags for unauthenticated callers (TBD) |
/api/traffic/* |
AUTHED-EXTERNAL | TomTom proxy. No local fallback. |
/api/aurora/* |
AUTHED-EXTERNAL | Treat as paid: compute-expensive, GPU-bound |
/api/gemini/* (if exposed) |
AUTHED-EXTERNAL | Paid API |
/api/contacts/* |
AUTHED-USER | Per-user via X-Authentik-Username |
/api/address-book/* |
AUTHED-USER | Per-user |
/api/keys/* |
NOT EXPOSED via navi | Lives on recon.echo6.co only |
/api/settings/* |
NOT EXPOSED via navi | Lives on recon.echo6.co only |
Verification step before migration: audit RECON's actual route definitions to confirm the mapping is complete. Routes not in this table must be classified before navi.echo6.co is opened.
4. Defense in Two Layers
External-API protection cannot rely on a single layer of config. Two independent gates:
Layer 1 — Caddy
navi.echo6.co's vhost on CT 101 applies forward-auth selectively:
- Public paths: no auth, rate-limited (see §6)
- AUTHED-EXTERNAL paths: forward-auth required. Unauthenticated requests rejected at Caddy with 401. Never reach RECON.
- AUTHED-USER paths: forward-auth required. Same.
- MIXED paths (
/api/place): no Caddy gate; backend handles enrichment decision.
recon.echo6.co's vhost remains fully forward-auth gated as today. Unchanged.
Layer 2 — RECON Backend
Mixed routes (and any other route that conditionally calls external APIs) MUST validate authentication independently before triggering paid calls. The pattern:
def get_place(place_id):
# Always-available local data
data = query_overture(place_id)
data.update(query_osm(place_id))
data.update(query_nominatim_reverse(...))
# External paid API — HARD GATE on verified auth
auth_user = request.headers.get('X-Authentik-Username')
if auth_user and is_valid_authentik_user(auth_user):
google_data = query_google_places(place_id)
data.update(google_data)
# No valid auth = no Google call. Period. No fallback. No exceptions.
return data
The is_valid_authentik_user() check is non-trivial. Header presence alone is insufficient if Caddy isn't stripping client-supplied headers (see §5).
5. Header Spoofing Defense
Caddy must strip incoming X-Authentik-Username headers from client requests, then inject the validated username after forward-auth succeeds. Otherwise, an attacker can set their own header and bypass the auth gate.
Required Caddy directive on every reverse_proxy block exposed to clients:
reverse_proxy {
header_up -X-Authentik-Username
# forward_auth then injects the validated header for authed requests
}
Pre-migration audit: verify this directive exists on recon.echo6.co's current vhost. If absent, that's a pre-existing security issue regardless of this migration. Fix before opening navi.echo6.co to public traffic.
6. Rate Limiting
Tiered: unauthenticated rate-limited, authenticated unlimited.
| Audience | Rate Limit |
|---|---|
| Unauthenticated requests (no valid Authentik session) | 60 req/min per IP, burst 30, 429 + Retry-After on exceeded |
| Authenticated requests (valid Authentik session) | No limit |
Rationale:
- Protects public surface from abuse and casual scraping
- Never gets in the way of the actual user (Matt) or any future authenticated users
- Single-user system today; tiered model remains correct under multi-user expansion
Implementation: Caddy matcher applies rate_limit only to requests lacking the validated auth header. Authed requests bypass the limiter entirely.
7. Mixed Route Behavior — /api/place
Single mixed route in current scope. Behavior:
- Unauthenticated: returns Overture + OSM + Nominatim reverse + PAD-US data. Google Places not called. Response is complete and useful — the panel renders, just without Google's enrichment.
- Authenticated: all of the above, plus Google Places enrichment merged into the response.
Frontend should not render visibly different UI for the two cases. Google Places fills gaps (phone, hours, ratings); when those gaps exist in local data they show as missing. No "log in to see more" prompt embedded in the panel — that adds friction and reveals which fields come from where.
If a future feature needs to be visibly authed-only (e.g., "save this place to contacts"), it gets its own UI affordance gated on auth state, separate from the place detail data.
8. Frontend 401 UX (Open Question)
When a user without a session triggers an authenticated route (clicking "save contact," opening Aurora chat, etc.), three patterns to consider:
| Pattern | Pros | Cons |
|---|---|---|
| Inline auth modal | Smooth UX. Returns to current map state after auth. | Most implementation work. |
| Redirect to Authentik, then back | Simple. Reuses existing flow. | Loses map state (zoom, pan, panel state). |
| Visual gating (gray out authed features pre-emptively) | No surprises. Clear what requires auth. | Reveals auth requirements visibly to all users; may feel restrictive. |
Recommendation: redirect-with-return-URL for rare actions (save contact, open Aurora). Visual gating for areas that don't make sense without state (contacts panel shows "log in to view your contacts" empty state).
Decision deferred to implementation session.
9. CDN / Edge Concerns (Open Question)
Cloudflare or similar in front of navi.echo6.co provides:
- DDoS protection
- Bot mitigation beyond simple rate limiting
- Geographic edge caching for static tiles (faster TTFB globally)
- TLS termination handoff
Not in scope for initial migration. Add after public exposure if abuse patterns emerge, or as a Phase 3 hardening pass.
If added later, the rate limiting (§6) and header-spoofing defense (§5) need to account for Cloudflare's CF-Connecting-IP header for accurate per-IP limiting.
10. Pre-Migration Diagnostics
These must complete before any Caddy public-exception change is deployed:
10.1 — TomTom traffic overlay (currently non-functional)
Per Matt's note: TomTom traffic overlay is not rendering despite has_traffic_overlay: true, an active API key, and an expected periodic auto-pull mechanism.
Required outcome: confirm whether TomTom is callable from unauthenticated requests in the current setup. If yes, this is a money-cost vector that must be closed before public exposure.
Diagnostic scope (separate task, see [TomTom diagnostic prompt]):
- Verify API key presence and validity
- Identify the auto-pull mechanism (cron/timer/in-process scheduler)
- Determine why pulls aren't happening (or aren't reaching frontend)
- Confirm the route's auth posture before fixing the rendering bug
10.2 — CT 101 Caddy configuration audit
SSH access to CT 101 from Matt's workstation requires jump through cortex (192.168.1.150). Once accessible:
- Read full Caddy config for
navi.echo6.coandrecon.echo6.co - Confirm
header_up -X-Authentik-Usernameis present on existing reverse_proxy blocks - Document the current vhost structure to inform the migration patch
- Append findings to this doc as §10.2 appendix
10.3 — RECON route inventory
Audit /opt/recon/lib/api.py (and any other route definitions) to confirm:
- Every route is classified per §3
- No admin routes are reachable through
navi.echo6.co's expected reverse-proxy path - Every route that calls a paid external service has a backend-layer auth check (§4 Layer 2)
If any route fails (3) — calls a paid API without checking auth — that's the highest-priority fix before migration.
10.4 — Background-job and integration audit
Identify anything that hits RECON via Caddy that might break when auth posture changes:
- Cron jobs hitting
/api/*from outside the Proxmox cluster - External integrations expecting public access
- Mobile PWA sessions (likely fine — Authentik sessions persist through Caddy reload)
recon-watchdog(already bypasses Caddy via localhost:8420 — unaffected)
Most of Matt's stack is internal and unaffected. Audit confirms this before flipping config.
11. Migration Plan
Each phase is independently deployable and rollback-able.
Phase 1 — Caddy public exception for static frontend
- Update
navi.echo6.covhost on CT 101: public access for/,/tiles/*,/assets/* - All
/api/*paths remain forward-auth gated for now (no behavior change yet) - Verify
header_up -X-Authentik-Usernameis on the reverse_proxy block - Test: unauthenticated browser loads the map and renders tiles. API calls still 401.
Rollback: revert Caddyfile, reload Caddy.
Phase 2 — Backend auth posture changes
- For each PUBLIC route in §3: confirm the RECON handler does not require
X-Authentik-Username(or makes it optional). No code changes needed if handlers don't currently check. - For each AUTHED-EXTERNAL route: confirm the RECON handler validates auth before calling external API.
- For MIXED routes (
/api/place): implement the §4 Layer 2 pattern. - Add
is_valid_authentik_user()helper if not present.
Rollback: git revert backend changes, restart recon.service.
Phase 3 — Caddy selective public exposure
- Update
navi.echo6.covhost: PUBLIC routes (per §3) become unauthenticated-accessible. - AUTHED-EXTERNAL and AUTHED-USER routes retain forward-auth.
- MIXED routes: no Caddy gate (backend handles).
- Test matrix: unauthenticated request to each PUBLIC route → 200. Unauthenticated request to each AUTHED route → 401. Authenticated request to each → 200.
Rollback: revert Caddyfile, reload Caddy.
Phase 4 — Tiered rate limiting
- Add Caddy
rate_limitmatcher for unauthenticated requests on PUBLIC routes. - 60 req/min per IP, burst 30.
- Authenticated requests bypass entirely.
- Test: 70 unauthenticated requests in 60s → 429s after 60. Authenticated client unaffected.
Rollback: remove rate_limit matcher, reload Caddy.
Phase 5 — Frontend 401 handling
- Implement chosen UX pattern from §8 (deferred decision).
- Visual gating for unauth-incompatible panels (contacts, etc.).
- Redirect-with-return-URL flow for one-shot authed actions.
- Test from incognito browser: every authed feature has a sensible empty/login-prompt state.
Rollback: revert frontend changes, rebuild, redeploy.
Phase 6 — Observability
- Logging: per-request auth state, route, response time
- Metrics: 429 rate, 401 rate, top public routes by request volume
- Alerting: spike in unauthenticated traffic, repeated 429s from single IP
- Dashboards in Nav-I admin panel
Rollback: N/A — additive only.
Acceptance criteria for declaring migration complete
Migration is "done" when ALL of the following are verified:
- Unauthenticated request to every AUTHED-EXTERNAL route → 401 from Caddy, no backend processing
- Unauthenticated request to
/api/placefor a known place → returns local data only, NO Google Places API call observed in logs - Unauthenticated request to
/api/traffic/*→ 401 - Unauthenticated request to
/api/aurora/*→ 401 - Header-spoofing test: unauthenticated request with hand-crafted
X-Authentik-Username: mattheader → still 401 (Caddy strips it before forward_auth) - Authenticated request through every route works as before
- Rate limit triggers for unauthenticated client, never for authenticated
recon.echo6.cois unchanged and remains fully gated
If any of these fails, migration is not done. Roll back the failing phase, fix, re-verify.
12. Resolved Decisions
| Question | Decision |
|---|---|
| Admin role distinction | Two separate frontends. navi.echo6.co has no admin surface. recon.echo6.co is admin-only and unchanged. |
| Rate limiting model | Tiered: unauthenticated limited, authenticated unlimited |
| Upload size limits | N/A — no upload endpoints in current scope. Future concern. |
| In-progress operations during migration | Authentik sessions persist through Caddy reload. RECON background jobs are internal and unaffected. recon-watchdog already bypasses Caddy. Pre-migration audit (§10.4) confirms no external integrations break. |
| Mixed route handling | /api/place returns local data unauthenticated, adds Google Places enrichment when authed. Backend-layer gate (§4 Layer 2). |
| TomTom cost model | Free tier 2.5k req/day. Authed-only regardless. Currently non-functional — diagnose before migration (§10.1). |
13. Open Questions
| Question | Owner | Decision needed by |
|---|---|---|
| Frontend 401 UX pattern (modal vs redirect vs visual gating) | Matt | Phase 5 implementation |
| CDN/Cloudflare in front of navi.echo6.co | Matt | Optional, post-migration hardening |
/api/config behavior — suppress authed-only feature flags for unauth callers, or return all and let frontend handle? |
Matt | Phase 2 implementation |
| TomTom traffic overlay — non-functional, root cause unknown | Diagnostic task | Before Phase 1 |
| CT 101 Caddy config — not yet read | SSH-jump task | Before Phase 1 |
14. Out of Scope
- Multi-user expansion (current system is single-user; design supports multi-user future without rework)
- Per-authed-user rate limits (single auth tier today; revisit if multi-user)
- "Aurora-lite" public read-only endpoint with token budget (interesting future option, explicitly deferred)
- GPX upload, image upload, or other upload features (not currently exposed)
- Tile-server CDN edge caching (Phase 3 hardening, not migration)
- Authentik group-based admin/user role separation (not needed for single-user)
Appendix: CT 101 Caddy Audit Findings (2026-04-25)
Audit Metadata
- Caddy version: v2.10.2
- Config location:
/etc/caddy/Caddyfile(no imports, single file) - SSH access:
ssh -J cortex root@192.168.1.101orssh ct101from cortex - Auditor: Claude + Matt
- Date: 2026-04-25
Executive Summary
CRITICAL FINDING: All RECON backends (8420, 8440, 8430, 8888) listen on 0.0.0.0 and accept connections from any LAN/Tailscale host. The X-Authentik-Username header is trusted without validation. An attacker on the LAN or Tailscale network can bypass Caddy entirely, hit backends directly, and spoof any user identity.
Verified exploitation: With a spoofed X-Authentik-Username: matt header sent directly to 100.64.0.24:8420, we successfully:
- Read all contacts (name, phone, email, address, call sign)
- Added a test API key to the Gemini key pool
- Accessed the full API keys listing
Caddy itself is NOT vulnerable to header spoofing. The forward_auth directive properly authenticates users via Authentik, and the copy_headers directive overwrites any client-supplied headers with Authentik's response. The vulnerability is the unprotected direct backend access.
Threat Model
| Attack Vector | Exploitable? | Notes |
|---|---|---|
| External internet → Caddy → Backend | NO | forward_auth gates access, Authentik determines identity |
| LAN device → Backend (bypass Caddy) | YES | No firewall, backends listen on 0.0.0.0 |
| Tailscale device → Backend (bypass Caddy) | YES | Same as LAN, Tailscale routes directly to backend IPs |
| Compromised LAN device → Backend | YES | Lateral movement allows full impersonation |
Per-Vhost Audit
| Site | Authentik Gated? | Backend | Backend Port | Backend Trusts Header? | Direct Access Exploitable? | Severity |
|---|---|---|---|---|---|---|
| navi.echo6.co | Partial (/tiles/* public) | RECON | 8440 | Yes (contacts, place enrichment) | YES | HIGH |
| recon.echo6.co | Full | RECON | 8420 | Yes (all admin, contacts, keys) | YES | CRITICAL |
| files.echo6.co | Full | Static file server | 8888 | No (just nginx autoindex) | No | LOW |
| wiki.echo6.co | Full | Kiwix-serve | 8430 | No (kiwix has no user auth) | No | LOW |
| mesh.echo6.co | Full | MeshMonitor | 192.168.1.100:8080 | Unknown | TBD | MEDIUM |
| lidarr.echo6.co | Full | Lidarr | 100.64.0.18:8686 | No (uses own API key) | No | LOW |
| navidrome.echo6.co | Partial (/rest/* public) | Navidrome | 100.64.0.18:4533 | No (uses own auth) | No | LOW |
| ai.echo6.co | None | Open WebUI | 100.64.0.14:8080 | No (uses own auth) | No | LOW |
| stream.echo6.co | None | PeerTube | 192.168.1.170:80 | No (uses own auth) | No | LOW |
| immich.echo6.co | None | Immich | 192.168.1.182:2283 | No (uses own auth) | No | LOW |
| nextcloud.echo6.co | None | Nextcloud | 192.168.1.183:11000 | No (uses own auth) | No | LOW |
| jellyfin.echo6.co | None | Jellyfin | 100.64.0.18:8096 | No (uses own auth) | No | LOW |
| requests.echo6.co | None | Overseerr | 100.64.0.18:5055 | No (uses own auth) | No | LOW |
| nas.echo6.co | None | Pi-NAS | 100.64.0.21:80 | Unknown | TBD | LOW |
| echo6.co | None | SearXNG | 100.64.0.15:8080 | No | No | LOW |
| vpn.idahomesh.com | None | WireGuard UI | 192.168.1.106:8080 | Unknown | TBD | MEDIUM |
| ots.k7zvx.com | None | OpenTAK Server | 192.168.1.109:443 | No (uses own auth) | No | LOW |
RECON Backend Binding Analysis
LISTEN 0.0.0.0:8420 # RECON dashboard/API - CRITICAL
LISTEN 0.0.0.0:8440 # Navi frontend - HIGH
LISTEN 0.0.0.0:8430 # Kiwix-serve - LOW (no user auth)
LISTEN 0.0.0.0:8888 # Static files - LOW (no user auth)
All services bind to 0.0.0.0 (all interfaces). No iptables rules restrict access. Any host on:
- LAN (192.168.1.0/24)
- Tailscale (100.64.0.0/10)
...can connect directly and bypass Caddy/Authentik entirely.
Exploitation Proof
Test 1: Read contacts with spoofed header
$ curl -H 'X-Authentik-Username: matt' 'http://100.64.0.24:8420/api/contacts'
[{"name":"Matt Johnson","phone":"2083080811","email":"matt@echo6.co",...}]
Test 2: Add API key with spoofed header
$ curl -X POST -H 'X-Authentik-Username: matt' \
-H 'Content-Type: application/json' \
-d '{"key":"test-key-123"}' \
'http://100.64.0.24:8420/api/keys'
{"count":5,"index":4} # Key was added
Test 3: Without header (baseline)
$ curl 'http://100.64.0.24:8420/api/contacts'
{"error":"Authentication required"} # Correctly rejected
Header Stripping Analysis
None of the Caddy vhosts include header_up -X-Authentik-Username or similar directives to strip incoming client headers. However, this is not the root cause of the vulnerability:
-
Through Caddy: The
forward_authdirective validates the session with Authentik. Thecopy_headersdirective copies Authentik's response headers to the upstream request, which overwrites any client-supplied headers. Header spoofing through Caddy does NOT work. -
Direct backend access: The backend is reachable without going through Caddy at all. No header stripping in Caddy helps here — the request never touches Caddy.
The header_up -X-Authentik-Username directive would be defense-in-depth but is not the primary fix.
Root Cause
The RECON backend trusts X-Authentik-Username header unconditionally, assuming all requests come through Caddy's forward_auth. This assumption fails when:
- Backends are reachable directly (no firewall)
- Attackers on LAN/Tailscale can bypass Caddy
Severity Classification
| Classification | Criteria | Vhosts |
|---|---|---|
| CRITICAL | Backend reachable directly + trusts header + admin functions | recon.echo6.co (8420) |
| HIGH | Backend reachable directly + trusts header + user data | navi.echo6.co (8440) |
| MEDIUM | Backend reachable directly + unknown header trust | mesh.echo6.co, vpn.idahomesh.com |
| LOW | Backend has own auth OR no sensitive data | All others |
Remediation Options
Option A: Network Segmentation (Recommended)
Firewall RECON VM to only accept connections from CT 101 (Caddy):
# On RECON VM (192.168.1.130 / 100.64.0.24)
iptables -A INPUT -p tcp --dport 8420 -s 192.168.1.101 -j ACCEPT
iptables -A INPUT -p tcp --dport 8420 -s 100.64.0.8 -j ACCEPT # Caddy Tailscale IP
iptables -A INPUT -p tcp --dport 8420 -j DROP
# Repeat for 8440, 8430, 8888
Pros: Blocks direct access entirely. Simple. No code changes. Cons: Breaks recon-watchdog if it runs from a different host. Breaks any legitimate direct access patterns.
Option B: Backend Auth Validation
Modify RECON to validate the Authentik session, not just trust the header:
def is_valid_authentik_user(username):
# Option B1: Check header came from trusted proxy IP
if request.remote_addr not in TRUSTED_PROXY_IPS:
return False
return True
# Option B2: Validate session with Authentik API (expensive)
# ...
Pros: Defense in depth. Works regardless of network topology. Cons: Requires code changes. IP checking can be spoofed if attacker is on same subnet.
Option C: Bind to Localhost Only
Configure RECON services to bind to 127.0.0.1 or a Unix socket, then proxy from Caddy via localhost or socket.
Pros: Completely eliminates direct access. Cons: Requires Caddy to run on same host as RECON, or use a local proxy. Architecture change.
Recommended Priority
| Priority | Action | Rationale |
|---|---|---|
| P0 (Today) | Firewall RECON VM port 8420 to CT 101 only | Blocks critical admin access exploitation |
| P1 (This week) | Firewall ports 8440, 8430, 8888 | Blocks remaining RECON exploitation |
| P2 (Before migration) | Implement backend auth validation (Option B) | Defense in depth for public exposure |
| P3 (Optional) | Add header_up -X-Authentik-* to Caddy |
Defense in depth, low priority |
Verification Commands
After remediation, verify with:
# From cortex (should fail after firewall)
curl -H 'X-Authentik-Username: matt' 'http://100.64.0.24:8420/api/contacts'
# Expected: connection refused or timeout
# From CT 101 (should still work)
curl -H 'X-Authentik-Username: matt' 'http://100.64.0.24:8420/api/contacts'
# Expected: returns data (Caddy will set this header after auth)
Appendix: Full Caddyfile Reference
The complete Caddyfile is available via:
ssh -J cortex root@192.168.1.101 'cat /etc/caddy/Caddyfile'
Key patterns observed:
- All Authentik-gated sites use identical
forward_authblock copy_headersincludes: X-Authentik-Username, X-Authentik-Groups, X-Authentik-Email, X-Authentik-Name, X-Authentik-Uid- No sites use
header_upto strip incoming headers (not needed for Caddy path, but noted for completeness) - wiki.echo6.co uses
header_down -Content-Security-Policyto strip CSP from Kiwix responses