DKIM signing
Purpose. Every message sent through the server is DKIM-signed for its sender domain so receivers (Gmail, Microsoft, anyone enforcing DMARC) can verify it came from this server unaltered. Signing is automatic; the admin’s only job is to publish one TXT record per domain.
Behaviour.
- A domain gets an RSA-2048 signing key the moment it exists: the first domain during setup (
POST /setup/complete), every later one onPOST /domains. Selectors arems<yyyymm>(ms202608), suffixedb,c, … when a month sees more than one key. - Rspamd signs (
dkim_signingmodule) any message from an authenticated or local sender whoseFrom:domain has an active key; a domain without a key is delivered unsigned, never rejected. The signature carriesd=<domain>; s=<selector>. - Rotation (Domains → the domain → DKIM keys → Rotate key, or
POST /domains/{id}/dkim): a new key becomes active immediately and the previous one is retired. Rspamd signs with the new selector from the next message; the retired selector’s TXT record must stay published for 30 days so mail still in transit verifies, after which the admin deletes the key (DELETE /domains/{id}/dkim/{selector}). The active key cannot be deleted, only rotated. An administrator approving an AI copilot proposal to rotate a key does the same thing (configuration messagedkim: rotated <domain> → <selector>). See Rotate a domain’s DKIM key. GET /domains/{id}/dkimlists keys (selector, status, created/retired dates, public key, the DNS name and value). Private keys are never returned by any endpoint and never appear in the configuration history or audit log.GET /domains/{id}/dnsincludes aTXT <selector>._domainkey.<domain>row per key — required for the active key, optional (with the “keep until” date) for retired ones — and DNS verification checks the publishedp=against the stored public key (DNS records & live verification).
Configuration. None. Key material lives in the dkim_key table and is pushed to the rspamd container as apply-time files (/etc/rspamd/dkim/<domain>.<selector>.key, /etc/rspamd/dkim/selectors.map) with every configuration apply. Rotation, domain create and domain delete each add a new server_config version whose message starts with dkim:; the first domain’s key goes out in setup’s initial apply, whose message is setup. The rendered local.d/dkim_signing.conf is static but for one line: with an outbound relay configured it sets sign_headers (below).
Limits & edge cases.
- RSA-2048 only (Ed25519 DKIM is still poorly supported by receivers). Key size and algorithm are not configurable.
- One active selector per domain, a hosted subdomain included:
news.example.comis signed with its own key and selector, whether or notexample.comis hosted here too (measured on the dev stack,DKIM_SIGNED{news.example.test:s=ms202610}with the parent hosted and with it not). The templates’use_eslddoes not override a selector map that names the domain itself. - Rolling back the server configuration does not roll back keys — keys are state, not configuration; the current key set rides along with every apply.
- Recreating the rspamd container (an image upgrade,
docker compose -f /opt/mailserver/compose/compose.yaml --env-file /opt/mailserver/.env up -d --force-recreate rspamd) restores the keys from the stored render at start, and the entrypoint makes them readable by the workers again. Before 2026-09-02 it did not: a recreated container could not open its keys and outbound mail went out unsigned until the next apply, with nothing but an rspamd log line (cannot map key file … Permission denied) to say so. CI now recreates the container and checks (scripts/ci/rspamd-recreate.sh). - A backup restored onto another host brings the keys with the database (
mailctl restore); the next apply writes them into rspamd again. Restore.envwith it: the keys are sealed under itsSECRET_ENCRYPTION_KEY. - Private keys are sealed at rest, with
SecretBoxunder the purposedkim.private-key:dkim_key.private_key_pemholdssb1.…, opened only to write rspamd’s key files at apply, so a database dump alone cannot sign as the customer’s domains (amailctl backupalso carries.env, and with it the key: guard it as you would the server). Rspamd’s own copy on its volume is plain PEM, as it must be to sign. Keys created before this are sealed at the API’s next start, and a key not yet sealed still renders. - If
SECRET_ENCRYPTION_KEYis lost or replaced, no apply is ever refused over a key: every apply also carries containment’s blocked-sender map, which must keep reaching Postfix. A key that cannot be opened is carried forward from rspamd’s live render, so the domain keeps signing, under a warningdkim_unreadablealert naming it. Where rspamd has no copy (a restore onto a new host without the old.env), the domain is left out of signing, so its mail goes out unsigned, under a critical alert. Restoring the old key from the.envbackup resolves either on the next apply; otherwise rotate the domain’s key (Rotate a domain’s DKIM key) and publish its new record. - Signing is only applied to mail from authenticated/local senders; inbound mail is verified, not signed.
- Through an outbound relay,
DateandMessage-IDare not signed. A relay may rewrite headers after Rspamd signs, and Amazon SES overrides both on every message, so a signature over either fails at the recipient: measured at Gmail asdkim=fail header.s=ms202609through SES, with DMARC passing only on SES’s own signature. With a relay configured the render setssign_headersto Rspamd’s default list without those two; without one, Rspamd’s default (both oversigned) applies, and removing the relay restores it on the next apply. It applies to every relay, not only SES, because the server cannot tell which relays rewrite headers, and a signature that fails costs more than two headers’ coverage. The signature still coversFrom,Subject,To, the content headers and the body. A relay that rewrites any other signed header, or the body, still breaks it: check a relayed message’sAuthentication-Resultsat the recipient. The default lists are copied into the engine (templates/rspamd/default-sign-headers.json); CI’sscripts/ci/rspamd-sign-headers.shfails if the shipped Rspamd’s own defaults stop matching them. ARC seals of forwarded mail follow the same rule (ARC sealing (forwarded mail)).