This software is experimental/in development. The configuration interface may still change. Feedback from early adopters is welcome, but note that this project is under construction and actively being worked on.
SPF Milter is a milter application that verifies email senders using the Sender Policy Framework (SPF) protocol. It can be integrated with milter-capable MTAs to check and enforce authorisation published as SPF policy in the DNS.
SPF Milter closely adheres to the rules and recommendations of the SPF
specification, [RFC 7208]. SPF verification strictly proceeds in the recommended
manner: The first, optional step is to verify a client’s HELO identity (the
domain name given with the SMTP HELO
command). If this step is not done or is
not conclusive, then the client’s MAIL FROM identity (the reverse-path or
envelope sender given with the SMTP MAIL
command) is verified. In either case,
verification produces a final SPF result. The milter may then take action,
either by rejecting the message with an SMTP error reply, or by recording the
result in the message header.
Within the constraints of the specification, SPF Milter exposes flexible configuration options. Configuration toggles and settings for the verification procedure, result handling, SMTP reply, header fields, and more, support a broad range of use cases. SPF Milter is capable of in-flight configuration reloading, so that no restart is necessary for configuration changes.
In terms of implementation, SPF Milter is essentially a configuration interface to an SPF verifier integrated in the milter protocol. The SPF implementation is provided by the [viaspf] library, and the DNS implementation is provided by the [domain] library. We believe these to be a solid foundation for SPF milter software.
SPF Milter is a [Rust] project. It can be built and/or installed using Cargo as usual. For example, use the following command to install the latest version published on crates.io:
cargo install --locked spf-milter
As a milter, SPF Milter requires the libmilter C library to be available. For
example, on Debian and Ubuntu one needs to install the libmilter-dev
package.
The build process uses the pkg-config
program to locate the milter library. If
your distribution does not provide pkg-config metadata for libmilter, try using
the provided milter.pc
file: Save milter.pc
to some directory, then run any
Cargo command with that directory added to the pkg-config path. For example, to
build the executable:
PKG_CONFIG_PATH=/path/to/dir cargo build
The [domain] library requires the OpenSSL library and development files. On
Debian and Ubuntu, install the package libssl-dev
.
Finally, the default configuration file location can be overridden at build time
by setting the environment variable SPF_MILTER_CONFIG_FILE
.
The minimum supported Rust version is 1.46.0.
Once installed, SPF Milter can be invoked on the command-line as spf-milter
.
SPF Milter reads configuration parameters from the default configuration file
/etc/spf-milter.conf
. At a minimum, the mandatory socket
parameter must be
set in that file. See the included spf-milter.conf
for a sample configuration.
Invoking spf-milter
starts the milter in the foreground. Send a termination
signal to the process or press Control-C to shut the milter down.
To set up SPF Milter as a system service, try using the provided
spf-milter.service
systemd service. Modify this file to suit your needs (for
example, by setting User
and UMask
), install it in /etc/systemd/system
,
then start and enable the service.
SPF Milter logs status messages to syslog. By default, in addition to warnings and errors the verification result for each verified identity is written to the log.
SPF Milter is configured primarily by setting configuration parameters in the
configuration file /etc/spf-milter.conf
. Parameters come with sensible default
settings, and SPF Milter can be used right away by specifying just the mandatory
socket
parameter.
The included man page spf-milter.conf(5) currently serves as the reference
documentation. (You can view the man page without installing by using the file’s
path: man ./spf-milter.conf.5
)
Configuration can be reloaded during operation by sending the signal SIGUSR1
to the milter process. Refer to the man page for details.
Before integrating SPF Milter in your mail server setup, consult at least the documentation for the parameters presented in brief below.
The mandatory socket
parameter controls where SPF Milter should open a
listening socket for the connection from the MTA. This parameter’s value should
be a socket specification in one of two forms:
inet:port@host
or
inet6:port@host
for a TCP socketunix:path
for a UNIX domain socketThe socket specification is passed to the milter library as-is.
The main configurable facet of the verification procedure is whether the HELO
identity is verified or not: this is controlled with the boolean parameter
verify_helo
. Thus there are two possible verification procedures:
Which HELO verification results are definitive can be controlled with
definitive_helo_results
.
Negative authorisation results and error results may be rejected at the SMTP
level. Which of them are can be controlled with the reject_results
parameter.
A final result that falls in the set of results that are to be rejected causes SPF Milter to respond to the client with a transient or permanent SMTP error reply.
The SMTP reply code and reply text, and the enhanced status code can be
customised for each SPF result. For example, for the fail result, use
parameters fail_reply_code
, fail_status_code
, and
fail_reply_text
.
A final result that does not get rejected causes SPF Milter to add an entry to
the message header. The type of header field can be configured using the
header
parameter. The values Received-SPF
and Authentication-Results
each select the header field of the same name. The default Received-SPF
header
records SPF verification parameters in full detail, whereas the
Authentication-Results
header only records the result and sender domain:
Received-SPF: pass (mail.gluet.ch: domain of
users-return-122976-dbuergin=gluet.ch@spamassassin.apache.org has authorized
host 95.216.194.37) receiver=mail.gluet.ch; client-ip=95.216.194.37;
helo=mxout1-he-de.apache.org;
envelope-from="users-return-122976-dbuergin=gluet.ch@spamassassin.apache.org";
identity=mailfrom; mechanism="include:_spf.apache.org"
Authentication-Results: mail.gluet.ch; spf=pass
smtp.mailfrom=spamassassin.apache.org
To make the above information more practical, this section presents two common example use cases: ‘standard’ SPF, and SPF as part of DMARC.
The standard, RFC-compliant SPF verification use case is well covered by SPF
Milter’s default configuration settings. Therefore, just setting the mandatory
socket
parameter and leaving all other parameters at their default is enough
to configure a standard SPF verification use case.
/etc/spf-milter.conf
:
socket = inet:3000@localhost
Don’t forget to integrate the milter with the MTA. For example, with [Postfix]
add the socket location to the milters in /etc/postfix/main.cf
:
smtpd_milters = inet:localhost:3000
non_smtpd_milters = $smtpd_milters
Let us do a brief walkthrough of the default behaviour. SPF Milter will first
verify the HELO identity (verify_helo=yes
). If the HELO identity does not
evaluate to one of the definitive authorisation results – pass or fail –,
next the MAIL FROM identity is verified.
The final result from either identity will then be enforced as suggested in RFC
7208. That is, the results fail, temperror, and permerror will be rejected
with an appropriate permanent or transient SMTP error reply, and for all other
results a header field recording the result is added to the message header. The
specifics of rejection at the SMTP level can be adjusted using the parameter
reject_results
and a number of parameters for each SPF result.
By default, a header field of type Received-SPF
is used. The parameter for
configuring the header field type is named header
.
Treat softfail
as a failing result. A very strict setup may treat
softfail
with the same severity as a fail
result. To implement this, the
softfail
result needs to be added to the definitive HELO results and also to
the results that get rejected.
/etc/spf-milter.conf
:
definitive_helo_results = pass, fail, softfail
reject_results = fail, softfail, temperror, permerror
Enable both header fields. SPF Milter supports both of the specified header fields, and one might want to have them both added to messages. The configuration is straightforward. Note that the RFC advises that care must be taken to ensure that both headers convey the same result. Of course, SPF Milter ensures that already.
/etc/spf-milter.conf
:
header = Received-SPF, Authentication-Results
SPF may also be used as part of a DMARC verification setup. Domain-based Message Authentication, Reporting, and Conformance (DMARC) is specified in [RFC 7489].
Since DMARC will use an SPF result as an input for its own validation procedure, a few adjustments to the default configuration are necessary.
/etc/spf-milter.conf
:
socket = inet:3000@localhost
verify_helo = no
reject_results =
header = Authentication-Results
First, in DMARC only the MAIL FROM identity is of interest; the HELO identity is
not considered. Therefore, this step should be skipped by disabling the
verify_helo
setting.
Second, with SPF being an input to DMARC, one might not want to reject senders
after SPF verification, but instead delegate such an action to a subsequent
DMARC component (though depending on requirements other approaches may make
sense). In the above configuration rejection is disabled with the setting
reject_results=
(the empty set). This causes the milter to record the result
in the header and not to return an SMTP error reply.
Finally, the parameter header
changes the header field from the default
Received-SPF
to Authentication-Results
, since this is the type of header
field that DMARC relies on.
Reject failing HELO identity early. With a bit of imagination, one may still
make use of the HELO identity when SPF is set up for DMARC: a sender with a
failing HELO identity is probably up to no good and may be rejected early. This
requirement can be implemented by including only fail
in the definitive HELO
results. This result is then rejected, but only for the HELO identity. In all
other aspects this configuration behaves like the setup above.
/etc/spf-milter.conf
:
verify_helo = yes
definitive_helo_results = fail
reject_helo_results = fail
reject_results =
Contributions of any kind are welcome. Open an issue for questions, suggestions, bug reports, feature requests, documentation, translations etc. on the issue tracker. In the interest of long-term viability of this project I can eventually also grant you commit access or similar.
Before implementing a new feature, please discuss it first on the issue tracker. Implementation is often the easiest part, designing and motivating a feature is where we spend the most time.
This is free software. Please respect the choice of licence, which is GPLv3+.
Copyright © 2020 David Bürgin
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.