diff --git a/CHANGELOG.md b/CHANGELOG.md index 0798200f..8502e8ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* **role:postfix**: Add `postfix__recipient_canonicals__group_var` / `__host_var` to rewrite recipient addresses via Postfix's `recipient_canonical_maps`, mirroring the existing sender canonical rewriting. Useful for redirecting all mail addressed to a host to a central team mailbox. * **all roles**: Add `lfops__skip_restart_handlers` to deploy configuration changes without restarting the affected services, for example when a bounce has to wait for a maintenance window. Reload handlers still run, since a reload applies the configuration without an outage. Note that the skipped restart is not remembered: a later ordinary run finds the configuration already correct and does not restart either, so the service has to be restarted explicitly (`--tags :state --extra-vars '__service_state=restarted'`). See the [README](https://github.com/Linuxfabrik/lfops/blob/main/README.md#lfops__skip_restart_handlers). * **role:apache_solr, role:bind, role:blocky, role:chrony, role:clamav, role:collabora, role:coturn, role:fail2ban, role:glpi_agent, role:grafana, role:graylog_datanode, role:graylog_server, role:icinga2_master, role:icinga_kubernetes, role:icingadb, role:influxdb, role:keepalived, role:opensearch, role:php, role:redis, role:snmp, role:squid, role:vsftpd**: Add a service state variable (for example `chrony__service_state`, `clamav__clamd_service_state` or `php__fpm_service_state`) to start, stop, restart or reload the managed service independently of whether it is enabled at boot. It defaults to `started` when the role's matching `*_service_enabled` is `true` and to `stopped` otherwise, so existing inventories keep their current behaviour. * **role:duplicity**: Add Debian and Ubuntu support (proven on Debian 12, Debian 13, Ubuntu 22.04, Ubuntu 24.04 and Ubuntu 26.04). The role now installs the `gnupg` package itself, so backups also work on minimal installs that ship without `gpg`. diff --git a/extensions/molecule/setup_basic/inventory/group_vars/systems_under_test.yml b/extensions/molecule/setup_basic/inventory/group_vars/systems_under_test.yml index c6f5ca51..194e1dde 100644 --- a/extensions/molecule/setup_basic/inventory/group_vars/systems_under_test.yml +++ b/extensions/molecule/setup_basic/inventory/group_vars/systems_under_test.yml @@ -4,7 +4,15 @@ mailto_root__to: monitoring_plugins__version: '5.0.0' +postfix__recipient_canonicals__host_var: + - pattern: '/^.+@example.com$/' + address: 'team@example.com' + state: 'present' postfix__relayhost: 'mail.example.com' +postfix__sender_canonicals__host_var: + - pattern: '/^.+@example.com$/' + address: 'noreply@example.com' + state: 'present' setup_basic__skip_duplicity: true setup_basic__skip_repo_icinga: true diff --git a/extensions/molecule/setup_basic/verify.yml b/extensions/molecule/setup_basic/verify.yml index db9c152f..6f52171f 100644 --- a/extensions/molecule/setup_basic/verify.yml +++ b/extensions/molecule/setup_basic/verify.yml @@ -36,3 +36,31 @@ loop: '{{ __molecule__atd_state_result["results"] }}' loop_control: label: '{{ item["item"] }}' + + - name: 'postconf -h sender_canonical_maps recipient_canonical_maps' + ansible.builtin.command: 'postconf -h sender_canonical_maps recipient_canonical_maps' + changed_when: false + register: '__molecule__postfix_canonical_maps_result' + + - name: 'Assert that postfix loaded the sender and recipient canonical maps' + ansible.builtin.assert: + that: + - '"regexp:/etc/postfix/canonical" in __molecule__postfix_canonical_maps_result["stdout_lines"]' + - '"regexp:/etc/postfix/recipient_canonical" in __molecule__postfix_canonical_maps_result["stdout_lines"]' + + - name: 'postmap -q against the sender and recipient canonical tables' + ansible.builtin.command: 'postmap -q "user@example.com" regexp:{{ item }}' + changed_when: false + register: '__molecule__postfix_postmap_query_result' + loop: + - '/etc/postfix/canonical' + - '/etc/postfix/recipient_canonical' + + - name: 'Assert that the canonical rules rewrite addresses as configured' + ansible.builtin.assert: + that: 'item["stdout"] == __molecule__expected' + vars: + __molecule__expected: '{{ (item["item"] == "/etc/postfix/canonical") | ternary("noreply@example.com", "team@example.com") }}' + loop: '{{ __molecule__postfix_postmap_query_result["results"] }}' + loop_control: + label: '{{ item["item"] }}' diff --git a/roles/postfix/README.md b/roles/postfix/README.md index 2e9c47f0..d6f3f933 100644 --- a/roles/postfix/README.md +++ b/roles/postfix/README.md @@ -214,6 +214,30 @@ postfix__relayhost: 'mail.example.com:587' * Type: Multiline string. * Default: unset +`postfix__recipient_canonicals__group_var` / `postfix__recipient_canonicals__host_var` + +* List of dictionaries for `/etc/postfix/recipient_canonical`, used to rewrite the recipient addresses. +* For the usage in `host_vars` / `group_vars` (can only be used in one group at a time). +* Type: List of dictionaries. +* Default: `[]` +* Subkeys: + + * `pattern`: + + * Mandatory. Regular expression to match the entire recipient address. + * Type: String. + + * `address`: + + * Mandatory. The rewrite address. + * Type: String. + + * `state`: + + * Optional. State of the entry. Either `'present'` or `'absent'`. + * Type: String. + * Default: `'present'` + `postfix__recipient_delimiter` * See https://www.postfix.org/postconf.5.html#recipient_delimiter @@ -406,6 +430,10 @@ postfix__raw: |- # DKIM smtpd_milters = inet:localhost:8891 non_smtpd_milters = $smtpd_milters +postfix__recipient_canonicals__host_var: + - pattern: '/^.+@example.com$/' + address: 'team@example.com' + state: 'present' postfix__recipient_delimiter: '' postfix__relayhost_password: '' postfix__relayhost_username: '' diff --git a/roles/postfix/defaults/main.yml b/roles/postfix/defaults/main.yml index 61380bd0..7094f837 100644 --- a/roles/postfix/defaults/main.yml +++ b/roles/postfix/defaults/main.yml @@ -40,6 +40,17 @@ postfix__maximal_queue_lifetime: '5d' postfix__message_size_limit: 10240000 postfix__mydestination: '$myhostname, localhost.$mydomain, localhost' postfix__myorigin: '$myhostname' +postfix__recipient_canonicals__combined_var: '{{ ( + postfix__recipient_canonicals__role_var + + postfix__recipient_canonicals__dependent_var + + postfix__recipient_canonicals__group_var + + postfix__recipient_canonicals__host_var + ) | linuxfabrik.lfops.combine_lod(unique_key="pattern") + }}' +postfix__recipient_canonicals__dependent_var: [] +postfix__recipient_canonicals__group_var: [] +postfix__recipient_canonicals__host_var: [] +postfix__recipient_canonicals__role_var: [] postfix__recipient_delimiter: '' postfix__relayhost_password: '' postfix__relayhost_username: '{{ mailto_root__from }}' diff --git a/roles/postfix/handlers/main.yml b/roles/postfix/handlers/main.yml index 75b1332f..4b4a637f 100644 --- a/roles/postfix/handlers/main.yml +++ b/roles/postfix/handlers/main.yml @@ -6,6 +6,10 @@ ansible.builtin.command: '/usr/sbin/postmap {{ __postfix__map_type }}:/etc/postfix/canonical' notify: 'postfix: reload postfix' +- name: 'postfix: postmap /etc/postfix/recipient_canonical' + ansible.builtin.command: '/usr/sbin/postmap {{ __postfix__map_type }}:/etc/postfix/recipient_canonical' + notify: 'postfix: reload postfix' + # skip reload if the service was just started (redundant) or if the user wants it stopped. # __postfix__service_state_result is registered on a dedicated state-only task (not the enabled # task), so that enabled-only changes don't prevent the reload. diff --git a/roles/postfix/tasks/main.yml b/roles/postfix/tasks/main.yml index df069b9e..6673fca5 100644 --- a/roles/postfix/tasks/main.yml +++ b/roles/postfix/tasks/main.yml @@ -65,6 +65,14 @@ mode: 0o644 notify: 'postfix: postmap /etc/postfix/canonical' + - name: 'Deploy /etc/postfix/recipient_canonical' + ansible.builtin.template: + backup: true + src: 'etc/postfix/recipient_canonical.j2' + dest: '/etc/postfix/recipient_canonical' + mode: 0o644 + notify: 'postfix: postmap /etc/postfix/recipient_canonical' + - block: - name: 'Deploy /etc/postfix/sasl_passwd' diff --git a/roles/postfix/templates/etc/postfix/Debian-main.cf.j2 b/roles/postfix/templates/etc/postfix/Debian-main.cf.j2 index c71e9316..ac70c326 100644 --- a/roles/postfix/templates/etc/postfix/Debian-main.cf.j2 +++ b/roles/postfix/templates/etc/postfix/Debian-main.cf.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2025072401 +# 2026071201 # note: adapted from /etc/postfix/main.cf.proto as /etc/postfix/main.cf is dynamically generated via debconf # Global Postfix configuration file. This file lists only a subset @@ -728,6 +728,7 @@ bounce_queue_lifetime = {{ postfix__bounce_queue_lifetime }} mailbox_size_limit = {{ postfix__mailbox_size_limit }} maximal_queue_lifetime = {{ postfix__maximal_queue_lifetime }} message_size_limit = {{ postfix__message_size_limit }} +recipient_canonical_maps = regexp:/etc/postfix/recipient_canonical sender_canonical_maps = regexp:/etc/postfix/canonical smtp_destination_concurrency_limit = {{ postfix__smtp_destination_concurrency_limit }} smtp_destination_recipient_limit = {{ postfix__smtp_destination_recipient_limit }} diff --git a/roles/postfix/templates/etc/postfix/RedHat-main.cf.j2 b/roles/postfix/templates/etc/postfix/RedHat-main.cf.j2 index a57d0b6e..06124579 100644 --- a/roles/postfix/templates/etc/postfix/RedHat-main.cf.j2 +++ b/roles/postfix/templates/etc/postfix/RedHat-main.cf.j2 @@ -1,5 +1,5 @@ # {{ ansible_managed }} -# 2025072401 +# 2026071201 # Global Postfix configuration file. This file lists only a subset # of all parameters. For the syntax, and for a complete parameter @@ -759,6 +759,7 @@ bounce_queue_lifetime = {{ postfix__bounce_queue_lifetime }} mailbox_size_limit = {{ postfix__mailbox_size_limit }} maximal_queue_lifetime = {{ postfix__maximal_queue_lifetime }} message_size_limit = {{ postfix__message_size_limit }} +recipient_canonical_maps = regexp:/etc/postfix/recipient_canonical sender_canonical_maps = regexp:/etc/postfix/canonical smtp_destination_concurrency_limit = {{ postfix__smtp_destination_concurrency_limit }} smtp_destination_recipient_limit = {{ postfix__smtp_destination_recipient_limit }} diff --git a/roles/postfix/templates/etc/postfix/recipient_canonical.j2 b/roles/postfix/templates/etc/postfix/recipient_canonical.j2 new file mode 100644 index 00000000..f970352a --- /dev/null +++ b/roles/postfix/templates/etc/postfix/recipient_canonical.j2 @@ -0,0 +1,315 @@ +# {{ ansible_managed }} +# 2026071201 + +# CANONICAL(5) CANONICAL(5) +# +# NAME +# canonical - Postfix canonical table format +# +# SYNOPSIS +# postmap /etc/postfix/canonical +# +# postmap -q "string" /etc/postfix/canonical +# +# postmap -q - /etc/postfix/canonical