Skip to content

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 on POST /domains. Selectors are ms<yyyymm> (ms202608), suffixed b, c, … when a month sees more than one key.
  • Rspamd signs (dkim_signing module) any message from an authenticated or local sender whose From: domain has an active key; a domain without a key is delivered unsigned, never rejected. The signature carries d=<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 message dkim: rotated <domain> → <selector>). See Rotate a domain’s DKIM key.
  • GET /domains/{id}/dkim lists 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}/dns includes a TXT <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 published p= 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.com is signed with its own key and selector, whether or not example.com is 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_esld does 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 .env with it: the keys are sealed under its SECRET_ENCRYPTION_KEY.
  • Private keys are sealed at rest, with SecretBox under the purpose dkim.private-key: dkim_key.private_key_pem holds sb1.…, opened only to write rspamd’s key files at apply, so a database dump alone cannot sign as the customer’s domains (a mailctl backup also 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_KEY is 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 warning dkim_unreadable alert 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 .env backup 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, Date and Message-ID are 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 as dkim=fail header.s=ms202609 through SES, with DMARC passing only on SES’s own signature. With a relay configured the render sets sign_headers to 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 covers From, 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’s Authentication-Results at the recipient. The default lists are copied into the engine (templates/rspamd/default-sign-headers.json); CI’s scripts/ci/rspamd-sign-headers.sh fails if the shipped Rspamd’s own defaults stop matching them. ARC seals of forwarded mail follow the same rule (ARC sealing (forwarded mail)).