TLS and trust
How Arctic secures the mesh and operator API with TLS 1.3 and Ed25519 identity pinning, and the three ways to establish trust in an agent
As of v1.4.2, every Arctic agent serves its HTTP surface over TLS 1.3 by
default. That covers both agent-to-agent mesh traffic and the operator REST
API on port 8080. The certificate is self-signed and generated in memory from
the agent's peer.key, so there is no CA, no certificate issuance, and no
certificate file to manage. Identity is verified by pinning the agent's
Ed25519 identity key, not by validating a certificate chain.
This page explains what that means in practice and how the CLI decides whether to trust the agent on the other end of a connection.
What changed in v1.4.2
- The agent wraps its HTTP surface in TLS 1.3: mesh dials between agents and the operator REST API both. Agents pin each other's Ed25519 identity keys on every dial, and cold-start dials (handshake, cluster join) use first-contact identity binding: the TLS certificate key must equal the identity that the signed handshake response authenticates.
- The CLI verifies agents by identity pinning. Tools that validate CA chains
(curl, browsers) see an untrusted self-signed certificate, which is
expected: use
curl -sk https://...for manual checks. The-kskips chain validation that Arctic never relied on; the CLI's own connections remain pinned. - Peer TLS is on by default and cluster-wide. To keep the prior plaintext
behaviour, for example behind a TLS-terminating proxy, set
PEER_TLS_ENABLED=falseon every agent (see the agent reference). - The operator API can instead serve a real certificate from files via
API_TLS_CERT/API_TLS_KEY. That path is pinned to TLS 1.3, so it cannot be downgraded.
The three trust tiers
The open question in any pinning scheme is how the first pin gets established. Arctic gives you three answers, in increasing order of ceremony. Declared fingerprints (tier 2) are optional but recommended for production clusters.
| Tier | Mechanism | First contact is |
|---|---|---|
| 1 | Trust on first use (default) | Blind, then pinned forever |
| 2 | fingerprint: in cluster.yaml | Verified before any contact |
| 3 | arctic cluster trust | Verified by hand, one host at a time |
Tier 1: trust on first use
This is the default and needs no configuration. On first contact with an
agent, the CLI records the agent's Ed25519 identity in an SSH-style
known-hosts file at ~/.config/arctic/known_hosts.yaml (mode 0600) and pins
it on every later connection. A learned host also defaults its endpoints to
https from then on.
Scheme-less endpoints in cluster.yaml default to https, matching the
agent's TLS-on default. On compose apply and compose diff each host's
identity is learned and pinned on first contact; a host that does not answer
TLS is named in a notice telling you to declare an explicit http://
endpoint for it. Explicit http:// or https:// schemes are honored
exactly as written. No --insecure run or other ceremony is needed to bring
up a fresh TLS cluster.
Older v1.4.2 CLI builds need explicit https on a fresh bootstrap
v1.4.2 CLI binaries built before the 2026-07-22 patch release treat a
scheme-less endpoint as plaintext on the very first bootstrap: the apply
fails with read: connection reset by peer while the agent logs a
plaintext-request error. If you see that, update the CLI (a patched build
lists the database command in arctic help) or write endpoints with an
explicit https:// scheme in cluster.yaml (all examples in these docs
do). Applies and diffs against an existing cluster are unaffected.
When the CLI learns an identity it says so:
Learned identity for 203.0.113.10 (SHA256:e6VUuXGGSkyBGKcuBM9NNEEsY0wRlAAqvGKqVyZQeXo)The trade-off is in the name: trust on first use is blind on the very first connection. Every connection after that is verified against the pin, so the exposure window is exactly one contact per host. If that window matters to you, close it with tier 2 or tier 3.
Tier 2: declared fingerprints (recommended for production)
A peer in cluster.yaml may declare its expected identity:
peers:
- name: node-a
endpoints:
- 203.0.113.10:8080
fingerprint: SHA256:e6VUuXGGSkyBGKcuBM9NNEEsY0wRlAAqvGKqVyZQeXoOn compose apply and compose diff, a provable-identity gate runs before
the cluster is contacted: each declared peer must present a TLS identity
matching its fingerprint or the run aborts. A mismatch is always fatal. It is
never downgraded by --ignore-unreachable, though a declared peer that is
unreachable does respect that flag. Peers without a declared fingerprint fall
back to trust on first use.
Read the fingerprint out-of-band, on the trusted host itself. Either ask the agent binary directly:
arctic-agent fingerprintSHA256:e6VUuXGGSkyBGKcuBM9NNEEsY0wRlAAqvGKqVyZQeXoThe command prints the bare fingerprint and nothing else, so it is safe to
capture in scripts. The default install places the binary at
/opt/tillered/bin/arctic-agent. Alternatively, read the same value from the
identity_fingerprint field of the "peer identity" line the agent logs at
every boot:
journalctl -u arctic | grep identity_fingerprintBecause peer.key never changes over the life of a peer, the fingerprint is
stable across restarts, upgrades, and database restores. Committing
cluster.yaml with fingerprints to git gives the whole cluster a reviewable
identity map that travels with the config, and any operator who clones the
repo verifies identities before first contact. See the
compose reference for the field and
the compose guide for the wider workflow.
Tier 3: pin one host by hand
arctic cluster trust verifies and pins a single agent without a compose
run. Read the fingerprint on the trusted host as above, then from your
workstation:
arctic cluster trust --url 203.0.113.10:8080 \
--fingerprint SHA256:e6VUuXGGSkyBGKcuBM9NNEEsY0wRlAAqvGKqVyZQeXoThe CLI probes the agent's TLS identity, verifies it against the given
value, and pins it. A mismatch is refused as a possible man-in-the-middle.
Without --fingerprint, the command prints the fingerprint the agent
presented and pins nothing:
arctic cluster trust --url 203.0.113.10:8080Verify this fingerprint against the agent's boot log on the host
(journalctl -u arctic | grep identity_fingerprint),
then re-run with --fingerprint <value> to pin it.
Host: 203.0.113.10
Fingerprint: SHA256:e6VUuXGGSkyBGKcuBM9NNEEsY0wRlAAqvGKqVyZQeXo
Pinned: Noarctic cluster identity shows the fingerprints of the cluster you are
connected to. See the CLI reference for
both commands.
When an identity changes
If a pinned host presents a different identity than the one on record, the
CLI refuses the connection with an SSH-style warning. Only two things produce
this: the host's peer.key genuinely changed (a rebuild without restoring
the key, or a deliberate rotation), or something between you and the host is
intercepting the connection. Confirm which one it is out-of-band before
proceeding; the exact warning text is covered in
troubleshooting.
Once you have confirmed the change is legitimate, --insecure accepts the
new identity and re-learns the pin. Despite the name, --insecure is the
deliberate path for key rotation and onboarding, not a testing-only flag: it
tells the CLI "do not hold this connection to the recorded pin", learns what
the host presents, and pins that for future connections.
If known_hosts.yaml itself becomes unreadable or unparseable, the CLI
fails closed: it exits with code 3 naming the file rather than silently
running unpinned. --insecure is also the repair path there; it proceeds
without the pin check and re-learns identities into the store.
Changed in v1.4.2
The trust store lives at ~/.config/arctic/known_hosts.yaml regardless of
--config; it no longer follows a custom config path. If you kept a
known-hosts file inside a custom config directory, point the CLI at it
explicitly with --known-hosts or ARCTIC_KNOWN_HOSTS.
Upgrading a cluster from plaintext
A cluster upgraded from a pre-TLS release migrates on its own. The first
compose apply or compose diff after the upgrade probes hosts the CLI has
no pinned identity for, learns their TLS identities, and rewrites saved
http:// endpoints in the CLI's own contexts to https:// (verified by a
live TLS probe first, so a genuinely plaintext peer is never upgraded by
mistake).
The one thing the CLI will not rewrite is your compose file: an explicit
http:// scheme in cluster.yaml is honored as written and must be updated
by hand. Scheme-less endpoints need no edit.
Because a dialing agent pins its target's identity and expects a TLS peer, upgrade every node in a cluster together; see Upgrades for the procedure.