Skip to content
SwornMail docs

Start

Receive with Postfix

The reference milter checks the address of every inbound connection against Mode 1 DNS records and adds an Authentication-Results field with the result, removing any forged ones on the way in. It never rejects, defers or reroutes a message.

Output on this page is real. It was captured by running sworn-milter and driving it with a milter client the way Postfix does. Addresses are from the documentation ranges, which publish no records, so the test connections come back none. The pass example near the end shows the format the milter uses.

1. Install

$ go install github.com/swornmail/swornmail-go/cmd/sworn-milter@latest

Needs Go 1.24 or later. Source: github.com/swornmail/swornmail-go.

2. Run it

$ sworn-milter --authserv-id mx.example.com
sworn-milter: listening on tcp:127.0.0.1:10025 (authserv-id mx.example.com)
FlagDefaultWhat it does
--listentcp:127.0.0.1:10025Where Postfix connects. tcp:host:port or unix:/path.
--authserv-idthe host nameThe id stamped on each result. Inbound results claiming it are removed, so set it to the id your downstream filters trust.
--dns-timeout5sTime allowed for the lookups behind one result. Running out gives temperror.
--ineligible-prefixnoneIPv6 prefixes never matched to an operator, comma-separated or repeated. If this host reaches IPv4 clients through NAT64 or SIIT, list the translation prefix: every translated client appears inside it. A source there gets sworn=none with no lookup. A malformed prefix stops the milter at startup.

It needs no privileges, no configuration file and no state. Run it under your service manager as an ordinary user.

Check that port 10025 is free first. Content-filter setups, amavis among them, often have Postfix listen on 127.0.0.1:10025 to take filtered mail back. postconf -M | grep 10025 shows whether Postfix has a service there, and ss -ltn (or lsof -iTCP -sTCP:LISTEN on macOS and the BSDs) shows anything else. If it’s taken, run sworn-milter --listen tcp:127.0.0.1:<free port> and use the same port in smtpd_milters below.

3. Point Postfix at it

# /etc/postfix/main.cf
smtpd_milters = { inet:127.0.0.1:10025, default_action=accept }

Then postfix reload.

default_action=accept is what keeps this fail-open. The milter never refuses mail. Without that setting, a stopped or timed-out milter gets the global milter_default_action: tempfail, or shutdown since Postfix 3.11, and either one turns it into refused mail. Set inside the braces, it covers this milter only, so your other milters keep failing the way they do now. Per-milter settings need Postfix 3.0 or later. Don’t set the global milter_default_action = accept instead: that would also let mail through unscanned whenever a virus or spam milter is down.

If smtpd_milters is already set, edit that line rather than adding a second one. Postfix keeps only the last definition, so a second line would replace the milters you already run, DKIM signing included. Put this entry first:

smtpd_milters = { inet:127.0.0.1:10025, default_action=accept },
    <your existing milters>

First, because milters such as OpenDKIM or OpenDMARC may use the same authserv-id. Run ahead of them, this one never sees their results, so it can’t mistake them for inbound ones and remove them.

This page uses TCP on loopback because it works the same everywhere. A unix: socket works too, but if smtpd runs chrooted (check the chroot column for smtp in master.cf), Postfix looks for an absolute socket path inside the chroot.

4. Check it

A test message from 2001:db8:f00:1234::25 arrives carrying a forged result under the receiver’s id and a genuine one from another domain:

Authentication-Results: mx.example.com 1; sworn=pass policy.op=victim.example
Authentication-Results: other.example; spf=pass smtp.mailfrom=sender.example

After the milter:

Authentication-Results: mx.example.com; sworn=none
Authentication-Results: other.example; spf=pass smtp.mailfrom=sender.example

The forged field is gone, the other domain’s result is untouched, and the milter’s own result is on top. A connection over IPv4 gets the same treatment and always sworn=none: Mode 1 attests IPv6 space only.

To see what the milter will say about a sender before any mail arrives, run the same discovery with the CLI from the deploy guide:

$ sworn discover --ip 2001:db8:f00:1234::25
sworn=none

What the results mean

ResultMeaning
sworn=passThe address is inside a prefix the operator publishes, and the operator has accepted accountability for it.
sworn=none policy.testing=y policy.wouldbe=passSame, but the operator is still in observe-only mode. Stake nothing on it, for credit or blame.
sworn=noneNo operator confirms this address, or it is IPv4.
sworn=temperrorDNS failed, ran out of time, or the lookup limit was reached. Treat it like none, never as worse.

A pass carries the details:

Authentication-Results: mx.example.com; sworn=pass policy.mode=dns
    policy.op=mailer.example.com policy.unit="2001:db8:f00:1234::/64"
    policy.observed="2001:db8:f00:1234::/64"

Key reputation on policy.op and policy.observed, the /64 this connection actually came from. policy.unit is what the operator asked for, which is a claim. The full rules are in reputation semantics.

Where it has to run

On the border MTA, the one that accepts connections from the Internet. The milter judges the address Postfix reports, so it must be the sender’s. Behind a load balancer or proxy, have Postfix take the real client address from it (for example smtpd_upstream_proxy_protocol = haproxy); otherwise every connection is judged as the balancer’s address. Only do that on a listener that nothing but the proxy can reach: anyone else who can connect to it can claim any address, including one inside an attested prefix, and get a pass.

Forged results

Anyone can write an Authentication-Results field, so the milter removes every inbound one that could be read as claiming your authserv-id (see the trust boundary). The match is deliberately loose: case, a trailing dot, odd whitespace and comments don’t hide a forgery, and any field containing a MIME encoded-word is removed outright. Removing a field it didn’t need to is harmless; keeping a forged one is not.

Removing forged results fails open too. While the milter is down, mail is accepted with no sworn result and with any inbound field claiming your id left in place. A downstream filter that trusts that id sees whatever the sender wrote until the milter is back.

DNS

The milter uses the host’s resolver and makes at most ten lookups per result. Point the host at a validating resolver if you can: an unsigned answer can be spoofed, and DNS is the root of trust (see trust in DNS). A lookup that times out or fails outright (a SERVFAIL, say), or reaching the limit, gives temperror, never none, so a sender’s attestation can’t be erased by breaking their DNS. A record that simply isn’t there gives none.

What it doesn’t do

It doesn’t score or filter. It adds a result, and acting on it is up to whatever reads the header next: your content filter, or the rspamd module if you would rather run the check there. It also can’t verify Mode 2 tokens: a milter sees the SMTP conversation but can’t extend it with an EHLO keyword or a new command (see implementations).