Security Guide

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.
  1. HTTP/HTTPS health checks
  • Default behavior: when health.http.tls: true, TLS certificates are verified.
  • Only set health.http.insecureSkipVerify: true for 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.host to match the cert.

Example (secure):

health:
type: http
port: 443
checkinterval: 10s
timeout: 2s
http:
path: "/healthz"
expectedstatus: 200
tls: true
insecureSkipVerify: false
  1. GitOps signing and access
  • gitops.requireSignature: true enforces signature verification on the latest commit checked out.
  • gitops.allowedSigners restricts accepted signatures to a set of GPG key fingerprints.
  • Deploy key: provide a read-only SSH key at gitops.auth.sshKeyPath; permissions should be 0600 and owned by the service user.
  • Trust model: ensure the signer keys are trusted on hosts or rely on allowedSigners allowlist.
  1. 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_connected and set alerts on disconnect.
  1. 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_service to the binary: setcap 'cap_net_bind_service=+ep' /usr/local/bin/gslbd and run as non-root.
  • Limit file system permissions for /etc/gslb and any private keys.
  1. Secrets and licensing
  • Provide license secrets via environment variables when possible:
    • GSLB_LICENSE_SECRET
    • GSLB_LICENSE_KEY
  • Use environment management in systemd (Environment=) or container secrets.
  1. 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.
  1. Logging and PII
  • Logs contain operational info (e.g., commit SHAs, signer fingerprints) but not sensitive payloads. Avoid logging credentials.
  1. 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/env with chmod 600, owned by the service user. Reference it from the systemd unit with EnvironmentFile=/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_KEY as a root password: rotate it if it is ever exposed, and restrict who knows it.
  • Keep bcryptCost at 12 or higher in production.

See Authentication & User Management for full setup instructions.

Checklist

  1. 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">&quot;example.com.&quot;</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">&quot;/etc/gslb/ksk.pem&quot;</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">&quot;2027-01-01&quot;</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">&quot;/etc/gslb/zsk.pem&quot;</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">&quot;2026-07-01&quot;</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">&quot;example.com.&quot;</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">&quot;GSLB_KSK_PEM&quot;</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">&quot;GSLB_ZSK_PEM&quot;</span></span>

Key rotation:

  1. Generate new ZSK; update config to point at new file.
  2. Restart gslbd — new signatures use new ZSK immediately.
  3. For KSK rotation, generate new KSK, export new DS record, submit to registrar, wait for TTL expiry, then restart.
  4. Monitor gslbd_dnssec_key_days_remaining and 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>
  1. 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 the gslbd user 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 webhook check to run custom logic off-node.

  • Webhook checks (type: webhook) make gslbd request 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 IP 169.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">&quot;10.20.0.0/24&quot;</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">&quot;192.0.2.50&quot;</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.

  1. API behind a reverse proxy If gslbd's API sits behind a reverse proxy (Caddy, nginx, a load balancer), set api.trustedProxies to the proxy's CIDR(s). The real client IP is then read from X-Forwarded-For (rightmost entry that is not itself a trusted proxy) for requests originating from those sources; otherwise the connection address is used and X-Forwarded-For is 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">&quot;10.0.0.0/8&quot;</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.

  1. WebSocket authentication The live-event WebSocket endpoint (/api/v1/ws) is gated by a short-lived JWT signed with GSLB_WS_SECRET, falling back to GSLB_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. Setting GSLB_API_KEY in 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.


Was this article helpful?