# Configuration

One TOML file. Every key and its default is under
[Reference](#reference). The package installs a commented example as
`/usr/share/doc/siftpd/siftp.example.toml`.

## Loading

| 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.

## 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]`

| 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 |

### `[ftp]`

| 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 |

### `[ftps]`

| 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.

### `[sftp]`

| 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 |

### `[admin]`

| 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 |

### `[mcp]`

| 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 |

### `[logging]`

| 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](/docs/operations.html#audit-log)) |

### `[performance]`

| 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 |

### `[s3]`

| 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](/docs/storage.html).

### `[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.

| 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](/docs/storage.html#tiered-storage).

### `[ldap]`

| 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](/docs/identity.html#ldap).

### `[webdav]`

| Key | Default | Meaning |
| --- | --- | --- |
| `bind` | empty | Read-only WebDAV gateway listener. Empty disables it |
| `tls` | `false` | Serve HTTPS with the `[ftps]` certificate |

### `[[groups]]`

| 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

```toml
[[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](/docs/identity.html) |
| `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`.

## 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:

| 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.
