spf-milter 0.0.4

Milter for SPF verification
Documentation
# SPF Milter

**This software is experimental/in development. The configuration interface may
still change, feedback welcome. The initial release is being actively worked on
and will be finalised soon.**

📖 *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* (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.

<!-- TODO pin link target to published version before release -->
<!-- [README.de-CH]: https://gitlab.com/glts/spf-milter/-/blob/0.1.0/README.de-CH.md -->
[README.de-CH]: https://gitlab.com/glts/spf-milter/-/blob/master/README.de-CH.md
[RFC 7208]: https://www.rfc-editor.org/rfc/rfc7208
[viaspf]: https://crates.io/crates/viaspf

## 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`.

The minimum supported Rust version is 1.46.0.

[Rust]: https://www.rust-lang.org
[crates.io]: https://crates.io
[domain]: https://crates.io/crates/domain

### Building

For building from source, the above requirements apply. Additionally, for
running the integration tests the `miltertest` program is required.
(`miltertest` is part of the OpenDKIM software package.)

The default configuration file path can be overridden at build time by setting
the environment variable `SPF_MILTER_CONFIG_FILE`.

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

For those new to SPF Milter, the following section has an introductory guide
that presents in all brevity the core configuration parameters.

### 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:

*   <code>inet:<em>port</em>@<em>host</em></code> or
    <code>inet6:<em>port</em>@<em>host</em></code> for a TCP socket
*   <code>unix:<em>path</em></code> 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:3000@localhost
```

Recall that configuration parameters go in the file `/etc/spf-milter.conf`,
using the syntax shown here and documented in the man 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
non_smtpd_milters = $smtpd_milters
```

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 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 identity verification does not mean disabling MAIL FROM
verification. Rather, the HELO identity is verified *in addition* before the
MAIL FROM identity. Only if it produces a definitive result can MAIL FROM
verification be skipped. It is generally a good idea to leave this enabled,
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. This setting can therefore
improve overall processing.

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. In the 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, instead 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 man page, *spf-milter.conf*(5).

[Postfix]: http://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:3000@localhost
```

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 set of
definitive HELO results, which cause MAIL FROM verification to be skipped, is
configurable with `definitive_helo_results`. As mentioned, this is initially set
to the value `pass, fail` but any other set of ‘definitive’ results may be
chosen instead.

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
again be adjusted freely with parameter `reject_results`. The SMTP error 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, 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. This header is a general-purpose device for
conveying authentication results 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 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 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 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.