Skip to content

Security Architecture

This document describes the security model, cryptographic choices, and threat mitigations across the bilbycast system.

Bilbycast uses defense-in-depth with multiple independent security layers:

  • TLS 1.3 (Rustls) for all manager connections — no legacy cipher suites
  • End-to-end encryption (ChaCha20-Poly1305) for tunnel traffic — relay is zero-knowledge
  • Encryption at rest (AES-256-GCM) for all secrets on manager and edge nodes
  • Role-based access control (4-level RBAC) on the manager
  • Audit logging for all security-relevant operations
  • Certificate pinning to protect against compromised CAs
  • Secret rotation for node authentication credentials

Edge and relay nodes enforce wss:// (TLS) for all manager connections. Plaintext ws:// URLs are rejected at connection time. The TLS implementation uses Rustls with TLS 1.3 only — no fallback to older protocols or weak cipher suites.

ModeConfigDescription
Direct (default)BILBYCAST_TLS_MODE=directManager handles TLS. Requires BILBYCAST_TLS_CERT and BILBYCAST_TLS_KEY. Sets Secure cookie flag and sends HSTS headers.
Behind ProxyBILBYCAST_TLS_MODE=behind_proxyLoad balancer terminates TLS. Manager listens on plain HTTP. Omits Secure flag and HSTS (LB handles these). Only safe on trusted networks between LB and manager.

Edge and relay nodes support optional SHA-256 certificate fingerprint pinning via the cert_fingerprint config field:

"manager": {
"url": "wss://manager:8443/ws/node",
"cert_fingerprint": "ab:cd:ef:01:23:45:67:89:..."
}

When configured, connections are rejected if the server presents a certificate with a different fingerprint — even if that certificate has a valid CA signature. This protects against compromised Certificate Authorities and targeted MITM attacks.

The pinning verifier performs both standard CA chain validation and fingerprint verification.

For development/testing, nodes can set accept_self_signed_cert: true. This mode completely disables all certificate validation and is protected by a dual safety mechanism:

  1. Environment variable guard: Requires BILBYCAST_ALLOW_INSECURE=1 to be set — without it, the connection fails with a clear error
  2. Startup warning: Logs a prominent SECURITY WARNING on every connection

This prevents accidental production use when the flag is left in a config file from testing.

AspectImplementation
Password hashingArgon2id (via argon2 crate)
Password requirements8-128 chars, must contain uppercase + lowercase + digit
Session tokensJWT with HMAC-SHA256, 24-hour expiry
JWT claimssub (user ID), role, jti (session ID), iat, exp, iss (“bilbycast-manager”)
Session deliveryHttpOnly + Secure + SameSite=Lax cookie
Session revocationLogout inserts jti into revoked_sessions table; checked on every request
CSRF protectionDouble-submit cookie pattern with constant-time comparison; header-only fallback for self-signed cert environments
Timing safetyDummy Argon2id hash computed on unknown usernames to prevent user enumeration

Node Authentication (Edge/Relay to Manager)

Section titled “Node Authentication (Edge/Relay to Manager)”

Two-phase authentication over WebSocket:

Phase 1 — Registration (one-time):

  1. Admin creates a node in the manager UI, receives a registration token
  2. Token is stored in the DB as an HMAC-SHA256 hash (never plaintext)
  3. Edge/relay sends the token as the first WebSocket frame
  4. Manager verifies by computing HMAC and comparing to stored hash
  5. Manager generates a UUID node secret, encrypts with AES-256-GCM, stores in DB
  6. Manager sends register_ack with node_id + node_secret
  7. Token is consumed (set to NULL) — single-use, cannot be replayed

Phase 2 — Reconnection (ongoing):

  1. Edge/relay sends node_id + node_secret as first WebSocket frame
  2. Manager decrypts stored secret, compares
  3. Returns auth_ok on match

Optional Bearer token auth via api_token config field (32-128 characters). When configured, all API endpoints except /health require Authorization: Bearer <token>. If absent, the API is open (backwards compatible; warning logged at startup).

OAuth 2.0 client_credentials grant at /oauth/token. Returns JWT (HMAC-SHA256) with configurable expiry (default 1 hour). Used by external systems integrating with the edge’s REST API.

All sensitive data is encrypted before storage using AES-256-GCM:

DataDB ColumnAlgorithm
Node auth secretsauth_client_secret_encAES-256-GCM
Tunnel encryption keystunnel_key_encAES-256-GCM
Tunnel bind secretstunnel_bind_secret_encAES-256-GCM
Tunnel PSKstunnel_psk_encAES-256-GCM
AI API keysapi_key_encAES-256-GCM
Registration tokensregistration_tokenHMAC-SHA256 hash (one-way)

Key derivation: HKDF-SHA256 from BILBYCAST_MASTER_KEY with salt "bilbycast-manager-master-key-v1". The master key must be a 64-character hex string (32 bytes). Weak values are rejected at startup.

Nonce management: 12 random bytes per encryption operation, prepended to ciphertext.

Edge nodes store infrastructure secrets (node credentials, tunnel encryption keys, TLS/auth config) in a separate secrets.json file, encrypted at rest using AES-256-GCM with a machine-specific key. Flow-level user parameters (SRT passphrases, RTSP credentials, RTMP keys, bearer tokens) remain in config.json for manager UI visibility.

PriorityKey SourceAvailability
1/etc/machine-idAll systemd-based Linux (Ubuntu, Debian, RHEL, Fedora, Arch)
2/var/lib/dbus/machine-idOlder Linux without systemd
3Generated .secrets_key filemacOS (development), containers, other environments

Key derivation: HKDF-SHA256 with salt "bilbycast-edge-secrets-v1".

File format: v1: prefix + Base64(nonce + ciphertext). The version prefix enables future format changes. Files written with Unix mode 0600 (owner read/write only).

Backward compatibility: Existing unencrypted secrets.json files are auto-detected and re-encrypted on the next save.

All tunnel traffic between edge nodes is encrypted with ChaCha20-Poly1305 (AEAD):

  • Key size: 256 bits (32 bytes), generated by the manager
  • Nonce: 12 random bytes per packet
  • Auth tag: 16 bytes (128 bits)
  • Overhead: 28 bytes per packet (12 nonce + 16 tag)

The relay server cannot decrypt tunnel traffic — it only sees encrypted payloads. This is a zero-knowledge relay architecture.

Optional per-tunnel HMAC-SHA256 bind tokens:

  1. Manager generates a tunnel_bind_secret (32 random bytes)
  2. Computes directional tokens: HMAC-SHA256(secret, "tunnel_id:ingress") and HMAC-SHA256(secret, "tunnel_id:egress")
  3. Sends tokens to relay via authorize_tunnel command
  4. Edge nodes include their bind token in TunnelBind messages
  5. Relay validates with constant-time comparison

Direct mode tunnels (edge-to-edge, no relay) use a per-tunnel PSK (pre-shared key) for QUIC transport authentication. The PSK is 32 random bytes, generated by the manager and distributed to both edges.

Node authentication secrets can be rotated via the manager API:

POST /api/v1/nodes/{id}/rotate-secret

Flow:

  1. Manager generates a new UUID secret
  2. Sends RotateSecret command to node via active WebSocket
  3. Node stores new secret locally (persisted to encrypted secrets.json)
  4. Node sends command_ack
  5. Manager updates DB with new encrypted secret
  6. Old secret is immediately invalidated

Requirements: The node must have an active WebSocket connection. The endpoint requires Admin role.

4-level permission hierarchy:

RoleLevelCapabilities
Viewer0Read-only access to dashboards and node status
Operator1Viewer + manage flows, send commands to nodes
Admin2Operator + manage nodes, users, and system settings
SuperAdmin3Admin + manage other admins, full system control

Each user can optionally have an allowed_node_ids list restricting access to specific nodes. If null/empty, the user can access all nodes within their role permissions.

TargetLimitWindowKeyResponse
Login attempts5 failures60 secondsIP addressHTTP 429
Node auth attempts5 failures60 secondsnode_id or token prefixWebSocket auth error + lockout message

Rate limiters use in-memory sliding windows (DashMap). Windows expire automatically after the cooldown period.

All manager HTTP responses include:

HeaderValuePurpose
X-Content-Type-OptionsnosniffPrevents MIME-type sniffing
X-Frame-OptionsDENYPrevents clickjacking (iframe embedding)
Strict-Transport-Securitymax-age=31536000; includeSubDomainsForces HTTPS (direct TLS mode only)

Session cookies use HttpOnly (no JavaScript access), Secure (HTTPS only, in direct mode), and SameSite=Lax.

All entry points validate inputs before processing:

CategoryLimits
String fieldsIDs: 64 chars, names: 256 chars, URLs: 2048 chars, tokens: 4096 chars
API payloadsConfig: 100 KB, commands/flows: 50 KB
WebSocket messages5 MB per node message
Event fieldsMessage: 10K chars, category: 256 chars
Network addressesSocket address format validation, URL scheme validation
SRT parametersRange checks on all advanced params (overhead, buffer sizes, etc.)

SQL injection is prevented by using parameterized queries (SQLx .bind()) throughout — no string interpolation in SQL statements.

All security-relevant mutations are logged to the audit_log table:

  • User authentication (login, logout, failed login)
  • Node operations (create, delete, config update, secret rotation)
  • Flow management (create, update, delete)
  • User management (create, update role, delete)
  • Settings changes

Each entry records: timestamp, user ID, action, target type, target ID, optional details, and IP address.

ThreatMitigationStatus
Network MITM (standard)TLS 1.3 via Rustls on all connectionsProtected
Compromised CACertificate pinning (cert_fingerprint)Protected (when configured)
Stolen DB backupAll secrets encrypted with AES-256-GCMProtected
Compromised edge hostsecrets.json encrypted at rest with machine-specific keyProtected
Compromised relayZero-knowledge — ChaCha20-Poly1305 E2E encryptionProtected
Token replayRegistration tokens are single-use, consumed on first authProtected
Brute force loginArgon2id + rate limiting (5/60s) + lockoutProtected
Brute force node authRate limiting (5/60s) + lockout per node_idProtected
Session hijackingHttpOnly+Secure cookies, CSRF double-submit, JWT revocationProtected
User enumerationConstant-time dummy hash on unknown usernamesProtected
Stale credentialsSecret rotation API (rotate-secret)Mitigatable
Self-signed cert MITMBILBYCAST_ALLOW_INSECURE=1 env var guard + startup warningGuarded
Behind-proxy interceptionInternal LB-to-manager link is HTTPRisk accepted (trusted network assumption)
  • Set BILBYCAST_JWT_SECRET to a unique 64-char hex string (openssl rand -hex 32)
  • Set BILBYCAST_MASTER_KEY to a unique 64-char hex string (openssl rand -hex 32)
  • Use CA-signed TLS certificates (not self-signed)
  • Remove accept_self_signed_cert: true from all production configs
  • Do not set BILBYCAST_ALLOW_INSECURE=1 in production
  • Configure cert_fingerprint on edge and relay nodes for certificate pinning
  • Set api_token on relay servers (32-128 char Bearer token)
  • Configure auth section on edge nodes if they expose a REST API
  • Use direct TLS mode unless behind a trusted load balancer
  • Rotate node secrets periodically via POST /api/v1/nodes/{id}/rotate-secret
  • Set node expiry times (expires_at) for temporary deployments
  • Review audit logs regularly (GET /api/v1/audit)
  • Restrict user permissions with appropriate RBAC roles
  • Use allowed_node_ids to limit operator access to relevant nodes only
VariableRequiredDescription
BILBYCAST_JWT_SECRETYes (manager)64-char hex string for JWT HMAC-SHA256 signing
BILBYCAST_MASTER_KEYYes (manager)64-char hex string for AES-256-GCM encryption at rest
BILBYCAST_TLS_CERTYes (direct mode)Path to TLS certificate PEM file
BILBYCAST_TLS_KEYYes (direct mode)Path to TLS private key PEM file
BILBYCAST_TLS_MODENo"direct" (default) or "behind_proxy"
BILBYCAST_ALLOW_INSECURENoSet to "1" to allow accept_self_signed_cert (dev/testing only)
BILBYCAST_PORTNoOverride manager listen port (default 8443)