# 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`.
```elixir
def deps do
[
{:sftpd, "~> 0.1.1"}
]
end
```
## Quick Start
```elixir
# 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@localhost
```
## Guides
- [Getting Started](GETTING_STARTED.md) for a step-by-step setup guide
- [Phoenix Setup](PHOENIX.md) for supervised Phoenix setup with app auth and S3
- [Backends](BACKENDS.md) for backend architecture and built-in backend tradeoffs
- [Custom Backends](CUSTOM_BACKENDS.md) for implementing your own backend
- [Telemetry](TELEMETRY.md) for emitted events, metadata, and examples
## Key Concepts
- `Sftpd.start_server/1` starts an SSH daemon configured with an SFTP
file-handler
- `Sftpd.child_spec/1` lets Phoenix and other OTP apps supervise the server
- `Sftpd.Auth` defines password and public-key auth callbacks
- `Sftpd.Backend` defines the storage contract
- `Sftpd.Backends.Memory` is the fastest local setup path
- `Sftpd.Backends.S3` is the built-in persistent backend
- `Sftpd.Telemetry` documents 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](BACKENDS.md) for backend tradeoffs and
[Custom Backends](CUSTOM_BACKENDS.md) for folder, GenServer, supervision, and
post-write processing examples.
## Next Steps
- Adding SFTP to a Phoenix app? Start with [Phoenix Setup](PHOENIX.md).
- Choosing storage? Read [Backends](BACKENDS.md).
- Implementing your own storage layer? Read [Custom Backends](CUSTOM_BACKENDS.md).
- Wiring observability? Read [Telemetry](TELEMETRY.md).
## Backends
### Memory Backend
Stores files in memory. Useful for development and testing without external dependencies.
```elixir
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:
```elixir
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](GETTING_STARTED.md)
and [Backends](BACKENDS.md); those guides also cover when to choose S3 instead
of Memory or a custom backend.
```elixir
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:
```elixir
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:
```elixir
# 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: 9000
```
### Optional Streaming Backend Callbacks
Custom module backends can implement optional callbacks for efficient large-file
transfers:
```elixir
# 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.
```elixir
: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](TELEMETRY.md) or
`Sftpd.Telemetry`.
### Custom Backends
Implement the `Sftpd.Backend` behaviour to create custom storage backends.
See [Backends](BACKENDS.md) for backend overview and
[Custom Backends](CUSTOM_BACKENDS.md) for a full authoring guide.
## SSH Host Keys
Generate SSH host keys for your server:
```bash
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`:
```elixir
Sftpd.start_server(
# ...
system_dir: "ssh_keys"
)
```
## Documentation
Full documentation is available at
[HexDocs](https://hexdocs.pm/sftpd).
## 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