> ## Documentation Index
> Fetch the complete documentation index at: https://blog.onetsolutions.net/llms.txt
> Use this file to discover all available pages before exploring further.

# SPF, DKIM, DMARC and Reverse DNS: Getting Your VPS Mail Out of the Spam Folder

> The four checks Gmail and others run before accepting your mail, how to pass them on a Debian or Ubuntu VPS, and how to prove it with dig.

*Published on September 24, 2026*

Since February 2024, Gmail has required **every** sender to its personal accounts to authenticate with SPF or DKIM, to have valid forward and reverse DNS for the sending address, and to use TLS. Above 5,000 messages a day, it takes SPF **and** DKIM, plus DMARC. Yahoo published rules along the same lines. Since November 2025, Google no longer just files non-compliant mail as spam: it applies temporary and permanent rejections.

A mail server on a VPS is no exception. Here are the four records that make the difference, in the order to set them, and how to check each one.

## What the receiving server checks

| Check       | Question asked                                                                                 | Record                           |
| ----------- | ---------------------------------------------------------------------------------------------- | -------------------------------- |
| Reverse DNS | Does the connecting IP have a name, and does that name point back to it?                       | `PTR` on the IP, `A` on the name |
| SPF         | Is this IP allowed to send for this domain?                                                    | `TXT` on the domain              |
| DKIM        | Was the message signed by the domain, and left unaltered?                                      | `TXT` on `<selector>._domainkey` |
| DMARC       | Does the domain in the visible `From:` match the one that passed SPF or DKIM, and what if not? | `TXT` on `_dmarc`                |

The examples below use `example.com`, the server `mail.example.com` and the address `203.0.113.42`. Substitute your own.

## 0. First: outbound port 25

None of this matters if your server cannot reach anyone else. Many providers block outbound port 25 by default. We do: outbound ports **25, 465 and 587** are closed on every new VPS, and open on request for reasonable use (a personal mail server, application notifications), never for bulk sending. The details are in the [network documentation](https://help.onetsolutions.net/vps/network#email-ports).

Test from the VPS:

```bash theme={null}
# Should report "succeeded" or "open"; a timeout means it is blocked
nc -vz -w 5 gmail-smtp-in.l.google.com 25
```

## 1. Hostname, A record and reverse DNS

Three values must agree: the name your server announces in its `HELO`, the `A` record for that name, and the reverse DNS (`PTR`) of the IP address.

```bash theme={null}
# The name Postfix announces in its HELO
postconf myhostname
# expected: myhostname = mail.example.com
```

If it is wrong, fix it:

```bash theme={null}
sudo postconf -e 'myhostname = mail.example.com'
sudo systemctl reload postfix
```

On the DNS side you need an `A` for `mail.example.com` pointing to `203.0.113.42`, and the domain's `MX` pointing to `mail.example.com`. The reverse DNS does not live in your zone: the IP belongs to your provider, and only they can set it. With us, it is set from the console; the steps are in the [Reverse DNS documentation](https://help.onetsolutions.net/vps/reverse-dns).

<Tip>
  If we host your domain's DNS zone, the **Domain** tab on your instance, profile **A website and email**, writes the `mail` `A` record, the `MX` (priority 10 to `mail.example.com`), the SPF and DMARC records described below, and sets the IP's reverse DNS. A preview shows everything before it is written, including your current `MX` records if they are about to be replaced. See [the announcement](/en/domain-linked-to-vps) and the [documentation](https://help.onetsolutions.net/vps/redirect-my-domain). Only DKIM is left to you: section 3.
</Tip>

Check that forward and reverse agree:

```bash theme={null}
dig +short -x 203.0.113.42
# expected: mail.example.com.

dig +short A mail.example.com
# expected: 203.0.113.42

dig +short MX example.com
# expected: 10 mail.example.com.
```

## 2. SPF: who may send

A single `TXT` record at the root of the domain. For a server that is both the website and the mail server:

```text theme={null}
example.com.  IN  TXT  "v=spf1 a mx -all"
```

`a` authorises the address in `example.com`'s `A` record, `mx` those of the `MX` hosts, and `-all` states that nothing else sends for this domain. This is the record the console's email profile writes.

If another service also sends on your behalf (hosted mailbox, newsletter tool), add its `include:` mechanism to **the same** record, not a second one.

```bash theme={null}
dig +short TXT example.com | grep spf1
# exactly one line expected
```

<Info>
  SPF checks the envelope address (`MAIL FROM`), not the `From:` header your recipient reads. DMARC, section 4, is what ties the two together.
</Info>

## 3. DKIM: sign what leaves

DKIM is the one of the four the console cannot write for you: the private key is generated on your server and must never leave it. You only publish the public key.

On Debian or Ubuntu with Postfix, OpenDKIM does the job:

```bash theme={null}
sudo apt install opendkim opendkim-tools

# 2048-bit RSA key pair, selector "s2026"
sudo opendkim-genkey -b 2048 -d example.com -D /etc/dkimkeys -s s2026 -v
sudo chown opendkim:opendkim /etc/dkimkeys/s2026.private
```

This produces `s2026.private` (the private key) and `s2026.txt` (the DNS record to publish). The selector is any name you like; putting the year in it makes rotation easier later.

In `/etc/opendkim.conf`, set the domain, selector and key, and replace the existing `Socket` line with a local port, which Postfix can reach even from its chroot:

```text theme={null}
Domain      example.com
Selector    s2026
KeyFile     /etc/dkimkeys/s2026.private
Socket      inet:8891@localhost
```

Then plug Postfix into the milter:

```bash theme={null}
sudo postconf -e 'smtpd_milters = inet:localhost:8891'
sudo postconf -e 'non_smtpd_milters = $smtpd_milters'
sudo postconf -e 'milter_default_action = accept'
sudo systemctl restart opendkim postfix
```

Publish the contents of `s2026.txt` as a `TXT` record on `s2026._domainkey.example.com`:

```bash theme={null}
sudo cat /etc/dkimkeys/s2026.txt
```

```text theme={null}
s2026._domainkey.example.com.  IN  TXT  "v=DKIM1; h=sha256; k=rsa; p=MIIBIjANBgkqh..."
```

A 2048-bit key exceeds the 255 characters a single `TXT` string allows, so the file splits it into several quoted strings. Together they form one record, not several. Once published, check that it comes back whole and matches the private key:

```bash theme={null}
dig +short TXT s2026._domainkey.example.com

sudo opendkim-testkey -d example.com -s s2026 -k /etc/dkimkeys/s2026.private -vvv
# expected at the end of the output: "key OK"
```

A `key not secure` line before it only means the zone is not DNSSEC-signed. It does not stop DKIM from working.

## 4. DMARC: tie it together and say what to do on failure

DMARC requires the domain in the `From:` header to align with the one that passed SPF or DKIM, publishes what the receiver should do otherwise, and says where to send you reports.

```text theme={null}
_dmarc.example.com.  IN  TXT  "v=DMARC1; p=quarantine; rua=mailto:postmaster@example.com"
```

* `p=quarantine`: a failing message goes to spam. `p=none` asks for nothing; `p=reject` has it refused.
* `rua=`: the address that receives daily aggregate reports (compressed XML) from the large providers. **That mailbox must exist.**

This is the record the console's email profile sets. It assumes everything sending as you already passes SPF or DKIM. If other services send for your domain and you are not sure of them, start with `p=none`, read the reports for a week or two, then move up.

```bash theme={null}
dig +short TXT _dmarc.example.com
```

## 5. Check end to end

The records can be right and the message still fail. Send a real message to a Gmail address, **through your own Postfix** so it gets signed:

```bash theme={null}
sudo apt install swaks
swaks --server 127.0.0.1 --from test@example.com --to you@gmail.com
```

In Gmail, open the message, the **⋮** menu, **Show original**. You should see `PASS` on all three lines: SPF, DKIM and DMARC. The detail is in the `Authentication-Results` header:

```text theme={null}
Authentication-Results: mx.google.com;
       dkim=pass header.i=@example.com header.s=s2026 ...
       spf=pass (google.com: domain of test@example.com designates 203.0.113.42 as permitted sender) ...
       dmarc=pass (p=QUARANTINE sp=QUARANTINE dis=NONE) header.from=example.com
```

On the server, OpenDKIM's log confirms the signature:

```bash theme={null}
sudo journalctl -u opendkim --since "10 min ago" | grep "DKIM-Signature field added"
```

## Common pitfalls

**Two SPF records.** A domain must carry exactly one. With two `TXT` records starting with `v=spf1`, the result is a permanent error and SPF fails for everyone. Merge them.

**More than ten DNS lookups.** Every `include`, `a`, `mx`, `ptr`, `exists` and `redirect` costs one query, and nested `include`s count too. Past ten, SPF returns a permanent error. Three or four third-party services are enough to get there.

**`-all` or `~all`.** `-all` (fail) says nothing else sends; `~all` (softfail) says it without insisting. Some receivers reject on an SPF fail before even evaluating DKIM, which penalises forwarded mail. If your domain's messages are often forwarded, `~all` with DKIM and DMARC in place is a common compromise.

**DMARC at `p=none` forever.** `p=none` is for watching. It meets the letter of the requirements but does nothing to stop someone spoofing your domain. Set yourself a date to move to `quarantine`.

**Reverse DNS and HELO that disagree.** A generic provider `PTR`, a `PTR` pointing to a name with no matching `A` record, or a `myhostname` left at `localhost` or the install-time name: any one of them is enough to make the receiver suspicious.

**Forgotten IPv6.** If your VPS has an IPv6 address, Postfix will happily use it to reach Gmail, and that address then needs its own `PTR` and must be covered by SPF (the `a` mechanism includes the domain's `AAAA` records). Check with `dig -x` on the IPv6 address. Failing that, tell Postfix to prefer IPv4:

```bash theme={null}
sudo postconf -e 'smtp_address_preference = ipv4'
sudo systemctl reload postfix
```

**The `mail` host with no SPF.** Receivers may also check SPF against the name announced in `HELO`. A `TXT` record `"v=spf1 a -all"` on `mail.example.com` covers it.

## Going further

* [Point a domain to a VPS](https://help.onetsolutions.net/vps/redirect-my-domain): the records the console writes for you.
* [Reverse DNS (PTR)](https://help.onetsolutions.net/vps/reverse-dns): set and check your IP's PTR.
* [Email ports](https://help.onetsolutions.net/vps/network#email-ports): the conditions for opening port 25.
* [Enhanced security on a Linux VPS](/en/enhanced-vps-linux-security) for the layer underneath.

Need port 25 opened, or a hand with the setup? [Our team is available](https://help.onetsolutions.net/console/support) from your client area.
