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. Peers you configured explicitly are always accepted; a provider only ever extends trust, never revokes what you wrote.
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:
- Revocation stops new sessions. An established session lives until its next rekey — with the default rekey interval, minutes, not days. Rotate the interval down if your threat model needs faster eviction.
- 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 missing or non-executable hook refuses to start the daemon. Misconfiguration is loud, at startup, on purpose.
#!/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
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. |
Library embedders have a fourth option: inject a custom authenticator implementation directly into the engine, bypassing the providers entirely.
[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.