orbit/server
The standalone observability server accepts agent connections
(AgentService.Stream) and serves the admin web UI plus its ControlService
API. Many agents stream to one server.
Run it
# from the server module (the UI bundle is embedded via go:embed)
cd server && go build -o bin/admin-server ./cmd/admin-server
./bin/admin-server # defaults: agents on :9090, UI on :8080
For local development, make the UI loadable from a plain browser (without a reverse proxy every UI request answers 401, because the SPA cannot present a bearer token):
./bin/admin-server \
--agent-addr=127.0.0.1:9090 \
--ui-addr=127.0.0.1:8080 \
--ui-insecure-open
# then open http://127.0.0.1:8080
--ui-insecure-open only works on a loopback --ui-addr (the server
refuses to start otherwise) and is for local development only.
A production-flavoured invocation:
./bin/admin-server \
--agent-addr=:9090 \
--ui-addr=:8080 \
--agent-token="$NUCLEUS_ADMIN_TOKEN" \
--agent-cert=/etc/nucleus/server.crt \
--agent-key=/etc/nucleus/server.key \
--ui-trusted-cidrs=10.42.0.0/16 \
--ui-proxy-secret="$NUCLEUS_ADMIN_UI_PROXY_SECRET" \
--log-format=json --log-level=info
Run ./bin/admin-server --help (or --version) for the full surface. Every
flag has a NUCLEUS_ADMIN_* env-var counterpart.
A Data Studio operation routed through the fleet plane executes on the
agent, through the same data-access contract the in-process panel uses,
as the operator the UI auth chain resolved: every request carries the
subject, role, read-only flag and tenant of the caller. On an agent from
v0.10.0 on, that identity reaches the application's model hooks, the
application's per-model policy applies to the operator per verb (list,
retrieve, create, update, delete, bulk_delete) through the
agent's authorizer, and an operator scoped to a tenant only sees and
touches that tenant's rows — reads are confined by the model's tenant
column, a create is stamped with the tenant, an update or delete first
confirms the row is the tenant's. The tenant comes from the trusted proxy:
--ui-tenant-header (default X-Auth-Tenant), honoured on the same path
as --ui-auth-header.
Every mutation the server routes leaves an entry in its audit log that
says what changed: the record as written on a create, the record as it was
on a delete, both on an update, and one record per row on a bulk action.
The agent returns the previous values with its answer, so the server writes
both sides without a second round trip. A side larger than 64 KiB is
replaced by a marker that says so and how large it was. The log is the
in-memory ring the Audit log screen reads; it does not survive a restart.
An agent older than that ignores the identity and behaves as before — no
policy, no tenant — so the server-side gates below stay in front for every
agent. The Access control screen is a read-only snapshot of each node's
policy; enforcement happens on the agent. A ListRecordsRequest carries
filters with an operator (where, the same twelve the panel accepts) and
the agent applies them — or refuses one it does not know, rather than
dropping it — and every list the fleet serves carries an exact total,
filtered or not.
Data Studio mutations are refused by default. The gates, all server-side:
--datastudio-allowed-models— comma-separated model names Data Studio may mutate (create/update/delete/bulk). Empty (the default) refuses every mutation withPermissionDenied;"*"allows all models. Reads are not gated by this list.--ui-role-header(defaultX-Auth-Role) — the trusted proxy sets it toviewerfor a read-only operator: mutations refused, reads keep working;--ui-read-only— makes every operator read-only, turning the server into a pure observability plane.
Also set --ui-proxy-secret (above), so a co-located process inside the
trusted range cannot forge an operator identity with CIDR membership alone,
and keep --ui-trusted-cidrs as narrow as your proxy's real source range.
Treat read-write access to the UI listener as admin access over every
allowlisted model of every connected node.
Behind an SSO reverse proxy (recommended)
The server does not implement OIDC. The canonical deployment runs an
auth-aware reverse proxy (oauth2-proxy, nginx auth_request, Traefik
forward-auth) in front of --ui-addr, forwarding the authenticated identity in
headers:
- the proxy authenticates the user (OIDC/SSO) and sets
X-Auth-User— and optionallyX-Auth-EmailandX-Auth-Role— on every upstream request; - it also sets
X-Auth-Proxy-Secret: $NUCLEUS_ADMIN_UI_PROXY_SECRET, so the server honours those headers only from the real proxy; --ui-trusted-cidrslists the proxy's source network. Requests from outside it are never trusted.
An oauth2-proxy sketch:
--set-xauthrequest=true # emits X-Auth-Request-User/-Email
# map those to the headers the server reads, e.g. via nginx:
# proxy_set_header X-Auth-User $upstream_http_x_auth_request_user;
# proxy_set_header X-Auth-Email $upstream_http_x_auth_request_email;
# proxy_set_header X-Auth-Proxy-Secret $ui_proxy_secret;
For a proxy-less setup — development, or a trusted internal network — a bearer
token works instead: start with --ui-bearer and send
Authorization: Bearer <token>.
What runs inside
Two listeners, one for agents and one for UIs, each with its own auth
chain: h2c by default, TLS when configured (mutual TLS on the agent
listener with --agent-client-ca, and --agent-identity-from-cert to make
the certificate's Common Name the only node_id that agent may register
under). Listener certificates are served from their files and re-read when
a handshake finds them changed, so a rotation needs no restart. /healthz is
public on both, carved out of auth so load balancers can probe it.
Routing primitives move frames between them:
- a registry of connected agents;
- per-UI subscription fanout, dropping newest under backpressure;
- a drop-oldest replay buffer serving
include_recent; - request-ID correlation for snapshots, Data Studio operations and RBAC snapshots.
The manage surface reads two different stores, and it is worth knowing which is which:
- The Access control screen shows a read-only Casbin snapshot routed to a connected agent. The application's authorizer stays the single writer.
- The Audit log screen shows the server's own fleet-plane audit ring: mutations an operator performed through this server (Data Studio create/update/delete/bulk), attributed to the identity resolved by the UI auth chain and to the node the request was routed to. It is in-memory and bounded, like event replay. Admin actions performed inside an application stay in that node's own in-process Orbit panel.
Auth is a shared bearer token for agents, and trusted-proxy or bearer middleware for UIs. The resolved operator identity travels in the request context, which is what attributes audit entries.
Operational notes
/metricsis opt-in.--metrics-addr(envNUCLEUS_ADMIN_METRICS_ADDR) runs a third listener serving the Prometheus default registry —go_*andprocess_*collectors; there are no server-specific collectors yet — plus/healthz. It is unauthenticated by design, so bind it to a private interface. Empty, the default, disables it.- Structured logging via
slog, JSON or text. - Per-stream events are never persisted. The replay buffer is in-memory and bounded.
- Graceful shutdown on signal: a best-effort
Shutdownwith a 2-second timeout per listener.