server

package module
v0.11.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 11, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

README

orbit/server

Standalone Nucleus admin observability server. Accepts agent connections (AgentService.Stream) and serves the admin web UI plus its ControlService API.

Framework floor. This module builds against Nucleus and moves in lockstep with the certified suite set. The exact version it requires is in this module's go.mod — that file is the source of truth, so this note cannot go stale.

Run it

# from this module (the UI bundle is embedded via go:embed)
go build -o bin/admin-server ./cmd/admin-server
./bin/admin-server      # default flags: agents :9090, UI :8080

# Local development: see the UI in a plain browser. Without a
# header-setting reverse proxy every UI request answers 401 (the SPA
# cannot present a bearer), so opt in to the loopback-only dev mode:
./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

# 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 \
  --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.

Security defaults

Read this before exposing either listener beyond localhost.

Agent listener is fail-closed. The agent listener (--agent-addr, default :9090) accepts agent registrations that then drive Data Studio CRUD, RBAC snapshots and fleet events. With no --agent-token and no client-certificate requirement (--agent-client-ca), AgentMiddleware is a pass-through, so an unauthenticated listener on a non-loopback interface would accept any rogue agent on the network. A server certificate alone (--agent-cert/--agent-key) encrypts the wire but authenticates nobody, so it does not count. The server therefore refuses to start in that configuration. To run it you must do one of:

  • set --agent-token (shared bearer) — the recommended minimum;
  • require client certificates: --agent-client-ca together with --agent-cert/--agent-key (mutual TLS — the handshake itself rejects agents without a certificate signed by that CA); or
  • bind --agent-addr to loopback (127.0.0.1:9090); or
  • pass --insecure-agent-listener (env NUCLEUS_ADMIN_INSECURE_AGENT_LISTENER=1) to override, only when a network-layer control (private subnet, service mesh, firewall) already restricts who can reach the address. The override logs a WARN on boot.

UI trusted-proxy trust and the X-Auth-Proxy-Secret gate. The UI listener authenticates operators via a trusted reverse proxy that sets X-Auth-User (per decision 14). By default the server honours that header for any request whose source IP is in --ui-trusted-cidrs (default 127.0.0.1/32, ::1/128). Localhost is always trusted by default, so any co-located process — a sidecar, a host-networked container, another local process — can forge an operator identity and falsify audit attribution. To require proof that the request really came through your proxy, set --ui-proxy-secret (env NUCLEUS_ADMIN_UI_PROXY_SECRET): the proxy must then echo the secret in the X-Auth-Proxy-Secret header, and a trusted-CIDR request without the matching secret falls through to the bearer path instead of being trusted. Keep --ui-trusted-cidrs as narrow as your proxy's real source range.

Data Studio mutations are deny-by-default; open models explicitly with --datastudio-allowed-models. Fleet-plane mutations run on the agent's database WITHOUT the application's per-model RBAC or tenant filtering (no operator identity crosses the agent stream), so which models the fleet may write is an explicit server-side decision:

  • --datastudio-allowed-models (env NUCLEUS_ADMIN_DATASTUDIO_ALLOWED_MODELS): comma-separated model names Data Studio may mutate (create/update/delete/bulk). Empty — the default — refuses every mutation with PermissionDenied; "*" allows all models. Reads are not gated by this list.
  • --ui-role-header (default X-Auth-Role): when the trusted reverse proxy sets this header to viewer (also readonly/read-only), that operator's Data Studio mutations are refused with PermissionDenied while every read surface (streams, nodes, Data Studio reads, RBAC/audit) keeps working. Any other value — including absent — keeps the operator read-write (still subject to the model allowlist).
  • --ui-read-only (env NUCLEUS_ADMIN_UI_READ_ONLY=1): makes EVERY operator read-only, turning the server into a pure observability plane.

The ManageService.GetRbac surface behind the UI's "Access control" screen remains a read-only snapshot of each node's Casbin policy (the app's own authorizer); it does not gate the operator's fleet-plane actions, which are audited and gated only by the model allowlist and the viewer/read-write distinction above — per-operator, per-verb authorization is still future work (see docs/adrs/ADR-002-fleet-datastudio-identidad.md in the repo root). Treat read-write access to the UI listener as admin access over every allowlisted model of every connected node.

--ui-insecure-open is local-development only. It authenticates any credential-less loopback request as the fixed operator insecure-open, because the embedded SPA cannot present a bearer and a browser cannot set trusted-proxy headers — without it the UI is unreachable without a reverse proxy. The server refuses to start with this flag on a non-loopback --ui-addr and logs a WARN on boot. A request that presents a wrong credential is still rejected.

Brute-force lockout. Requests that PRESENT a wrong credential (bad bearer on either listener) are rate limited per source IP (20 failures per minute, then 429); credential-less requests are never counted, so an unauthenticated browser can't lock anyone out.

Inactivity expiry. An agent whose stream goes silent for longer than Config.AgentInactivityTimeout (default 45s) is marked disconnected in the fleet UI (the entry revives automatically if frames resume) — a hung peer no longer shows "online" forever.

Browser security headers. Every UI-listener response carries a strict Content-Security-Policy (self-contained SPA, no external origins), X-Content-Type-Options: nosniff, X-Frame-Options: DENY and Referrer-Policy: no-referrer.

Sub-packages

Sub-package Responsibility
config Config struct: addresses, TLS, tokens, ring buffer sizes, snapshot timeout, agent inactivity timeout.
nodes Connected-agents registry with watchers and per-entry frame-send channels.
routing/eventbus Server-side fanout: per-UI subscriptions, drop-newest on full channel, AggregateFilter for the agent-side union sub.
routing/replay Per-event-kind drop-oldest replay buffer for include_recent.
routing/snapshot Request-ID correlation between UI's GetSnapshot and the agent's SnapshotResponse.
routing/match HTTP method/glob/status-class + SQL model matchers shared with the in-process Filter.
routing (rbac) Request-ID correlation for RBAC snapshots routed to agents (RbacRouter).
routing (audit) Bounded in-memory fleet-plane audit ring (AuditRing, drop-oldest, never persisted).
auth Agent shared bearer token + UI trusted-proxy/bearer middlewares (the resolved operator identity travels in the request context). /healthz is carved out of auth on both listeners.
services Connect-RPC handlers for AgentService.Stream, ControlService.{ListNodes,StreamEvents,GetSnapshot}, DataStudioService (UI CRUD routed to agents) and ManageService.{GetRbac,ListAudit}.
ui //go:embed all:dist. Serves the React bundle at /, falls back to a placeholder if the dist hasn't been built.
cmd/admin-server The binary's main: flags, env, signal handling, TLS loading.

The top-level Server (server.go) composes everything:

  • Two http.Server listeners (h2c by default; a TLS listener with ALPN HTTP/2 when a certificate is configured, mutual TLS with --agent-client-ca) with separate auth chains — one for agents, one for UIs.
  • /healthz public on both listeners (load balancer-friendly).
  • Graceful shutdown on ctx cancel: best-effort http.Server.Shutdown with a 2-second timeout per listener.

Observability of the observability server

  • /metrics is opt-in: --metrics-addr (env NUCLEUS_ADMIN_METRICS_ADDR) runs a third listener serving the Prometheus default registry (go_*/process_* collectors; server-specific collectors are future work) plus /healthz. Unauthenticated by design — bind it to a private interface. Empty (the default) disables it.
  • Structured logging via slog. JSON or text format.
  • Per-stream events are NEVER persisted. The replay buffer is in-memory and bounded.

Tests

cd server && go test -race ./...
  • server_integration_test.go — full agent-server-UI happy paths and the auth gates (AgentToken, UIBearer, UI placeholder).
  • nodes/registry_test.go, routing/eventbus_test.go, routing/snapshot_test.go — table-driven unit coverage of the routing primitives.

Distribution

Released as its own Go module with component tags (server/vX.Y.Z, via release-please). It is a deployable, not a library: the supported artifact is the admin-server binary, installable directly once the module resolves by tag:

go install github.com/jcsvwinston/orbit/server/cmd/admin-server@latest

or built from a checkout as shown above (the UI bundle is embedded via go:embed).

This is also the only binary the repository publishes. Each ROOT release (vX.Y.Z, not the module tags) carries admin-server built by .goreleaser.yaml for six OS/arch combinations, one SPDX bill of materials per archive, a checksums.txt, a keyless cosign signature over it and a build provenance attestation — every other module here is a library that ships no executable, and the runnable examples in the tree (examples/minimal, agent/examples/fleet-app) are demos rather than release artefacts. .github/workflows/release.yml builds them at the root tag ref, release_asset_smoke.yml verifies and runs a published one, and website/docs/operations/verifying-releases.md is the consumer-facing side of the same commands. Its Go API (server.New, config types) carries no compatibility promise — the frozen v1.0 surfaces of orbit are the root module and datasource.

Documentation

Overview

Package server implements the standalone Nucleus admin observability server. It accepts AgentService streams from agents (one per framework process) and ControlService unary/server-streaming calls from the embedded web UI.

The module is implemented: the connection registry (nodes), the fanout and replay routing primitives (routing/*), the auth middlewares, and the Connect-RPC services live in the sub-packages listed in README.md.

Architecture invariants for the server:

  • The admin server is single-instance by default. Active-passive failover is supported by configuring multiple endpoints in the agents' ExtensionConfig.Endpoints list. Active-active is documented as a future extension but is NOT implemented.

  • The server NEVER calls back into agents over a separate connection. All server-to-agent traffic (Subscribe, Unsubscribe, SnapshotRequest) travels on the existing AgentService.Stream multiplexed Frame channel.

  • The server is NOT persistence: events live in bounded ring buffers and are dropped on overflow. Long-term retention is OpenTelemetry's job.

  • A shared token and/or mutual TLS (client certificates verified against --agent-client-ca) gate the agent listener; trusted-proxy headers (X-Auth-User, X-Auth-Email) plus an optional bearer fallback gate the UI listener. The server is never exposed through the application's public load balancer.

Package server is the top-level admin observability server. Construct with New, drive with Run. Implementation details live in the sub- packages: nodes, routing, services, auth, ui.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	// AgentAddr is the [host]:port the AgentService listens on. Agents
	// dial here. Default ":9090".
	AgentAddr string

	// UIAddr is the [host]:port the ControlService and embedded UI listen
	// on. The web browser hits this address, optionally fronted by an
	// auth-aware reverse proxy (oauth2-proxy, nginx auth_request,
	// traefik forward-auth) per decision 14. Default ":8080".
	UIAddr string

	// AgentTLS configures TLS for the agent listener: Run wraps the
	// listener with tls.NewListener and negotiates HTTP/2 via ALPN. When
	// nil the listener serves h2c (plaintext HTTP/2).
	//
	// A certificate alone encrypts the wire; it authenticates nobody. Only
	// a config that requires and verifies client certificates (ClientCAs
	// set and ClientAuth == tls.RequireAndVerifyClientCert — what the
	// binary's --agent-client-ca produces) counts as agent authentication
	// for the fail-closed guard in Run; otherwise set AgentToken too.
	AgentTLS *tls.Config

	// UITLS configures TLS for the UI listener. When nil the listener
	// serves plain HTTP and relies on a TLS-terminating reverse proxy.
	UITLS *tls.Config

	// AgentToken is the shared bearer token agents present. Empty
	// disables token auth (rely on mutual TLS via AgentTLS, or on the
	// listener being on a private network).
	AgentToken string

	// InsecureAgentListener overrides the fail-closed guard that refuses
	// to start the agent listener on a non-loopback interface when it has
	// no authentication (AgentToken == "" and AgentTLS does not require a
	// verified client certificate). Leave false
	// in production; set it only when a network-layer control (private
	// subnet, service mesh mTLS, firewall) already restricts who can reach
	// AgentAddr. See Run for the exact condition.
	InsecureAgentListener bool

	// UIBearerToken is the optional fallback token for direct UI access
	// without a reverse proxy. Empty disables this fallback.
	UIBearerToken string

	// UIAuthHeader is the trusted-proxy header that carries the
	// authenticated user identity (default "X-Auth-User"). The server
	// trusts this header only when the connection arrives from
	// UITrustedProxyCIDRs.
	UIAuthHeader string

	// UIEmailHeader is the optional email header (default "X-Auth-Email").
	UIEmailHeader string

	// UITrustedProxyCIDRs is the list of CIDRs allowed to set
	// UIAuthHeader / UIEmailHeader. Empty means "trust 127.0.0.1/32 and
	// ::1/128 only". Configure your reverse proxy's network here.
	UITrustedProxyCIDRs []string

	// UIProxySecret, when non-empty, requires the trusted reverse proxy to
	// also present a shared secret in the "X-Auth-Proxy-Secret" header
	// before the server honours UIAuthHeader / UIEmailHeader. This closes
	// the gap where any process inside a trusted CIDR (a sidecar, a
	// host-networked container, another local process) could forge an
	// operator identity with just the CIDR membership. Empty preserves the
	// CIDR-only behaviour. See auth.UIMiddleware.
	UIProxySecret string

	// UIRoleHeader is the trusted-proxy header that carries the operator's
	// role (default "X-Auth-Role"). Honoured only on the same trusted-proxy
	// path as UIAuthHeader. Value "viewer" (or "readonly"/"read-only")
	// makes the operator read-only: Data Studio mutations are refused with
	// PermissionDenied. Any other value — including absent — keeps the
	// operator read-write, preserving existing deployments.
	UIRoleHeader string

	// UIInsecureOpen authenticates credential-less UI requests arriving
	// from loopback as the fixed operator "insecure-open". It exists for
	// local development: the embedded SPA cannot present a bearer token,
	// so without a header-setting reverse proxy a browser could never
	// load the UI at all. Fail-closed: Run refuses to start when this is
	// set and UIAddr is not provably loopback (e.g. ":8080" binds every
	// interface), and a WARN is logged on boot. Never set it in any
	// shared or production deployment. Data Studio mutations remain
	// gated by DataStudioAllowedModels and UIReadOnly.
	UIInsecureOpen bool

	// DataStudioAllowedModels is the allowlist of model names Data Studio
	// mutations (create/update/delete/bulk) may touch, matched
	// case-insensitively against the model name the agents register.
	// Deny-by-default: when the list is empty, EVERY Data Studio mutation
	// is refused with PermissionDenied — the fleet plane executes
	// mutations on the agent's database without the application's
	// per-model RBAC or tenant filtering, so writes must be an explicit
	// operator decision. The single entry "*" allows mutations on every
	// model. Reads are never gated by this list.
	DataStudioAllowedModels []string

	// UIReadOnly, when true, makes EVERY UI operator read-only regardless
	// of role header or bearer: the fleet UI can observe (streams, nodes,
	// Data Studio reads, RBAC/audit) but every Data Studio mutation is
	// refused. Use it to run the server as a pure observability plane.
	UIReadOnly bool

	// HTTPReplayBufferSize is the per-kind ring buffer capacity for
	// replaying recent events to a freshly opened UI panel. Default 256.
	HTTPReplayBufferSize    int
	SQLReplayBufferSize     int
	SessionReplayBufferSize int
	CustomReplayBufferSize  int

	// SnapshotTimeout caps how long the server waits for an agent to
	// answer a SnapshotRequest before returning an error to the UI.
	// Default 5s.
	SnapshotTimeout time.Duration

	// AgentInactivityTimeout marks a connected agent as "stale" if no
	// frame (event or heartbeat) arrives within this window. Default 45s
	// (3× the agent's default 10s heartbeat + buffer for jitter).
	AgentInactivityTimeout time.Duration

	// EventChannelSize is the per-UI-subscription buffered channel
	// capacity. Subscribers that fall behind by more than this many
	// events see overflow drops. Default 256.
	EventChannelSize int

	// MetricsAddr, when non-empty, runs a third HTTP listener on this
	// address serving Prometheus /metrics (the default registry: go_* and
	// process_* collectors; server-specific collectors are future work)
	// plus /healthz. Empty (the default) disables the listener — metrics
	// are strictly opt-in.
	MetricsAddr string

	// Logger receives diagnostics. Pass nil for slog.Default.
	Logger *slog.Logger
}

Config tunes the admin server. Two listeners are exposed: one for agents (shared token, optionally over TLS or mutual TLS) and one for UI/operators (trusted-proxy headers or bearer fallback).

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server is the assembled admin observability server. It owns two HTTP listeners: one for agents and one for UIs/operators. Both serve over HTTP/2 (h2c when no TLS config is provided).

func New

func New(cfg Config) *Server

New constructs a Server. It performs no IO; call Run to start serving.

func (*Server) AgentAddr

func (s *Server) AgentAddr() string

AgentAddr returns the resolved agent address (after Run has bound).

func (*Server) MetricsAddr

func (s *Server) MetricsAddr() string

MetricsAddr returns the resolved metrics address (after Run has bound), or "" when the metrics listener is disabled.

func (*Server) Run

func (s *Server) Run(ctx context.Context) error

Run starts both listeners and blocks until ctx is cancelled. Returns nil on graceful shutdown, non-nil on listen errors. Not idempotent.

func (*Server) State

func (s *Server) State() *services.State

State returns the wired-up services.State. Useful for tests.

func (*Server) UIAddr

func (s *Server) UIAddr() string

UIAddr returns the resolved UI address.

Directories

Path Synopsis
Package auth holds the two thin auth surfaces of the admin server:
Package auth holds the two thin auth surfaces of the admin server:
cmd
admin-server command
Command admin-server is the standalone Nucleus admin observability server.
Command admin-server is the standalone Nucleus admin observability server.
Package nodes is the in-memory registry of agents currently connected to the admin server.
Package nodes is the in-memory registry of agents currently connected to the admin server.
Package routing implements the server-side fanout: it receives proto events from agents (via the AgentService handler) and republishes them to UI subscribers (via the ControlService.StreamEvents handler).
Package routing implements the server-side fanout: it receives proto events from agents (via the AgentService handler) and republishes them to UI subscribers (via the ControlService.StreamEvents handler).
Package services holds the Connect-RPC handler implementations for AgentService (admin <-> agent) and ControlService (UI <-> admin).
Package services holds the Connect-RPC handler implementations for AgentService (admin <-> agent) and ControlService (UI <-> admin).
Package ui exposes the admin observability web UI as an embedded filesystem so the admin server binary is fully self-contained.
Package ui exposes the admin observability web UI as an embedded filesystem so the admin server binary is fully self-contained.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL