Operations

Run it like you’ll be running it at 03:00.

Every operational surface assumes the worst night of the deployment: configs that can’t brick a remote node, applies that revert themselves, fleets that converge without a controller, and a dashboard that tells the truth. This page is the whole operator story — including the REST API and the full parameter reference.

Canonical path: /etc/atlas/config.tomlRuns air-gapped
Artifacts

One binary per architecture

x86-64 and ARM64 Linux, from datacenter servers down to ~15 g single-board computers. Reference packaging ships alongside: a hardened systemd unit, SELinux policy module, AppArmor profile, and an Ansible role (binary + sha256, optional STIG baseline chaining).

Identity

atlasd keygen and go

An X25519 keypair, a config file, systemctl start. The unit runs unprivileged with a single capability; the daemon warns at load if the config file’s permissions would expose the key.

Schema

Generated from the structs, so it cannot drift

A JSON Schema is derived from the daemon’s own config types — doc comments become descriptions — committed to the repo, served at GET /api/config/schema, and referenced from the example config so TOML-aware editors autocomplete and validate as you type. The dashboard’s form view is generated from the same schema.

Field-aware checks

The mistakes that bite in the field, caught at load

Duplicate bind device+address+port (SO_REUSEPORT would silently load-balance the sockets against each other) · single-IP STUN lists (classification needs two server IPs) · peer endpoints whose address family doesn’t match the link’s bind · relay links missing their server with discovery off · unknown link names in per-link maps · range knobs configured without position prediction (an inert knob is a warning, not a mystery) · malformed CIDRs as hard errors. Typos surface at start, not as silent misbehaviour at the worst moment.

CI-able

config check --json

The daemon’s exact load pipeline without starting the engine: exit 0 = provably loadable (warnings listed), exit 1 = fatal, with a JSON findings mode for scripts. A config edit is proven before any restart window opens.

Secrets

Redacted at the API boundary

GET /api/config returns the real file — comments and formatting preserved — with secret values replaced by a placeholder; validation resolves placeholders against the live file server-side. The browser edits the real config without a private key ever crossing the wire.

Automation

Credentials for the things that cannot hold a passkey

Once a FIDO2 passkey is enrolled, signing in requires a browser ceremony and a physical key — which CI, kiosks and scripted demos cannot perform. Two credentials exist for them. Operator-minted API tokens (atlasd web) authenticate automation against the node’s own API. The mesh node token (atlasd config node-token) is derived from [mesh] mesh_secret, so every member can mint the same one: strictly read-only (GET /api/config, GET /api/stats) and window-limited — it stops being accepted 5–10 minutes after minting, so you mint per run rather than storing it. Disable it per node with [mesh] node_token_reads = false.

Stage

Validate → diff → classify

Staging validates the candidate, computes a unified diff between redacted texts (secrets stay off the wire — a key-only rotation shows an empty diff by design), and classifies hot vs restart. The v1 truth table is honest: nothing is hot yet; every change restarts.

Commit

Deadman marker, in-place restart

The daemon re-validates, backs up the live file, swaps in the staged one, writes a deadman marker, and restarts itself in place — same PID, so systemd never sees a failure. On every boot, before config parsing, an expired marker triggers swap-back: even an unparseable staged file cannot brick the node.

Confirm or revert

Prove connectivity, or go back

After the staged boot, the change commits on an explicit confirm — or automatically the moment a peer session re-establishes (proof the node still has its network). If the window lapses (default 90 s), it reverts and restarts again, recording why. The dashboard shows the countdown and survives the restart gap with a “reconnecting” chip. Proven end-to-end by the apply suite: peers-auto-confirm, deadline revert, fatal-config reject. Measured

The deadman

The change cut the path it travelled on. It came back anyway.

Nobody believes this one until they watch it. Each node arms its own timer against its own deadline on its own disk — there is no coordinator holding a countdown for the fleet, and nothing has to go right for the rollback to happen. Push the change that removes the route your confirmation would have taken, then watch six nodes independently reach the same conclusion.

Six nodes take one pushed change — each arms its own confirm windowSimulation
Push a change
90 sconfirm window each node arms
0 of 6deadman timers running
upoperator session to the fleet
What you’re watching: the line in each lane is the operator’s session to that node — solid while it carries traffic, dashed amber for the in-place restart, dashed red once the change has taken it away. The bar underneath is that node’s own confirm window: 90 s by default, counted against an absolute deadline on its own disk, red in the last fifteen. Time is compressed 10× so a full window fits in the frame. The three changes on offer are real fields with their real defaults, and all three sit inside the section allowlist an epoch is permitted to carry — identity, peers and auth never travel.
Envelope

Ed25519-signed, strictly increasing

Fleet policy travels as a signed envelope — epoch number, author, optional role targeting and minimum daemon version, the policy TOML, the signature over a canonical serialization. Nodes verify against trust anchors on disk and persist a high-water epoch, so replayed or rolled-back envelopes are rejected.

Boundary

The whitelist is a security decision

Policy may carry the sections that should match fleet-wide — scheduler, link monitor, QoS, traversal, plus per-role overlays — and nothing else. An envelope smuggling identity, peers, auth or mesh settings is rejected at signing and at verification: knowing the mesh secret means you may see the mesh; holding an author key means you may change its policy; nobody can rewrite a node’s identity remotely.

Distribution

Gossip does the delivery, apply does the safety

Epochs ride the existing encrypted gossip anti-entropy — offline nodes catch up on reconnect — and every accepted epoch runs through the commit-confirmed lifecycle above. Each node gossips its own status back (applied / staged / rejected-with-reason / not-targeted, config hash, version), so the authoring node watches the whole fleet converge from one screen. Authoring: offline CLI signing, or “Push to mesh” in the dashboard. Proven in the epoch suite: propagate, apply, tamper-reject, rollback-reject, recover. Measured

Compare

Any node is the console

There is no controller, so the question “is the fleet actually running the policy I think it is?” is answered from whichever node’s browser you have. GET /api/fleet/configs pulls every mesh member’s config over the tunnels — each peer redacts its own secrets and keeps its own read protection — and the compare view canonicalises them into a skew matrix. The distinction is what makes it readable: policy sections (scheduler, link_monitor, qos, traversal) are the finding; node-local sections (interface, links, peers, mesh) are expected to differ and stay quiet. Redaction placeholders compare equal, so secrets neither leak nor false-alarm.

Consistency

Configs that are wrong about each other

A harder question than “do these differ?”: node A can never know that B forgot to list A back, so a controller-less system cannot detect the mistake locally — by definition. With every member’s config already in hand, the whole relation graph is cross-checked instead: 39 checks covering peering reciprocity, keys that resolve to no node, and the rest, each naming the nodes involved, the exact keys, and what will actually go wrong.

Roles

One epoch, different policy per platform

A node carries a role ([mesh] role), and a signed epoch can target it: [roles.<name>.<section>] overlays apply only to matching nodes, and target_roles decides whether an epoch applies here at all. So one push can tighten the drones’ link monitor without touching the ground stations — and a node with no role is untargeted rather than silently included.

One pass

Describe the mesh, download the bundle

atlasd config wizard serves the dashboard SPA loopback-only with an authoring API: describe the subnet, nodes, links and roles once, and the bundle builder emits a complete per-node config set — fresh X25519 identities, the full peer cross-graph, shared mesh secret, optional epoch-author kit and install notes — zipped deterministically with tight permissions on the secret-bearing files.

Self-checking

Every generated file passes the daemon’s own validation

A fatal finding in a generated config is treated as a wizard bug, not a user error; a preview mode returns the whole plan first, and minted keys round-trip so the reviewed text and the downloaded zip are byte-identical. Provisioning a five-node mesh is one wizard pass and five file copies.

The dashboard

Every node serves its own truth.

Ten tabs on :9800 — overview, topology, peers, mesh & routes, metrics, scheduler, QoS, enroll, configure, settings — rendered from the same once-per-second snapshot every other consumer reads. These are real captures, not mockups.

Access control: the dashboard has local accounts with optional FIDO2 hardware keys and mandatory-2FA upgrade — the write endpoints demand more than being on the right subnet. Details under security.
CommandWhat it does
keygenGenerate an X25519 keypair for configs
run -c <config>The daemon itself
statusFreshness-checked digest of every instance on the host — one command on a relay shows both interfaces
stats -f jsonOne-shot snapshot dump, scriptable
monitorLive terminal dashboard, 500 ms refresh, sparklines
webREST + embedded dashboard server (plus web adduser · passwd · deluser · list-users · remove-passkey)
enroll <code>Claim this node in the hosted console by proving possession of its key — nothing copy-pasted
config schema · checkPrint the JSON Schema; run the exact load pipeline without starting (--json for CI)
config keygen-author · sign-epochMint an epoch-author key; sign a policy TOML into an epoch envelope, offline
config wizardThe offline provisioning shell (loopback-only unless explicitly overridden)
REST API

The same truth, over HTTP.

Same-origin, gzip, session-gated writes (?interface= selects an instance). The daemon’s own consumers use these endpoints — there is no second, better API.

GET/api/stats

The live snapshot: links, peers, routes, topology with positions, crypto posture, QoS counters, bonding tax.

GET/api/interfaces

Every daemon instance on the host.

GET/api/scheduler-log

Recent scheduler decisions — timestamp, strategy, chosen links, costs. “Why did it pick the serial radio there?” is answerable after the fact.

GET/api/qos

Live class policy + per-class counters and escalation state.

PUT/api/qos

Merge-update the policy at runtime — no restart.

GET/api/netcheck

Per-link public endpoints and NAT classification from the last netcheck.

PUT/api/position

Operator position inject — persisted, validated, picked up within an advertisement tick.

GET/api/config · /api/config/schema

The live TOML (secrets redacted, comments preserved) and its schema.

POST/api/config/validate · stage · commit · confirm · revert

The commit-confirmed lifecycle, exactly as described above.

GET/api/config/apply-status · /api/config/epoch

The apply state machine the countdown UI polls; the current epoch envelope + per-node fleet status.

POST/api/config/push

Sign the live file’s policy sections as the next epoch and hand it to gossip (409 with a hint when no author key exists).

POST/api/auth/login · logout · webauthn/*

Session and FIDO2 ceremonies for the dashboard’s local accounts.

Adapter ingest

The telemetry socket

A Unix socket where adapter processes inject newline-JSON: per-link radio telemetry (RSSI local/remote, noise, TX buffer, error counters) and node position fixes. Crash-resilient, and the daemon never depends on it — telemetry loss can never back-pressure a data path. the adapters ↗

Measured

The bonding-tax ledger

The first steady-state question every datalink engineer asks — “what does bonding cost me?” — answered by the daemon about itself, continuously: five sampled one-way series (TX pipeline, TX queue wait, RX pipeline, reassembly wait, and the scheduler’s path-choice penalty) with p50/p95/max and added-jitter digests. Headline figures in the reference run: p50 0.18 ms / p95 0.42 ms added latency, reproducible from /api/stats. The panel footnotes the two things internal clocks can’t see: the 72 B/packet wire overhead and the remote node’s receive half.

Hosted console

Fleet view without inbound access Live service

An enrolled node self-reports per-link health (~every 30 s, on by default, opt-out) to console.nexusatlas.net: honest states, EWMA RTT, loss, byte counters — and fields with no genuine source are omitted, never fabricated. Enrollment is a one-time code plus cryptographic proof of key possession; the console adds orgs/projects/roles, a live map, offline alerts with recovery notices, an audit trail and FIDO2. The console is optional: lose it and you lose nothing but the map.

NMS

Prometheus, without a second agent

GET /metrics renders the snapshot the daemon already publishes as Prometheus/OpenMetrics text: atlas_link_up, atlas_link_rtt_seconds, atlas_link_loss_ratio, atlas_link_jitter_seconds, atlas_link_quality_score, per-link byte and probe counters, atlas_peer_session_established, atlas_qos_{enqueued,sent,dropped}_packets, the bonding-tax percentiles and the metered-link budget series. It is a formatter over the stats file, not a second collection path — no exporter process, no agent to keep alive on a node you cannot reach — and atlas_scrape_duration_seconds reports what the formatting itself cost. On by default at /metrics; [metrics] enabled = false answers 404.

SIEM

Syslog and CEF, event-shaped

[syslog] ships state changes rather than log lines — a link going down, a peer session tearing down — to a collector over UDP, TCP or unix:///dev/log, as RFC 5424 structured syslog or ArcSight CEF. Facility, minimum severity, hostname and SD-ID are yours. The queue is bounded: a collector outage drops and counts events rather than back-pressuring the daemon. Opt-in, and separate from [logging], which is verbosity for humans.

Liveness

A health check that fails when the data path does

“The process is up” is the check every monitoring system does by default and the one worth the least here: atlasd can be running, answering HTTP and carrying nothing — TUN gone, links dead, engine loop wedged. GET /api/health answers three questions separately: is the daemon alive (stats snapshot fresh), is the data path serviced (watchdog counters advancing, TUN present, probe loop running), and is anything reachable (a link answered a probe recently). Only the first two can fail the check — a node whose peer is legitimately offline is a healthy node with nothing to talk to, and a check that pages on that is a check that gets muted.

Fleet

Is the fleet healthy, asked from anywhere

GET /api/fleet/health is a daemon route rather than a browser loop for a specific reason: the dashboard is same-origin only, with no CORS, no peer registry and no way to attach the mesh read token. The daemon has all three. One request, one answer for the whole mesh, from any member.

Escalation

One zip, redacted by allowlist

atlasd support-bundle, or one button in the dashboard: config, stats, health, the event journal, the scheduler log, netcheck results, versions and the crash spool, collected once instead of over a dozen round-trips. The config is rewritten through a deny-by-default allowlist, so private keys, the mesh secret and auth credentials never leave the node — and neither does any key this build does not recognise. Topology is included deliberately: a bundle with half a topology cannot be diagnosed from. Treat the file as sensitive.

Crashes

A crash that reports itself — or doesn’t

A panic or a fatal signal is captured in the dying process, redacted at capture (private keys, session keys and the mesh secret are never written down in the first place), and spooled on disk under hard bounds. Nothing in the capture path opens a socket: upload is a separate decision and auto_send is off by default. Three ways out, for three realities — attributed, anonymous, or via a peer for a radio-only node that cannot reach the internet at all, where a mesh member that can forwards the payload without being able to file under its own identity. Reports group by a blake2s fingerprint of the normalised top frames plus the panic message, so one bug is one group and not fifty. Coredumps are opt-in and require a public key: they are encrypted or they are not written.

Reference

The full parameter surface.

Defaults in parentheses. This is the complete top-level map — the schema (and your editor, via the schema) carries every field’s documentation.

All configuration sections, one table+
SectionWhat it configuresKey parameters (defaults)
[interface]TUN identityname · address · private_key · listen_port · mtu (1420) · node_name · position · endpoint_roaming (true)
[[link]]One underlay linkname · bind_device · bind_address · weight (1) · capacity (80 Mbps) · probe_interval_ms · max_range_m · range_warn_m · type (direct|relay) · relay_server · relay_pubkey · listen_port
[[peer]]A peer and its routespublic_key · endpoint[] · endpoint_per_link · allowed_ips[] · persistent_keepalive
[crypto]Session lifetimerekey_interval_secs (120) · rekey_data_limit (1 GiB) · disable_encryption (false — testing only)
[auth]Unknown-key policyprovider (static) · keyfile · command · deny_cache_secs (30) · command_timeout_ms (5000)
[scheduler]Strategy + thresholdsstrategy (lowest_delivery_time) · redundancy_factor (1) · priority_order · adaptive_* ratios · loss_* pct (20/50) · latency_* ms (40/100) · adaptive_dwell_ms (5000) · data_loss_probe_every_n (10)
[link_monitor]Probing + predictionprobe_interval_ms (250) · dead_probe_count (5) · prediction_source (none) · rssi_floor_dbm (−88) · rssi_lookahead_secs (2) · range_warn_m (600) · range_full_m (1000)
[routing]Multipath + pinned pathspath_redundancy (1) · [[routing.static_path]] name · path_id · hops[]
[mesh]Control planeenabled (false) · mesh_secret · bootstrap_peers · gossip/heartbeat/expiry intervals · role · gossip_v1_compat (false)
[qos]Transport classesenabled (false) · backlog_bytes (4 MB) · deadline_k (3.5) · floor/ceil (80/1000 ms) · fec_enabled (false) · fec_block_packets (8) · bundle_backend (memory) · rx_release_buffer (false)
[traversal]NAT traversalenabled (false) · stun_servers · netcheck_interval_secs (300) · punch (true) · discover (true) · region · relay_paths (1) · auto_relay (false)
[telemetry]Adapter socket + consoleenabled (false) · socket_path · console_report (on, opt-out) · console_url
[logging]Verbositylevel (info) — journald/syslog via tracing
Boundaries

Operator honesty, itemised.

Three behaviours that are deliberate and worth knowing before the first apply.

Nothing is hot yet

The v1 apply lifecycle restarts on every committed change — even a knob the QoS reload path could technically hot-apply. Deliberate: one lifecycle, one guarantee, no “which kind of change was that?” at 03:00.

Secret edits look empty

Diffs are computed between redacted texts by design, so a private-key rotation shows “no visible changes”. The apply is still real; the emptiness is the proof the secret never crossed the wire.

Auth changes the API contract

Once dashboard users exist, endpoints that used to be open writes — QoS apply, position inject, config — require login. Scripts speaking to :9800 must authenticate, or stay on nodes with no users configured.

Request an evaluation

One binary. One file. One screen.

Running Atlas is deliberately boring: install it, validate the file, watch the dashboard. The interesting parts happen to your traffic, not to your evening.