refactored-recon/AUTH-PUBLIC-FRONTEND.md
Matt db078caa93 docs: add P0 auth spoofing mitigation findings (§10.2.1)
Applied iptables firewall on VM 1130 to restrict ports 8420/8440
to CT 101 (Caddy) and localhost only. Documents Tailscale ts-input
chain ordering requirement for future firewall work.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-26 02:02:38 +00:00

30 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.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:

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.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.2.1 — P0 Mitigation Applied (2026-04-25)

Vulnerability Description

CRITICAL: RECON backends (ports 8420, 8440) bound to 0.0.0.0 with no firewall rules, accepting the X-Authentik-Username header unconditionally. Any host on the LAN (192.168.1.0/24) or Tailscale network (100.64.0.0/10) could bypass Caddy entirely, connect directly to backends, and spoof any user identity via HTTP header injection.

Verified exploitation: From cortex (100.64.0.14), we successfully read all contacts, added/removed API keys, and accessed admin functions using a single spoofed header.

Caddy itself was NOT vulnerable—the forward_auth directive properly authenticates and copy_headers overwrites client-supplied headers. The vulnerability was unprotected direct backend access.

Mitigation Summary

Applied iptables firewall rules on VM 1130 (RECON host) restricting ports 8420 and 8440 to three allowed sources:

  1. localhost (lo interface) — for internal health checks and local tooling
  2. CT 101 LAN IP (192.168.1.101) — Caddy reverse proxy
  3. CT 101 Tailscale IP (100.64.0.8) — Caddy reverse proxy via Tailscale

All other traffic to these ports is dropped.

Tailscale iptables Ordering Requirement

Critical implementation note: On hosts running Tailscale, the ts-input chain is jumped to early in the INPUT chain and accepts all traffic from the tailscale0 interface before any subsequent rules are evaluated.

Chain INPUT (policy ACCEPT)
num  target    prot  source         destination
1    ts-input  all   0.0.0.0/0      0.0.0.0/0      ← Tailscale jumps here first
...

Inside ts-input, rule 4 is typically ACCEPT all -- tailscale0 * 0.0.0.0/0 0.0.0.0/0, which accepts ALL Tailscale traffic unconditionally.

Consequence: Any port-specific DROP rules appended to INPUT will never be reached for Tailscale traffic. Custom firewall rules on Tailscale hosts must be inserted at position 1 (before the ts-input jump) to take effect.

# WRONG — will never block Tailscale traffic:
iptables -A INPUT -p tcp --dport 8420 -j DROP

# CORRECT — inserted before ts-input:
iptables -I INPUT 1 -p tcp --dport 8420 -j DROP

Final Rule Set

Rules applied and persisted via netfilter-persistent:

Chain INPUT (policy ACCEPT)
num  target   prot  in   source         dport  comment
1    ACCEPT   tcp   lo   0.0.0.0/0      8420   P0 auth spoofing fix 2026-04-25
2    ACCEPT   tcp   *    192.168.1.101  8420   P0 auth spoofing fix 2026-04-25
3    ACCEPT   tcp   *    100.64.0.8     8420   P0 auth spoofing fix 2026-04-25
4    DROP     tcp   *    0.0.0.0/0      8420   P0 auth spoofing fix 2026-04-25
5    ACCEPT   tcp   lo   0.0.0.0/0      8440   P0 auth spoofing fix 2026-04-25
6    ACCEPT   tcp   *    192.168.1.101  8440   P0 auth spoofing fix 2026-04-25
7    ACCEPT   tcp   *    100.64.0.8     8440   P0 auth spoofing fix 2026-04-25
8    DROP     tcp   *    0.0.0.0/0      8440   P0 auth spoofing fix 2026-04-25
9    ts-input all   *    0.0.0.0/0      *      (Tailscale chain)

Verification Results

Test Expected Actual
Direct 8420 from cortex (100.64.0.14) BLOCKED BLOCKED (8 pkts dropped)
Direct 8440 from cortex (100.64.0.14) BLOCKED BLOCKED (5 pkts dropped)
Direct 8420 from TOC (100.64.0.5) BLOCKED BLOCKED
Via Caddy (CT 101) → 8440 ALLOWED ALLOWED (4 pkts accepted)
Localhost → 8420 (health check) ALLOWED ALLOWED

Outstanding Items (Broader Auth Migration)

The following remain part of the scheduled migration work and are not addressed by this P0 fix:

  1. header_up -X-Authentik-Username on Caddy reverse_proxy blocks

    • Defense-in-depth measure to strip client-supplied auth headers before forwarding
    • Scheduled for migration session (Phase 3)
    • Not urgent: Caddy's copy_headers already overwrites these headers with Authentik's response
  2. Other 0.0.0.0-bound services on VM 1130

    • Kiwix (8430), files nginx (8888), Valhalla (8002), Nominatim (8010)
    • Lower priority: these services do NOT trust X-Authentik-Username header
    • Separate audit scheduled; network segmentation may be applied for consistency
  3. Postgres/Samba/NFS/CUPS

    • Separate infrastructure concerns, separate audit
    • Not part of the Caddy/RECON auth migration scope

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

$ 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:

  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):

# 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.

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_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