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

> Discover how Tailcat's SFTP server uses SSH subsystems for secure file transfers. Explore its subsystem architecture and security modes for flexible access control.

- Repository: [Tailscale/tailcat](https://github.com/tailscale/tailcat)
- Tags: internals
- Published: 2026-09-08

---

**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`](https://github.com/tailscale/tailcat/blob/main/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`](https://github.com/tailscale/tailcat/blob/main/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:

```go
// 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

```

```go
// 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()

```

```go
// 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`](https://github.com/tailscale/tailcat/blob/main/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.