# How to Run an Auth-Free SSH Server with Tailcat

> Set up an auth-free SSH server with Tailcat. Eliminate password and key management by using Tailscale for secure peer identity. Simplify your SSH access today.

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

---

**Tailcat enables an authentication-free SSH server that eliminates password and key management by relying on the underlying Tailscale WireGuard tunnel for peer identity verification.**

Tailcat, an experimental open-source project from the Tailscale team, ships with a built-in SSH server that removes traditional authentication burdens. Unlike standard SSH daemons that require password or public-key validation, this implementation trusts the encrypted WireGuard tunnel established by Tailscale to verify peer identity. The server runs on Unix platforms (Linux and macOS) and exposes a standard SSH interface on port 22 without prompting for credentials.

## How Tailcat Eliminates SSH Authentication

### The NoClientAuthHandler Implementation

In [`tailcat_ssh.go`](https://github.com/tailscale/tailcat/blob/main/tailcat_ssh.go), the server configures `gliderssh.Server` with a `NoClientAuthHandler` that immediately returns `nil`, effectively disabling the authentication step. When a TCP connection arrives on the SSH port, `Server.HandleTailscaleSSHConn` creates the server instance with this handler, accepting all connections originating from the Tailscale network because the WireGuard tunnel already guarantees the peer's identity.

```go
// From tailcat_ssh.go - authentication is bypassed
NoClientAuthHandler: func(ctx ssh.Context) error { 
    return nil 
}

```

### Persistent Host Key Generation

Although authentication is skipped, the server still requires a host key for the SSH protocol handshake. The function `getHostKeys()` generates an **ed25519** host key on first use and persists it to `~/.config/tailcat/ssh/ssh_host_ed25519_key`. This ensures connection consistency across server restarts while maintaining the zero-credential workflow for connecting users.

## Starting the Auth-Free SSH Server

### Programmatic Setup with Go

You can embed the SSH server directly into your Go application by calling `StartSSH()` on a Tailcat server instance. The following example demonstrates initializing the data plane and enabling the SSH listener:

```go
package main

import (
    "log"
    "tailscale.com/tailcat"
)

func main() {
    // Create a Tailcat server instance
    s, err := tailcat.NewServer(tailcat.ServerOptions{
        // Configure DERP region, ConnBlob, etc.
    })
    if err != nil {
        log.Fatalf("tailcat server: %v", err)
    }

    // Start the WireGuard tunnel in the background
    go s.Serve()

    // Enable auth-free SSH on port 22
    s.StartSSH(22)

    // Block forever
    select {}
}

```

When `StartSSH()` is invoked, it binds to the Tailscale tunnel interface and routes incoming connections through `HandleTailscaleSSHConn`, which internally instantiates the `gliderssh.Server`.

### Command-Line Interface

For immediate usage without coding, the `cmd/tailcat` CLI exposes the SSH functionality through flags:

```bash

# Install the CLI

go install tailscale.dev/tailcat/cmd/tailcat@latest

# Start server with SSH enabled

tailcat server -ssh :22

# Connect from a client using the ConnBlob

tailcat client -ssh <connblob>

```

## Session Handling and PTY Support

### Command vs Interactive Sessions

The `sessionHandler` function in [`tailcat_ssh.go`](https://github.com/tailscale/tailcat/blob/main/tailcat_ssh.go) differentiates between command execution and interactive login shells. It queries the OS user via `user.Current()`, determines the login shell through `loginShell()`, and builds an environment containing `SHELL`, `USER`, `HOME`, `PATH`, and accepted `SSH_` variables. If the client provides a command via `sess.RawCommand()`, the server executes the shell with `-c <cmd>`; otherwise, it launches an interactive login shell with the `-l` flag.

### Pseudo-Terminal Management

When a client requests a PTY (pseudo-terminal), `runWithPTY` creates the terminal using `github.com/creack/pty`, propagates the client's terminal size and mode settings, and ties the PTY master to the SSH session. For non-PTY sessions, `runWithPipes` simply pipes STDIN, STDOUT, and STDERR between the command and the SSH channel without terminal emulation.

```go
// PTY sessions use creack/pty for terminal emulation
func runWithPTY(cmd *exec.Cmd, sess ssh.Session) error {
    ptyReq, winCh, isPty := sess.Pty()
    if !isPty {
        return runWithPipes(cmd, sess)
    }
    // ... PTY setup and I/O forwarding
}

```

## Security Boundaries for Auth-Free Operation

Because the SSH server implements **NoClientAuth**, you must ensure the Tailscale tunnel itself is the sole security boundary. The server binds only to the WireGuard interface created by Tailscale, meaning only peers possessing the correct `ConnBlob` can reach port 22. The SSH daemon does not listen on public network interfaces, preventing exposure to brute-force attacks from the open internet. According to the [`tailcat_ssh.go`](https://github.com/tailscale/tailcat/blob/main/tailcat_ssh.go) source, this design shifts trust from SSH credentials to the cryptographic identity verification already performed by the Tailscale network layer.

## Summary

- **Authentication-free operation**: The server uses `NoClientAuthHandler` returning `nil` to skip password and key checks, relying entirely on the WireGuard tunnel for security.
- **Host key persistence**: Ed25519 host keys are generated on demand and stored in `~/.config/tailcat/ssh/ssh_host_ed25519_key`.
- **Entry point**: `Server.HandleTailscaleSSHConn` in [`tailcat_ssh.go`](https://github.com/tailscale/tailcat/blob/main/tailcat_ssh.go) processes incoming TCP connections and initializes the SSH session.
- **Session flexibility**: `sessionHandler` supports both command execution (`-c` flag) and interactive login shells (`-l` flag).
- **PTY support**: Interactive sessions use `runWithPTY` with `creack/pty`, while non-interactive sessions use `runWithPipes`.
- **Activation methods**: Enable via `Server.StartSSH()` in Go code or the `-ssh` flag in the `cmd/tailcat` CLI.

## Frequently Asked Questions

### How does Tailcat secure SSH without passwords?

Tailcat relies on the Tailscale WireGuard tunnel to authenticate peers at the network layer before they ever reach the SSH server. Because the tunnel cryptographically verifies the identity of connecting machines, the SSH server itself can safely disable authentication using the `NoClientAuthHandler`, eliminating credential management while maintaining security through the encrypted channel.

### Where does Tailcat store SSH host keys?

The host key is stored at `~/.config/tailcat/ssh/ssh_host_ed25519_key`. The function `getHostKeys()` in [`tailcat_ssh.go`](https://github.com/tailscale/tailcat/blob/main/tailcat_ssh.go) handles generation of the ed25519 key on first server startup and loads it on subsequent runs to ensure consistent server fingerprinting without requiring manual key distribution.

### Can I run the Tailcat SSH server on Windows?

No, the current implementation requires Unix sockets and is limited to Linux and macOS platforms. The `HandleTailscaleSSHConn` function and underlying `creack/pty` library depend on Unix-specific features for pseudo-terminal management that are not available on Windows.

### How do I restrict which users can access the SSH server?

The server uses the OS user database via `user.Current()` to validate usernames, meaning any user existing on the host system can connect. There is no additional authorization layer within Tailcat itself; access control is enforced entirely through Tailscale's network policies and `ConnBlob` distribution, which determine which peers can establish the initial connection to the SSH port.