# How to Use Tailcat for SSH Access: Complete Configuration Guide

> Learn how to use Tailcat for secure SSH access with this complete configuration guide. Effortlessly expose an SSH server over a Tailscale tunnel using the serve sub-command.

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

---

**Tailcat exposes an SSH server over a Tailscale-only data-plane tunnel using the `serve` sub-command with `ssh` or `no-auth-ssh` as the service type.**

Tailcat is a Tailscale utility that creates secure tunnels between nodes. Its built-in SSH server runs directly over these tunnels, eliminating the need for separate SSH daemon configuration while leveraging Tailscale's mesh networking for transport security.

## Understanding Tailcat SSH Architecture

The SSH server is compiled into the binary when the build tag `!ts_omit_ssh` is present. Unlike traditional SSH servers that listen on public interfaces, Tailcat's SSH handler wraps incoming TCP connections inside an **SSH server** that operates exclusively over the Tailcat tunnel layer.

In `tailcat_ssh.go:42-57`, the `sshHandler` ([`s.SSHConnHandler`](https://github.com/tailscale/tailcat/blob/main/tailcat_ssh.go#L42-L57)) receives the raw connection and initializes the SSH protocol handshake. This design ensures that all SSH traffic remains within the Tailscale mesh network, never touching the public internet.

## Starting the SSH Server

### Public Key Authentication Mode

The standard **`ssh`** service requires explicit key authorization. Use the `--ssh-authorized-keys` flag to specify allowed public keys:

```bash
tailcat serve \
    --ssh-authorized-keys=alice@github \
    ssh

```

The flag accepts comma-separated sources: local file paths, literal public keys, or `user@github` shortcuts. In `main/cmd/tailcat/tailcat.go:78-88`, the `loadSSHAuthorizedKeys` function parses these at startup and passes the resulting slice to `SSHOptions.AuthorizedKeys`.

The server prints a tailcat address (format `tc://...`) to stdout upon successful initialization. This address serves as the target for client connections.

### Authentication-Free Mode

The alternative **`no-auth-ssh`** service disables public key verification entirely:

```bash
tailcat serve no-auth-ssh

```

In this mode, the tunnel itself provides identity. The handler extracts the peer's node key and injects it as the **`TAILCAT_PEER_KEY`** environment variable ([`sessionHandler`](https://github.com/tailscale/tailcat/blob/main/tailcat_ssh.go#L30-L33)). This suits environments where Tailscale's node-level authentication satisfies security requirements.

## Connecting from Clients

Use the built-in **`tailcat ssh`** client to establish connections:

```bash
tailcat ssh user@<tc-addr>

```

The client creates a `tailcat.Client`, dials the server's TCP port 22, and proxies the raw connection to the local system `ssh` binary via standard input/output streams. This preserves all native SSH client features: terminal handling, escape sequences, and agent forwarding.

## Forced Commands and Shell Behavior

By default, Tailcat spawns an interactive login shell. The platform-specific implementations in [[`tailcat_ssh_unix.go`](https://github.com/tailscale/tailcat/blob/main/tailcat_ssh_unix.go)](https://github.com/tailscale/tailcat/blob/main/tailcat_ssh_unix.go) and [[`tailcat_ssh_windows.go`](https://github.com/tailscale/tailcat/blob/main/tailcat_ssh_windows.go)](https://github.com/tailscale/tailcat/blob/main/tailcat_ssh_windows.go) construct the appropriate `exec.Command` via `newSessionCommand`.

To execute a fixed command for every session, append `--` followed by the command:

```bash
tailcat serve \
    --ssh-authorized-keys=alice@github \
    ssh -- /usr/local/bin/deploy.sh

```

This **`SSHOptions.Exec`** mechanism behaves identically to OpenSSH's `ForceCommand` directive, making it ideal for deployment automation, restricted shells, and CI/CD pipelines.

## Combining SSH with File Transfer

When the **`files`** service is enabled alongside SSH, the SFTP subsystem activates automatically:

```bash
tailcat serve \
    --ssh-authorized-keys=alice@github \
    --files=/srv/share:rw \
    ssh files

```

The `sftpSubsystemHandler` registers with the SSH server, allowing clients to use standard SFTP clients to read from and write to the directory specified by `--files`. Access permissions (`:rw` or `:ro`) control client capabilities.

## Environment Variables in SSH Sessions

Every SSH session receives two Tailcat-specific variables:

- **`TAILCAT_PEER_KEY`** — The remote node's public key (matches the value used by `--allow` on the tunnel layer)
- **`TAILCAT_REMOTE_ADDR`** — The remote address from the Tailcat connection metadata

These enable scripts to implement fine-grained access control based on verified node identity without relying on traditional SSH key management.

## Complete Configuration Examples

### Basic Development Server with GitHub Keys

```bash
tailcat serve \
    --ssh-authorized-keys="alice@github,bob@github" \
    --files=/home/dev/projects:ro \
    ssh files

```

### Automated Deployment Tunnel

```bash
tailcat serve \
    --ssh-authorized-keys=/etc/tailcat/deploy_keys \
    ssh -- /opt/scripts/validate-and-deploy.sh

```

### Internal Tooling Without Key Management

```bash
tailcat serve no-auth-ssh

# Client scripts check $TAILCAT_PEER_KEY against allowed node list

```

## Key Source Files

| File | Responsibility |
|------|---------------|
| [`tailcat_ssh.go`](https://github.com/tailscale/tailcat/blob/main/tailcat_ssh.go) | Core SSH server: `ssh.Server` construction, session handling, host key generation |
| [`tailcat_ssh_unix.go`](https://github.com/tailscale/tailcat/blob/main/tailcat_ssh_unix.go) / [`tailcat_ssh_windows.go`](https://github.com/tailscale/tailcat/blob/main/tailcat_ssh_windows.go) | Platform-specific command execution for login shells |
| [`cmd/tailcat/tailcat.go`](https://github.com/tailscale/tailcat/blob/main/cmd/tailcat/tailcat.go) | CLI flag parsing, `SSHOptions` assembly, authorized key loading |
| [`readme.go`](https://github.com/tailscale/tailcat/blob/main/readme.go) | Embedded documentation with usage examples |

These files in [tailscale/tailcat](https://github.com/tailscale/tailcat) collectively implement the SSH-over-Tailscale functionality, from connection wrapping to shell execution.

## Summary

- Use **`tailcat serve ssh`** with `--ssh-authorized-keys` for standard public key authentication, or **`no-auth-ssh`** for tunnel-only identity
- Connect with **`tailcat ssh user@<tc-addr>`** to proxy through the tunnel to your local SSH client
- Append **`-- <command>`** to enforce a fixed command for all sessions (forced command)
- Enable **`files`** alongside SSH for automatic SFTP subsystem support
- Inspect **`$TAILCAT_PEER_KEY`** in sessions to identify the connecting Tailscale node

## Frequently Asked Questions

### Does Tailcat SSH replace OpenSSH on my system?

No. Tailcat's SSH server binds only to the internal tunnel interface, not public ports. It complements rather than replaces system SSH daemons. You can run both simultaneously on the same host.

### How does `no-auth-ssh` maintain security without public keys?

Authentication shifts to the Tailscale layer—only nodes in your tailnet can reach the SSH port. The server exposes the verified peer identity via `$TAILCAT_PEER_KEY`, allowing authorization logic in shell profiles or forced commands to gate access.

### Can I use standard `ssh` directly instead of `tailcat ssh`?

Not directly. The `tailcat ssh` sub-command handles the tunnel establishment and TCP dialing before handing off to the system SSH binary. Without this wrapper, the `tc://` addresses are not resolvable by standard SSH clients.

### What SSH protocols and ciphers are supported?

Tailcat delegates to Go's `crypto/ssh` package in [`tailcat_ssh.go`](https://github.com/tailscale/tailcat/blob/main/tailcat_ssh.go). The implementation follows modern defaults from the Go standard library, with host keys generated automatically if not pre-configured.