A single-binary SFTP server for ad-hoc file transfers on a trusted network.
Run it in a directory, connect with any SFTP client, stop it when you're done. No daemon, no system configuration, no root privileges, no config file.
$ portable-sftp
portable-sftp v1.0.0
serving /home/you/Pictures (read-write)
listening [::]:2222
user sftp
password cAds-L29c-W88E-KWce
(generated for this run; set -password or $SFTP_PASSWORD to choose your own)
host key SHA256:PAbgL0iubsiG2h4YN1Fp9REmEGy2ai4L6q91JOvKTa4
connect with:
sftp -P 2222 sftp@192.168.1.2
Ctrl-C to stop.Moving a few files between a laptop and a phone, or between two machines on the same network, is a solved problem with about six unsatisfying solutions. This is one binary you can drop anywhere and delete afterwards.
Download a binary from Releases, or:
go install github.com/H7O/portable-sftp@latestBuild it yourself:
git clone https://github.com/H7O/portable-sftp
cd portable-sftp
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o portable-sftp .CGO_ENABLED=0 produces a static binary that runs on any Linux of the same
architecture. Cross-compile with GOOS/GOARCH:
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o portable-sftp-arm64 .Requires Go 1.25 or newer to build (it uses os.Root for path confinement).
portable-sftp [flags]
| Flag | Default | Meaning |
|---|---|---|
-root |
. |
Directory to serve |
-user |
sftp |
Username clients must log in as |
-password |
generated | Password to require |
-keyfile |
authorized_keys file to accept instead of a password |
|
-port |
2222 |
Port to listen on (0 picks a free one) |
-bind |
all | Address to bind; 127.0.0.1 for local only |
-read-only |
false |
Refuse writes, renames, and deletions |
-max-conns |
10 |
Maximum concurrent connections |
-hostkey |
~/.config/portable-sftp/host_key |
Host key path |
-stats-interval |
0 |
Log a summary at this interval |
-verbose |
false |
Log every request, including rejected ones |
-version |
Print the version and exit |
Share a folder read-only:
portable-sftp -root ~/Pictures -read-onlyUse a password you choose, without putting it in your shell history or in ps:
SFTP_PASSWORD='the one you want' portable-sftpUse key authentication:
portable-sftp -keyfile ~/.ssh/authorized_keysAccept connections from this machine only:
portable-sftp -bind 127.0.0.1Run it on a cloud VM for an hour, with keys only, on a port that sees less scanner traffic:
# on the VM
portable-sftp -root ~/transfer -keyfile ~/.ssh/authorized_keys -port 52222
# from your laptop
sftp -P 52222 -i ~/.ssh/id_ed25519 sftp@203.0.113.10A client that tries a password never sees a prompt, just Permission denied (publickey). The unusual port only cuts scanner noise — it is not a control,
and everything under Security still applies. If you can already
reach the machine over SSH, the next example avoids exposing a port at all.
Reach it through an SSH connection you already have, instead of opening it to the network:
# on the remote machine
portable-sftp -root ~/transfer -bind 127.0.0.1 -keyfile ~/.ssh/authorized_keys
# on your laptop
ssh -f -N -L 2222:localhost:2222 you@vm
sftp -P 2222 sftp@127.0.0.1-bind 127.0.0.1 listens on loopback only, so nothing on the network can reach
the server; the SSH connection carries the traffic. The forwarded port on your
laptop is loopback-only too. -f puts the forward in the background, where it
stays until you kill it. The username is still sftp, not your account on the
remote machine.
If you give neither -password nor -keyfile, a strong password is generated
and printed once at startup. There is no default password.
-password and -keyfile are mutually exclusive, and only the corresponding
authentication method is offered to clients. -keyfile accepts a normal
OpenSSH authorized_keys file, so multiple keys and comments work as expected.
A persistent Ed25519 host key is generated on first run and stored in
~/.config/portable-sftp/host_key, so your client won't warn about a changed
fingerprint every time you start the server. The fingerprint is printed at
startup — check it against what your client shows on first connection.
The key is deliberately kept outside the served directory, and the server
refuses to start if -hostkey points inside -root, since a client could
otherwise download it and impersonate the server.
GNOME Files connects from "Other Locations → Connect to Server" with a URI (Thunar, Nemo and Caja use the same GVfs backend and take the same URI in their location bar, Ctrl+L, if your distribution's gvfs backends are installed):
sftp://sftp@203.0.113.10:52222/
The username is your -user value, sftp — not your account name on either
machine. Leave it out and the connection is refused. The port is required too,
or the client tries 22. Through the forward above, the same URI is
sftp://sftp@127.0.0.1:2222/.
With a password you get a prompt. With a key there is nowhere in the dialog to
point at a key file — GVfs runs the OpenSSH client without -i, so the key has
to come from your desktop session's ssh-agent (GVfs asks the keyring for it,
so an agent you started yourself in a shell is not the one it uses) or from an
IdentityFile line in ~/.ssh/config. A Host pattern there has to match the
host as you typed it in the URI, not a friendly alias.
Through a forwarded loopback port the file manager shows no fingerprint prompt,
because GVfs passes -oNoHostAuthenticationForLocalhost yes. The sftp command
in the example above still prompts, and the ssh -L connection's own host key
check is what authenticates the machine.
KDE's Dolphin takes the same URI form, but speaks SSH through libssh rather than
the OpenSSH client, so the agent and ~/.ssh/config details above do not carry
over unchanged. Windows Explorer and macOS Finder have no native sftp://
support; use WinSCP, Cyberduck, or FileZilla.
This is a tool for trusted local networks. Do not expose it to the internet.
- Serves exactly one directory. Paths are confined with
os.Root, which resolves every path component through the kernel (openat2withRESOLVE_BENEATHwhere available)..., absolute paths, and symlinks pointing outside the root are all rejected, with no window between checking a path and opening it. - Never creates symlinks or hard links on the client's behalf.
- Offers only the authentication method you configured.
- Compares credentials in constant time.
- Caps concurrent connections and bounds the pre-authentication handshake.
- Recovers from panics in a session without taking down the server.
- No rate limiting on failed passwords. SSH caps attempts per connection, but nothing stops a client from reconnecting. Use the generated password or a key; don't pick a short one.
- No encryption at rest, no auditing, no multi-user support. One user, one directory, one password or key set.
- No protection against whoever runs it. The server has your filesystem
permissions.
-rootis a boundary for the client, not a sandbox for you. - Not hardened for hostile networks. It has not been audited, and it is not a replacement for OpenSSH.
If you need a real SFTP server, use OpenSSH with ChrootDirectory.
Found a security problem? See SECURITY.md.
go test ./... # unit and end-to-end tests
go test -race ./... # the end-to-end tests are concurrent
go vet ./...
govulncheck ./... # go install golang.org/x/vuln/cmd/govulncheck@latest
gofmt -l .The test suite starts real servers on ephemeral ports and drives them with a real SFTP client, including regression tests for path escapes, authentication bypasses, malformed protocol messages, and connection-limit enforcement.