TALOS Ground Station Network -- Security Review¶
Date: 2026-04-01 Scope: core/ (FastAPI web API), director/ (mission director), agent/ (edge client), ops/ (Docker Compose orchestration) Classification: Internal -- Pre-Production Assessment
Executive Summary¶
The TALOS system in its current state has no effective security boundary between the public internet and physical ground station hardware. An unauthenticated attacker on the same network -- or any network with access to the MQTT broker -- can take full control of antenna rotators, retune receivers, and disrupt satellite tracking operations. The authentication system is trivially bypassable, secrets are hardcoded in source, and the MQTT message bus operates with anonymous access over unencrypted channels.
This review identifies 7 critical, 6 high, and 8 medium severity findings. None of the critical findings require any prior access or credentials to exploit.
1. Critical Vulnerabilities (Exploit-Ready)¶
CRIT-01: Anonymous MQTT Broker Grants Full Command Authority¶
Location: ops/mosquitto/config/mosquitto.conf lines 7-11; agent/agent.py line 31
The Mosquitto broker is configured with allow_anonymous true on both the TCP listener (port 1883) and the WebSocket listener (port 9001). The agent subscribes to talos/gs/{STATION_ID}/cmd/# and executes every command it receives without verifying the sender.
Impact: Any client that can reach port 1883 or 9001 can publish to talos/gs/+/cmd/rot and slew antenna rotators to arbitrary positions, publish to talos/gs/+/cmd/rig to retune receivers, or publish to talos/gs/+/cmd/session to start/stop tracking sessions. This is a direct path from network access to physical hardware control.
Proof of concept: A single mosquitto_pub command moves hardware:
Fix: Enable MQTT authentication with per-client credentials. Use TLS (port 8883). Implement a topic-level ACL so agents can only subscribe to their own station prefix and the director is the sole publisher to cmd/# topics.
CRIT-02: Unsigned Cookie Authentication -- Trivial Session Forgery¶
Location: core/main.py lines 98-101, 124
The get_current_user function reads the user identity directly from a plain-text cookie named session_user. The /auth/verify endpoint sets this cookie with resp.set_cookie("session_user", email) -- no signing, no encryption, no HttpOnly flag, no Secure flag, no SameSite attribute.
Impact: Any user can set document.cookie = "session_user=admin@talos.io" in their browser console and immediately authenticate as any user in the system.
Fix: Use signed session tokens (e.g., itsdangerous or JWT-based session cookies). Set httponly=True, secure=True, samesite="Lax" on all session cookies. Validate the token server-side on every request.
CRIT-03: Hardcoded JWT Secret Key¶
Location: core/main.py line 18
This secret is committed to version control and is identical across all deployments. Anyone with read access to the repository can forge valid magic-link JWT tokens for any email address.
Impact: Complete authentication bypass. An attacker can generate a valid token offline and hit /auth/verify?token=<forged_token> to authenticate as any user.
Fix: Generate the secret from a cryptographically secure source (secrets.token_urlsafe(64)). Load it from an environment variable or a secrets manager. Rotate on every deployment. Never commit secrets to source control.
CRIT-04: Browser-Side MQTT Enables Direct Hardware Control from JavaScript¶
Location: core/templates/dashboard.html lines 210, 237, 259
The dashboard opens a Paho MQTT WebSocket connection directly from the browser to port 9001 and subscribes to talos/#. The updateTransmitter function on line 259 publishes directly to talos/mission/select. Because the broker has no authentication, any JavaScript running in any browser tab on the same network can publish commands.
Impact: A cross-site scripting payload, a malicious browser extension, or simply opening the browser console on the dashboard page gives full publish access to every MQTT topic, including hardware commands.
Fix: Remove direct browser-to-MQTT publishing. Route all commands through authenticated API endpoints. If real-time push to the browser is needed, use server-sent events or a WebSocket proxy that authenticates sessions.
CRIT-05: Database Credentials Hardcoded and Exposed¶
Location: ops/docker-compose.yml lines 9, 42, 62; core/database.py line 9
The PostgreSQL password talos_password appears in four places in plain text. The database port 5432 is published to the host network, making it reachable from any machine on the LAN.
Impact: Direct database access. An attacker can connect with psql -h <host> -U talos -d talos_core using the known password, read all station API keys, modify mission parameters, or drop tables.
Fix: Use Docker secrets or an .env file excluded from version control. Stop exposing port 5432 to the host; let containers communicate over the internal Docker network only. Use a unique, generated password per environment.
CRIT-06: No Authorization Model -- Any User Controls Any Station¶
Location: core/main.py lines 139-142, 244-254
The /system/sync, /missions/add, /missions/{mid}/activate, and several other endpoints have no authentication check at all (no Depends(get_current_user)). Endpoints that do check authentication perform no authorization -- there is no verification that a user owns a station before issuing commands, and any authenticated user can activate any mission for the entire network.
Impact: A single compromised or malicious account can redirect every ground station in the network to track an arbitrary satellite, disrupting all operations.
Fix: Implement role-based access control (RBAC). At minimum: operator (own stations only), mission controller (can activate missions), admin (full access). Enforce ownership checks on every station and mission endpoint.
CRIT-07: SQL Injection via Satellite Search¶
Location: core/main.py line 137
session.exec(select(SatelliteCache).where(col(SatelliteCache.name).ilike(f"%{q}%")).limit(15)).all()
The search parameter q is interpolated directly into the ilike pattern. While SQLModel/SQLAlchemy parameterize the outer query, the % wildcards around q mean a user can inject LIKE pattern metacharacters. More critically, this endpoint has no authentication -- it is completely public.
SQLAlchemy's ilike does use parameterized queries for the value itself, so classical SQL injection (with quotes/semicolons) is mitigated by the ORM. However, the endpoint being unauthenticated and unrate-limited still allows enumeration of the entire satellite database.
Revised severity: High (not full SQL injection, but unauthenticated data enumeration with no rate limiting).
2. High-Risk Issues¶
HIGH-01: No TLS on MQTT Connections¶
Location: ops/mosquitto/config/mosquitto.conf, director/mission_director.py line 178, agent/agent.py line 71
All MQTT traffic flows over plain TCP on port 1883. Commands to slew rotators, frequency tuning data, and telemetry are all transmitted in cleartext.
Impact: Any network observer (passive Wi-Fi sniffing, compromised switch, ISP-level interception) can read all commands and telemetry. Active MITM can inject or modify commands in transit.
Fix: Configure Mosquitto with TLS certificates. Require ssl on all client connections. Use Let's Encrypt or an internal CA for certificate management.
HIGH-02: Agent API Key Never Validated¶
Location: agent/agent.py lines 6-7; director/mission_director.py lines 159-172
The agent accepts --key as a CLI argument but never sends it to the broker or the API for validation. The director's handshake handler on line 163 looks up the station by ID alone -- the API key is never checked. The key exists in the database (Station.api_key) but is purely decorative.
Impact: The API key provides no actual security. Anyone who knows or guesses a station ID (which follows a predictable pattern like gs_station_name_xxxx) can impersonate that station.
Fix: Implement MQTT username/password authentication where the station ID is the username and the API key is the password. Validate credentials on the broker side using the Mosquitto password_file or a dynamic auth plugin.
HIGH-03: API Key Exposed in Process List¶
Location: core/main.py line 173
The provisioning endpoint returns a CLI command containing the API key, which will be visible in ps aux on any multi-user system where the agent runs. The key is passed as a command-line argument.
Fix: Pass the API key via environment variable or a configuration file with restricted permissions (chmod 600). Never expose secrets in command-line arguments.
HIGH-04: No CORS Configuration¶
Location: core/main.py -- FastAPI app has no CORSMiddleware
The API has no CORS policy, which means the browser's default same-origin policy applies. However, because the MQTT WebSocket on port 9001 has no origin restrictions and no authentication, CORS is irrelevant for the most dangerous attack vector. Any web page can open a WebSocket to the broker.
Fix: Add CORSMiddleware with an explicit allow_origins list. More importantly, secure the MQTT WebSocket with authentication and origin checking.
HIGH-05: No Rate Limiting on Any Endpoint¶
Location: All endpoints in core/main.py
There is no rate limiting on login attempts, satellite search, sync triggers, station provisioning, or mission activation.
Impact: Brute-force attacks on the login flow, denial of service via repeated sync triggers (which fetch external data), and automated mass provisioning of rogue stations.
Fix: Add rate limiting middleware (e.g., slowapi for FastAPI). Apply strict limits to /auth/login (5 attempts per minute per IP), /system/sync (1 per 5 minutes), and /stations/create (3 per hour per user).
HIGH-06: Raw TCP to Rotator Hardware with No Authentication¶
Location: agent/agent.py lines 13-20, 51-53
The agent opens a raw TCP socket to rotctld and sends commands directly. The rotctld address is received from the MQTT broker in the cmd/config message, meaning an attacker who controls the broker can redirect the agent to connect to any TCP endpoint.
Impact: An attacker can make the agent connect to a malicious server by publishing a spoofed config message. The agent will establish a TCP connection to the attacker-controlled host, potentially leaking information or enabling further exploitation.
Fix: Validate the rotator address against a local allowlist. Do not accept network addresses from the MQTT broker. Use a local configuration file for hardware addresses.
3. Medium-Risk Issues¶
MED-01: No CSRF Protection¶
Location: core/main.py -- all POST endpoints
No CSRF tokens are used on any form or API endpoint. The session cookie (plain email) will be sent automatically by the browser on cross-origin requests.
Fix: Implement CSRF tokens for all state-changing operations, or switch to token-based authentication (Bearer tokens in headers) which is inherently CSRF-resistant.
MED-02: No Content Security Policy Headers¶
Location: core/templates/dashboard.html, core/main.py
No CSP headers are set. The dashboard loads scripts from three external CDNs (Bootstrap, Leaflet, Paho MQTT). A compromised CDN or a supply-chain attack on any of these libraries would give an attacker full JavaScript execution in the dashboard context -- which includes MQTT publish capability.
Fix: Add a strict CSP header. Use Subresource Integrity (SRI) hashes on all external script tags. Consider self-hosting critical libraries.
MED-03: Magic Link Token Has No Expiration¶
Location: core/main.py line 115
The JWT has no exp claim. Once generated, a magic link token is valid forever.
Fix: Add an exp claim with a short TTL (e.g., 10 minutes). Validate expiration on the /auth/verify endpoint. Consider single-use tokens stored in the database.
MED-04: Auto-Registration on Login¶
Location: core/main.py lines 110-113
Any email address submitted to /auth/login automatically creates a user account. There is no allow-list, no domain restriction, and no approval workflow.
Fix: Implement an invitation-only registration flow, or restrict to a specific email domain. Require admin approval for new accounts.
MED-05: Sensitive Data in Server Logs¶
Location: core/main.py line 116
Authentication tokens are printed to stdout/container logs. In a Docker environment, these logs are accessible to anyone with docker logs access and may be shipped to centralized logging systems.
Fix: In production, send magic links via email or a secure side-channel. Never log authentication tokens.
MED-06: No Input Validation on Station Provisioning¶
Location: core/main.py lines 152-173
The network_id field is cast to int by Pydantic, but there is no validation of its range. The endpoint makes an external HTTP request to the SatNOGS API with the user-supplied ID, enabling SSRF-like behavior if the external API were to redirect or if the URL construction were modified.
Fix: Validate network_id is within a reasonable range. Add a timeout (already present at 10s, which is good). Log and monitor external API calls.
MED-07: Docker Network Has No Segmentation¶
Location: ops/docker-compose.yml lines 71-73
All services share a single flat Docker network (talos_net). The web API, database, MQTT broker, and mission director can all communicate freely.
Fix: Create separate networks: a frontend network for the web API and broker, and a backend network for the database and director. Only services that need to communicate should share a network.
MED-08: External Dependencies Loaded Without Integrity Checks¶
Location: core/templates/dashboard.html lines 8-10
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" ...>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.1/mqttws31.min.js"></script>
Three external scripts are loaded without integrity attributes. A CDN compromise would inject arbitrary JavaScript into every dashboard session.
Fix: Add SRI hashes (integrity="sha384-...") to every external <script> and <link> tag. Pin exact versions.
4. Threat Model¶
4.1 System Assets¶
| Asset | Value | Current Protection |
|---|---|---|
| Antenna rotator control | Physical hardware, safety risk if misused | None -- unauthenticated MQTT |
| Receiver tuning | Determines what signals are captured | None -- unauthenticated MQTT |
| Mission scheduling | Determines what satellite is tracked | Cookie forgery sufficient |
| Station telemetry | Operational intelligence | Broadcast in cleartext |
| User database | Email addresses, station locations | Known password, exposed port |
| Satellite TLE cache | Orbital parameters | Public data, low sensitivity |
4.2 Adversary Profiles¶
A1 -- Opportunistic Network Attacker (High Likelihood) Scans open ports, discovers MQTT on 1883/9001 and PostgreSQL on 5432. Uses known credentials or anonymous MQTT access. Motivation: curiosity, vandalism, or cryptocurrency mining on exposed infrastructure.
A2 -- Targeted Attacker Against Space Infrastructure (Medium Likelihood) Nation-state or advanced persistent threat targeting satellite ground station networks. Objectives: intelligence collection on tracking capabilities, disruption of satellite communications, or using compromised stations as pivot points into broader networks.
A3 -- Insider Threat / Rogue Operator (Medium Likelihood) Authenticated user who exploits the lack of authorization to control stations they do not own, redirect missions, or exfiltrate telemetry from other operators' stations.
A4 -- Supply-Chain Attacker (Low Likelihood, High Impact) Compromises one of the three external CDN-hosted JavaScript libraries to inject malicious code into the dashboard, gaining MQTT publish access and session cookie theft across all active dashboard sessions.
4.3 Attack Surfaces¶
- MQTT Broker (ports 1883, 9001) -- Primary attack surface. No authentication, no encryption, no access control. Directly connected to physical hardware through the agent.
- Web API (port 8000) -- Forgeable session cookies, no authorization, no rate limiting.
- PostgreSQL (port 5432) -- Known credentials, exposed to host network.
- Agent CLI -- API key in process list, no broker authentication, raw TCP to hardware.
- External CDN dependencies -- No SRI, three separate trust anchors.
5. Security Roadmap -- Prioritized Remediation¶
Phase 1: Stop the Bleeding (Week 1)¶
These changes prevent the most dangerous exploits with minimal code changes:
- Enable MQTT authentication. Create a
password_filefor Mosquitto. Assign credentials to the director, each agent, and the web backend. Setallow_anonymous false. (Addresses CRIT-01, CRIT-04, HIGH-02) - Stop exposing PostgreSQL. Remove
ports: "5432:5432"fromdocker-compose.yml. Services ontalos_netcan still reach the database. (Addresses CRIT-05) - Replace the hardcoded secret key. Generate a random key, load from environment variable. (Addresses CRIT-03)
- Sign session cookies. Use
itsdangerousor switch to JWT-based sessions withhttponly,secure, andsamesiteflags. (Addresses CRIT-02)
Phase 2: Access Control (Weeks 2-3)¶
- Add authentication to all API endpoints. Every endpoint should require
Depends(get_current_user). (Addresses CRIT-06) - Implement RBAC. Add a
rolefield to the User model. Enforce ownership checks on station operations. (Addresses CRIT-06) - Add rate limiting. Deploy
slowapior equivalent. (Addresses HIGH-05) - Add JWT expiration. Set
expclaim to 10 minutes on magic link tokens. (Addresses MED-03) - Restrict user registration. Allow-list email domains or require admin approval. (Addresses MED-04)
Phase 3: Transport Security (Weeks 3-4)¶
- Enable TLS on MQTT. Generate certificates, configure Mosquitto
cafile,certfile,keyfile. Update all clients. (Addresses HIGH-01) - Implement MQTT topic ACLs. Agents can subscribe only to their own
talos/gs/{id}/cmd/#. Only the director can publish tocmd/#topics. (Addresses CRIT-01) - Remove browser-to-MQTT direct connection. Route all commands through authenticated API endpoints. Use SSE or authenticated WebSocket for push. (Addresses CRIT-04)
- Add CORS middleware. Restrict origins to the known dashboard URL. (Addresses HIGH-04)
Phase 4: Hardening (Weeks 4-6)¶
- Add CSP headers and SRI hashes. (Addresses MED-02, MED-08)
- Add CSRF protection. (Addresses MED-01)
- Segment Docker networks. (Addresses MED-07)
- Validate rotator addresses locally. Do not accept hardware addresses from MQTT. (Addresses HIGH-06)
- Move API keys to environment variables. Stop passing them as CLI arguments. (Addresses HIGH-03)
- Implement secrets management. Use Docker secrets or HashiCorp Vault for all credentials. (Addresses CRIT-05)
- Stop logging authentication tokens. (Addresses MED-05)
Phase 5: Monitoring and Incident Response (Ongoing)¶
- Add structured logging. Log all authentication events, command publications, and mission changes with timestamps and source IPs.
- Deploy anomaly detection on MQTT. Alert on unexpected client IDs, unexpected topic publications, or unusual command rates.
- Implement audit trail. Record who activated which mission, when, and from what IP.
- Set up automated dependency scanning. Monitor external libraries for known vulnerabilities.
6. Compliance Considerations¶
6.1 ITU Radio Regulations¶
Ground stations operating radio receivers may be subject to ITU Radio Regulations and national spectrum licensing. Unauthorized control of receiver tuning could constitute unauthorized spectrum access. The lack of access control on frequency commands creates regulatory risk.
6.2 ITAR / EAR Export Controls¶
Satellite tracking software and TLE data handling may fall under U.S. Export Administration Regulations (EAR) Category 9 (spacecraft) or ITAR Category XV (spacecraft systems). If TALOS is used to track controlled satellites or process controlled data, the system must implement access controls that enforce nationality-based restrictions. The current system has no such controls.
6.3 NIST Cybersecurity Framework for Space Systems¶
NIST published the "Satellite Ground Segment Cybersecurity" guide (IR 8401). Key gaps relative to that framework:
- Identify: No asset inventory, no risk assessment process documented.
- Protect: No access control, no encryption in transit, no data protection.
- Detect: No logging, no anomaly detection, no monitoring.
- Respond: No incident response plan, no communication procedures.
- Recover: No backup strategy, no recovery procedures documented.
6.4 GDPR / Data Protection¶
User email addresses are stored in the database. The auto-registration feature on login creates user records without explicit consent. There is no privacy policy, no data retention policy, and no mechanism for users to request data deletion.
6.5 General Recommendations for Space Operations Compliance¶
- Implement end-to-end encryption for all command and telemetry channels.
- Maintain a full audit log of all commands sent to ground station hardware.
- Establish a formal change management process for mission parameters.
- Define and enforce operator qualification requirements before granting system access.
- Conduct periodic penetration testing, especially after any changes to the MQTT or authentication subsystems.
Appendix: Finding Summary¶
| ID | Severity | Title | Effort to Fix |
|---|---|---|---|
| CRIT-01 | Critical | Anonymous MQTT broker | 2 hours |
| CRIT-02 | Critical | Unsigned cookie authentication | 4 hours |
| CRIT-03 | Critical | Hardcoded JWT secret | 30 minutes |
| CRIT-04 | Critical | Browser-side MQTT hardware control | 1-2 days |
| CRIT-05 | Critical | Exposed database with known credentials | 1 hour |
| CRIT-06 | Critical | No authorization model | 2-3 days |
| CRIT-07 | High | Unauthenticated satellite search | 1 hour |
| HIGH-01 | High | No TLS on MQTT | 4 hours |
| HIGH-02 | High | Agent API key never validated | 4 hours |
| HIGH-03 | High | API key in process list | 2 hours |
| HIGH-04 | High | No CORS configuration | 1 hour |
| HIGH-05 | High | No rate limiting | 2 hours |
| HIGH-06 | High | Raw TCP to hardware, address from MQTT | 2 hours |
| MED-01 | Medium | No CSRF protection | 2 hours |
| MED-02 | Medium | No CSP headers | 2 hours |
| MED-03 | Medium | Magic link tokens never expire | 30 minutes |
| MED-04 | Medium | Auto-registration on login | 2 hours |
| MED-05 | Medium | Auth tokens in server logs | 30 minutes |
| MED-06 | Medium | No input validation on provisioning | 1 hour |
| MED-07 | Medium | No Docker network segmentation | 1 hour |
| MED-08 | Medium | External scripts without SRI | 1 hour |