Crumb's API historically served plain HTTP only (http://<host>:8080), fine
on a trusted LAN, but passwords, JWTs, and video ride the wire cleartext.
This adds in-product TLS via a Caddy sidecar in docker-compose.yml,
turned on by default, with zero breaking changes for existing installs.
- A new
caddyservice (caddy/Caddyfile) reverse-proxies the API (api:8080over the internal compose network) and terminates HTTPS on a published port, default 8443 (CRUMB_HTTPS_PORTin.envif you want a different one, kept off 443 so it never collides with something else already bound to that port on the host). - The API's plain HTTP on :8080 keeps working exactly as before. Caddy doesn't replace it, it's an additional, encrypted way in. Nothing about the API changed; Caddy is a pass-through in front of it.
- On a fresh install with no domain, Caddy uses its automatic internal CA
(
tls internalin the Caddyfile / thelocal_certsglobal option): it mints its own root certificate once (stored in thecrumb_caddy_datavolume) and issues a leaf cert off of it for whatever hostname/IP you hit it on. No domain, no port-forwarding, no outbound calls to Let's Encrypt.
Reach it at:
https://<this-host>:8443
e.g. https://198.51.100.50:8443, same admin console / API as
http://198.51.100.50:8080, just encrypted.
Because the internal CA isn't in your OS/browser's trust store, the first
time you visit https://<host>:8443 your browser will show a warning
("Your connection is not private" / "Warning: Potential Security Risk" /
similar), this is expected on a LAN install with no public domain name, not
a sign anything is broken. Your traffic is still fully encrypted; the
warning only means the browser can't verify the cert came from a CA it
already trusts.
Two ways to deal with it:
- Click through once per browser, "Advanced" → "Proceed to
<host>(unsafe)" (wording varies by browser). Most browsers remember this per-site after the first time. - Trust Caddy's local CA properly (removes the warning everywhere on
that machine):
- Get the root cert out of the running container:
docker compose cp caddy:/data/caddy/pki/authorities/local/root.crt ./crumb-local-ca.crt - Import
crumb-local-ca.crtinto your OS/browser trust store:- Windows: double-click the
.crt→ "Install Certificate" → "Local Machine" → "Place all certificates in the following store" → "Trusted Root Certification Authorities". - macOS: open in Keychain Access, add to "System", then set "Always Trust" in the cert's trust settings.
- Linux: copy to
/usr/local/share/ca-certificates/, runsudo update-ca-certificates(Debian/Ubuntu) or the equivalent for your distro; browsers using the system store will pick it up (Firefox has its own store, import via Settings → Privacy & Security → Certificates → View Certificates → Authorities → Import). - Android/iOS: install the
.crtas a trusted CA profile (Settings → Security → "Install from storage" on Android; Settings → General → VPN & Device Management on iOS, then also enable full trust under Certificate Trust Settings).
- Windows: double-click the
- This only needs to happen once per device/browser you use to reach
Crumb, after that,
https://<host>:8443is fully trusted with no warning.
- Get the root cert out of the running container:
Native clients (desktop/Android/iOS apps) currently talk to the API over plain HTTP/RTSP by design (see "Non-breaking by design" below), the browser warning above only applies if you point a browser at the HTTPS port.
docker-compose.yml'sapiservice still publishes0.0.0.0:8080:8080unchanged. Any existing bookmark, desktop client "Server address", Android app config, or script hardcoded tohttp://<host>:8080keeps working with no changes required.- The
caddyservice is purely additive: it depends onapi, adds one new published port (CRUMB_HTTPS_PORT, default 8443), and two new named volumes (crumb_caddy_data,crumb_caddy_config) for its cert storage and autosaved config. If you don't want it, delete thecaddy:block (and the two volumes) fromdocker-compose.yml, the rest of the stack is unaffected. - Rate limiting behind the proxy: with clients coming through Caddy, the
API sees every request from Caddy's container IP, so the login rate limiter
shares one bucket across all HTTPS users. Set
TRUST_PROXY=1in.envand the limiter keys on the client IP fromX-Forwarded-For(which Caddy sets) instead. Leave it unset when clients hit:8080directly, trusting a header that any direct client can forge would let an attacker dodge the limiter.
Once you've confirmed HTTPS works for you (browser warning accepted or CA trusted, per above), you can stop publishing the API's plain port so only the encrypted path is reachable from the LAN:
- In
docker-compose.yml, under theapi:service'sports:, remove (or comment out) the"0.0.0.0:8080:8080"line, or change it to"127.0.0.1:8080:8080"if you still want local/host-only plain-HTTP access for debugging. docker compose up -dto apply.- Update any client "server address" settings to the
https://URL.
Caddy needs no changes for this step, it already reaches api over the
internal compose network regardless of what's published to the host.
If you have a domain pointed at this host and can forward port 443 (and 80, for the ACME HTTP-01 challenge) from your router/firewall to it, Caddy can get you a real, browser-trusted certificate with zero manual renewal:
- Edit
caddy/Caddyfile:- Remove the
{ local_certs }global options block and thetls internalline. - Replace the
:{$CRUMB_HTTPS_PORT} { ... }site block with your domain:crumb.example.com { reverse_proxy api:8080 }
- Remove the
- Edit
docker-compose.yml'scaddyserviceports:to publish80:80and443:443instead ofCRUMB_HTTPS_PORT:8443(Caddy needs 80 for the ACME challenge and to redirect to 443). - Make sure
crumb.example.comresolves (public DNS) to this host's public IP, and that your router/firewall forwards 80 and 443 to it. docker compose up -d. Caddy automatically requests, installs, and renews the certificate, no browser warning, no manual steps after that.
This is a bigger step (public DNS + port-forwarding) than the LAN-only default, so it's left as a manual, documented opt-in rather than something the base compose file does for you.
- Self-signed cert UX is real friction for a first-time LAN user, the browser warning above is unavoidable without a domain. This is the standard trade-off for any self-hosted app with no public DNS name (Caddy, Portainer, Proxmox, etc. all show the same kind of warning by default).
- Native clients (desktop/Android/iOS) still default to plain HTTP/RTSP
for their server-address/streaming config, this task only adds the HTTPS
option at the infrastructure layer (Caddy in front of the API). Wiring
each client to prefer/require HTTPS, and to trust (or pin) Caddy's
internal CA so they don't need a manual per-device import, is future work.
(The related auth hardening that pairs with encrypted transport, revocable
sessions and scoped short-lived media
?token=claims, already ships.) - RTSP (
:18554) and the WebRTC media plane (:8556) are unaffected by this change, they're go2rtc's own listeners, not proxied through Caddy, and continue to run unencrypted (with go2rtc's own Basic-auth/RTSP-auth on top, per the compose file's recorder-service comments, go2rtc is embedded in the recorder container). Encrypting those is a separate, larger effort (SRTP/DTLS or an RTSPS listener) not in scope here.