- infra/ansible/roles/mail: refactor OpenDKIM to support multiple signing domains via opendkim_signing_domains list (root + send.performancewest.net). Loops keygen/ownership/keytable/signingtable so the live two-domain setup is reproducible from ansible. - infra/ansible group_vars: add bulk_mail_subdomain + campaign_from_* + campaign_reply_to documentation vars (map to CAMPAIGN_FROM / HC_CAMPAIGN_FROM env read by the builder scripts). smtp_from (transactional) stays on root. - docs/deliverability.md: rewrite TL;DR with the carrierone-vs-performancewest A/B proof (same server/IPs, different From domain -> Inbox vs Junk) and the ~85% Microsoft / 14% Google / <1% Yahoo audience mix; add the bulk-subdomain section, SPF trim, rehab-disabled, and the Hestia DNS automation runbook.
141 lines
4 KiB
YAML
141 lines
4 KiB
YAML
---
|
|
- name: Install OpenDKIM + tools
|
|
ansible.builtin.apt:
|
|
name:
|
|
- opendkim
|
|
- opendkim-tools
|
|
state: present
|
|
|
|
- name: Ensure OpenDKIM key directory exists
|
|
ansible.builtin.file:
|
|
path: "/etc/opendkim/keys/{{ item.domain }}"
|
|
state: directory
|
|
owner: opendkim
|
|
group: opendkim
|
|
mode: "0750"
|
|
loop: "{{ opendkim_signing_domains }}"
|
|
loop_control:
|
|
label: "{{ item.domain }}"
|
|
|
|
- name: Generate DKIM keypair if missing
|
|
ansible.builtin.command:
|
|
cmd: >-
|
|
opendkim-genkey
|
|
-b 2048
|
|
-d {{ item.domain }}
|
|
-s {{ item.selector }}
|
|
-D /etc/opendkim/keys/{{ item.domain }}
|
|
creates: "/etc/opendkim/keys/{{ item.domain }}/{{ item.selector }}.private"
|
|
loop: "{{ opendkim_signing_domains }}"
|
|
loop_control:
|
|
label: "{{ item.domain }} ({{ item.selector }})"
|
|
register: dkim_keygen
|
|
|
|
- name: Fix DKIM private key ownership
|
|
ansible.builtin.file:
|
|
path: "/etc/opendkim/keys/{{ item.domain }}/{{ item.selector }}.private"
|
|
owner: opendkim
|
|
group: opendkim
|
|
mode: "0600"
|
|
loop: "{{ opendkim_signing_domains }}"
|
|
loop_control:
|
|
label: "{{ item.domain }}"
|
|
|
|
- name: Show DKIM public DNS records to publish (only when newly generated)
|
|
ansible.builtin.debug:
|
|
msg: >-
|
|
A new DKIM key was generated. Publish the TXT record from
|
|
/etc/opendkim/keys/{{ item.item.domain }}/{{ item.item.selector }}.txt
|
|
at {{ item.item.selector }}._domainkey.{{ item.item.domain }}
|
|
loop: "{{ dkim_keygen.results }}"
|
|
loop_control:
|
|
label: "{{ item.item.domain }}"
|
|
when: item is changed
|
|
|
|
- name: Deploy OpenDKIM KeyTable
|
|
ansible.builtin.copy:
|
|
dest: /etc/opendkim/key.table
|
|
content: |
|
|
{% for d in opendkim_signing_domains %}
|
|
{{ d.selector }}._domainkey.{{ d.domain }} {{ d.domain }}:{{ d.selector }}:/etc/opendkim/keys/{{ d.domain }}/{{ d.selector }}.private
|
|
{% endfor %}
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
notify: Restart opendkim
|
|
|
|
- name: Deploy OpenDKIM SigningTable
|
|
ansible.builtin.copy:
|
|
dest: /etc/opendkim/signing.table
|
|
content: |
|
|
{% for d in opendkim_signing_domains %}
|
|
*@{{ d.domain }} {{ d.selector }}._domainkey.{{ d.domain }}
|
|
{% endfor %}
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
notify: Restart opendkim
|
|
|
|
- name: Deploy OpenDKIM trusted/internal hosts (MUST include Docker subnet)
|
|
ansible.builtin.template:
|
|
src: trusted.hosts.j2
|
|
dest: /etc/opendkim/trusted.hosts
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
notify: Restart opendkim
|
|
|
|
- name: Deploy opendkim.conf (table signing + InternalHosts)
|
|
ansible.builtin.template:
|
|
src: opendkim.conf.j2
|
|
dest: /etc/opendkim.conf
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
validate: "opendkim -n -f -x %s"
|
|
notify: Restart opendkim
|
|
|
|
- name: Ensure OpenDKIM is enabled and running
|
|
ansible.builtin.systemd:
|
|
name: opendkim
|
|
enabled: true
|
|
state: started
|
|
|
|
- name: Wire Postfix to the OpenDKIM milter
|
|
ansible.builtin.command:
|
|
cmd: "postconf -e {{ item }}"
|
|
loop:
|
|
- "smtpd_milters={{ opendkim_socket }}"
|
|
- "non_smtpd_milters={{ opendkim_socket }}"
|
|
- "milter_default_action=accept"
|
|
- "milter_protocol=6"
|
|
register: postfix_milter
|
|
changed_when: false
|
|
notify: Reload postfix
|
|
|
|
# Postfix on this host logs via its built-in postlogd (maillog_file mode), not
|
|
# rsyslog -- there is no rsyslog.service. postlogd holds mail.log open, so a
|
|
# plain rename+create leaves it writing to the old inode. Use copytruncate
|
|
# (copy then truncate in place) which needs no daemon signal. mail.log had
|
|
# grown unbounded to ~1 GB (~150 MB/day) with no rotation rule at all.
|
|
- name: Install logrotate rule for Postfix (postlogd) mail logs
|
|
ansible.builtin.copy:
|
|
dest: /etc/logrotate.d/rsyslog-mail
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
content: |
|
|
/var/log/mail.log
|
|
/var/log/mail.err
|
|
/var/log/mail.warn
|
|
/var/log/mail.info
|
|
{
|
|
rotate 14
|
|
daily
|
|
missingok
|
|
notifempty
|
|
compress
|
|
delaycompress
|
|
copytruncate
|
|
}
|
|
|