---
title: "How Pairing Works"
url: "https://control.actana.ai/docs/pairing/how-pairing-works"
description: "The protocol: mint, first contact that sends nothing, the CA fingerprint, redeem, and what a refusal looks like."
updated: 2026-09-01T09:03:04+00:00
---

Pairing is a one-time code that buys a certificate the client generated itself. Nothing both parties hold travels through the channel you used to read the code out.

## Mint

[`actana pair new`](/docs/pairing/create-a-pairing-code) opens a pairing session on the Core and prints the code once, to that terminal. The Core stores a keyed digest of the code, not the code. [`actana pair ls`](/docs/pairing/create-a-pairing-code) cannot print it again. A lost code is re-minted, not recovered.

The session dies after five minutes (unless you set `--ttl`), after five wrong guesses, or after one successful redemption. The session id travels with the code: the Core hashes a candidate together with that id and will not search every open session for a match.

## First contact

The client has an address and a fingerprint on paper, and no trust anchor yet. First contact dials the Core's TLS port with nothing trusted and **sends nothing**. It reads the CA from the handshake, shows you that [fingerprint](/docs/pairing/how-pairing-works), and waits.

A caller with no fingerprint is not a caller with a waived one. Unconfirmed means the code stays unsent.

## Redeem

Only after the fingerprint matches does the client open a **second** connection. That one is pinned: the CA from first contact, `rejectUnauthorized`, and a `checkServerIdentity` that re-runs the fingerprint comparison before the handshake completes. Then it `POST`s `/v1/pair/redeem` with `{sessionId, code, client, csr}`. The private key is not in that body. It was born on the client and stays there.

`/v1/pair/redeem` is the only pre-auth route on a Core. Everything else still requires a client certificate.

A 200 body is exactly `{endpoint, caCert, clientCert, bearer}` — four fields, and the absence of a fifth is the point. There is no private key on the wire, not in the request and not in the response.

The Core consumes the code **before** it signs the CSR, so two concurrent redemptions of one code cannot both be issued a certificate. If signing fails after that, mint a fresh code.

```mermaid
sequenceDiagram
  autonumber
  participant Op as Operator
  participant Core
  participant Client
  Op->>Core: actana pair new
  Note over Core: Mint session, print code once, store digest only
  Core-->>Op: code, fingerprint, session, expiry
  Op->>Client: carry those four
  Client->>Core: TLS, nothing trusted
  Note over Client,Core: First contact sends nothing
  Core-->>Client: CA in the handshake
  Client->>Op: show fingerprint
  Op->>Client: confirm
  Client->>Core: pinned TLS, then POST /v1/pair/redeem
  Note over Client,Core: sessionId, code, client, csr
  Note over Core: Consume the code, then sign the CSR
  Core-->>Client: endpoint, caCert, clientCert, bearer
```

## What a refusal looks like

Wrong code, unknown session, expired, already redeemed, attempts spent: an unauthenticated caller sees one status and one body, `refused`. Distinguishing those would tell a stranger whether a session exists and whether a guess was close. The distinction lives in the Core's audit log. Ask for a fresh code and, if you need the reason, read the log on the Core.

Rate limiting is separate, per caller, not per session. A fingerprint mismatch or an unconfirmed fingerprint never spends the code.

## The CA fingerprint

The fingerprint exists because first contact cannot pin anything yet. The client has an address and a code, and no trust anchor. The only way to know the machine on the other end is the Core that minted the code is to look at the CA it presents, **before** the code goes.

[`actana pair new`](/docs/pairing/create-a-pairing-code) prints it beside the code:

```text
CA fingerprint AA:BB:...
```

That string is SHA-256 of the Core's CA certificate, as colon-separated upper-case hex. Copy the whole line — the handout wraps it and never shortens it, because a truncated value is not a fingerprint. Whitespace, colons, case, and a leading `sha256:` are tidied when a client reads it back.

The [Panel](/docs/pairing/panel-pairing) dials first, shows what the machine presented, and asks you to type the line from `pair new`; the code fields appear only after they match. [`actana core pair`](/docs/pairing/cli-pairing) takes `--fingerprint`, or prints the presented value and asks. There is no skip flag.

**Absent is not waived.** No fingerprint means `fingerprint-unconfirmed` and the code is not sent — not on a pipe, not in CI, not because a UI forgot the field. A mismatch refuses, and the code stays unsent: wrong host, or a Core whose CA was rotated since you wrote the fingerprint down. `actana pair new` prints the current one.

After you confirm, redemption is a second connection pinned to that exact CA. A Core that presented one CA in the handshake and handed back another is refused the same way.

## See also

- [Create a Pairing Code](/docs/pairing/create-a-pairing-code)
- [Actana CLI Pairing](/docs/pairing/cli-pairing)
- [Panel Pairing](/docs/pairing/panel-pairing)
- [Pairing Troubleshooting](/docs/pairing/troubleshooting)
