Peer authentication
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
staticbehavior 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 acommandrefuses 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:
| Mode | Admitting side | Joining side | Pick it when |
|---|---|---|---|
| Pairing code | atlasd fleet invite or POST /api/admission/invites; secret in invite_secret_file | invite_token | A user adds a device from your application: one code, one node, once, least privilege by default |
| Fleet certificate | The authority's key in config-authors.toml | cert_file from atlasd fleet cert | You build the hardware and sign each unit's record once, with no entry on any hub |
| Key plus secret | psk, or psk_file with a record's psk_id — on both ends | A stolen key must not be enough; a wrong secret gets no answer at all | |
| Approval queue | pending = true, pending_max, block_secs | nothing | Devices 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 shape | Provider | Why |
|---|---|---|
| Fixed, small, security-reviewed configs | static | The config is the audit artifact. |
| Growing fleet, human-approved joins | keyfile | Grant/revoke is a file edit — scriptable, diffable, no restarts. |
| Enrolment owned by an external system | command | Atlas asks; your system answers; the contract is two arguments and an exit code. |
| Per-node permissions, keys that rotate | fleet | Each node is a signed record with its own flags; the list distributes itself. |
| Devices added from an application or built at a factory | fleet + pairing codes or certificates | The node becomes a record on its first handshake, with nothing edited by hand. |
| Unknown devices that should ask | any + pending = true | The 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.
[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.