Skip to content

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:

mosquitto_pub -h <broker_ip> -t "talos/gs/<station_id>/cmd/rot" -m '{"az":180,"el":90}'

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.


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

SECRET_KEY = "super_secret_mission_key"

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.


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

return { "command": f"python3 agent.py --id {new_id} --key {new_key}" }

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.


Location: core/main.py line 115

token = jwt.encode({"sub": user.email, "type": "magic"}, SECRET_KEY, algorithm=ALGORITHM)

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

if not user:
    user = User(email=data['email'])
    session.add(user)
    session.commit()

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

print(f"\n LOGIN: http://localhost:8000/auth/verify?token={token}\n")

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

  1. MQTT Broker (ports 1883, 9001) -- Primary attack surface. No authentication, no encryption, no access control. Directly connected to physical hardware through the agent.
  2. Web API (port 8000) -- Forgeable session cookies, no authorization, no rate limiting.
  3. PostgreSQL (port 5432) -- Known credentials, exposed to host network.
  4. Agent CLI -- API key in process list, no broker authentication, raw TCP to hardware.
  5. 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:

  1. Enable MQTT authentication. Create a password_file for Mosquitto. Assign credentials to the director, each agent, and the web backend. Set allow_anonymous false. (Addresses CRIT-01, CRIT-04, HIGH-02)
  2. Stop exposing PostgreSQL. Remove ports: "5432:5432" from docker-compose.yml. Services on talos_net can still reach the database. (Addresses CRIT-05)
  3. Replace the hardcoded secret key. Generate a random key, load from environment variable. (Addresses CRIT-03)
  4. Sign session cookies. Use itsdangerous or switch to JWT-based sessions with httponly, secure, and samesite flags. (Addresses CRIT-02)

Phase 2: Access Control (Weeks 2-3)

  1. Add authentication to all API endpoints. Every endpoint should require Depends(get_current_user). (Addresses CRIT-06)
  2. Implement RBAC. Add a role field to the User model. Enforce ownership checks on station operations. (Addresses CRIT-06)
  3. Add rate limiting. Deploy slowapi or equivalent. (Addresses HIGH-05)
  4. Add JWT expiration. Set exp claim to 10 minutes on magic link tokens. (Addresses MED-03)
  5. Restrict user registration. Allow-list email domains or require admin approval. (Addresses MED-04)

Phase 3: Transport Security (Weeks 3-4)

  1. Enable TLS on MQTT. Generate certificates, configure Mosquitto cafile, certfile, keyfile. Update all clients. (Addresses HIGH-01)
  2. Implement MQTT topic ACLs. Agents can subscribe only to their own talos/gs/{id}/cmd/#. Only the director can publish to cmd/# topics. (Addresses CRIT-01)
  3. Remove browser-to-MQTT direct connection. Route all commands through authenticated API endpoints. Use SSE or authenticated WebSocket for push. (Addresses CRIT-04)
  4. Add CORS middleware. Restrict origins to the known dashboard URL. (Addresses HIGH-04)

Phase 4: Hardening (Weeks 4-6)

  1. Add CSP headers and SRI hashes. (Addresses MED-02, MED-08)
  2. Add CSRF protection. (Addresses MED-01)
  3. Segment Docker networks. (Addresses MED-07)
  4. Validate rotator addresses locally. Do not accept hardware addresses from MQTT. (Addresses HIGH-06)
  5. Move API keys to environment variables. Stop passing them as CLI arguments. (Addresses HIGH-03)
  6. Implement secrets management. Use Docker secrets or HashiCorp Vault for all credentials. (Addresses CRIT-05)
  7. Stop logging authentication tokens. (Addresses MED-05)

Phase 5: Monitoring and Incident Response (Ongoing)

  1. Add structured logging. Log all authentication events, command publications, and mission changes with timestamps and source IPs.
  2. Deploy anomaly detection on MQTT. Alert on unexpected client IDs, unexpected topic publications, or unusual command rates.
  3. Implement audit trail. Record who activated which mission, when, and from what IP.
  4. 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