This guide summarizes security-related settings and best practices when deploying Nexus GSLB.
Principles
- Secure by default: HTTPS health checks verify TLS by default; GitOps requires signed commits when enabled.
- Least privilege: limit credentials and subject permissions to the minimum required.
- Defense in depth: mTLS for NATS, signed GitOps config, and restricted runtime privileges.
- HTTP/HTTPS health checks
- Default behavior: when
health.http.tls: true, TLS certificates are verified. - Only set
health.http.insecureSkipVerify: truefor lab/testing or when using self-signed certs and you fully understand the risk. - Prefer using proper certificates (internal CA or public CA) and correct
http.hostto match the cert.
Example (secure):
health: type: http port: 443 checkinterval: 10s timeout: 2s http: path: "/healthz" expectedstatus: 200 tls: true insecureSkipVerify: false
- GitOps signing and access
gitops.requireSignature: trueenforces signature verification on the latest commit checked out.gitops.allowedSignersrestricts accepted signatures to a set of GPG key fingerprints.- Deploy key: provide a read-only SSH key at
gitops.auth.sshKeyPath; permissions should be0600and owned by the service user. - Trust model: ensure the signer keys are trusted on hosts or rely on
allowedSignersallowlist.
- NATS and JetStream
- Use TLS with server authentication at minimum; prefer mTLS (client cert auth) or NATS accounts with JWT.
- Restrict subjects and KV permissions to the cluster namespace:
gslb.<cluster>.*. - Rotate client certificates and credentials regularly. Plan for CA rotation.
- Monitor
gslbd_state_nats_connectedand set alerts on disconnect.
- Runtime privileges
- Binding to high ports (≥1024) avoids root. Default DNS port is 5353; if you must use port 53, either:
- run as root (not preferred), or
- grant
cap_net_bind_serviceto the binary:setcap 'cap_net_bind_service=+ep' /usr/local/bin/gslbdand run as non-root.
- Limit file system permissions for
/etc/gslband any private keys.
- Secrets and licensing
- Provide license secrets via environment variables when possible:
GSLB_LICENSE_SECRETGSLB_LICENSE_KEY
- Use environment management in systemd (
Environment=) or container secrets.
- Supply chain and binaries
- Build from source in a controlled CI/CD or use signed release artifacts (future work).
- Pin container base images and scan them for vulnerabilities.
- Logging and PII
- Logs contain operational info (e.g., commit SHAs, signer fingerprints) but not sensitive payloads. Avoid logging credentials.
- Authentication and user accounts
- Set
GSLB_API_KEY(32-byte hex,openssl rand -hex 32) in the systemd EnvironmentFile before exposing the API. Without it, the daemon runs in dev mode — all requests are treated as system admin. - Set
GSLB_SECRET_KEY(also 32-byte hex) before enrolling any TOTP devices. The daemon will refuse to start if TOTP users exist and the key is absent. - Store both values in
/etc/gslb/envwithchmod 600, owned by the service user. Reference it from the systemd unit withEnvironmentFile=/etc/gslb/env. - Create named user accounts (
tenant_admin,operator,viewer) instead of sharing the system API key. The system key is a break-glass credential, not a day-to-day login. - Treat
GSLB_API_KEYas a root password: rotate it if it is ever exposed, and restrict who knows it. - Keep
bcryptCostat 12 or higher in production.
See Authentication & User Management for full setup instructions.
Checklist
- DNSSEC Pleiades signs DNS responses online using ECDSA P-256 (algorithm 13). KSK signs the DNSKEY RRset; ZSK signs all other RRsets.
Key generation:
<span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="co"># KSK</span></span><span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a><span class="ex">openssl</span> ecparam <span class="at">-name</span> prime256v1 <span class="at">-genkey</span> <span class="at">-noout</span> <span class="kw">|</span> <span class="dt">\</span></span><span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a> <span class="ex">openssl</span> pkcs8 <span class="at">-topk8</span> <span class="at">-nocrypt</span> <span class="at">-out</span> /etc/gslb/ksk.pem</span><span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a></span><span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a><span class="co"># ZSK</span></span><span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a><span class="ex">openssl</span> ecparam <span class="at">-name</span> prime256v1 <span class="at">-genkey</span> <span class="at">-noout</span> <span class="kw">|</span> <span class="dt">\</span></span><span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a> <span class="ex">openssl</span> pkcs8 <span class="at">-topk8</span> <span class="at">-nocrypt</span> <span class="at">-out</span> /etc/gslb/zsk.pem</span><span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a></span><span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a><span class="fu">chmod</span> 640 /etc/gslb/ksk.pem /etc/gslb/zsk.pem</span><span id="cb2-10"><a href="#cb2-10" aria-hidden="true" tabindex="-1"></a><span class="fu">chown</span> root:gslbd /etc/gslb/ksk.pem /etc/gslb/zsk.pem</span>
Configuration:
<span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="fu">dnssec</span><span class="kw">:</span></span><span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">enabled</span><span class="kw">:</span><span class="at"> </span><span class="ch">true</span></span><span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">zone</span><span class="kw">:</span><span class="at"> </span><span class="st">"example.com."</span></span><span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">ksk</span><span class="kw">:</span></span><span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">pemFile</span><span class="kw">:</span><span class="at"> </span><span class="st">"/etc/gslb/ksk.pem"</span></span><span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">expiryDate</span><span class="kw">:</span><span class="at"> </span><span class="st">"2027-01-01"</span></span><span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">zsk</span><span class="kw">:</span></span><span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">pemFile</span><span class="kw">:</span><span class="at"> </span><span class="st">"/etc/gslb/zsk.pem"</span></span><span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">expiryDate</span><span class="kw">:</span><span class="at"> </span><span class="st">"2026-07-01"</span></span><span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">signatureValidityDays</span><span class="kw">:</span><span class="at"> </span><span class="dv">7</span></span>
Alternatively load keys from environment variables:
<span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="fu">dnssec</span><span class="kw">:</span></span><span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">enabled</span><span class="kw">:</span><span class="at"> </span><span class="ch">true</span></span><span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">zone</span><span class="kw">:</span><span class="at"> </span><span class="st">"example.com."</span></span><span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">ksk</span><span class="kw">:</span></span><span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">envVar</span><span class="kw">:</span><span class="at"> </span><span class="st">"GSLB_KSK_PEM"</span></span><span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">zsk</span><span class="kw">:</span></span><span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">envVar</span><span class="kw">:</span><span class="at"> </span><span class="st">"GSLB_ZSK_PEM"</span></span>
Key rotation:
- Generate new ZSK; update config to point at new file.
- Restart
gslbd— new signatures use new ZSK immediately. - For KSK rotation, generate new KSK, export new DS record, submit to registrar, wait for TTL expiry, then restart.
- Monitor
gslbd_dnssec_key_days_remainingand alert before expiry.
DS record export:
<span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="ex">gslbctl</span> dnssec ds <span class="at">--zone</span> example.com. <span class="at">--ksk-file</span> /etc/gslb/ksk.pem</span>
- Health-check safety (script and webhook checks) Health checks can execute code or make outbound requests from the node, so both kinds are hardened by default.
Script checks (
type: script) run arbitrary commands as thegslbduser on every node and are disabled by default. Enable them only if you trust every operator who can create a health check:<span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="fu">health</span><span class="kw">:</span></span><span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">allowScriptChecks</span><span class="kw">:</span><span class="at"> </span><span class="ch">true</span><span class="co"> # off by default</span></span>Even when enabled, a script check may only be created via the API by a system administrator — never a tenant/operator key. Prefer a
webhookcheck to run custom logic off-node.Webhook checks (
type: webhook) makegslbdrequest a URL you supply. To prevent Server-Side Request Forgery (SSRF) they may target public addresses only by default — loopback, link-local (including the cloud metadata IP169.254.169.254), private/ULA, and CGNAT ranges are blocked. This is enforced both when the check is created and at request time on the resolved address (defeating DNS rebinding; redirects are not followed). To allow a specific internal host, list its CIDR/IP:<span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="fu">health</span><span class="kw">:</span></span><span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">webhookAllowedHosts</span><span class="kw">:</span></span><span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="kw">-</span><span class="at"> </span><span class="st">"10.20.0.0/24"</span><span class="co"> # internal monitoring subnet</span></span><span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="kw">-</span><span class="at"> </span><span class="st">"192.0.2.50"</span><span class="co"> # a single host</span></span>Public destinations remain allowed; entries override the deny list for those ranges only. See Health Checks for details.
- API behind a reverse proxy If
gslbd's API sits behind a reverse proxy (Caddy, nginx, a load balancer), setapi.trustedProxiesto the proxy's CIDR(s). The real client IP is then read fromX-Forwarded-For(rightmost entry that is not itself a trusted proxy) for requests originating from those sources; otherwise the connection address is used andX-Forwarded-Foris ignored (so it cannot be spoofed by direct clients).
<span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="fu">api</span><span class="kw">:</span></span><span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">trustedProxies</span><span class="kw">:</span></span><span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="kw">-</span><span class="at"> </span><span class="st">"10.0.0.0/8"</span></span>
This is required for correct RUM client attribution and per-client rate limiting behind a proxy — without it every visitor collapses into the proxy's single IP. Leave it empty when the API is exposed directly.
- WebSocket authentication The live-event WebSocket endpoint (
/api/v1/ws) is gated by a short-lived JWT signed withGSLB_WS_SECRET, falling back toGSLB_API_KEY. When neither is set (dev mode) the daemon generates a random per-process secret and logs a warning — it never signs tokens with a shipped constant. SettingGSLB_API_KEYin production (section 0) covers this automatically.
NSEC3: denial-of-existence records are generated on-the-fly per RFC 5155 §7.1. No static NSEC3 chain is maintained.