siftp

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

AreaControl
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
Passwordsargon2id 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
SSHEd25519/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
TLSrustls, TLS 1.2+, optional require_tls, CCC refused, optional enforced session reuse on FTPS data channels, certificate hot reload
FTP dataPassive connections must come from the client's address; active mode only to the client, port ≥ 1024
AbuseLogin 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 APILoopback 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
WebDAVRead-only; files served with nosniff and Content-Security-Policy: sandbox
StorageUser store written atomically with mode 0600; uploads to S3 are published only on success
AuditHash-chained JSON lines; siftpd audit-verify detects edits, insertions, deletions
SecretsPasswords, 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:

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.