Tillered Docs
Cluster Operations

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 -k skips 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=false on 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.

TierMechanismFirst contact is
1Trust on first use (default)Blind, then pinned forever
2fingerprint: in cluster.yamlVerified before any contact
3arctic cluster trustVerified 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.

A peer in cluster.yaml may declare its expected identity:

peers:
  - name: node-a
    endpoints:
      - 203.0.113.10:8080
    fingerprint: SHA256:e6VUuXGGSkyBGKcuBM9NNEEsY0wRlAAqvGKqVyZQeXo

On 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 fingerprint
SHA256:e6VUuXGGSkyBGKcuBM9NNEEsY0wRlAAqvGKqVyZQeXo

The 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_fingerprint

Because 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:e6VUuXGGSkyBGKcuBM9NNEEsY0wRlAAqvGKqVyZQeXo

The 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:8080
Verify 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:      No

arctic 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.

On this page