DKIM Settings
DKIM Settings
Admin path: Content Checks > DKIM Settings
(view_dkim_settings.cfm, inc/get_dkim_settings.cfm,
inc/dkim_save_settings.cfm, inc/dkim_set_settings.cfm,
inc/dkim_generate_config_file.cfm,
inc/dkim_generate_keytable.cfm,
inc/dkim_generate_signingtable.cfm,
inc/dkim_generate_hosts.cfm,
inc/dkim_generate_domains.cfm,
inc/restart_opendkim.cfm,
inc/generate_postfix_configuration.cfm).
This page controls inbound DKIM verification and the OpenDKIM
runtime configuration that also drives outbound signing.
DKIM (RFC 6376) lets
a sending domain attach a cryptographic signature
(DKIM-Signature: v=1; a=rsa-sha256; d=example.com; s=mail1; ...)
covering selected headers and a hash of the message body; receivers
fetch the public key at <selector>._domainkey.<domain> in DNS and
verify the signature. Unlike SPF, DKIM survives most forwarding —
the signature stays attached to the message and verifies wherever the
body and signed headers remain unchanged.
Per-domain key generation (selector, RSA 1024 / 2048, DNS TXT record
to publish) is managed elsewhere — on the
Email Server Domains page via
edit_domain_dkim.cfm, which writes rows into the dkim_sign table.
This Settings page configures the OpenDKIM daemon's runtime behavior
and maintains the verification-side bypass lists.
Two OpenDKIM instances, one config page
To avoid the body-modification trap that breaks any signer running
after a body-modifying milter, Hermes (issue #232) runs two
separate OpenDKIM instances inside hermes_postfix_dkim:
| Instance | Config | Socket | Mode | Role |
|---|---|---|---|---|
| Primary | /etc/opendkim.conf |
inet:8891@0.0.0.0 |
sv (sign + verify) |
Verifies inbound DKIM at smtpd :25; signs outbound at :587 / :465 (submission ports — pre-Amavis, pre-CipherMail) |
| Sign-only | /etc/opendkim-sign.conf |
inet:8892@127.0.0.1 |
s (sign only) |
Signs at the :10026 re-injection port after Amavis, CipherMail, and the body milter have finished modifying the body. Never adds an Authentication-Results header |
Where DKIM sits in the flow
+--------------------------+
| Remote SMTP peer |
+-----------+--------------+
|
v
+-----------+--------------------------------+
| smtpd :25 (hermes_postfix_dkim) |
| smtpd_milters = inet:127.0.0.1:8891, ... |
| primary OpenDKIM (sv) verifies inbound |
| DKIM-Signature, adds |
| Authentication-Results: dkim=pass/... |
| (consumed downstream by OpenDMARC) |
+-----------+--------------------------------+
|
v
Amavis :10024 (content scoring, CipherMail)
|
v (reinject)
+-----------+--------------------------------+
| smtpd :10026 (post-content, post-body-mod) |
| smtpd_milters = inet:127.0.0.1:8891 |
| sign-only OpenDKIM at :8892 actually |
| signs the final outbound body |
| (KeyTable selects per-domain key by |
| "*@<domain>" SigningTable match) |
+-----------+--------------------------------+
|
v
OpenARC seal (if enabled)
|
v
Outbound to receiver
The actual signing decision happens against the SigningTable:
# /opt/hermes/dkim/SigningTable
*@example.com mail1._domainkey.example.com
*@partner.org k2024._domainkey.partner.org
…joined to the KeyTable:
# /opt/hermes/dkim/KeyTable
mail1._domainkey.example.com example.com:mail1:/opt/hermes/dkim/keys/mail1_example.com.dkim.private
k2024._domainkey.partner.org partner.org:k2024:/opt/hermes/dkim/keys/k2024_partner.org.dkim.private
Both files are regenerated from the dkim_sign table on every
key add / enable / disable / delete on the per-domain page.
The two cards on the page
1. DKIM Settings (master toggle + OpenDKIM runtime controls)
DKIM Enabled flips the child row in parameters whose
parameter matches inet:%:8891 under the smtpd_milters parent
(and the same under non_smtpd_milters). Disabling DKIM here also
disables DMARC, mirroring the SPF-disable behavior — DMARC needs at
least one of the two to align against. The in-page callout warns
about this dependency.
When enabled, nine controls are written to parameters2 rows in the
dkim module, then substituted into the OpenDKIM template at
/opt/hermes/conf_files/opendkim.conf.HERMES:
| Control | OpenDKIM directive | Effect |
|---|---|---|
| Body Canonicalization | Canonicalization (body half) |
relaxed (recommended) ignores trailing whitespace and end-of-line changes; simple requires byte-exact body. Most relays touch line endings, so relaxed is the only practical choice unless you fully control every downstream hop |
| Headers Canonicalization | Canonicalization (header half) |
relaxed lowercases header names and folds whitespace; simple requires headers unchanged. Same reasoning — relaxed survives normal relay reformatting |
| Default Message Action | On-Default |
Catch-all for verification outcomes not covered by the more specific actions below. accept is the recommended default |
| Bad Signature Action | On-BadSignature |
Signature present, present-and-valid in syntax, but verification fails (body or signed-header bytes changed). accept (recommended) lets DMARC + spam scoring make the call |
| DNS Error Action | On-DNSError |
The selector's _domainkey TXT record is unreachable or returned SERVFAIL. accept (recommended) — DNS instability is the sender's problem, not yours; do not block real mail on transient resolver failures |
| Internal Error Action | On-InternalError |
OpenDKIM ran out of resources or hit an unexpected runtime error. accept (recommended) prevents silent mail loss when the verifier itself fails |
| No Signature Action | On-NoSignature |
Message arrived unsigned. Many legitimate senders still don't sign — DMARC enforcement is the correct gate for "must be signed", not this knob. accept (recommended) |
| Security Concern Action | On-Security |
Signature references a weak algorithm or unusually short key. accept (recommended) — score downstream rather than reject at the milter |
| Signature Algorithm | SignatureAlgorithm |
rsa-sha256 (current standard, recommended) or the deprecated rsa-sha1. Many receivers reject rsa-sha1 outright; do not change unless you know why |
Each "Action" option set is: accept, discard, reject,
tempfail, quarantine. The save handler validates that submitted
values are members of this set before writing.
Operational consequence — accept everywhere is intentional. The recommended baseline accepts on every error and every failure condition because DKIM at the milter is not a delivery gate. The verification result is meant to be consumed by DMARC and by spam scoring, not to drop mail. Setting any of these to
rejectmeans a single sender DNS hiccup or a single intermediate relay rewriting a header can cause real mail to bounce. Leave them atacceptand let DMARC enforcement (which considers the sender-published policy) make the discard decision.
2. Whitelisted Domains and Trusted Hosts
Two row-per-entry lists that together drive three OpenDKIM directives:
| Entry type | OpenDKIM directive(s) | File on disk | Table |
|---|---|---|---|
| Whitelisted Domain | ExemptDomains |
/opt/hermes/dkim/ExemptDomains |
dkim_bypass (entry, note) |
| Trusted Host | InternalHosts + ExternalIgnoreList |
/opt/hermes/dkim/TrustedHosts |
dkim_trusted_hosts (host, note) |
Whitelisted Domain exempts the listed sender domain from inbound DKIM verification entirely — OpenDKIM logs the bypass and does not fetch the selector record. Use for known-broken signers whose mail you still need to receive (some legacy mailing-list infrastructure, specific government endpoints with unmaintained selectors).
Trusted Host is dual-purpose. The same entries are written to
both InternalHosts (mail from these hosts is considered locally
originated and will be DKIM-signed on the way out) and
ExternalIgnoreList (mail from these hosts skips inbound DKIM
verification). Accepts IP addresses, CIDR ranges, hostnames, and
bare domain names. The Docker subnet (172.16.32.0/24 by default)
is pre-populated so the post-Amavis re-inject from 127.0.0.1 and
the inter-container hops are correctly treated as internal.
The DataTable supports add (textarea — one entry per line,
deduplicated), inline edit, single delete, and bulk delete; the row
checkboxes carry an id|type composite value so the bulk handler can
route each delete to the right table.
What this page does NOT control
- Per-domain DKIM key generation, selector choice, key size, key
rotation, and the DNS TXT record to publish. Those live on the
Email Server Domains page via
edit_domain_dkim.cfm— one selector / key per domain, stored in thedkim_signtable, written under/opt/hermes/dkim/keys/<selector>_<domain>.dkim.{private,txt}. - The KeyTable and SigningTable content. These are regenerated
from
dkim_signrows on every key change; do not edit them by hand. - ARC sealing. The post-modification chain seal is a separate daemon — see ARC Settings.
- Outbound signing for sub-domains of a signed parent. OpenDKIM's
*@<domain>SigningTable match does not implicitly cover*@sub.<domain>. If you sign forexample.comand needmail.example.comsigned too, generate a separate key for it.
Per-domain key rotation pattern
A working selector-rotation looks like this (operator-side, not a single button on the page):
1. On edit_domain_dkim.cfm, generate a new key with a new selector
(e.g. existing "mail1" -> new "mail2"). Mark NEW key disabled.
2. Publish the new key's TXT record at
mail2._domainkey.example.com in authoritative DNS. The old
mail1._domainkey.example.com record STAYS published.
3. Verify DNS propagation globally.
4. Enable the new key (disables the old one in dkim_sign atomically).
KeyTable + SigningTable regenerate; OpenDKIM reloads.
5. Outbound mail now signs with mail2; mail signed with mail1 while
in flight still verifies because the mail1 TXT record is still
live.
6. Wait through the typical re-delivery window (24-72 hours).
7. Delete the old mail1 row in dkim_sign; remove the
mail1._domainkey.example.com TXT record.
Selectors are arbitrary DNS labels — mail1, 2026q1, hermes,
etc. — and there is no DKIM-defined upper bound on how many you
publish concurrently.
Save flow
1. Validate form fields exist (when enabling DKIM)
- Missing or out-of-set values -> session.m = 20, redirect, no DB write
2. cfinclude dkim_set_settings.cfm
a. UPDATE parameters child rows for the smtpd_milters / non_smtpd_milters
:8891 entries (on or off)
b. UPDATE parameters2 rows for the nine OpenDKIM runtime directives
c. cfinclude dkim_generate_config_file.cfm — read
/opt/hermes/conf_files/opendkim.conf.HERMES, REReplace the
Canonicalization / On-* / SignatureAlgorithm placeholders, write
/etc/opendkim.conf
d. cfinclude dkim_generate_hosts.cfm — regenerate
/opt/hermes/dkim/TrustedHosts from dkim_trusted_hosts
e. cfinclude dkim_generate_domains.cfm — regenerate
/opt/hermes/dkim/ExemptDomains from dkim_bypass
f. cfinclude dkim_generate_keytable.cfm + dkim_generate_signingtable.cfm
— rebuild from dkim_sign
g. cfinclude restart_opendkim.cfm — docker exec inside
hermes_postfix_dkim to restart BOTH opendkim instances
3. cfinclude generate_postfix_configuration.cfm — regenerate main.cf
(smtpd_milters list reflects DKIM on/off) and reload Postfix
4. If DKIM was DISABLED: also flip off OpenDMARC milter rows, clear
FailureReports, deactivate the DMARC report Ofelia job, regenerate
opendmarc.conf, restart OpenDMARC
5. session.m = 9 -> green "DKIM settings saved" alert on redirect
Add / Edit / Delete on the second card calls
dkim_generate_hosts.cfm or dkim_generate_domains.cfm (whichever
applies) plus restart_opendkim.cfm inline — Postfix is not
reloaded since the milter chain itself did not change.
Files and containers touched
| Path | Owner | Role |
|---|---|---|
config/hermes/var/www/html/admin/2/view_dkim_settings.cfm |
hermes_commandbox |
The page |
config/hermes/var/www/html/admin/2/inc/get_dkim_settings.cfm |
hermes_commandbox |
Loads current parameters / parameters2 / bypass / trusted-host values |
config/hermes/var/www/html/admin/2/inc/dkim_save_settings.cfm |
hermes_commandbox |
Validates form, calls set + generate + restart chain; disables DMARC if DKIM off |
config/hermes/var/www/html/admin/2/inc/dkim_set_settings.cfm |
hermes_commandbox |
UPDATEs the parameters / parameters2 rows, regenerates all four config files, restarts OpenDKIM |
config/hermes/var/www/html/admin/2/inc/dkim_generate_config_file.cfm |
hermes_commandbox |
Renders /etc/opendkim.conf from the template + DB |
config/hermes/var/www/html/admin/2/inc/dkim_generate_keytable.cfm |
hermes_commandbox |
Rebuilds /opt/hermes/dkim/KeyTable from dkim_sign |
config/hermes/var/www/html/admin/2/inc/dkim_generate_signingtable.cfm |
hermes_commandbox |
Rebuilds /opt/hermes/dkim/SigningTable from dkim_sign |
config/hermes/var/www/html/admin/2/inc/dkim_generate_hosts.cfm |
hermes_commandbox |
Rebuilds /opt/hermes/dkim/TrustedHosts from dkim_trusted_hosts |
config/hermes/var/www/html/admin/2/inc/dkim_generate_domains.cfm |
hermes_commandbox |
Rebuilds /opt/hermes/dkim/ExemptDomains from dkim_bypass |
config/hermes/opt/hermes/conf_files/opendkim.conf.HERMES |
hermes_commandbox (read) → hermes_postfix_dkim (live /etc/opendkim.conf) |
Template with HEADER-CANONICALIZATION, BODY-CANONICALIZATION, DEFAULT-ACTION, etc. placeholders |
config/postfix-dkim/etc/opendkim-sign.conf |
hermes_postfix_dkim |
Static config for the sign-only instance at :8892 (no placeholders — relaxed/relaxed + rsa-sha256 are fixed for the re-injection signer) |
parameters table (inet:%:8891 rows under smtpd_milters and non_smtpd_milters) |
hermes_db_server (hermes DB) |
DKIM milter on/off |
parameters2 table (rows where module='dkim') |
hermes_db_server (hermes DB) |
The nine OpenDKIM runtime settings |
dkim_sign, dkim_bypass, dkim_trusted_hosts tables |
hermes_db_server (hermes DB) |
Per-domain keys, exempt-domain list, trusted-host list |
hermes_postfix_dkim container |
— | Runs both OpenDKIM instances and hosts the live config + key files |
hermes_unbound container |
— | Resolves every <selector>._domainkey.<domain> lookup |
Failure semantics
| Failure | Behavior |
|---|---|
| Missing form fields when enabling DKIM | session.m = 20, redirect, no DB write |
| Out-of-set value submitted for an Action / Canonicalization / Algorithm field | session.m = 20, redirect, no DB write |
| Empty entry on Add | session.m = 13, redirect, no DB write |
| Invalid syntax on Add / Edit | session.m = 17, redirect, no DB write |
| Duplicate entry on Add | session.m = 14, redirect, no DB write |
dkim_generate_config_file.cfm write fails |
Surfaces as cfcatch from the inline include — save aborts |
restart_opendkim.cfm fails |
Same path — Postfix is reloaded anyway in step 3, but DKIM service is left in the prior runtime state |
KeyTable / SigningTable missing because no dkim_sign rows exist yet |
OpenDKIM starts but signs nothing — outbound mail goes out unsigned |
Related
- SPF Settings — the second authentication
service whose result is consumed by DMARC; paired conceptually
with DKIM as a "DNS-based outbound sender authentication"
mechanism. SPF checks at envelope
MAIL FROMtime; DKIM checks header signatures afterDATA. DKIM survives forwarding; SPF generally doesn't - DMARC Settings — the policy layer that consumes DKIM (and SPF) results; disabling DKIM here automatically disables DMARC
- ARC Settings — the post-modification chain
seal, which runs after the sign-only OpenDKIM at
:8892so the ARC record covers the final outbound body - Trusted ARC Sealers (M365) — for M365 customers whose downstream verifiers escalate when a Hermes-forwarded message's original DKIM signature breaks against the body-modified bytes
- Perimeter Checks — the SPF / DKIM / DMARC status card on Perimeter Checks links here for the per-service toggle
- Domains (Email Server) — where per-domain DKIM keys are generated, selectors chosen, and DNS TXT records exposed for publication
- Domains (Email Relay) — relay-mode domains can also sign outbound; same per-domain key UX
- Email Policies > Disclaimers —
documents the body milter that modifies outbound bodies before the
sign-only OpenDKIM at
:8892produces the final signature; the two-instance OpenDKIM design exists precisely because of this body modification - DNS Resolver — every
<selector>._domainkey.<domain>lookup flows throughhermes_unbound; resolver mode directly affects DKIM verification reliability - System Certificates — TLS on outbound delivery is independent of DKIM, but receivers that enforce strict transport security may surface DKIM failures more prominently in failure reports