Sftpd
A pluggable SFTP server for Elixir with memory, custom, and optional S3 backends.
Sftpd wraps Erlang's :ssh_sftpd subsystem and lets you plug storage behind
it through a small backend behaviour. It ships with:
- an in-memory backend for development and tests
- an optional S3 backend with range reads and multipart streaming writes
- password and public-key auth callbacks that return per-session context
- telemetry hooks around server lifecycle and SFTP operations
Installation
Version notes for this package:
-
verified minimum Elixir:
~> 1.14 -
verified minimum OTP for CI:
26 - current pinned development environment: Erlang/OTP 29.0
- current pinned development environment: Elixir 1.20.0-rc.5 on OTP 29
The package requirement is declared in mix.exs. The development environment
is pinned in .tool-versions.
def deps do
[
{:sftpd, "~> 0.1.1"}
]
endQuick Start
# Start with in-memory backend (great for development)
{:ok, ref} = Sftpd.start_server(
port: 2222,
backend: Sftpd.Backends.Memory,
backend_opts: [],
auth: {:passwords, [{"dev", "dev"}]},
system_dir: "/path/to/ssh_host_keys"
)
# Connect with: sftp -P 2222 dev@localhostGuides
- Getting Started for a step-by-step setup guide
- Phoenix Setup for supervised Phoenix setup with app auth and S3
- Backends for backend architecture and built-in backend tradeoffs
- Custom Backends for implementing your own backend
- Telemetry for emitted events, metadata, and examples
Key Concepts
Sftpd.start_server/1starts an SSH daemon configured with an SFTP file-handlerSftpd.child_spec/1lets Phoenix and other OTP apps supervise the serverSftpd.Authdefines password and public-key auth callbacksSftpd.Backenddefines the storage contractSftpd.Backends.Memoryis the fastest local setup pathSftpd.Backends.S3is the built-in persistent backendSftpd.Telemetrydocuments the instrumentation surface
Choosing a Backend
| Need | Use |
|---|---|
| Tests, demos, and local development | Sftpd.Backends.Memory |
| Amazon S3, MinIO, or another S3-compatible store | Sftpd.Backends.S3 |
| A local disk folder | A custom folder backend |
| A shared process, cache, queue, or connection pool | {:genserver, name_or_pid} |
| Async ingestion after upload | Store synchronously in the backend, then enqueue a Broadway job |
See Backends for backend tradeoffs and Custom Backends for folder, GenServer, supervision, and post-write processing examples.
Next Steps
- Adding SFTP to a Phoenix app? Start with Phoenix Setup.
- Choosing storage? Read Backends.
- Implementing your own storage layer? Read Custom Backends.
- Wiring observability? Read Telemetry.
Backends
Memory Backend
Stores files in memory. Useful for development and testing without external dependencies.
Sftpd.start_server(
port: 2222,
backend: Sftpd.Backends.Memory,
backend_opts: [],
auth: {:passwords, [{"user", "pass"}]},
system_dir: "/path/to/ssh_host_keys"
)S3 Backend
Stores files in Amazon S3 or S3-compatible storage such as MinIO. The built-in S3 backend now uses range reads, paginated delimiter-based directory listings, and multipart streaming writes for better large-file performance.
The S3 backend is optional. Core users can depend on :sftpd without ExAws.
Applications that use Sftpd.Backends.S3 must add the S3 dependency set:
def deps do
[
{:sftpd, "~> 0.1.1"},
{:ex_aws, "~> 2.0"},
{:ex_aws_s3, "~> 2.0"},
{:hackney, "~> 1.9"},
{:sweet_xml, "~> 0.7"},
{:jason, "~> 1.3"},
{:configparser_ex, "~> 4.0"}
]
end
Without those dependencies, Sftpd.Backends.S3.init/1 returns
{:error, :missing_s3_dependency}.
The same dependency set is documented in Getting Started and Backends; those guides also cover when to choose S3 instead of Memory or a custom backend.
Sftpd.start_server(
port: 2222,
backend: Sftpd.Backends.S3,
backend_opts: [bucket: "my-bucket", prefix: "tenant-a/"],
auth: {:passwords, [{"user", "pass"}]},
system_dir: "/path/to/ssh_host_keys"
)backend_opts supports:
:bucket- required S3 bucket name:prefix- optional static key prefix, or{:session, key}to read a prefix from the authenticated session map:aws_client- optional ExAws-compatible client module, mainly useful for tests or custom request adapters
For Phoenix apps, use Sftpd.child_spec/1 and an auth module:
children = [
{Sftpd,
port: 2222,
system_dir: "/run/secrets/sftp_host_keys",
auth: {MyApp.SftpAuth, []},
backend: Sftpd.Backends.S3,
backend_opts: [bucket: "uploads", prefix: {:session, :sftp_prefix}]}
]
Your auth callbacks return a session map such as %{user_id: user.id, tenant_id: user.tenant_id, sftp_prefix: "tenants/#{user.tenant_id}/"}. Backend
callbacks receive that map, and the built-in S3 backend can use it to scope
object keys per tenant.
Configure ExAws for your S3 endpoint:
# config/config.exs
config :ex_aws,
access_key_id: "your-key",
secret_access_key: "your-secret",
region: "us-east-1"
# For MinIO
config :ex_aws, :s3,
scheme: "http://",
host: "localhost",
port: 9000Optional Streaming Backend Callbacks
Custom module backends can implement optional callbacks for efficient large-file transfers:
# read_file_range(path, offset, len, state) -> {:ok, binary} | :eof | {:error, reason}
# begin_write(path, state) -> {:ok, writer_handle} | {:error, reason}
# write_chunk(writer_handle, offset, chunk, state) -> {:ok, writer_handle} | {:error, reason}
# finish_write(writer_handle, state) -> :ok | {:error, reason}
# abort_write(writer_handle, state) -> :ok
These callbacks let Sftpd.IODevice avoid loading whole files into memory on
open and reduce write-side buffering. See Sftpd.Backend for the exact
callback contracts.
Note that OTP's built-in :ssh_sftpd implementation always reports success for
close operations, even if final close-time flushing fails. Write errors are
therefore surfaced during active writes whenever possible, while close-only
failures are logged server-side.
If you need to bound how long file opens or close-time finalization can block a
session, pass open_timeout: timeout_in_ms or close_timeout: timeout_in_ms to
Sftpd.start_server/1. Both default to 30_000.
Telemetry
Sftpd emits :telemetry events for server lifecycle and SFTP operations.
The package depends on :telemetry directly, so applications can attach
handlers without adding another dependency.
:telemetry.attach(
"sftpd-read-logger",
[:sftpd, :sftp, :read],
fn _event, measurements, metadata, _config ->
Logger.info(
"sftp read io_device=#{inspect(metadata.io_device)} bytes=#{measurements.bytes} result=#{metadata.result}"
)
end,
nil
)
See the full telemetry event reference in Telemetry or
Sftpd.Telemetry.
Custom Backends
Implement the Sftpd.Backend behaviour to create custom storage backends.
See Backends for backend overview and
Custom Backends for a full authoring guide.
SSH Host Keys
Generate SSH host keys for your server:
mkdir -p ssh_keys
ssh-keygen -t rsa -f ssh_keys/ssh_host_rsa_key -N ""
ssh-keygen -t ecdsa -f ssh_keys/ssh_host_ecdsa_key -N ""
ssh-keygen -t ed25519 -f ssh_keys/ssh_host_ed25519_key -N ""
Then pass the directory to system_dir:
Sftpd.start_server(
# ...
system_dir: "ssh_keys"
)Documentation
Full documentation is available at HexDocs.
Erlang/OTP 29 Note
OTP 29 no longer enables SFTP implicitly for SSH daemons and also disables
shell and exec services by default. Sftpd.start_server/1 already passes the
required SFTP subsystem configuration, so applications using this package do
not need to configure OTP SSH subsystems themselves.
License
Apache 2.0