Skip to content

TLS Deployment

bilbycast-manager supports three TLS deployment modes. Pick the one that matches your environment:

ModeWho handles TLSBest for
ACME (direct mode + BILBYCAST_ACME_ENABLED=true)Manager — automatic Let’s EncryptPublic-internet manager with a stable DNS name
File-based (direct mode + BILBYCAST_TLS_CERT/KEY)Manager — operator-supplied PEM filesInternal CAs, certbot-managed certs, hardware security modules
Behind-proxy (BILBYCAST_TLS_MODE=behind_proxy)Load balancer / reverse proxyCloud deployments where the LB already terminates TLS

All three modes serve the same ports and APIs — the only difference is who terminates TLS. Edge nodes always connect over wss:// regardless of which mode you pick; plaintext ws:// is rejected at the edge.

The simplest path for any manager that has a stable public DNS name. The manager handles certificate issuance, renewal, and hot-reload internally — no certbot, no cron jobs, no operator action required after initial setup.

Two paths to enable ACME, same outcome:

  • Manager UI (recommended for interactive installs). Settings → TLS / ACME → tick Enable Let’s Encrypt → fill in Domain and Contact emailSave. The manager kicks off issuance, hot-reloads the cert when it lands, and lets you watch status flip from Requesting → Active in the UI.

  • Env vars (declarative provisioning — Ansible / Terraform / Compose). Append to your existing manager.env, drop any prior file-based TLS lines, restart:

    Terminal window
    sed -i '/^BILBYCAST_TLS_CERT=/d; /^BILBYCAST_TLS_KEY=/d' manager.env
    cat >> manager.env <<'EOF'
    BILBYCAST_ACME_ENABLED=true
    BILBYCAST_ACME_DOMAIN=manager.example.com
    BILBYCAST_ACME_EMAIL=ops@example.com
    # BILBYCAST_ACME_HTTP_PORT=80 # Default; only set if non-standard
    BILBYCAST_ACME_DIR=/var/lib/bilbycast-manager/acme
    EOF
    set -a; . ./manager.env; set +a
    sudo -E ./bilbycast-manager serve --config config/default.toml

The end-to-end flow (DNS pre-flight, port-80 reachability check, log lines that confirm issuance, staging vs production endpoints, UI vs env-var precedence) is documented in the install guide’s ACME walkthrough.

Requirements:

  • Port 80 must be reachable from the internet for the HTTP-01 challenge. The manager spins up a temporary HTTP listener on this port for the duration of the challenge.
  • Port 8443 (or whatever BILBYCAST_PORT is set to) must be reachable for the actual HTTPS service.
  • The DNS name in BILBYCAST_ACME_DOMAIN must resolve to the manager’s public IP.

The manager runs a background task that checks the cert every 12 hours (plus an immediate check on process startup). When the cert is within 30 days of expiry — or missing entirely — it triggers a renewal in the background. The new cert is hot-reloaded without restarting the manager — active WebSocket connections stay up, and new connections immediately use the new cert.

Renewal failures are logged as tls.renewal_failed events. The retry uses exponential backoff (1h initial, capped at 24h). The manager keeps retrying until renewal succeeds or the cert actually expires.

The ACME state — including the account key, the issued cert, and the renewal metadata — is stored under the manager’s data directory (BILBYCAST_DATA_DIR, default /var/lib/bilbycast-manager/). Back this up if you don’t want to re-issue from scratch after a manager rebuild. The Postgres cluster the manager points at via BILBYCAST_DATABASE_URL lives separately and is backed up on its own schedule.

Use this when:

  • Your CA is internal (not Let’s Encrypt).
  • You’re already using certbot or cert-manager and want to keep that workflow.
  • You’re behind an HSM or other key-storage mechanism that produces PEM files on disk.

Replace the cert/key paths in your existing manager.env (and drop any ACME lines so they don’t conflict), then restart the manager:

Terminal window
sed -i '/^BILBYCAST_TLS_CERT=/d; /^BILBYCAST_TLS_KEY=/d; /^BILBYCAST_ACME_/d' manager.env
cat >> manager.env <<'EOF'
BILBYCAST_TLS_CERT=/etc/bilbycast/manager.crt
BILBYCAST_TLS_KEY=/etc/bilbycast/manager.key
EOF
set -a; . ./manager.env; set +a
./bilbycast-manager serve --config config/default.toml

Both files must exist and be readable by the manager process. The manager validates them at startup and refuses to start if they’re missing or unreadable.

bilbycast-manager does not watch the cert file for changes in this mode (yet). When you renew the cert, restart the manager process — for example, via your certbot deploy hook:

/etc/letsencrypt/renewal-hooks/deploy/bilbycast-manager.sh
#!/bin/bash
systemctl restart bilbycast-manager

Active WebSocket connections will reconnect after the restart.

Use this when a load balancer, reverse proxy, or service mesh is already terminating TLS for you and the manager just needs to listen on plain HTTP. Common in cloud deployments (AWS ALB, GCP HTTPS LB, nginx, Traefik, Envoy, etc.).

Drop every TLS-related line from your existing manager.env and add the behind-proxy switch + plain-HTTP port, then restart:

Terminal window
sed -i '/^BILBYCAST_TLS_CERT=/d; /^BILBYCAST_TLS_KEY=/d; /^BILBYCAST_ACME_/d' manager.env
cat >> manager.env <<'EOF'
BILBYCAST_TLS_MODE=behind_proxy
BILBYCAST_PORT=8080
EOF
set -a; . ./manager.env; set +a
./bilbycast-manager serve --config config/default.toml

The manager listens on plain HTTP. Your proxy is responsible for:

  1. Terminating TLS with whatever cert the proxy uses.
  2. Forwarding wss:// upgrades correctly (the Upgrade and Connection headers must be passed through).
  3. Setting X-Forwarded-Proto: https so the manager knows the original request was HTTPS (used for cookie Secure flag enforcement).
  4. Not modifying the auth payload on the WebSocket upgrade.
upstream bilbycast_manager {
server 127.0.0.1:8080;
}
server {
listen 443 ssl http2;
server_name manager.example.com;
ssl_certificate /etc/letsencrypt/live/manager.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/manager.example.com/privkey.pem;
location / {
proxy_pass http://bilbycast_manager;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600s;
}
}

The long proxy_read_timeout is important: WebSocket connections are long-lived and shouldn’t be killed by the proxy mid-session.

Self-signed certs are supported in direct mode for development only. When the manager detects it is serving a self-signed cert, the UI shows a banner warning operators and linking to Settings → TLS so they can replace it.

Edge nodes connecting to a self-signed manager need to explicitly opt in:

// edge config.json
{
"manager": {
"urls": ["wss://manager.example.com"],
"accept_self_signed_cert": true
}
}

And the edge process needs the BILBYCAST_ALLOW_INSECURE=1 env var set, otherwise the config is rejected at load time. This is a deliberate safety guard against accidentally shipping self-signed mode to production.

Better than self-signed in dev: use cert pinning. Set accept_self_signed_cert: false and provide cert_fingerprint (the SHA-256 fingerprint of the manager’s cert) on the edge side. The edge will validate the exact cert without trusting the system CA store.

All three modes serve the same security headers on every response:

HeaderValue
X-Content-Type-Optionsnosniff
X-Frame-OptionsDENY
Strict-Transport-Securitymax-age=31536000; includeSubDomains

In behind-proxy mode, you may also want to add HSTS at the proxy level to ensure browsers see it on all responses (including any served directly by the proxy).

You can switch between modes by changing env vars and restarting the manager. The data directory and the Postgres database the manager points at are mode-independent — only the listener configuration changes. Edge nodes will automatically reconnect after the manager restart and continue using their existing credentials.

If…Use
You have a public DNS name and don’t want to think about renewalACME
You already manage certs with certbot, cert-manager, or an HSMFile-based
You’re deploying in a cloud with an existing load balancerBehind-proxy
You’re running locally for developmentSelf-signed (direct mode without BILBYCAST_TLS_CERT)