Documentation
Getting Started
Documentation Index Installation Details Quickstart
Architecture
Alert Geometry Coverage Data Flow Sequences Design Standards Display System Architecture Eas Decoding Summary Eas Monitor V3 Architecture Sdr Service Architecture System Architecture Theory Of Operation
Development
Agents Certification Reliability Plan Component Library Contributing Fcc Part11 Compliance Matrix Javascript Api Large File Refactor Plan Navigation Releasing Sdr Frequency Validation User Interface Guide
Hardware
Alpha Led Guide Argon40 Zigbee Setup Gpio Guide Gps Hat Setup Hwsetup Helper Neopixel Led Control Sdr Setup Serial To Ethernet Adapters Vfd Display Setup Waveshare Rs232 Wifi Setup
Guides
Alert Signals Analytics And Reporting Api Key Management Application Settings Audio Monitoring Audit Log Review Database Backups Disk Space Cleanup Eas Config Tool Eas Test Signal Pipeline Gated Alerts Hardware Quickstart Health Monitoring Help Https Setup Icecast Streaming Setup Ipaws Feed Integration Local Authorities Local Mail Server Manual Eas Events Mfa Totp Setup Notifications One Button Upgrade Setup Instructions Smart Setup Ssl Web Ui Guide Tailscale Setup Tts Normalization
Troubleshooting
Audio Sdr Fix Tool Firewall Requirements Polling Not Working Sdr Master Troubleshooting Guide Troubleshooting 504 Timeout Tts Troubleshooting
Security
Audit Log Integrity Public Routes Security
Reference
About Alpha M Protocol Changelog Dependency Attribution Diagrams Ecig Cap To Eas Implementation Guide V1 0 Fips Data Sources Mdc1200 Nrsc4B Same Standard Nws Alert Parameters Nws Zone Catalog Ohio Eas Documentation Overview Protocols Overview Rbds Standard Repository Statistics Sage Endec Same Vtec Event Linking
Policies
Privacy Policy Sms Messaging Terms Of Use Trademark Policy

Public vs. Authenticated Routes

EAS Station denies by default: before_request in app.py redirects any anonymous request to /login unless the path is explicitly allowed. This page records what is allowed, why, and how to decide where a new route belongs.

A station published at a public hostname (easstation.com, a Tailscale funnel, a port-forward) serves these routes to the whole internet. Treat this list as the perimeter.


The three tiers

Tier Who can read it Defined in app.py
Public Anyone, from anywhere _PUBLIC_PAGE_PATHS, _PUBLIC_PAGE_PREFIXES, PUBLIC_API_GET_PATHS
Local Anyone on the appliance or its LAN, no session needed LOCAL_API_GET_PATHS
Authenticated Signed-in users, subject to RBAC everything else (the default)

Signing in grants access to all three from anywhere. The local tier only removes the anonymous-from-the-internet path.


Tier 1 — Public

Pages

/ · /about · /help · /help/version · /terms · /privacy · /sms-compliance · /support · /version · /repo-stats · /attribution · /style-guide · /docs and everything under /docs/ (viewer, assets, search) · /login · /logout · /mfa/verify · /setup… · /static/… · /sitemap.xml · /robots.txt · /favicon.ico · /ping · /health · /health/dependencies

Documentation is never gated. The /docs tree, the help and about pages, the legal pages, and /attribution (AGPL-3.0 and third-party licence disclosures) must be readable without an account — a licence notice behind a password is not a licence notice. /style-guide is the UI component reference the developer docs link to; it is static markup with no station data.

APIs (GET only)

Endpoint Why it is public
/api/alerts, /api/alerts/historical Emergency alert content — the point of the station
/api/boundaries Map geometry the public landing page draws
/api/broadcast/state Polled by base.html/navbar on every page including /login; active flag + count only
/api/health Liveness probe for external monitoring
/api/release-manifest Version shown on the public /version page
/api/traffic/client Visitor screen-resolution beacon

Non-GET methods are never public, even on these paths.


Tier 2 — Local network only

These describe the machine, not the alerting service, and nothing on a public hostname should serve them anonymously:

Endpoint Exposes
/api/smart_diag Raw smartctl output — drive models, serial numbers, firmware, temperatures, power-on hours, error logs; lsblk topology
/api/system_status Hostname, primary IP address, CPU/memory/disk utilisation, uptime
/api/system_health Service and dependency health detail
/api/monitoring/radio Receiver/SDR state
/api/eas-monitor/status Decoder state
/api/audio/metrics, /api/audio/metrics/latest, /api/audio/health, /api/audio/sources Audio hardware and source configuration

They stay unauthenticated for local callers because scripts/screen_renderer.ScreenRenderer — used by the displays subsystem to populate OLED/LED/VFD screens — fetches them from http://localhost:5000 with no session. Gating them outright would blank those screens.

_is_local_network_client() accepts loopback, RFC1918/RFC4193 private ranges, link-local and CGNAT, and fails closed on a missing or malformed address. request.remote_addr is the real client IP because ProxyFix(x_for=1) trusts exactly one hop (our own nginx), so a remote caller cannot claim to be local by sending its own X-Forwarded-For.


Tier 3 — Authenticated (the default)

Everything else: /admin/…, /security/…, /settings/…, /logs, /alerts, /audio…, /eas/…, /analytics, /stats, /diagnostics, /displays, /screens, /export/…, /debug/…, /rwt-schedule, /system_health, /navigation, /search, and every non-GET API.

Most also carry a @require_permission(...) decorator, so RBAC narrows access further for signed-in users.

One deliberate exception: on a system with no administrator account yet, /admin and /admin/users answer anonymously so the first admin can be created. That exemption is guarded by g.admin_setup_mode (AdminUser.query.count() == 0) and closes permanently once an account exists.


Adding a route

Ask what the response reveals:

  • Alert content, legal text, documentation, or a version string? Public.
  • Anything about this machine — hostnames, addresses, hardware, disks, service internals, configuration? Local tier at most; authenticated if no on-box consumer needs it.
  • Anything about operations — logs, audit trails, recordings, settings, station identity? Authenticated, with a @require_permission.
  • Any method other than GET? Authenticated. No exceptions.

When in doubt, leave it authenticated: the default is safe, and a missing entry shows up immediately as a redirect to /login.

Re-running the audit

tests/test_public_route_audit.py enforces the invariants above and fails if a documentation route is gated or a machine-describing endpoint becomes internet-public. To review the full picture:

python - <<'PY'
import app as appmod
pub, prefixes = appmod._PUBLIC_PAGE_PATHS, appmod._PUBLIC_PAGE_PREFIXES
for rule in sorted(appmod.app.url_map.iter_rules(), key=str):
    path = str(rule)
    if path in pub or any(path.startswith(p) for p in prefixes):
        tier = "PUBLIC"
    elif path.rstrip('/') in appmod.PUBLIC_API_GET_PATHS:
        tier = "PUBLIC-API"
    elif path.rstrip('/') in appmod.LOCAL_API_GET_PATHS:
        tier = "LOCAL"
    else:
        tier = "AUTH"
    print(f"{tier:<11}{path}")
PY

To confirm the perimeter on a running station, request an endpoint from off-box and expect 401:

curl -si https://<your-station>/api/smart_diag | head -1   # HTTP/1.1 401
curl -si https://<your-station>/attribution   | head -1    # HTTP/1.1 200

This document is served from docs/security/PUBLIC_ROUTES.md.md in the EAS Station™ installation.