Security
Model
siftp assumes clients are untrusted, the network is hostile, and the operator controls the host and config. Every client path is resolved inside the user's root and permission-checked before any filesystem or object-storage call. The daemon runs as an unprivileged user; the systemd unit grants only CAP_NET_BIND_SERVICE.
Controls
| Area | Control |
|---|---|
| Paths | .. past the root, NUL bytes, and encoded variants refused on every protocol; symlinks followed only inside the root, and created only with targets inside it; directory rules re-checked against symlink targets |
| Passwords | argon2id only (m≥19 MiB, t≥2), constant-time comparison, dummy verification for unknown users, identical failures for every cause, optional maximum age. Repeat logins within login_cache_ttl_secs are served from a cache of keyed digests (process-only key, never the password); wrong passwords always pay the full hash |
| SSH | Ed25519/ECDSA host keys (RSA refused, see below), post-quantum hybrid kex mlkem768x25519-sha256, strict KEX (Terrapin), AEAD ciphers only, key expiry and revocation, no shell/exec/forwarding |
| TLS | rustls, TLS 1.2+, optional require_tls, CCC refused, optional enforced session reuse on FTPS data channels, certificate hot reload |
| FTP data | Passive connections must come from the client's address; active mode only to the client, port ≥ 1024 |
| Abuse | Login bans across all listeners and the admin API, global and per-IP connection caps, per-user session caps, command/packet/handle limits, idle and handshake timeouts, rate limits on every data path |
| Admin API | Loopback by default; bearer token compared in constant time, fail-closed when unset; 1 MiB bodies, 64 concurrent requests, 30 s per request; nosniff, no-store, DENY framing |
| WebDAV | Read-only; files served with nosniff and Content-Security-Policy: sandbox |
| Storage | User store written atomically with mode 0600; uploads to S3 are published only on success |
| Audit | Hash-chained JSON lines; siftpd audit-verify detects edits, insertions, deletions |
| Secrets | Passwords, keys, tokens, and file contents never appear in logs, errors, or API responses |
| Code | #![forbid(unsafe_code)] everywhere except the assembly FFI shims and one documented io_uring submission helper |
Deploying the admin API
The admin listener speaks plain HTTP and defaults to 127.0.0.1:8080. To reach it remotely, keep it on loopback and put a TLS proxy in front:
# Caddyfile
admin.example.com {
reverse_proxy 127.0.0.1:8080
}
server {
listen 443 ssl;
server_name admin.example.com;
ssl_certificate /etc/ssl/admin.crt;
ssl_certificate_key /etc/ssl/admin.key;
location / { proxy_pass http://127.0.0.1:8080; }
}
siftpd --check-config and startup both warn when the admin bind is not loopback. Use a token of at least 32 random bytes.
Dependencies
cargo deny (advisories, licenses, banned crates — no OpenSSL — and sources) and cargo audit run on every change. One advisory is accepted with a reason:
- RUSTSEC-2023-0071 (
rsa, timing side channel on private-key operations). siftp performs no RSA private-key operations: RSA host keys are refused at startup, and client RSA keys are used only to verify signatures.
Fuzzing
cargo-fuzz targets cover FTP command lines, SFTP packets and ATTRS, the config loader and key parser, and path/Range handling (fuzz/). CI runs each against its seed corpus; cargo test replays the corpus plus thousands of deterministic mutations on every build.
Reporting a vulnerability
Report privately through the project repository's security advisory flow, with steps to reproduce against a local instance. Please do not open a public issue first. Expect an acknowledgement, a fix or mitigation plan, and credit in the release notes unless you prefer otherwise.