# How to Allow‑list Specific Clients in Tailcat: A Complete Guide

> Learn to allowlist specific clients in Tailcat using the --allow flag or Server.AddAllowedClient. Restrict tunnel connections by WireGuard public keys for enhanced security.

- Repository: [Tailscale/tailcat](https://github.com/tailscale/tailcat)
- Tags: how-to-guide
- Published: 2026-09-06

---

**Use the `--allow` flag at startup or call `Server.AddAllowedClient` at runtime to restrict Tailcat tunnel connections to specific WireGuard node public keys.**

Tailcat enforces client access control at the network layer by validating peer identities against an allow‑list of WireGuard node public keys. This mechanism prevents unauthorized nodes from establishing tunnels, even if they possess valid Tailscale credentials. The implementation resides in the core server logic of the `tailscale/tailcat` repository, with both CLI and programmatic interfaces available for managing allowed clients.

## How Tailcat's Client Allow‑listing Works

At its core, Tailcat stores permitted clients in an internal map keyed by `key.NodePublic`. The server performs a fast map lookup when handling incoming connections. If no allow‑list is configured, the map remains `nil` and all clients pass through.

In [[`tailcat.go`](https://github.com/tailscale/tailcat/blob/main/tailcat.go)](https://github.com/tailscale/tailcat/blob/main/tailcat.go), the critical validation occurs around line 1595:

```go
if b.allowedClients != nil && !b.allowedClients[src] {
    return nil, fmt.Errorf("client %v not allowed", src)
}

```

The `allowedClients` field lives on the `locoBackend` struct and is populated through `Server.AddAllowedClient` or CLI flag parsing. When `allowedClients` is `nil`, this check short‑circuits, preserving backward compatibility for open servers.

## Method 1: Using the `--allow` CLI Flag

The simplest way to restrict access is passing public keys via the `--allow` flag when starting the server. This is implemented in [[`cmd/tailcat/tailcat.go`](https://github.com/tailscale/tailcat/blob/main/cmd/tailcat/tailcat.go)](https://github.com/tailscale/tailcat/blob/main/cmd/tailcat/tailcat.go).

### Flag Syntax and Parsing

The flag accepts a comma‑separated list of base64‑encoded node public keys or the special value `none` to block all clients:

```bash

# Allow specific clients

tailcat serve --allow=nodekey:abc123...,nodekey:def456... 22

# Block all clients (explicit deny)

tailcat serve --allow=none 22

```

Behind the scenes, [`cmd/tailcat/tailcat.go`](https://github.com/tailscale/tailcat/blob/main/cmd/tailcat/tailcat.go) parses each entry around line 1405 using `key.NewNodePublicFromString`, then invokes `Server.AddAllowedClient` for each valid key.

### Generating Client Keys

Use the built‑in key generation command to create client credentials:

```bash

# Generate a client key pair

tailcat genkey --client > client1.key
cat client1.key | tailcat pubkey > client1.pub

# Copy the public key (nodekey:...) for the server --allow flag

cat client1.pub

```

### Complete Server Example

```bash
#!/bin/bash

# Generate two client keys

CLIENT1=$(tailcat genkey --client | tailcat pubkey | tr -d '\n')
CLIENT2=$(tailcat genkey --client | tailcat pubkey | tr -d '\n')

# Start server allowing only these two clients

tailcat serve \
  --allow="${CLIENT1},${CLIENT2}" \
  --statedir=/var/lib/tailcat \
  0.0.0.0:22

```

## Method 2: Programmatic Allow‑listing with `Server.AddAllowedClient`

For dynamic access control, import Tailcat as a library and manipulate the allow‑list at runtime. The `Server.AddAllowedClient` method in [[`tailcat.go`](https://github.com/tailscale/tailcat/blob/main/tailcat.go)](https://github.com/tailscale/tailcat/blob/main/tailcat.go) (lines 893‑898) provides a safe, concurrency‑aware way to add keys.

### Method Signature

```go
func (s *Server) AddAllowedClient(k key.NodePublic)

```

The method uses `mak.Set` to handle map initialization safely:

```go
func (s *Server) AddAllowedClient(k key.NodePublic) {
    s.lb.mu.Lock()
    defer s.lb.mu.Unlock()
    mak.Set(&s.lb.allowedClients, k, true)
}

```

### Runtime Allow‑listing Example

```go
package main

import (
    "context"
    "log"
    "os"
    "os/signal"

    "tailscale.dev/tailcat"
    "tailscale.dev/tailcat/key"
)

func main() {
    ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
    defer cancel()

    // Initialize server with default configuration
    srv := &tailcat.Server{
        Port: 22,
    }

    if err := srv.Start(); err != nil {
        log.Fatalf("server start failed: %v", err)
    }

    // Add permitted clients from environment or external source
    allowedKeys := []string{
        os.Getenv("ALLOWED_CLIENT_1"),
        os.Getenv("ALLOWED_CLIENT_2"),
    }

    for _, ks := range allowedKeys {
        if ks == "" {
            continue
        }
        k, err := key.NewNodePublicFromString(ks)
        if err != nil {
            log.Printf("invalid key %q: %v", ks, err)
            continue
        }
        srv.AddAllowedClient(k)
        log.Printf("added client: %v", k)
    }

    <-ctx.Done()
    srv.Close()
}

```

## Method 3: SSH‑level Authorization (Separate from Tunnel Allow‑list)

Tailcat's `ssh` subcommand supports an additional layer of access control via `--ssh-authorized-keys`. This operates at the SSH protocol layer and is distinct from the WireGuard tunnel allow‑list described above.

Key differences:

- **Tunnel allow‑list (`--allow`)**: Controls who may establish the encrypted WireGuard tunnel. Denied clients cannot reach the SSH server at all.
- **SSH authorization**: Controls who may authenticate via SSH once connected. A client may pass tunnel allow‑listing but fail SSH key verification.

For defense‑in‑depth, configure both layers as shown in [[`cmd/tailcat/ssh_authorized_keys.go`](https://github.com/tailscale/tailcat/blob/main/cmd/tailcat/ssh_authorized_keys.go)](https://github.com/tailscale/tailcat/blob/main/cmd/tailcat/ssh_authorized_keys.go):

```bash
tailcat serve \
  --allow=nodekey:abc123... \
  --ssh-authorized-keys=/etc/tailcat/authorized_keys \
  22

```

## Testing Allow‑list Behavior

The repository includes comprehensive tests in [[`tailcat_test.go`](https://github.com/tailscale/tailcat/blob/main/tailcat_test.go)](https://github.com/tailscale/tailcat/blob/main/tailcat_test.go) (lines 293‑331) demonstrating allow‑list enforcement. These tests verify that:

1. Servers with empty allow‑lists accept all clients
2. Servers with populated allow‑lists reject unknown keys
3. Runtime mutations via `AddAllowedClient` take effect immediately

Excerpt from the test suite:

```go
func TestServerAllowlist(t *testing.T) {
    s := newTestServer(t)
    
    // Initially nil allow‑list permits all
    if s.lb.allowedClients != nil {
        t.Fatal("expected nil allowedClients initially")
    }

    // Add specific client
    allowed := key.NewNode().Public()
    s.AddAllowedClient(allowed)

    // Verify map populated
    s.lb.mu.Lock()
    if !s.lb.allowedClients[allowed] {
        t.Fatal("expected client to be allowed")
    }
    s.lb.mu.Unlock()

    // Connection from disallowed client should fail
    // ... test helper simulates handshake with random key
}

```

## Comparison of Allow‑listing Approaches

| Approach | Use Case | Persistence | Performance Impact |
|----------|----------|-------------|------------------|
| **`--allow` flag** | Static, known client sets | Configured at startup; restart required to modify | None; map built once |
| **`AddAllowedClient`** | Dynamic, runtime membership changes | In‑memory only; implement persistence externally | Negligible; lock‑contended only on mutation |
| **`none` value** | Maintenance mode, emergency lockdown | Immediate effect | N/A (blocks all) |

## Security Considerations

- **Key rotation**: When rotating client keys, add the new key before removing the old to avoid connection interruptions.
- **Audit logging**: Wrap `AddAllowedClient` calls with logging to maintain an audit trail of runtime permission changes.
- **Least privilege**: Start with `--allow=none` and explicitly add required clients rather than relying on default open behavior.

## Summary

- Tailcat's client allow‑list operates on WireGuard node public keys stored in `locoBackend.allowedClients`
- Use `--allow` at startup for static configuration; parse keys with `key.NewNodePublicFromString`
- Call `Server.AddAllowedClient` for dynamic, runtime access control
- The special `none` value creates an empty allow‑list, blocking all clients
- SSH authorization (`--ssh-authorized-keys`) provides a separate, complementary security layer
- Test coverage in [`tailcat_test.go`](https://github.com/tailscale/tailcat/blob/main/tailcat_test.go) validates allow‑list behavior across connection scenarios

## Frequently Asked Questions

### What happens if I don't specify the `--allow` flag?

If no `--allow` flag is provided, `allowedClients` remains `nil` and Tailcat permits connections from any authenticated Tailscale node. This default behavior ensures backward compatibility but should be explicitly restricted in production deployments.

### Can I remove a client from the allow‑list at runtime?

The current API only supports adding clients via `AddAllowedClient`. To remove access, restart the server with an updated `--allow` list or implement a custom wrapper that maintains a separate revocation layer. The underlying map structure supports deletion, but no public method exposes this functionality.

### How does Tailcat validate that a presented key is legitimate?

Tailcat relies on the WireGuard handshake and Tailscale control plane to cryptographically verify that a peer possesses the private key corresponding to its claimed public key. The allow‑list check occurs after this verification, ensuring that only cryptographically authenticated identities are evaluated against your access policy.