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

```text
# Caddyfile
admin.example.com {
    reverse_proxy 127.0.0.1:8080
}
```

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