SPF Milter

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.

Installation

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.

Usage

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.

Configuration

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.

Socket

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:

The socket specification is passed to the milter library as-is.

Verification procedure

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.

SMTP reply

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.

Header

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

Use cases

To make the above information more practical, this section presents two common example use cases: ‘standard’ SPF, and SPF as part of DMARC.

Standard SPF

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.

Variants

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 as part of DMARC

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.

Variants

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 =

Contributing

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+.

Licence

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.