No description
  • Scheme 64.5%
  • Shell 15.8%
  • Rust 15.5%
  • Makefile 2.7%
  • Python 1.3%
  • Other 0.2%
Find a file
2026-07-30 15:47:26 -06:00
.builds ci: set umask 022 in package/verify tasks too (jerbuild creates group-writable cache dirs during pkg operations) 2026-07-22 13:10:58 -06:00
.jerboa Security hardening and release readiness 2026-06-23 10:51:31 -06:00
bin Initial secure SMTP server scaffold 2026-06-10 17:19:26 -06:00
docs fix: close security audit findings 2026-07-11 17:25:10 -06:00
examples fix: close security audit findings 2026-07-11 17:25:10 -06:00
fuzz Security hardening and release readiness 2026-06-23 10:51:31 -06:00
lib/jerboa-smtp perf: json-escape via output port (avoid per-char append/reverse) 2026-07-22 14:01:47 -06:00
rust fix: DATA desync, JSON control char escaping, panic handler abort 2026-07-21 10:31:22 -06:00
scripts fix: close security audit findings 2026-07-11 17:25:10 -06:00
support Security hardening and release readiness 2026-06-23 10:51:31 -06:00
tests fix: close security audit findings 2026-07-11 17:25:10 -06:00
wasm Security hardening and release readiness 2026-06-23 10:51:31 -06:00
.gitignore jpkg: complete binary package setup 2026-07-30 15:22:50 -06:00
.jerbuild jpkg: complete binary package setup 2026-07-30 15:22:50 -06:00
AGENTS.md docs: remove jerboa-emacs restriction 2026-07-30 15:47:26 -06:00
build.ss Add native file publication helper 2026-06-10 20:24:17 -06:00
jpkg.lock jpkg: complete binary package setup 2026-07-30 15:22:50 -06:00
jpkg.policy.sexp jpkg: complete binary package setup 2026-07-30 15:22:50 -06:00
jpkg.sexp jpkg: complete binary package setup 2026-07-30 15:22:50 -06:00
LICENSE Switch to MIT license 2026-07-21 13:42:20 -06:00
Makefile build: add lint target 2026-07-21 11:58:05 -06:00
plan.md Clean delivered queue inputs 2026-06-11 09:47:34 -06:00
README.md fix: close security audit findings 2026-07-11 17:25:10 -06:00
SECURITY.md fix: close security audit findings 2026-07-11 17:25:10 -06:00

jerboa-smtp

Security-first SMTP server in Jerboa.

This project is starting from the same core security idea as jerboa-dns: Jerboa owns policy and orchestration, while hostile protocol bytes are parsed in small Rust WebAssembly modules with fixed caps and fuel.

Current Status

The first slice is project skeleton plus a sandboxed SMTP command parser:

  • Jerboa protocol representation for SMTP commands.
  • no_std Rust/WASM command parser with envelope reverse-path/forward-path parsing and normalization for MAIL FROM and RCPT TO.
  • no_std Rust/WASM DATA full-block framer for CRLF, terminator, dot-stuffing, line-limit, and NUL rejection checks.
  • Jerboa host wrapper that validates command and envelope metadata returned by the sandbox.
  • Pure Jerboa SMTP session state machine for command sequencing, no-relay policy, and explicit local-recipient allowlisting.
  • Strict JSON config loader with local-domain, local-recipient, IPv4, and resource-limit validation.
  • Durable queue admission API with sharded spool layout, unguessable queue IDs, envelope validation, atomic reserved quota counters, and metadata-as-admission-marker semantics.
  • Queue janitor with janitor --config FILE for safe temp-file cleanup and orphan message-file removal, including leftover delivery-marker temps.
  • no_std Rust native file publication helper linked into the standalone binary: open validated directories, create temp with openat(O_EXCL), write and fsync bytes, linkat into the final directory without overwriting, fsync the destination directory, then unlink temp by basename.
  • DATA-aware session path: DATA enters a pending state, the complete DATA block is framed in WASM, and accepted messages are queued before 250.
  • Binary-port SMTP listener with serve --config FILE [--once], configured IPv4 bind address, bounded concurrent workers, global/per-IP shedding, absolute command/DATA/connection deadlines, minimum DATA progress-rate enforcement, DATA collection, and queue handoff.
  • Local Maildir delivery worker with deliver --config FILE, configured local-recipient Maildir layout, native final-file publication, delivered markers published through native no-overwrite file operations, and idempotent repeat runs that clean stale incoming files after a delivered marker exists.
  • Smoke tests for plain parsing, sandboxed command parsing, sandboxed DATA parsing, config, native publication, queue admission, Maildir delivery, server connection handling, and session behavior.

See plan.md for the full implementation plan.

Build And Test

make build
make test

Release evidence:

make release-evidence

The release bundle records SMTP load/slow-client status under dist/release-evidence/soak/. By default this is a blocked, record-only status; set JSMTP_RUN_RELEASE_SOAK=1 with production-scale JSMTP_SOAK_SESSIONS to capture current SMTP session evidence plus local DATA queueing and slow-DATA rejection smokes. Deployment confinement and STARTTLS/AUTH policy are documented in docs/deployment-hardening.md and copied into dist/release-evidence/deployment-policy.txt. Bounded local coverage-guided fuzz evidence is recorded with:

JSMTP_RUN_COVERAGE_FUZZ=1 JSMTP_FUZZ_RUNS=2048 make fuzz-evidence

When JSMTP_RUN_COVERAGE_FUZZ=1 is set for make release-evidence, the release bundle also records coverage_fuzz_status=local-smoke-recorded under dist/release-evidence/fuzz-evidence/ after smtp_command and smtp_data complete.

Build just the WASM parser:

make wasm

Run the CLI parser smoke:

make run ARGS='parse "EHLO mx.example"'

Run the pure session smoke:

make run ARGS='session "EHLO client.example" "MAIL FROM:<alice@example.net>" "RCPT TO:<bob@example.org>"'

The CLI session smoke uses the default config, which has no local domains, so that recipient is denied as relay by design.

Run the session smoke with validated local-recipient policy:

make run ARGS='session --config examples/jsmtp.json "EHLO client.example" "MAIL FROM:<alice@example.net>" "RCPT TO:<bob@local.test>"'

The command parser requires WASM by default. JSMTP_ALLOW_WASM_COMMAND_FALLBACK=1 enables the in-process Jerboa parser only for development fallback.

Run a single-connection listener:

make run ARGS='serve --config examples/jsmtp.json --once'

Deliver queued local messages once:

make run ARGS='deliver --config examples/jsmtp.json'

Clean queue temp files and orphan message files:

make run ARGS='janitor --config examples/jsmtp.json'

Security Defaults

Production deployments must run as an unprivileged service identity behind service-manager confinement; see docs/deployment-hardening.md. Command parser fallback is development-only and requires JSMTP_ALLOW_WASM_COMMAND_FALLBACK=1.

The (std wasm sandbox) loader binds registered static symbols first in the standalone daemon. Development tests may still use jerbuild's trusted native library cache explicitly.

DATA can now be framed by the WASM parser and admitted through the queue API, including from the SMTP session state machine and the binary-port listener. Queued messages can be delivered to local Maildir storage by running the delivery worker.

The current DATA parser is a full-block WASM parser with a 64 KiB hard cap. max_message_bytes defaults to 65536 and larger configured values are rejected until the DATA path is moved to a streaming WASM parser.

local_recipients is required in config. RCPT accepts only full normalized addresses in that allowlist; unknown local users are rejected during the SMTP transaction with 550 No such local recipient.

The listener accepts continuously into bounded workers. max_connections and max_connections_per_ip shed excess clients with 421; the defaults are 64 and 8. connection_timeout_seconds, command_timeout_seconds, idle_timeout_seconds, and data_timeout_seconds are absolute deadlines that activity cannot extend. The default DATA rate floor is 1024 bytes/second after the configured grace period.

tls_available must remain false until the native rustls STARTTLS helper is implemented. Configurations that set it to true are rejected instead of advertising a plaintext placeholder.

auth_available must remain false until authenticated submission is implemented. Configurations that set it to true are rejected instead of accepting a nonfunctional AUTH deployment.

Queue publication currently writes the message file before its metadata file; consumers must treat metadata as the admission marker and ignore unmatched message files. The file publication primitive is now native Rust and refuses overwrite; queue and Maildir publication use directory-relative native calls with validated basenames. Local delivery publishes the delivered marker before deleting the incoming message and metadata files, so restart cleanup treats the marker as authoritative.

Quota admission uses a durable, atomically replaced counter under a native cross-process lock, so normal admission is O(1) and concurrent reservations cannot exceed the configured cap. Startup and the offline janitor reconcile the counter from disk. Queue IDs are distributed across 256 two-hex-digit shards under tmp, incoming, and delivered to avoid single-directory growth; legacy unsharded queue files remain readable and cleanable.