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:
- Trusted-proxy headers. The request comes from an IP inside
--ui-trusted-cidrs(default: loopback only), carries a non-empty identity header (X-Auth-Userby default; rename with--ui-auth-header), and — when--ui-proxy-secretis set — echoes that secret in the fixedX-Auth-Proxy-Secretheader. Optional headers carry the email (X-Auth-Email) and the role (X-Auth-Role). - Bearer fallback. The request carries
Authorization: Bearer <token>matching--ui-bearer. Useful without a proxy (development, small trusted networks). When--ui-beareris 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 mTLS 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-timeRequireConnectiongate 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: viewer—readonlyandread-onlyalso 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-onlymarks 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
connectedlog 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: nosniffX-Frame-Options: DENYReferrer-Policy: no-referrer
TLS
Both listeners speak HTTP/2: cleartext (h2c) by default, TLS 1.2+ when a
PEM pair is supplied (--agent-cert/--agent-key,
--ui-cert/--ui-key). 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.
Agents accept https:// endpoints and use the system trust store.
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-tokenset (or agent-listener TLS), and not passed on the command line in production — useNUCLEUS_ADMIN_AGENT_TOKENfrom a root-only environment file. -
--insecure-agent-listenernot set. - UI listener behind an SSO reverse proxy;
--ui-trusted-cidrsnarrowed to the proxy's real source range. -
--ui-proxy-secretset and echoed by the proxy inX-Auth-Proxy-Secret. -
--ui-bearerempty 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-addrbound 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.