Operations
Install
Debian or Ubuntu package
From the apt repository (Download adds it), or from a downloaded package:
sudo apt install siftpd # from apt.siftp.org
sudo apt install ./siftpd_0.2.1-1_amd64.deb # or _arm64
sudo systemctl status siftpd
The package creates the siftp system user, /srv/siftp, /var/lib/siftp, /var/log/siftp, a random admin token in /etc/siftp/siftpd.env, and enables the hardened unit. The default config (/etc/siftp/siftp.toml) serves FTP on 21, implicit FTPS on 990 (once a certificate exists), SFTP on 2222 (so the host's OpenSSH keeps 22), and the admin API on 127.0.0.1:8080.
sudo -u siftp siftpd hash-password 'a strong password'
sudoedit /etc/siftp/siftp.toml # or create users through the API
sudo systemctl reload siftpd
Container
Release images are published to the project's GitHub Container Registry (ghcr.io/<owner>/siftp:<version>, multi-arch, signed). To build the same image locally:
docker build -f packaging/container/Dockerfile -t siftp:0.2.1 .
docker run -d --name siftp \
-e SIFTP_ADMIN_TOKEN="$(openssl rand -hex 32)" \
-e SIFTP_FTP_PASSIVE_EXTERNAL_IP=203.0.113.10 \
-p 2121:2121 -p 2222:2222 -p 127.0.0.1:8080:8080 -p 40000-40019:40000-40019 \
-v siftp-data:/srv/siftp -v siftp-state:/var/lib/siftp \
siftp:0.2.1
The image is distroless, runs as UID 65532, and has a HEALTHCHECK (siftpd health). Mount a certificate pair at /etc/siftp/tls/ to enable FTPS on 2990. Create users through the admin API:
curl -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"alice","password":"...","permissions":"rwdnml"}' \
http://127.0.0.1:8080/api/v1/users
Binary and systemd
sudo install -m 755 siftpd /usr/bin/siftpd
sudo install -m 644 packaging/systemd/siftpd.service /lib/systemd/system/
sudo useradd --system --home /var/lib/siftp --shell /usr/sbin/nologin siftp
sudo install -d -o siftp -g siftp -m 750 /srv/siftp
sudo install -d -o root -g siftp -m 750 /etc/siftp
sudo install -m 640 -g siftp packaging/siftp.toml /etc/siftp/siftp.toml
echo "SIFTP_ADMIN_TOKEN=$(openssl rand -hex 32)" | sudo install -m 640 -g siftp /dev/stdin /etc/siftp/siftpd.env
sudo systemctl daemon-reload && sudo systemctl enable --now siftpd
macOS: packaging/launchd/org.siftp.siftpd.plist (same signals; the pooled data path is used on macOS).
Verify a release
Every release asset has a Sigstore signature, certificate, and a build provenance attestation; SHA256SUMS covers all files.
sha256sum -c SHA256SUMS --ignore-missing
cosign verify-blob siftpd-v0.2.1-x86_64-linux.tar.gz \
--signature siftpd-v0.2.1-x86_64-linux.tar.gz.sig \
--certificate siftpd-v0.2.1-x86_64-linux.tar.gz.pem \
--certificate-identity https://github.com/sitevue/siftp/.github/workflows/release.yml@refs/tags/v0.2.1 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
gh attestation verify siftpd-v0.2.1-x86_64-linux.tar.gz --owner sitevue
cosign verify ghcr.io/sitevue/siftp:v0.2.1 \
--certificate-identity https://github.com/sitevue/siftp/.github/workflows/release.yml@refs/tags/v0.2.1 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
The identity is the release workflow in the sitevue/siftp repository at the tag you are verifying. A signature from any other repository, workflow, or tag fails the check.
Each release also ships a CycloneDX SBOM (*.cdx.json).
Service control
| Action | Command | Effect |
|---|---|---|
| Reload | systemctl reload siftpd (SIGHUP) | Re-reads config, user store, revoked keys, TLS certificate; reopens the audit log; sessions keep running |
| Stop | systemctl stop siftpd (SIGTERM) | Drain: stop accepting, 421 to idle sessions, transfers finish, exit 0 within shutdown_grace_secs (30 s) |
| Stop now | second SIGTERM | Skip the wait |
| Validate | siftpd --check-config --config /etc/siftp/siftp.toml | JSON summary with warnings, or the error |
Bind address changes need a restart. Everything else reloads.
Health and readiness
| Endpoint | Auth | 200 when | 503 when |
|---|---|---|---|
GET /healthz | none | process is up | never |
GET /readyz | none | all listeners bound, not draining | starting or draining |
siftpd health --config <file> checks /readyz and exits 0 or 1, for container probes. Point load balancers at /readyz so a draining node leaves rotation before it stops accepting.
Metrics and alerts
GET /metrics on the admin listener, with the admin token:
scrape_configs:
- job_name: siftp
authorization: { credentials_file: /etc/prometheus/siftp-token }
static_configs: [{ targets: ["127.0.0.1:8080"] }]
| Metric | Type |
|---|---|
siftp_bytes_sent_total, siftp_bytes_received_total | counter |
siftp_transfers_total{path="sendfile|splice|pool|uring"} | counter |
siftp_transfers_failed_total | counter |
siftp_logins_ok_total, siftp_logins_failed_total, siftp_logins_cached_total, siftp_bans_total | counter |
siftp_connections_total, siftp_connections_refused_total | counter |
siftp_tls_data_resumed_total, siftp_tls_data_full_handshake_total | counter |
siftp_active_sessions, siftp_active_connections, siftp_active_transfers | gauge |
siftp_banned_addresses, siftp_buffer_pool_outstanding_buffers | gauge |
siftp_ready, siftp_start_time_seconds, siftp_build_info | gauge |
Alert rules: packaging/prometheus/alerts.yml (down, not ready, login failure spikes, bans, refused connections, transfer failures, buffer pool exhaustion, connection count).
Logs
Logs go to stderr (journald under systemd). [logging].format = "json" gives one JSON object per line with the session span: peer, session, user, and host (after FTP HOST). Every transfer logs transfer complete or transfer failed with verb, path, bytes, data path, and milliseconds.
Audit log
[logging].audit_file holds one JSON line per login (success or failure), ban, transfer, file change, and admin change, with seq and a SHA-256 chain linking each line to the previous one. Lookups such as CWD, SIZE, LIST, and SFTP STAT are not logged. SPEC §8.5 lists every action value.
{"ts":1760320000,"user":"alice","action":"STOR","path":"/report.pdf","result":"ok","remote":"203.0.113.7:51522","seq":42,"chain":"9f2c…"}
siftpd audit-verify /var/log/siftp/audit.jsonl # exit 2 if altered
siftpd audit-verify audit.jsonl --anchor <last chain of the rotated file>
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8080/api/v1/audit/verify
Rotation: the package installs /etc/logrotate.d/siftpd (daily, 90 days, create, then systemctl reload). The chain continues into the new file. Ship lines off the host if you need to detect truncation of the newest lines.
SSH host key
siftpd generates an Ed25519 host key at [sftp].host_key_file on first start. Publish its fingerprint so SFTP clients can check it instead of trusting the first connection:
ssh-keygen -lf /var/lib/siftp/ssh_host_ed25519_key # SHA256:... to publish
ssh-keygen -yf /var/lib/siftp/ssh_host_ed25519_key # public key line
On a client, put the public key in known_hosts with the server's name and port, then connect with strict checking:
echo "[files.example.com]:2222 ssh-ed25519 AAAAC3..." >> ~/.ssh/known_hosts
sftp -o StrictHostKeyChecking=yes -P 2222 alice@files.example.com
curl --hostpubsha256 '<base64 part of the SHA256 fingerprint>' sftp://files.example.com:2222/
Back the key up with the user store. A new key makes every client warn until it updates known_hosts.
Backups
| Path | Contents |
|---|---|
/var/lib/siftp/users.toml | API-managed users (argon2id hashes) |
/var/lib/siftp/ssh_host_ed25519_key | SSH host key (clients pin it) |
/etc/siftp/ | Config, TLS certificate and key, admin token |
/var/log/siftp/audit.jsonl* | Audit trail |
/srv/siftp/ | User files |
The user store is replaced atomically on every change, so copying it at any time yields a complete file.
Tiered storage
Tiered storage needs a bucket that nothing else writes to, and a role or keys that can use it.
aws s3api create-bucket --bucket example-siftp-tier \
--create-bucket-configuration LocationConstraint=us-east-2
aws s3api put-public-access-block --bucket example-siftp-tier \
--public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
aws s3api put-bucket-versioning --bucket example-siftp-tier \
--versioning-configuration Status=Enabled
aws s3api put-bucket-lifecycle-configuration --bucket example-siftp-tier \
--lifecycle-configuration '{"Rules":[{"ID":"siftp","Status":"Enabled","Filter":{},
"NoncurrentVersionExpiration":{"NoncurrentDays":30},
"AbortIncompleteMultipartUpload":{"DaysAfterInitiation":7}}]}'
Versioning keeps an overwritten or deleted object for 30 days. The instance role (or the keys in [s3]) needs:
{"Version": "2012-10-17", "Statement": [
{"Effect": "Allow", "Action": ["s3:ListBucket"],
"Resource": "arn:aws:s3:::example-siftp-tier"},
{"Effect": "Allow", "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject",
"s3:AbortMultipartUpload"],
"Resource": "arn:aws:s3:::example-siftp-tier/*"}]}
- siftp writes
objects/(file copies),manifests/(the newest 48 lists of every file, directory, and link), androot-id. The data disk holds.siftp-tier/with the matchingroot-id, a lock, and temporary files. - If the two
root-ids differ, for example after the data disk was replaced or emptied, tiering turns read-only and logs an error. Stop siftpd and rebuild the root as placeholders:
sudo systemctl stop siftpd
sudo -u siftp siftpd tier-restore --config /etc/siftp/siftp.toml
sudo systemctl start siftpd
It prints a JSON report. Existing files are never replaced, and objects no manifest lists are placed under siftp-recovered/.
- Metrics:
siftp_tier_uploads_total,siftp_tier_evictions_total,siftp_tier_fetches_total(with_bytes_totalcompanions),siftp_tier_errors_total, and the gaugessiftp_tier_pending,siftp_tier_quarantined,siftp_tier_orphans. Objects no file refers to are counted, not deleted. - In a container, put
[server].rooton a volume: the overlay filesystem may lack extended attributes, and siftpd refuses to start tiering without them.
Upgrades and rollback
- Read the release notes; check the config with the new binary before switching:
siftpd-new --check-config --config /etc/siftp/siftp.toml. - Install the package (or replace the binary), then
systemctl restart siftpd. Restart drains first; clients reconnect. - Roll back by installing the previous package. The config, user store, host key, and audit chain are compatible within 0.x minor releases; new config keys default to off.
- Versioned docs for each release stay at
/v/<version>/.