Peer authentication

Documents the atlasd 3.0.0 release lineView as Markdown

Ten nodes tolerate hand-edited peer lists. A hundred don't. The [auth] section answers exactly one question — what happens when a handshake arrives from a public key that is not in the config? — and it answers it with three providers of increasing dynamism, every one of which fails closed. Under those three, peers you configured explicitly are always accepted; a provider only ever extends trust, never revokes what you wrote. A fourth, fleet, replaces the question: every key, configured or not, needs a signed record. On top of the providers, four admission modes give a new node a deliberate way in.

Provider static — the default

Unknown keys are rejected. Identical to having no [auth] table at all. This is the right answer for fixed fleets: the config file is the complete trust statement, and nothing can widen it.

Provider keyfile — an allowlist that updates without restarts

[auth]
provider = "keyfile"
keyfile = "/etc/atlas/authorized_peers.toml"

The keyfile is a TOML list of [[peer]] entries — public_key plus allowed_ips — re-read whenever its modification time changes. Granting a new node is appending an entry; revoking is deleting one; neither touches the daemon. Two properties to plan around:

  • Removing a key refuses it once its peer entry is gone. A key the keyfile admitted becomes a known peer, and Atlas does not ask the keyfile about a known peer again — not at rekey and not on reconnection. To remove a node now, delete its line and run atlasd peer evict <key>: its sessions end and its next handshake is refused. A daemon restart, or the peer expiring after a silence, has the same effect.
  • An unreadable keyfile authorizes nobody. Fail closed means a botched deploy of the allowlist quietly reverts you to static behavior rather than open doors.

Provider command — your control plane decides

[auth]
provider = "command"
command = "/usr/local/lib/atlas/authorize-peer"
command_timeout_ms = 5000
deny_cache_secs = 30

For every unknown key, the daemon executes your hook as command <base64-pubkey> <source-addr>. Exit 0 authorizes, and each stdout line becomes an allowed-IPs CIDR for the new peer; any other exit denies. This is the integration point for an enrolment database, LDAP, an inventory system — whatever owns identity in your world. The contract is small enough to implement in a shell script and strict enough to trust:

  • The hook runs off the packet path, under a hard time budget (command_timeout_ms) — a hung hook drops that handshake, never stalls the daemon.
  • Denied keys are cached (deny_cache_secs) so a handshake flood from an unauthorized key cannot hammer your backend.
  • A hook that cannot be started denies every key, and provider = "command" without a command refuses to start the daemon. Misconfiguration fails closed.
#!/bin/sh
# authorize-peer <base64-pubkey> <source-addr>
# Example: ask an internal inventory service; print the peer's tunnel /32.
KEY="$1"
RESP=$(curl -fsS --max-time 3 "https://inventory.example.internal/atlas/authorize?key=${KEY}") || exit 1
echo "$RESP"      # e.g. "10.0.100.57/32"
exit 0

Provider fleet — signed node records

[auth]
provider   = "fleet"
fleet_list = "/etc/atlas/fleet-list.toml"

Keys are admitted from a signed fleet list: a key that belongs to a record joins as that record, with the record's allowed_ips and capability flags, and a key without one gets no session. Unlike the other three, fleet also governs configured peers: a [[peer]] whose key has no record is refused like any stranger. The node's id becomes the record's id, so a key can rotate without the node changing identity. The list is authored with atlasd fleet, signed by a config-author key, and travels over the mesh's sessions after the first start. See Identity and authorisation.

Admission modes shipped

The providers answer for keys you planned for. Four admission modes add a deliberate way in for the ones you did not, configured in the same [auth] table:

ModeAdmitting sideJoining sidePick it when
Pairing codeatlasd fleet invite or POST /api/admission/invites; secret in invite_secret_fileinvite_tokenA user adds a device from your application: one code, one node, once, least privilege by default
Fleet certificateThe authority's key in config-authors.tomlcert_file from atlasd fleet certYou build the hardware and sign each unit's record once, with no entry on any hub
Key plus secretpsk, or psk_file with a record's psk_id — on both endsA stolen key must not be enough; a wrong secret gets no answer at all
Approval queuepending = true, pending_max, block_secsnothingDevices nobody can list in advance; a person decides each knock

Pairing codes and certificates admit into a record fleet (provider = "fleet"); the secret and the queue work under every provider, and the queue catches what every other check refused. Every invitation, admission and decision is journalled with who made it.

Choosing, honestly

Fleet shapeProviderWhy
Fixed, small, security-reviewed configsstaticThe config is the audit artifact.
Growing fleet, human-approved joinskeyfileGrant/revoke is a file edit — scriptable, diffable, no restarts.
Enrolment owned by an external systemcommandAtlas asks; your system answers; the contract is two arguments and an exit code.
Per-node permissions, keys that rotatefleetEach node is a signed record with its own flags; the list distributes itself.
Devices added from an application or built at a factoryfleet + pairing codes or certificatesThe node becomes a record on its first handshake, with nothing edited by hand.
Unknown devices that should askany + pending = trueThe knock waits for a decision instead of failing silently.

Library embedders have one more option: inject a custom authenticator implementation directly into the engine, bypassing the providers entirely.

Both the command hook and the fleet provider are worked through end to end — aircraft on cellular modems, two hubs, admission from your database, removal — in A cellular fleet behind a web ground station.

Scope check: [auth] governs data-plane peers — who may join the tunnel. Dashboard users, API tokens and mesh read tokens are a separate, independently documented surface: see the REST API auth model.
Esc
↑ ↓ navigateEnter — openEsc — close