---
title: Prevent spoofing using DKIM and SPF
lastUpdated: 2025-11-01
slug: ispmail-trixie/anti-spoofing-dkim-spf
sidebar:
  order: 310
---

import { Aside } from "@astrojs/starlight/components";

<Aside type="tip" title="Optional feature">
  This feature is completely optional. Just skip this page if you don't care about villains sending emails in your name.
</Aside>

Email sender spoofing means pretending to be in control of someone else’s email address. This is a common problem with
[phishing](https://en.wikipedia.org/wiki/Phishing). Often scammers send emails with a sender address of
`something@paypal.com` and hope that the recipient falls for it and trusts them. SMTP does not care which sender address
you use. Most mail service providers enforce that you send emails only using your own email address. But some do not.
And bad guys obviously don't care.

On this page I will help you set up the three common measures to prevent spoofing:

- [SPF](https://en.wikipedia.org/wiki/Sender_Policy_Framework) (_Sender Policy Framework_)
- [DKIM](https://en.wikipedia.org/wiki/DomainKeys) (_DomainKeys Identified Mail_)
- [DMARC](https://en.wikipedia.org/wiki/DMARC) (_Domain-based Message Authentication, Reporting and Conformance_)

## Envelope sender or visible sender?

Each email that you receive actually contains two different sender addresses. Understanding the difference is important
when it comes to DKIM and DMARC.

The **envelope sender** is used by SMTP for delivery and bounce messages. It is _not_ usually shown to the user. It
corresponds to the MAIL FROM command in SMTP. If during the transmission the sending server sent
`MAIL FROM:<bounce@example.net>` then this is the envelope sender. It will be added to the header part as
`Return-Path: <bounce@example.net>`. If you think of a written letter, then this is the sender which is written onto the
envelope. The postman will not look inside the envelope. So if the delivery fails, this is the address that the letter
will be sent back to as undeliverable.

The **visible sender** (this is not an official term) is what the recipient sees in their email software. In an actual
email this is the `From: John <john@example.org>` header. If you think of a written letter, then this is the sender as
found in the actual letter within the envelope.

## SPF

SPF defines which servers (IP addresses) are allowed to send emails on behalf of a specific domain.

SPF is essentially a TXT record in your DNS zone that provides machine-readable information for other mail servers. When
an email is received, the receiving server can (and usually does) check for an SPF record for your domain to determine
which IP addresses are authorized to send emails on its behalf.

Let’s take a look at a typical SPF record:

```
"v=spf1 ip4:157.97.194.11 mx ~all"
```

What it means:

1. this is an SPF record of version 1 of the standard (there is currently no other version)
2. please accept emails from the IP address 157.97.194.11
3. also accept emails from any server that is mentioned in our domain’s MX record (the server(s) that receive email for
   your domain)
4. any other email should be considered suspicious – proceed with caution

There are websites like [EasyDMARC](https://easydmarc.com/tools/spf-record-generator) that help you create your SPF
string to add to your DNS domain. Keep in mind:

- You must know which mail servers send email from your domain. Do not forget to include mailing list or newsletter
  services that send in your name.
- Start with "~all" to mark emails as spam that do not meet the criteria. If all goes well switch to "-all" after a few
  weeks.
- Note that forwarding emails from your domain may break SPF because suddenly the email appears to be coming from an IP
  address that is not authorized.

Of course SPF not only helps other mail servers. Rspamd on your mail server will check for other domains' SPF records,
too.

## DKIM

DKIM can prove that an email has not been tampered with on its journey from the sender to the recipient.

DKIM uses _public key cryptography_ to automatically add a _cryptographic signature_ into the header of each outgoing
email. It also uses a TXT record in your domain's DNS zone to publish the public key.

If you run the _sending_ mail server, you need to:

1. Create a **public key** and a **private key** for each domain.
2. Publish the **public key** through a DNS record of your domain. As only an administrator of the sending domain has
   access to the DNS zone, this proves the authenticity.
3. Give your mail server the **private key** so it can **sign** all emails automatically.

On the other hand the _receiving_ mail server needs to:

1. Check if there is a **signature** in an incoming email.
2. Fetch the **public key** from the DNS zone of the sending domain.
3. Make a cryptographic check if the **signature** matches the **public key**.

I don't want to dive deeper into public key cryptography. Take a look at the
[Wikipedia article](https://en.wikipedia.org/wiki/Public-key_cryptography) for a decent explanation.

Obviously your mail server will both _receive_ and _send_ emails. So if you want to use DKIM (which I recommend) then
you need to take care of both parts. But luckily that's not too hard. The _rspamd_ software on your mail server can take
care of both signing outgoing emails and validating incoming signatures.

### Example

Let’s take a look at such a signature in real life. I have just sent an email from GMail to my personal email account on
my own mail server. Google uses DKIM signing so the email got this additional header from Google’s mail servers:

```
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
  d=gmail.com; s=20230601; t=1760017775; x=1760622575; darn=workaround.org;
  h=to:subject:message-id:date:from:mime-version:from:to:cc:subject
    :date:message-id:reply-to;
  bh=HmztzLfHhgZON2788h5cwCX7liAbwFqnFGISA15M18Q=;
  b=OKu5D/Ejp7tGy3uxydb7qDccK03OxrdD9vxEKiAXk/y9T5WViCqpg3HrdeB5Izkzdq
    MXvQ+SgGVzJmy9ZgV1A930RRqUIoR2wmlr3GDXPDyRfagJMRV/3v9J71lfSduOFE8q7t
    AIw4oWBm91QAnC5gW+XS7h4lNmRd4pVILkoe09qkLz+7WrU5Xp/WGIKHRK0ST5+EAR25…
```

I need Google’s DKIM public key to verify that signature. It is stored in their DNS zone as a TXT record of
"**20230601**.\_domainkey.google.com". The "**20230601**" is the key _selector_ that is mentioned in the signature as
"s=**20230601**". You can use multiple keys as long as you create the signatures with the matching private key. The
"\_domainkey" part is the standard subdomain for DKIM keys. So let’s get that TXT record:

```sh title="Run this on your server"
dig +short 20230601._domainkey.google.com txt
```

This returns…

```
"v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4zd3nfUoLHWFbfoPZzAb8bvjsFIIFsNypweLuPe4M+vAP1YxObFxRnpvLYz7Z+bORKLber5aGmgFF9iaufsH1z0+aw8Qex7uDaafzWoJOM/6lAS5iI0JggZiUkqNpRQLL7H6E7HcvOMC61nJcO4r0PwLDZKwEaCs8gUHiqRn/SS3wqEZX29v/VOUVcI4BjaOz" "OCLaz7V8Bkwmj4Rqq4kaLQQrbfpjas1naScHTAmzULj0Rdp+L1vVyGitm+dd460PcTIG3Pn+FYrgQQo2fvnTcGiFFuMa8cpxgfH3rJztf1YFehLWwJWgeXTriuIyuxUabGdRQu7vh7GrObTsHmIHwIDAQAB"
```

That is the public key that I can use to verify the signature. An automated check can be done using the
`opendkim-testmsg` tool as described later. I can run it and paste the entire email including headers and body into it.
If it doesn’t complain with an error message then the signature is correct.

<Aside type="note">

The signature does **not** certify the domain of the sender. In this example the signature is for `d=gmail.com`. But it
could also have been `d=abc.xyz`. A DKIM signature just says:

_This email has been sent by a mail server that has the key for the `gmail.com` domain. And it has not been tampered
with on its way to you._

So it is **not** about the authenticity of the person who sent it to you. Of course it makes sense that the signature
matches the domain of the sending user. Enforcing that will be a job for DMARC which is explained later on this page.

</Aside>

### Creating a keypair

Sounds good? Then let’s implement that for your email domains, too.

If you have skipped the previous section on detecting and filtering out spam emails, then just run these two commands to
tell Postfix to send all emails through rspamd:

```sh title="Run this on your server"
postconf smtpd_milters=inet:127.0.0.1:11332
postconf non_smtpd_milters=inet:127.0.0.1:11332
```

As explained above you need a **private key** that your mail server will use for signing and a **public key** that gets
added to your DNS zone.

If you put your key file into /var/lib/rspamd/dkim/ using a certain naming scheme, `rspamd` will pick it up
automatically:

```sh title="Run this on your server – but use your own domain here"
# Create the directory
mkdir /var/lib/rspamd/dkim

# Fix permissions
chown _rspamd:_rspamd /var/lib/rspamd/dkim

# Create a new keypair (use your own domain here)
cd /var/lib/rspamd/dkim
rspamadm dkim_keygen -d example.org -s 2025100901
```

The **selector** (-s) I chose is **2025100901** because that’s the day I created it. The first (01) key on 2025-10-09.
It doesn’t matter though – you can name it anything you want. If you are lazy and unconcerned you could even use "dkim"
as the selector and later save some work by not needing DKIM maps that define which key is supposed to be used for each
domain. "dkim" is the default selector if you do not use maps. But some day you may want to add new keys so I recommend
you stay flexible and use a custom selector.

The output will contain you both the private key…

```
-----BEGIN PRIVATE KEY-----
MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALhyevPohneucRGQ
LXgqtajXhDgOr9kvoX5igRuChC+j7Fxvwh4SuUphMKLynabAvcUUF951BgVB4PXU
Qoxae7cEkjWATNSiv+Eq0w+AKErdC+BFNDSTk74GZDBBhlroxyudk7QMCZ2nhiOA
/rY1LypaDQzBiRSV8zjcHC8E2Z5lAgMBAAECgYEAodSn9isVGBrwz8Udb+qhOoPl
WT3hogA9hGMu/hEUo6wCrNPNkI9DDXSatM2D7xP7LNhFXQR2BNTZaTcUMIsPMXZ9
157tBVUAcnoRES6BbPyKC7k4dgSXxh687jhSgQWy+OZIU8IbJcoDXXsGjx+6Sb4W
Ilo9NMCF/QqxRFIne70CQQDhNC4YasFOdJyWACxIbFBt+/HhO5+UboUIrZU5nde7
N5HEfnP3Ow2C6vdNnGWiv4YpobN7zjz+lFj19rlWvmHDAkEA0auDQVfIiLEUrKvW
TdhbscpEoYHP+AlM48C+QltKVk+eAnJJHNbBYMVmCmXafgMzFh8QhgG5WXKSlvtz
G/+UtwJAS4QL0PQ7TeucdlAoiUCI19icC0LLCCLnSTmryF49cPH5V0Jv8H9E0Jmp
w8Sl8CT5AAzQ+rr1iQiFrDTHgWblsQJBAJGEtSMrts1QAaZZ2PHMQM+jL4DcZKek
ZM4JXsMVMwG2XlcaVK4EStBMEKcUy9D7jYBD7SbkmWyAPUc2m0yKVP8CQB1zxjA0
Z700vkWGKhwY4DznuDGjpU9rvQw7pyCJ3TYDT3ACQePmGmVeCtyETJ5XvFV2jSqf
o9J+GsdbljSnnSw=
-----END PRIVATE KEY-----
```

…and the public key – in the format of a DNS record:

```
2025100901._domainkey IN TXT ( "v=DKIM1; k=rsa;"
	"p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC4cnrz6IZ3rnERkC14KrWo14Q4Dq/ZL6F+YoEbgoQvo+xcb8IeErlKYTCi8p2mwL3FFBfedQYFQeD11EKMWnu3BJI1gEzUor/hKtMPgChK3QvgRTQ0k5O+BmQwQYZa6McrnZO0DAmdp4YjgP62NS8qWg0MwYkUlfM43BwvBNmeZQIDAQAB" ) ;
```

### Adding the public key to a DNS record

Before you start signing your emails you must make sure that the public key is properly present in your DNS zone for the
domain you are sending emails from. Otherwise the recipient will be unable to verify the signature and may incorrectly
assume that the email was spoofed.

Take a look at the TXT record. It will look something like this:

```
2019122401._domainkey IN TXT v=DKIM1; k=rsa;
```

If you are running your own DNS server you should be able to copy this entire file and put it into your DNS zone.
However if your internet provider offers you just a web interface to manage your domains then create a new TXT record
with a host name of "**2019122401**.\_domainkey" in your domain and put the string within the double-quotes into it as
the value. In my example:

```
2025100901._domainkey IN TXT ( "v=DKIM1; k=rsa;"
	"p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC4cnrz6IZ3rnERkC14KrWo14Q4Dq/ZL6F+YoEbgoQvo+xcb8IeErlKYTCi8p2mwL3FFBfedQYFQeD11EKMWnu3BJI1gEzUor/hKtMPgChK3QvgRTQ0k5O+BmQwQYZa6McrnZO0DAmdp4YjgP62NS8qWg0MwYkUlfM43BwvBNmeZQIDAQAB" ) ;
```

Be aware that the string you got contains two strings "…" + "…" that must be merged into one to work. If you are running
your own name server with `bind` then you can just take that record as is. Other software or your favorite DNS provider
may expect a different syntax. In the end the value of the TXT record needs to be one long string. You can use *dig* to
verify that:

```sh title="Run this on your server"
dig +short 2025100901._domainkey.example.org txt
```

If you get the TXT entry like as follows then you are ready to enable DKIM signing in rspamd for that domain:

```
"p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC4cnrz6IZ3rnERkC14KrWo14Q4Dq/ZL6F+YoEbgoQvo+xcb8IeErlKYTCi8p2mwL3FFBfedQYFQeD11EKMWnu3BJI1gEzUor/hKtMPgChK3QvgRTQ0k5O+BmQwQYZa6McrnZO0DAmdp4YjgP62NS8qWg0MwYkUlfM43BwvBNmeZQIDAQAB"
```

### Adding the private key to rspamd

Take the private key that was created earlier (the multi-line string including "`…BEGIN PRIVATE KEY…`" and
"`…END PRIVATE KEY…`") and put it into a file at the location where rspamd will look for it:

```
/var/lib/rspamd/dkim/example.org.2025100901.key
```

The name of the file has to be  DOMAIN + dot + SELECTOR + ".key" like above. If you name the file incorrectly you will
get an error in your rspamd.log file like "_lua_dkim_sign_handler: cannot load dkim key
/var/lib/rspamd/dkim/example.org.dkim.key_".

Make sure that only the `_rspamd` user can read it:

```sh title="Run this on your server"
chown _rspamd /var/lib/rspamd/dkim/*
chmod u=r,go= /var/lib/rspamd/dkim/*
```

rspamd will automatically pick up the files and does not need to be restarted when you add new keys for further domains.

### Enabling DKIM maps in rspamd

As explained above it is advised to use DKIM **maps**. It’s nothing fancy. Just a simple file defining which selector
you want to use for signing for a certain domain. rspamd will assume that your selector is always `dkim` unless
specified otherwise in a map. Always use a specific selector so that you can later add new keys to DNS without getting
into trouble.

Using maps is simple. First we need to change the *selector_map* setting of the dkim_signing module. Create a new rspamd
configuration file:

```sh title="Run this on your server"
# Tell rspamd to look up selectors in our mapping file
cat > /etc/rspamd/local.d/dkim_signing.conf << 'EOF'
path = "/var/lib/rspamd/dkim/$domain.$selector.key";
selector_map = "/etc/rspamd/dkim_selectors.map";
EOF

# Restart rspamd
systemctl restart rspamd
```

The configuration is simple. rspamd will look for the domain-to-key mapping in the `dkim_selectors.map` file. Create
that file for your own domain and selector:

```sh title="Run this on your server"
# Add the mapping of domain/selector (use your own domain here)
echo example.org 20251000901 > /etc/rspamd/dkim_selectors.map
```

That’s all. rspamd now knows that whenever it sees an outgoing email from `anyone@example.org` it will get the DKIM
private key from /var/lib/rspamd/dkim/**example.org**.**2025100901**.key and use it to sign the email.

### Send a test email

You could either just send an email from your mail client (or Roundcube) through your mail server to another email
address. Or you could use swaks. Use your own addresses and password of course:

```sh title="Run this on your server"
swaks --server localhost:587 \
    --from some.address@example.org \
    --to some.external.address@example.net \
    -tls \
    --auth-user some.address@example.org \
    --auth-password your_secreet_password
```

If you take a look at the received email it should have a DKIM header now like:

```
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.org; s=2019122401; t=1519400838
h=from:subject:date:message-id:to:mime-version:content-type:content-transfer-encoding;
bh=kpYel1IlDvqXEUc0SyIpXbMte3XpQOCXHl+zTyHQvGc=; b=NEUyWUoeKE…
```

To verify the signature install the _opendkim-tools_ package, copy the entire test email (including headers and body),
run *opendkim-testmsg* in your shell and paste the email (finish with CTRL-D).

If you get no output then the signature verified correctly. But if you get something like "opendkim-testmsg: dkim_eom():
Unable to verify" then double-check your DNS record.

You can also use websites like [dkimvalidator.com](http://dkimvalidator.com/) or
[mail-tester.com](https://www.mail-tester.com/) to verify that your signatures are working well.

<details class="collapsible">
  <summary>Click here if you do not get a DKIM signature…</summary>

Enable logging:

```sh title="Run this on your server"
cat > /etc/rspamd/local.d/logging.inc << 'EOF'
debug_modules = ["dkim", "dkim_signing"];
EOF

systemctl restart rspamd

tail -f /var/log/rspamd/rspamd.log
```

Look for any errors. And check that the correct key from `/var/lib/rspamd/dkim` is chosen and used. This is what it
should look like:

```
2025-10-19 10:23:40 #1806716(normal) <ec2f11>; dkim_signing; lua_dkim_tools.lua:185: user is authenticated
2025-10-19 10:23:40 #1806716(normal) <ec2f11>; dkim_signing; lua_dkim_tools.lua:407: use domain(header) for signature: example.org
2025-10-19 10:23:40 #1806716(normal) <ec2f11>; dkim_signing; lua_dkim_tools.lua:427: final DKIM domain: example.org
2025-10-19 10:23:40 #1806716(normal) <ec2f11>; dkim_signing; lua_dkim_tools.lua:48: add selector "2025100901" using selector_map
2025-10-19 10:23:40 #1806716(normal) <ec2f11>; dkim_signing; lua_dkim_tools.lua:53: set key to "/var/lib/rspamd/dkim/$domain.$selector.key" using default path
2025-10-19 10:23:40 #1806716(normal) <ec2f11>; dkim_signing; lua_dkim_tools.lua:53: set domain to "example.org" using dkim_domain
2025-10-19 10:23:40 #1806716(normal) <ec2f11>; dkim_signing; dkim_signing.lua:130: using key "/var/lib/rspamd/dkim/example.org.2025100901.key", use selector "2025100901" for domain "example.org"
```

One caveat is the handling of _subdomains_. `example.org` is a second-level domain. But you may want to use
`service.example.org` for your domain. That's totally fine. But rspamd will strip it down to `example.org` and use the
DKIM key for `example.org`. If that's not what you want, then add this line to `/etc/rspamd/local.d/dkim_signing.conf`:

```
use_esld = false;
```

**eSLD** is a rarely used acronym that stands for _effective second-level domain_. In layman's terms: the part of a
domain that only consists of the rightmost two parts. For `mails.hawkeye.example.com` the eSLD is `example.com`.

</details>

## DMARC

### Example

Let's take an example header of a spoofed mail:

```
Return-Path: <noreply@evil-spammer.com>
DKIM-Signature: d=evil-spammer.com
From: <service@paypal.com>
```

In this example a bad guy just sent you this email. The SPF check for the `evil-spammer.com` domain was successful. (You
can't see that from these headers though.) He even added a DKIM signature from his domain. So everything looks right.
But in your mail program you see `service@paypal.com`. So nobody checks that the _envelop sender_ and the _visible
sender_ "align".

### How it works

That's what DMARC is supposed to solve. It works like this:

- (A) SPF
  - Run a normal **SPF** check (on the _envelope sender_'s domain as usual)
  - Make sure that the domain of this _envelope sender_ **aligns** with (matches) the domain of the _visible sender_
- (B) DKIM
  - Look for a valid **DKIM** signature (also of the _envelope sender_'s domain)
  - Make sure that the domain of the _envelope sender_ **aligns** with (matches) the domain of the _visible sender_
- Make sure that **at least one** of (A) or (B) are true

There are also two flavors of that _alignment_:

- **strict** means that the domains need to match exactly
- **relaxed** means that it can be a subdomain

You can find more examples about the _alignment_ in the [RFC 7489](https://www.ietf.org/rfc/rfc7489.txt) in Appendix
B.1.

### Reporting

Another feature of DMARC is reporting. You may want to know whether your emails had delivery problems. DMARC has two
tags for that purpose:

- **ruf** stands for "**R**eporting **U**RI for **F**ailure data". The email address you use in the "ruf" tag will get
  machine-readable information on every mail that failed the DMARC test. If the receiving mail server supports DMARC, it
  **may** automatically generate and send that email to this address. However most internet providers just ignore "ruf"
  because it contains sensible information on email addresses.
- **rua** stands for "**R**eporting **U**RI for **A**ggregated data". The email address you use in the "rua" tag will
  get statistical data usually once a day. If you want to know if your emails deliver properly, you can use this.

There are commercial offers that allow you to point your "rua" record to their service and show you human-readable
information. Or you can use the tools `dmarc-cat` or `dmarcts-report-parser` that can be installed as Debian packages on
your system.

### Creating a record

If you want to use DMARC (and I suggest that you do), just add a DNS record to your domain. These web sites help you
craft a valid record:

- http://www.kitterman.com/dmarc/assistant.html
- https://dmarcian.com/dmarc-inspector/
- https://www.unlocktheinbox.com/dmarcwizard/
- https://www.agari.com/resources/tools/dmarc

Please note that the DMARC record is a TXT record like `_dmarc.example.com`. That `_dmarc` host name is important.
