# siftp

An FTP, FTPS, and SFTP server in Rust. One daemon, one config file,
the same users and permissions on every protocol.

```sh
siftpd hash-password 'a strong password'      # argon2id hash for the config
siftpd --check-config --config siftp.toml     # validate; errors are JSON
siftpd --config siftp.toml                    # serve
curl -T report.pdf ftp://alice:'a strong password'@127.0.0.1:2121/
```

[Quickstart](/docs/quickstart.html) · [Download](/download.html) ·
[Specification](/spec.html)

Built by [SiteVue](https://sitevue.ai).

## Install

Debian 12 or later and Ubuntu 22.04 or later, amd64 and arm64
(including AWS Graviton):

```sh
sudo curl -fsSLo /etc/apt/keyrings/siftp.gpg https://apt.siftp.org/siftp-archive-keyring.gpg
echo "deb [signed-by=/etc/apt/keyrings/siftp.gpg] https://apt.siftp.org stable main" | sudo tee /etc/apt/sources.list.d/siftp.list
sudo apt update && sudo apt install siftpd
```

Key fingerprint, release files, and other platforms:
[Download](/download.html).

## What it does

| Area | Details |
| --- | --- |
| Protocols | FTP (RFC 959, 2428, 3659, 7151), FTPS explicit and implicit (TLS 1.2/1.3), SSH/SFTP v3 with OpenSSH extensions, read-only WebDAV |
| Storage | Local directories with a symlink-proof jail, or S3-compatible object storage, behind the same interface; optional tiering keeps every file in S3 and only what is in use on local disk |
| Access | argon2id passwords, SSH keys with expiry and revocation, groups, per-directory permissions, quotas, rate limits, login bans, LDAP |
| Data path | `sendfile` and `splice` on Linux, optional io_uring, a bounded buffer pool for backpressure, assembly CRC32C and scanning kernels |
| Operations | Durable user store, SIGHUP reload, graceful drain, `/healthz` and `/readyz`, Prometheus metrics, hash-chained audit log, systemd unit, container image, .deb |
| Automation | REST API with OpenAPI 3.1, MCP tools, a capability document, and ten stable error codes shared by every protocol |

## Measured

Against vsftpd 3.0.5, ProFTPD 1.3.8c, and OpenSSH 10.0p2 on one
bare-metal Linux host (loopback, tmpfs, median of 5 runs):

| | siftp | vsftpd | ProFTPD | OpenSSH |
| --- | ---: | ---: | ---: | ---: |
| FTPS download, MiB/s | 2357 | 1040 | 1391 | – |
| FTPS upload, MiB/s | 1752 | 683 | 1520 | – |
| SFTP download, MiB/s | 954 | – | – | 763 |
| SFTP upload, MiB/s | 900 | – | – | 811 |
| Memory with 200 FTP sessions, MiB | 17.1 | 65.5 | 174.0 | – |
| First FTP login, 32 clients, logins/s | 142 | 393 | 106 | – |
| Repeat FTP login, 32 clients, logins/s | 97173 | 393 | 106 | – |

Plaintext FTP is the same for all three FTP servers: each uses
`sendfile`, and the client or the host is the limit. A first login in
siftp runs argon2id, which is deliberately expensive; a repeat login by
the same user is served from a keyed-digest cache without recomputing
the hash.

Method, full table, and how to reproduce it: [Performance](/docs/performance.html).

## Errors are data

Every failure carries one of ten stable codes. The admin API, MCP, and
WebDAV return the whole object: code, message, hint, and a link to the
docs. FTP replies and SFTP status messages start with the code,
followed by the message.

```text
550 permission_denied: permission denied: write is not allowed for this user
```

```json
{"code":"permission_denied","message":"permission denied: write is not allowed for this user","hint":"grant the 'w' permission bit or choose a different operation","docs_url":"https://siftp.org/docs/errors#permission_denied"}
```

Ten codes, append-only: [Error codes](/docs/errors.html).

## For agents

A running server describes itself at `GET /.well-known/siftp.json` on
its admin port. Start there, then read [`/llms.txt`](/llms.txt) and the
OpenAPI document it points to. [Agents and MCP](/docs/llm.html) has the
full sequence.

## License

MIT or Apache-2.0, at your option.
