How to Use Tailcat for SSH Access: Complete Configuration Guide

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) 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:

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:

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). 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:

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) and [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:

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:

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

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

Automated Deployment Tunnel

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

Internal Tooling Without Key Management

tailcat serve no-auth-ssh

# Client scripts check $TAILCAT_PEER_KEY against allowed node list

Key Source Files

File Responsibility
tailcat_ssh.go Core SSH server: ssh.Server construction, session handling, host key generation
tailcat_ssh_unix.go / tailcat_ssh_windows.go Platform-specific command execution for login shells
cmd/tailcat/tailcat.go CLI flag parsing, SSHOptions assembly, authorized key loading
readme.go Embedded documentation with usage examples

These files in 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. The implementation follows modern defaults from the Go standard library, with host keys generated automatically if not pre-configured.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →