Email authentication
SPF, DKIM signing and key rotation, DMARC and ARC in Nixt Server, and the DMARC aggregate and TLS reports it receives and sends.
Email authentication is how receiving servers decide whether a message really comes from the domain it claims, and how you learn what they decided. Nixt Server checks it on every message it receives, signs the mail your people send, and both files and sends the daily reports that tell each side what happened.
At a glance
| Standard | Incoming mail | Outgoing mail | Your part |
|---|---|---|---|
| SPF | Checked; the result is scored and recorded. | Passes when your SPF record names this server or your relay. | Publish the SPF record dns prints. |
| DKIM | Every signature checked; scored and recorded. | Signed with an RSA and an Ed25519 key per domain. | Publish both DKIM records; rotate keys now and then. |
| DMARC | Checked; p=reject failures refused; scored. Counted for aggregate reports. | Passes when SPF or DKIM passes and aligns with the From domain. | Publish a DMARC record; read the reports. |
| ARC | The chain on forwarded mail is checked and recorded. | — | — |
| Reverse DNS | Checked; scored and recorded. | Receivers check your PTR record. | Ask your address provider for a PTR. |
Checks on incoming mail
For every message received on port 25, the server records what it found in one header added to the stored message:
Authentication-Results: mail.example.com;
spf=pass smtp.mailfrom=bounces@sender.example;
dkim=pass header.d=sender.example header.s=s1 header.b=<start of the signature>;
arc=none smtp.remote-ip=198.51.100.7;
dmarc=pass header.from=sender.example policy.dmarc=reject;
iprev=pass policy.iprev=198.51.100.7 smtp.ptr=mx.sender.example
There is one dkim= entry per signature, or dkim=none when there are none. When the envelope sender is empty, SPF is reported against the HELO name as smtp.helo=. Mail apps and Sieve scripts can read the header, and the filter pipeline adds scores for each result; see Spam and malware filtering.
- SPF checks the connecting address against the envelope sender’s domain.
- DKIM checks every signature. A signature whose signing domain is not a valid name counts as a permanent error, not as neutral.
- DMARC finds the policy of the domain in the
Fromheader by walking up the domain tree when the domain has none of its own, as DMARCbis (RFC 9989) describes, and checks whether SPF or DKIM passed for an aligned domain. When theFromheader names addresses in several domains, each domain is checked, and the strictest policy among those that fail applies. - ARC checks the seals forwarders added.
- Reverse DNS checks that the connecting address has a name that resolves back to it.
A message that fails DMARC when the From domain publishes p=reject is refused with 550 5.7.1 Refused by the sender domain's DMARC policy. A p=quarantine failure is not refused; it adds to the score.
Mailing lists and forwarders you trust
A mailing list that tags the subject or adds a footer breaks the original sender’s DKIM signature, and a forwarder’s address is not in the sender’s SPF record. So genuine mail that reaches you through a list can fail DMARC, and be refused when the sender’s domain publishes p=reject. The list’s ARC seal records what it found when the message reached it.
Name the lists and forwarders whose seal you take at their word:
[filter.arc]
trusted_sealers = ["lists.example.org", "googlegroups.com"]
A message that fails DMARC is then let through when its ARC chain validates and its newest seal is one of these. Each name is matched exactly, as the sealer writes d= in its ARC-Seal: trusting lists.example.org does not trust anything else under example.org, and a wildcard is refused. Mail forged in a sender’s name still fails, and the Authentication-Results header says when a trusted sealer let a message through. Nothing is trusted until you name it.
A message with more than one From header is refused with 550 5.7.1 A message has one From header, and one whose From header names more than five domains with 550 5.7.1 The From header names more than 5 domains, whatever the domains publish.
SPF for your domains
dns prints v=spf1 mx -all for a server that delivers directly, and v=spf1 mx include:<relay domain> -all when the configuration has a relay. See DNS records.
Two things to keep in mind:
- If you add a relay later, add it to the SPF record at the same time, or everything you send fails SPF.
- Mail forwarded by a Sieve
redirectis sent with an envelope sender at the parent domain of the server’s host name, so that domain’s SPF record must allow the server too.
DKIM signing
Every domain has signing keys kept in the server’s store. Mail submitted over SMTP is signed with every key the envelope sender’s domain has that is not retired.
- Two algorithms. Each generation makes a pair: a 2048-bit RSA key and an Ed25519 key. Some receivers verify only RSA, so both are used.
- Selectors. The pair shares a selector name, with
radded for the RSA key andefor the Ed25519 key. You choose the name, or it is built from the date the keys were made. - Signed headers.
From,To,Cc,Reply-To,Subject,Date,Message-ID,In-Reply-To,References,MIME-Version,Content-Type,Content-Transfer-Encoding,List-UnsubscribeandList-Unsubscribe-Post. - No private keys leave the store. The API, the command line and the audit log show only the public records.
init makes the first pair for its domain. A domain added through the API has no keys until you make some.
Mail the server writes itself
The server also signs the mail it writes itself, as it leaves for another server: out-of-office replies, Sieve notifications, delivery status notifications, alerts to administrators, and the DMARC and TLS reports it sends. It signs with the active keys of the domain in the message’s From header, or, when no organisation here holds that domain, of the nearest parent domain one does. An out-of-office reply is sent with an empty envelope sender, so SPF is checked against the server’s own name and cannot align with the person’s domain; the signature is what lets it pass DMARC at a receiver whose domain publishes p=reject.
Only the server’s own mail is signed this way:
- Mail submitted by a person is signed once, when it is submitted, and not again.
- Mail passed on — a forward, a Sieve
redirect, mail for a domain whose mailboxes are elsewhere — is somebody else’s message, and is not signed in your organisation’s name. The ARC seal vouches for the forwarding. - A message already signed by its domain is left as it is.
Each attempt signs afresh, and the message kept in the queue is not changed. In the message trace, each step about another server — relayed, deferred or failed — ends with signed by and the domains that signed the attempt. A domain with no keys sends its mail unsigned, and the log says no DKIM key for this domain; its mail goes out unsigned once a day for each such domain.
Making keys through the API
On a running server, use the admin API, which is audited:
vsx admin post tenants/1/domains/example.org/dkim selector=s2026
{
"items": [
{
"algorithm": "rsa",
"createdAt": 1789387200000,
"record": "s2026r._domainkey.example.org. IN TXT \"v=DKIM1; k=rsa; p=<first part>\" \"<rest of the key>\"",
"retired": false,
"selector": "s2026r"
},
{
"algorithm": "ed25519",
"createdAt": 1789387200000,
"record": "s2026e._domainkey.example.org. IN TXT \"v=DKIM1; k=ed25519; p=<key>\"",
"retired": false,
"selector": "s2026e"
}
],
"records": [
"s2026r._domainkey.example.org. IN TXT \"v=DKIM1; k=rsa; p=<first part>\" \"<rest of the key>\"",
"s2026e._domainkey.example.org. IN TXT \"v=DKIM1; k=ed25519; p=<key>\""
]
}
selector is optional. When given, it must be a DNS label — letters, digits and hyphens, at most 62 characters.
| Call | What it does |
|---|---|
vsx admin get tenants/1/domains/example.org/dkim | Lists every key: selector, algorithm, record, when made, and whether retired. |
vsx admin post tenants/1/domains/example.org/dkim [selector=<name>] | Makes a new pair and answers with the records to publish. The existing keys keep signing. |
vsx admin delete tenants/1/domains/example.org/dkim/<selector> | Retires one key: it signs nothing more. It stays in the store. |
Making keys from the command line
versealx-server dkim opens the store directly. Use it when the server is not running:
sudo -u versealx versealx-server dkim generate example.com example.org s2026 --config /etc/versealx-server/versealx-server.toml
sudo -u versealx versealx-server dkim show example.com example.org --config /etc/versealx-server/versealx-server.toml
The arguments are the tenant’s name, the domain, and optionally the selector. generate prints the records and ends with Publish the records above, then check with: versealx-server dkim show <tenant> <domain>. show prints every key’s record, marking retired ones ; retired, or no keys; generate some with: versealx-server dkim generate <tenant> <domain>.
| Error | Cause |
|---|---|
no tenant named <name> | The first argument must be the tenant’s name, not its number. |
<domain> is not a domain of <tenant> | The domain belongs to another tenant, or to none. |
usage: dkim <generate|show> <tenant> <domain> [selector] | Missing arguments. |
Rotating keys
- Make a new pair with a new selector:
vsx admin post tenants/1/domains/example.org/dkim selector=s2027. - Publish both new records. Leave the old ones in place.
- Run
doctoruntil both new selectors report… is the key this node signs with. Until you retire the old keys, messages carry four signatures, which is harmless. - Retire the old keys, both of them:
vsx admin delete tenants/1/domains/example.org/dkim/s2026rand…/s2026e. - Leave the old records published for a few days, so mail already signed with the old keys can still be verified, then remove them.
versealx-server dnsno longer prints retired keys.
doctor reports BAD for a domain whose keys are all retired: all 2 of this domain's keys are retired, so nothing signs.
DMARC for your domains
dns prints v=DMARC1; p=none; rua=mailto:dmarc@example.com. A sensible path:
- Publish it as printed. Receivers apply nothing and send you daily aggregate reports.
- For a few weeks, read the reports (below). Look for your own mail failing — a system sending as your domain that you forgot, or a relay missing from SPF.
- When your own mail passes, change
p=nonetop=quarantine, then later top=reject.
doctor warns when a domain has no DMARC record.
A brand logo (BIMI)
BIMI lets mail apps that support it, such as Gmail, Apple Mail and Yahoo, show your organisation’s logo beside mail from your domain. The server keeps the logo, serves it from its own host name, and gives you the DNS record that points receivers at it.
Receivers show a logo only for mail that passes DMARC under a policy of quarantine or reject, so finish the DMARC path above first. Many also want a mark certificate (a VMC or CMC) bought from a certificate authority; without one, some receivers still show the logo and others don’t.
The logo must be an SVG in the SVG Tiny Portable/Secure profile, at most 32 KB. The server checks it before keeping it and lists everything that stops it from qualifying. The checks are:
- the root
<svg>hasversion="1.2",baseProfile="tiny-ps"and the SVG namespace, and noxory; - it has a
<title>that isn’t empty; - it uses only the shapes, text, gradients and groups the profile allows;
- it has nothing that runs, animates or links: no scripts, no event attributes, and no reference outside the file.
Design tools can export this profile, and most brand agencies supply it.
To publish it from the console, open the domain and use the Brand logo card: choose the SVG, and the mark certificate if you have one, then Publish the logo. From the command line:
versealx-server admin domain logo example.com logo.svg --evidence vmc.pem
versealx-server admin domain logo-show example.com
versealx-server admin domain logo-remove example.com
Then publish the record the domain’s DNS records now include (see DNS records). The server serves the files at https://mail.example.com/.well-known/bimi/example.com/logo.svg and …/evidence.pem. --selector publishes under a name other than default. The mark certificate may hold certificates only, because it is served to anybody.
Through the API, GET, PUT and DELETE /tenants/{tenant}/domains/{name}/bimi. PUT takes the SVG as logo, and the certificate as evidence in PEM. Every change is in the audit log.
Logos on mail you receive
The server can also check the logos other senders publish, so that mail apps reading the BIMI headers can show them beside their mail. Turn it on with a [bimi] table (see Configuration):
[bimi]
The server ships with the roots of the issuers the BIMI Group lists: DigiCert, GlobalSign and SSL.com. You can add your own, leave the shipped ones out, or withdraw any one of them by its fingerprint, without waiting for a release:
[bimi]
roots = "/etc/versealx-server/bimi-roots" # a PEM file, or a directory of them
distrust = ["CD:12:2C:B8:…"] # SHA-256, as openssl prints it
See the roots in force, each with its fingerprint, expiry and where it came from:
sudo -u versealx versealx-server trust roots bimi
versealx-server doctor warns a year before a trusted root expires, when no root is trusted, and when a distrust entry matches none.
A logo is shown only when every one of these holds:
- the message passed DMARC under a policy of
quarantineorreject, with nopctbelow 100, not in testing (t=y), and not undersp=nonefor a subdomain; - the sender’s BIMI record names a mark certificate (a VMC or CMC), which chains to one of your roots, is in date, names the sender’s domain, and is issued for BIMI;
- the logo the record names is the one inside the certificate, and passes the SVG Tiny PS checks.
The server then adds BIMI-Location and BIMI-Indicator headers to the delivered message. A logo without a mark certificate is never shown, so a domain that passes DMARC cannot show somebody else’s logo. Headers by those names that a sender wrote are always removed on arrival. Each message records BIMI_PASS, BIMI_FAIL with the reason, or BIMI_NONE in its filter evidence, adding nothing to the score. The result is also added to the server’s Authentication-Results header as bimi=pass (with the domain and selector of the record used and the certificate’s address), bimi=fail, bimi=none, bimi=declined, or bimi=skipped for a message that was not checked. A verified logo is remembered for a day. Where an edge checks arriving mail and passes it on to the node behind it, the logo the edge verified is kept on the message; logo headers from anywhere else are removed.
ARC
ARC lets a forwarder vouch for the authentication results a message had before it was forwarded. The server checks the ARC chain on incoming mail and records the result as arc= in Authentication-Results.
Reports you receive
Receivers that honour your records send reports to the addresses those records name. Every domain on the server accepts mail at dmarc@<domain> and tlsrpt@<domain> without an account, and the server reads the reports out of those messages as they are delivered.
- DMARC aggregate reports say, per sending address, how much mail claiming to be from your domain a receiver saw, and whether it passed SPF, DKIM and DMARC.
- TLS reports say how many deliveries to your server succeeded or failed to set up TLS, and why they failed.
The attached report may be gzip-compressed, in a ZIP file, or plain. Reports are filed under the domain and the day their period ends. A report about a domain other than the one it was sent to is not filed, and nor is a report sent to any other address. At most 8 MiB of one report and 10,000 of its rows are read.
Reading reports
vsx admin get reports domain=example.com day=2026-09-13
vsx admin get reports domain=example.com day=2026-09-13 kind=dmarc
| Parameter | Required | Meaning |
|---|---|---|
domain | Yes | The domain the reports are about. It must belong to the tenant. |
day | Yes | YYYY-MM-DD: the day the reports’ periods end on. |
kind | No | dmarc or tls. Both when left out. |
Each report in items carries:
| Field | Meaning |
|---|---|
kind | dmarc or tls. |
domain | The domain it is about. |
org | The organisation that sent it. |
reportId | The sender’s id for the report; quote it if you ask them about it. |
begin, end | The period covered, in Unix seconds. |
contact | How to reach the sender, when the report says. |
messages | DMARC: how many messages the report covers. |
unauthenticated | DMARC: how many of those passed neither DKIM nor SPF — the number to look at first. |
rows | DMARC: one row per sending address, with source_ip, count, disposition, dkim, spf and header_from. |
successful, failed | TLS: sessions that set up TLS, and sessions that did not. |
policies | TLS: per policy, policy_type (sts, tlsa or no-policy-found), policy_domain, successful, failed, and failures, each with result_type, sending_mta_ip, receiving_mx_hostname and count. |
Reports you send
Once a day, the server sends the reports other domains ask for about mail your server exchanged with them. It looks every ten minutes for the previous day (in UTC) and sends what is due. On several nodes sharing a store, one node sends them.
DMARC aggregate reports
- About: mail your server received claiming to be from a domain whose DMARC record asks for aggregate reports (
rua=). - Contents: per sending address, how many messages, the DMARC result, whether DKIM and SPF aligned, and what the server did — delivered, quarantined or refused because of the policy. The format is DMARCbis aggregate reporting (RFC 9990), gzip-compressed.
- To: the
mailto:addresses in the domain’s record. An address in another domain receives the report only when that domain publishes its consent at<reported domain>._report._dmarc.<its domain>. - From:
DMARC Reporting <postmaster@mail.example.com>, with the subjectReport Domain: <domain> Submitter: <submitter> Report-ID: <id>.
TLS reports
- About: the connections your server made when delivering mail to a domain that publishes a
_smtp._tlsrecord. - Contents: per policy (DANE, MTA-STS or none), how many sessions succeeded and failed, and why.
- To: the
mailto:addresses in the domain’s TLS reporting record. - From:
TLS Reporting <postmaster@mail.example.com>, with the subjectReport Domain: <domain> Submitter: <submitter> Report-ID: <id>.
Reports leave through the normal queue, so they appear in the queue and the message trace like any other outgoing mail.
TLS reporting and MTA-STS for your domains
MTA-STS and TLS reporting protect mail sent to you. The usual path:
- Publish the
_mta-stsand_smtp._tlsrecords and themta-stsCNAME, with the policy intestingmode (whatinitsets). - Mark the domain verified, so the policy is served.
- Read the TLS reports for a few weeks. Check that
failedstays at zero for your MX hosts. - Switch to
enforcefor the tenant without a restart:vsx admin patch tenants/1/settings mta_sts.mode=enforce. Change theidin the_mta-stsrecord at the same time, so senders fetch the new policy.
See DNS records and Runtime settings.
Something unclear or out of date on this page? Tell us.