diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..485dee6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea diff --git a/README.md b/README.md index b20d731..e748849 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,47 @@ -# role-common-send_only_mta +[//]: # (SPDX-License-Identifier: MIT) +# Role Name -Configure Postfix on Debian as a send-only Mail Transfer Agent \ No newline at end of file +role-common-send_only_mta + +# Description + +Configure Postfix on Debian as a send-only Mail Transfer Agent. + +# Requirements + +Your target machines must be Debian. + +# Role Variables + +Per [defaults/main.yml](defaults/main.yml) this role expects both a recipient e-mail address and login credentials for an SMTP mail submission server to be available in a HashiCorp Vault instance. Feel free to override these variables as needed with host or group vars. If you stick with HashiCorp Vault the default path for SMTP credentials is `settings/comms/e-mail/default/sender`, the default path for a recipient address is `settings/comms/e-mail/default/recipient`. + +## Sender data + +- `addr-spec`: Your sender e-mail address rendered as an [RFC 2822](https://datatracker.ietf.org/doc/html/rfc2822#section-3.4.1) `addr-spec` string such as `noreply@example.com`. +- `credentials-password-sasl-smtp-auth-login`: A password that Postfix will use to log in to the SMTP mail submission server via the `AUTH LOGIN` SMTP SASL authentication mechanism. +- `credentials-username`: The SMTP username needed to log in to the SMTP mail submission server. +- `submission-server-fqdn`: Your upstream SMTP mail submission server's fully qualified domain name such as `smtp.example.com`. +- `submission-server-port`: The TCP port you want Postfix to use to connect to the SMTP mail submission server. + +## Recipient data + +- `addr-spec`: Same as above, a recipient e-mail address rendered as an [RFC 2822](https://datatracker.ietf.org/doc/html/rfc2822#section-3.4.1) `addr-spec` string such as `noreply@example.com`. + +# Dependencies + +None. + +# Example Playbook + +In your `playbook.yml` call it like so: + +``` +- name: 'Awesome playbook' + hosts: all + roles: + - 'role-common-send_only_mta' +``` + +# License + +MIT diff --git a/defaults/main.yml b/defaults/main.yml new file mode 100644 index 0000000..d5172f0 --- /dev/null +++ b/defaults/main.yml @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: MIT +somta__e_mail_default_recipient_addr_spec: '{{ lookup(''hashi_vault'', ''secret=secret/data/settings/comms/e-mail/default/recipient:addr-spec'') }}' +somta__e_mail_default_sender_addr_spec: '{{ lookup(''hashi_vault'', ''secret=secret/data/settings/comms/e-mail/default/sender:addr-spec'') }}' +somta__e_mail_default_sender_credentials_password_smtp_auth_login: '{{ lookup(''hashi_vault'', ''secret=secret/data/settings/comms/e-mail/default/sender:credentials-password-sasl-smtp-auth-login'') }}' +somta__e_mail_default_sender_credentials_username: '{{ lookup(''hashi_vault'', ''secret=secret/data/settings/comms/e-mail/default/sender:credentials-username'') }}' +somta__e_mail_default_sender_submission_server_fqdn: '{{ lookup(''hashi_vault'', ''secret=secret/data/settings/comms/e-mail/default/sender:submission-server-fqdn'') }}' +somta__e_mail_default_sender_submission_server_port: '{{ lookup(''hashi_vault'', ''secret=secret/data/settings/comms/e-mail/default/sender:submission-server-port'') }}' diff --git a/handlers/main.yml b/handlers/main.yml new file mode 100644 index 0000000..def3e82 --- /dev/null +++ b/handlers/main.yml @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: MIT +- name: 'Rebuild Postfix lookup tables' + loop_control: + loop_var: 'somta_postfix_postmap_handler' + label: 'Rebuild ''/etc/postfix/{{ somta_postfix_postmap_handler.lookup_table_source_file }}'' lookup table' + loop: + - { lookup_table_source_file: 'sasl_passwd.j2' } + - { lookup_table_source_file: 'sender_canonical_maps.j2' } + - { lookup_table_source_file: 'smtp_header_checks' } + ansible.builtin.shell: | + postmap '/etc/postfix/{{ somta_postfix_postmap_handler.lookup_table_source_file }}' + listen: 'Ensure that a Mail Transfer Agent is running with newest config' + +- name: 'Rebuild e-mail aliases lookup tables' + ansible.builtin.shell: | + newaliases + listen: 'Ensure that a Mail Transfer Agent is running with newest config' + +- name: 'Restart postfix.service' + ansible.builtin.service: + name: 'postfix.service' + state: 'restarted' + enabled: true + listen: 'Ensure that a Mail Transfer Agent is running with newest config' diff --git a/tasks/main.yml b/tasks/main.yml new file mode 100644 index 0000000..6f4441f --- /dev/null +++ b/tasks/main.yml @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: MIT +- import_tasks: postfix-setup.yml diff --git a/tasks/postfix-setup.yml b/tasks/postfix-setup.yml new file mode 100644 index 0000000..dd3c0b3 --- /dev/null +++ b/tasks/postfix-setup.yml @@ -0,0 +1,78 @@ +# SPDX-License-Identifier: MIT +- name: 'If OS is a Linux flavor install Postfix' + when: ansible_facts['system'] | lower == 'linux' + ansible.builtin.package: + name: + - 'postfix' + - 'postfix-pcre' + state: 'present' + +- name: 'Ensure Postfix lookup table files exist with correct perms' + loop_control: + loop_var: 'somta_postfix_postconf_lookup_table' + label: 'Copy lookup table file ''/etc/postfix/{{ somta_postfix_postconf_lookup_table.file }}'' and set perms' + loop: + - { mode: '0600', file: 'sasl_passwd' } + - { mode: '0644', file: 'sender_canonical_maps' } + - { mode: '0644', file: 'smtp_header_checks' } + ansible.builtin.template: + src: 'etc/postfix/{{ somta_postfix_postconf_lookup_table.file }}.j2' + dest: '/etc/postfix/{{ somta_postfix_postconf_lookup_table.file }}' + mode: '{{ somta_postfix_postconf_lookup_table.mode }}' + notify: + - 'Ensure that a Mail Transfer Agent is running with newest config' + +- name: 'Add e-mail alias for user ''root''' + ansible.builtin.lineinfile: + path: '/etc/aliases' + insertafter: 'EOF' + regexp: '^root:.*' + line: 'root: {{ somta__e_mail_default_recipient_addr_spec }}' + notify: + - 'Ensure that a Mail Transfer Agent is running with newest config' + +# Add our own config block to the end of Postfix' main.cf file. In +# 'ansible.builtin.blockinfile' we use the default 'marker' param '# +# {mark} ANSIBLE MANAGED BLOCK'. We 'insertafter: EOF' so we know for a +# fact that our config block is the bottommost thing in main.cf. The +# next task 'ansible.builtin.replace' uses the marker string as an +# anchor to comment out any duplicate parameters /before/ the marker. +- name: 'Configure Postfix main.cf to SMTP-deliver e-mails to an upstream mail gateway' + ansible.builtin.blockinfile: + block: "{{ lookup('ansible.builtin.template', 'etc/postfix/main.cf.blockinfile.j2') }}" + path: '/etc/postfix/main.cf' + create: true + insertafter: 'EOF' + prepend_newline: true + notify: + - 'Ensure that a Mail Transfer Agent is running with newest config' + +- name: 'In Postfix main.cf comment out params managed by this playbook; Postfix doesn''t like dupes' + loop_control: + label: 'Comment out unmanaged occurrences of param ''{{ item | regex_replace(''^(#\s?)?(?P[^=\s]+)([^\r\n\f]*)'', ''\g'') }}''' +# Look up file content from our main.cf config template file. Split the +# result by line delimiters into a list that contains each line as a +# list item via Python string splitlines() method. Now that we have a +# list apply the Jinja2 'select' filter to it. For each list item filter +# it by using the Jinja2 built-in test 'search' against it to search for +# an occurrence of the equals sign '=' in that list item. When a config +# line (i.e. a list item) does not contain an equals sign we reject it +# thus pruning it from the list. We lastly generate a new list from our +# result, one that only contains lines where an equals sign appears. + loop: '{{ lookup(''ansible.builtin.template'', ''etc/postfix/main.cf.blockinfile.j2'').splitlines() | select(''search'', ''='') | list }}' + ansible.builtin.replace: + path: '/etc/postfix/main.cf' + before: '.*?# BEGIN ANSIBLE MANAGED BLOCK' + # regex_replace each {{ item }}. Instead of one complete line from + # the main.cf template file we only want the name of each parameter; + # that's whatever appears in front of the first equals sign ('=') in + # that line minus any comment markers ('#') we may have put in our + # our main.cf template. Store the param name in a named capture + # group (?P...) - with a capital letter P because this + # behavior is a Python-specific regex extension + # (https://stackoverflow.com/a/10060065) - and lastly reuse + # '\g' as our 'regexp:' string. + regexp: '^(#\s?)?({{ item | regex_replace(''^(#\s?)?(?P[^=\s]+)([^\r\n\f]*)'', ''\g'') }})' + replace: '# \2' + notify: + - 'Ensure that a Mail Transfer Agent is running with newest config' diff --git a/templates/etc/postfix/main.cf.blockinfile.j2 b/templates/etc/postfix/main.cf.blockinfile.j2 new file mode 100644 index 0000000..b93d36c --- /dev/null +++ b/templates/etc/postfix/main.cf.blockinfile.j2 @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: MIT +# Per 'man 5 postconfig': When the same parameter is defined multiple +# times, only the last instance is remembered. +# +# While that's true we still do not want to duplicate params that +# originally exist in main.cf. If we did and inevitably set our params +# to different values than the default main.cf params Postfix tools such +# as mailq/sendmail, postqueue etc. would warn us that we're duplicating +# and reconfiguring params multiple times. Tools would begin reporting +# 'warning: main.cf, line : overriding earlier entry'. This is a +# useful feature for when some config /actually/ goes wrong so we don't +# want to contaminate troubleshooting by forcing this behavior with our +# Ansible-managed config. +# +# For each of the params below we've gone ahead and commented out the +# same param where it appeared before this Ansible-managed config block. +# That way we're avoiding param duplicates. +myhostname = {{ ansible_fqdn }} +mydestination = $myhostname, localhost.$mydomain, localhost +inet_interfaces = loopback-only +relayhost = [{{ somta__e_mail_default_sender_submission_server_fqdn }}]:{{ somta__e_mail_default_sender_submission_server_port }} +sender_canonical_classes = envelope_sender, header_sender +sender_canonical_maps = pcre:/etc/postfix/sender_canonical_maps +smtp_header_checks = pcre:/etc/postfix/smtp_header_checks +smtp_sasl_auth_enable = yes +smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd +smtp_sasl_tls_security_options = noanonymous +smtp_tls_security_level = encrypt +# debug_peer_list = example.com, mail.example.net, 1.2.3.4 diff --git a/templates/etc/postfix/sasl_passwd.j2 b/templates/etc/postfix/sasl_passwd.j2 new file mode 100644 index 0000000..838e43f --- /dev/null +++ b/templates/etc/postfix/sasl_passwd.j2 @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: MIT +[{{ somta__e_mail_default_sender_submission_server_fqdn }}]:{{ somta__e_mail_default_sender_submission_server_port }} {{ somta__e_mail_default_sender_credentials_username }}:{{ somta__e_mail_default_sender_credentials_password_smtp_auth_login }} diff --git a/templates/etc/postfix/sender_canonical_maps.j2 b/templates/etc/postfix/sender_canonical_maps.j2 new file mode 100644 index 0000000..2f285c3 --- /dev/null +++ b/templates/etc/postfix/sender_canonical_maps.j2 @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: MIT +/.+/ {{ somta__e_mail_default_sender_addr_spec }} diff --git a/templates/etc/postfix/smtp_header_checks.j2 b/templates/etc/postfix/smtp_header_checks.j2 new file mode 100644 index 0000000..b780049 --- /dev/null +++ b/templates/etc/postfix/smtp_header_checks.j2 @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: MIT +# In our 'From:' header reuse any mail display name then append our +# official sender e-mail address. +/^From:[[:space:]]*(.+?)([[:space:]]*<)/ REPLACE From: "${1}" <{{ somta__e_mail_default_sender_addr_spec }}> + +# Hide the sender's IP and user agent in the Received header +# https://wiki.archlinux.org/title/Postfix +/^Received:.*/ IGNORE +/^User-Agent:.*/ IGNORE