Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <role>:state --extra-vars '<role>__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`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions extensions/molecule/setup_basic/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }}'
28 changes: 28 additions & 0 deletions roles/postfix/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: ''
Expand Down
11 changes: 11 additions & 0 deletions roles/postfix/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}'
Expand Down
4 changes: 4 additions & 0 deletions roles/postfix/handlers/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions roles/postfix/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
3 changes: 2 additions & 1 deletion roles/postfix/templates/etc/postfix/Debian-main.cf.j2
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 }}
Expand Down
3 changes: 2 additions & 1 deletion roles/postfix/templates/etc/postfix/RedHat-main.cf.j2
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 }}
Expand Down
Loading