mirror of
https://github.com/zvx-echo6/refactored-recon.git
synced 2026-05-20 14:44:39 +02:00
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>
630 lines
30 KiB
Markdown
630 lines
30 KiB
Markdown
# 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.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.
|
|
|
|
```bash
|
|
# 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**
|
|
```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
|