# SPF Milter
📖 *Deutsche Version [hier][README.de-CH]. (German version
[here][README.de-CH].)*
SPF Milter is a milter application that verifies email senders using the *Sender
Policy Framework* 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 FROM` 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 parameters 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.
[README.de-CH]: https://codeberg.org/glts/spf-milter/src/tag/0.7.0-alpha.1/README.de-CH.md
[RFC 7208]: https://www.rfc-editor.org/rfc/rfc7208
[viaspf]: https://crates.io/crates/viaspf
[domain]: https://crates.io/crates/domain
## 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 discussed in the following sections, the default, compiled-in configuration
file path is `/etc/spf-milter.conf`. When building SPF Milter, this default path
can be overridden by setting the environment variable `SPF_MILTER_CONFIG_FILE`
to the desired path.
Once built, installation can be achieved by copying `spf-milter` to `/usr/sbin`;
`spf-milter.service` to `/etc/systemd/system`; and `spf-milter.conf` to `/etc`.
After reviewing the configuration, start the service by executing `systemctl
enable --now spf-milter`.
Installation of manual pages can be done in the same way by simply copying:
```
cp spf-milter.8 /usr/local/share/man/man8/
cp spf-milter.conf.5 /usr/local/share/man/man5/
mandb
```
The minimum supported Rust version is 1.74.0.
[Rust]: https://www.rust-lang.org
[crates.io]: https://crates.io/crates/spf-milter
## 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`, `Group`, 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 errors and
warnings the verification result for each verified identity is written to the
log.
[`spf-milter.conf`]: https://codeberg.org/glts/spf-milter/src/tag/0.7.0-alpha.1/spf-milter.conf
[`spf-milter.service`]: https://codeberg.org/glts/spf-milter/src/tag/0.7.0-alpha.1/spf-milter.service
## 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 manual page [*spf-milter.conf*(5)] serves as the reference
documentation. (You can view the manual page without installing by passing the
file’s path to `man`: `man ./spf-milter.conf.5`)
Configuration can be reloaded from disk during operation by sending the signal
`SIGHUP` to the milter process. Refer to the manual page for details.
For those new to SPF Milter, the following section has an introductory guide
that presents in all brevity the core configuration parameters.
[*spf-milter.conf*(5)]: https://codeberg.org/glts/spf-milter/src/tag/0.7.0-alpha.1/spf-milter.conf.5
### Getting started
Let’s take two minutes to run through an initial setup of SPF Milter.
The first step is picking and configuring the listening socket of the milter for
the connection from the MTA. This purpose is served by the **`socket`**
parameter. This parameter’s value should be a socket specification in one of two
forms:
* inet:host:port
for a TCP socket
* unix:path
for a UNIX domain socket
And here is how you set it in the configuration file:
```
# A TCP socket listening on port 3000:
socket = inet:localhost:3000
```
Recall that configuration parameters go in the file `/etc/spf-milter.conf`,
using the syntax shown here and documented in the manual page. And of course,
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
```
With this set up, the milter started and the Postfix configuration reloaded, SPF
Milter will begin processing messages as they arrive.
Various aspects of SPF Milter operation can be adjusted. The main configurable
facet of the verification procedure is whether to also verify the HELO identity:
this is controlled with the Boolean parameter **`verify_helo`**:
```
# Also verify HELO before MAIL FROM:
verify_helo = yes
```
Enabling HELO verification does not mean disabling MAIL FROM verification.
Rather, the HELO identity is verified *in addition* before the MAIL FROM
identity. Provided that the HELO identity is of interest at all, leaving this
enabled may be advantageous, because, as noted in section 2.3 of RFC 7208,
processing of the HELO identity is usually simpler than the more complex MAIL
FROM identity, often using a less complex SPF policy and therefore less DNS
resources.
Sender identities that evaluate to a negative authorisation result or an error
result may be rejected at the SMTP level, by having the milter respond with a
transient or permanent SMTP error reply. The set of SPF results to reject is
declared with the **`reject_results`** parameter:
```
# Reject senders that evaluate to one of the following results:
reject_results = fail, temperror, permerror
```
The value lists the SPF results, separated by commas. Above, messages from a
sender with SPF result *fail*, for example, are rejected, while those with
result *softfail* would be accepted.
Messages from accepted senders, instead of being rejected, have the result
recorded in a new entry added to the message header. The type of header field
can be configured with the **`header`** parameter. The values `Received-SPF` and
`Authentication-Results` each select the header field of the same name. For
example:
```
# Add a ‘Received-SPF’ header to accepted messages:
header = Received-SPF
```
And with this we leave you to experiment on your own.
While experimenting with the configuration you do not need to restart SPF
Milter, just send it the reload signal. An explanation of this and of many more
settings can be found in the manual page, *spf-milter.conf*(5).
[Postfix]: https://www.postfix.org
## Use cases
To make the above information more practical, this section discusses in more
detail 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:localhost:3000
```
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 evaluates to a result that is neither in the set of results to be
rejected, nor in the set of ‘definitive’ HELO results, next the MAIL FROM
identity is verified. The above two settings, `reject_helo_results` and
`definitive_helo_results`, allow tuning precisely which HELO results may
shortcut or bypass MAIL FROM identity verification. By default, the same set of
results is rejected for both the HELO and MAIL FROM identity, and no HELO result
is ‘definitive’.
For either the HELO or MAIL FROM identity a final outcome results. This is then
enforced in line with the suggestions in RFC 7208. The results *fail*,
*temperror*, and *permerror* are 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 set of results to reject can be
adjusted freely with parameters `reject_results` and `reject_helo_results`. The
SMTP reply code and text can also be configured for each SPF result
individually.
For the header entry, by default a header field of type *Received-SPF* is
generated. The parameter for configuring the header field type is named `header`
(`header = Received-SPF`). Both header types of RFC 7208 are ‘standard’, but
they serve different purposes: *Received-SPF* provides full information about
input parameters and additional information about the result, whereas
*Authentication-Results* only conveys the result itself. Here as elsewhere, SPF
Milter aims for perfect compliance with specifications, especially RFC 7208 and
8601, but also those referenced therein. When in doubt, refer to the RFCs.
#### Variants
**Treat `softfail` as a failing result.** A very strict setup may wish to treat
`softfail` with the same severity as a `fail` result, and reject it. To
implement this, simply add `softfail` to the results to be rejected.
`/etc/spf-milter.conf`:
```
reject_results = fail, softfail, temperror, permerror
```
**More information in the header.** SPF Milter supports both of the specified
header fields, and one might want to have them both added to messages.
Configuration using the `header` parameter is straightforward. In addition,
enabling `include_all_results` causes results for all verified identities to be
recorded (both HELO and MAIL FROM).
`/etc/spf-milter.conf`:
```
header = Received-SPF, Authentication-Results
include_all_results = yes
```
### 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:localhost:3000
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
`verify_helo`.
Second, with the SPF result 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 instructs 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. The *Authentication-Results* header is a
general-purpose device for conveying the authentication status for later machine
processing. It was specified more recently in [RFC 8601].
[RFC 7489]: https://www.rfc-editor.org/rfc/rfc7489
[RFC 8601]: https://www.rfc-editor.org/rfc/rfc8601
#### 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 `fail` in the results to be
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
reject_helo_results = fail
reject_results =
```
## Contributing
Contributions of any kind are welcome. Open a ticket on the issue tracker for
questions, suggestions, bug reports, feature requests, documentation,
translations etc. In the interest of long-term viability of this project I can
also grant you commit access.
Before implementing a new feature, please discuss it first on the issue tracker.
Implementation is often the easy part, designing and motivating a feature is
where we find we spend the most time. Compliance with and robust implementation
of RFCs is one of the main accomplishments of SPF Milter vis-Ã -vis similar
software; please do consult the RFCs.
This project is developed as free software under a GPL licence. Please respect
this choice of licence.
## Licence
Copyright © 2020–2024 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.
This program is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
this program. If not, see .