How to Run an Auth-Free SSH Server with Tailcat
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, 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.
// 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:
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:
# 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 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.
// 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 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
NoClientAuthHandlerreturningnilto 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.HandleTailscaleSSHConnintailcat_ssh.goprocesses incoming TCP connections and initializes the SSH session. - Session flexibility:
sessionHandlersupports both command execution (-cflag) and interactive login shells (-lflag). - PTY support: Interactive sessions use
runWithPTYwithcreack/pty, while non-interactive sessions userunWithPipes. - Activation methods: Enable via
Server.StartSSH()in Go code or the-sshflag in thecmd/tailcatCLI.
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 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.
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 →