How Pairing Works
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 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 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, 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 POSTs /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.
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 prints it beside the code:
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 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 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.