# AUTH-PUBLIC-FRONTEND.md — Public Navi Frontend with Selective Backend Auth **Status:** DRAFT **Created:** 2026-04-25 **Author:** Matt + Claude **Target:** Phase 3 dedicated session **Location:** recon_refactor repo on cortex --- ## 1. Principle Statement **Single rule:** Auth-lock anything that (a) costs real money to serve or (b) is tied to a specific user account. Everything else is public. This enables a public-facing navigation frontend at navi.echo6.co while protecting: - Paid API calls (Google Places, TomTom commercial endpoints) - Per-user data (contacts, saved locations) - Administrative functions (key management, service restart) --- ## 2. Endpoint Inventory ### 2.1 Main API Routes (api.py) | Route | Method | Classification | Rationale | |-------|--------|----------------|-----------| | `/` | GET | PUBLIC | Dashboard landing page | | `/search` | GET | PUBLIC | Vector search UI (uses local embedding) | | `/catalogue` | GET | PUBLIC | Document catalogue browser | | `/upload` | GET | PUBLIC | Upload page (form only) | | `/web-ingest` | GET | PUBLIC | Web ingest form | | `/failures` | GET | PUBLIC | Failure browser | | `/deleted-contacts` | GET | AUTHED-USER | Per-user deleted contacts page | | `/nav-i` | GET | AUTHED-USER | Nav-I landing (shows user contact count) | | `/nav-i/api-keys` | GET | AUTHED-USER | API key management UI | | `/peertube` | GET | PUBLIC | PeerTube dashboard | | `/peertube/channels` | GET | PUBLIC | Channel listing | | `/settings/keys` | GET | AUTHED-USER | Gemini key management | | `/settings/cookies` | GET | AUTHED-USER | YouTube cookie management | | `/settings/vpn` | GET | AUTHED-USER | VPN control panel | | `/settings/health` | GET | PUBLIC | Service health dashboard | | `/kiwix` | GET | PUBLIC | Kiwix library browser | | `/kiwix/scraper` | GET | PUBLIC | Scraper status | | `/api/upload` | POST | AUTHED-USER | Document upload (disk usage concern) | | `/api/upload//status` | GET | PUBLIC | Upload status check | | `/api/upload/categories` | GET | PUBLIC | Category listing | | `/api/quick-stats` | GET | PUBLIC | Cached stats | | `/api/retry-all` | POST | AUTHED-USER | Bulk retry (admin action) | | `/api/ingest-url` | POST | AUTHED-USER | URL ingestion (disk usage) | | `/api/ingest-urls` | POST | AUTHED-USER | Batch URL ingestion | | `/api/crawl` | POST | AUTHED-USER | Site crawl (resource intensive) | | `/api/crawl//status` | GET | PUBLIC | Crawl status | | `/api/ingest-peertube` | POST | AUTHED-USER | PeerTube ingestion | | `/api/ingest-peertube//status` | GET | PUBLIC | Ingestion status | | `/api/peertube/stats` | GET | PUBLIC | Stats endpoint | | `/api/search` | POST | PUBLIC | Vector search (local embedding) | | `/api/status` | GET | PUBLIC | Pipeline status | | `/api/retry/` | POST | AUTHED-USER | Single retry | | `/api/ingest` | POST | AUTHED-USER | Raw intel ingestion | | `/api/knowledge-stats` | GET | PUBLIC | Cached knowledge stats | | `/api/traffic/flow///.png` | GET | **AUTHED-COST** | TomTom traffic tiles (paid API) | | `/api/place//` | GET | **MIXED** | See Section 8 | | `/api/landclass` | GET | PUBLIC | PAD-US lookup (local DuckDB) | | `/api/config` | GET | PUBLIC | Feature flags | | `/api/health` | GET | PUBLIC | Health check | | `/api/service/restart` | POST | AUTHED-USER | Service restart (admin) | | `/api/keys` | GET | AUTHED-USER | List Gemini keys | | `/api/keys` | POST | AUTHED-USER | Add Gemini key | | `/api/keys/` | PUT | AUTHED-USER | Replace key | | `/api/keys/` | DELETE | AUTHED-USER | Remove key | | `/api/keys/validate` | POST | AUTHED-USER | Validate all keys | | `/api/keys//validate` | POST | AUTHED-USER | Validate one key | | `/api/keys/reload` | POST | AUTHED-USER | Reload from env | | `/api/nav-i/api-keys/list` | GET | AUTHED-USER | Nav-I key listing | | `/api/nav-i/api-keys/update` | POST | AUTHED-USER | Update key | | `/api/nav-i/api-keys/test` | POST | AUTHED-USER | Test key | | `/api/nav-i/api-keys/restart-recon` | POST | AUTHED-USER | Restart service | | `/api/cookies/status` | GET | AUTHED-USER | Cookie freshness | | `/api/cookies/upload` | POST | AUTHED-USER | Upload cookies | | `/api/vpn/*` | * | AUTHED-USER | VPN control (all 5 routes) | | `/api/peertube/channels/stats` | GET | PUBLIC | Channel stats | | `/api/peertube/channels` | GET | PUBLIC | Channel listing | | `/api/peertube/channels/add` | POST | AUTHED-USER | Add channel | | `/api/peertube/channels/` | DELETE | AUTHED-USER | Remove channel | | `/api/peertube/dashboard` | GET | PUBLIC | Cached dashboard | | `/api/kiwix/sources` | GET | PUBLIC | ZIM source listing | | `/api/kiwix/toggle-ingest/` | POST | AUTHED-USER | Toggle ingest | | `/api/kiwix/trigger-ingest/` | POST | AUTHED-USER | Trigger ingest | | `/api/kiwix/upload` | POST | AUTHED-USER | ZIM upload (disk usage) | | `/api/kiwix/remove/` | POST | AUTHED-USER | Remove ZIM | | `/api/scraper/submit` | POST | AUTHED-USER | Submit scrape job | | `/api/scraper/jobs` | GET | PUBLIC | Job listing | | `/api/scraper/cancel/` | POST | AUTHED-USER | Cancel job | | `/api/scraper/retry/` | POST | AUTHED-USER | Retry job | | `/api/scraper/delete/` | POST | AUTHED-USER | Delete job | | `/api/scraper/clear-failed` | POST | AUTHED-USER | Clear failed jobs | | `/api/metrics/history` | GET | PUBLIC | Metrics history | ### 2.2 Contacts API (contacts_api.py) — ALL AUTHED-USER All routes use `@require_auth` decorator. Per-user phone book. | Route | Method | Classification | |-------|--------|----------------| | `/api/contacts` | GET | AUTHED-USER | | `/api/contacts` | POST | AUTHED-USER | | `/api/contacts/nearby` | GET | AUTHED-USER | | `/api/contacts/deleted` | GET | AUTHED-USER | | `/api/contacts/` | GET | AUTHED-USER | | `/api/contacts/` | PATCH | AUTHED-USER | | `/api/contacts/` | DELETE | AUTHED-USER | | `/api/contacts//restore` | POST | AUTHED-USER | | `/api/contacts//restore-as` | POST | AUTHED-USER | | `/api/contacts//purge` | DELETE | AUTHED-USER | ### 2.3 Address Book API (address_book_api.py) — ALL PUBLIC Static address lookups from YAML. No auth required. | Route | Method | Classification | |-------|--------|----------------| | `/api/address_book/lookup` | GET | PUBLIC | | `/api/address_book/list` | GET | PUBLIC | ### 2.4 Geocode API (netsyms_api.py) — ALL PUBLIC Local Photon geocoding and Netsyms address lookup. | Route | Method | Classification | |-------|--------|----------------| | `/api/netsyms/lookup` | GET | PUBLIC | | `/api/netsyms/health` | GET | PUBLIC | | `/api/geocode` | GET | PUBLIC | | `/api/reverse` | GET | PUBLIC | ### 2.5 Routes NOT on RECON Backend The following routes mentioned in the original spec do not exist on RECON: - **`/api/photon/*`** — Photon is accessed via `/api/geocode` and `/api/reverse` - **`/api/nominatim/*`** — Nominatim is accessed via `/api/place` internally - **`/api/valhalla/*`** — Valhalla is called directly by Aurora (cortex), not proxied through RECON - **`/api/overture/*`** — Overture is an enrichment layer inside `place_detail.py`, not a direct route - **`/api/padus/*`** — PAD-US is accessed via `/api/landclass` - **`/api/aurora/*`** — Aurora runs on Open WebUI (cortex), not on RECON --- ## 3. Current Auth Architecture ### 3.1 Authentik Forward Auth (CT 101 Caddy) **Current state:** Unable to read Caddy config from CT 101 (SSH access issue during doc creation). **Expected configuration:** - `navi.echo6.co` domain likely uses `forward_auth` directive to Authentik - All paths currently gated at Caddy level - Authentik application name: likely "navi" or "recon" ### 3.2 X-Authentik-Username Header The backend checks this header in two places: **auth.py:** ```python def get_user_id(): """Return X-Authentik-Username or None.""" return request.headers.get('X-Authentik-Username') def require_auth(f): """Decorator: 401 if no Authentik auth header.""" @wraps(f) def wrapper(*args, **kwargs): user_id = get_user_id() if not user_id: return jsonify({'error': 'Authentication required'}), 401 request.user_id = user_id return f(*args, **kwargs) return wrapper ``` **Usage:** - `contacts_api.py` — ALL routes use `@require_auth` - `api.py` — `get_user_id()` called in `/deleted-contacts` and `/nav-i` page handlers, but not enforced (returns "anonymous" if missing) ### 3.3 Public Exception: recon-watchdog The monitoring system uses `localhost:8420` to bypass Caddy/Authentik entirely when polling health endpoints. --- ## 4. Proposed Caddy Changes ### 4.1 Path-Based Forward Auth Split ```caddyfile navi.echo6.co { # Static frontend assets — PUBLIC handle /assets/* { reverse_proxy 192.168.1.130:8420 } # Public API routes — no auth required handle_path /api/geocode* { reverse_proxy 192.168.1.130:8420 } handle_path /api/reverse* { reverse_proxy 192.168.1.130:8420 } handle_path /api/address_book/* { reverse_proxy 192.168.1.130:8420 } handle_path /api/netsyms/* { reverse_proxy 192.168.1.130:8420 } handle_path /api/place/* { reverse_proxy 192.168.1.130:8420 } handle_path /api/landclass* { reverse_proxy 192.168.1.130:8420 } handle_path /api/config* { reverse_proxy 192.168.1.130:8420 } handle_path /api/health* { reverse_proxy 192.168.1.130:8420 } handle_path /api/kiwix/sources* { reverse_proxy 192.168.1.130:8420 } # Auth-required routes — forward_auth first handle /api/contacts/* { forward_auth { uri /outpost.goauthentik.io/auth/caddy copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email } reverse_proxy 192.168.1.130:8420 } handle /api/keys/* { forward_auth { uri /outpost.goauthentik.io/auth/caddy copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email } reverse_proxy 192.168.1.130:8420 } handle /api/nav-i/* { forward_auth { uri /outpost.goauthentik.io/auth/caddy copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email } reverse_proxy 192.168.1.130:8420 } handle /api/traffic/* { forward_auth { uri /outpost.goauthentik.io/auth/caddy copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email } reverse_proxy 192.168.1.130:8420 } # ... additional auth-required paths ... # Default: public (frontend pages) handle { reverse_proxy 192.168.1.130:8420 } } ``` ### 4.2 Alternative: Path Matchers For cleaner config, use path matchers: ```caddyfile @public_api path /api/geocode* /api/reverse* /api/address_book/* /api/netsyms/* /api/place/* /api/landclass* /api/config* /api/health* /api/kiwix/sources* @authed_api path /api/contacts/* /api/keys/* /api/nav-i/* /api/traffic/* /api/upload /api/ingest* /api/crawl /api/service/* /api/cookies/* /api/vpn/* ``` --- ## 5. Proposed RECON Backend Changes ### 5.1 Routes Needing Explicit Auth Checks Currently these routes do not enforce auth but should: | Route | Current State | Required Change | |-------|---------------|-----------------| | `/api/upload` POST | No check | Add `@require_auth` | | `/api/ingest-url` POST | No check | Add `@require_auth` | | `/api/ingest-urls` POST | No check | Add `@require_auth` | | `/api/crawl` POST | No check | Add `@require_auth` | | `/api/retry-all` POST | No check | Add `@require_auth` | | `/api/retry/` POST | No check | Add `@require_auth` | | `/api/ingest` POST | No check | Add `@require_auth` | | `/api/service/restart` POST | No check | Add `@require_auth` | | `/api/keys/*` | No check | Add `@require_auth` | | `/api/nav-i/api-keys/*` | No check | Add `@require_auth` | | `/api/cookies/*` | No check | Add `@require_auth` | | `/api/vpn/*` | No check | Add `@require_auth` | | `/api/peertube/channels/add` POST | No check | Add `@require_auth` | | `/api/peertube/channels/` DELETE | No check | Add `@require_auth` | | `/api/kiwix/toggle-ingest/*` POST | No check | Add `@require_auth` | | `/api/kiwix/trigger-ingest/*` POST | No check | Add `@require_auth` | | `/api/kiwix/upload` POST | No check | Add `@require_auth` | | `/api/kiwix/remove/*` POST | No check | Add `@require_auth` | | `/api/scraper/submit` POST | No check | Add `@require_auth` | | `/api/scraper/cancel/*` POST | No check | Add `@require_auth` | | `/api/scraper/retry/*` POST | No check | Add `@require_auth` | | `/api/scraper/delete/*` POST | No check | Add `@require_auth` | | `/api/scraper/clear-failed` POST | No check | Add `@require_auth` | | `/api/traffic/flow/*` | No check | Add `@require_auth` | ### 5.2 Implementation Pattern For routes that currently assume auth, make explicit: ```python from .auth import require_auth @app.route('/api/upload', methods=['POST']) @require_auth def api_upload(): # request.user_id now guaranteed to be set ... ``` ### 5.3 Routes That Can Remain Public These need no changes — no user state, no cost: - `/api/geocode`, `/api/reverse` (Photon — local) - `/api/address_book/*` (YAML lookup — local) - `/api/netsyms/*` (SQLite — local) - `/api/place/*` (Nominatim/Overpass — local/free, but see Section 8) - `/api/landclass` (DuckDB — local) - `/api/config` (static config) - `/api/health` (health check) - `/api/kiwix/sources` (read-only listing) - `/api/search` (local embedding + Qdrant) - `/api/knowledge-stats`, `/api/quick-stats` (cached stats) - `/api/peertube/dashboard`, `/api/peertube/stats`, `/api/peertube/channels` GET (read-only) - `/api/scraper/jobs` GET (read-only listing) - `/api/metrics/history` (read-only) --- ## 6. Frontend Auth-Aware UX ### 6.1 Options Considered | Option | Description | Pros | Cons | |--------|-------------|------|------| | **A. Inline auth modal** | Intercept 401, show login modal, retry | Smooth UX, no context loss | Complex state management | | **B. Redirect to Authentik** | On 401, redirect to `/outpost.goauthentik.io/start` | Simple, Authentik handles flow | Loses frontend context/state | | **C. Visually gate features** | Hide/disable authed features until logged in | Prevents 401 entirely | Requires knowing auth state upfront | ### 6.2 Recommendation: Option C (Visually Gate) + B (Fallback) **Primary:** Check auth state on page load via presence of `X-Authentik-Username` cookie or a `/api/whoami` endpoint. Show authed features only when logged in. **Fallback:** If a 401 occurs unexpectedly, redirect to Authentik with return URL. **Implementation sketch:** ```javascript // On app init async function checkAuth() { const resp = await fetch('/api/whoami'); // new endpoint if (resp.ok) { const { username, groups } = await resp.json(); window.authState = { authenticated: true, username, groups }; } else { window.authState = { authenticated: false }; } renderAuthAwareUI(); } ``` **New backend endpoint:** ```python @app.route('/api/whoami') def api_whoami(): user_id = get_user_id() if not user_id: return jsonify({'authenticated': False}), 200 return jsonify({ 'authenticated': True, 'username': user_id, 'groups': request.headers.get('X-Authentik-Groups', '').split(','), }) ``` ### 6.3 UI Patterns - **Contacts panel:** Hidden entirely when unauthenticated - **API keys button:** Hidden when unauthenticated - **Traffic layer toggle:** Hidden or shows "Login for traffic data" - **Upload button:** Shows "Login to upload" when clicked unauthenticated --- ## 7. Rate Limiting Plan ### 7.1 Caddy Rate Limit Configuration Use `rate_limit` directive for public endpoints: ```caddyfile @public_api path /api/geocode* /api/reverse* /api/place/* ... handle @public_api { rate_limit { zone geocode { key {remote_host} events 60 window 60s } } reverse_proxy 192.168.1.130:8420 } ``` ### 7.2 Proposed Thresholds | Endpoint Group | Requests/min | Burst | Rationale | |----------------|--------------|-------|-----------| | `/api/geocode`, `/api/reverse` | 60/min | 10 | Typical map interaction | | `/api/place/*` | 30/min | 5 | Heavy backend processing | | `/api/landclass` | 30/min | 5 | DuckDB query | | `/api/search` | 20/min | 3 | Embedding + vector search | | `/api/kiwix/sources` | 10/min | 2 | Cached, rarely changes | | `/api/address_book/*` | 120/min | 20 | Very cheap lookup | ### 7.3 429 Response Shape ```json { "error": "Rate limit exceeded", "retry_after": 45, "limit": "60/min" } ``` Include `Retry-After` header. --- ## 8. Mixed-Route Handling: /api/place ### 8.1 Current Behavior `/api/place//` performs: 1. Local Nominatim lookup (free) 2. Local Overture enrichment (free) 3. **Google Places enrichment** (paid) — fills phone, website, hours for business POIs ### 8.2 Options Considered | Option | Description | Pros | Cons | |--------|-------------|------|------| | **A. Return partial, flag missing** | Return OSM+Overture data, add `enrichment_available: "google"` flag | Clean public API | Frontend complexity | | **B. Split routes** | `/api/place/basic/...` and `/api/place/enriched/...` | Clear API contract | Breaking change | | **C. Auth-conditional enrichment** | Check `X-Authentik-Username`, skip Google if missing | Backward compatible | Inconsistent response shape | ### 8.3 Recommendation: Option C (Auth-Conditional) **Implementation:** ```python # In place_detail.py def _enrich_with_google(result, osm_type, osm_id): # Check auth header from flask import request user_id = request.headers.get('X-Authentik-Username') if not user_id: # Add metadata indicating what is missing result.setdefault('sources', {})['google_places'] = { 'status': 'auth_required', 'reason': 'Google Places enrichment requires authentication' } return result # ... existing enrichment logic ... ``` **Response shape (unauthenticated):** ```json { "osm_type": "N", "osm_id": 12345, "name": "Boise Coffee House", "sources": { "primary": "nominatim_local", "enrichment": "overture", "google_places": { "status": "auth_required", "reason": "Google Places enrichment requires authentication" } } } ``` **Response shape (authenticated):** ```json { "osm_type": "N", "osm_id": 12345, "name": "Boise Coffee House", "extratags": { "phone": "+1-208-555-0123", "opening_hours": "Mo-Fr 06:00-18:00" }, "sources": { "primary": "nominatim_local", "enrichment": "overture", "google_places": { "place_id": "ChIJ...", "source": "api" } } } ``` --- ## 9. Aurora Handling ### 9.1 Current Architecture Aurora runs on Open WebUI (cortex), NOT on the RECON backend. It is accessed via: - `aurora.echo6.co` (web interface) - Mesh bridge integration (future) Aurora calls RECON APIs (geocode, search) but is not exposed through navi.echo6.co. ### 9.2 Classification **AUTHED** — Aurora is compute-expensive (LLM inference on cortex GPU). ### 9.3 Future Consideration: Aurora-Lite Out of scope for this design, but noted for future: > Rate-limited "Aurora-lite" public endpoint with tight token budget per IP. > Could expose `/api/aurora/ask` with: > - 5 requests/hour/IP > - Max 100 input tokens > - Predefined safe prompts only (directions, place info) --- ## 10. Migration Plan ### Phase 1: Backend Auth Enforcement **Risk:** Low **Rollback:** Revert `@require_auth` decorators 1. Add `@require_auth` to all identified routes (Section 5.1) 2. Add `/api/whoami` endpoint 3. Test with existing Caddy config (all routes still gated) 4. Deploy to VM 130 5. Verify Authentik headers flow correctly ### Phase 2: Caddy Public Exceptions **Risk:** Medium **Rollback:** Remove path exceptions from Caddy config 1. Update CT 101 Caddyfile with public path exceptions 2. Keep sensitive paths behind forward_auth 3. Test unauthenticated access to public endpoints 4. Verify authenticated endpoints still require login ### Phase 3: Frontend Auth-Aware UI **Risk:** Low **Rollback:** N/A (additive change) 1. Implement `checkAuth()` on frontend 2. Conditionally render authed features 3. Add 401 handling with Authentik redirect 4. Deploy frontend changes ### Phase 4: Rate Limiting **Risk:** Low **Rollback:** Remove rate_limit directives 1. Add rate_limit zones to Caddy config 2. Start with high limits, monitor 3. Tune thresholds based on traffic patterns 4. Add monitoring/alerting for 429s ### Phase 5: DNS/CDN Consideration **Risk:** Low **Rollback:** DNS change 1. Evaluate Cloudflare for static asset caching 2. Consider geographic distribution for tile proxying 3. Out of scope for this design — separate decision --- ## 11. Open Questions ### Q1: Caddy Configuration Access **Issue:** Unable to SSH to CT 101 to read current Caddyfile during doc creation. **Action needed:** User to provide current navi.echo6.co Caddy block or SSH access path. ### Q2: TomTom Traffic Tile Cost Model **Issue:** Unknown per-tile cost for traffic API. **Decision needed:** Is auth + rate limiting sufficient, or should traffic be entirely disabled for public users? ### Q3: Knowledge Dashboard Admin Actions **Issue:** Several routes like `/api/retry-all` are admin-only but do not have role checks. **Decision needed:** Is `@require_auth` sufficient, or should we add `@require_admin` that checks `X-Authentik-Groups`? ### Q4: Upload Size Limits **Issue:** `/api/upload` and `/api/kiwix/upload` accept large files. **Decision needed:** Should public users (if upload ever becomes public) have stricter size limits than authenticated users? ### Q5: Existing Session State **Issue:** What happens to in-progress operations (crawls, scraper jobs) when auth is enforced? **Decision needed:** Grandfather existing jobs or require re-auth? ### Q6: Rate Limit Per-User vs Per-IP **Issue:** Authenticated users could get higher limits. **Decision needed:** Implement tiered rate limiting based on auth state? --- ## Appendix A: Route Summary by Classification ### PUBLIC (28 routes) - `/api/geocode`, `/api/reverse`, `/api/address_book/*`, `/api/netsyms/*` - `/api/place/*` (basic), `/api/landclass`, `/api/config`, `/api/health` - `/api/kiwix/sources`, `/api/search`, `/api/knowledge-stats`, `/api/quick-stats` - `/api/peertube/dashboard`, `/api/peertube/stats`, `/api/peertube/channels` GET - `/api/scraper/jobs`, `/api/metrics/history`, `/api/status` - `/api/upload//status`, `/api/upload/categories` - `/api/crawl//status`, `/api/ingest-peertube//status` - All page routes except settings/nav-i ### AUTHED-USER (35 routes) - `/api/contacts/*` (10 routes) - `/api/keys/*` (7 routes) - `/api/nav-i/*` (4 routes) - `/api/cookies/*` (2 routes) - `/api/vpn/*` (5 routes) - `/api/upload`, `/api/ingest-*`, `/api/crawl` POST - `/api/peertube/channels/add`, `/api/peertube/channels/` DELETE - `/api/kiwix/toggle-ingest/*`, `/api/kiwix/trigger-ingest/*`, `/api/kiwix/upload`, `/api/kiwix/remove/*` - `/api/scraper/submit`, `/api/scraper/cancel/*`, `/api/scraper/retry/*`, `/api/scraper/delete/*`, `/api/scraper/clear-failed` - `/api/service/restart`, `/api/retry-all`, `/api/retry/`, `/api/ingest` - Settings pages, Nav-I pages ### AUTHED-COST (1 route) - `/api/traffic/flow/*` — TomTom paid API ### MIXED (1 route) - `/api/place/*` — Returns OSM+Overture freely, Google enrichment requires auth --- ## Appendix B: Files to Modify ### Backend (VM 130) - `/opt/recon/lib/api.py` — Add `@require_auth` to 25+ routes - `/opt/recon/lib/place_detail.py` — Auth-conditional Google enrichment - `/opt/recon/lib/auth.py` — No changes needed (already has `require_auth`) ### Caddy (CT 101) - `/etc/caddy/Caddyfile` — Path-based forward_auth split + rate limiting ### Frontend (TBD) - Auth state detection - Conditional feature rendering - 401 handling --- ## Appendix C: Actual Caddy Configuration (2026-04-25) **SSH Access Path:** `ssh -J cortex root@192.168.1.101` or from cortex: `ssh ct101` ### C.1 navi.echo6.co — Current Config ```caddyfile navi.echo6.co { tls /etc/caddy/certs/navi.echo6.co.fullchain.crt /etc/caddy/certs/navi.echo6.co.key # Tiles: public, no auth, no encoding (PMTiles needs raw range responses) handle /tiles/* { reverse_proxy 100.64.0.24:8440 } # Everything else: Authentik forward auth handle { forward_auth https://auth.echo6.co { uri /outpost.goauthentik.io/auth/caddy copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Name X-Authentik-Uid header_up Host auth.echo6.co trusted_proxies private_ranges } reverse_proxy 100.64.0.24:8440 } } ``` **Key Observations:** - **Port 8440** — Navi uses a SEPARATE service from RECON (8420) - **`/tiles/*` already public** — Map tiles bypass auth - **All other paths gated** — Full forward_auth to Authentik - **Headers copied:** Username, Groups, Email, Name, Uid ### C.2 recon.echo6.co — Current Config ```caddyfile recon.echo6.co { tls /etc/caddy/certs/recon.echo6.co.fullchain.crt /etc/caddy/certs/recon.echo6.co.key forward_auth https://auth.echo6.co { uri /outpost.goauthentik.io/auth/caddy copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Name X-Authentik-Uid header_up Host auth.echo6.co trusted_proxies private_ranges } reverse_proxy 100.64.0.24:8420 } ``` **Key Observations:** - **Port 8420** — RECON dashboard/API - **Full forward_auth** — No public exceptions - **Same Authentik headers** as navi ### C.3 wiki.echo6.co — Current Config (for reference) ```caddyfile wiki.echo6.co { tls /etc/caddy/certs/wiki.echo6.co.fullchain.crt /etc/caddy/certs/wiki.echo6.co.key forward_auth https://auth.echo6.co { uri /outpost.goauthentik.io/auth/caddy copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Name X-Authentik-Uid header_up Host auth.echo6.co trusted_proxies private_ranges } reverse_proxy 100.64.0.24:8430 { header_down -Content-Security-Policy } } ``` ### C.4 Implications for Design 1. **Port Correction:** The design doc Section 4 needs to use port **8440** for navi.echo6.co, not 8420. 2. **Existing Public Path:** `/tiles/*` is already public — can use same pattern for API routes. 3. **Separate Services:** navi.echo6.co (8440) and recon.echo6.co (8420) are different backends: - If navi frontend needs RECON API routes, those would need to be either: a. Exposed on 8440 as well, OR b. Proxied from navi to recon internally, OR c. Frontend calls recon.echo6.co directly (cross-origin) 4. **Updated Caddy Proposal:** To add public API routes to navi.echo6.co: ```caddyfile navi.echo6.co { tls /etc/caddy/certs/navi.echo6.co.fullchain.crt /etc/caddy/certs/navi.echo6.co.key # Tiles: public (existing) handle /tiles/* { reverse_proxy 100.64.0.24:8440 } # Public API routes - geocoding, place lookup, etc. @public_api path /api/geocode* /api/reverse* /api/address_book/* /api/netsyms/* /api/place/* /api/landclass* /api/config* /api/health* /api/kiwix/sources* /api/search /api/whoami handle @public_api { reverse_proxy 100.64.0.24:8440 } # Auth-required API routes @authed_api path /api/contacts/* /api/keys/* /api/nav-i/* /api/traffic/* /api/upload* /api/ingest* /api/crawl* /api/service/* /api/cookies/* /api/vpn/* /api/peertube/channels/add /api/kiwix/toggle* /api/kiwix/trigger* /api/kiwix/upload /api/kiwix/remove* /api/scraper/submit /api/scraper/cancel/* /api/scraper/retry/* /api/scraper/delete/* /api/scraper/clear* handle @authed_api { forward_auth https://auth.echo6.co { uri /outpost.goauthentik.io/auth/caddy copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Name X-Authentik-Uid header_up Host auth.echo6.co trusted_proxies private_ranges } reverse_proxy 100.64.0.24:8440 } # Default: auth for everything else (pages, etc.) handle { forward_auth https://auth.echo6.co { uri /outpost.goauthentik.io/auth/caddy copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Name X-Authentik-Uid header_up Host auth.echo6.co trusted_proxies private_ranges } reverse_proxy 100.64.0.24:8440 } } ``` ### C.5 Open Question Resolved **Q1 from Section 11 is now answered:** - SSH path: `ssh -J cortex root@192.168.1.101` (or `ssh ct101` from cortex) - CT 101 IP: 192.168.1.101 (local) / 100.64.0.8 (Tailscale) - Caddy config location: `/etc/caddy/Caddyfile` - Keys authorized: cortex, toc