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--allowon 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 sshwith--ssh-authorized-keysfor standard public key authentication, orno-auth-sshfor 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
filesalongside SSH for automatic SFTP subsystem support - Inspect
$TAILCAT_PEER_KEYin 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →