How to Use Tailcat for Secure Shell (SSH) Access: Two Methods Explained
Tailcat provides a built-in SSH server that runs over WireGuard-encrypted tunnels, offering both public-key authentication and bearer-token access modes without requiring root privileges or host networking changes.
Tailcat, an open-source project from the Tailscale team (tailscale/tailcat), ships with a complete SSH implementation that operates entirely in userspace. Unlike traditional SSH daemons that bind to host network interfaces, Tailcat's SSH server leverages the repository's WireGuard-based tunneling to provide secure shell access through encrypted, NAT-traversing connections.
Understanding Tailcat's Built-In SSH Server
The SSH implementation resides primarily in tailcat_ssh.go, which contains the core logic for creating an SSH server instance and wiring it to Tailcat's netstack TCP listener. When a client connects, the server maps the SSH session onto a pseudo-terminal or executes forced commands. Platform-specific PTY handling is implemented in tailcat_ssh_unix.go and tailcat_ssh_windows.go, ensuring native terminal behavior on Linux, macOS, and Windows.
The server registers itself within Tailcat's service framework. When started via tailcat serve ssh, the handler attaches to Server.OnTCP and activates upon incoming connections to port 22 over the Tailcat tunnel.
Method 1: Public-Key-Authenticated SSH (Recommended)
For production environments, Tailcat supports standard OpenSSH public-key authentication through the --ssh-authorized-keys flag. The CLI parses this flag in cmd/tailcat/ssh_authorized_keys.go, loading keys from local files, raw OpenSSH format, or directly from GitHub user accounts.
The server creates an ssh.ServerConfig that requires a matching public key before opening a shell, providing defense-in-depth alongside the WireGuard tunnel encryption.
To start a public-key-authenticated server:
# Use your local authorized_keys file
tailcat serve --ssh-authorized-keys=~/.ssh/authorized_keys ssh
The server outputs a Tailcat address (e.g., tc1a2b3c...) that serves as the connection endpoint.
Connect from a client machine:
tailcat ssh <tc-address>
You will be prompted for your SSH key's passphrase if applicable, then receive a shell.
You can also specify multiple key sources, including GitHub users:
tailcat serve --ssh-authorized-keys=alice@github,./team-key.pub,bob@github ssh
Method 2: Auth-Free SSH (No-Auth Mode)
Tailcat offers an auth-free mode (no-auth-ssh) where the secrecy of the Tailcat address itself serves as the sole credential. The address contains a WireGuard pre-shared key (PSK) that acts as a bearer capability.
Start an auth-free server:
tailcat serve no-auth-ssh
Connect using the printed address:
tailcat ssh tcXyZ...
Warning: According to the tailscale/tailcat README, this mode is equivalent to exposing a shell on the host. Anyone who discovers the address can connect, making the PSK the only protection. Share the address only over private channels and use this mode exclusively for temporary testing or trusted environments.
Client Access Patterns
Direct SSH Connections
The simplest client workflow uses the built-in ssh subcommand:
# Server side
tailcat serve ssh
# Note the address: tcAbc...
# Client side
tailcat ssh tcAbc...
Port Forwarding for Standard SSH Clients
If you prefer using your system's ssh binary or need to integrate with existing tools, forward the remote SSH port to a local port:
# Server side
tailcat serve ssh
# Client side - forward remote port 22 to local 2222
tailcat forward tcAbc... 2222:22
# Use any SSH client
ssh -p 2222 user@localhost
Implementing SSH Services in Go
Developers can embed Tailcat's SSH server directly into Go applications. The tailcat_ssh.go file exposes SSHHandler(), which registers the built-in SSH server on port 22.
Example server implementation:
package main
import (
"context"
"log"
"net"
"os"
"github.com/tailscale/tailcat"
)
func main() {
// Initialize server with SSH handler on port 22
s := &tailcat.Server{
OnTCP: func(port uint16) func(net.Conn) {
if port == 22 {
return tailcat.SSHHandler()
}
return nil
},
}
if err := s.Start(); err != nil {
log.Fatal(err)
}
log.Println("Tailcat address:", s.TailcatAddr())
select {}
}
Client connection via the Go library:
cl := tailcat.NewClient(tailcat.Addr(os.Args[1]))
c, err := cl.DialTCPPort(context.Background(), 22)
if err != nil {
log.Fatal(err)
}
// Use golang.org/x/crypto/ssh to negotiate session over c
_ = c // placeholder – real code would create ssh.ClientConn over c
Security Architecture and Defense in Depth
Tailcat's SSH implementation provides two distinct security layers:
- Transport Layer: WireGuard encryption combined with the pre-shared key embedded in the Tailcat address ensures confidentiality and integrity. The address format includes the WireGuard public key and PSK.
- Authentication Layer: In public-key mode, the SSH server enforces OpenSSH-compatible key verification using sources parsed by
cmd/tailcat/ssh_authorized_keys.go. This prevents unauthorized access even if the address becomes public.
The auth-free mode removes the authentication layer, relying solely on the PSK's secrecy. This trade-off prioritizes convenience over granular access control.
Summary
- Tailcat provides a userspace SSH server in
tailcat_ssh.gothat requires no root privileges or host network configuration. - Public-key authentication uses
--ssh-authorized-keysto accept connections only from authorized keys, with support for local files and GitHub accounts. - Auth-free mode treats the Tailcat address as a bearer token; the WireGuard PSK embedded in the address provides the sole access control.
- The server handles PTY allocation through platform-specific implementations in
tailcat_ssh_unix.goandtailcat_ssh_windows.go. - Clients can connect via
tailcat ssh, port forwarding, or programmatically using the Go library'sDialTCPPortmethod.
Frequently Asked Questions
Is Tailcat SSH suitable for production environments?
Yes, when using public-key authentication. The cmd/tailcat/ssh_authorized_keys.go implementation validates keys against OpenSSH standards, and the WireGuard tunnel provides transport security. However, the auth-free mode should be restricted to development or highly trusted networks due to its reliance on address secrecy alone.
How does Tailcat SSH differ from running OpenSSH over Tailscale?
Tailcat's SSH implementation operates entirely in userspace through the repository's netstack, eliminating the need for host networking privileges or separate daemon configuration. Unlike OpenSSH, which binds to system ports and requires root access for port 22, Tailcat's server runs as a standard user process while still providing encrypted tunneling via WireGuard.
Can I use my existing OpenSSH authorized_keys files?
Absolutely. The --ssh-authorized-keys flag accepts standard OpenSSH authorized_keys file formats. The parser in tailcat_ssh_keys.go (referenced by the CLI) processes these files identically to OpenSSH, ensuring compatibility with existing key infrastructure.
What platforms support Tailcat SSH?
All major platforms are supported through conditional compilation. The core logic in tailcat_ssh.go handles protocol operations, while tailcat_ssh_unix.go provides POSIX PTY support and tailcat_ssh_windows.go manages Windows console handles. This architecture ensures functional parity across Linux, macOS, and Windows without platform-specific configuration.
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 →