Skip to main content
Version: v1.9.2

Security

The admin server exposes two listeners, and each has its own authentication surface. This page covers both, and spells out what the deliberate exceptions imply.

It is about the standalone fleet plane. For the in-process panel's own login, sessions and RBAC, see How it works.

Two auth planes

Operators (the UI listener)

The server does not implement OIDC or password login on the fleet UI. Identity comes from outside: the canonical deployment puts an auth-aware reverse proxy (oauth2-proxy, nginx auth_request, Traefik forward-auth) in front of --ui-addr, and that proxy forwards the authenticated user in headers.

A request is authenticated as an operator when either of the following succeeds, checked in this order:

  1. Trusted-proxy headers. The request comes from an IP inside --ui-trusted-cidrs (default: loopback only), carries a non-empty identity header (X-Auth-User by default; rename with --ui-auth-header), and — when --ui-proxy-secret is set — echoes that secret in the fixed X-Auth-Proxy-Secret header. Optional headers carry the email (X-Auth-Email) and the role (X-Auth-Role).
  2. Bearer fallback. The request carries Authorization: Bearer <token> matching --ui-bearer. Useful without a proxy (development, small trusted networks). When --ui-bearer is empty, the fallback is disabled.

Failures get a generic 401 — the response does not reveal which credential mode was attempted or why it failed.

Set --ui-proxy-secret in production. Without it, CIDR membership alone is enough to claim any identity: a sidecar, a host-networked container, or another local process behind the same NAT can forge an operator just by setting the header.

The secret is compared in constant time. A request that fails the check falls through to the bearer path rather than being rejected outright, so a misconfigured proxy never blocks a valid bearer.

Agents (the agent listener)

Agents authenticate with a single shared bearer token: --agent-token on the server, Token in the agent's configuration. The agent attaches it to every call, including the long-lived telemetry stream itself. Token comparison is constant-time.

The server is fail-closed here: it refuses to start the agent listener on a non-loopback interface with no token and no TLS. The --insecure-agent-listener override exists for networks where a firewall, private subnet, or service-mesh mutual TLS already restricts reachability, and the server logs a warning at boot when you use it.

Treat access to the agent listener as fleet-write access. An unauthenticated one lets any host on the network register as an agent and feed the fleet plane.

The /healthz exemption

/healthz answers 200 ok on both listeners, and on the metrics listener, without authentication. That is intentional: load balancers and the agent's endpoint-failover dialer need to probe reachability without owning a token. It has two consequences worth knowing.

  • Anyone who can reach a listener can learn that an Orbit admin server is running there. Nothing else is exposed without credentials.
  • Reachable is not authenticated. The agent's dial probe hits /healthz, so a booting agent can find the server "reachable" while its token is being rejected on the stream. The boot-time RequireConnection gate does not trust that probe. It passes only once the admin server accepts the agent's first stream frame under authentication, so a wrong token fails the application's boot at the configured deadline — with the token-rejected warnings described below explaining why.

Read-only operators

Two mechanisms, verified in the auth chain on every request:

  • Per operator: the trusted proxy sets the role header (X-Auth-Role: viewerreadonly and read-only also work, case-insensitively). That operator can use every read surface, but Data Studio mutations (create, update, delete, bulk) are refused. Any other value, including no header, keeps the operator read-write.
  • Globally: --ui-read-only marks every operator read-only, turning the server into a pure observability plane.

Know the default they are scoping down from: any read-write operator can run every Data Studio mutation on every model of every connected node. The fleet Access control screen does not restrain that — it is a read-only snapshot of each node's policy, not a per-verb gate on the operator.

Mutations are attributed and recorded in the server's fleet Audit log, which tells you afterwards who did what. If some operators should not be writing at all, use the role header or run the whole server read-only.

Credential lockout

Both listeners keep a small per-IP lockout: 20 wrong credentials within a minute lock that IP out with 429 Too Many Requests until the window expires.

Only requests that actually presented a wrong credential count — a bad bearer, or a wrong proxy secret alongside a bearer attempt. Credential-less requests never do, so a browser hitting the SPA before signing in is harmless and nobody can lock operators out by poking the login page.

The limiter exists to make online brute force of the shared tokens impractical. It is not a general-purpose WAF.

Rejected tokens are loud

A bad agent token announces itself from both ends, so it cannot fail quietly:

  • Server side: a warning naming the remote IP — admin server rejected agent request: invalid or missing bearer token — rate-limited to one per minute per IP, with a count of the rejections suppressed in between.
  • Agent side: admin agent token rejected by admin server; check --agent-token, at most once per minute per endpoint.
  • Backoff: the agent's reconnect backoff resets only after the server has demonstrably accepted the stream, meaning the first frame was received. A rejected token therefore retries at growing intervals up to 30 seconds rather than once per second forever, and the agent's connected log line appears only on real acceptance.

Browser-facing headers

Every response on the UI listener carries:

  • Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self' — the SPA is fully self-contained, so a strict CSP costs nothing ('unsafe-inline' is needed for styles only).
  • X-Content-Type-Options: nosniff
  • X-Frame-Options: DENY
  • Referrer-Policy: no-referrer

TLS

Both listeners speak HTTP/2: cleartext (h2c) by default, TLS 1.2+ with HTTP/2 negotiated through ALPN when a PEM pair is supplied (--agent-cert/--agent-key, --ui-cert/--ui-key). A TLS listener serves nothing in the clear: a plain http:// request fails the handshake. Typical production setups either terminate TLS at the reverse proxy (UI listener) and give the agent listener its own certificate, or keep both listeners on a private, mesh-encrypted network.

A server certificate encrypts the wire; it does not authenticate the agent. For that the agent listener supports mutual TLS: pass --agent-client-ca (a PEM CA bundle) and every agent must present a certificate signed by it — the handshake rejects the rest before any RPC is read, and the certificate's Common Name becomes the agent's identity (agent:<CN>). Mutual TLS satisfies the fail-closed startup rule on its own; a certificate without --agent-client-ca does not, so pair it with --agent-token.

Agents accept https:// endpoints and use the system trust store by default. For a private CA, or to present a client certificate, set the TLS field of the agent's configuration (agent.ExtensionConfig.TLS, a *tls.Config built from your PEM files) — see the agent page.

The metrics listener

--metrics-addr (empty by default — disabled) serves Prometheus /metrics and /healthz without authentication, by design. Bind it to a private interface (127.0.0.1:9091, a node-internal address), exactly as you would any metrics port.

Hardening checklist

  • --agent-token set (or mutual TLS via --agent-client-ca), and not passed on the command line in production — use NUCLEUS_ADMIN_AGENT_TOKEN from a root-only environment file.
  • --insecure-agent-listener not set.
  • UI listener behind an SSO reverse proxy; --ui-trusted-cidrs narrowed to the proxy's real source range.
  • --ui-proxy-secret set and echoed by the proxy in X-Auth-Proxy-Secret.
  • --ui-bearer empty in proxy fronted setups (leave the fallback off unless you need it).
  • Operators who only observe get X-Auth-Role: viewer; a pure observability deployment runs --ui-read-only.
  • TLS on any listener that crosses a network you do not fully trust.
  • --metrics-addr bound to a private interface, or left disabled.
  • Log pipeline alerts on the two token-rejected warnings and on the boot warning about an exposed agent listener.