How Tailcat's SFTP File Server Works: Subsystem Architecture and Security Modes

Tailcat implements SFTP as an SSH subsystem that creates either a full-filesystem server mirroring shell access or a root-confined server restricted to a specific directory, with configurable read-only, write-only, or write-only-plus modes for secure file transfers.

Tailcat, Tailscale's open-source secure file transfer utility, embeds its SFTP capabilities directly into the SSH server stack rather than running as a standalone daemon. When an SSH client requests the "sftp" subsystem, Tailcat's SFTP file server dynamically provisions access based on the FileService configuration, using either a standard sftp.Server for full access or a constrained sftp.RequestServer for restricted drop-box scenarios. The core implementation resides in tailcat_sftp.go, which wraps the github.com/pkg/sftp library to provide portable, permission-controlled file operations across Linux, macOS, and Windows.

SSH Subsystem Registration

Tailcat registers the SFTP subsystem through the sftpSubsystemHandler function defined in tailcat_sftp.go (lines 30‑73). This handler evaluates the session's SSHOptions to determine whether to enable file transfer capabilities.

If the user has not requested file access (opts.Files == nil) and no shell session is enabled, the handler returns nil, effectively disabling the SFTP subsystem for that connection. Otherwise, the handler instantiates either a full or rooted server based on configuration and runs it for the duration of the SSH session, blocking until the client disconnects.

Server Modes: Full vs. Rooted

Tailcat supports two distinct SFTP server implementations depending on the desired security boundary.

Full Filesystem Access

The newFullSFTPServer function (lines 74‑84) creates a standard SFTP server using sftp.NewServer, passing the session's io.ReadWriteCloser as the transport. This mode optionally sets the working directory to the user's home path using sftp.WithServerWorkingDirectory. Clients connecting to a full server see the same filesystem tree as they would in a normal interactive shell session, with identical permissions and path resolution.

Root-Confined Access

For restricted environments, newRootedSFTPServer (lines 86‑102) establishes an os.Root at the directory specified by FileService.Dir. This function constructs an sftp.RequestServer with a custom handler (rootedFiles) that confines all file operations—including Stat, ReadDir, and Open—to the specified subtree. The use of os.Root ensures that path traversal attacks are blocked at the OS level, making the implementation safe across different operating systems.

Configurable File Service Modes

The FileService configuration defines a Mode field that enforces access controls at the request level. The rootedFiles handler checks this mode in methods such as Fileread, Filewrite, Filecmd, and Filelist to determine operation validity.

  • Read-only (FileServeRO): Permits only read operations. Clients can list directories, download files, and retrieve attributes, but any write attempt returns a permission error.

  • Write-only (FileServeWO): Implements a drop-box pattern where clients can create new files but cannot read existing content or modify files they did not upload. Each upload generates a random, server-chosen filename via uniqueUploadPath to prevent collisions and information leakage.

  • Write-only-plus (FileServeWOPlus): Extends write-only mode to allow overwriting existing files by generating a fresh unique filename when a conflict occurs, effectively supporting atomic replacement without revealing directory contents.

Path Handling and Confinement

All SFTP paths arrive as absolute strings (e.g., "/foo/bar"). The helper function rel (lines 18‑26) strips the leading slash and returns a path relative to the os.Root, returning "." when the client references the root directory itself. This normalization ensures that the os.Root implementation safely resolves paths without exposing parent directories, maintaining security boundaries even when clients request absolute paths.

Write-Only Drop-Box Semantics

When operating in write-only modes, Tailcat enforces strict visibility constraints through the wrote map protected by a mutex. The markOwn function records each successfully uploaded file path, allowing the ownPath helper (lines 28‑44) to verify whether subsequent operations—such as Setstat—target files owned by the current session.

This design allows clients to modify metadata on their own uploads while preventing directory listings that would reveal other users' files. In Filelist, write-only modes return PermissionDenied for List operations, effectively creating a blind drop-box where files disappear into the directory without revealing the contents to subsequent operations.

Unique Upload Filenames and Atomic Creation

The uniqueUploadPath function (lines 24‑40) generates collision-resistant filenames by combining the original file stem with a UTC timestamp and eight random bytes encoded as hexadecimal. When creating the file, Tailcat passes the O_EXCL flag to ensure atomic creation, preventing race conditions where two concurrent uploads might target the same generated name. This guarantees that each upload receives a distinct filename without requiring client-side coordination.

Permission Handling and Attribute Management

The Filecmd method determines whether requests such as Mkdir, Setstat, Rename, or Remove are permitted based on the current FileService.Mode. In write-only configurations, only a limited subset of commands that support the drop-box workflow are allowed.

The setstat helper applies changes to file permissions (chmod), timestamps, and size while deliberately ignoring UID and GID modifications. Because Tailcat operates as an identity-free tunnel where authentication is handled by the underlying Tailscale network layer, UID/GID mappings are irrelevant and discarded to maintain portability across operating systems with different user management schemes (lines 92‑122).

Directory Listings and Status Operations

The Filelist method handles List and Stat requests. In read-only mode, it delegates to the underlying os.Root to return directory entries and file attributes. In write-only modes, it returns PermissionDenied for directory listings to enforce the drop-box visibility policy, while still allowing Stat operations on specific files if the client knows the exact path.

Lstat and Readlink operations follow the same permission rules, delegating to the confined os.Root filesystem when the request passes mode checks. This ensures that symbolic links and file metadata respect the same boundary constraints as regular file operations.

Session Termination and Exit Status

After srv.Serve() returns, Tailcat handles session teardown in a specific order to prevent client-side failures. The server logs any error, sends an exit status via sess.Exit(0) on success or sess.Exit(1) on error, and only then closes the server with srv.Close() (lines 56‑70). This sequencing ensures that SFTP clients—including scp implementations—receive the exit status before the underlying channel closes, avoiding premature connection errors during file transfer finalization.

Practical Implementation Examples

The following examples demonstrate how to configure and interact with Tailcat's SFTP server:

// Server-side: Configure a write-only drop-box SFTP subsystem.
opts := tailcat.SSHOptions{
    Files: &tailcat.FileService{
        Dir:  "/var/uploads",
        Mode: tailcat.FileServeWO, // Write-only mode
    },
}
handler := server.sftpSubsystemHandler(opts)
handler(sess) // Blocks until client disconnects
// Client-side: Upload a file to the write-only drop-box.
import "github.com/pkg/sftp"
client, _ := sftp.NewClient(conn)
f, _ := client.Create("report.pdf") // Server generates unique filename
f.Write([]byte("confidential data"))
f.Close()
// Client-side: List files when connected to a read-only server.
client, _ := sftp.NewClient(conn)
files, _ := client.ReadDir(".")
for _, fi := range files {
    fmt.Println(fi.Name(), fi.Size())
}

Summary

  • Subsystem Integration: Tailcat's SFTP server registers as an SSH subsystem via sftpSubsystemHandler in tailcat_sftp.go, conditionally enabling based on SSHOptions.
  • Dual Server Types: Full filesystem access uses sftp.NewServer, while confined access uses sftp.RequestServer with an os.Root for path isolation.
  • Access Modes: Three modes (FileServeRO, FileServeWO, FileServeWOPlus) control read and write permissions at the request handler level.
  • Drop-Box Security: Write-only modes use uniqueUploadPath with O_EXCL for atomic uploads and the wrote map to track session-owned files, preventing directory snooping.
  • Cross-Platform: All filesystem operations route through os.Root, ensuring consistent behavior across Linux, macOS, and Windows while blocking directory traversal attacks.

Frequently Asked Questions

What is the difference between full and rooted SFTP modes in Tailcat?

Full mode creates a standard sftp.Server that exposes the entire filesystem accessible to the user, identical to a normal SSH shell session. Rooted mode creates an sftp.RequestServer confined to a specific directory via os.Root, preventing access to any paths outside the configured FileService.Dir. Full mode is implemented in newFullSFTPServer (lines 74‑84), while rooted mode uses newRootedSFTPServer (lines 86‑102).

How does Tailcat prevent filename collisions in write-only drop-boxes?

Tailcat generates unique filenames using the uniqueUploadPath function, which concatenates the original file stem with a UTC timestamp and eight random hex-encoded bytes. The server creates the file with the O_EXCL flag to ensure atomic, fail-if-exists semantics, guaranteeing that concurrent uploads receive distinct filenames without client coordination.

Can clients modify files after uploading them in write-only mode?

Yes, but only the files they uploaded during the current session. Tailcat tracks uploaded files in a concurrency-safe wrote map using markOwn, and the ownPath helper verifies ownership before allowing Setstat or other modification operations. Clients cannot see, read, or modify files uploaded by other sessions, maintaining the drop-box isolation guarantee.

Why does Tailcat ignore UID and GID changes in SFTP setstat requests?

Because Tailcat operates as an identity-free tunnel where authentication and authorization are handled by the Tailscale mesh network rather than traditional Unix user accounts, the setstat implementation deliberately ignores UID and GID modifications. It applies only chmod permissions, timestamps, and file size changes, ensuring consistent behavior across platforms with different user management systems.

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 →