This guide shows the intended Phoenix integration path: supervise Sftpd,
authenticate through your app, and use the auth session map to scope backend
storage.
It covers:
- supervised startup and shutdown
- runtime configuration for deploy-specific values
- local static auth for development
- password and public-key auth backed by your app
- tenant-scoped S3 storage using session context
- deployment notes for host keys and port exposure
Supervision
Add Sftpd to your application supervisor:
children = [
{Sftpd,
port: Application.fetch_env!(:my_app, :sftp_port),
system_dir: Application.fetch_env!(:my_app, :sftp_system_dir),
auth: {MyApp.SftpAuth, []},
backend: Sftpd.Backends.S3,
backend_opts: [
bucket: Application.fetch_env!(:my_app, :sftp_bucket),
prefix: {:session, :sftp_prefix}
]}
]Sftpd.child_spec/1 owns the SSH daemon lifecycle and stops it when your
supervisor stops.
Runtime Config
Use runtime config for deploy-specific values:
# config/runtime.exs
config :my_app,
sftp_port: String.to_integer(System.get_env("SFTP_PORT", "2222")),
sftp_system_dir: System.fetch_env!("SFTP_SYSTEM_DIR"),
sftp_bucket: System.fetch_env!("SFTP_BUCKET")system_dir must point at persistent SSH host keys. Do not generate new host
keys on every deploy unless clients are prepared for host-key rotation.
Local Static Auth
For local development without app auth:
{Sftpd,
port: 2222,
system_dir: "priv/sftp_host_keys",
auth: {:passwords, [{"dev", "dev"}]},
backend: Sftpd.Backends.Memory,
backend_opts: []}Password Auth
Implement Sftpd.Auth in your app. The returned session map is opaque to
Sftpd except where built-in backends document specific keys.
defmodule MyApp.SftpAuth do
@behaviour Sftpd.Auth
alias MyApp.Accounts
@impl true
def authenticate_password(username, password, _peer, _opts) do
with {:ok, user} <- Accounts.authenticate_sftp_user(username, password) do
{:ok,
%{
user_id: user.id,
tenant_id: user.tenant_id,
sftp_prefix: "tenants/#{user.tenant_id}/"
}}
else
_ -> :error
end
end
@impl true
def authorize_public_key(username, public_key, _opts) do
fingerprint = Sftpd.Auth.fingerprint(public_key)
with {:ok, user} <- Accounts.get_sftp_user_by_key(username, fingerprint) do
{:ok,
%{
user_id: user.id,
tenant_id: user.tenant_id,
sftp_prefix: "tenants/#{user.tenant_id}/"
}}
else
_ -> :error
end
end
endPublic Keys
Store public-key fingerprints in your database:
{:ok, public_key} = Sftpd.Auth.decode_authorized_key(authorized_key_line)
fingerprint = Sftpd.Auth.fingerprint(public_key)The fingerprint is suitable for lookup during authorize_public_key/3.
Tenant-Scoped S3
Use prefix: {:session, :sftp_prefix} to scope every S3 object key by the
authenticated session:
backend_opts: [
bucket: "uploads",
prefix: {:session, :sftp_prefix}
]If auth returns %{sftp_prefix: "tenants/123/"}, an upload to /invoice.pdf
is stored as tenants/123/invoice.pdf.
Deployment Notes
Expose the configured TCP port through your load balancer or host firewall. Persist the SSH host key directory across deploys. Keep application passwords, public-key fingerprints, S3 bucket names, and endpoint credentials in your normal runtime secret/config path.
Next Steps
- Choose production storage in Backends.
- Add metrics and logging with Telemetry.
- Implement non-S3 storage with Custom Backends.