Root cause of the Jun 2026 deliverability collapse / 'no new sales': opendkim.conf was in single-key mode with no InternalHosts, so it signed only 127.0.0.1. Transactional/cron mail (injected locally) was signed, but ALL campaign mail -- injected over the Docker bridge from the Listmonk containers (172.18.0.5 trucking, 172.18.0.25 healthcare) -- went out UNSIGNED. Gmail/Yahoo require DKIM on bulk mail since Feb 2024, so cold campaigns were junked/blocked (~23% delivery, 550-5.7.1). Proof: 2,620 campaign msgs that day, 0 DKIM sigs. The correct table files already existed on the server but were never wired into opendkim.conf. Fix points the daemon at key.table/signing.table and sets InternalHosts/ExternalIgnoreList to trusted.hosts (which includes 172.16.0.0/12, the Docker subnet). Fixes BOTH streams: HC submission ports 2526-2528 inherit the global smtpd_milters and *@performancewest.net covers compliance@. Verified by injecting from a Docker IP through port 25 and port 2526 -- both now get 'DKIM-Signature field added'. Codified as new Ansible role 'mail' so it can't silently regress (OpenDKIM was previously not in IaC at all).
22 lines
1,021 B
YAML
22 lines
1,021 B
YAML
---
|
|
# OpenDKIM signing for outbound mail (Postfix milter).
|
|
#
|
|
# CRITICAL: campaign mail is injected into Postfix from the Listmonk containers
|
|
# over the Docker bridge network, NOT from localhost. OpenDKIM only signs mail
|
|
# whose client is in InternalHosts; if the Docker subnet is missing there,
|
|
# OpenDKIM *verifies* (rather than *signs*) campaign mail, so every cold email
|
|
# goes out UNSIGNED. Since Feb 2024 Gmail/Yahoo require DKIM on bulk mail, so
|
|
# unsigned campaigns get junked/blocked (this caused the Jun 2026 deliverability
|
|
# collapse: ~23% delivery, Gmail 550-5.7.1). The Docker subnet below MUST be in
|
|
# opendkim_internal_hosts.
|
|
opendkim_selector: mail
|
|
opendkim_signing_domain: performancewest.net
|
|
opendkim_socket: "inet:8891@localhost"
|
|
|
|
# Hosts OpenDKIM will SIGN for (vs verify). Must include the Docker bridge
|
|
# subnet so Listmonk container traffic is signed.
|
|
opendkim_internal_hosts:
|
|
- "127.0.0.1"
|
|
- "localhost"
|
|
- "172.16.0.0/12" # Docker bridge networks (Listmonk, workers, etc.)
|
|
- "10.0.0.0/8"
|