siftp

siftp Specification — Super Intelligent FTP

This document is normative. Code, configs/siftp.example.toml, the admin OpenAPI document, the MCP tool schemas, llms.txt, and the website must not contradict it; when they disagree, fix the code or amend this spec in the same change.

1. Goals and non-goals

1.1 Goals

  1. One daemon, siftpd, serving FTP, explicit and implicit FTPS, SSH/SFTP v3, and a read-only WebDAV/HTTP gateway against the same users, virtual roots, and permissions.
  2. Throughput close to kernel limits: zero-copy file transfer (sendfile/splice), pooled buffers, backpressure everywhere, an optional io_uring data plane, and hand-written assembly kernels for checksum/scanning hot paths with bit-identical Rust fallbacks.
  3. Secure by default: argon2id password hashing, SSH public-key auth, per-user virtual-root jails with per-directory permissions, rate limits, login bans, TLS 1.2+, post-quantum SSH key exchange.
  4. LLM-operable ("Super Intelligent"): every error is stable structured JSON {code,message,hint,docs_url}; a capability document at /.well-known/siftp.json; an OpenAPI 3.1 admin REST API; MCP tools mirroring the admin API; llms.txt; a JSON-lines audit log.
  5. Boring operations: one TOML file with SIFTP_* environment overrides, a durable store for API-managed users, graceful reload and drain, health and readiness probes, Prometheus metrics.

1.2 Non-goals

2. Architecture

            ┌──────────────────────────────────────────────────────┐
 clients ──▶│ siftpd (crates/siftpd) — tokio runtime               │
            │  ┌──────────┐ ┌───────────┐ ┌────────────┐ ┌───────┐ │
            │  │siftp-ftp │ │siftp-sftp │ │siftp-webdav│ │siftp- │ │
            │  │FTP/FTPS  │ │SSH/SFTP v3│ │read-only   │ │admin  │ │
            │  └────┬─────┘ └─────┬─────┘ └─────┬──────┘ └───┬───┘ │
            │       └─────────────┴──────┬──────┘     siftp-mcp│     │
            │                       siftp-core ◀───────────────┘     │
            │   (auth, jail, Storage trait, errors, config, audit)   │
            │   siftp-s3 (S3 Storage)   siftp-ldap (identity source)  │
            │   siftp-asm (kernels + Rust fallback)                   │
            └──────────────────────────────────────────────────────┘

2.1 Crate layout

CrateResponsibilityKey public surface
siftp-coreError contract, jail and Storage trait, permissions and Access, users, durable store, config, context, audit, statsApiError, ErrorCode, Storage, JailedFs, Access, ServerContext, AuditLog, capability_document
siftp-asmAssembly kernels + pure-Rust fallbacks, backend selectioncrc32c, find_newline, ct_eq, backend_name; asm/<arch>/*.s
siftp-ftpFTP/FTPS control + data channels, TLS contextCommand, SUPPORTED_COMMANDS, serve_ftp, serve_implicit_ftps, FtpsTls
siftp-sftpSSH transport + SFTP v3 subsystemPacketType, StatusCode, serve_sftp, load_or_generate_host_key, EXTENSIONS
siftp-webdavRead-only WebDAV/HTTP gatewayserve_webdav, router
siftp-s3S3-compatible Storage backend and tier object store (+ test mock)S3Storage, S3Factory, S3ObjectStore, CredentialProvider
siftp-tierTiered storage: local roots as a cache over an object store (§3.2)TierManager, TieredStorage, restore
siftp-ldapLDAP identity sourceLdapProvider
siftp-adminAdmin REST API, OpenAPI, health/readiness, metricsROUTES, openapi_document, router, service::*
siftp-mcpMCP tool registry and dispatch over the admin serviceTOOLS, call_tool, serve_stdio, http_router
siftpdBinary: config, listeners, signals, subcommandsserve, --check-config, hash-password, mcp-stdio, audit-verify, health, tier-restore

Dependency rule: protocol crates (ftp, sftp, webdav), backends (s3, ldap), and siftp-tier depend on siftp-core (+ siftp-asm) only; siftp-mcp depends on siftp-admin; only siftpd depends on everything and wires backends into the context (register_storage, set_identity_provider). No crate may depend "upwards" or sideways (dev-dependencies excepted).

2.2 Concurrency model

3. Virtual filesystem and jail semantics

3.1 Storage backends

Protocols program against siftp_core::Storage; a backend implements metadata, listing, positional open/read/write with explicit commit/abort, rename (with and without overwrite), mkdir/rmdir, remove, symlinks, usage, attribute changes, free space, and a canonical virtual path for permission checks.

virtual_rootBackendNotes
a host pathJailedFs (local)symlink-proof jail; kernel fast paths available
s3://bucket/prefixS3Storage (siftp-s3)[s3] endpoint and credentials; SigV4 presigned requests over rustls

S3 semantics: virtual /a/b is object prefix/a/b; directories are key prefixes, with an empty dir/ marker object for directories created by mkdir; reads are ranged GETs with an 8 MiB window; writes are staged in [s3].spool_dir and published on commit (single PUT up to part_size_bytes, multipart above) — an aborted upload publishes nothing and the previous object survives; rename is copy + delete (directories up to 10 000 objects); symlinks are invalid_command; modification times and mode bits cannot be set (requests succeed and are ignored); free space is reported as 1 PiB (quotas still apply). [s3].credentials selects static keys, the AWS_* environment, or the EC2 instance role (IMDSv2, refreshed five minutes before expiry).

3.2 Tiered storage

With [tier].remote set (s3://bucket/prefix, Unix only), the local roots under [server].root are a cache over an object store (siftp-tier).

4. Protocol matrix

AreaStandardScope
FTP coreRFC 959USER PASS ACCT QUIT SYST NOOP HELP SITE; PWD/XPWD CWD/XCWD CDUP/XCUP; TYPE A/I, MODE S, STRU F; PASV, PORT; RETR STOR STOU APPE REST ABOR; DELE RMD/XRMD MKD/XMKD RNFR RNTO; LIST NLST
FTP extensionsRFC 3659, RFC 7151, draft-somers-ftp-mfxxMLSD MLST (facts type, size, modify, perm, unique; OPTS MLST selects facts), SIZE, MDTM, MFMT, TVFS; FEAT, OPTS (UTF8, MLST); HOST (virtual hosts)
IPv6 / NATRFC 2428EPSV (incl. EPSV ALL), EPRT; passive port range + external IP config
FTPSRFC 4217 (+ RFC 2228 verbs)Explicit: AUTH TLS, PBSZ 0, PROT P/C (CCC refused by policy, §10); implicit FTPS listener; TLS 1.2/1.3 via rustls; require_tls rejects plaintext PASS; data connections resume the control session's TLS session, enforced with require_session_reuse
SSHRFC 4251–4254SSH-2.0; host keys Ed25519 (generated) or ECDSA, RSA host keys refused (§10); kex mlkem768x25519-sha256, curve25519-sha256, ecdh-sha2-nistp256, with ext-info and OpenSSH strict KEX; ciphers aes256-gcm and aes128-gcm, with chacha20-poly1305 offered first only when the CPU lacks AES and carry-less multiply instructions (clients pick it whenever it is offered, and AES-GCM is several times faster with them), or the [sftp].ciphers list; auth publickey (Ed25519, ECDSA, RSA-SHA2 verify-only) and password; only session channels with subsystem sftp
SFTPdraft-ietf-secsh-filexfer-02 (v3)INIT/VERSION, OPEN CLOSE READ WRITE, LSTAT FSTAT STAT SETSTAT FSETSTAT, OPENDIR READDIR, REMOVE MKDIR RMDIR RENAME, READLINK SYMLINK, REALPATH; extensions posix-rename, statvfs, fstatvfs, expand-path, limits, fsync (@openssh.com); OpenSSH wire numbering (REALPATH 16, STAT 17, SYMLINK 20, NAME 104, ATTRS 105), SYMLINK target first; exit-status 0 sent when the subsystem ends
WebDAV/HTTPRFC 4918 (class 1, read-only)OPTIONS, PROPFIND (Depth 0/1), GET/HEAD with single Range; HTTP Basic auth; write methods refused (§4.1)
Admin APIOpenAPI 3.1 (§8.3)REST JSON over HTTP/1.1 on the admin listener (loopback by default), bearer token
MCPModel Context Protocol (2025-06-18)tools/list, tools/call over stdio or streamable HTTP at /mcp; tools in §8.4

FTP details:

SFTP details:

4.1 Read-only WebDAV/HTTP gateway

Enabled by [webdav].bind (HTTPS with [webdav].tls = true, using the FTPS certificate). Users, credentials (HTTP Basic, verified through the shared authentication path), storage, per-path permissions, rate limits, bans, and audit are the same as for FTP/SFTP. GET of a file needs r; PROPFIND and directory GET need l (HTML index, or JSON with Accept: application/json). Every write method gets permission_denied (403). File responses carry X-Content-Type-Options: nosniff and Content-Security-Policy: sandbox. Verified credentials are cached for 60 s under a keyed digest from the login verification cache's process-only key (§5.1), with the stored hash in the input, so a changed password or a deleted user stops working at once.

5. Authentication and authorisation

5.1 Passwords

5.2 SSH public keys

5.3 Permissions, groups, and directory rules

5.4 Rate limits and throttling

5.5 Users from the durable store

See §7.1. Store users are validated exactly like config users.

5.6 External identity: LDAP

With [ldap].url set, a login name that is not a local user (config or store) is checked against LDAP: search-then-bind (bind_dn + base_dn + user_filter, filter-escaped) or direct bind (user_dn_template, DN-escaped), over ldaps:// or StartTLS. Empty passwords are refused before any network I/O (anonymous-bind bypass). Group DNs from group_attribute map onto [[groups]] through group_map; without a mapped group the user gets default_permissions, or is refused with require_group = true. The virtual root is virtual_root_template ({user} substituted) or <server.root>/<user>. LDAP users are session-only (never stored) and go through the same jail, permissions, quotas, and audit. An unreachable directory fails logins as auth_failed on the wire (audited as internal).

6. Performance design

6.1 Zero-copy data plane (Linux)

6.2 io_uring (feature io-uring, Linux ≥ 5.6)

With the feature compiled and [performance].io_uring = true: downloads read the file with IORING_OP_READ (after sendfile in preference) and uploads write it with IORING_OP_WRITE (after splice), one ring per transfer, one entry in flight. If ring setup fails (old kernel, seccomp — Docker's default profile blocks io_uring), that transfer falls back. The feature never changes wire behaviour.

6.3 Buffer pools and backpressure

6.4 Assembly kernels (siftp-asm)

Sources are .s files under crates/siftp-asm/asm/<arch>/, assembled via build.rs when the asm feature is on, target_os ∈ {linux, macos}, and target_arch ∈ {x86_64, aarch64}. Every kernel has a pure- Rust twin that is always compiled and tested and is the reference for differential tests. Runtime feature detection (SSE4.2 cpuid, aarch64 HWCAP_CRC32) gates each kernel; absent the feature, Rust serves.

KernelSignature extern "C"UseRust reference
siftp_crc32c_<arch>fn(*const u8, usize, u32) -> u32integrity helperscrc32c_rust
siftp_find_newline_<arch>fn(*const u8, usize) -> usizeFTP command-line scanningfind_newline_rust
siftp_ct_eq_<arch>fn(*const u8, *const u8, usize) -> u8token comparisonct_eq_rust
siftp_find_byte_<arch>fn(*const u8, usize, u8) -> usizebyte scanningfind_byte_rust
siftp_adler32_<arch>fn(*const u8, usize, u32) -> u32weak checksumsadler32_rust

Rules: (1) assembly is bit-identical to the Rust reference on all inputs; (2) no kernel allocates, panics, or reads past len; (3) without the feature backend_name() returns "rust"; (4) unsafe is confined to the FFI shims in siftp-asm and the one io_uring submission helper in siftp-ftp::fastpath, with its safety argument written out. Everything else is #![forbid(unsafe_code)] (the FTP crate uses deny).

7. Configuration

7.1 Durable user store

7.2 Lifecycle

8. LLM contracts

8.1 Structured errors — everywhere

Every error on every surface derives from siftp_core::ApiError and serializes to exactly:

{"code":"permission_denied","message":"user bob lacks write permission","hint":"grant the 'w' permission bit or choose a writable path","docs_url":"https://siftp.org/docs/errors#permission_denied"}

8.2 Capability document

GET /.well-known/siftp.json on the admin listener (unauthenticated):

{"name":"siftp","version":"0.2.1","protocols":["ftp","ftps-explicit","ftps-implicit","sftp","webdav-readonly"],"docs":"https://siftp.org/v/0.2.1/docs/","llms_txt":"https://siftp.org/v/0.2.1/llms.txt","docs_latest":"https://siftp.org/docs","openapi":"/api/v1/openapi.json","mcp":"/mcp","metrics":"/metrics","health":"/healthz","ready":"/readyz"}

protocols lists what this server serves with its current config (FTPS only with a certificate, WebDAV only when bound). docs and llms_txt point at the documentation for this exact version (§9.1). mcp is null when [mcp].transport = "stdio". An agent's boot sequence: this document → llms_txt → openapi → operate.

8.3 Admin REST API + OpenAPI

Base: [admin].bind (default 127.0.0.1:8080), JSON bodies up to 1 MiB, Authorization: Bearer <token> on everything except /.well-known/siftp.json, /healthz, /readyz, and /api/v1/openapi.json. At most 64 concurrent requests (429 beyond) and 30 s per request. Responses carry X-Content-Type-Options: nosniff, Cache-Control: no-store, X-Frame-Options: DENY. The route table is siftp_admin::ROUTES: each route carries its parameters, request body, success status, and response schema, and the OpenAPI document is generated from it (every $ref resolves; a test enforces it).

Method & pathPurpose
GET /api/v1/users · POST /api/v1/usersList / create users (create hashes a plaintext password server-side; persisted in the store)
GET/PUT/DELETE /api/v1/users/{name}Get / replace / delete one user (password_hash is always null; config users are read-only)
POST /api/v1/users/{name}/passwordSet or rotate a password (§5.1)
GET /api/v1/sessions · DELETE /api/v1/sessions/{id}List / terminate sessions
GET /api/v1/files?user=&path=Directory listing inside a user's root
GET /api/v1/audit?lines= · GET /api/v1/audit/verifyTail the audit log / verify its hash chain
GET /api/v1/stats · GET /metricsCounters as JSON / Prometheus text (§8.7)
POST /api/v1/reloadReload (§7.2)
GET /healthz · GET /readyzLiveness / readiness

8.4 MCP tools

Transports: stdio (siftpd mcp-stdio, always available) and streamable HTTP at POST /mcp (same bearer token and ban accounting), which [mcp].transport = "stdio" turns off: requests then get not_found. The setting is read per request, so a reload applies it. Results are the admin JSON payloads; errors are the §8.1 object in an error result. Registry: siftp_mcp::TOOLS (names are stable API).

ToolMirrors
siftp_capabilities§8.2 document
siftp_list_users · siftp_get_userGET /api/v1/users[/{name}]
siftp_create_user · siftp_update_user · siftp_delete_userPOST/PUT/DELETE users
siftp_set_passwordPOST /api/v1/users/{name}/password
siftp_list_filesGET /api/v1/files
siftp_list_sessions · siftp_kill_sessionsessions
siftp_tail_audit · siftp_verify_auditaudit tail / verify
siftp_statsGET /api/v1/stats
siftp_reload_configPOST /api/v1/reload

8.5 Audit log

[logging].audit_file is append-only JSON-lines (mode 0640): {"ts":<unix>,"user":…,"action":"RETR|STOR|LOGIN|…","path":…, "result":"ok|<error code>","remote":"ip:port","seq":<n>,"chain":"<hex>"}. Passwords, keys, tokens, and file contents never appear. One line per:

Eventaction values
Login, success or failureLOGIN (FTP, SFTP, WebDAV), ADMIN_AUTH (wrong admin token)
Banauth_ban
TransferFTP RETR, STOR, STOU, APPE; SFTP READ, WRITE (at CLOSE); WebDAV GET
File changeFTP MKD, RMD, DELE, RNTO, MFMT; SFTP MKDIR, RMDIR, REMOVE, RENAME, SYMLINK, SETSTAT
Admin change (REST or MCP; remote is admin-api)USER_CREATE, USER_REPLACE, USER_DELETE, PASSWORD_SET, SESSION_KILL, RELOAD
OtherRELOAD on SIGHUP (remote is signal), DATA_REFUSED (FTP data connection from a foreign address), PROT with path C (FTPS data channel set to clear)

Failed transfers and changes are logged with their error code as result. path is the resolved virtual path (absolute, starting with /) on every protocol; when a path does not resolve (an escape attempt), it is the text the client sent. Lookups (FTP CWD, SIZE, MDTM, LIST, MLSD, STAT; SFTP STAT, REALPATH, OPENDIR, READDIR; admin GET routes) are not logged, including when they fail.

8.6 llms.txt rules

website/llms.txt (llmstxt.org format): an H1, a blockquote summary, then H2 sections of links to the spec, config, error, OpenAPI, and MCP references, each with one line. Every link resolves; no marketing copy; updated with any contract change; under ~200 lines. A copy is published per version (§9.1).

8.7 Observability

9. Website information architecture (siftp.org)

Static files only. Markdown files are the canonical sources and the served .md mirrors; website/build.py (stdlib-only) renders HTML, sitemap.xml, and llms-full.txt:

website/
  index.md            what siftp is, quick start, measured numbers
  llms.txt, llms-full.txt
  docs/               quickstart, configuration, protocols, storage,
                      identity, operations, llm, performance, security,
                      errors
  spec.md             verbatim copy of SPEC.md
  download.md         releases, verification, source builds
  v/<version>/        frozen copy of the docs for each release (§9.1)

Rules: canonical URL per topic; every error code anchorable at /docs/errors#<code>; no JavaScript required; code samples runnable.

9.1 Versioned documentation

Each release freezes the docs under /v/<version>/ (HTML, Markdown mirrors, and its own llms.txt/llms-full.txt); versions.json lists them. Pages under /v/<version>/ that are not the latest carry a banner linking to the current page. The capability document (§8.2) points agents at the version they are talking to.

10. Security considerations

11. Acceptance tests

Run in CI on Linux x86_64 and aarch64 (and macOS for the dev lane):

  1. cargo build/test/clippy -D warnings on the workspace, also with --features asm (siftp-asm) and --features io-uring (siftp-ftp).
  2. FTP with curl, lftp, and Python ftplib (tests/interop/run.sh): login, PASV/EPSV transfers byte-identical, MLSD/SIZE/MDTM/MFMT, REST resume, mirror both ways, .. refused 553, read-only STOR 550.
  3. FTPS explicit (curl, lftp, ftplib with PROT P) and implicit; with require_tls, plaintext PASS gets 534; session reuse enforced when configured.
  4. SFTP with OpenSSH sftp/scp and a russh client: pubkey + password, put/get byte-identical, readdir, rename semantics, statvfs, symlinks, jail escape refused, oversized packet rejected, expired/revoked keys refused.
  5. Auth/abuse: bans trip and expire; quota exceeded removes the partial file; caps refuse; drain completes an in-flight upload.
  6. LLM surface: capability document; create a user via the admin API and log in over FTP; every MCP tool once over stdio; error bodies match §8.1; audit lines parse and the chain verifies.
  7. Performance smoke (Linux): sendfile/splice/io_uring counters advance on real transfers; asm kernels match references; bench/run.sh reproduces the published table.
  8. Config: --check-config accepts the substituted example and rejects unknown keys and weak hashes; env overrides win; reload adds a user without dropping a session; API users survive restart, reload, and SIGKILL mid-write.
  9. Storage: the FTP and SFTP suites pass on S3 roots (in-process mock in cargo test; VersityGW in CI).

12. Roadmap

13. Stage map

Stage 1 (spec + scaffold) → config/auth core → FTP/FTPS → SSH/SFTP → data-plane fast paths → admin API → MCP → website → production (storage backends, identity, durability, lifecycle, observability, hardening, packaging, CI, benchmarks). No stage merges with a red build or a placeholder where this spec defines behaviour.