OpenDKIM with Postfix: Signing, Verification and Key Rotation
DKIM signs selected outbound messages and verifies inbound signatures without claiming that a valid signature makes content safe. This lesson treats OpenDKIM signing and verification as one decision point in a longer message path. The configuration is useful only when an operator can show which message entered it, which identity and rule matched, what the next daemon returned, and how a failure is retried or contained.
Prerequisites and inherited lab checkpoint
Lesson 10 must deliver and retrieve a message through LMTP before a signing milter is inserted.
For the OpenDKIM signing and verification lab, use reserved domain example.test, documentation addresses and disposable messages. Preserve postconf -n, postconf -M, package versions, DNS answers and a queue baseline before changing the lab. Keep console access and do not copy credentials, private keys or customer messages into the evidence record.
Install and prepare the required components
sudo dnf info opendkim opendkim-tools
sudo dnf install -y opendkim opendkim-tools
sudo cp -a /etc/opendkim.conf /etc/opendkim.conf.before-nitwings
rpm -q opendkim opendkim-tools- Confirm package availability and ownership in the enabled RHEL repositories before adding a third-party source. Record the repository, signing key fingerprint, version and support lifecycle.
- A package install creates files and service identities; it does not establish safe relay, authentication, delivery or filtering behavior.
- Back up only the files this lesson changes and record modes, owners and SELinux labels so rollback restores more than text.
- Use
systemctl cat, package file lists and local manual pages to identify paths on the installed build instead of assuming a path from another distribution.
Build the OpenDKIM signing and verification configuration and understand every boundary
The following lab configuration keeps ownership and failure behavior visible. Replace reserved identities only after the matching DNS, database, socket or filesystem object has been created.
Socket local:/run/opendkim/opendkim.sock
Mode sv
Canonicalization relaxed/simple
KeyTable refile:/etc/opendkim/KeyTable
SigningTable refile:/etc/opendkim/SigningTable
InternalHosts refile:/etc/opendkim/TrustedHosts
# KeyTable
mail2026._domainkey.example.test example.test:mail2026:/etc/opendkim/keys/example.test/mail2026.private
# SigningTable
*@example.test mail2026._domainkey.example.testValidate the effective configuration before reload, trace one accepted message and one rejected message, and retain the queue ID. A syntactically valid file can still express the wrong trust boundary.
Verify the working path
sudo opendkim-testkey -d example.test -s mail2026 -vvv
sudo opendkim-testmsg < signed-message.eml
sudo postfix check
sudo systemctl restart opendkim
sudo postfix reload
dig +short TXT mail2026._domainkey.example.test- Run the syntax or lookup test before reload. A reload must never be the first parser of a production configuration.
- The positive test proves the intended path. The negative test proves an unauthorized sender, recipient or client is not accidentally accepted.
- Stop the named dependency in the disposable lab and verify temporary failure or controlled bypass matches the documented policy.
- Repeat the accepted path after restart and reboot, then compare effective configuration rather than only source files.
Production decisions before continuing
| Decision | Choose deliberately | Evidence to retain |
|---|---|---|
| Failure policy | A signer outage follows the explicitly chosen milter default action; it must be visible and must never expose the private key. | SMTP transcript, queue state and dependency alert |
| Trust boundary | Trust only the explicitly named local daemon, authenticated identity, internal host or validated result required at this stage. Never infer trust merely because traffic originates on localhost. | Matching client, sender, recipient or daemon identity |
| Secrets and data | Restrict credentials, message samples and keys to the minimum service identity. | Owner, mode, label and secret rotation record |
| Activation | Validate, reload, run positive and negative tests, then watch one complete message. | Syntax output, queue ID and linked log events |
| Rollback | Restore the exact files and map/database state changed by this lesson. | Rollback command and repeated acceptance result |
Place OpenDKIM signing and verification in the message path
DKIM signs selected outbound messages and verifies inbound signatures without claiming that a valid signature makes content safe. SigningTable selects the signing identity; KeyTable binds selector and domain to one protected private key.Understand the component before configuring it
| Layer | Question to answer | Evidence |
|---|---|---|
| Input | Connection, envelope, content or stored-message evidence entering this stage | SMTP transcript, lookup input or message header |
| Decision | SigningTable selects the signing identity; KeyTable binds selector and domain to one protected private key. | Effective configuration and exact matched rule |
| Output | An explicit accept, reject, defer, annotate, route or delivery result | Queue state, delivery status or downstream response |
| Dependency | The named daemon, lookup, socket, DNS record and storage required for the decision | Socket, timeout, journal and controlled outage test |
| Recovery | Can processing resume without duplicate, loss or unauthorized delivery? | Retained queue ID, backup and repeated acceptance |
Build it step by step
- Draw the path. Mark the connection, envelope, content and authenticated identities available at this stage.
- Inventory the effective state. Capture package version, active service, sockets, Postfix parameters, master services and lookup results.
- Prepare one coherent configuration. Substitute documented lab values and verify ownership, mode and SELinux context.
- Validate before activation. Run component syntax checks, map queries and a non-delivering test where supported.
- Exercise three outcomes. Send an intended message, an intended denial and a message while the named dependency is unavailable.
- Trace one queue ID. Join ingress, policy, filtering, routing and final delivery events without relying on subject text.
- Close the change. Restart or reboot where relevant, repeat tests, check queue age and document rollback.
Operate and inspect the component
opendkim-genkey -b 2048 -d example.test -s mail2026
opendkim-testkey -d example.test -s mail2026 -vvv
postconf smtpd_milters non_smtpd_milters milter_default_action
journalctl -u opendkim -u postfix --since '-15 minutes'- Replace sample hostnames, addresses and queue IDs only after resolving them from the lab. Do not paste production identities into a public command transcript.
postconf -nshows non-default global parameters;postconf -Mandpostconf -Pexpose master service and field overrides.- A successful lookup proves only that input. Test present, absent, disabled and dependency-unavailable results separately.
- Use the queue ID as the correlation key. Message subjects and recipient addresses are not unique and may contain sensitive information.
Evidence and acceptance criteria
| Evidence | Healthy result | Failure meaning |
|---|---|---|
| Syntax | All component validators succeed before activation | The running service would parse an unreviewed or invalid state |
| Positive path | A reserved valid message follows the intended path and produces the expected evidence. | The intended message cannot complete this decision point |
| Negative path | A deliberately invalid identity, recipient or content sample is denied or classified at the designed stage. | The configuration may relay, authenticate, route or deliver too broadly |
| Dependency failure | Stopping the dependency produces the documented temporary failure or reviewed bypass and raises an observable signal. | Messages may be lost, permanently rejected or silently bypass controls |
| Persistence | Effective state and tests agree after restart and reboot | Only transient state was changed |
Worked incident: OpenDKIM signing and verification behaves differently from the design
A new selector is published but some resolvers cannot retrieve it. Signing begins too early, so valid outbound mail shows DKIM temperror. Rotation publishes and verifies first, overlaps old and new selectors, then retires the old key after retained mail no longer depends on it.Troubleshooting by symptom
| Symptom | Inspect first | Defensible next action |
|---|---|---|
| Service active but no decision | Socket, effective configuration and queue-linked logs | Find the first layer where the message bypasses the component |
| Every message fails | Syntax, permissions, label, dependency and timeout | Restore the last valid state and retest one fixture |
| Invalid sample passes | Rule order, trusted-source exception and actual input identity | Correct the narrow matching rule and rerun negative tests |
| Intermittent deferral | Dependency latency, process limits, queue age and resource pressure | Fix capacity or failure policy without discarding queued mail |
Unsafe operations and recovery boundaries
- Unsafe: changing several mail-flow stages in one reload makes causality and rollback ambiguous.
- Unsafe: logging full credentials, private keys or customer messages expands the incident boundary.
- Unsafe: deleting or requeueing the whole queue to hide a failure destroys mail or creates duplicates.
Rewritten knowledge checks
Cumulative lab checkpoint
- Capture the inherited checkpoint and state the exact sender, recipient, client address and expected SMTP result.
- Install the required package from a recorded source and save the package/file/service inventory.
- Apply the complete lab configuration, including permissions, socket paths, map generation and service ownership.
- Run syntax and lookup validation, then activate without closing the recovery session.
- Complete positive, negative and dependency-outage tests while retaining queue IDs and UTC logs.
- Restart the participating services, repeat the accepted path and confirm no unexplained deferred mail remains.
- Execute rollback once, prove the previous behavior, then reapply the reviewed state as the checkpoint for the next lesson.