Deployment & Security¶
This guide helps you choose the setup style that fits your environment.
- Lab-friendly setup: quickest path to get running.
- Hardened setup: stronger protection for broader access.
1) Lab-Friendly Setup (Fast Start)¶
Best for private, trusted networks and quick testing.
Recommended baseline:
- Keep access limited to trusted network segments.
- Use strong local credentials.
- Keep backups current.
2) Hardened Setup (Recommended for Shared or Exposed Environments)¶
Use this profile when more users or broader network access are involved.
For v1.0 release candidates, directly internet-exposed operation is not a supported deployment boundary unless the release owner records an approved exception. Prefer trusted LAN/VPN access while SEC-3 and SEC-5 acceptance work is open.
Core hardening controls¶
These are the controls Circuit Breaker actually implements. Each one names the setting or role you can act on.
Roles and scopes. Every account carries a role in the hierarchy viewer < editor < admin, plus
a read-only demo role. Roles map to default scopes: viewer and demo get read:*; editor
adds per-entity write scopes (write:hardware, write:services, write:networks, and so on);
admin gets read:* write:* delete:* admin:*. Give each account the lowest role that works.
Service accounts for machine access. Create them with POST /api/v1/auth/service-account
rather than sharing a human account or a static token.
Rate limiting. Requests are limited per client, keyed by the trusted forwarded identity. It
requires shared Redis storage — the backend refuses to start if rate-limit storage resolves to
in-process memory — and a correct CB_TRUSTED_PROXY_CIDRS, or every client behind your proxy shares
one bucket. See Remote Access.
Outbound URL / SSRF policy. Every URL the app is asked to fetch is validated against a policy chosen for the use case — webhooks, threat feeds, LAN integrations, monitor targets, OIDC, and the egress proxy each get their own. Literal IPs, resolved DNS answers, and every redirect hop are all checked, and the cloud metadata address is refused under every policy.
Validation resolves the name, and then the request is pinned to the address that was approved —
otherwise the name could resolve to something else between the check and the connect (DNS
rebinding). The pin carries the original hostname in Host and in the TLS SNI, so certificates are
still verified against the name and not the address.
The pin is skipped when CB_EGRESS_PROXY_URL is set, deliberately. On the CONNECT path the HTTP
client builds TLS server_hostname from the request URL and ignores the SNI override, so a pinned
request would present the IP literal to the origin and fail certificate verification for every HTTPS
request — i.e. pinning would break exactly the hardened deployment this page recommends. A forward
proxy resolves the name itself and the app never opens the socket, so the rebinding window the pin
closes is not the app's to close there. Threat-feed downloads additionally run with connection
keep-alive disabled: pooled connections are keyed on address, so two feed hostnames resolving to one
address would otherwise share a connection whose certificate was verified for only the first.
Scan target ACL. Discovery targets are checked against the allowed-network ACL, RFC 1918
private-address enforcement, and air-gap mode (CB_AIRGAP, or the airgap setting in the UI).
Audit log hash chain. Each audit entry stores the SHA-256 of its payload plus the previous
entry's hash, so a deleted or edited row breaks the chain. Repairing a broken chain requires the
explicit REPAIR_AUDIT_CHAIN authorization.
Destructive-action confirmation. High-impact operations require an x-cb-confirmation header
matching the action, an idempotency-key of at least 12 characters, and — where a verified backup
is required — x-cb-backup-verified: true. Denied attempts are written to the audit log.
Network boundaries. Keep the app behind trusted network boundaries and publish only the ports you need. See Remote Access before exposing it further.
Native HTTPS¶
Native Linux installs terminate TLS at nginx. There are no HTTPS "modes" to choose between — the installer takes one of three paths:
- Self-signed (default). If no certificate is already present, the installer generates a
4096-bit self-signed certificate valid for 10 years with a DNS SAN for the FQDN (or
circuitbreaker) and an IP SAN for the detected server address. Browsers will warn until you trust it or replace it. - Existing certificate. If
fullchain.pemalready exists in the TLS directory, it is reused untouched. This is how you install a real certificate: placefullchain.pemandprivkey.pemthere before or after install. - Let's Encrypt (
--cert-type letsencrypt --fqdn ... --email ...). The installer validates that the FQDN resolves to this server's IP and then expects certbot-issued certificates to already be at the TLS path. If DNS does not check out, or--fqdn/--emailare missing, it warns and falls back to self-signed.--emailis also written to the environment file asCB_TLS_EMAIL, which is what the application uses as its ACME account address — so you can obtain and renew a certificate from the Certificates page afterwards without editing anything. See TLS Certificates.
Certificates live in ${CB_DATA_DIR}/tls (default /var/lib/circuitbreaker/tls), owned by root
and the nginx group (nginx or www-data), with mode 750 on the directory and 640 on the
.pem files.
The Certificates page writes to this same directory: activating a certificate replaces
fullchain.pem and privkey.pem and reloads nginx. That is the only thing that changes what the
server presents — creating or renewing a certificate stores it and nothing more.
Pass --no-tls to the installer to skip HTTPS entirely and serve plain HTTP on CB_PORT. Only do
this behind another proxy that terminates TLS. Note that HTTP-01 certificate validation is served
from the plain-HTTP listener, so it keeps working in either mode.
Configuration is an environment file at /etc/circuitbreaker/.env. There is no config.yaml.
Important environment values¶
CB_VAULT_KEY: secures sensitive stored credentials. Required — the backend will not start without it once encrypted secrets exist.CB_JWT_SECRET: signs sessions. Must be at least 32 characters, or startup fails. Keep it distinct fromCB_VAULT_KEY— the container entrypoint refuses to start if the two match.CB_TRUSTED_PROXY_CIDRS: which peers may setX-Forwarded-*. See Remote Access.CB_API_TOKEN: deprecated and rejected. Any Bearer token matching it is answered with HTTP 401 telling you to migrate to service accounts (POST /api/v1/auth/service-account), and the backend logs a removal warning at startup if it is set.CB_LEGACY_AUTH=truerestores the old behaviour as a temporary rollback only — do not run with it long-term.
Outbound egress¶
Public outbound HTTP can be forced through a forward proxy:
CB_EGRESS_PROXY_URL: the forward proxy to route public outbound requests through. Set this if you have one, then setCB_ALLOW_DIRECT_EGRESS=false. An invalid value fails startup.CB_ALLOW_DIRECT_EGRESS: records the decision to run without a proxy. Every shipped template defaults it totrue, because most single-node hosts have no forward proxy; the code default when the variable is absent isfalse, so an emptyCB_EGRESS_PROXY_URLwould otherwise abort startup.
CB_ALLOW_DIRECT_EGRESS=true waives the proxy requirement and nothing else. The outbound URL
policy still applies to every public request — SSRF checks, scheme validation, and redirect
validation are unchanged — and the Redis, NATS, rate-limit-storage, and secret gates still fail
closed.
Do not confuse it with CB_ALLOW_DEGRADED_DEPENDENCIES, which is a blanket break-glass flag that
waives all of those dependency gates at once. Use that one only to get a broken instance back up
long enough to fix it.
The daily release check leaves the box by default¶
One outbound call is made without you configuring anything, and it discloses the instance version, so it is stated here rather than left to be discovered:
- Where:
https://api.github.com/repos/BlkLeg/CircuitBreaker/releases. - When: once at startup, then every 24 hours plus up to 30 minutes of random jitter. The jitter spreads load across instances; it does not obscure anything.
- What is sent: an unauthenticated
GET, withUser-Agent: circuit-breaker/<version>and, unavoidably, the source IP of whatever egress path the request takes. The running version of this instance is therefore disclosed to a third party once a day. Nothing about your inventory, your users, your network, or your configuration is sent — the request body is empty and the URL carries onlyper_page. - What comes back: the public release list. Nothing is downloaded and nothing is installed; the result is a version comparison held in memory and shown to admins as a banner.
This deserves stating plainly because the application deliberately withholds its version from
unauthenticated callers as fingerprinting material — GET /api/v1/health returns version only to
an authenticated caller (main.py: "unauthenticated fingerprinting material — they tell a scanner
which published CVEs to try before it has any credentials"), and GET /api/v1/system/update is
admin-only. Shipping a silent daily disclosure of that same value
to GitHub is a different trade-off, made because an instance that cannot learn it is out of date is
the more common real-world harm: a user running an unpatched release without knowing it. If your
threat model says otherwise, turn it off.
Turning it off — any one of these is sufficient, and each stops the socket being opened at all rather than merely hiding the banner:
CB_UPDATE_CHECK=falseCB_AIRGAP=true- the
airgap_modeswitch in Settings
The check honours CB_EGRESS_PROXY_URL like every other public outbound client, so a proxied
deployment sees it at the proxy rather than leaving the host directly.
NATS authentication and TLS¶
NATS is the internal bus used for discovery, worker dispatch, and notifications. Token auth is required in both shipped install paths — it is not optional and not off by default:
- Docker Compose aborts
docker compose upifNATS_AUTH_TOKENis unset. - The native installer generates
CB_NATS_TOKENfor you and writes it to/etc/circuitbreaker/.env.
Authentication options:
- Token auth:
CB_NATS_TOKENis the preferred variable;NATS_AUTH_TOKENis the fallback the client reads if it is unset. Use the same value for the NATS server and for the backend and all workers. - User/password: Set
NATS_USERandNATS_PASSWORDfor backend and workers. The NATS server must be configured for user auth (e.g. via a custom config file or override command); the client will embed credentials in the connection URL. - TLS: Set
NATS_TLS=truefor backend and workers so they connect withtls://and TLS enabled. The NATS server must be configured for TLS (certificates and--tls/ config); document cert paths and mount them into the server container as needed.
Content-Security-Policy¶
The app is served through nginx in both shipped install paths, and nginx sets its own Content-Security-Policy — that is the one the browser applies. If you put a further reverse proxy in front, its CSP wins instead. Either way, the CSP in force must allow the same origins as the backend, or the first-run wizard, map UI, and weather widget break:
- Gravatar —
https://www.gravatar.com(img-src) for profile avatars. - Google Fonts —
https://fonts.googleapis.com(style-src) andhttps://fonts.gstatic.com(font-src) for the font picker. - Open-Meteo —
https://geocoding-api.open-meteo.comandhttps://api.open-meteo.com(connect-src) for the weather widget.
Three files carry the directive string and must stay in sync:
apps/backend/src/app/middleware/security_headers.py— the backend'sSecurityHeadersMiddleware.docker/nginx.mono.conf— the proxy inside the single-container image.deploy/nginx/circuitbreaker-tls.conf— the proxy on native installs.
All three also set X-Content-Type-Options: nosniff, X-Frame-Options: DENY,
Referrer-Policy: strict-origin-when-cross-origin, HSTS, and a Permissions-Policy that denies
camera, microphone, geolocation, payment, USB, and the motion sensors.
3) Secrets Management & Vault¶
Circuit Breaker uses a Fernet-based secure vault to encrypt sensitive credentials at rest — entirely local, no third-party key management required.
The vault protects:
- SMTP credentials — used for password reset and invite emails.
- Proxmox API tokens — the secret half of the PVEAuditor token used during cluster scans.
- SNMP community strings and iDRAC/iLO credentials.
Vault key lifecycle¶
You do not need to generate the vault key manually. Both shipped installers create it for you:
- The native installer generates
CB_VAULT_KEYand writes it to/etc/circuitbreaker/.env. install.sh --dockergenerates it into the.envnext to the compose file. Compose itself refuses to start without it —docker-compose.ymldeclaresCB_VAULT_KEYas required.
If no key is present anywhere, the first-run setup wizard generates one and persists it to
$CB_DATA_DIR/.env. At startup the backend resolves the key in this order:
- The
CB_VAULT_KEYenvironment variable $CB_DATA_DIR/.env- A legacy plaintext database column, which is migrated out to
$CB_DATA_DIR/.envand cleared
Only a hash of the key is ever stored in the database.
If the vault ends up uninitialized — after a crash, an accidental data-directory deletion, or a
restore that missed the environment file — put the original key back into CB_VAULT_KEY in
/etc/circuitbreaker/.env (native) or your compose .env, and restart. There is no command that
recovers a lost key; a snapshot from
Backup & Restore is the recovery path.
Vault best practices:
- Back up the key as soon as the install finishes — store it in a password manager or offline secure location, separately from your backups.
- Treat the vault key like a master root credential. Anyone with it can decrypt your stored secrets.
- If you generate a new key instead of restoring the original, existing encrypted secrets (SMTP, Proxmox tokens, SNMP strings) become unreadable and must be re-entered in Settings.
What must be persisted¶
The single-container deployment declares exactly two mounts, and no named volumes at all:
| Mount | Stores |
|---|---|
${CB_DATA_DIR:-./circuitbreaker-data} → /data |
All persistent state |
/run/circuitbreaker → /run/circuitbreaker |
The optional cb-helperd socket. Not persistence — harmless if the daemon is not installed |
Everything lives under that one /data mount. The entrypoint creates these subdirectories inside
it on first start:
| Subdirectory | Stores |
|---|---|
pgdata |
PostgreSQL data |
uploads |
Uploaded icons, branding assets, and other user files |
nats |
NATS/JetStream state |
tls |
The fullchain.pem / privkey.pem nginx serves on 8443 |
certs |
Certificate material |
redis |
Redis persistence |
The vault key is read from the CB_VAULT_KEY environment variable, and persisted to /data/.env
when the app has to generate one itself.
Backing up CB_DATA_DIR therefore captures everything except the environment file that carries your
secrets — back that up separately, or use the full-state snapshot described in
Backup & Restore.
If you restore the database without the vault key, encrypted secrets such as Proxmox API tokens and SMTP passwords will no longer be readable.
Native install persistence¶
For native Linux installs, the important paths are:
| Path | Stores | Why it matters |
|---|---|---|
/var/lib/circuitbreaker |
Default CB_DATA_DIR: database, uploads, TLS certs, backups, Redis and NATS state |
Core persistent application state |
/etc/circuitbreaker/.env |
All generated secrets and runtime environment settings | Without it the vault cannot decrypt anything and the services will not start |
${CB_DATA_DIR}/tls |
TLS cert/key files | Required when HTTPS is enabled |
/opt/circuitbreaker |
Application files: bin, deploy, apps, and share/VERSION |
Reinstalled by the installer; nothing user-specific lives here |
Note the paths are unhyphenated — circuitbreaker, not circuit-breaker. There is no
config.yaml; configuration is the .env file.
4) WebSockets (WSS)¶
Discovery, topology, and status dashboards use WebSockets for live updates. In production you must use WSS (WebSocket over HTTPS) so the auth token is not sent in the clear.
- Use HTTPS: nginx terminates TLS in both shipped install paths, so connect to
wss://your-host/...and the WebSocket is tunneled over TLS. Plainws://is only suitable for local development. - Cookie-based auth: When the app is served from the same origin, the session cookie (
cb_session) is sent automatically with the WebSocket handshake. Prefer this over sending the token as the first message so the token is never visible in client code. - Strict WSS only: Set
CB_WS_REQUIRE_WSS=truein the backend environment to reject any WebSocket connection that is not considered secure. Use this when the app is exposed and you want to forbid plain-WS access.X-Forwarded-Protois only believed from a trusted peer, so setCB_TRUSTED_PROXY_CIDRScorrectly first or this will reject every connection — see Remote Access.
5) Container isolation (Docker)¶
The shipped compose file runs one service, circuitbreaker, and there are no custom networks to
configure. Isolation is done at the container level instead:
| Control | Setting | Effect |
|---|---|---|
| Read-only root filesystem | read_only: true |
The image cannot be modified at runtime; only the mounts and tmpfs paths are writable |
| Writable scratch only | tmpfs on /tmp, /run, /var/log, /var/lib/nginx, /var/lib/postgresql |
Size-capped and discarded on restart |
| No privilege escalation | security_opt: no-new-privileges:true |
setuid binaries cannot gain privileges |
| Dropped capabilities | cap_drop: ALL |
Everything is dropped, then a named list is added back |
| Added capabilities | NET_RAW, NET_BIND_SERVICE, CHOWN, FOWNER, SETUID, SETGID, DAC_OVERRIDE |
SNMP/ICMP polling, binding low ports, the entrypoint's volume-ownership fix, and supervisor dropping privileges for postgres/nginx/nats/redis |
| Resource limits | deploy.resources.limits |
2 CPUs and 2 GB memory by default |
| Log rotation | logging options |
100 MB × 5 compressed files |
If you narrow these, start with the capability list: NET_RAW is only needed if you use SNMP or
ICMP polling, and NET_BIND_SERVICE only if CB_PORT is below 1024.
The Docker socket is not mounted by default. Discovery against the local Docker daemon is an
explicit opt-in overlay (docker/docker-compose.socket.yml).
6) Secret scanning (Gitleaks)¶
The repo includes a .gitleaks.toml allowlist so that Alembic migration revision IDs (hex strings like b4a9c1d2e8f0 in down_revision) are not reported as API keys. Those revision hashes are not secrets — they are version identifiers for the migration chain.
7) Practical Security Habits¶
- Rotate tokens on a regular schedule.
- Avoid sharing admin credentials.
- Review audit history for unexpected changes.
- Use least-privilege network access where possible.
- Keep your deployment updated.
8) Before You Go Live¶
- Confirm authentication behavior matches your policy.
- Confirm token and secret values are set and persisted.
- Confirm backups can be exported and restored.
- Confirm audit history is visible and reviewed.