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.
| Step | Rule |
|---|
| File | --config <path>, else ./siftp.toml, else configs/siftp.toml |
| Unknown keys | An 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_LOG | Overrides [logging].level |
| Validation | siftpd --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.
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.
| Key | Default | Meaning |
|---|
root | /srv/siftp | Parent of default user roots (<root>/<name>). s3://bucket/prefix puts them in S3 |
max_connections | 512 | Open connections across all protocols; more are refused with rate_limited |
max_connections_per_ip | 32 | The same, per client address |
idle_timeout_secs | 300 | Idle control or data connections are closed after this long |
max_failed_logins | 5 | Failed logins from one address within ban_window_secs that ban it; 0 turns bans off |
ban_window_secs | 600 | Window for counting failures |
ban_duration_secs | 900 | How long a ban lasts |
shutdown_grace_secs | 30 | How long SIGTERM waits for transfers before closing them |
user_store | /var/lib/siftp/users.toml | Where API- and MCP-created users are kept. Empty keeps them in memory only |
revoked_keys_file | empty | SSH keys refused for every user: OpenSSH lines or SHA256: fingerprints, one per line |
password_max_age_days | 0 | Passwords older than this stop working; 0 means never |
login_cache_ttl_secs | 300 | How 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_checks | 0 | argon2id checks running at once; others wait. Each uses the hash's memory (64 MiB by default) while it runs. 0 means one per CPU |
| Key | Default | Meaning |
|---|
bind | 0.0.0.0:21 | FTP listener; explicit FTPS (AUTH TLS) uses it too |
passive_port_range | [40000, 40100] | Ports for passive data connections. Environment form: 40000-40100 |
passive_external_ip | empty | Address sent in PASV replies behind NAT; empty sends the local address |
passive_promiscuous | false | Accept data connections from any address. Off: only from the control connection's address |
allow_anonymous | false | Allow anonymous and ftp logins, read-only (rl), in anonymous_root |
anonymous_root | /srv/siftp/anonymous | Root for anonymous logins |
greeting | siftp ready | Text of the 220 greeting |
require_tls | false | Refuse login and data transfers until AUTH TLS (tls_required) |
virtual_hosts | [] | Names accepted by HOST; empty accepts any name |
| Key | Default | Meaning |
|---|
cert_file | /etc/siftp/tls.crt | PEM 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.key | PEM private key |
implicit_bind | 0.0.0.0:990 | Implicit FTPS listener |
min_tls_version | 1.2 | 1.2 or 1.3 |
require_session_reuse | false | Refuse TLS data connections that do not resume the control connection's TLS session |
The certificate and key are re-read on reload.
| Key | Default | Meaning |
|---|
bind | 0.0.0.0:22 | SSH listener |
host_key_file | /etc/siftp/ssh_host_ed25519_key | OpenSSH-format private key. Missing: an Ed25519 key is generated (mode 0600). ECDSA works; RSA is refused |
ciphers | empty | SSH 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 |
| Key | Default | Meaning |
|---|
bind | 127.0.0.1:8080 | Admin HTTP: the API, /metrics, /mcp, health checks, capability document |
token | empty | Bearer token. Empty refuses every request except the public bootstrap routes |
| Key | Default | Meaning |
|---|
transport | http | http serves MCP at POST /mcp on the admin listener. stdio turns that off; siftpd mcp-stdio works either way |
| Key | Default | Meaning |
|---|
level | info | error, warn, info, debug, or trace. RUST_LOG overrides it |
format | text | text or json, written to stderr |
audit_file | /var/log/siftp/audit.jsonl | Hash-chained audit log (Operations) |
| Key | Default | Meaning |
|---|
sendfile | true | Linux downloads without a user-space copy |
splice | true | Linux uploads without a user-space copy |
io_uring | false | io_uring data path; needs a build with the io-uring feature |
buffer_pool_bytes | 67108864 | Memory for all pooled (TLS, S3, non-Linux) transfers together |
transfer_chunk_bytes | 262144 | Size of one pooled chunk |
| Key | Default | Meaning |
|---|
endpoint | empty | S3 endpoint URL. Empty disables s3:// roots |
region | us-east-1 | Signing region |
access_key_id | empty | Access key |
secret_access_key | empty | Secret key; use ${ENV:NAME} |
path_style | true | endpoint/bucket/key URLs; most self-hosted S3 needs this |
spool_dir | /var/lib/siftp/spool | Uploads are staged here until the transfer completes. Created when an s3:// root is first used; never touched otherwise |
part_size_bytes | 16777216 | Multipart upload part size |
timeout_secs | 30 | Per-request timeout |
credentials | static | static: 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.
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.
| Key | Default | Meaning |
|---|
remote | empty | s3://bucket/prefix for the copies, using the [s3] connection settings. Empty disables tiering. Read at startup |
storage_class | INTELLIGENT_TIERING | S3 class of the copies: STANDARD, STANDARD_IA, ONEZONE_IA, INTELLIGENT_TIERING, or GLACIER_IR (classes that serve reads immediately) |
upload_after_secs | 60 | Upload a file once it has not changed for this long |
evict_after_days | 30 | Replace the local copy of a file not read or written for this many days |
min_evict_bytes | 1048576 | Smaller files always stay local |
high_watermark_percent | 80 | Above this disk usage, evict least recently used files first ... |
low_watermark_percent | 70 | ... until usage is below this |
scan_interval_secs | 300 | Time between scans of [server].root (minimum 10) |
The policy keys apply on reload. Details: Storage.
| Key | Default | Meaning |
|---|
url | empty | ldap:// or ldaps:// URL. Empty disables LDAP |
starttls | false | Upgrade an ldap:// connection with StartTLS |
ca_file | empty | PEM CA bundle for the LDAP server's certificate |
bind_dn | empty | Service account for search-then-bind |
bind_password | empty | Its password; use ${ENV:NAME} |
base_dn | empty | Where to search for users |
user_filter | (uid={user}) | Search filter; {user} is the login name, escaped |
user_dn_template | empty | Direct bind instead of search, e.g. uid={user},ou=people,dc=example,dc=org |
group_attribute | memberOf | Attribute holding the user's group DNs |
group_map | {} | Table of group DN to [[groups]] name |
default_permissions | rl | Permissions for users in no mapped group |
require_group | false | Refuse users in no mapped group |
virtual_root_template | empty | Root for LDAP users with {user}; empty: <server.root>/<user> |
timeout_secs | 5 | Connect and operation timeout |
Details: Users and access.
| Key | Default | Meaning |
|---|
bind | empty | Read-only WebDAV gateway listener. Empty disables it |
tls | false | Serve HTTPS with the [ftps] certificate |
| Key | Meaning |
|---|
name | Group name, referenced from users' groups and [ldap.group_map] |
permissions | Bits r w d n m l |
directories | [[groups.directories]] entries with path and permissions |
[[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
| Key | Meaning |
|---|
name | 1-255 characters, not . or .., no whitespace, /, \, : |
password_hash | argon2id PHC string from siftpd hash-password; plaintext and weak parameters are rejected |
password_set_unix | When the password was set; used by password_max_age_days |
ssh_public_keys | OpenSSH lines, optional expiry-time="YYYYMMDD" prefix |
virtual_root | Empty: <root>/<name>. Relative: under [server].root. Absolute path or s3://bucket/prefix as written |
permissions | Bits r w d n m l; omitted: union of the user's groups, else rl |
groups, directories | See Users and access |
hosts | FTP HOST names this user may log in under |
max_sessions, quota_bytes, rate_limit_bps | 0 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.
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:
| Variable | Example |
|---|
SIFTP_ADMIN_TOKEN | a long random string |
SIFTP_FTP_BIND, SIFTP_SFTP_BIND, SIFTP_ADMIN_BIND | 0.0.0.0:2121 |
SIFTP_FTP_PASSIVE_PORT_RANGE | 40000-40100 |
SIFTP_FTP_PASSIVE_EXTERNAL_IP | 203.0.113.10 |
SIFTP_FTP_VIRTUAL_HOSTS | files.example.com,backup.example.com |
SIFTP_LOGGING_FORMAT | json |
SIFTP_PERFORMANCE_IO_URING | true |
SIFTP_S3_SECRET_ACCESS_KEY, SIFTP_LDAP_BIND_PASSWORD | secrets |
Booleans accept true, false, 1, 0. Lists take comma-separated values.