siftp

Configuration

One TOML file. Every key and its default is under Reference. The package installs a commented example as /usr/share/doc/siftpd/siftp.example.toml.

Loading

StepRule
File--config <path>, else ./siftp.toml, else configs/siftp.toml
Unknown keysAn error, never ignored
${ENV:NAME}Replaced inside any string value (tokens, LDAP and S3 secrets, hashes)
SIFTP_<SECTION>_<KEY>Overrides the file, e.g. SIFTP_FTP_BIND=0.0.0.0:2121
RUST_LOGOverrides [logging].level
Validationsiftpd --check-config prints a JSON summary with warnings, or one JSON error naming the field

Reload with kill -HUP or POST /api/v1/reload: config, users, the revoked-key list, and the FTPS certificate are re-read and the audit file is reopened. Live sessions keep their permissions. Changing a bind address needs a restart.

Reference

Every key, with its default. A section you leave out takes all its defaults. configs/siftp.example.toml in the source tree is the same reference as a commented file.

[server]

KeyDefaultMeaning
root/srv/siftpParent of default user roots (<root>/<name>). s3://bucket/prefix puts them in S3
max_connections512Open connections across all protocols; more are refused with rate_limited
max_connections_per_ip32The same, per client address
idle_timeout_secs300Idle control or data connections are closed after this long
max_failed_logins5Failed logins from one address within ban_window_secs that ban it; 0 turns bans off
ban_window_secs600Window for counting failures
ban_duration_secs900How long a ban lasts
shutdown_grace_secs30How long SIGTERM waits for transfers before closing them
user_store/var/lib/siftp/users.tomlWhere API- and MCP-created users are kept. Empty keeps them in memory only
revoked_keys_fileemptySSH keys refused for every user: OpenSSH lines or SHA256: fingerprints, one per line
password_max_age_days0Passwords older than this stop working; 0 means never
login_cache_ttl_secs300How long a password that argon2id accepted is remembered, so the same user logging in again skips the hash. The server keeps a keyed digest under a key that exists only in process memory, never the password. Wrong passwords always pay the full hash. 0 turns this off
max_concurrent_password_checks0argon2id checks running at once; others wait. Each uses the hash's memory (64 MiB by default) while it runs. 0 means one per CPU

[ftp]

KeyDefaultMeaning
bind0.0.0.0:21FTP listener; explicit FTPS (AUTH TLS) uses it too
passive_port_range[40000, 40100]Ports for passive data connections. Environment form: 40000-40100
passive_external_ipemptyAddress sent in PASV replies behind NAT; empty sends the local address
passive_promiscuousfalseAccept data connections from any address. Off: only from the control connection's address
allow_anonymousfalseAllow anonymous and ftp logins, read-only (rl), in anonymous_root
anonymous_root/srv/siftp/anonymousRoot for anonymous logins
greetingsiftp readyText of the 220 greeting
require_tlsfalseRefuse login and data transfers until AUTH TLS (tls_required)
virtual_hosts[]Names accepted by HOST; empty accepts any name

[ftps]

KeyDefaultMeaning
cert_file/etc/siftp/tls.crtPEM certificate chain. If it or the key is missing, FTPS is off: AUTH TLS is refused, the implicit listener does not start, and --check-config warns
key_file/etc/siftp/tls.keyPEM private key
implicit_bind0.0.0.0:990Implicit FTPS listener
min_tls_version1.21.2 or 1.3
require_session_reusefalseRefuse TLS data connections that do not resume the control connection's TLS session

The certificate and key are re-read on reload.

[sftp]

KeyDefaultMeaning
bind0.0.0.0:22SSH listener
host_key_file/etc/siftp/ssh_host_ed25519_keyOpenSSH-format private key. Missing: an Ed25519 key is generated (mode 0600). ECDSA works; RSA is refused
ciphersemptySSH ciphers to offer, in order: any of aes256-gcm@openssh.com, aes128-gcm@openssh.com, chacha20-poly1305@openssh.com. Empty: the two AES-GCM ciphers when the CPU has AES instructions, otherwise chacha20-poly1305@openssh.com first and then AES-GCM. Read at startup

[admin]

KeyDefaultMeaning
bind127.0.0.1:8080Admin HTTP: the API, /metrics, /mcp, health checks, capability document
tokenemptyBearer token. Empty refuses every request except the public bootstrap routes

[mcp]

KeyDefaultMeaning
transporthttphttp serves MCP at POST /mcp on the admin listener. stdio turns that off; siftpd mcp-stdio works either way

[logging]

KeyDefaultMeaning
levelinfoerror, warn, info, debug, or trace. RUST_LOG overrides it
formattexttext or json, written to stderr
audit_file/var/log/siftp/audit.jsonlHash-chained audit log (Operations)

[performance]

KeyDefaultMeaning
sendfiletrueLinux downloads without a user-space copy
splicetrueLinux uploads without a user-space copy
io_uringfalseio_uring data path; needs a build with the io-uring feature
buffer_pool_bytes67108864Memory for all pooled (TLS, S3, non-Linux) transfers together
transfer_chunk_bytes262144Size of one pooled chunk

[s3]

KeyDefaultMeaning
endpointemptyS3 endpoint URL. Empty disables s3:// roots
regionus-east-1Signing region
access_key_idemptyAccess key
secret_access_keyemptySecret key; use ${ENV:NAME}
path_styletrueendpoint/bucket/key URLs; most self-hosted S3 needs this
spool_dir/var/lib/siftp/spoolUploads are staged here until the transfer completes. Created when an s3:// root is first used; never touched otherwise
part_size_bytes16777216Multipart upload part size
timeout_secs30Per-request timeout
credentialsstaticstatic: the keys above (unsigned requests when empty). env: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN. instance: the EC2 instance role through IMDSv2, refreshed before it expires

Details: Storage.

[tier]

Tiered storage for local roots: every file under [server].root is copied to object storage, and local copies that are not being used are replaced by placeholders that are fetched back on the next read.

KeyDefaultMeaning
remoteemptys3://bucket/prefix for the copies, using the [s3] connection settings. Empty disables tiering. Read at startup
storage_classINTELLIGENT_TIERINGS3 class of the copies: STANDARD, STANDARD_IA, ONEZONE_IA, INTELLIGENT_TIERING, or GLACIER_IR (classes that serve reads immediately)
upload_after_secs60Upload a file once it has not changed for this long
evict_after_days30Replace the local copy of a file not read or written for this many days
min_evict_bytes1048576Smaller files always stay local
high_watermark_percent80Above this disk usage, evict least recently used files first ...
low_watermark_percent70... until usage is below this
scan_interval_secs300Time between scans of [server].root (minimum 10)

The policy keys apply on reload. Details: Storage.

[ldap]

KeyDefaultMeaning
urlemptyldap:// or ldaps:// URL. Empty disables LDAP
starttlsfalseUpgrade an ldap:// connection with StartTLS
ca_fileemptyPEM CA bundle for the LDAP server's certificate
bind_dnemptyService account for search-then-bind
bind_passwordemptyIts password; use ${ENV:NAME}
base_dnemptyWhere to search for users
user_filter(uid={user})Search filter; {user} is the login name, escaped
user_dn_templateemptyDirect bind instead of search, e.g. uid={user},ou=people,dc=example,dc=org
group_attributememberOfAttribute holding the user's group DNs
group_map{}Table of group DN to [[groups]] name
default_permissionsrlPermissions for users in no mapped group
require_groupfalseRefuse users in no mapped group
virtual_root_templateemptyRoot for LDAP users with {user}; empty: <server.root>/<user>
timeout_secs5Connect and operation timeout

Details: Users and access.

[webdav]

KeyDefaultMeaning
bindemptyRead-only WebDAV gateway listener. Empty disables it
tlsfalseServe HTTPS with the [ftps] certificate

[[groups]]

KeyMeaning
nameGroup name, referenced from users' groups and [ldap.group_map]
permissionsBits r w d n m l
directories[[groups.directories]] entries with path and permissions

Users

[[users]]
name = "backup"
ssh_public_keys = ["ssh-ed25519 AAAAC3... backup@host"]
virtual_root = "backup"          # relative: under [server].root
permissions = "rwl"
quota_bytes = 107374182400
rate_limit_bps = 50000000
max_sessions = 4
KeyMeaning
name1-255 characters, not . or .., no whitespace, /, \, :
password_hashargon2id PHC string from siftpd hash-password; plaintext and weak parameters are rejected
password_set_unixWhen the password was set; used by password_max_age_days
ssh_public_keysOpenSSH lines, optional expiry-time="YYYYMMDD" prefix
virtual_rootEmpty: <root>/<name>. Relative: under [server].root. Absolute path or s3://bucket/prefix as written
permissionsBits r w d n m l; omitted: union of the user's groups, else rl
groups, directoriesSee Users and access
hostsFTP HOST names this user may log in under
max_sessions, quota_bytes, rate_limit_bps0 means unlimited

Users created with the admin API or MCP go to [server].user_store (default /var/lib/siftp/users.toml) and survive restarts. Users in the config file are read-only through the API; a store user with the same name is ignored and reported by --check-config.

Environment overrides

Every key in the tables above, except group_map and the [[groups]] and [[users]] arrays, has a variable named SIFTP_<SECTION>_<KEY> in upper case:

VariableExample
SIFTP_ADMIN_TOKENa long random string
SIFTP_FTP_BIND, SIFTP_SFTP_BIND, SIFTP_ADMIN_BIND0.0.0.0:2121
SIFTP_FTP_PASSIVE_PORT_RANGE40000-40100
SIFTP_FTP_PASSIVE_EXTERNAL_IP203.0.113.10
SIFTP_FTP_VIRTUAL_HOSTSfiles.example.com,backup.example.com
SIFTP_LOGGING_FORMATjson
SIFTP_PERFORMANCE_IO_URINGtrue
SIFTP_S3_SECRET_ACCESS_KEY, SIFTP_LDAP_BIND_PASSWORDsecrets

Booleans accept true, false, 1, 0. Lists take comma-separated values.