new-site/infra/ansible/roles/mail/defaults/main.yml
justin 4d5901921e mail: fix OpenDKIM not signing campaign mail (Docker-injected) + codify in Ansible
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).
2026-06-17 19:31:19 -05:00

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"