This document describes what Sovite defends against, the privileges it runs with, and the rules every component must follow. It is a living document; each roadmap phase extends it. To report a vulnerability, see SECURITY.md.
1. Assets
| Asset | Why it matters |
|---|---|
| Queued mail | Confidential content. A 250 OK is a promise to deliver it. |
| Relay permission | An open relay gets the host blocklisted and used for abuse. |
| Credentials | SMTP AUTH passwords and backend (SQL/LDAP) credentials. |
| Private keys | TLS keys and DKIM signing keys. A leaked DKIM key lets anyone forge the domain's mail. |
| Configuration and lookup tables | Decide who may relay and where mail goes. |
| Availability | Mail delayed by more than the queue lifetime is bounced. |
2. Threat Actors
| Actor | Capabilities |
|---|---|
| Remote unauthenticated client | Opens TCP connections to ports 25/465/587 and sends arbitrary bytes. This is the main attack surface. |
| Authenticated user | Valid credentials (possibly stolen); tries to relay, spoof senders, or exceed quotas. |
| Malicious remote server | Answers our outbound connections; sends hostile replies, TLS certificates, or DSNs. |
| DNS attacker | Spoofs or manipulates DNS answers (MX, TLSA, SPF, DKIM records). |
| Local unprivileged user | Shell on the host; uses sendmail(1) and may read world-readable files. |
Out of scope: an attacker with root, or with the Sovite user's privileges, on the host.
3. Privileges and Files
- Sovite runs as a dedicated unprivileged user (
sovite), never as root. - Ports below 1024 are bound with systemd socket activation or
CAP_NET_BIND_SERVICE. The VM never needs root. - The spool directory (
[queue] directory) is owned bysovite, mode0700. Queue files are0600. - The config file and lookup tables are owned by
root, groupsovite, mode0640. Sovite reads them but cannot modify them. - TLS and DKIM private keys are owned by
root, groupsovite, mode0640. - The
sendmail(1)compatibility command submits mail over SMTP to a local listener and is not setuid. It reads the config file only when the user running it can, so local users never need access to its secrets. Like Postfix's, it lets a local user choose the envelope sender (-f); relaying needs the local listener's address insmtp.trusted_networks.
4. Rules for All Code
These apply to every component and are checked in code review.
- Never an open relay. Relay requires an explicit grant: an authenticated user or a configured trusted network. The default config relays for no one.
- Durability before
250. A message is written andfsynced (file and directory) before the reply is sent. - Bounded parsing. Everything parsed from the network has a hard limit: line length, command count, header count and size, recipients per message, message size, MIME nesting depth, DNS response size, SPF lookup count. Limits are checked while streaming, not after buffering.
- No atoms from untrusted input. The BEAM atom table is never garbage-collected. Network input, DNS data, and config keys are mapped to atoms only through fixed tables, as the config loader does.
- SMTP smuggling. Only
<CRLF>.<CRLF>ends DATA. Bare LF and bare CR are rejected or normalized by an explicit policy, never interpreted inconsistently. - STARTTLS injection. Any plaintext buffered after the
STARTTLScommand is discarded once the handshake completes, on both server and client side. - Secrets are never logged (see Logging) and never appear in crash reports. Processes holding secrets use
:sensitiveprocess flags or keep secrets out of their state. - Credential checks are constant-time. Passwords are stored salted and stretched:
SCRAM-SHA-256(PBKDF2, RFC 7677) by default, or SHA-crypt. Unknown users cost as much time as wrong passwords, andSCRAM-SHA-256answers them with a stable fake salt, so neither timing nor the exchange reveals which users exist. - Modern TLS only. TLS 1.2 and 1.3 with BCP 195 (RFC 9325) cipher suites. Certificates are verified wherever the policy says so.
- Untrusted text is escaped in logs and headers. Values from the network cannot inject log lines or header fields (CR/LF in
EHLOnames, addresses, and similar). - Least data. Message bodies are streamed, not held in memory, and are not copied into crash dumps or logs.
5. Defenses by Phase
| Threat | Defense | Phase |
|---|---|---|
| Open relay | Explicit relay permission, open-relay test in the definition of done | 1 |
| Resource exhaustion | Connection limits (global and per IP), timeouts, bounded parsing | 1 |
| SMTP smuggling | Strict end-of-data handling | 1 |
| Lost mail on crash | fsync before 250, delivery results fsynced before they count, crash recovery | 1–2 |
| Hostile remote servers | Bounded reply parsing (line length and count), timeouts on every wait, remote text sanitized before it goes into notifications or logs | 2 |
| Mail loops | Notifications sent from <> and never answered; double-bounce reports never reported again; MX hosts at or below this server's preference skipped; a server greeting with our own name treated as a loop | 2 |
| Credential theft in transit | AUTH offered only after TLS by default; submission listeners require TLS; relay host credentials only sent over TLS; SASL data never logged | 3 |
| Weak or downgraded inbound TLS | TLS 1.2+ only, forward-secret AEAD suites only, server cipher order, no client renegotiation (testssl.sh: A+ with a trusted certificate) | 3 |
| STARTTLS command injection (CVE-2011-0411 class) | Input received after STARTTLS and before the handshake is discarded; the session restarts after it; the client discards pre-TLS server data | 3 |
| Brute-force AUTH | Failed logins counted per address (per /64 for IPv6) with temporary bans; delay after each failure; session closed after 3 failures | 3 |
| Sender spoofing by authenticated users | Sender login maps | 3 |
| Directory and database injection | LDAP filters parsed before user names are inserted; database access only through Ecto with bound parameters; empty LDAP passwords refused (anonymous bind) | 3 |
| A broken certificate renewal taking TLS down | Certificates reloaded only when the new pair loads and matches; otherwise the old one stays | 3 |
| Downgrade and MITM on outbound TLS | Per-destination TLS policy (encrypt, verify), DANE with DNSSEC-validated TLSA records | 3 |
| Downgrade on outbound TLS without DNSSEC | MTA-STS | 7 |
| Spam and bot traffic | Postscreen-style screen (greeting delay against early talkers, weighted DNSBL/DNSWL/RHSBL scores), greylisting, reverse DNS and EHLO checks, per-client rate limits, tarpit after errors | 8 |
| Protocol abuse and SMTP smuggling | Clients that pipeline where RFC 2920 forbids it (including message data sent before 354) are disconnected; HTTP requests and header lines close the session; bare LF or CR is refused | 8 |
| Compromised accounts | Per-user sending quotas; users suspended when too much of their mail fails | 8 |
| Client address spoofing through proxy features | PROXY protocol headers only on listeners that enable it (and from smtp.proxy_networks), XCLIENT and XFORWARD only from smtp.xclient_networks and smtp.xforward_networks, none by default; strict, bounded PROXY v1/v2 parsing that never reads past the header; a missing or invalid header closes the connection | 9 |
| Hostile internationalized input | UTF-8 only where RFC 6531 allows it (addresses of SMTPUTF8 transactions), validated as UTF-8 without control characters; domain names checked with IDNA2008 (disallowed code points, contextual and Bidi rules) and used only as A-labels, so one domain has one form for routing, lookups, and the database; Punycode decoding bounded by label length; UTF-8 from remote servers in notifications stripped of control characters | 10 |
| Rogue cluster nodes and eavesdropping between nodes | Cluster nodes talk only over TLS 1.3 with mutual authentication against the cluster's own CA, with no plaintext mode; Partisan starts only when [cluster] is enabled (it is not started with the application), Distributed Erlang stays off; peers are as trusted as the config file, so the CA must sign nothing else and the peer port should only be reachable from the other nodes | 13 |
| One tenant abusing or impersonating another | Per-tenant message, recipient, and size limits shared by the cluster; a tenant's DKIM keys sign only its own mail; per-tenant IP pools | 13 |
| Hostile or failing milters and policy servers | Bounded packets and replies (64 MiB milter packets, 4 KiB policy replies), timeouts on every wait, no atoms from their input; failures answer with a temporary error by default (default_action), so mail is never accepted unchecked; spawn: policy programs run without a shell | 9 |
6. Supply Chain
- Dependencies are kept minimal and pinned in
mix.lock. CI fails on unused locked dependencies. - Release builds are reproducible and release artifacts are signed (Phase 12).