Hardening checklist
One page to go through before a backd deployment faces the internet: realms, credentials, the edge, MongoDB and operations.
Go through this page before any realm faces the internet, and again after changing the configuration or the deployment. The production reference deployment implements the deployment items, and its test checks many of them.
Realms
- No realm has
auth: disabled. Such a realm is open to anyone who can reachbackd.backdlogs aWARNline for each one at startup. - The sample realms aren’t deployed as they are.
blog,shopandexpensesinexamples/config, and the realmsbackd template realm --samplecreates, are examples:shophasauth: disabled,blogandexpenseslet anyone sign up, andexpenseshas known holes. Start from them, then review every setting. - Each realm’s
signupmode is the intended one.openlets anyone create an account (and needs the sign-up rate limit below),inviteneeds an invitation,closedleaves it to operators. - Every collection that users or anonymous callers need has a reviewed
rules.yaml, with arules.test.yamlthatbackd rules test --strictruns in CI: see Testing rules. A missing rule denies; a too-broad one (read: "true") publishes a collection. See access rules. -
cors.originslists exactly your web apps’ origins, and no*for realms with users. Browser apps can keep sessions in an HttpOnly cookie (sessions.cookie), which needs this list. - Someone can administer each realm: an admin role (
admin: true) held by the right people, the first created withbackd bootstrap. Startup warns when nobody holds an admin role. - Administrators hold only what they use: besides one or two people with
admin: true, roles for the rest list their areas (admin: [users, invitations],admin: [audit]), so the person who invites teammates doesn’t hold the secrets and the API keys. - If you run functions: the executor and
backd’s internal listener (BACKD_INTERNAL_ADDR) are reachable only from each other, never published;BACKD_EXECUTOR_TOKEN,BACKD_CALLBACK_KEY,BACKD_EGRESS_KEYand, if any function declares secrets,BACKD_SECRETS_KEYare random, 32+ characters, kept as secrets (never inCONFIG_DIR) — the production reference’ssetup.shgenerates all four;admin: truefunctions andnetworklists are reviewed. - The executor can reach only
backd egressandbackd’s internal listener — nothing else, including MongoDB, is on its network. This is layer 3 of the egress design: without it, a function can reach anything its container’s network reaches over raw TCP, bypassing both the per-function allowlist and the egress proxy. The production reference wires this up (its own test proves it from a real function call); if you assemble your own deployment instead,docker/egress-test/in the repository proves the isolation and is the reference to reproduce. - Every function callable anonymously has a
rate_limit. Startup warns, once, for any that don’t — a public function with no per-caller limit can be abused as a relay (e.g. one that sends email). - Every
webhookfunction verifies its sender’s signature itself, before trusting the body.backdonly enforces that itsinvokerule allows anonymous callers (it has to — a webhook sender has no session or key); it can’t tell a genuine request from a forged one. -
BACKD_SECRETS_KEY(the master key) is backed up separately from database backups, never alongside them: losing it loses every stored function secret, and storing it with a backup defeats that backup’s own encryption. See Backup and restore. -
functions.log_retentionandfunctions.job_retention(function logs, async jobs) match your privacy policy: both keep a function’s own console output, and a job additionally keeps its input and output, in the realm’s system database until they expire. - Someone reviews the audit trail (
backd audit --realm <realm> --since 7d, or the"msg":"audit"log lines in your log pipeline), andaudit.retentionmatches your privacy policy. - Operators’ machines protect the CLI’s sessions (
~/.config/backd/credentials): log out withbackd logouton shared machines, and prefer short session lifetimes for realms with administrators.
Credentials
- Every API key has an expiry (
backd apikey create … --expires 90d), is used by one service, lives in a secret store, and is rotated before it expires. See the expiry and rotation policy. Startup warns about keys without an expiry. - API keys never reach browsers or mobile apps.
- Internet-facing instances serve no admin API when an internal instance does:
BACKD_ADMIN_API=falseon the public ones (the production reference runs the split), and the CLI’sBACKD_URLpoints at the internal instance. - The admin UI, if used, is served only by the internal instance (
BACKD_ADMIN_UI=truethere, nothing on the public one), on its own origin, over TLS, with the proxy passing itsContent-Security-Policyand other headers through unchanged.BACKD_ADMIN_UI_IDLEis short and the realm’ssessions.admin_idle_timeoutandadmin_max_lifetimeare shorter still: the page’s timer is a convenience, the server’s is the limit. See its security review. - Developers who only look hold
admin: read, notadmin: true, andadmin.read_access.users/.datastayfalseunless they need personal data. See The read-only level. - Admin sessions are short:
sessions.admin_idle_timeoutandsessions.admin_max_lifetimein each realm’srealm.yaml(for example1hand12h), so a stolen administrator’s session is worth little. See Sessions. - Keys are scoped to what their service does (
--scopes read:main/posts,call:main/contact): a leaked key then exposes that and nothing more. See Scopes. - Services use
datakeys;adminkeys (--role admin) exist only for management tooling. - Secrets (MongoDB passwords, TLS keys, the CA key,
.env) are outside version control and readable only by the services that need them.
The edge
- TLS everywhere: the edge terminates TLS with a trusted certificate that renews automatically, and plain HTTP only redirects. MongoDB connections use TLS too (below).
- Rate limits: sign-up, login and the account flows (emailed links, password and address changes) have a strict per-address limit, and the rest of
/v1/a general one. Requests over a limit get429. - Body size: the edge caps request bodies at
MAX_BODY_BYTES. - Client addresses: the edge overwrites
X-Forwarded-For, andTRUSTED_PROXIESnames exactly the edge’s addresses, never whole private ranges.backd’s access log shows real client addresses. - Only the edge is reachable:
backdisn’t published directly, and/healthzand/readyzaren’t public. - The admin API is only reachable from your operators’ network:
admin.allowed_networksin eachrealm.yaml(andadmin_networksper admin where useful), plusADMIN_ALLOW_FROMat the edge in the reference. - The metrics port is private (
METRICS_ADDRis never published or proxied, andMETRICS_TOKENis set if anything but Prometheus can reach it). - API keys of services with fixed addresses are pinned to them with
--networks.
MongoDB
- MongoDB requires TLS and authentication, and isn’t reachable from the internet.
-
backdruns withPROVISION_MODE=verifyas a user limited to the configured collections; admin credentials are only available to the provisioning step. See least-privilege provisioning. - Encrypted backups of every data and
<realm>___systemdatabase run on a schedule, are copied off the server, and a restore has been practised. The decryption key isn’t on the server. See Backup and restore.
Operations
-
LOG_LEVELisinfo(the default).debuglogs every access denial with its reason, which is useful while writing rules but noisy and revealing in production. - Logs are collected and kept, and startup
WARNlines are reviewed: realms withauth: disabled, realms without rules, API keys without an expiry or about to expire, anonymous-callable functions with norate_limit, and (if you run functions) an executor that isn’t configured or doesn’t answer. - The container runs hardened: read-only filesystem, no Linux capabilities, CPU and memory limits (they also bound password hashing), and a stop grace period longer than
SHUTDOWN_TIMEOUT. - You know how to report and receive security fixes: see
SECURITY.md. Only the latest release gets them.