3.5 Security hardening
A fresh install is built to get you running, not to be locked down: it seeds admin/admin, ships with account lockout off, and leaves the appliance Web UI open to any source. This module is the checklist to work through before an install carries production DNS and DHCP. Each step says why it matters, where the control lives, and which doc backs it. Work through them in order; the early steps close the biggest gaps.
What you’ll learn
Section titled “What you’ll learn”- Close the defaults that ship open: the admin account, lockout, sessions and install secrets
- Apply least privilege to people, API tokens and Windows service accounts
- Protect transport and keys: Web UI TLS, agent bootstrap keys, TSIG and dynamic-update ACLs
- Keep evidence and recovery durable with forwarded audit, a verified hash chain and drilled backups
- Shrink the attack surface: network exposure, outbound calls, DEMO_MODE, support bundles and unused features
What ships open
Section titled “What ships open”| Setting | Ships as | For production |
|---|---|---|
| Admin account | admin/admin, with a forced change at first sign-in |
Named accounts for each admin, with MFA |
| Account lockout | Threshold 0 (off) | A non-zero threshold |
| API token expiry | 90 days suggested, Never allowed | Always an expiry |
| Appliance Web UI access | Open to any source | Your management CIDRs |
| Appliance SSH | Open to any source | Your management CIDRs, once console access is tested |
| Appliance fleet firewall | Off, behind two gates | On, once every node is hardened |
| GitHub release check | On | Off if the install must stay offline |
| DNS agent approval | Off | On only after you’ve tested it (see the note after step 6) |
| Service start/stop control | Off, except on the appliance | Leave it off on Compose and Helm |
Several defaults now ship closed. They explain errors you may meet after an upgrade from 2026.09.04-1:
- The api refuses to start on the
.env.exampleplaceholderSECRET_KEY, or on a key under 32 characters (#1222). - Compose publishes the API port on
127.0.0.1only (API_BIND, #1221). - Remote agents verify the control plane’s TLS certificate (#1220).
/metricsneeds a bearer token,/api/openapi.jsonneeds a session or an API token, and an anonymousGET /api/v1/versionanswers onlyrestricted(#1159, #2935).- Sign-in is throttled: 30 attempts a minute per source, 20 failed passwords per 15 minutes per username, and 500 per 15 minutes across the install. The budgets fail open when Redis is down, and the install-wide one is a bounded denial of service that anyone who reaches the sign-in page can spend.
- Recursive BIND9 and Technitium groups answer recursive queries only from localhost and private networks; add public or CGNAT client ranges to the group’s
allow_recursionif they need it (#1817). - A fresh install turns on 16 of the 57 feature modules (#1069).
The checklist
Section titled “The checklist”People and access
Section titled “People and access”-
Replace the default admin, and set the password, lockout and session policy.
Why: every install seeds
admin/admin, and account lockout ships off. Where: create a named account for each administrator, then work through Settings → Security: Password Policy (by default at least 12 characters with an uppercase letter, a lowercase letter and a digit, a history of 5, and no maximum age), Account Lockout (set a failure threshold; the lockout and reset windows default to 15 minutes) and Session & Security (session timeout, 60 minutes by default). A fresh install applies that policy and timeout from the first password, before anyone saves Settings (#3203).On Compose, change
POSTGRES_PASSWORD(defaultchangeme) and setSECRET_KEYfromopenssl rand -hex 32before the first start; the api won’t start on the placeholder. To replace a key an install already uses, follow RotatingSECRET_KEY, which re-encrypts every stored credential; if the old key was a placeholder, review API tokens, users and superadmins afterwards. On Helm, pin the key withauth.existingSecret. Source: Auth, System administration §6, Docker Compose §3 and §8. -
Turn on MFA, for SSO admins too.
Why: TOTP adds a second factor to local sign-in. SSO users still sign in at their identity provider, but secret reveals (agent bootstrap keys, pairing codes, the appliance kubeconfig, the SNMP community) and minting an API token re-check the operator, and an SSO account has no local password to check (#1355). Where: the two-factor section of your Account page (Set up authenticator). Store the ten recovery codes somewhere safe; each works once. Wrong step-up answers spend a per-account budget, after which the API answers
429for a while.If an account enrolled MFA on a release from 2026.05.05-1 to 2026.06.13-1, disable and re-enroll it: that enrollment screen sent the TOTP secret to a third-party QR-code service. Source: Auth: TOTP MFA, Privacy.
-
Grant least privilege, to people and to tokens.
Why: a role is a list of
{action, resource_type, resource_id}entries, and an entry scoped to one resource never satisfies a check on the whole type. You can hand out one subnet or zone without handing out all of them. Where: put people in groups that carry built-in roles (Viewer, IPAM Editor, DNS Editor, DHCP Editor, Auditor and others), and keep Superadmin for the few who need it; Ridgeline’sHelpdeskgroup gets Viewer. A custom role needs a grant on each type it reads: since 1.0.0-beta.3 one IPAM or DNS grant no longer reads the rest of IPAM or DNS (#3058, #3073).For automation, create tokens under Administration → Identity → API Tokens with scopes (
read,ipam:write,dns:write,dhcp:write) and an expiry; creating one asks for your password, or an authenticator code on an SSO account. A token can never do more than both its scopes and its owner’s roles allow. Windows service accounts belong inDnsAdminsfor DNS andDHCP Administratorsfor DHCP writes and failover (DHCP Usersonly for read-only mirroring), never Domain Admins. Source: Permissions, Auth: API tokens, Windows setup §5.
Transport and keys
Section titled “Transport and keys”-
Serve the Web UI over TLS, and set the External URL.
Why: sign-in, tokens and secret reveals all cross this connection, and SAML with a hosted identity provider only works over HTTPS (#873). Where: on the appliance, replace the self-signed certificate under Appliance → Fleet → Web UI Certificate: upload one, generate a CSR, or issue one from Let’s Encrypt with the built-in ACME client, which can renew it automatically. On Compose, put a reverse proxy or Caddy in front, or enable TLS in the frontend’s nginx; on Helm, use an Ingress with cert-manager. Then set the public
https://address as the External URL (Settings → Application → Branding & URL), which OIDC and SAML need.Behind your own reverse proxy, list only the proxy in
FRONTEND_TRUSTED_PROXY_CIDRS(Helmfrontend.trustedProxyCidrs), leaveTRUSTED_HOSTSunset or exact, and turn response buffering off with a read timeout of at least an hour for the streaming features. Without the trusted proxy, every user shares one login-throttle bucket and SSO callbacks are built ashttp://.Agents verify the control plane’s certificate by default. For a private CA, mount it into standalone agents and set
TLS_CA_PATH; skipping verification is for labs only. Appliances pin the certificate on first contact: compare the fingerprint the supervisor logs with Appliance → Fleet → Web UI Certificate. Source: ACME client, Running behind a reverse proxy, Docker Compose §5 and §10, Auth: SAML. -
Guard the agent bootstrap keys and pairing codes.
Why: the bootstrap key is what lets a DNS or DHCP server register with your control plane. Where: generate
DNS_AGENT_KEYandDHCP_AGENT_KEYwithopenssl rand -hex 32and keep them in your secret store. Reveal them only through Settings → Security → Agent bootstrap keys, which re-checks your password or TOTP code and writes an audit row. Rotating a key doesn’t disturb agents that already hold a token.Standalone Docker and Kubernetes agents use the bootstrap key; an appliance Additional node pairs with an 8-digit code instead. Ephemeral codes are single-use and expire (15 minutes by default). Persistent codes expire after 30 days by default (
0means never), so give them amax_claimslimit and revoke them when you’re done. Wrong codes are throttled (#1356). Source: DNS agent §6, OS appliance §9. -
Sign updates and transfers with TSIG, and scope dynamic updates.
Why: an unsigned RFC 2136 update or zone transfer is only as safe as the source address it trusts, and the docs flag IP-based entries as spoofable over UDP. Where: agent-managed BIND9 already signs every record change with TSIG over loopback, and the control plane signs its own zone transfers from BIND9 and Technitium with the group’s key. That group key has been encrypted at rest since 1.0.0-beta.1. On an install upgraded from 2026.09.04-1, rotate it once in the group’s Edit Group dialog (Group TSIG key, Rotate…), because the old plaintext copy is kept for one release (#1364).
For external writers (a domain controller, a DHCP server), set the zone’s dynamic-update ACL and grant by TSIG key rather than IP; on BIND9 you can also limit a key to certain names and record types. On Windows, any IP entry becomes
NonsecureAndSecure, which accepts any nonsecure client, and the API warns you. Source: DNS drivers §8, DNS §12, System administration §2.9.
Evidence and recovery
Section titled “Evidence and recovery”-
Forward the audit log, and watch the hash chain.
Why: an audit trail that lives only on the box it describes can be lost with the box. Where: Settings → Security → Audit Event Forwarding sends audit rows to syslog (UDP, TCP or TLS; RFC 5424 with JSON, CEF or LEEF, RFC 3164, or JSON lines), an HTTP webhook, or SMTP. Delivery is best-effort after commit, so for at-least-once delivery point a webhook at a queue, and know that some scheduled-task audit rows may not forward yet. Credential-shaped values arrive as
[redacted].The Audit Log page shows Chain verified or Tampering detected. A nightly job checks the rows added since its last run and re-walks the whole chain about once a week, so an edit to an old row can take about eight days to raise the critical
audit-chain-brokenalert;GET /api/v1/audit/integritywalks the chain on demand. Source: Observability §5, System administration. -
Back up with a passphrase you keep elsewhere, and drill the restore.
Why: each archive carries your install’s encryption keys wrapped in your passphrase. Lose the passphrase and you can’t restore the archive. And only that key envelope (
secrets.enc) is encrypted: the database dump beside it isn’t, so anyone who can read an archive can read the whole database, although the stored credentials inside stay encrypted. Where: the Backup page under Administration. Schedule archives to a remote destination whose access you restrict, with a retention rule, and keep the passphrase (8 characters or more) in your password manager, not beside the archive.Schedule Restore Drills (weekly drills against nightly backups is the documented shape) so a broken archive raises
restore_drill_failed; thebackup_failedandbackup_stalealerts are on by default. Take a manual backup before every upgrade. On the appliance, also keep a copy of thespatium-control-app-keysSecret, which holdsSECRET_KEY.On Docker Compose the release file mounts the
spatium_backupsvolume on the api and worker for Local volume targets, and Test connection warns when a target’s path isn’t on a mounted volume. The 2026.09.04-1 file shipped that volume commented out, so before upgrading from it, copy existing archives out of both containers (docker compose cp api:/var/lib/spatiumddi/backups ./backups-api, and the same forworker). Source: System administration §2.9, Docker Compose §9.
Exposure
Section titled “Exposure”-
Expose only what clients need.
Why: PostgreSQL, Redis and the raw API port have no business on the network. Where: keep 5432 and 6379 internal, and publish only the Web UI, DNS and DHCP. On Compose the API port is published on
127.0.0.1only by default (API_BIND=127.0.0.1); if you ever widenAPI_BIND, narrowTRUSTED_PROXY_CIDRSto the frontend’s address. An.envthat setsAPI_PORT=127.0.0.1:8000, as this course once advised, breaks the port mapping from 2026.10.02-1 on: setAPI_PORTback to a bare port number before you upgrade.The interactive API docs (
/api/docs,/api/redoc) work through the web port for signed-in users, and/api/openapi.jsonneeds a session or an API token./metricsneeds a bearer token, either the scrape token (PROMETHEUS_METRICS_TOKEN) or any API token, so give each Prometheus a read-only token of its own.On appliances, enable the fleet firewall (module 3.3) and restrict Web UI access to your management CIDRs; its guard refuses a list that doesn’t cover your own address. SSH on 22 stays open by default, so a bad Web UI scope is recoverable over SSH, unless you also restrict SSH with Enforce source restriction (#1009). A change that would leave no remote way in at all needs an explicit console-only acknowledgement, and then the console is your last resort. For Windows, allow WinRM (5986, HTTPS) and port 53 only from the SpatiumDDI host, and restrict zone transfers to it. Source: Docker Compose §1 and §3, Observability §6, OS appliance: fleet firewall, Windows setup §5.
-
Decide on outbound connections, or go air-gapped.
Why: a DDI server shouldn’t talk to the internet unless you chose that. Where: the one outbound connection that’s on by default is a daily GitHub release check: an anonymous GET to GitHub’s public API that sends nothing about your install. Turn it off under Settings → Application → Updates → Check for GitHub Releases. The privacy page lists every feature that reaches a third party (Fingerbank device profiling, the Copilot’s LLM provider, Let’s Encrypt, blocklist feeds, cloud DNS and integration mirrors, the whois and RBL tools), and each stays off until you configure it.
The appliance boots from baked images without contacting any registry and takes upgrades from uploaded images. Its host uses
pool.ntp.orgfor time and Debian’s APT mirrors unless you point them at your own (the installer’s Time source screen, then Appliance → Fleet → Services). Source: Privacy §2 and §3, System administration §7, OS appliance. -
Never run DEMO_MODE in production.
Why: DEMO_MODE exists for the public demo. It locks the abusable surfaces server-side (nmap, AI provider creation, webhook and integration targets, outbound mail, audit forwarding and backups, factory reset and password change), switches off a set of feature modules, skips the forced password change, and keeps sign-in at
admin/admin. Where: make sure no production.env, Helm values or automation ever setsDEMO_MODE, and treat a demo-mode banner on a real install as an incident. Source: the project README. -
Review a support bundle before you share it.
Why: bundles tend to end up attached to public GitHub issues, where attachments are effectively world-readable. Where: Administration → Diagnostics → Support bundle. Secrets (keys, password hashes, JWTs, PSKs, API tokens, TSIG keys) are removed in every mode, and IPs, hostnames, MACs and usernames become stable synthetic values. Scrubbing is best-effort, so read the preview before you download, and keep the decode map, which is never inside the archive, to yourself. The unscrubbed variant needs a typed confirmation and carries
UNSCRUBBEDin its file name. Source: System administration §9. -
Switch off what you don’t use.
Why: every module you leave on is surface that a mistake, or an attacker, can reach. Where: the feature-module catalog, Features & Integrations near the bottom of the sidebar. A disabled module disappears from the sidebar, and its API returns
404, as if it weren’t installed. After a migration, turn off the importers and the Windows cutover module (migration.cutover). Leave service start/stop control off on Compose and Helm: a restart-capabledocker.sockis equivalent to root on the host. Source: REST API §5, Getting Started: project status.
Knowledge check
Section titled “Knowledge check”Go deeper
Section titled “Go deeper”- Auth and permissions: local auth, TOTP, sign-in throttles, identity providers and API tokens
- Permissions model: the grammar, evaluation rules and built-in roles
- System administration: password and lockout settings, backup and restore, the support bundle
- Rotating
SECRET_KEY: replacing a placeholder or leaked key on Compose without losing stored credentials - Running behind a reverse proxy: trusted proxies, the External URL, headers and streaming
- Observability: the audit log, forwarding and the
/metricstoken - DNS agent: security: bootstrap keys, tokens, approval and TSIG handling
- Privacy: every outbound connection and how to turn it off