# 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.co` and is fully Authentik-gated. Never exposed via `navi.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: ```python 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:** ```caddyfile 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.co` and `recon.echo6.co` - Confirm `header_up -X-Authentik-Username` is 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.co` vhost 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-Username` is 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.co` vhost: 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_limit` matcher 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: 1. Unauthenticated request to every AUTHED-EXTERNAL route → 401 from Caddy, no backend processing 2. Unauthenticated request to `/api/place` for a known place → returns local data only, NO Google Places API call observed in logs 3. Unauthenticated request to `/api/traffic/*` → 401 4. Unauthenticated request to `/api/aurora/*` → 401 5. Header-spoofing test: unauthenticated request with hand-crafted `X-Authentik-Username: matt` header → still 401 (Caddy strips it before forward_auth) 6. Authenticated request through every route works as before 7. Rate limit triggers for unauthenticated client, never for authenticated 8. `recon.echo6.co` is 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.101` or `ssh ct101` from 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** ```bash $ 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** ```bash $ 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)** ```bash $ 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: 1. **Through Caddy:** The `forward_auth` directive validates the session with Authentik. The `copy_headers` directive copies Authentik's response headers to the upstream request, which **overwrites** any client-supplied headers. Header spoofing through Caddy does NOT work. 2. **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: 1. Backends are reachable directly (no firewall) 2. 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): ```bash # 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: ```python 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: ```bash # 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: ```bash ssh -J cortex root@192.168.1.101 'cat /etc/caddy/Caddyfile' ``` Key patterns observed: - All Authentik-gated sites use identical `forward_auth` block - `copy_headers` includes: X-Authentik-Username, X-Authentik-Groups, X-Authentik-Email, X-Authentik-Name, X-Authentik-Uid - No sites use `header_up` to strip incoming headers (not needed for Caddy path, but noted for completeness) - wiki.echo6.co uses `header_down -Content-Security-Policy` to strip CSP from Kiwix responses