# Understanding the Tailcat Key Management Model: A Control-Plane-Free Approach

> Discover the Tailcat key management model's control-plane-free approach. Learn how nodes generate identities and exchange public keys securely using ConnBlob tokens.

- Repository: [Tailscale/tailcat](https://github.com/tailscale/tailcat)
- Tags: deep-dive
- Published: 2026-08-30

---

**Tailcat implements a decentralized key management model where each node generates its own cryptographic identity (node key) and derives a separate discovery key (disco key), exchanging public keys via compact ConnBlob tokens without requiring a centralized control plane.**

The Tailcat key management model powers peer-to-peer encrypted connections in the `tailscale/tailcat` repository by eliminating traditional certificate authorities. Instead of relying on external identity providers, each endpoint maintains autonomous control over its cryptographic material, enabling fully stateless operation in user space.

## Core Cryptographic Primitives

Tailcat builds its security model on two distinct key pairs that serve separate purposes in the connection lifecycle.

### Node Keys for Authentication

Every Tailcat instance owns a **node private key** (`key.NodePrivate`) and corresponding **node public key** (`key.NodePublic`). According to the source code in [`tailcat.go`](https://github.com/tailscale/tailcat/blob/main/tailcat.go), the server can accept an explicit key via the `Server.Key` field; if left uninitialized, the system generates a fresh ephemeral key on startup.

```go
// Server side – generate a new key if none provided, then start
s := &tailcat.Server{}
if err := s.Start(); err != nil { log.Fatal(err) }
blob := s.ConnBlob() // token containing public keys to give to clients

```

### Disco Keys for Path Discovery

The **disco key** (discovery key) operates independently from the node key but is cryptographically derived from it. In [`tailcat.go`](https://github.com/tailscale/tailcat/blob/main/tailcat.go), the function `discoPrivateForNode` deterministically generates the disco private key from the node private key. This design ensures that the disco key remains safe to expose on the wire while keeping the node key private.

Disco keys seal "call-me-maybe" packets that advertise UDP endpoints, enabling NAT traversal without exposing the sensitive node key material.

## Key Generation and Derivation

The `NewPrivateKey` helper function in [`tailcat.go`](https://github.com/tailscale/tailcat/blob/main/tailcat.go) (lines 33-40) orchestrates the creation of both key pairs. This function generates a new node key using `key.NewNode()` and immediately derives the disco key from it.

```go
// Create new identity
pk := tailcat.NewPrivateKey()
// pk.Private contains key.NodePrivate
// pk.Public.ServerPublic contains key.NodePublic  
// pk.Public.ServerDiscoPublic contains the derived disco public key

```

The function returns a `PrivateKey` struct containing the private key material and a populated `ConnInfo` object that carries the public components.

## Public Key Distribution via ConnBlob

Tailcat eliminates the need for a coordination server by embedding public keys directly into connection tokens. The `ConnInfo` struct (defined in [`tailcat.go`](https://github.com/tailscale/tailcat/blob/main/tailcat.go) lines 44-51) exposes two critical fields:

- `ServerPublic` – the node public key for WireGuard authentication
- `ServerDiscoPublic` – the disco public key for endpoint discovery

When a server starts, the `connBlob()` method encodes this information into a **ConnBlob**—a self-contained token that includes the public keys and DERP region data. Clients parse this blob to recover the server's cryptographic identity without network round-trips to a control plane.

## Access Control with Allowed Clients

The Tailcat key management model supports fine-grained access control through allow-listing. The `Server` type maintains an `AllowedClients` field—a slice of `key.NodePublic` values that restricts which client identities may connect.

```go
// Restrict clients to a specific node public key
allowedKey := tailcat.NewPrivateKey().Public.ServerPublic
s.AddAllowedClient(allowedKey.NodePublic)

```

The backend stores these authorized keys in an internal map (`allowedClients`) and validates incoming connections in the `onMeow` handler. New clients can be added dynamically at runtime via `Server.AddAllowedClient`, allowing for flexible key rotation without restarting the server.

## Ephemeral vs. Persistent Key Lifecycle

Tailcat defaults to **ephemeral keys** that exist only in memory. The source code explicitly avoids built-in persistence:

- **Server lifecycle**: On `Start`, the server uses the supplied `Server.Key` or generates a fresh ephemeral key via `key.NewNode()`. The public key becomes part of the `ConnBlob` distributed to clients.
- **Client lifecycle**: On first use (`nodeKeyLocked`), the client either uses a supplied private key or generates an ephemeral one, then parses the server's `ConnBlob` to obtain the server's public keys.

For scenarios requiring persistent identities, the `PrivateKey` struct (lines 22-28 in [`tailcat.go`](https://github.com/tailscale/tailcat/blob/main/tailcat.go)) exposes the raw `key.NodePrivate` field, allowing users to serialize and store keys manually using `json.Marshal`. The library deliberately provides no built-in key store, maintaining stateless operation by default.

## Source Code Implementation

The key management logic resides in specific files within the repository:

- **[`tailcat.go`](https://github.com/tailscale/tailcat/blob/main/tailcat.go)** – Core library containing `PrivateKey` struct, `Server` and `Client` types, `NewPrivateKey` generation, and `ConnBlob` handling
- **[`wire.go`](https://github.com/tailscale/tailcat/blob/main/wire.go)** – CBOR wire format for encoding/decoding `ConnInfo` and `ConnBlob` structures
- **[`tailcat_ssh.go`](https://github.com/tailscale/tailcat/blob/main/tailcat_ssh.go)** – SSH host-key handling (separate from the WireGuard node key model)
- **[`client_test.go`](https://github.com/tailscale/tailcat/blob/main/client_test.go)** – Verification tests for key creation, serialization, and allowed-client enforcement

## Summary

- The Tailcat key management model uses **node keys** for WireGuard authentication and **disco keys** for safe public endpoint advertisement.
- Keys are generated locally via `NewPrivateKey` in [`tailcat.go`](https://github.com/tailscale/tailcat/blob/main/tailcat.go), with disco keys derived deterministically from node keys using `discoPrivateForNode`.
- Public keys travel via **ConnBlob** tokens rather than control plane lookups, enabling fully peer-to-peer connections.
- Access control operates through `AllowedClients` checks against `key.NodePublic` identities, with runtime updates via `AddAllowedClient`.
- By default, keys remain ephemeral and memory-resident; persistence requires manual handling of the `PrivateKey` struct.

## Frequently Asked Questions

### How does Tailcat handle key storage and persistence?

Tailcat does not persist keys to disk by default. The `PrivateKey` struct in [`tailcat.go`](https://github.com/tailscale/tailcat/blob/main/tailcat.go) exposes the raw `Private` field as a `key.NodePrivate` type, which users can manually serialize (e.g., via `json.Marshal`) when persistent identities are required. This design keeps the library stateless and eliminates filesystem dependencies.

### What is the difference between node keys and disco keys in Tailcat?

**Node keys** (`key.NodePrivate`/`key.NodePublic`) authenticate WireGuard connections and must remain confidential. **Disco keys** are derived from node keys but are safe to transmit openly; they encrypt UDP endpoint advertisements for NAT traversal. This separation allows public endpoint discovery without exposing the long-term authentication key.

### How does Tailcat authenticate clients without a control plane?

Tailcat validates client identities against an explicit allow-list. The `Server` type stores permitted `key.NodePublic` values in `AllowedClients` and checks incoming connections in the `onMeow` handler. Because public keys are exchanged offline via `ConnBlob` tokens, no online certificate authority or coordination server is necessary for authentication.

### Can I use pre-existing keys instead of generating ephemeral ones?

Yes. The `Server.Key` field accepts a pre-generated `key.NodePrivate`, and the client similarly accepts existing keys through its configuration. If these fields remain zero-valued, both server and client automatically generate fresh ephemeral keys using `key.NewNode()` during initialization.