Ctrl K
ComunidadGuías…

CHANGELOG.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).


## [Unreleased]

**Highlights:** Apache Solr runs on Debian, Ubuntu and RHEL 10 and supports Solr 9.11. Hosts that deploy the OWASP Core Rule Set with apache_httpd need `apache_httpd__mod_security_coreruleset_version` in their inventory, since the role no longer picks a release on its own and verifies the download against a checksum. The `systemd_unit` role also manages socket units, so a host can forward a TCP port with `systemd-socket-proxyd` without installing a proxy server. Hosts set up with `setup_basic` follow more of the CIS Benchmarks out of the box: root can no longer log in over SSH, USB storage is blocked, a sysctl hardening baseline is applied, and the cron configuration is readable by root only. Before updating, make sure Ansible logs in with an unprivileged user, and allow USB storage on hosts that need it. Nextcloud keeps the apps and app settings that the admin changed in the web interface, and JPEG previews work again. Large runs are easier to follow with the `linuxfabrik.lfops.compact` stdout callback, which shows one block per task, a progress bar per host, and only what changed. The `linuxfabrik.lfops.dashboard` stdout callback shows a run on one fixed screen, with the hosts sorted by severity.

### Breaking Changes

* **role:grav, playbook:setup_grav**: The code of Grav, its configuration, pages, plugins and themes belong to `root`, so a hole in Grav or a plugin can no longer plant code in the site. The web server only writes `assets`, `backup`, `cache`, `images`, `logs`, `tmp`, `user/accounts` and `user/data`, and only these keep the SELinux type `httpd_sys_rw_content_t`. Grav, its plugins and themes are updated as `root` by `grav-selfupgrade-<instance>.timer` and `grav-update-<instance>.timer`, which replace `grav-selfupgrade.timer` and `grav-update.timer`, so several Grav sites on one host are each updated. Run other `bin/gpm` commands with `grav-gpm <installation directory> <arguments>` instead of `sudo -u apache bin/gpm`, which now fails, and deploy content below `user/` as `root`. The role no longer installs the Admin Panel, which could not save anything anymore, and drops `grav__skip_admin` and `grav__users`; it lists the Admin Panel plugins still installed at the end of the play, for you to remove. The vHost no longer accepts `POST`, and `setup_grav` no longer sets the SELinux boolean `httpd_can_network_connect`, which keeps its current value.
* **role:apache_httpd**: The `proxy` vHost template waits 30 instead of 5 seconds for the backend, so slower responses no longer end in a 502, and a hanging backend holds the connection longer. Set `conf_proxy_timeout: 5` on the vHost to keep the previous value.
* **role:icingaweb2, playbook:setup_icinga2_master**: IcingaWeb2 accepts selections of up to 500 objects and has 5 minutes for long requests such as PDF exports. A reverse proxy in front of it needs `apache_httpd__conf_limit_request_field_size` and `apache_httpd__conf_limit_request_line` set to `131072` as well.
* **role:nextcloud**: On an existing installation, the role no longer resets apps and app settings that the Nextcloud admin changed in the web interface, such as an app the role disables by default. It applies its full list of apps and app settings to a fresh installation only, and afterwards keeps the entries of `nextcloud__apps__*_var` and `nextcloud__app_configs__*_var` plus the apps and settings it relies on, such as `admin_audit`, `password_policy` and `twofactor_totp`. To keep an app or setting under Ansible control, add it to `nextcloud__apps__host_var` / `nextcloud__app_configs__host_var`.
* **role:system_update**: Hosts install all updates daily instead of every Tuesday, Rocky hosts check for security hot-fixes four times a day instead of once, and pending updates are no longer announced by mail the day before. Mails for applied updates, reboots and failures stay. To keep weekly updates with the announcement, set `system_update__update_day: 'Tue'` and `system_update__notify_and_schedule_enabled: true`; to keep the single daily security run, set `system_update__security_on_calendar`.
* **role:nextcloud, playbook:setup_nextcloud**: Nextcloud keeps its local cache in APCu instead of Redis or Valkey, as the Nextcloud admin manual recommends, with 128 MB of shared memory and APCu enabled for `occ` and the cron jobs. Redis or Valkey keeps the distributed cache and file locking. To stay with Redis or Valkey, set `memcache.local` in `nextcloud__sysconfig__host_var`.
* **role:nextcloud**: The role sets `dbhost` in `config.php` to `nextcloud__database_host` on every run, where it only used the variable for the installation, and writes `localhost` as the path to the MariaDB socket, so notify_push connects through the socket instead of over TCP. On a host whose `config.php` names another database server than the inventory, set `nextcloud__database_host` to that server before the run.
* **role:nextcloud**: `nextcloud__database_login` is mandatory, and `nextcloud__mariadb_login` is gone. A new installation connects to MariaDB as this user, which `setup_nextcloud` creates with access to the Nextcloud database from `localhost` only, instead of handing the database administrator to the installer, which created an `oc_` user that may connect from any host. Existing installations keep their database user.
* **role:nextcloud**: `nextcloud__version` is mandatory, for example `'latest-35'`. It only picks the release that a new installation downloads.
* **role:nextcloud**: Nextcloud reaches Redis or Valkey through its Unix socket instead of over TCP, and the web server user joins the `redis` or `valkey` group, which may use the socket. Run the complete `setup_nextcloud` playbook rather than the `nextcloud` role alone, so that Redis or Valkey open the socket first. Set `nextcloud__redis_unixsocket: ''` to stay with TCP.
* **role:nextcloud**: The role aborts before it changes anything when the installed PHP or MariaDB does not fit the installed Nextcloud major version, or when it does not know that major version (30 to 35 are known). For example, Nextcloud 35 needs PHP 8.3 to 8.5 and MariaDB 10.11 or newer. Upgrade PHP or MariaDB, as the error message says, before running the role again.
* **role:nextcloud**: Nextcloud verifies the certificate of the SMTP server, where the role had switched the check off. If your mail server presents a self-signed certificate, make Nextcloud trust it, or set `mail_smtpstreamoptions ssl allow_self_signed` to `true` and `mail_smtpstreamoptions ssl verify_peer` and `mail_smtpstreamoptions ssl verify_peer_name` to `false` in `nextcloud__sysconfig__host_var` to keep the previous behaviour.
* **playbook:setup_icinga2_master, playbook:setup_nextcloud**: MariaDB runs with `skip_name_resolve = ON`, so accounts bound to a host name no longer log in. Move them to an IP address, or set `mariadb_server__cnf_skip_name_resolve__host_var`.
* **role:mariadb_server**: `table_definition_cache` defaults to 600 instead of 400, which needs a restart of MariaDB.
* **role:sshd, playbook:setup_basic**: `PermitRootLogin` defaults to `no` instead of `yes`, as the CIS Benchmarks require, so no one logs in as root over SSH anymore, also not with keys between machines or with a break-glass password. A run in which Ansible logs in as root aborts before sshd is changed, instead of locking Ansible out. Log in as an unprivileged user with sudo, or pin `sshd__permit_root_login` (`'prohibit-password'` for key logins, `'yes'` for the previous behaviour) on hosts that need root logins.
* **role:sshd**: sshd disconnects a client that has not answered for 30 seconds (`ClientAliveInterval 15`), as the CIS Benchmarks require. Set `sshd__client_alive_interval: 0` to keep idle sessions with an unresponsive client open.
* **role:kernel_modules**: The `usb-storage` kernel module is blocked by default, as the CIS Benchmarks require, which stops USB flash drives and USB disks after the next reboot. The run aborts on hosts with a USB disk attached, since a host booting from one would no longer boot. Add `kernel_modules__modules__host_var: [{name: 'usb-storage', enabled: true}]` to the inventory of hosts that need USB storage.
* **role:kernel_settings**: A sysctl hardening baseline following the CIS Benchmarks is applied by default: ICMP redirects are neither accepted nor sent, source-routed packets are dropped, martian packets are logged, unprivileged users can no longer read the kernel log or attach a debugger to processes they did not start themselves, and unprivileged BPF is off for good. Override single values in `kernel_settings__sysctl__host_var`, or remove them with `state: 'absent'`.
* **role:duplicity, playbook:duplicity, playbook:setup_basic**: On a host where `duba` is not installed yet, the run aborts unless `/tmp` has 1024 MiB free, since smaller filesystems fail the backup. Grow `/tmp` with `lvm__lvs__host_var`, or set `lfops__skip_disk_space_check` to skip the check.
* **playbook:setup_nextcloud**: The Nextcloud vHost waits up to 3600 seconds for PHP and clients and no longer limits the time for reading a request body, so slow uploads and the assembly of large files are not cut off. A reverse proxy in front of Nextcloud needs `conf_timeout` and `conf_proxy_timeout` set to `3600` and `conf_request_read_timeout` set to `header=20-40,MinRate=500 body=0`. To keep the previous values, add the Nextcloud vHost (same `conf_server_name` and `virtualhost_port`) to `apache_httpd__vhosts__host_var` / `apache_httpd__vhosts__group_var` with `conf_timeout: 600` and `conf_request_read_timeout: 'header=20-40,MinRate=500 body=20,MinRate=500'`.
* **playbook:setup_nextcloud**: PHP keeps sessions for 24 hours instead of 24 minutes, so users stay logged in for the full Nextcloud session lifetime. To keep the previous value, set `php__ini_session_gc_maxlifetime__host_var` / `php__ini_session_gc_maxlifetime__group_var` to `1440`.
* **role:nextcloud**: The installation downloads the release archive on the Ansible controller instead of the target, so the controller needs outbound access to `download.nextcloud.com` and `nextcloud.com`.
* **playbook:setup_icinga2_master**: PDF export runs on the IcingaWeb2 PDF Export Module with headless Chromium by default, since the fallback fails on current versions. Set `icingaweb2_module_pdfexport__version` in the inventory (`v0.13.0` works with `icinga-php-thirdparty` 1.1.0), or set `setup_icinga2_master__icingaweb2_module_pdfexport__skip_role: true`.
* **role:graylog_datanode, role:graylog_server, playbook:setup_graylog_datanode, playbook:setup_graylog_server, playbook:setup_icinga2_master**: On a host where the software is not installed yet, the run aborts before it installs anything if the disk space is too small, instead of failing halfway through: Graylog needs the 4 GB on `/` and 15 GB on `/var` its README asks for, and `setup_icinga2_master` up to 2.4 GB on the filesystem of `/usr`. Grow the filesystem, or set `lfops__skip_disk_space_check: true` if you know that the space suffices.
* **role:gitlab_ce**: The role needs GitLab 19.2 or newer and aborts on a host that runs an older version, or when `gitlab_ce__version` names one. `gitlab.rb` follows GitLab 19.5 and uses the NGINX settings introduced in 19.2, so `gitlab-ctl reconfigure` no longer warns about deprecated settings. Upgrade older hosts along the GitLab upgrade path first.
* **role:repo_monitoring_plugins, playbook:icinga2_agent, playbook:monitoring_plugins, playbook:setup_basic, playbook:setup_icinga2_master**: The Monitoring Plugins packages come from the Linuxfabrik package repository (role `repo_linuxfabrik`) instead of repo.linuxfabrik.ch/monitoring-plugins/, whose repository file the playbooks remove. Rename `repo_monitoring_plugins__basic_auth_login` and `repo_monitoring_plugins__mirror_url` to `repo_linuxfabrik__basic_auth_login` and `repo_linuxfabrik__mirror_url`, and replace `repo_monitoring_plugins__testing` with `repo_linuxfabrik__testing_repo_enabled`, which adds the testing channel instead of replacing the release channel. The run aborts as long as one of the former variables is set. A mirror has to serve the Linuxfabrik repository below `<mirror URL>/linuxfabrik/`.
* **role:opensearch**: OpenSearch 1.x is no longer supported, and the run aborts on a host that runs it. Upgrade such hosts to OpenSearch 2.19 first, following the [OpenSearch upgrade documentation](https://docs.opensearch.org/latest/install-and-configure/upgrade-opensearch/index/). OpenSearch 2.x cannot be installed on RHEL 10 and Debian 13, whose crypto policy rejects the SHA-1 signature in the OpenSearch 2.x signing key; use OpenSearch 3.x there.
* **role:mongodb**: MongoDB 4.2, 4.4, 5.0 and 6.0 are no longer supported, since they are end of life; the role aborts on a host that runs one of them. Upgrade such hosts to MongoDB 7.0 or 8.0 first, one major release at a time, as described in the MongoDB upgrade documentation, and set `repo_mongodb__version` accordingly.
* **role:chrony**: Without `chrony__ntp_pools` or `chrony__ntp_servers` in the inventory, chronyd synchronises with `ntp.linuxfabrik.ch` instead of `ntp.metas.ch`. Allow outgoing NTP to `ntp.linuxfabrik.ch` (195.15.242.254) where a firewall only lets the METAS servers through, or set `chrony__ntp_servers: ['ntp.metas.ch']`.
* **role:exoscale_vm**: The role scales an existing instance and grows its disk when `exoscale_vm__service_offering` or `exoscale_vm__disk_size` differ from it, and aborts on a running instance instead of stopping it, unless `exoscale_vm__force: true` is set. Before running the role against existing instances, set both variables to their current values, or set `exoscale_vm__force: true` in a maintenance window. `exoscale_vm__state` only accepts `absent`, `present`, `restarted`, `started` and `stopped`.
* **role:php**: PHP no longer tracks file uploads in the session (`session.upload_progress.enabled = Off`), a confirmed way into the remote code execution CVE-2026-109619 ([GHSA-6wmf-4rv5-p3rp](https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp)) that the advisory recommends closing even on patched PHP, and the PHP-FPM pools enforce this against `.user.ini` files. Upload progress bars that depend on it stop working. Set `php__ini_session_upload_progress_enabled__host_var: 'On'` to restore the feature.
* **role:postgresql_server**: Databases are created with the `C.UTF-8` locale instead of `en_US.UTF-8`, which failed on hosts with only the minimal glibc language pack. `C.UTF-8` sorts text by code point instead of by language rules. Existing databases are unaffected. To create new databases as before, set `lc_collate: 'en_US.UTF-8'` and `lc_ctype: 'en_US.UTF-8'` on their entries in `postgresql_server__databases__*_var`.
* **role:postgresql_server, playbook:setup_mastodon**: `postgresql_server__version` is mandatory, and PostgreSQL is only installed from the PostgreSQL Yum Repository instead of optionally from the distribution packages. Set the major version in your inventory, for example `postgresql_server__version: '18'`. On a host that already runs PostgreSQL from the distribution packages, the run aborts before it installs anything; move the data to the PostgreSQL Yum Repository release first, as described in the role README.
* **role:postgresql_server**: `postgresql_server__pg_hba_entries` is replaced by `postgresql_server__pg_hba_local_entries__host_var` / `__group_var` for `local` records and `postgresql_server__pg_hba_host_entries__host_var` / `__group_var` for all `host*` records. Both are merged with the role defaults instead of replacing them, and an entry with `state: 'absent'` removes a default. Move your `local` entries to the first variable without their `type` key, and all other entries to the second, where `address` is mandatory. Entries from the inventory are written before the role defaults. The run aborts as long as the old variable is set.
* **role:apache_tomcat**: `--tags apache_tomcat:configure` no longer deploys `/etc/logrotate.d/tomcat`. Use `--tags apache_tomcat:logrotate` for it.
* **role:apache_tomcat**: `/etc/tomcat/tomcat-users.xml` stores the passwords from `apache_tomcat__users__*_var` as salted PBKDF2 hashes instead of in clear text. Web applications that authenticate against it with HTTP DIGEST authentication no longer let anyone log in; switch them to BASIC or FORM authentication.
* **role:repo_mydumper**: On the Red Hat family, mydumper comes from the official mydumper repository instead of `repo.linuxfabrik.ch`, and dnf checks the package signature. A `repo_mydumper__mirror_url` must therefore serve the signed packages of the official repository unchanged, otherwise the installation fails with a signature error.
* **role:apache_httpd**: The role no longer hands everything below the document root to the web server user, so a hole in a web application can no longer rewrite the code of every site on the host. The roles that install an application set the owners they need. For content placed by hand that Apache has to write, give the writable directories to `apache` (`www-data` on Debian and Ubuntu) yourself. Remove `apache_httpd__skip_document_root_chown` from your inventory.
* **role:openvpn_server**: The role requires OpenVPN 2.7 and installs or upgrades it from the OpenVPN repository, which the `openvpn_server` playbook enables with the new `repo_openvpn` role. The upgrade restarts the OpenVPN service twice and drops every client each time, so run it in a maintenance window. The server refuses client certificates with an RSA key below 2048 bits, a SHA-1 signature or without the TLS Web Client Authentication key usage, and no longer accepts several clients with the same certificate; set `openvpn_server__duplicate_cn: true` where clients share one. Clients that only speak TLS 1.2 and carry the `tls-cipher` line of the former example client config must drop that line. Remove `openvpn_server__dh` and `openvpn_server__dh_skip_deploy` from your inventory.
* **role:apache_httpd**: `apache_httpd__mod_security_coreruleset_version` has no default anymore and accepts only the OWASP Core Rule Set releases the CRS project supports with security fixes, currently `4.25.2`, `4.29.0` and `4.30.0`. On hosts with `apache_httpd__skip_mod_security_coreruleset: false`, set it in the inventory, otherwise the run aborts.
* **role:apache_solr**: Solr listens on `127.0.0.1` only and gets 180 seconds to stop gracefully, as upstream ships it, instead of listening on all addresses and being killed after 15 seconds. Set `apache_solr__http_bind_address: '0.0.0.0'` for a Solr that other hosts have to reach.
* **role:bind**: named validates DNSSEC by default, as the bind package does, and follows the system-wide crypto policy. Forged or broken answers for signed zones are answered with `SERVFAIL` instead of being passed on. The `forward`, `static-stub` and `stub` zones in `bind__zones` are excluded from validation; list other internal zones below a signed domain or TLD in `bind__dnssec_validate_except`, or set `bind__dnssec_validation: false`. On RHEL 8 the role fails if it has zones to exclude, since BIND 9.11 cannot; set `bind__dnssec_validation: false` there ([#355](https://github.com/Linuxfabrik/lfops/issues/355), [#356](https://github.com/Linuxfabrik/lfops/issues/356)).
* **role:system_update**: On Debian and Ubuntu, the system update no longer updates the database of an AIDE installed by hand, since it also accepted changes that were pending before the update. On such hosts the daily AIDE mail now lists the files each update changed. Deploy the aide role to have its database updated after updates again.
* **role:grav**: The `grav:cron` tag is gone. Run the role with `--tags grav` to deploy the timers and their services, or with `--tags grav:state` to enable or disable the timers.
* **role:firewall**: With `firewall__firewall: 'fwbuilder'`, the default, the run aborts on a host that has neither `/etc/fwb.sh` nor `firewall__fwbuilder_repo_url`, before the role stops any firewall. Until now `fwb.service` failed there and the host ran without a firewall. Deploy `/etc/fwb.sh`, set `firewall__fwbuilder_repo_url`, or set `firewall__firewall` to the firewall the host uses.
* **role:chrony**: The role takes effect on Debian and Ubuntu, where chronyd reads `/etc/chrony/chrony.conf` and ignored the `/etc/chrony.conf` the role deployed, so these hosts synchronised with the distribution's default pools until now. As on the Red Hat family, chronyd uses only the sources from `chrony__ntp_pools` and `chrony__ntp_servers`, without the distribution's pools, DHCP sources or `/etc/chrony/sources.d`. Without either of them, chronyd synchronises with `ntp.linuxfabrik.ch`.
* **role:monitoring_plugins**: The source install downloads the Python dependencies on the Ansible controller, which therefore needs pip for the Python that runs Ansible and access to PyPI; the targets no longer need Internet access.
* **role:kernel_modules**: The `tun` kernel module is blocked by default (CVE-2026-81000, [RHSB-2026-011](https://access.redhat.com/security/vulnerabilities/RHSB-2026-011)). This stops OpenVPN, WireGuard in userspace, rootless Podman and Docker networking and libvirt VM networking after the next reboot, which the role requests on hosts where `tun` is loaded. Before running the role, add `kernel_modules__modules__host_var: [{name: 'tun', enabled: true}]` to the inventory of every such host. Rootful Docker and Podman with bridge networking are not affected.
* **role:grafana**: `grafana__users_case_insensitive_login` is gone; remove it from your inventory. Grafana has ignored the setting since v11.0.0 and always matches logins case-insensitively.
* **role:system_update**: `system_update__pre_update_code` and `system_update__post_update_code` now also run in the daily security lane on Rocky, around the transaction that installs the hot-fixes, where until now only the weekly lane ran them. Set `system_update__security_pre_update_code: ''` and `system_update__security_post_update_code: ''` to keep the security lane free of it, or set either to a codeblock of its own to have the two lanes do different things.
* **role:wordpress**: The vHost file is named after the host of `wordpress__url`, for example `wordpress.example.com.80.conf`, instead of `wordpress.conf`. Remove the old file with `rm -f /etc/httpd/sites-{enabled,available}/wordpress.conf` and reload httpd, otherwise Apache may keep serving the site from it.
* **role:wordpress**: The WordPress core, `wp-config.php` and `wp-content/mu-plugins` belong to `root`, so a vulnerable plugin can no longer modify them; plugins and themes can still be installed from the web interface. The core is updated by `wordpress-core-minor-update-<instance>.timer` (minor releases) and `--tags wordpress:update` instead of by WordPress itself. After a permalink change in the web interface, add the displayed rewrite rules to `.htaccess` by hand.
* **role:wordpress**: The REST API is restricted to logged-in users by the Disable WP REST API plugin, which replaces Disable REST API (`disable-json-api`) and also blocks the routes of plugins installed later. A front-end feature that calls the REST API without a login, such as some contact forms, needs an exception in code.
* **role:wordpress**: Application passwords are switched off, since they bypass a second factor. Set `wordpress__application_passwords_enabled: true` for integrations that use them, such as the WordPress mobile app.
* **role:wordpress**: WordPress honours `X-Forwarded-For` only from the proxies in `wordpress__trusted_proxies` and ignores `X-Forwarded-Host`, which any client could set before. Behind a reverse proxy, list its IP address.
* **role:wordpress**: `wordpress__url` must include the scheme, for example `https://wordpress.example.com`. The role sets the site address (`home` and `siteurl`) to it on every run, so the login cookie of an `https://` site carries the `Secure` flag, and an address changed in the WordPress settings is set back.
* **role:icinga2_agent**: If the agent cannot get a PKI ticket from the Icinga2 master, the run aborts and names the cause and the fix. Until now the role set the agent up anyway, which also replaced the signed certificate of an agent that was already connected with an unsigned one. If you sign agent certificates on the master by hand, or do not set `icinga2_agent__icinga2_api_user_login`, set `icinga2_agent__skip_pki_ticket: true`.
* **role:dnf_makecache**: `dnf_makecache__service_enabled` and `dnf_makecache__service_state` are gone; remove them from your inventory. The role only manages `dnf-makecache.timer` now, since `dnf-makecache.service` cannot be enabled at boot and only runs when the timer triggers it. `dnf_makecache__service_enabled` never had an effect, but a run against an unchanged host reported a change for it. A host that set `dnf_makecache__service_state: 'started'` no longer runs `dnf makecache` on every run of the role. Use `dnf_makecache__timer_enabled` and `dnf_makecache__timer_state` for the periodic cache refresh.
* **role:kibana**: The session cookie always carries the `Secure` flag, also behind a reverse proxy that terminates TLS, where Kibana left the flag off. A Kibana that browsers reach over plain HTTP no longer logs anyone in until `kibana__xpack_security_secure_cookies: false` is set. Remove `xpack.security.secureCookies` from `kibana__raw` if you set it there.
* **role:icingaweb2**: The session and remember-me cookies always carry the `Secure` flag, also behind a reverse proxy that terminates TLS and talks plain HTTP to IcingaWeb2, where IcingaWeb2 left the flag off. An IcingaWeb2 that browsers reach over plain HTTP no longer logs anyone in until `icingaweb2__cookie_secure: false` is set.
* **role:apache_httpd**: Every entry in `apache_httpd__htpasswd__*_var` needs the `path` subkey, because the username and the path together identify an entry. Until now, entries with the same username but a different `path` were collapsed into one, so only the last file got the user; a user listed with different paths in group and host variables is now written to both files. On entries that relied on the default, set `path: '/etc/httpd/.htpasswd'` (RedHat) or `path: '/etc/apache2/.htpasswd'` (Debian and Ubuntu), otherwise the play fails with an error naming the entry.
* **role:postfix**: On RHEL 10 the role deploys the compatibility level the distribution ships (`3.8`) instead of the RHEL 8 / 9 value it applied everywhere. Postfix now matches TLS fingerprints with SHA-256 instead of MD5, evaluates the relay restrictions before the recipient restrictions, and uses the neutral wording in its postscreen log lines. Re-generate any peer fingerprint pinned as MD5 in a TLS policy table, and check log parsers keyed on the old postscreen wording. Set `postfix__compatibility_level: '2'` to restore the previous behaviour. RHEL 8, RHEL 9 and Debian are unaffected.
* **role:login**: The default umask in `/etc/login.defs` is back to the `022` the distributions ship, instead of the `027` set since v7.0.0, so files that users create in a login session are readable by other local users again. Set `login__login_defs_umask: '027'` to keep the stricter value.
* **playbook:crypto_policy, playbook:kernel_modules, playbook:selinux**: These playbooks run `postfix`, `mailto_root` and `schedule_reboot` before their own role, so that a change needing a reboot can request one. `mailto_root__from` and `mailto_root__to` are therefore mandatory for them; a host set up with `setup_basic` already has both. To keep a playbook as it was, set `<playbook>__skip_postfix`, `<playbook>__skip_mailto_root` and `<playbook>__skip_schedule_reboot` to `true`, for example `crypto_policy__skip_schedule_reboot: true`.
* **role:fail2ban**: Removed `apache-404-matomo` filter and jail. The `apache-404` filter now matches all supported LogFormats including matomo and vhost_common. Remove `apache-404-matomo` entries from `fail2ban__filters__*_var` and `fail2ban__jails__*_var` in your inventory and use `apache-404` instead.
* **role:fail2ban**: Rename `fail2ban__jail_apache_404_ignoreregex` to `fail2ban__filter_apache_404_ignoreregex` in your inventory. The regular expressions land in the `apache-404` and `apache-404-matomo` filters, which both jails share, so the old name pointed at a jail that never carried the setting. The value itself is unchanged.

### Added

* **role:icingaweb2_module_director, playbook:setup_icinga2_master**: Runs the Director database housekeeping daily at 03:xx, at a fixed minute per host. Set `icingaweb2_module_director__housekeeping_timer_enabled: false` to turn it off.
* **role:collabora**: Add support for Collabora Online CODE 26.04.6.
* **role:collabora**: Add support for Collabora Online Enterprise 26.04.5.
* **role:icingaweb2_module_reporting**: Add `icingaweb2_module_reporting__mail_from` to set the sender address of the report mails.
* **role:freeipa_server**: The `freeipa_server__healthcheck_*` variables set the certificate warning period, the timeout per check and the excludes of `ipa-healthcheck`, by default with 30 instead of 10 seconds per check.
* **role:freeipa_server**: Installs `ipa-healthcheck` and runs it right after the installation and then daily at 06:xx, at a fixed minute per host or after the next boot if the host was down at that time, so its report in `/var/log/ipa/healthcheck/healthcheck.log` is ready for monitoring.
* **execution-environment**: The image carries its LFOps version in the `LFOPS_EE_VERSION` environment variable and the `org.opencontainers.image.version` label, and the dashboard callback shows it.
* **plugin:dashboard**: The `linuxfabrik.lfops.dashboard` stdout callback shows a run on one fixed screen with counters and the hosts sorted by severity, and the details of the tasks with changes, warnings or failures at the end.
* **plugin:compact**: The `linuxfabrik.lfops.compact` stdout callback shortens the output of large runs to one block per task, with a progress bar per host, one line each for the ok and the skipped hosts, the diff or module output of the changed hosts and the warnings, each grouped by identical text, and a report of the distinct changes, warnings and errors by severity at the end.
* **role:rpm, playbook:rpm, playbook:setup_basic**: Checks the RPM database on RHEL 8, 9 and 10 before the first package task, aborts on a damaged one instead of failing later with `rpmdb open failed`, and on RHEL 8 repairs a database that a killed rpm process left unusable, keeping all packages.
* **role:dnf, playbook:dnf**: Sets options in `/etc/dnf/dnf.conf` on RHEL 8, 9 and 10, by default `minrate=200k` and `timeout=15`, so DNF drops slow mirrors sooner. Hosts without DNF 4 are left alone.
* **role:telegraf**: Add the `telegraf:configure` tag.
* **role:redis**: Add template for Redis 8.10.
* **role:nextcloud, playbook:setup_nextcloud**: The run aborts before it changes anything if `nextcloud__database_login` is missing or names the database user of the installed Nextcloud with another password, and tells how to read the login with `occ`. Until now a wrong password locked Nextcloud out of its database.
* **module:systemd_credential**: `scope: 'user'` stores a credential for the services of an unprivileged user, such as a rootless Podman container, on hosts with systemd 256 or newer.
* **role:nextcloud_whiteboard_server, playbook:nextcloud_whiteboard_server**: New role and playbook for the collaboration server of the Nextcloud Whiteboard app (real-time collaboration and recordings), run as an automatically updated rootless Podman container on hosts with systemd 256 or newer.
* **role:php**: `php__ini_apc_enable_cli` and `php__ini_apc_shm_size` configure APCu.
* **role:mariadb_server**: `mariadb_server__cnf_innodb_linux_aio` selects the Linux AIO implementation, for hosts where the kernel blocks io_uring.
* **role:cron, playbook:cron, playbook:setup_basic**: Add a role and playbook that keep the cron daemon running and make `/etc/crontab`, `/etc/cron.deny` and the `/etc/cron.*` directories readable by root only, as the CIS Benchmarks require.
* **role:cron_permissions, playbook:cron_permissions, playbook:setup_basic**: Add a role and playbook that make `/etc/crontab`, `/etc/cron.deny` and the `/etc/cron.*` directories readable by root only on hosts with cron, as the CIS Benchmarks require.
* **role:nextcloud**: A fresh installation enforces two-factor authentication, has TOTP enabled and no mimetype migrations pending. `nextcloud-update` runs the mimetype migrations after each update. Existing installations keep their enforcement state.
* **role:nextcloud**: Nextcloud sends mail through the relay host of the `postfix` role by default, unless a mail server is already configured.
* **role:duplicity**: `duba restore <path>` restores a file or directory by its path on the host, optionally as of an older `--time`, into a separate directory. It finds the right backup on its own, also for sources with `divide: true`, and warns about low disk space.
* **role:duplicity**: `duba exec -- <duplicity arguments>` runs any duplicity command (for example `collection-status` or `restore`) with the settings and the password of the backup, without exporting the password into the shell.
* **role:mongodb, role:repo_mongodb**: Support MongoDB 9.0, also on Debian 13 and Ubuntu 26.04, where upstream offers 9.0 only. MongoDB upgrades to 9.0 from 8.0 only, so upgrade a 7.0 host to 8.0 first.
* **role:graylog_server**: Add `graylog_server__stream_aware_field_types` to switch stream-aware field types on or off on Graylog 7.3 and later.
* **role:graylog_datanode, role:graylog_server**: Add template for Graylog 7.3. It is based on the pre-release 7.3.0-alpha.1 and will be updated for the final 7.3.0 release.
* **module:systemd_credential**: Stores a secret as a systemd credential, encrypted with `systemd-creds` on systemd 251 and newer and as a root-only file in `/etc/credstore` on older systems, so that services can load it with `LoadCredentialEncrypted=` instead of reading it from a plain configuration file.
* **role:opensearch**: Supports OpenSearch 3.x. The role README describes the upgrade from 2.x.
* **playbook:setup_basic**: Deploys the Linuxfabrik package repository on Debian, Ubuntu and RHEL 8 to 10 and their derivatives, right after EPEL; on hosts whose mirror does not serve it, dnf skips it with an error message and apt stops the run (`setup_basic__skip_repo_linuxfabrik`).
* **role:repo_linuxfabrik, playbook:repo_linuxfabrik**: Add a role and playbook that deploy the Linuxfabrik package repository repo.linuxfabrik.ch on Debian, Fedora, the Red Hat family, SUSE and Ubuntu, with an optional testing channel.
* **role:chrony**: chronyd can serve as a public NTP server, for example in the NTP Pool, with response rate limiting (`chrony__ratelimit`) and room for many clients (`chrony__clientloglimit`).
* **role:exoscale_vm**: Security group rules support every protocol Exoscale offers, ICMP with `icmp_type` and `icmp_code`, private networks can be detached with `state: 'absent'`, and an existing instance is attached to a security group added later.
* **role:collabora**: Add support for Collabora Online Enterprise 25.04.14.
* **execution-environment**: Add the `telekom_mms.icinga_director` collection, so inventories using its Icinga Director inventory plugin work inside the EE.
* **role:curaline, playbook:curaline**: Add a role and playbook that install and configure the curaLINE Client of Swisscom, which exchanges files with the curaLINE Document Routing service.
* **role:gitlab_ce**: Behind a reverse proxy, GitLab can show and log the real client IP address instead of the address of the proxy, for example under "Current sign-in IP". Set `gitlab_ce__rb_nginx_real_ip_trusted_addresses` to the address of the proxy.
* **role:apache_tomcat**: With `lfops__remove_rpmnew_rpmsave`, the role also removes the `.rpmnew` and `.rpmsave` files next to the Tomcat configuration files it deploys.
* **role:apache_tomcat**: Add support for Tomcat 10.1 on RHEL 10.
* **role:mongodb**: Supports MongoDB 8.0 on Debian and Ubuntu, so MongoDB runs on Ubuntu 24.04, where upstream offers 8.0 only.
* **role:redis**: The role's inventory variables are type-checked when it starts, so a mistyped value fails the run right away instead of surfacing further in as a confusing error.
* **role:graylog_datanode, role:graylog_server**: Add template for Graylog 7.2.
* **role:duplicity**: `duplicity__archive_dir` sets the directory of duplicity's local metadata cache, for example to keep the cache of a separate long-term backup apart from the daily one. Without it, the cache stays in `~/.cache/duplicity`.
* **role:systemd_unit**: Manages systemd socket units with `systemd_unit__sockets__host_var` / `systemd_unit__sockets__group_var`, for example to forward a TCP port with `systemd-socket-proxyd` without installing a proxy server.
* **role:collabora**: Add support for Collabora Online Enterprise 24.04.19, 26.04.3 and 26.04.4.
* **role:collabora**: Add support for Collabora Online Enterprise 25.04.13.
* **role:collabora**: Add support for Collabora Online CODE 26.04.5.
* **playbook:setup_basic, role:monitoring_plugins**: On the Red Hat family, `psi=1` is put on the kernel command line, so the psi-* monitoring plugins report pressure stall information instead of finding none; each host reboots once at its maintenance window, and `setup_basic__skip_bootloader` skips it.
* **role:apache_httpd**: `apache_httpd__mod_remoteip_internal_proxy` lists the reverse proxies in front of a host, and Apache then logs the client from `X-Forwarded-For` instead of the proxy in the access and the error log.
* **role:apache_httpd**: Creates a self-signed placeholder for every certificate in `apache_httpd__placeholder_certificates__*_var` that does not exist yet, so a TLS vHost starts before its certificate is issued; the playbooks fill the list from `acme_sh__certificates`.
* `acme_sh` can run as the last role of every playbook with `apache_httpd`, so a fresh host gets its Let's Encrypt certificates in a single run; enable it with the playbook's `acme_sh` skip variable (`apache_httpd` and the `setup_*` playbooks with `apache_httpd`).
* **role:repo_openvpn, playbook:repo_openvpn**: Add a role and playbook that deploy the OpenVPN 2.7 release repository of the OpenVPN community on RHEL 8, 9 and 10, where EPEL ships outdated OpenVPN versions.
* **role:openvpn_server**: Add `meta/argument_specs.yml` declaring the user-facing variables, so role-entry validation catches type mismatches and invalid values before any task runs.
* **role:apache_solr**: Supports Solr 10, deployed as a single instance without ZooKeeper, as with Solr 9.
* **role:apache_solr**: Supports Debian 12 and 13, RHEL 10 and Ubuntu 22.04, 24.04 and 26.04, with the Java the Solr version needs installed from the distribution.
* **role:bind**: Add `meta/argument_specs.yml` declaring the user-facing variables, so role-entry validation catches type mismatches and invalid values before any task runs.
* **role:duplicity**: The backup includes the data of the applications LFOps deploys by default: `/data`, `/srv`, `/var/lib/grafana`, `/var/lib/icinga2` (including the Icinga2 CA), `/var/lib/shiny-server`, `/var/lib/turn`, `/var/mail`, `/var/named`, `/var/solr/data`, `/var/spool/mail` and `/var/www` (without the repository mirrors). Hosts without these directories are not affected. On hosts with large data, for example VM images in `/data`, check the backup size or set the path to `state: 'absent'`.
* **playbook:setup_basic**: Installs AIDE on every host, which checks file integrity twice a day and after every boot; skip it with `setup_basic__skip_aide`.
* **role:aide, playbook:aide**: Add a role and playbook that install AIDE on Debian 12 and 13, RHEL 8, 9 and 10 and Ubuntu 22.04, 24.04 and 26.04 as the CIS benchmarks recommend, leaving `aidecheck.service` failed on any finding and keeping the database in step with `system_update` and `unattended-upgrades`.
* **role:lynis, playbook:lynis, playbook:setup_basic**: Add a role and playbook that install Lynis on every `setup_basic` host and audit it once a day for the lynis-logfile monitoring plugin, with findings accepted through `lynis__skip_tests` instead of a hand-edited `/etc/lynis/custom.prf`. By default the role skips the tests whose suggestion LFOps deliberately does not follow, giving the reason for each in `custom.prf`.
* **role:repo_monitoring_plugins**: `--tags repo_monitoring_plugins:remove` removes the repository and its signing key.
* **role:grafana**: `grafana__preinstall_auto_update` controls whether Grafana updates its preinstalled plugins on every start.
* **role:grafana**: `grafana__feature_toggles__*_var` sets feature toggles in the `[feature_toggles]` section of `grafana.ini`.
* **role:icingaweb2**: `icingaweb2__cookie_path` sets the path of the session and remember-me cookies, for example `/`.
* **role:collabora**: Add support for Collabora Online CODE 26.04.4.
* **role:wordpress**: Entries in `wordpress__plugins` accept `enabled: false`, which keeps a plugin installed but deactivated.
* **role:system_update**: The role's inventory variables are type-checked when it starts, so a mistyped value fails the run right away instead of surfacing further in as a confusing error.
* **role:wordpress**: Several WordPress instances can share a host as pseudo hosts in the inventory, under different host names as well as under different paths of one host name, such as `https://example.com/blog`.
* **role:fail2ban**: The `wordpress-login` filter and `z10-wordpress-login` jail ban IPs with too many failed WordPress logins, on the host whose Apache logs the visitor's address.
* **role:rstudio_server, playbook:rstudio_server**: Add a role and playbook to install RStudio Server Open Source, the browser-based R development environment. Users sign in with their account on the host and have to be a member of a group to be let in at all, the PAM profile covers directory users where the vendor's covers local ones only, and the R sessions can be given a memory and process budget.
* **role:shiny_server, playbook:setup_shiny_server**: Add a role and playbook to install Shiny Server Open Source and serve several tenants from one host, each with its own hostname, password file, R worker and system account, behind an Apache httpd reverse proxy that also passes the authenticated user into the application. Shiny Server itself listens on the loopback only, because it authenticates nobody and hands every client header to the application.
* **role:r, playbook:r**: Add a role and playbook to install R and pandoc, point R at the Posit Public Package Manager so that CRAN packages arrive as prebuilt binaries instead of being compiled on the host, and install the CRAN packages an application needs.
* **role:borg_local, role:icinga2_master, role:nextcloud, role:schedule_reboot, role:tools**: `icinga2_master__downtime_api_user` creates an Icinga2 API user that may only schedule and remove downtimes, and the roles that set a downtime around a backup, a Nextcloud update or a reboot use it unless their own `*__icinga2_api_user_login` is set.
* **role:crypto_policy, role:kernel_modules, role:selinux**: A change that only takes effect after a reboot requests one at the maintenance window instead of being left to the operator to notice: a switched crypto policy, a blocked kernel module that is still loaded, and switching SELinux on or off. Where the reboot mechanism is not deployed, the role reports the pending reboot as before. `lfops__reboot_now` performs it in the same run.
* Every playbook prints the manual steps a run leaves to the operator as one block directly above the `PLAY RECAP`, collected from all roles of the play instead of scattered over its output. The roles keep printing their message where it occurs as well, so a role used outside this collection still reports it.
* **role:bootloader**: New role that manages the kernel command line, for parameters that only take effect at boot time such as `psi=1`. Options are applied to every boot entry of the host, on the Red Hat family with `grubby` and on Debian and Ubuntu through a GRUB drop-in of its own. A changed command line requests a reboot at the maintenance window instead of rebooting right away, or applies it during the run when `lfops__reboot_now` is set, and a `--check` run reports what it would change without touching the host.
* **role:fail2ban**: The `fail2ban:configure` tag deploys the actions, filters and jails without touching the packages.
* **role:fail2ban**: Add `meta/argument_specs.yml` declaring the user-facing variables, so role-entry validation catches type mismatches and invalid values before any task runs.

### Changed

* **collection**: The collection declares ansible-core 2.16 as its minimum, as the README already did, so ansible-core 2.15 warns that it does not support the collection.
* **role:grafana**: The role no longer installs `chkconfig` on RHEL 9 and 10, since Grafana ships no SysV init script anymore.
* **role:icingaweb2_module_jira**: `config.ini` no longer sets `project` and `issuetype` in the `[ui]` section, which the module does not read; the default project and issue type stay in `default_project` and `default_issuetype`.
* **role:acme_sh**: acme.sh checks for due renewals daily instead of weekly, so certificates are renewed in time with the shorter Let's Encrypt lifetimes (64 days from 2027-02-10), a failed renewal is retried the next day, and an early renewal that Let's Encrypt requests via ARI is no longer delayed by up to a week.
* **role:librenms**: The nightly self-update of LibreNMS keeps the AIDE database in step, as the system_update role does for package updates. `daily.sh` runs from `librenms-daily.timer` instead of `/etc/cron.d/librenms`, at the same time. After a clean AIDE check it accepts the new release into the database, so a LibreNMS update is no longer reported as a finding.
* **playbook:setup_basic**: Sets `minrate=200k` and `timeout=15` in `/etc/dnf/dnf.conf` on RHEL 8, 9 and 10, so DNF drops slow mirrors sooner; other settings stay as they are.
* **role:lynis**: The daily audit runs at 06:xx instead of 02:xx, after the reboot window, so a report from before the nightly reboot no longer warns about a pending reboot for a whole day.
* **role:nextcloud**: The role removes the PHP extensions bcmath, IMAP and memcached, which Nextcloud does not use in the setup the role creates: the caches live in APCu, Redis or Valkey. An installation that needs one of them, such as the IMAP backend of the External user authentication app, lists it in `php__modules__host_var` with `state: 'present'`.
* **role:icingadb_web, playbook:setup_icinga2_master**: Icinga DB Web shows up to 65536 characters of a plugin output instead of 20000, so long check outputs are no longer cut off.
* **role:aide, role:system_update**: AIDE always runs with the lowest CPU and I/O priority, also where the role and the update scripts call it directly, so it yields to everything else on the host.
* **role:mariadb_server**: The role no longer sets `sunrpc.tcp_slot_table_entries`, which only concerns NFS clients, and removes it from the kernel settings of hosts that got it from an earlier version.
* **role:kernel_modules**: Every blocked module is also blacklisted, so the CIS audit, which checks for both lines, accepts the configuration.
* **role:at**: On the Red Hat family, `/etc/at.deny` is readable by root only, as the CIS Benchmarks require.
* **role:nextcloud, playbook:setup_nextcloud**: MariaDB listens on localhost only, which needs MariaDB 10.11 or newer, and Redis and Valkey evict old keys instead of failing writes when they are full.
* **role:nextcloud, playbook:setup_nextcloud**: Nextcloud gets 256 MB OPcache for 16229 files, 2048 px previews at JPEG quality 60 and no web updater, and `admin_audit` logs to `audit.log`. A fresh installation deletes files from the trash bin after 30 days, and earlier when a user runs out of space.
* **role:nextcloud**: The `app_api` app is disabled by default and `serverid` is set, which removes the corresponding setup warnings. Set `app_api` to `state: 'enabled'` to use External Apps.
* **role:graylog_server**: Hosts running Graylog 7.3 get stream-aware field types, the upstream default from 7.3 on, which can decrease performance on systems with many streams and fields.
* **role:mongodb**: The dump script is installed as `/usr/local/libexec/mongodb-dump`, since it only works when started by `mongodb-dump.service`.
* **role:system_update**: On Debian and Ubuntu, the reboot notification lists the packages and services that asked for the reboot.
* **role:system_update**: On Debian and Ubuntu, the weekly update removes the packages it left unused itself, typically replaced libraries, like unattended-upgrades does. Packages that were unused before stay installed.
* **role:elasticsearch**: The query log of Elasticsearch 9.5 (`*_querylog.json`) is deleted after `elasticsearch__log4j2_retention_days`, like the other logs.
* **role:telegraf**: `telegraf.conf` follows the file that current Telegraf packages ship. The effective settings stay the same, but deploying the file restarts Telegraf once.
* **role:opensearch**: `/etc/sysconfig/opensearch` adds the native library path of the Neural Search plugin, as the OpenSearch package does.
* **role:acme_sh, playbooks with acme_sh**: acme.sh comes as a package from the Linuxfabrik repository, which these playbooks now deploy, and is updated by the package manager instead of updating itself from GitHub on every renewal run; existing installations below `/opt/acme.sh` are migrated on the next run, certificates are kept.
* **role:postfix**: On hosts without an IPv6 default route, Postfix uses IPv4 only, so it no longer logs `Network is unreachable` for the IPv6 address of the next hop on part of the deliveries.
* **role:nextcloud**: The `support` app is enabled by default, as Nextcloud ships it, so an Enterprise instance keeps its subscription record current.
* **role:postgresql_server**: A changed `pg_hba.conf` or `postgresql_server__conf_password_encryption` reloads PostgreSQL instead of restarting it, so open connections are kept; the first run after the update still restarts it once.
* **role:postgresql_server**: The configuration is checked before PostgreSQL is restarted, so a broken setting in `postgresql.conf` or `pg_hba.conf` aborts the run with the error message instead of leaving PostgreSQL down after the restart.
* **role:postfix**: The SMTP greeting banner no longer names Postfix or the OS (`<hostname> ESMTP LFOps`), which satisfies the Lynis check MAIL-8818.
* **role:apache_tomcat**: The Tomcat 9.0 configuration follows the files shipped with the current RHEL 8 and 9 packages.
* **role:apache_tomcat**: The role's inventory variables are type-checked when it starts, so a mistyped value fails the run right away instead of surfacing further in as a confusing error.
* **role:postgresql_server**: The daily dump runs with low CPU and IO priority, so it no longer slows down PostgreSQL.
* **role:mongodb**: The daily dump runs with low CPU and IO priority, so it no longer slows down MongoDB.
* **role:mariadb_server**: The daily dump runs with low CPU and IO priority, so it no longer slows down MariaDB.
* **role:influxdb**: The daily dump runs with low CPU and IO priority, so it no longer slows down InfluxDB.
* **role:gitlab_ce**: The daily dump runs with low CPU and IO priority, so it no longer slows down GitLab.
* **role:redis, role:valkey**: Redis and Valkey also listen on a Unix socket that only the members of their group may use, on RHEL at the path the package ships (`/run/redis/redis.sock`, `/run/valkey/valkey.sock`).
* **role:apache_httpd**: Apache answers requests for PHP files that do not exist with 404 itself instead of passing them to PHP-FPM, so scanners no longer tie up pool workers or fill the error log.
* **playbook:setup_basic**: Checks the variables of all its roles, the compiled firewall in the Firewall Builder repository and the reachability of the Icinga2 master before it changes anything on the host, so a missing prerequisite no longer aborts the run halfway through, after SSH was already hardened.
* Handlers that a role notified before a later task failed run anyway, so a changed configuration reaches the running service; until now it stayed inactive, also across later runs, which reported no change (all playbooks).
* **playbook:acme_sh**: No longer runs the `apps` role, since the `acme_sh` role installs curl, openssl and tar itself.
* **role:acme_sh**: Installs a certificate again when the installed file differs from the one acme.sh issued, which replaces a placeholder that an aborted run left behind.
* **role:apache_solr**: Downloads Solr from the Apache CDN, which is much faster than the Apache archive, and falls back to the archive for releases the CDN no longer carries.
* **role:apache_solr**: The run aborts for a user whose password equals the username, since Solr 9.11 and newer reject such logins.
* **role:apache_solr**: A role holding the `all` permission is allowed every request, also one that a more specific permission of another role matches first.
* **role:apache_solr**: The Solr program files belong to `root`, as with Solr's own installation script, and files that Solr creates are no longer readable by other users.
* **role:apache_httpd**: Bump the OWASP Core Rule Set to 4.30.0.
* **role:apache_httpd**: The OWASP Core Rule Set is downloaded on the Ansible controller, so the target no longer needs access to GitHub.
* Downloads from the Internet, such as release tarballs, GPG keys, git clones, GitHub release lookups and package installations, are retried up to three times, so a brief outage of a download source or package repository no longer aborts the run (all roles).
* **role:monitoring_plugins**: On Linux, the role no longer stops the Icinga2 agent while it deploys the plugins, so a run no longer interrupts the monitoring of the host.
* **role:icinga2_agent**: The role runs `icinga2 node setup` and restarts the agent only when the agent needs a new certificate or its settings changed, instead of on every run.
* LFOps requires community.general 7.0.0 or newer (still below 9.0.0), which `ansible-galaxy collection install linuxfabrik.lfops` pulls in, while a manually maintained collection list has to be raised.
* **plugin:bitwarden_item**: The lookup syncs the Bitwarden vault once per Ansible run instead of every 60 seconds, which makes runs with many lookups considerably faster, since each sync makes `bw serve` list the whole vault. Before it creates a missing item, it syncs again, so an item created elsewhere during the run is not created a second time.
* **role:icingaweb2_module_generictts**: Downloads the module from Linuxfabrik, who maintain it since Icinga archived the original repository. The tarballs of v2.1.0 are identical.
* **role:duplicity**: `/var/lib/aide` is backed up by default, so that the AIDE database can be compared with a copy outside the host. Hosts without AIDE are not affected.
* **role:monitoring_plugins**: The source install removes plugins that an earlier run deployed and the checked-out version no longer carries.
* **role:monitoring_plugins**: The source install deploys the dependency versions pinned in the monitoring-plugins lockfiles and, for a release, the Linuxfabrik library release they pin, instead of the newest versions of both.
* **playbook:icinga2_agent, playbook:setup_basic, playbook:setup_icinga2_master**: The Monitoring Plugins repository is only registered for `monitoring_plugins__install_method: 'package'`.
* **role:fail2ban**: The `portscan` jail no longer bans TCP scans that send no plain SYN, such as FIN, NULL, Xmas and ACK scans, since they find no open port on a stateful firewall.
* **role:grafana**: Grafana no longer updates its preinstalled plugins on every start, so datasources such as InfluxDB and Prometheus no longer disappear from the web interface when the plugin download server is unreachable.
* **role:grafana**: `grafana.ini` follows the file that current Grafana packages ship, so deploying it only changes the settings LFOps manages. As a side effect, recording rules time out after 30 seconds instead of 10.
* **plugin:bitwarden_item, module:bitwarden_item**: A run against a vault that contains no items at all aborts instead of creating the first one, because `bw serve` briefly reports an empty vault after every sync ([bitwarden/clients#23283](https://github.com/bitwarden/clients/issues/23283)).
* **role:repo_postgresql**: The PostgreSQL version repositories take precedence over the distribution's packages of the same name, so on RHEL 10 an install or update no longer switches a PostgreSQL server from the PGDG build to the AppStream build, which uses a different file layout.
* **role:apache_httpd**: Responses of type `text/markdown` are compressed like HTML, so the Markdown versions of pages that CMSs such as Grav hand to AI agents no longer go out uncompressed.
* **role:grav**: The README lists setting `session.secure` as a manual step behind a reverse proxy that terminates TLS, where Grav sends its session cookies without the `Secure` flag.
* **role:gitlab_ce**: `gitlab_ce__rb_external_url` supports `https://` behind a reverse proxy that terminates TLS, and the README recommends it there. With `http://`, GitLab's session cookie goes out without the `Secure` flag.
* **role:grafana**: The login cookie carries the `Secure` flag when `grafana__root_url` is an `https://` URL. `grafana__cookie_secure` overrides this.
* **playbook:setup_basic**: The mail and reboot roles run before the security roles, so a first run against a fresh host files the reboot request that a changed crypto policy, SELinux state or kernel module blocklist needs. Until now the reboot mechanism was deployed further down the playbook and such a change could only be reported to the operator.
* **role:network**: The reminder that NetworkManager may have to be restarted by hand is printed only when a connection profile actually changed, instead of on every run.

### Deprecated

* **role:repo_monitoring_plugins**: The role only removes the former Monitoring Plugins repository and will be removed in a future release.

### Removed

* **collection, execution-environment**: Installing LFOps no longer pulls in the `ngine_io.cloudstack` collection, and the execution environment no longer ships it or the `cs` Python library, since no role uses them anymore.
* **playbook:apache_tomcat**: The playbook no longer enables the EPEL and CRB repositories or installs `policycoreutils`, none of which Tomcat needs, and `apache_tomcat__skip_repo_baseos` has no effect.
* **role:github_project_createrepo**: `github_project_createrepo__webserver_user` is gone, since the web server no longer gets an ACL entry; remove it from your inventory.

### Fixed

* **role:apache_httpd**: The `wordpress` vHost template no longer blocks images and comments of a WordPress site that is reached on a non-default port, such as `https://wordpress.example.com:8443`, as hotlinking and comment spam.
* **role:moodle**: Moodle is reachable when `moodle__url` ends with a slash or contains a path, such as `https://learning.example.com/`. Apache sent these requests to another vHost.
* **role:icingaweb2_module_grafana**: Graphs load again when `icingaweb2_module_grafana__url` contains a port, such as `https://grafana.example.com:3000`. The port was dropped from the module configuration.
* **role:aide**: The check no longer reports the runtime data of LibreNMS, the WordPress downloads of WP-CLI below `/root/.wp-cli/cache`, or the caches, the LibreOffice profile and the Ansible temporary files in the home of the web server user, `/usr/share/httpd`. Each of them blocked the database update after the nightly system update.
* **role:redis**: Hosts without the Redis package of packages.redis.io, such as Remi and the distribution packages, no longer warn `Skipped '/usr/lib/redis/modules' path due to this access issue` on every run.
* **role:lvm**: A PV on a partition of an SD card or eMMC disk, such as `/dev/mmcblk0p1`, is rescanned before the resize, so a grown disk is picked up.
* **role:lvm**: A PV listed by a symlink such as `/dev/disk/by-id/...` on a SCSI or NVMe disk is rescanned before the resize, so a disk grown on the hypervisor is picked up.
* **role:lvm**: Runs on virtio disks (`/dev/vd*`) no longer warn `Rescan path does not exist for device` for every PV.
* **role:monitoring_plugins**: Hosts without custom plugins in `host_files/<host>/usr/lib64/nagios/plugins` no longer print an `Unable to find` warning on every run.
* **role:mariadb_server**: MariaDB 10.6 before 10.6.18 and 10.11 before 10.11.8 start again with the configuration of the role, which sets `innodb_snapshot_isolation`, a variable these releases do not know.
* **role:icingaweb2_module_grafana**: `icingaweb2_module_grafana__skip_monitoring_plugins_graphs_config` is back, so a host can again be kept out of runs with the `icingaweb2_module_grafana:monitoring_plugins_graphs` tag, the same way `icingaweb2_module_director__skip_basket_import` works for the Director basket.
* **role:nextcloud**: Nextcloud generates previews of JPEG images again, which failed because the role stored the JPEG preview quality as text instead of a number.
* **role:monitoring_plugins**: On RHEL, a run no longer rewrites `/etc/dnf/plugins/versionlock.list`, which AIDE reported after every run ([#596](https://github.com/Linuxfabrik/lfops/issues/596)).
* **role:system_update**: `--tags system_update:state` no longer aborts on an undefined variable.
* **role:nextcloud**: notify_push waits for MariaDB, Redis and Valkey at boot and restarts after a failure, instead of staying down when it started before the database was ready.
* **role:nodejs**: `nodejs__dnf_module_stream` works on RHEL 10, which has no modules, by installing the versioned package such as `nodejs24`. On RHEL 8 and 9 it no longer reports a change on every run, and on Debian and Ubuntu it no longer makes the run fail ([#374](https://github.com/Linuxfabrik/lfops/issues/374)).
* **role:opensearch**: Heap and environment settings take effect on Debian and Ubuntu, where the role wrote them to `/etc/sysconfig/opensearch` instead of `/etc/default/opensearch` ([#512](https://github.com/Linuxfabrik/lfops/issues/512)).
* **role:icingaweb2_module_vspheredb**: On RHEL 10, the SELinux file context for the daemon's runtime directory is set instead of aborting the run with `conflicts with equivalency rule '/var/run /run'`.
* **role:redis**: Redis 8 from packages.redis.io loads the modules the package ships (JSON, Search, Bloom, TimeSeries), as its own configuration does; `redis__skip_packaged_modules: true` keeps them off. Redis restarts once ([#589](https://github.com/Linuxfabrik/lfops/issues/589)).
* **role:redis**: Redis 8.0 starts with the deployed configuration and loads the modules of the installed Remi packages (`redis-json`, `redis-bloom`, `redis-timeseries`); the configuration loaded modules from paths that do not exist ([#346](https://github.com/Linuxfabrik/lfops/issues/346)).
* **role:telegraf**: A `"` or `\` in a username or password no longer breaks the configuration.
* **role:logstash**: `.rpmnew` files of `/etc/default/logstash` and `/etc/logstash/pipelines.yml` are removed after a package upgrade ([#515](https://github.com/Linuxfabrik/lfops/issues/515)).
* **role:mongodb**: mongod keeps logging to `/var/log/mongodb/mongod.log` after the log rotation on RHEL; until now its log was lost until the next restart. The next run restarts mongod once ([#531](https://github.com/Linuxfabrik/lfops/issues/531)).
* **role:mongodb**: `mongodb__conf_replication_oplog_size_mb` can be set as a number on Debian and Ubuntu; the run aborted with `object of type 'int' has no len()` ([#532](https://github.com/Linuxfabrik/lfops/issues/532)).
* A changed schedule of a timer no longer runs its job right away when the new time already passed since the last run, for example system updates and reboots in the middle of the day after switching from weekly to daily (acme_sh, aide, clamav, duplicity, fangfrisch, gitlab_ce, glpi_agent, grav, influxdb, lynis, mariadb_server, matomo_import_logs, mongodb, moodle, nextcloud, podman_containers, postgresql_server, schedule_reboot, system_update, systemd_unit, wordpress).
* **role:postgresql_server**: The role works with the community.postgresql collection 5.x, where it aborted with "Unsupported parameters ... port".
* **plugin:uptimerobot_mwindow, role:uptimerobot**: Daily, weekly and monthly maintenance windows can be created again after UptimeRobot started to reject them with HTTP 400, changing the duration of a weekly window with several days works, and a failed API call names UptimeRobot's reason instead of only "Bad Request".
* **role:aide**: AIDE no longer reports the files Collabora caches below `/opt/cool/cache` for every new user and document setting.
* **role:aide**: The check no longer reports the rotation of the dnf logs, the container storage of rootless podman below `/opt`, the service state of GitLab, the shell history of service users or the mongosh logs of root. Each of them blocked the database update after the nightly system update.
* **role:nextcloud**: The README example for the Collabora WOPI allow list includes the Nextcloud host next to the reverse proxy. With the proxy alone, documents fail to load with "Unauthorized WOPI host".
* **role:podman_containers**: On Debian and Ubuntu, rootless containers can be updated by `podman auto-update` and reached with `podman exec` right after the first run, not only after a reboot.
* **role:podman_containers**: A container whose settings change is restarted, so that it runs with the new settings instead of the ones it was started with.
* **role:login, playbook:setup_basic**: A run no longer locks Ansible out of `sudo` with "Missing sudo password" on hosts set up by LFOps before v6.0.0, where the Ansible user gets its password-less sudo from `/etc/sudoers.d/lfops_login` only, and the role leaves that file alone if `login__passwordless_sudo_group` is not set.
* **role:monitoring_plugins**: Windows hosts install a monitoring plugins release that was rebuilt on download.linuxfabrik.ch, such as 8.1.0, where the download failed with a 404.
* **role:monitoring_plugins**: On a host switched from the source install to the package, the plugins use the Linuxfabrik library of the package again, since the role removes what the source install left behind, such as `/usr/lib64/nagios/plugins/lib`.
* **role:python_venv, role:duplicity**: A venv built with another Python minor release than `python_executable` is rebuilt, so the duplicity venv on RHEL 8 hosts set up before the switch to Python 3.11 installs again instead of failing with `ResolutionImpossible`.
* **role:repo_mydumper**: A run against an unchanged host no longer reports the signing key of the mydumper repository as changed from time to time.
* **role:nextcloud**: A run after a failed installation installs Nextcloud, where it skipped the installer because the failed attempt had left a `config.php` behind.
* **role:nextcloud**: A run against an unchanged host no longer restarts PHP-FPM and reports no change at all.
* **role:nextcloud**: `nextcloud-update` adds missing primary keys after an update, which Nextcloud leaves to the administrator.
* **role:nextcloud**: A `nextcloud__datadir` other than `/data` gets the ownership and the SELinux label Nextcloud needs, which the role only ever set on `/data`.
* **role:aide**: The check no longer reports the sockets and locks of gpg-agent and keyboxd below `/root/.gnupg`, or LVM metadata archives coming and going in `/etc/lvm/archive`.
* **role:lvm**: A run no longer writes an LVM metadata archive for every PV whose size did not change, which AIDE reported, and `--check` no longer reports such PVs as changed.
* **role:monitoring_plugins**: The icons in IcingaWeb2 belong to root instead of the user ID of the Ansible controller user, and a run no longer rewrites all of them, which AIDE reported.
* **role:mariadb_server**: A run no longer logs denied logins of `root`, and the `proxies_priv` warning goes away with `skip_name_resolve`, also after a change of the host name.
* **role:sshd**: `sshd__gssapi_authentication` and `sshd__x11_forwarding` take effect on RHEL 9 and newer, where the `50-redhat.conf` of the package kept GSSAPI authentication and X11 forwarding on ([#354](https://github.com/Linuxfabrik/lfops/issues/354)).
* **role:kernel_settings**: A run no longer fails on hosts whose kernel settings still contain a `sunrpc.*` value from an earlier run while the `sunrpc` module is not loaded.
* **role:duplicity**: Swift backups work again on fresh installations, since the role installs `python-keystoneclient` into the virtual environment.
* **role:aide**: The check no longer reports the rotation of the audit logs or a new directory below `/var/log`.
* **role:aide**: The Collabora jails below `/opt/cool/child-roots` are no longer reported as added and removed files after every restart of `coolwsd`.
* **role:apache_httpd**: The 502 error page shows its separators correctly instead of a stray `Â`.
* **playbook:setup_basic**: A fresh VM whose image brings a dead nameserver no longer aborts at the reachability check of the Icinga2 master, since the network is configured ahead of that check.
* **role:duplicity**: `duba.service` fails if the backup or the cleanup of any single source fails. Until now it usually reported success, so a failed backup of one source went unnoticed. The journal names the failed duplicity command.
* **plugin:gpg_key, plugin:nextcloud_occ_app, plugin:nextcloud_occ_app_config, plugin:nextcloud_occ_system_config**: The modules no longer fail with `env: 'python3': No such file or directory` on RHEL 8 hosts that have no `python3` installed, for example when a run skips the role that installs it, and without Mitogen. All in-house modules use the Python found by interpreter discovery or set in `ansible_python_interpreter`, like the modules of ansible-core.
* **role:repo_mongodb**: The repository for MongoDB 8.2 and 8.3 can be installed, the role fetched a signing key that does not exist.
* **role:librenms**: Renaming an API token or changing its expiry in the web interface works again, as do the API v0 calls that update devices, device groups, locations, ports and services. Apache rejected their `PATCH` requests.
* **role:nextcloud**: `nextcloud-ldap-show-remnants.timer` reports the LDAP remnants once a month; until now it started the app update instead.
* **role:nextcloud**: CalDAV and CardDAV clients find their endpoints through `/.well-known/caldav` and `/.well-known/carddav` without a reverse proxy that redirects them, and `/remote/`, `/ocm-provider/` and the other `.well-known` URLs reach Nextcloud again.
* **role:nextcloud**: httpd starts when `mod_dav` is loaded; the vHost turned WebDAV off outside a directory context, which httpd rejects.
* **role:system_update**: On Debian and Ubuntu, the update check before the update window waits for a busy package manager and reports a failed refresh of the package lists, instead of silently announcing updates from stale lists.
* **role:system_update**: On Debian and Ubuntu, the weekly update waits up to 10 minutes for the apt and dpkg locks instead of failing when, for example, an Ansible run holds them.
* **role:system_update**: On Debian and Ubuntu, the weekly update repairs a dpkg run that was cut short (`dpkg --configure -a`) instead of failing every week until someone repairs the host by hand.
* **role:system_update**: On Debian and Ubuntu, the journal of the weekly update no longer shows `yes: standard output: Broken pipe`.
* **role:system_update**: On Debian and Ubuntu, the weekly update removes kernels apt no longer needs before installing new ones, the way unattended-upgrades does, so old kernels no longer fill up `/boot`. The running kernel, the newest one and held packages are kept.
* **role:mariadb_server**: The dump script is installed as `/usr/local/libexec/mariadb-dump`, so it no longer shadows MariaDB's own `mariadb-dump` client for users with `/usr/local/bin` in their `PATH`.
* **role:mongodb**: The file-based dump (`mongodb__dump_method_file_based`) locks the instance also with authorization enabled and works on Debian and Ubuntu, and a dump that cannot lock the instance fails instead of copying the data files of the running instance.
* **role:mongodb**: A run against an unchanged host no longer reports the dump user and the users from `mongodb__users__*_var` as changed; a password changed in the inventory is still applied.
* **role:mongodb**: `mongodb__dump_only_if_hidden: true` dumps only hidden members of a replica set, as documented; until now it dumped every member.
* **role:mongodb**: `mongodb-dump.service` fails when `mongodump` fails, instead of reporting success with an incomplete dump.
* **role:repo_opensearch**: A host switched from OpenSearch 2.x to 3.x gets the signing key of 3.x, instead of the key the controller downloaded first, which made `dnf` fail with `repomd.xml GPG signature verification error: Bad GPG signature`.
* **role:repo_opensearch**: The repository works on Ubuntu 26.04, where `apt` ignored the signing key because of its file name.
* **role:opensearch**: The transport layer uses the key of the transport certificate (`node-transport.key`) instead of the one of the HTTP certificate, so cluster nodes whose two certificates have different keys can talk to each other.
* **role:repo_proxysql, playbook:proxysql**: ProxySQL 3.0 installs on RHEL 8, and ProxySQL 2.4 and 2.5 on RHEL 9, instead of failing with a 404 on the repository metadata. The `proxysql` playbook deploys EPEL (and CRB), which ProxySQL 3.0 on RHEL 8 needs.
* **role:repo_proxysql**: With `repo_proxysql__mirror_url`, the repository is expected below the same path as on repo.proxysql.com (`/ProxySQL/proxysql-<version>.x/...`), instead of a path that never matched it.
* **role:logstash**: `/etc/default/logstash` is no longer world-readable and no longer reported as changed after every Logstash package update, which restarted Logstash. The first run after updating LFOps restarts Logstash once.
* **role:apache_httpd**: A vHost with `state: 'absent'` can be removed without a `template`, instead of aborting with `'dict object' has no attribute 'template'`.
* **role:exoscale_vm**: The role works again after Exoscale switched off its CloudStack API, and no longer needs the `exo` CLI, the `cs` Python library or `exoscale_vm__account` on the Ansible controller.
* **role:network**: `network_state` applies on hosts whose active connection profile, such as `cloud-init eth0` or `System eth0`, still stores DNS servers, instead of aborting with `ipv6.dns: this property is not allowed for 'method=disabled'`. DNS servers that `dns-resolver` does not list are removed from that profile.
* **role:postfix**: A changed `postfix__inet_protocols` takes effect, since the role restarts Postfix for it instead of only reloading it.
* **role:cloud_init**: NetworkManager manages `/etc/resolv.conf` right after cloud-init's `99-cloud-init.conf` is removed, instead of only after the next reboot or NetworkManager restart.
* **role:nextcloud**: A Nextcloud upgrade no longer logs an error from a repair step of the `dav` app and no longer rebuilds the calendar search index every time.
* **role:nextcloud**: The admin section of the `support` app is hidden again.
* **role:monitoring_plugins**: The package install works on SLE, where it failed with `No provider of 'linuxfabrik-monitoring-plugins-<version>*' found`.
* **role:postgresql_server**: `postgresql-dump` also saves the roles and tablespaces (`globals.sql.gz`), without which a restored database fails on every object owner, no longer skips the `postgres` database and databases whose name starts with `template`, and uses the client tools of the installed PostgreSQL version.
* **role:postgresql_server**: Clients connecting to `localhost` are accepted over IPv6 as well, where they were rejected with `no pg_hba.conf entry for host "::1"`.
* **module:bitwarden_item**: The module no longer edits the item and reports a change on every run when `collection_id` is set.
* **plugin:bitwarden_item, module:bitwarden_item**: Items without a username work with Bitwarden CLI 2026.10.0, where the lookup and the module aborted with `KeyError: 'username'`.
* **role:kernel_settings**: The role no longer prints the `Conditional result ... was of type 'int'` deprecation warning.
* **role:apache_tomcat**: Changing `apache_tomcat__env_xms`, `apache_tomcat__env_xmx` or `apache_tomcat__env_xx` restarts Tomcat, so the new JVM settings take effect right away instead of at the next restart.
* **role:apache_tomcat**: `apache_tomcat__skip_admin_webapps: true` no longer aborts the run on a host with the ROOT webapp.
* **role:apache_tomcat**: The role no longer aborts on an undefined variable when `apache_tomcat__server_xml_connector_connection_timeout`, `apache_tomcat__webapps_manager_web_xml_max_file_size` or `apache_tomcat__webapps_manager_web_xml_max_request_size` is not set in the inventory, and uses the documented defaults.
* **playbook:mariadb_server**: Debian and Ubuntu install mydumper from the official mydumper repository, so the daily dump also works on Debian 13 (all playbooks that install MariaDB with dumps).
* **role:mariadb_server**: The daily dump works with the mydumper that Debian and Ubuntu ship.
* **role:lvm**: `growpart: true` grows the right partition when the PV is partition 10 or higher, such as `/dev/vda12` or `/dev/nvme0n1p12`. Until now it addressed partition 2 there, or failed.
* **role:lvm**: The argument specs name the `fstype` subkey correctly and accept `lvm__lvs__dependent_var` and `lvm__vgs__dependent_var`, so the `setup_*` playbooks can inject LVs and VGs.
* **role:repo_mongodb**: On RHEL, the repository key always matches `repo_mongodb__version`, instead of reusing the key of an earlier run with another version, which made the installation fail.
* **role:repo_mongodb**: On Ubuntu, the role points to MongoDB's Ubuntu repository instead of the Debian one, so MongoDB can be installed there.
* **role:redis, role:valkey**: On RHEL, a run after a reboot no longer reports the configuration file as changed, since the role keeps the ownership that the package's tmpfiles.d rule restores at every boot.
* **role:borg_local**: The Icinga downtime for `clamd@scan` during a backup is set, where Icinga had rejected the request as invalid JSON.
* **role:acme_sh**: `--check` no longer aborts on hosts without the acme.sh package. On hosts that still have the former `/opt/acme.sh` installation, it shows the certificates a real run would issue.
* **role:acme_sh**: Removes a copy of the acme.sh program left in `/root/.acme.sh` by an earlier `acme.sh --install`, and the lines of the former installation in `/root/.bash_profile`, `/root/.profile` and `/root/.zshrc`. A `/root/.acme.sh` that holds anything else, such as certificates or an account, is kept and reported at the end of the play.
* **role:acme_sh**: A changed `acme-sh.service` or `acme-sh.timer`, for example a new schedule, takes effect in the same run. Until now systemd kept the old unit until the next reload.
* **role:clamav**: A changed `clamav-clamdscan.service` takes effect in the same run. Until now only a changed timer reloaded systemd.
* **role:gitlab_ce**: A changed `gitlab-dump.service` takes effect in the same run. Until now only a changed timer reloaded systemd.
* **role:gitlab_ce**: A configuration change no longer aborts the run while Puma is still finishing its requests. The role leaves the restart to `gitlab-ctl reconfigure`, which restarts only the services whose configuration changed and Puma without dropping requests, instead of restarting every service, and waits until GitLab answers again.
* **role:influxdb**: A changed `influxdb-dump.service` or `influxdb-dump.timer`, for example a new schedule, takes effect in the same run. Until now systemd kept the old unit until the next reload.
* **role:mariadb_server**: A changed `mariadb-dump.service` takes effect in the same run. Until now only a changed timer reloaded systemd.
* **role:mongodb**: A changed `mongodb-dump.service` takes effect in the same run. Until now only a changed timer reloaded systemd.
* **role:postgresql_server**: A changed `postgresql-dump.service` takes effect in the same run. Until now only a changed timer reloaded systemd.
* **role:duplicity**: A changed backup schedule or `duba.service` takes effect in the same run, also with `--tags duplicity:configure`. Until now it applied only with a full run, or after the next change of the timer file.
* **role:files**: Symlinks from `files__symlinks__*_var` no longer change the mode, owner and group of their target. Until now a link to a directory set the directory to `0644` and `root:root` by default, so services could no longer enter it.
* **role:icingaweb2_module_grafana**: `graphs.ini` no longer keeps the graphs of monitoring plugins that were removed or renamed upstream.
* **role:uptimerobot**: Alert contacts past the first 50 are found, so `alert_contacts` of a monitor can name them and `uptimerobot_alert_contact_info` lists all of them.
* **role:uptimerobot**: Port monitors with `sub_type` `https`, `ftp`, `smtp`, `pop3` or `imap` are created instead of rejected by UptimeRobot, a changed `sub_type` takes effect on an existing monitor, and a port monitor with a `sub_type` no longer reports a change on every run.
* **role:php**: `--tags php:ini` also deploys the PHP-FPM pools, so a changed `php__ini_memory_limit__*_var`, `php__ini_max_execution_time__*_var` and the like take effect in PHP-FPM, not only on the command line.
* **role:chrony**: Without `chrony__ntp_pools` or `chrony__ntp_servers` in the inventory, chronyd synchronises with `ntp.metas.ch`, the time server of the Swiss Federal Institute of Metrology, instead of running without a time source. If both are set to `[]`, the role aborts.
* **role:icingaweb2**: PHP gets a `memory_limit` of 256M instead of 128M, so the CSV and JSON exports, such as History > Event Overview, handle about twice as many rows before they break off with "Allowed memory size exhausted". Override it with `php__ini_memory_limit__group_var` / `php__ini_memory_limit__host_var`.
* **role:nextcloud**: The README documents the app states correctly. `state: 'present'` installs an app but leaves it disabled; use `state: 'enabled'` to switch on an app, including the apps the role disables by default, such as `activity`, `notifications` and `systemtags`.
* **role:icingaweb2_module_grafana**: With JWT authentication, the Grafana graphs in IcingaWeb2 no longer switch to the Grafana login after 30 seconds. The token stays valid for 3 days; adjust with `icingaweb2_module_grafana__auth_jwt__expires`.
* **playbook:uptimerobot**: The playbook runs as a normal user on the Ansible controller, without `--become`. Until now it aborted because it tried to write `/var/log/linuxfabrik-lfops.log` on the controller, and with `--become` it could not find the API key in `~/.uptimerobot`.
* **role:repo_epel**: On RHEL 10 hosts with `repo_epel__mirror_url` set, EPEL is installed from the mirror. Until now dnf failed to download the EPEL metadata, which also broke every other dnf call on the host.
* **role:php**: Apache starts after PHP-FPM at boot, so the first PHP requests after a reboot no longer fail.
* **role:firewall**: With `firewall__fwbuilder_repo_url`, a repository without a compiled firewall for the host aborts the run before the role stops any firewall. Until now the role stopped a running firewalld, iptables, nftables or ufw first and left the host without a firewall.
* **role:acme_sh**: On Debian and Ubuntu, certificates are installed with `systemctl reload apache2` instead of `systemctl reload httpd`, which does not exist there; certificates installed before keep the old command until they are issued again.
* **role:mount**: A freshly created filesystem is relabelled for SELinux after mounting, so confined services such as Apache httpd can access it without a manual `restorecon`.
* **role:mirror**: Repairs the ACL mask of `mirror__base_path` when a chmod narrowed it to `r-x`, which made reposync fail with `Permission denied` on every new repository.
* **role:mirror**: Runs on minimal installations such as Rocky 10, where it aborted for lack of `setfacl`, since the role installs acl, createrepo and git itself instead of the mirror playbook running the apps role.
* **role:openvpn_server**: The role runs on a minimal RHEL 10 installation, where it aborted for lack of `openssl` while generating Diffie-Hellman parameters, which OpenVPN 2.7 no longer needs.
* **role:apache_solr**: Passwords no longer show up in the output of a run, and a user without `state` no longer aborts it.
* **role:apache_solr**: The role runs without EPEL on RHEL and no longer restarts Solr on every run.
* **role:apache_solr**: A role with several permissions takes effect, where Solr discarded it as invalid so far.
* **role:apache_solr**: Users can log in on hosts without `xxd`, such as a minimal RHEL 9, where the role wrote an empty password hash.
* **role:apache_httpd**: On Debian and Ubuntu, the first run against a fresh host no longer fails reloading Apache, since the role points `PidFile` to where `apachectl` looks; a server started by an earlier version of the role is restarted once.
* **role:apache_httpd**: Apache reloads after the OWASP Core Rule Set is updated, instead of applying the previous rules until its next reload.
* **role:apache_httpd**: The OWASP Core Rule Set deploys on Debian and Ubuntu, where the run failed until now, and on Ubuntu 22.04, whose ModSecurity 2.9.5 is too old for it, the run aborts with a message saying so.
* **playbook:redis, playbook:setup_mastodon**: On Debian and Ubuntu, the playbooks no longer abort with "'redis__skip_repo_redis' is undefined" or "'setup_mastodon__skip_repo_redis' is undefined" unless the skip variable is set in the inventory.
* **role:grafana_grizzly, role:monitoring_plugins_grafana_dashboards**: On Grafana 13.1 and newer, dashboards that the roles add or update stay in their folder, instead of landing in the root, where viewers and the graphs embedded in IcingaWeb2 got "403". A run also moves back the dashboards that earlier runs left in the root.
* **role:grafana_grizzly**: Datasources can be updated again on Grafana 13. The playbooks with `grafana_grizzly` enable Grafana's `datasourceLegacyIdApi` feature toggle, which grizzly needs, and restart Grafana for it.
* **playbook:monitoring_plugins_grafana_dashboards**: Creates the Grafana service account that grizzly uses. The playbook handed it to the wrong role, so it was only created by other playbooks.
* **role:aide**: The AIDE check no longer fails on hosts with fwupd, where it reported `/etc/fwupd/fwupd.conf` as changed after the first start of the fwupd daemon.
* **role:aide**: The AIDE check no longer fails on hosts with an EFI system partition, where it reported the files below `/boot/efi` as changed a while after the database was created.
* **role:system_update**: The security lane installs hot-fixes that need a newer package from BaseOS or AppStream, such as the kernel on Rocky 8.3, instead of failing on every run.
* **role:system_update**: On Fedora 42 and later, where `/usr/local/sbin` is a link to `/usr/local/bin`, the role no longer deletes the `update-and-reboot` script right after deploying it, so the weekly update runs again.
* **role:monitoring_plugins**: A run against an unchanged host no longer reports a change for the legacy dependency list of the source install.
* **roles**: Tasks that run on the Ansible controller no longer escalate via sudo when the inventory sets `ansible_become: true`, where they failed without passwordless sudo on the controller or ran as root and left root-owned files in `/tmp`.
* **plugin:bitwarden_item, module:bitwarden_item**: A failed sync of the Bitwarden vault, such as an "HTTP Error 400: Bad Request" or a timeout of `bw serve`, is tried again after 10, 30 and 60 seconds instead of aborting the run right away.
* **role:aide**: Before it creates the database, the role also waits for running `dnf-automatic` jobs on the Red Hat family and for the update jobs of the system_update role on every platform, not only for the apt jobs on Debian and Ubuntu. An update during the initialisation left files in the database that the first check then reported.
* **playbook:setup_basic**: With `setup_basic__skip_duplicity` or `setup_basic__skip_glances`, the playbook no longer builds the Python venv of the skipped role, which could abort the run with a pip error on hosts that do not back up with duplicity.
* **role:kernel_settings**: `sunrpc.*` settings, such as the `sunrpc.tcp_slot_table_entries` the mariadb_server role sets, survive a reboot. Until now the `sunrpc` module was not loaded again after a reboot on hosts without NFS, so TuneD could not apply the setting and the next run of the role failed in `tuned-adm verify`.
* **role:repo_baseos**: The Rocky Linux `security` repository works on Rocky 8 releases before 8.5, where dnf failed to download its metadata.
* **role:monitoring_plugins**: The source install deploys the OID lists and MIBs of the `snmp` plugin, which until now failed with "No such file or directory" on every host installed this way.
* **role:bind**: A secondary zone with `type: 'slave'` is saved to its file again, so the secondary answers it after a restart without waiting for the primary.
* **role:bind**: Reverse lookups for private and special-use addresses, such as `10.0.0.0/8` or `fd00::/8`, are answered locally, as BIND does by default, instead of waiting for the forwarders, which also no longer see the internal addressing.
* **role:kernel_settings**: The role works with fedora.linux_system_roles 2.5.0 and later, which a fresh installation of LFOps pulls in. Until now the run aborted with "kernel_settings_transparent_hugepages must be null, one of always, madvise, never" unless `kernel_settings__transparent_hugepages__*_var` and `kernel_settings__transparent_hugepages_defrag__*_var` were set.
* **role:system_update**: The AIDE database is only updated after an update if a check right before the update comes out clean, instead of relying on the last scheduled check, so changes made since then are no longer accepted along with the update; a check that cannot run at all is reported in a mail of its own.
* **role:fangfrisch**: `--tags fangfrisch:state` no longer aborts on an undefined variable.
* **role:firewall**: Hosts in one run with different `firewall__fwbuilder_repo_url` values each deploy `/etc/fwb.sh` from their own repository, instead of all from the repository of the first host.
* **role:firewall**: `firewall__firewall: 'iptables'` no longer aborts when there is no iptables file in the inventory's `host_files`, which the README describes as optional.
* **role:firewall**: A host switched away from `fwbuilder` or `iptables` stops, disables and masks that firewall instead of keeping it running next to the new one, and no longer reports a change on every run.
* **module:nextcloud_occ_app**: An `occ app:list` output or `installed_apps_json` that is valid JSON but not an object aborts with a clear message instead of a Python traceback.
* **module:nextcloud_occ_system_config**: Setting a key that does not exist yet to an empty value (`value: ''`) creates it. Until now the module reported no change and left the key missing.
* **module:uptimerobot_mwindow_info**: Monthly maintenance windows on day 1 to 7 of the month are reported with their day numbers instead of weekday names, e.g. `1-15` instead of `mon-15`.
* **role:system_update**: The daily security lane on Rocky refreshes the AIDE database after installing hot-fixes, like the weekly lane, so the next AIDE check no longer fails on every file a hot-fix touched.
* **role:lvm**: On Debian and Ubuntu, mounting an LV no longer fails on `restorecon`, which only runs where SELinux is enabled.
* **role:lvm**: The role installs the tools for XFS and ext2/3/4, so creating the default XFS file system no longer fails on hosts without `mkfs.xfs`, such as those deployed from a Debian cloud image.
* **role:lvm**: `mount_owner`, `mount_group` and `mount_mode` apply to the mounted file system from the first run on. Until now they only took effect on the second run, which therefore reported a change.
* **role:lvm**: Removing an LV (`state: absent`) no longer aborts with `'dict object' has no attribute 'size'`.
* **role:lvm**: Removing a VG (`state: absent`) no longer aborts with `could not find 'pvs' key` when the entry lists no `pvs`.
* **role:lvm**: Shrinking an LV works. It needs `force: true` in addition to `shrink: true`; with `shrink: true` alone the run aborted.
* **role:lvm**: The role installs `lvm2`, so it also works on hosts installed without LVM, such as those deployed from a cloud image.
* **role:lvm**: Creating or resizing a PV no longer fails on RHEL 8 with `SyntaxError: future feature annotations is not defined`.
* **role:network**: Hosts without `network_connections` or `network_state` no longer run the upstream network role at all, which occasionally hung the play for good after it had finished.
* **role:kernel_settings**: The role no longer aborts on Ubuntu 22.04 with `Verification failed, current system settings differ from the preset profile`.
* **role:tools**: The role no longer aborts on Ubuntu with `No package matching 'cloud-utils-growpart' is available`.
* **role:python_venv**: The role no longer aborts on Debian and Ubuntu with `Failed to import the required Python library (packaging)`.
* **playbook:setup_basic**: The play no longer aborts on Debian and Ubuntu with `No package matching 'network-scripts' is available` or `'__yum_utils__package' is undefined`, since it runs the `network` and `yum_utils` roles on the Red Hat family only.
* **role:monitoring_plugins**: The plugin icons for IcingaWeb2 are no longer missing when the directory `ansible-playbook` runs in contains `.svg` files.
* **role:monitoring_plugins**: The source install deploys only the modules of the Linuxfabrik library, as the one-line installer does, removes the documentation and development files earlier runs placed next to them, and removes plugin assets the checked-out version no longer carries.
* **role:monitoring_plugins**: The source install no longer reports a change on every run once the plugins have run.
* **role:monitoring_plugins**: The source install no longer clears the setuid bit of the distribution's `check_icmp` and `check_dhcp` on every run.
* **role:monitoring_plugins**: `--tags monitoring_plugins:remove` no longer aborts on Debian and Ubuntu, and also removes the version lock of the SELinux package, the SELinux policy module, the Debian conffiles and every file a release of the plugins ever installed.
* **role:monitoring_plugins**: The source install sets `nagios_run_sudo` and, for Monitoring Plugins releases after 8.0.0, loads the SELinux policy module, so plugins run through sudo under enforcing SELinux on RHEL 8 and 9.
* **role:monitoring_plugins**: The source install of a release before 8.0.0 no longer fails on the missing logging sudoers file.
* **role:fail2ban**: An empty `fail2ban__jail_portscan_allowed_ports` exempts no port, so the `portscan` jail bans every denied connection attempt instead of none.
* **role:fail2ban**: The `portscan` jail no longer bans a server that a local proxy talks to because the firewall logged a late TCP packet or an ICMP error from it, for example the final FIN of a half-closed connection.
* **role:grafana**: The `from_name` of `grafana__smtp_config` is used as the sender name of emails, instead of the value of `skip_verify`.
* **module:bitwarden_item**: The module works with the Mitogen strategy, where it aborted with `MODULE FAILURE` on every run, for example when the `grafana` role stores its service account tokens.
* **plugin:bitwarden_item, module:bitwarden_item**: Running against several hosts in parallel no longer creates duplicates of a Bitwarden item, whether the item is new or has existed for a long time, so the next run no longer aborts with "Found multiple Bitwarden items".
* **role:wordpress**: The installation no longer aborts at `wp core download` when Ansible connects as `root` without privilege escalation.
* **role:wordpress**: A second run no longer reports the `wp-config.php` constants, `wordpress__plugins` and `wordpress__theme` as changed.
* **role:wordpress**: The WXR file in `wordpress__wxr_export` is imported when the role installs WordPress; the import never ran before.
* **role:wordpress**: Ansible's temporary directory for `apache` is `/usr/share/httpd/.ansible/tmp` instead of a path with two trailing spaces.
* **role:apache_httpd**: The `wordpress` vHost blocks direct calls to the PHP files below `wp-includes/` and `wp-admin/includes/`, whose rules never matched.
* **role:postfix**: `postfix__compatibility_level` takes effect on Debian and Ubuntu as well, and defaults to the level the distribution ships, so Debian 13 and Ubuntu 26.04 run at `3.9` instead of `3.6` ([#364](https://github.com/Linuxfabrik/lfops/issues/364)).
* **role:mariadb_server**: On RHEL 10 with a `selinux-policy-targeted` older than `42.1.18-4.el10_2.3`, MariaDB runs confined in `mysqld_t` again, so web applications such as WordPress or Nextcloud reach its socket. Until now it ran in `initrc_t` there, and PHP-FPM failed to connect until the SELinux policy was updated and MariaDB restarted.
* **role:monitoring_plugins**: A package install that fails no longer leaves the Monitoring Plugins unlocked, so a later system update cannot move them past `monitoring_plugins__version`. The lock that existed before the run is set again.
* **role:monitoring_plugins**: A run against an unchanged host no longer reports changes for the package versionlock ([#353](https://github.com/Linuxfabrik/lfops/issues/353)).
* **role:collabora**: A run against an unchanged host no longer reports changes for the coolwsd log file and the ownership of `/etc/coolwsd`.
* **role:collabora**: The role runs on RHEL 10, since it no longer installs the distribution's `mythes` and `hunspell` dictionary packages, which Collabora does not need next to its own dictionary packages. Packages already installed are left in place ([#373](https://github.com/Linuxfabrik/lfops/issues/373)).
* **playbook:kvm_host**: The playbook deploys the EPEL repository, and CRB on Rocky 9 and newer, so it no longer fails to install `genisoimage` on RHEL 9 and 10 hosts that did not have EPEL set up already ([#375](https://github.com/Linuxfabrik/lfops/issues/375)).
* **playbook:chromium_headless, playbook:icingaweb2_module_pdfexport, playbook:repo_epel**: On Rocky 9 and newer, these playbooks enable the CRB repository together with EPEL, like every other playbook that deploys EPEL, since EPEL packages depend on packages from CRB.
* **role:repo_icinga**: On Fedora the role deploys Icinga's public repository instead of failing on a release package that Icinga no longer publishes ([#360](https://github.com/Linuxfabrik/lfops/issues/360)).
* **role:repo_postgresql**: The role no longer aborts on RHEL 10 right after deploying the repository ([#370](https://github.com/Linuxfabrik/lfops/issues/370)).
* **role:repo_collabora_code**: The role deploys the repository on RHEL 10 instead of failing on a missing template ([#377](https://github.com/Linuxfabrik/lfops/issues/377)).
* **role:lvm**: `growpart: true` works on minimal installations, where the role failed because nothing installed `growpart` ([#365](https://github.com/Linuxfabrik/lfops/issues/365)).
* **role:icinga2_agent, role:icinga2_master**: Icinga 2 starts after SSSD at boot as intended, so its early `sudo` calls no longer fail with `problem with defaults entries`; the ordering pointed at a unit that does not exist and never took effect ([#357](https://github.com/Linuxfabrik/lfops/issues/357)).
* **role:grafana**: With `grafana__auth_jwt: true`, the run no longer aborts at `generate JWT RSA private key` with `Cannot detect the required Python library cryptography` on hosts that lack it, because the playbooks deploying Grafana install `python3-cryptography` first.
* **role:clamav, role:sshd**: `--tags clamav:configure`, `--tags clamav:state` and `--tags sshd:state` no longer abort on an undefined variable, so a restart skipped with `lfops__skip_restart_handlers` can be caught up with `--tags <role>:state` as the README describes.
* **role:php**: A playbook that includes PHP, such as `setup_nextcloud`, no longer aborts at `Get PHP version` when it is run with another role's tags, for example `--tags apache_httpd`, against a host that has no PHP installed yet.
* **role:kdump**: `kdump__service_enabled: true` turns kdump on on RHEL 10 as well instead of leaving it off without an error, and a kdump that is supposed to run fails the run when its service cannot be managed ([#359](https://github.com/Linuxfabrik/lfops/issues/359)).
* **role:repo_mariadb**: On RHEL 10 the role aborts for MariaDB versions older than 10.11, which MariaDB publishes no RHEL 10 packages for, instead of deploying a repository that answers 404 and breaks every dnf transaction on the host.
* **role:repo_epel**: The role no longer aborts on RHEL 10, AlmaLinux 10 and CentOS Stream 10.
* **role:chrony**: `chronyd` on RHEL 10 no longer logs `Could not open keyfile` on every start, since the deployed `chrony.conf` loads no key file on any release.
* **role:nextcloud**: `nextcloud-update` sets the Icinga downtime again, taking the API user from `icinga2_master__downtime_api_user` (see Added) instead of from the `system_update__icinga2_api_user_login` removed in v8.0.0.
* **role:borg_local, role:schedule_reboot, role:tools**: The Icinga downtime around a backup or a reboot is set again when the deploying playbook does not run `icinga2_agent` itself, such as `bootloader`, `system_update` or `tools`, and the inventory only sets the mandatory `icinga2_agent__icinga2_master_cn`.
* **role:fail2ban**: The `apache-botsearch`, `apache-fakegooglebot`, `apache-nohome` and `apache-noscript` jail templates can be deployed again. Using one of them aborted the run.

### Security

* **role:telegraf**: The configuration, which contains credentials, and its backups are no longer world-readable (`0640 root:telegraf`).
* **role:nextcloud**: The login of the Icinga2 API user that `nextcloud-update` uses is stored as a systemd credential, encrypted on RHEL 9 and newer, instead of in the script, and the role deletes the older copies of the script that still contain it.
* **role:nextcloud**: The password of the Icinga API user no longer shows up in the process list while `nextcloud-update` sets or removes the downtime.
* **role:nextcloud**: `/usr/local/bin/nextcloud-update` is readable by root only.
* **role:nextcloud**: The database and admin passwords no longer show up in the process list during the installation.
* **role:nextcloud**: The installation aborts unless Nextcloud signed the downloaded release archive.
* **role:borg_local**: The repository passphrase and the Icinga2 API login are stored encrypted with `systemd-creds` on systemd 251 and newer instead of in plain text in `/etc/borg/borg-passphrase` and `/usr/local/bin/borg-backup`, so the passphrase is no longer readable in the backup it protects. The old files and their copies are deleted. On RHEL 8 and Ubuntu 22.04 they stay in plain text, in `/etc/credstore/`. `source /etc/borg/borg.conf` followed by `borg` still works for restores, as root.
* **role:schedule_reboot**: The Icinga2 API login and the Rocket.Chat webhook URL are stored encrypted with `systemd-creds` on systemd 251 and newer instead of in plain text in `/usr/local/libexec/do-reboot`. On RHEL 8 and Ubuntu 22.04 they stay in plain text, in `/etc/credstore/`. If the credentials cannot be decrypted, the host still reboots, without the downtime and the chat message.
* **role:duplicity**: The Swift or SFTP password is stored encrypted with `systemd-creds` on systemd 251 and newer instead of in plain text in `/etc/duba/duba.json`, so it is no longer readable in a backup of `/etc`. The old copies of `duba.json` are deleted. On RHEL 8 and Ubuntu 22.04 it stays in plain text, in `/etc/credstore/`. Restores that read the password from `duba.json` have to switch to `duba exec`.
* **role:nextcloud**: The vHost blocks `3rdparty/`, `lib/`, `config/`, `data/`, `occ` and `console.php` as Nextcloud's `.htaccess` intends; until now its rules never matched, so anyone could read the versions of all bundled libraries and call internal PHP files directly.
* **role:mongodb**: The password of the dump user is stored encrypted with `systemd-creds` on systemd 251 and newer instead of in plain text in `/etc/mongodb-dump-login.json`, so it is no longer readable in a backup of `/etc`. On RHEL 8 and Ubuntu 22.04 it stays in plain text, in `/etc/credstore/`.
* **role:mariadb_server**: The login of the dump user is stored encrypted with `systemd-creds` on systemd 251 and newer instead of in plain text in `/etc/mariadb-dump-login.cnf`, so it is no longer readable in a backup of `/etc`. On RHEL 8 and Ubuntu 22.04 it stays in plain text, in `/etc/credstore/`.
* **role:mongodb**: The daily dump no longer passes the password of the dump user on the command line, where every local user could read it in the process list.
* **role:mariadb_server**: The daily dump and the log rotation no longer pass MariaDB passwords on the command line, where every local user could read them in the process list.
* **role:php**: PHP 8.5 logs fatal errors without a backtrace (`fatal_error_backtraces = Off`), which closes the main way into the remote code execution CVE-2026-109619 ([GHSA-6wmf-4rv5-p3rp](https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp)) on unpatched builds. The role settings only narrow the attack paths: update PHP to 8.2.35, 8.3.36, 8.4.27, 8.5.12 or your distribution's fixed package to close the vulnerability.
* **role:apache_tomcat**: The host-manager webapp only accepts the clients that `apache_tomcat__webapps_manager_context_xml_allow` allows, as documented, instead of those that `apache_tomcat__webapps_docs_context_xml_allow` allows.
* **role:borg_local, role:schedule_reboot, role:tools**: Neither the password of the Icinga API user nor the Rocket.Chat webhook of `schedule_reboot` shows up in the process list or in a file that every local user can read, which moves `schedule-icinga-downtime` from a shell function in `/etc/profile.d/alias.sh` to a command in `/usr/local/sbin` that only root may run.
* **role:openvpn_server**: The server negotiates TLS 1.2 only with forward secrecy and checks client certificates against OpenSSL security level 2 ([#358](https://github.com/Linuxfabrik/lfops/issues/358)).
* **role:apache_httpd**: The OWASP Core Rule Set archive is checked against a SHA-256 checksum before it is deployed, so a tampered or corrupted download aborts the run.
* **role:monitoring_plugins**: The source install checks every pinned Python dependency against the checksums in the lockfile instead of installing whatever PyPI serves, and puts the sudoers drop-ins in place only once `visudo` accepts them.
* **role:github_project_createrepo**: The service can only write to the repositories it maintains instead of to everything below `github_project_createrepo__base_path`, where it could replace other files such as a repository signing key. The role removes the ACL entries it granted before.
* **role:kernel_modules**: Blocks further rarely used kernel modules by default that unprivileged users can get loaded and that are prone to local privilege escalations, among them `ah6`, `pppoe` and `sctp_diag` from [RHSB-2026-011](https://access.redhat.com/security/vulnerabilities/RHSB-2026-011). This stops Bluetooth, L2TP/IPsec, PPPoE, PPTP and IPsec AH; set `enabled: true` for the modules a host needs. The role README lists them all.
* **role:wordpress**: `--tags wordpress:export` writes to `/backup/wordpress-export/<instance>`, readable by `apache` and `root` only, instead of to `/tmp`.
* **role:wordpress**: The database and admin passwords no longer show up in the process list during the installation.
* **role:wordpress**: WP-CLI is verified against its published checksum, and an installed WP-CLI that differs from the current release is replaced.
* **role:wordpress**: `wp-config.php`, which holds the database password and the salts, is no longer readable by every local user.
* **role:apache_httpd**: The `wordpress` vHost sends `X-Content-Type-Options: nosniff` and `Referrer-Policy: strict-origin-when-cross-origin`.
* **role:apache_httpd**: The `wordpress` vHost refuses to run PHP files below `wp-content/uploads`, so an upload flaw in a plugin no longer leads to code execution.
* **role:repo_collabora_code**: dnf verifies the signatures of the Collabora packages, as Collabora's own installation instructions do, where the repository file had switched the check off.


## [v9.0.0] - 2026-09-09

**Highlights:** Valkey gets its own role and playbook instead of being installed under the Redis name, which is what RHEL 10 hosts need now that Red Hat ships no Redis at all; those hosts have to be moved over. On RHEL 8, a MariaDB package upgrade no longer cuts applications on the same host off from their database. Apache no longer loads `mod_info`, which served the complete configuration including other modules' credentials. A broken PHP-FPM configuration aborts the run instead of taking the service down on the restart. Sudo rules deployed by `freeipa_server` can carry their commands again. The Bitwarden lookup can be told to abort instead of silently generating a new password, for runs against hosts whose credentials must already exist. The Grafana graph configuration for the Monitoring Plugins is no longer deployed on every ordinary run and has to be requested explicitly by its tag. Apache serves HTTP/2 to every client that offers it over TLS, which in a typical setup is the reverse proxy in front of an application; the hop from that proxy to the backend is unchanged. LibreNMS keeps its RRD updates in RRDCached and writes them out every 30 minutes, taking a large share of the poller's disk I/O off the host.

### Breaking Changes

* **role:icingadb, role:icingaweb2, role:icingaweb2_module_reporting, role:icingaweb2_module_x509, role:mariadb_server**: The MariaDB tasks move from `ansible.mysql` to its MariaDB counterpart `ansible.mariadb`, so the deprecation warning printed on every run is gone and the roles keep working once `ansible.mysql` drops MariaDB support. `ansible-galaxy collection install linuxfabrik.lfops` pulls the new collection in as a dependency; a manually maintained collection list has to add it. If you run LFOps in the Execution Environment, pull the current image (`podman pull ghcr.io/linuxfabrik/lfops_ee:latest`), because an EE ships the collections it was built with and an older one aborts these roles with `couldn't resolve module/action`.
* **role:php**: The session cookie is marked `Secure`, so browsers only send it back over HTTPS. A site served over plain HTTP can no longer log anyone in, because the browser accepts the cookie and never returns it: set `php__ini_session_cookie_secure__group_var: 'Off'` for such a host, or `php_value_session_cookie_secure: 'Off'` for the affected pool alone. HTTPS hosts are unaffected, including behind a reverse proxy that terminates TLS, and nobody is logged out by the change: a running session resumes and keeps its existing cookie until the application regenerates the session ID.
* **role:php**: The session cookie carries `SameSite=Lax` from PHP 7.3 on, where PHP so far emitted no attribute at all and left the decision to the browser. The cookie is no longer sent on cross-site POSTs, in iframes or on XHR, which is what stops a foreign page from acting under a visitor's session. An application whose identity provider returns through a cross-site POST, as SAML HTTP-POST binding and the OIDC `form_post` response mode do, loops on login until it is set back, for that pool alone via the new `php_value_session_cookie_samesite: 'None'` and `php_value_session_cookie_secure: 'On'` subkeys of `php__fpm_pools__*_var` (browsers require `Secure` for `None`), or host-wide via `php__ini_session_cookie_samesite__group_var`. An identity provider under the same registrable domain is unaffected.
* **role:redis**: `redis__conf_daemonize` and `redis__conf_supervised` are gone; remove them from your inventory. Neither ever reached the running service: a Redis supervised by systemd never daemonizes, and every packaged unit either overrides both on its `ExecStart` line or depends on the `supervised auto` the role deploys. The deployed `redis.conf` keeps the values it had, so nothing changes on a host that used the defaults.
* **role:librenms**: `librenms__fqdn` and `librenms__config_app_url` are replaced by `librenms__url`, which takes the URL of the instance including the scheme, for example `https://librenms.example.com`. Set it in place of the two, using the URL your users open in the browser even when Apache httpd on the host serves plain HTTP behind a TLS-terminating proxy.
* **role:librenms**: LibreNMS trusts no reverse proxy any more, where it previously trusted one running on the LibreNMS host itself. It therefore ignores the `X-Forwarded-*` headers of such a proxy, and logs, access control and the detected protocol use the proxy's address instead of the client's. Hosts with a proxy in front of LibreNMS list it in `librenms__config_app_trusted_proxies`; `['127.0.0.1']` restores the previous behaviour.
* **role:php**: The PHP-FPM pool configuration changed for existing hosts. Sessions now live in a per-pool directory (the default `www` pool moves from `/var/lib/php/session` to `/var/lib/php/session/www`), so logged-in users are signed out once after the upgrade. `memory_limit`, `max_execution_time`, `max_input_vars`, `post_max_size`, `upload_max_filesize`, `session.save_handler` and `session.save_path` are now enforced as `php_admin_value`, so applications can no longer change them at runtime via `ini_set()`. A pool that stores its sessions in redis or memcached sets `php_admin_value_session_save_handler` accordingly. `soap.wsdl_cache_dir` moves from the shared `/var/lib/php/wsdlcache` to a per-pool directory below it. On Debian the pool socket now belongs to `root` and grants the web server access through a POSIX ACL entry, where it used to be owned by the web server user; anything of your own that checks the socket's ownership rather than connecting to it needs adjusting. Worker processes recycle after 500 requests (`pm.max_requests`) instead of running indefinitely, and a worker still serving a single request after 60 seconds is killed (`request_terminate_timeout`, previously off). Hosts with legitimately long-running web requests raise `php__fpm_pool_conf_request_terminate_timeout__group_var`.
* **role:keycloak**: Rename `keycloak__state` to `keycloak__service_state`, the name every other LFOps role uses. The value `reloaded` is gone: Keycloak's systemd unit has no `ExecReload`, so a reload never worked; use `restarted` instead.
* **role:keycloak**: The role installs the OpenJDK its Keycloak version needs (OpenJDK 17 for Keycloak 24, OpenJDK 21 for 25 and newer) instead of relying on the `apps` role, which `setup_keycloak` no longer runs. Hosts that used `apps__apps__*_var` through this playbook to install further packages have to run the `apps` playbook for them.
* **playbook:setup_keycloak**: All skip variables are named `setup_keycloak__skip_<role>` now. Rename `keycloak__skip_kernel_settings`, `keycloak__skip_policycoreutils`, `keycloak__skip_repo_mydumper`, `mariadb_server__skip_python` and `mariadb_server__skip_repo_mariadb` accordingly.
* **role:php**: On RedHat, every host running this role gets the `lfops_php_fpm_slowlog` SELinux policy module, which grants the `httpd_t` domain the `sys_ptrace` capability and `ptrace` on itself. Without it the PHP-FPM slowlog stays empty, because the master is not allowed to ptrace the worker whose backtrace it is supposed to write. The module is deployed regardless of whether the slowlog is switched on, the permissions apply to the whole `httpd_t` domain and therefore to Apache httpd as well, and compiling it installs `make` and `selinux-policy-devel` on the host. The `php` and `icingaweb2` playbooks run the `policycoreutils` and `selinux` roles for this; set the playbook's `__skip_selinux` variable (for example `php__skip_selinux: true`) to leave the host's policy untouched.
* **role:mariadb_server**: The InnoDB buffer pool grows from 128 MiB to 512 MiB, so a database with more than a trivial amount of data is served from memory instead of from disk. Every host running this role therefore uses roughly 384 MiB more RAM after the next restart of the service.
* **role:mariadb_server**: The InnoDB redo log grows from 32 MiB to the 96 MiB MariaDB itself ships, so a write-heavy server no longer stalls waiting for a checkpoint on a redo log sized for much smaller workloads. InnoDB resizes the log itself when the service next restarts, also after an unclean shutdown, but the data directory needs 64 MiB more free space for it; check that on hosts that are tight before deploying. Set `mariadb_server__cnf_innodb_log_file_size__group_var: '32M'` (or the `__host_var`) to keep the previous size.
* **role:mariadb_server**: `innodb_snapshot_isolation` now defaults to `OFF`. Turning it on requires support from the application: a transaction in `REPEATABLE READ` that modifies a row another transaction changed after its snapshot was taken is aborted with `ER_CHECKREAD`. The application has to catch that error and retry the transaction, otherwise the write fails under concurrent load. To restore the previous behaviour on hosts whose application is known to handle it, set `mariadb_server__cnf_innodb_snapshot_isolation__group_var: 'ON'` (or the `__host_var`).
* **role:collabora**: `collabora__coolwsd_ssl_settings_ssl_verification` and `collabora__coolwsd_welcome_enable` reach the deployed `coolwsd.xml` again, and SSL verification of the WOPI host now defaults to strict. Both variables were only wired into a few old templates, so on newer versions the value came from the package instead of from the inventory, and most packages ship verification off. A host whose WOPI host presents a self-signed or otherwise untrusted certificate needs `collabora__coolwsd_ssl_settings_ssl_verification: false`, otherwise its documents stop loading.
* **role:apache_httpd**: `mod_info` is no longer enabled, so `/server-info` stops serving the complete Apache configuration, including the credentials of other modules. Hosts that need the endpoint re-enable the `info` module in their inventory via `apache_httpd__mods__group_var` / `apache_httpd__mods__host_var`.
* **role:monitoring_plugins**: A source install now places the notification plugins in `/usr/lib64/nagios/plugins`, next to the check plugins, and removes the `/usr/lib64/nagios/plugins/notifications` directory it used before. This is where the shipped Icinga command definitions and the rpm/deb packages have always expected them. Adjust any command definition of your own that points into the `notifications` subdirectory.
* **role:redis**: RHEL 10 is no longer supported, since Red Hat ships no Redis there. On RHEL 10 the role used to install Valkey under the Redis name, which aborted the run when the configuration file was deployed. Move those hosts to the `valkey` role: put them into the `lfops_valkey` inventory group and rename their `redis__*` variables to `valkey__*`. All other platforms are unaffected.
* **role:collabora**: Drop support for EOL Collabora 23.05. Upgrade to 24.04 or newer.
* **role:icingaweb2_module_grafana**: The graph configuration for the Linuxfabrik Monitoring Plugins is only deployed when the role is called with `--tags icingaweb2_module_grafana:monitoring_plugins_graphs`, matching the `icingaweb2_module_director:basket` tag. Run the role with that tag to update `/etc/icingaweb2/modules/grafana/graphs.ini`. The `icingaweb2_module_grafana__skip_monitoring_plugins_graphs_config` variable is gone; remove it from your inventory.

### Added

* **role:fail2ban**: The `apache-404-matomo` filter and jail work like `apache-404` but for the matomo LogFormat, where the virtual host precedes the client IP in the log line.
* **role:fail2ban**: The `apache-404` filter accepts `fail2ban__jail_apache_404_ignoreregex`, a list of regular expressions whose matching log lines are excluded from the 404 count, so known missing resources like `/favicon.ico` or `/assets/style.css` no longer trigger a ban.
* **role:php**: The `[global]` section of the PHP-FPM configuration can be set from the inventory, so the log level and the reload after repeated worker crashes are configurable; the reload is on by default after ten crashes within a minute.
* **role:fail2ban**: The `apache-404` filter and jail ban IPs that cause excessive HTTP 404 errors in the Apache access log, configurable via `fail2ban__jail_apache_404_bantime`, `fail2ban__jail_apache_404_findtime` and `fail2ban__jail_apache_404_maxretry`.
* **role:apache_solr**: The maximum size of the Java heap can be configured via `apache_solr__heap`.
* **role:apache_solr**: The Java Security Manager can be switched off via `apache_solr__security_manager_enabled`, for instances that have to follow symlinks pointing outside of Solr's own directories.
* **role:apache_httpd**: The GELF access log carries the request protocol as its own `_http_version` field, so the share of traffic on HTTP/2 can be graphed per vHost in Graylog. The text log formats already contained it as part of the request line and are unchanged, because fail2ban and the Matomo log importer parse them positionally.
* **role:librenms**: `librenms__scheduler_service_enabled` and `librenms__scheduler_service_state` start, stop or disable the timer of the LibreNMS scheduler on its own.
* **role:librenms**: The `librenms:cron`, `librenms:logrotate` and `librenms:state` tags deploy the scheduled jobs, deploy the logrotate configuration and manage the service state without touching the rest of the installation.
* **role:librenms**: The session cookie is marked as secure on hosts whose `librenms__url` is an `https://` URL, so a browser only sends it over HTTPS. LibreNMS reports the missing flag as a failure in its own validation. Set `librenms__config_session_secure_cookie` to override.
* **role:librenms**: LibreNMS stores its RRD files through RRDCached, which cuts the disk I/O of the poller by roughly a third.
* **role:librenms**: The `librenms:rrdcached` tag deploys and configures RRDCached without touching the rest of the installation.
* **role:php**: PHP-FPM pools are now fully configurable, each with its own user and group, process-manager tuning, timeouts and `php_admin_value` overrides. Every pool gets an isolated session directory, its own error and slow logs, and its own socket, so several applications can share a host without sharing a PHP process, a session store or a memory limit. One pool template now serves both distribution families.
* **role:php**: Add `meta/argument_specs.yml` declaring the user-facing variables, so role-entry validation catches type mismatches and unknown variables, and an explicit `vars/Ubuntu.yml`.
* **role:apache_httpd**: `apache_httpd__mod_http2_protocols` sets the protocols offered server-wide, the `conf_protocols` vHost key overrides it for a single vHost, and the remaining `apache_httpd__mod_http2_*` variables size the HTTP/2 worker pool and its per-connection buffers.
* **role:keycloak**: The Keycloak log file is rotated, with `keycloak__logrotate` for the number of rotations kept and the `keycloak:logrotate` tag to deploy the configuration on its own. Until now the log grew unbounded, since Keycloak has no built-in rotation for its file log handler.
* **role:keycloak**: Add `keycloak__limit_nofile`, `keycloak__transaction_default_timeout` and `keycloak__transaction_setup_timeout` for values that were hardcoded in the systemd unit and in `keycloak.conf`.
* **role:selinux**: A policy module can be defined inline through the `content_te` subkey of `selinux__modules__*_var`, instead of pointing `src` at a directory on the Ansible controller.
* **role:openvpn_server**: Add `openvpn_server__service_state` to start, stop, restart or reload the OpenVPN service independently of whether it is enabled at boot.
* **role:valkey**: Add a role and playbook to install and configure Valkey, listening on TCP port 6379 on the loopback interfaces by default. Valkey is taken from the distribution repositories (EPEL on RHEL 8, AppStream on RHEL 9 and 10, the distribution's own repository on Debian 13 and Ubuntu 24.04 / 26.04), and the configuration template matching the installed version is deployed. Debian 12 and Ubuntu 22.04 ship no Valkey and are not supported.
* **playbook:setup_icinga2_master, playbook:setup_mastodon, playbook:setup_moodle, playbook:setup_nextcloud**: These stacks install Valkey on RHEL 10 and Redis everywhere else, so they can be deployed on RHEL 10 again. Both servers speak the same protocol on the same port, so the applications are configured identically either way. Override `setup_*__skip_redis` / `setup_*__skip_valkey` (`setup_icinga2_master__redis__skip_role` / `setup_icinga2_master__valkey__skip_role`) to force a specific server or to run neither.
* **role:files**: A file can opt out of the backup copy that is written before it is overwritten, via the `backup` subkey of `files__files__*_var`.
* **role:collabora**: The `collabora:configure` tag deploys `coolwsd.xml` and the logrotate configuration without touching the packages.
* **role:docker**: The address pools docker assigns container network subnets from (`default-address-pools`) can be configured.
* **role:collabora**: Add support for Collabora Online CODE 26.04.1, 26.04.2 and 26.04.3, and Collabora Enterprise 24.04.18, 25.04.12 and 26.04.2.
* **role:monitoring_plugins**: Deploy the bash completion file for source installs.
* **role:kvm_vm**: VMs can now also be installed from an ISO or install tree instead of a prepared cloud image.
* **role:files**: The `files:directories`, `files:files` and `files:symlinks` tags manage one kind of file system entity each, so a single directory or symlink can be deployed without touching the rest.
* **plugin:bitwarden_item**: The lookup can be told not to create secrets, so a lookup that finds no matching item aborts the run instead of silently generating a new password. Set `LFOPS_BITWARDEN_LOOKUP_ITEM_CREATE=false`, or `create = false` in the `[bitwarden_item_lookup]` section of your `ansible.cfg`; the default is the previous behaviour.

### Changed

* **role:php**: A run aborts when a pool's `request_terminate_timeout` is not above its `max_execution_time`, instead of deploying a pool whose longer execution time is capped by PHP-FPM without saying so. Setting either to `0` still switches that limit off.
* **role:php**: The logrotate configuration for the per-pool PHP-FPM logs on Debian is deployed by the `php:logrotate` tag instead of `php:fpm`.
* **role:redis**: Redis also listens on the IPv6 loopback `[::1]:6379`, where it previously answered on `127.0.0.1` only, so clients that resolve `localhost` to `::1` are no longer refused. Hosts without an IPv6 loopback are unaffected: the address is marked optional, so Redis logs a warning and carries on instead of aborting.
* **role:apache_httpd**: Restarting Apache takes about 5 seconds instead of 13 on a host that holds long-lived connections, such as a proxy for WebSockets, because systemd no longer waits out Apache's own shutdown escalation. Apache refuses new connections for the whole of that wait, so this shortens the outage. The hard stop leaks about three semaphores per restart (`ipcs -s`), which a reboot clears; raise `apache_httpd__systemd_timeout_stop_sec` to trade restart speed back for a longer graceful window.
* **playbook:setup_librenms**: The skip variables of the playbook are all named after the playbook, `setup_librenms__skip_php` for example, instead of after the `librenms` role.
* **playbook:setup_librenms**: The playbook no longer runs the `apps` role, since the `librenms` role installs `git` itself.
* **role:librenms**: The `http_fping` SELinux policy module carries the rules the LibreNMS documentation now lists, so pinging a device from the web interface also works where fping uses an ICMP socket or binds a source address.
* **role:librenms**: PHP GMP is installed, so rates derived from 64-bit interface counters stay exact instead of silently losing precision in float arithmetic.
* **role:librenms**: The `httpd_can_sendmail` SELinux boolean is enabled, and `httpd_can_network_connect_db` in addition when the database is on another host.
* **role:librenms**: The SELinux file context of `/opt/librenms/cache` is set.
* **role:librenms**: ImageMagick is no longer installed, LibreNMS does not use it.
* **role:librenms**: The `http_fping` SELinux policy module is deployed by the `selinux` role, so a playbook run that skips that role no longer installs it.
* **role:librenms**: The role no longer makes the RRD, log, cache and storage directories group-writable, LibreNMS grants the access it needs itself while installing.
* **role:apache_httpd**: Restarting Apache no longer takes 90 seconds on a host that holds long-lived connections, such as a proxy for WebSockets. The server now waits 3 seconds for running requests to finish instead of waiting for the last connection to close by itself and being killed by systemd. Apache stops accepting new connections for the whole of that wait, so this shortens the outage rather than extending it. Hosts with legitimately long-running requests raise `apache_httpd__conf_graceful_shutdown_timeout`.
* **role:apache_httpd**: HTTP/2 is enabled and is the preferred protocol on every connection that terminates TLS, while a client that does not offer it is still served HTTP/1.1 and cleartext HTTP/2 (h2c) is not offered; on RedHat the `mod_http2` package is installed for this.
* **role:keycloak**: `keycloak__https_cipher_suites`, `keycloak__https_protocols`, `keycloak__log` and `keycloak__proxy_trusted_addresses` are YAML lists instead of comma-separated strings. A comma-separated value already in an inventory keeps working, Ansible splits it into the same list.
* **role:keycloak**: The Keycloak tarball is downloaded on the Ansible controller and copied to the target from there, so a target without internet access can be installed. The controller has to reach `github.com`.
* **role:keycloak**: `--tags keycloak:configure` deploys the configuration and rebuilds the server, but no longer starts the service or bootstraps the admin account. Use the `keycloak` tag for a full run and `keycloak:state` for the service state.
* **role:selinux**: A policy module is only recompiled and reinstalled when its source changed, so a run against an up-to-date host no longer reports a change for every module it manages.
* **role:monitoring_plugins**: A source install also deploys the `*-logging.sudoers` companion, which keeps the plugin calls out of the authentication log. Without it every check costs five entries there, and a monitored host runs dozens of checks a minute. A host running sudo-rs, Ubuntu 26.04 for example, does not get the file and loses it again if it had one, because sudo-rs knows none of its settings and warns about each of them on every `sudo` call by any user.
* A service that depends on a kernel setting deployed by the `kernel_settings` role now starts after TuneD, so the setting is in place before the service reads it. Until now such a service could come up while TuneD was still applying the profile and then run with the old value until its next restart, while `sysctl` and `tuned-adm verify` already reported the new one (roles `graylog_datanode`, `graylog_server`, `mariadb_server`, `mongodb`, `redis`).
* A repository file that carries mirror credentials is deployed with mode `0600` instead of `0644`, so an unprivileged `dnf` or `zypper` no longer lists those repositories (all `repo_*` roles).
* **role:collabora**: A host running a Collabora version the role has no configuration template for aborts with that version and the list of supported ones, instead of failing on a missing file.
* **role:collabora**: The `localhost` WOPI host is an ordinary entry of `collabora__coolwsd_storage_wopi__*` instead of being hard-coded in the template, so it can be dropped with `state: 'absent'` like any other host.
* **role:php**: The PHP-FPM configuration is checked with `php-fpm --test` before the service is restarted, so a broken pool or ini aborts the run with the error message instead of taking PHP-FPM down on the restart.

### Fixed

* **role:php**: A run limited to `--tags php:fpm` deploys the pools with the configured `memory_limit`, `max_execution_time` and `post_max_size` again, instead of writing them empty. php-fpm accepts the empty values without complaint, and as `php_admin_value` they override what `php.ini` sets.
* **role:nextcloud**: Long web requests are no longer killed after 60 seconds, so assembling a large chunked upload completes again. The role raises `max_execution_time` to 3600, but PHP-FPM has terminated a request after 60 seconds since the pool rework, and its limit fired first.
* **role:monitoring_plugins_grafana_dashboards**: The Grafana dashboards are collected in an empty directory on the controller. That directory was reused across runs, so the dashboard of a plugin that upstream renamed or removed kept being deployed, and a downgrade to an older Monitoring Plugins version still rolled out the dashboards of the newer one.
* **role:fail2ban**: The `apache-404` filter no longer produces false positives on the `linuxfabrikio` log format when a later field (such as bytes received `%I`) happens to contain `404`.
* **role:librenms**: The Python packages LibreNMS requires are installed, so its own validation no longer reports `command_runner` and `psutil` as missing.
* **role:librenms**: An update of LibreNMS that changes the units of its scheduler takes effect, because systemd is told to read them again.
* **role:librenms**: The poller works on a host whose database is not local, because the Python modules it imports are installed by the role instead of arriving through the MariaDB role.
* **role:librenms**: A second run of the role updates an existing installation instead of aborting at the git checkout.
* **playbook:setup_librenms**: A fresh installation no longer aborts while starting PHP-FPM.
* **playbook:setup_librenms**: The SELinux file contexts for `/opt/librenms` are applied.
* **role:apache_httpd**: Enabling an Apache module restarts httpd instead of reloading it, so the module actually takes effect. A reload loads the module but skips its initialization, which left HTTP/2 configured but silently inactive on RHEL 8 ([#339](https://github.com/Linuxfabrik/lfops/issues/339)).
* **role:kvm_vm**: Two VMs created from the same inventory host no longer get the same MAC address, which made the second one fail with `The MAC address ... is in use by another virtual machine`. The generated MAC follows the VM name now. Hosts that leave `kvm_vm__name` at its default keep the MAC they had; on a host that sets it, a VM created from now on gets a different MAC, so check DHCP reservations and firewall rules keyed on it before recreating such a VM. Running VMs are not touched.
* **role:keycloak**: A run against an unchanged host reports no changes any more. The tarball is only downloaded and extracted when the installed version differs from `keycloak__version`, and `kc.sh build` only runs when the installation or `keycloak.conf` actually changed, which also takes minutes off an ordinary run.
* **role:php**: The PHP-FPM slowlog holds the backtrace of a slow request on RedHat, instead of staying empty while php-fpm logs `failed to ptrace(ATTACH) child <pid>: Operation not permitted (1)`.
* **role:openvpn_server**: A new server certificate, a changed `server.conf` or a regenerated Diffie-Hellman file restarts OpenVPN. Until now the files were written to disk while the running service kept its old configuration, so a renewed certificate only took effect at the next reboot. The certificate revocation list and the client configs still apply without a restart, since OpenVPN re-reads them per connection.
* **role:apache_httpd**: Set `apache_httpd__mod_ssl_ssl_use_stapling` to off by default because Let's Encrypt does not provide an OCSP URL-endpoint.
* **plugin:bitwarden_item, module:bitwarden_item**: A vault that is not unlocked is reported with the `bw serve` endpoint it was read from and the status it actually has, plus the hint that `bw serve` keeps the session it was started with. The previous message pointed at `bw login` and `bw unlock`, which do not reach a running `bw serve`.
* **plugin:bitwarden_item**: Error messages no longer carry a doubled period in the middle.
* **role:rocketchat**: The environment file no longer sets `MONGO_OPLOG_URL`, which Rocket.Chat has ignored since 5.0.1. A host without a MongoDB replica set also gets a usable environment file again, instead of one whose MongoDB URL and port ended up on the same line.
* **role:rocketchat**: The container image is pulled from Docker Hub directly instead of through `registry.rocket.chat`, whose pull rate limit is shared by everyone using it, so image pulls and `podman auto-update` no longer fail with `toomanyrequests`.
* **role:system_update**: A requested reboot names the core packages or the running services that ask for it, instead of only listing what the run changed.
* **role:system_update**: A host with nothing to update no longer sends a "System updated without Reboot" mail on every update day.
* **role:system_update**: The update mail and the reboot request describe the packages the run just changed instead of those of an earlier run.
* **role:system_update**: The AIDE database is only refreshed when the update changed packages and the AIDE check was passing beforehand, so a host that was already reporting changes keeps reporting them instead of having them accepted as the new baseline.
* **role:system_update**: The AIDE database is refreshed after the update rather than before it, on Debian and Ubuntu as well as on RHEL, so the next check no longer flags every file the update touched.
* **role:system_update**: A failed update on Debian is reported and stops the run, instead of being followed by a success mail or by the reboot of a half-configured host.
* **role:schedule_reboot**: The role no longer aborts while deploying its reboot helper on Debian 12 and older and on Ubuntu 24.04 and older. Roles that pull it in, `system_update` among them, were unusable on those releases as well.
* **role:mariadb_server**: On RHEL 8, MariaDB keeps its own SELinux confinement after a package upgrade, by installing `mysql-selinux` the way the MariaDB packages already do on RHEL 9 and 10. Without it, applications on the same host lose their database connection with `Permission denied` as of MariaDB 11.4.13 and 11.8.9.
* **playbook:icingaweb2, playbook:setup_icinga2_master, role:icingaweb2** update `icingaweb2` dependent vars to ensure php.ini value `post_max_size` > `upload_max_filesize` by default.
* **role:monitoring_plugins**: A source install installs the dependencies of the Linuxfabrik library, so checks that speak HTTP, MySQL, SMB or WinRM no longer report `Python module "httpx" is not installed` and its equivalents.
* **role:monitoring_plugins**: A source install deploys the event plugins, which only the rpm/deb package used to ship.
* **role:monitoring_plugins**: A source install completes on a minimal installation, which has neither the Python module `ansible.builtin.pip` needs nor an `/etc/bash_completion.d` directory.
* **role:monitoring_plugins**: A source install leaves the plugins, the library and the virtual environment readable and executable for the monitoring user, even when the Ansible controller runs with a hardened umask.
* **role:monitoring_plugins**: A second source install run against an unchanged host no longer reports changes.
* **role:login**: Removing a user that had lingering enabled no longer aborts the run.
* **role:example**: The config-validation handler of the reference role triggers the restart handler it notifies; only the template new roles are copied from was affected, not any role that manages an application.
* **role:freeipa_server**: Commands and command groups can be assigned to a sudo rule, through the `allow_sudocmds` and `allow_sudocmdgroups` subkeys of `freeipa_server__sudorules`; the former `cmds` and `cmdgroups` names never reached FreeIPA and aborted the run with `Unsupported parameters`.
* **role:apache_solr**: `__apache_solr__java_package` covers the Java package required by Solr 10.
* **role:collabora**: The WOPI hosts from `collabora__coolwsd_storage_wopi__*` reach coolwsd again. Every entry was discarded on load, and access only kept working because coolwsd trusted the first host that happened to connect. Collabora 26.04 drops that fallback, where the result would have been that no document loads at all.


## [v8.0.0] - 2026-07-31

**Highlights:** Host reboots move to one configurable maintenance window managed by the new `schedule_reboot` role, so `system_update__update_time` and the reboot-downtime settings have to be moved in the inventory. `dnf_versionlock` changes its variable format and no longer unlocks packages that are simply dropped from the inventory. PHP on Debian can be pinned to a declared version instead of drifting with whatever sury promotes, and a fresh installation no longer bounces its services right after starting them. Review the Breaking Changes before updating: `php`, `mariadb_server`, `apache_httpd` and `repo_baseos` all change defaults.

### Breaking Changes

* **role:dnf_versionlock**: Rename `dnf_versionlock__versionlocks` to `dnf_versionlock__versionlocks__group_var` / `__host_var` and change it from a list of strings to a list of dictionaries (`name`, plus the optional `raw` and `state` subkeys). The role no longer rewrites the whole lock list, so locks set elsewhere (for example by `monitoring_plugins`) survive, but removing an entry from the inventory no longer unlocks the package: set `state: 'absent'` instead.
* **role:apache_httpd**: Role-internal variables are now `__`-prefixed. If you copied the SSL/TLS snippet from `EXAMPLES.md` into a vHost `raw` block, rename `apache_httpd__openssl_certificate_path`, `apache_httpd__openssl_privatekey_path` and `apache_httpd__openssl_chain_path` to their `__`-prefixed form, otherwise the vHost fails to render.
* **role:system_update**: Host reboots are now performed at one configurable maintenance window by the new `schedule_reboot` role (see Added). Adjust in your inventory: `system_update__update_time` to `schedule_reboot__reboot_time__group_var` (now a plain time of day, e.g. `'04:00'`), and any `system_update__icinga2_*` reboot-downtime settings to `schedule_reboot__icinga2_*`. Also, in most cases `system_update__update_day` should be used instead of `system_update__notify_and_schedule_on_calendar`.
* **role:php**: In preparation for the upcoming PHP 8.6, `php__ini_session_cookie_httponly` now defaults to `On`, so session cookies are marked HttpOnly and are no longer accessible to JavaScript via `document.cookie`. This matches the hardened session defaults PHP 8.6 ships. Applications that must read the session cookie from JavaScript have to set `php__ini_session_cookie_httponly__group_var: 'Off'` (or the `__host_var`) to restore the previous behaviour.
* **role:php**: The default `php__ini_opcache_blacklist_filename` now points to the distribution-neutral `/etc/opcache.blacklist` instead of `/etc/php-zts.d/opcache*.blacklist`. The old default pointed at the thread-safe (ZTS) config directory, which the non-ZTS PHP-FPM this role deploys never reads. No file exists at the new path by default, so no scripts are excluded from OPcache unless an admin creates one.
* **role:apache_httpd**: The Matomo log-analytics import script (`import_logs.py`) and the `apache_httpd:matomo` tag have been removed and moved to the new `matomo_import_logs` role. Hosts that import their access logs into Matomo, or that pipe their access logs to `/usr/local/sbin/import_logs.py` for realtime tracking, must now also run the `matomo_import_logs` role, which deploys the script. The `matomo` LogFormat itself stays in `apache_httpd`.
* **role:repo_baseos**: The Rocky Linux `security` repository now always points at the upstream mirrorlist, even when a custom `repo_baseos__mirror_url` is set, so critical CVE fixes keep coming straight from upstream instead of a potentially lagging mirror. Hosts that previously pulled the `security` repository from their custom mirror now reach upstream directly. Set `repo_baseos__security_repo_use_upstream: false` to restore the previous behaviour and have the `security` repository follow `repo_baseos__mirror_url` again.
* **role:mariadb_server**: The default for `skip_name_resolve` is now `OFF` instead of `ON`. Hosts that relied on the previous default and grant access by hostname keep working, but connections are now resolved via DNS again. Set `mariadb_server__cnf_skip_name_resolve__group_var: 'ON'` (or the `__host_var`) to restore the previous behaviour.

### Added

* **role:glpi_agent**: Add optional scheduled database inventory via `glpi_agent__database_inventory_enabled` and `glpi_agent__database_inventory_login`, which runs `glpi-agent --partial=database` as a dedicated read-only database user on a systemd timer instead of letting the always-on daemon connect as root on every cycle. `glpi_agent__conf_no_category` disables arbitrary inventory categories and `glpi_agent__database_inventory_on_calendar` sets the schedule.
* **role:php**: Add `php__version` to declare which PHP version a Debian host runs, for example `'8.4'`, so an ordinary `apt upgrade` no longer migrates the host to a new major PHP version on its own. With the sury repo enabled, the unversioned metapackages follow whatever sury currently declares as its default; declaring a version makes the role install the versioned packages, pin the `php`, `phar` and `phar.phar` alternatives to it, and purge the stacks of all other versions on `php:update`. Leave it empty to keep the previous behaviour. Has no effect on RedHat, where the module stream pins the version at repo level.
* **playbook:php**: Now runs the `repo_sury` role on Debian (skip with `php__skip_repo_sury`), since sury is what makes any PHP version other than the distribution's own available in the first place.
* **role:fail2ban**: Add `fail2ban__filters__*_var` (combined-var pattern, `unique_key="filename"`) so custom filter definitions can be deployed from the inventory in the same generic shape as `fail2ban__jails__*_var`. The built-in `apache-dos` and `portscan` filters move into `fail2ban__filters__role_var` with no behaviour change, and can now be opted out via `state: 'absent'`.
* **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, which is 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).
* 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 (apache_solr, bind, blocky, chrony, clamav, collabora, coturn, fail2ban, glpi_agent, grafana, graylog_datanode, graylog_server, icinga2_master, icinga_kubernetes, icingadb, influxdb, keepalived, opensearch, php, redis, snmp, squid, vsftpd).
* **role:duplicity**: Add Debian and Ubuntu support (proven on Debian 12, Debian 13, Ubuntu 22.04, Ubuntu 24.04 and Ubuntu 26.04), and install the `gnupg` package itself so backups also work on minimal installs that ship without `gpg`.
* **role:python_venv**: Add an optional per-venv `pip_constraints` key that pins transitive dependencies through a pip constraints file, without having to list them as direct packages.
* **role:opensearch**: Add `opensearch__path_repo` to register file system paths as `path.repo` in `opensearch.yml`, required for file system based snapshot repositories.
* **role:nextcloud**: Add `nextcloud__jobs_timeout_start_sec` to configure the start-up timeout of the `nextcloud-jobs.service`, defaulting to `10m`, for instances where background jobs regularly need longer.
* **role:icinga2_agent, role:icinga2_master**: Deploy a systemd drop-in override ensuring the Icinga 2 service starts after SSSD on hosts where SSSD is installed.
* **role:librenms**: Add `librenms__config_app_trusted_proxies` and `librenms__config_app_url` to set the `APP_TRUSTED_PROXIES` and `APP_URL` variables in `/opt/librenms/.env`, needed when running LibreNMS behind a reverse proxy.
* **role:mariadb_server**: Add `mariadb_server__cnf_innodb_flush_neighbors__group_var` / `__host_var` to configure the `innodb_flush_neighbors` InnoDB system variable, defaulting to `0`, which is the recommended value for SSD and NVMe storage.
* **role:freeipa_server**: Add `freeipa_server__limit_groups`, `freeipa_server__limit_hbacrules`, `freeipa_server__limit_hostgroups`, `freeipa_server__limit_pwpolicies`, `freeipa_server__limit_sudocmdgroups`, `freeipa_server__limit_sudocmds`, `freeipa_server__limit_sudorules` and `freeipa_server__limit_users` to manage only specific resources via `--extra-vars`, which speeds up deployments on servers with many FreeIPA resources.
* **role:vsftpd**: Add `vsftpd__pam_use_userdb` to authenticate virtual users via `pam_userdb` against `/etc/vsftpd/login.db`, so virtual-user logins work instead of failing against the local-user PAM stack the role always rendered.
* **role:opensearch**: Add `opensearch__heap` to set the JVM heap size via a drop-in at `/etc/opensearch/jvm.options.d/heap.options`, defaulting to 50% of system memory capped at 31 GB.
* **role:trend_micro_v1es**: Add a role to install and activate the Trend Vision One Endpoint Security agent (Endpoint Sensor and Server & Workload Protection).
* **role:matomo_import_logs**: New role that imports Apache access logs into Matomo on a schedule, one systemd timer per site, and ships the Matomo log-analytics import script (`import_logs.py`). The `token_auth` is provided via a per-site auth file instead of the command line, since `--token-auth`, `--login` and `--password` are visible in the process list and now log a deprecation warning. The script also supports the Traefik access-log format.
* **role:glances**: Add RHEL 10 / Rocky 10 / Alma 10 support by installing glances into a Python venv via the `python_venv` role, since the package is not available in EPEL 10. RHEL 10 is now marked proven (`x`) in COMPATIBILITY.
* **role:graylog_datanode**: Add `graylog_datanode__http_publish_uri` to set the REST API URI the DataNode advertises, needed when the bind address is not directly reachable (multiple interfaces, a NAT gateway, or a `0.0.0.0` bind address).

### Changed

* **role:php**: The `php:update` tag also deploys the PHP-FPM pools and enables and starts the FPM service, because both live under version-specific paths on Debian, so a host that changed `php__version` would otherwise run the new version with none of its pools and a unit that was never enabled.
* **role:fail2ban**: README documents the `state` subkey of the filter and jail entries as optional (defaults to `present`), notes that `raw` only applies to entries using the `raw` template, and states up front that filters and jails are defined in the inventory.
* **role:nextcloud**: Adds Debian and Ubuntu support alongside Red Hat-family systems, marked `(x)` in `COMPATIBILITY.md` since package names are verified on Debian 13 but the role is not yet proven end to end. SELinux relabeling is skipped automatically on hosts where SELinux is disabled.
* **role:apache_solr, role:blocky, role:fail2ban, role:rsyslog**: The service is started after its configuration has been deployed, not before, so on a fresh installation it comes up with the configuration the role just wrote instead of starting on the package defaults and being restarted afterwards.
* **role:duplicity**: Validate the role variables at start, and align the task tags with the LFOps vocabulary: the `duplicity:script` tag is gone (the `duba` script now deploys under `duplicity:configure`), and the new `duplicity:dump` tag manages the backup schedule.
* **role:schedule_reboot**: Hosts without an explicit reboot window are assigned a deterministic minute within the 04:00-04:59 window, staggered by hostname, so a fleet no longer reboots in lockstep at exactly 04:00. Pin `schedule_reboot__reboot_time__group_var` (or the `__host_var`) to keep a specific window.
* **role:monitoring_plugins**: A source install deploys the plugins into a self-contained Python virtual environment and provisions a suitable Python by itself, so it works on RHEL 8 where the system Python 3.6 is too old. The Linuxfabrik library is deployed newest straight from GitHub and the third-party dependencies are installed unpinned, so a source install always tracks the newest code for the selected `monitoring_plugins__version`. The dependencies the former source install placed into the home directories of root and the icinga user are cleaned up on the next run.
* **role:icinga2_agent**: The `icinga2_agent:update` tag refreshes the apt cache before the upgrade on Debian-family hosts, so it reliably installs the latest package instead of running against a stale cache.
* **role:mariadb_server**: Databases created via `mariadb_server__databases` without an explicit `collation` or `encoding` inherit the server default character set and collation (utf8mb4) instead of being pinned to the legacy `utf8` / `utf8_general_ci`. Existing databases are unaffected; set `collation` / `encoding` per database to override.
* **role:collabora**: Support Collabora Online CODE 25.04.10, which had no `coolwsd.xml` template and therefore aborted the deploy on hosts that had updated to it.
* **role:clamav**: Send notification mails through `sendmail` (provided by postfix) instead of the `mail` command (mailx), so one invocation works across distributions and delivery no longer depends on mailx being installed.
* **role:icingadb, role:icingaweb2, role:icingaweb2_module_reporting, role:icingaweb2_module_x509, role:mariadb_server**: Move the MariaDB tasks from the deprecated `community.mysql` collection to its replacement `ansible.mysql`, so the deprecation warnings printed on every run are gone and the roles keep working once `community.mysql` is removed upstream.

### Fixed

* A configuration change no longer restarts or reloads a service that the very same run had just started, so a fresh installation no longer bounces the service right after starting it, and no longer starts a service that is configured to stay stopped (apache_httpd, apache_solr, apache_tomcat, bind, blocky, chrony, clamav, collabora, coturn, docker, elasticsearch, fail2ban, glpi_agent, grafana, graylog_datanode, graylog_server, icinga2_master, icinga_kubernetes, icingadb, influxdb, keepalived, kibana, logstash, mariadb_server, mongodb, opensearch, php, postfix, postgresql_server, redis, rocketchat, rsyslog, snmp, squid, sshd, systemd_journald, telegraf, vsftpd).
* **role:mariadb_server, role:monitoring_plugins, role:php**: The update tags refresh the apt cache before upgrading on Debian-family hosts, so they reliably install the latest packages instead of running against a stale cache. RHEL-family hosts are unaffected, since dnf refreshes its metadata on its own.
* **role:mastodon**: Modules set via `apache_httpd__mods__host_var` in the inventory are no longer discarded, because the role used the wrong dependent variable name.
* **role:postgresql_server**: The `grant_option` subkey of `postgresql_server__privs` takes effect, and the entry's `roles` and `state` subkeys are documented.
* **playbook:php, playbook:redis, playbook:setup_grav, playbook:setup_moodle**: These playbooks enable EPEL (and CRB on Rocky 9 and newer) before installing packages from Remi's repository, which depend on it; on RedHat 8 the run previously aborted with `nothing provides libcapstone.so.4 needed by php-opcache`. Skippable via `<prefix>__skip_repo_epel` and `<prefix>__skip_repo_baseos`.
* **role:php**: A dry run (`--check`) against a host that does not have PHP-FPM installed yet no longer aborts, so the role can be previewed before the first real run.
* **role:icingaweb2, role:icingaweb2_module_fileshipper, role:icingaweb2_module_vspheredb, role:icingaweb2_module_x509, role:nextcloud**: On Debian and Ubuntu, PHP extension names are built from the PHP version the `php` role manages, so the unversioned `php-<module>` metapackages can no longer pull a second, undeclared PHP runtime onto the host whenever sury promotes a new default. RedHat is unaffected.
* **role:nextcloud**: The IMAP PHP extension installs on PHP 8.4 and newer, where IMAP was removed from PHP core, by installing it from the PECL package instead of the no-longer-existing `php-imap`.
* **role:php**: Running the role with a specific tag such as `--tags php:state` on Debian and Ubuntu no longer fails with an undefined PHP version, so roles that build on php and only restart php-fpm work when run with their own tags.
* **role:nextcloud**: The `nextcloud-ldap-show-remnants` script no longer aborts the `nextcloud:cron` deploy with `'setup_basic__skip_mailto_root' is undefined` when the role runs outside the `setup_basic` playbook; report recipients come from `nextcloud__mail_recipients`, defaulting to the global `mailto_root__to`.
* **role:nextcloud**: Replace ws.linuxfabrik.io with www.linuxfabrik.ch, since ws.linuxfabrik.io is decommissioned.
* **role:icingaweb2_module_grafana**: `icingaweb2_module_grafana__auth_jwt` honours a quoted `'false'`, which previously enabled JWT authentication and deployed the private key anyway; unquoted `true` / `false` behaved correctly before and are unaffected.
* **role:shared**: `lfops__remove_rpmnew_rpmsave` honours an explicit opt-out, so `--extra-vars='lfops__remove_rpmnew_rpmsave=false'` keeps the `.rpmnew`, `.rpmsave`, `.dpkg-dist` and `.ucf-dist` files instead of removing them. Leaving the variable unset was, and still is, safe.
* **role:apache_tomcat**: `apache_tomcat__service_state` takes effect instead of being silently ignored in favour of `apache_tomcat__service_enabled`; its default is `started` if `apache_tomcat__service_enabled` is `true` and `stopped` otherwise, so hosts that never set it are unaffected.
* **role:freeipa_server**: The `pki-tomcatd` start-up timeout configured via `freeipa_server__systemd_timeoutstartsec` is in effect, because the role now applies the same value to systemd and to FreeIPA and reloads systemd. On slow machines `ipactl start`, server upgrades and CA certificate renewals previously failed with a timeout even though the CA was still coming up.
* **role:duplicity**: Swift backups work out of the box on Python 3.10 and newer, since the role pins a modern `oslo.*` stack in the venv, which fixes the `collections.Mapping` crash and drops the deprecated `netifaces` dependency. As a result the role no longer installs a C compiler or development headers on backup hosts, which removes the build toolchain from production machines.
* **role:keycloak**: Keycloak is no longer restarted a second time right after it was started on a fresh install, and a configuration change no longer starts the service on hosts that pin `keycloak__state` to `stopped`.
* **role:nextcloud**: Set the `text workspace_available` app config key as `boolean` instead of `string`, since newer Nextcloud enforces the config lexicon and the text app declares the key as `ValueType::BOOL`.
* **role:system_update**: The update and security-update jobs no longer send a failure mail when a mirror hiccups briefly, because repository metadata is refreshed with a few retries before updates are applied.
* **role:mod_maxminddb, role:monitoring_plugins**: The role no longer aborts at start demanding a variable that has an OS-specific default (`monitoring_plugins__icinga_user` respectively `mod_maxminddb__apache_conf_modules_d`), so there is no need to set it in the inventory.
* **role:monitoring_plugins**: A source install no longer aborts on RHEL 8, where the system Python 3.6 is older than the required 3.9; the role installs and uses Python 3.9 automatically.
* **role:python_venv**: Install `python3-packaging` on EL10, which ships Python 3.12 without the stdlib `distutils`, so Ansible's `pip` module can create a venv again.
* **role:icingaweb2_module_vspheredb**: Download the module tarball from the canonical `archive/refs/tags/<version>.tar.gz` URL, so the pinned release tag is fetched reliably.
* **role:monitoring_plugins**: A source install deploys the sudoers drop-in as `/etc/sudoers.d/linuxfabrik-monitoring-plugins`, the same file name the packages use, and both install methods remove the drop-in under the former name, so sudo no longer warns about a duplicate `Cmnd_Alias`.
* **role:collect_rpmnew_rpmsave**: Stop emitting an Ansible deprecation warning on every run, which keeps the role working on Ansible 2.19 and later.
* **role:kvm_vm**: Use `kvm_vm__connect_url` for every libvirt operation, so disk resizes (`virsh blockresize`) and a few other steps no longer ignore the configured connection URL and act on the wrong libvirt.

### Security

* **role:opensearch**: The OpenSearch data directory is no longer readable by other local users; its root uses mode `0750` instead of `2755`, matching the OpenSearch package default.
* **role:monitoring_plugins**: A source install leaves the plugins, the bundled library and the dependency venv owned by root instead of the monitoring user, closing a local privilege escalation in which that account could edit a plugin, a library module or the venv interpreter that runs as root via sudo.


## [v7.0.0] - 2026-06-11

**Highlights:** `setup_basic` gains three CIS-oriented hardening roles (`core_dumps`, `kernel_modules`, `login`) and the `sshd` defaults are tightened, which stops sessions that rely on X11 or agent forwarding and can lock out clients offering more than three keys. Reboots move to one windowed mechanism in the new `schedule_reboot` role. Tags were renamed across a dozen roles and the MinIO roles were removed, so review your `--tags` invocations and any object-store backup first. RHEL 10 is now proven for the roles `setup_basic` runs.

### Breaking Changes

* Internal OS-specific variables are `__`-prefixed to mark them as not overridable from inventory. Rename any inventory override of `dnf_versionlock__list_path`, `dnf_versionlock__packages` or a role's `*__icingaweb2_owner` variable to the `__`-prefixed form; the values are unchanged (dnf_versionlock, icingaweb2_module_businessprocess, icingaweb2_module_company, icingaweb2_module_cube, icingaweb2_module_fileshipper, icingaweb2_module_generictts, icingaweb2_module_incubator, icingaweb2_module_pdfexport, icingaweb2_theme_linuxfabrik).
* **plugin:combine_lod, role:apache_httpd, role:mariadb_server, role:proxysql, role:selinux**: A composite `unique_key` (a list of keys) now requires every component to be set on each item, instead of letting one be filled by a downstream default. Set the previously optional component explicitly: `virtualhost_port` on every `apache_httpd` vHost, `host` on every `mariadb_server` user/role, `port` on every `proxysql` server, and `proto` on every `selinux` port. Otherwise the play fails with a clear error.
* **role:apache_httpd, role:apache_tomcat, role:mastodon, role:postgresql_server**: Rename tags to the project-wide naming scheme. `apache_httpd:config` becomes `apache_httpd:configure`, and `apache_tomcat:users`, `mastodon:users`, `postgresql_server:users` and `postgresql_server:databases` lose their trailing `s` (`...:user`, `...:database`). Adjust any `--tags` / `--skip-tags` invocations and automation that reference the old tag names.
* **role:sshd**: Ship hardened SSH defaults that change the behaviour of existing installations on the next run: X11 forwarding, agent forwarding and TCP keepalives are now off, `MaxAuthTries` is lowered to `3`, `ClientAliveCountMax` to `2`, and `LogLevel` is raised to `VERBOSE`. Sessions relying on X11 or agent forwarding stop working, and a client offering more than three keys from its SSH agent can be locked out. Restore the previous behaviour where needed via the new variables: `sshd__x11_forwarding: true`, `sshd__allow_agent_forwarding: true`, `sshd__tcp_keep_alive: true`, `sshd__max_auth_tries: 6`, `sshd__client_alive_count_max: 3`, `sshd__log_level: 'INFO'`. Additionally configurable are `sshd__allow_tcp_forwarding` and `sshd__max_sessions`.
* **role:apache_httpd, role:apache_solr, role:freeipa_server, role:grav, role:icingaweb2, role:influxdb, role:mariadb_server, role:mongodb, role:nextcloud, role:opensearch**: Align section tags to the controlled vocabulary, which uses plural names for sections that manage multiple objects. The `:user` tags become `:users`, the `:database` tags become `:databases`, and `apache_httpd:config` becomes `apache_httpd:configure`. Adjust any `--tags` / `--skip-tags` invocations and automation that reference the old tag names.
* **role:minio_client, role:objectstore_backup**: Both roles and their playbooks (`playbooks/minio_client.yml`, `playbooks/objectstore_backup.yml`) have been removed, along with the corresponding role blocks in `playbooks/setup_nextcloud.yml` and the `setup_nextcloud__skip_minio_client` / `setup_nextcloud__skip_objectstore_backup` variables. MinIO Server has been archived as no-longer-maintained since February 2026, and we are moving away from using object storage for critical data. Users relying on these roles must replace the MinIO-based object-store backup with their own solution (e.g. `rclone`); the `mc` binary, its config under `/etc/mc/`, the `objectstore-backup` systemd timer/service, and `/usr/local/bin/mc-mirror.sh` are no longer managed by lfops and will remain on existing hosts until removed manually ([#241](https://github.com/Linuxfabrik/lfops/issues/241)).
* **role:infomaniak_vm**: Always create a managed port for every entry in `infomaniak_vm__networks`, even when no `fixed_ip` is set. Previously only networks with a `fixed_ip` got a managed port; networks without one relied on OpenStack's auto-created port. To avoid creating unused (but billed) managed ports on VMs provisioned under the old behavior, make sure to manually rename the existing port in OpenStack to match the `port_name`. Note that this port will not survive VM deletion / detachment, since it was automatically created and therefore is owned by OpenStack, not the user.
* **role:shared**: The Apache httpd user and group are defined once as `__shared__apache_httpd_user` / `__shared__apache_httpd_group` and loaded into every playbook through a new `global-variables.yml` task in `pre_tasks`, instead of being repeated in the `vars/` of around 20 roles. Running one of those roles ad-hoc outside the bundled playbooks now requires importing `shared`'s `global-variables.yml` first. On Suse, the `monitoring_plugins` web files use the correct apache group `www` instead of `wwwrun`.
* **role:crypto_policy, role:duplicity, role:icingaweb2, role:icingaweb2_module_x509, role:mariadb_server, role:php**: The internal package-selection dicts behind the OS-keyed default lookups are no longer overridable from inventory; they were never meant to be. Behaviour is unchanged on supported platforms.

### Added

* **all roles**: Role variables are validated at role entry through `meta/argument_specs.yml`, so a type mismatch or a missing mandatory variable in the inventory fails immediately with a clear message instead of an obscure error later in the run.
* **role:schedule_reboot**: New role providing a single, windowed reboot mechanism: a request spool (`/run/schedule-reboot/`), an ad-hoc `schedule-reboot` command, and one actor that performs a single reboot for all pending requests at a configurable window (`schedule_reboot__reboot_time__*`), setting an Icinga downtime around it. Other roles request a reboot instead of rebooting themselves; `system_update` uses it.
* **role:core_dumps**: New role that disables core dumps, which can leak sensitive process memory to disk, following the CIS Benchmark recommendations. Runs as part of `setup_basic`.
* **role:kernel_modules**: New role that hardens a host by blocking rarely used or risky kernel modules (FireWire, legacy filesystems, uncommon network protocols) following the CIS Benchmark recommendations, and runs as part of `setup_basic`. The defaults stay clear of modules that would break common workloads such as containers, snap and USB storage.
* **role:login**: New role that sets a password-aging policy and a stricter default umask in `/etc/login.defs`, applying to newly created accounts and password changes rather than retroactively.
* **role:chromium_headless**: New role providing a hardened, socket-activated headless Chromium backend for tools such as the Icinga Web 2 PDF Export Module, started on the first request and stopped again after an idle timeout so it uses no RAM while unused. Installs `chromium-headless` from EPEL instead of Google's proprietary repository.
* **role:tmux**: New role that installs tmux and deploys a system-wide `/etc/tmux.conf` with a larger scrollback buffer and mouse support. Selections are copied to the local clipboard over SSH via OSC 52 where the terminal supports it, and `prefix + P` dumps a pane's whole scrollback buffer to a file.
* **role:hostname**: Maintains an `/etc/hosts` entry mapping the FQDN and short name to the host's primary IPv4 address, configurable via `hostname__etc_hosts_ip` and disablable with `hostname__manage_etc_hosts: false`.
* **role:uptimerobot, plugins/modules/uptimerobot_\***: New role and nine custom modules to manage UptimeRobot resources from a playbook: `uptimerobot_monitor`, `uptimerobot_mwindow` and `uptimerobot_psp` for CRUD, `uptimerobot_alert_contact` for deletion only (UptimeRobot API v2 does not expose creating contacts), plus five read-only info modules. All CRUD modules support `--check` and `--diff` and are idempotent on re-run. Configured via `uptimerobot__monitors`, `uptimerobot__mwindows`, `uptimerobot__psps` and `uptimerobot__alert_contacts`; the API key comes from the `api_key` parameter, `api_key_file` (default `~/.uptimerobot`) or `UPTIMEROBOT_API_KEY`.
* **plugin:platform_select**: New filter plugin for selecting a value from a platform-keyed dictionary by OS family, distribution or version.
* **role:system_update**: Add a security lane for Rocky Linux: a daily timer installs only Rocky Linux security hot-fixes from the dedicated `security` repository and requests a reboot if needed, performed at the host's maintenance window. Enabled by default, a no-op where the `security` repository is not enabled, and turned off with `system_update__security_enabled: false`.
* **role:repo_baseos**: Add the Rocky Linux `security` repository for critical CVE fixes, enabled by default. Opt out per host or group via `repo_baseos__security_repo_enabled__host_var` / `__group_var`.
* **role:sshd**: Add Debian 13 and Ubuntu 22.04 / 24.04 / 26.04 support and run on Fedora. On Debian and Ubuntu the role manages the correct service unit (`ssh.service`) and disables OpenSSH socket activation (`ssh.socket`), and Red Hat-family releases without a version-specific template fall back to a generic `RedHat` `sshd_config` template instead of failing.
* **role:icinga2_master, role:icingadb, role:icingaweb2, role:icingaweb2_module_reporting, role:icingaweb2_module_x509**: Add explicit Ubuntu variable files, making Ubuntu support visible alongside Debian. The Icinga repository, GPG key and package names were verified on Debian 13 and Ubuntu 24.04.
* **role:libmaxminddb, role:mod_maxminddb**: Run on Debian and Ubuntu in addition to Red Hat-family systems, with the Apache module enabled automatically on Debian and Ubuntu.
* **role:monitoring_plugins, role:repo_monitoring_plugins**: Add SLES 15 and SLES 16 support, installing the Linuxfabrik Monitoring Plugins from the SUSE channel of `repo.linuxfabrik.ch` and applying the SUSE-specific package version lock ([#245](https://github.com/Linuxfabrik/lfops/issues/245)).
* **role:repo_remi**: Add RHEL 10 / Rocky 10 support with a new GPG key, repo templates and module-stream tasks for EL 10.
* **role:repo_monitoring_plugins**: Add `repo_monitoring_plugins__testing` (default `false`) to switch from the `release` to the `testing` channel. On Red Hat-family systems a single `/etc/yum.repos.d/linuxfabrik-monitoring-plugins.repo` is deployed containing both channel sections with `enabled=` toggled by the variable, so DNF metadata for both can stay cached across switches; on Debian and Ubuntu the `-release` suffix in the apt sources file is replaced with `-testing`.
* **role:graylog_server**: Make more HTTP, Elasticsearch, processing/output buffer and message journal settings configurable via `graylog_server__http_external_uri`, `graylog_server__http_enable_cors`, `graylog_server__elasticsearch_max_total_connections`, `graylog_server__elasticsearch_max_total_connections_per_route`, `graylog_server__output_batch_size`, `graylog_server__processbuffer_processors`, `graylog_server__outputbuffer_processors`, `graylog_server__ring_size`, `graylog_server__inputbuffer_ring_size`, `graylog_server__message_journal_max_age` and `graylog_server__message_journal_max_size`.
* **role:graylog_datanode**: Add `graylog_datanode__raw`, plus `graylog_datanode__path_repos` and `graylog_datanode__node_search_cache_size` to configure searchable snapshot locations and the size of the disk-based searchable snapshot cache.
* **role:graylog_datanode, role:graylog_server**: Add template for Graylog 7.1.
* **role:mariadb_server**: Make `aria_pagecache_buffer_size`, `key_buffer_size` and `sort_buffer_size` configurable via the corresponding `mariadb_server__cnf_*` variables, and add `mariadb_server__cnf_innodb_snapshot_isolation` (MariaDB 10.6+), defaulting to `'ON'`.
* **role:elasticsearch**: Add optional variables `elasticsearch__cluster_routing_allocation_disk_watermark_flood_stage_frozen` and the four corresponding `*_max_headroom` variables.
* **role:kibana**: Add `kibana__logging` to make the `logging:` block in `kibana.yml` fully user-configurable (appenders, loggers, root, rotation). The default preserves the previous behaviour: JSON logs at `/var/log/kibana/kibana.log`, rotated daily, 14 rotations kept.
* **role:logstash**: Add optional variables `logstash__monitoring_cluster_uuid` and `logstash__monitoring_enabled`.
* **role:alternatives**: Support managing `subcommands` (slaves/followers) and the Red Hat-only `family` grouping, ensure the alternatives tooling is installed, and allow the role to be included without variables as a no-op.
* **role:at, role:dnf_makecache, role:open_vm_tools, role:qemu_guest_agent**: Add service state variables (`at__service_state`, `dnf_makecache__service_state` and `__timer_state`, `open_vm_tools__service_enabled` and `__service_state`, `qemu_guest_agent__service_state`) to control the running state independently from boot autostart. Default behaviour is unchanged.
* **role:infomaniak_vm**: Add the `keep_port_on_absent` subkey on `infomaniak_vm__networks` entries to preserve the port and its fixed IP when the VM is set to `absent`, so the same IP can be re-used, and `port_name` to override the managed port's name.
* **role:mirror**: Document the per-repository `newest_only` subkey on `mirror__reposync_repos` entries, defaulting to `true`. Set it to `false` for repositories that publish multiple versions in parallel, such as Icinga.
* **role:redis**: Add template for version 8.8.
* **playbooks/setup_basic**: Add `setup_basic__skip_policycoreutils` to skip the `policycoreutils` role, matching the pattern used by the other roles in the playbook.

### Changed

* **all roles**: The role READMEs follow one standard format and explain what the underlying software actually is and when an admin would want it, instead of only naming it. Install behaviour, upgrade paths, role scope and previously undocumented variables are spelled out, and tags and variables are bullet lists instead of markdown tables.
* **COMPATIBILITY**: RHEL 10 is promoted to proven (`x`) for the 23 roles that `setup_basic` exercises, Ubuntu 26.04 is added, the EOL Debian 11 and Ubuntu 20.04 columns are dropped, and roles that are expected to work but are untested are marked `(x)`. `glances` stays at `(x)` because the package is missing in EPEL 10.
* **role:nextcloud**: Automatic app updates are enabled by default (`nextcloud__timer_app_update_enabled`), and the scheduled update only switches Nextcloud into maintenance mode when an app update is actually pending, so an instance that is already up to date keeps serving requests. The recommended database migrations are applied afterwards, and a failed run no longer leaves the instance stuck in maintenance mode.
* **role:keycloak**: The bootstrap admin credentials no longer stay in `/etc/sysconfig/keycloak` after the first run. The role writes them, waits for Keycloak to consume them on startup, re-renders the sysconfig file without them, and stores a state marker so subsequent runs skip the credential render; `keycloak__admin_login` can then be removed from the inventory. For disaster recovery, delete the marker file, re-add the variable and re-run. A `-temp` suffix on the initial admin username is recommended so it is obvious which account must be deleted once a permanent admin exists.
* **role:icinga2_master, role:icingadb**: Validate the Icinga 2 configuration before restarting the service, so a faulty config fails the playbook run loudly instead of bouncing the daemon into a broken state and leaving Icinga 2 down.
* **role:clamav**: Runs on Debian and Ubuntu in addition to Red Hat-family systems, and works on RHEL 10. The role seeds the signature database on first install so the scanner starts reliably, and runs an EICAR self-test (also available via the `clamav:test` tag) that confirms detection actually works.
* **role:acme_sh**: Issue ECDSA P-256 certificates by default instead of RSA-4096, for faster TLS handshakes at equivalent security. Certificates previously issued as RSA are reissued as ECDSA on the next run and the superseded RSA certificate is dropped from renewal; set `acme_sh__key_length` to an RSA value such as `4096` to keep RSA.
* **role:mailto_root**: Send the verification mails via `sendmail` (provided by `postfix`) instead of the `mail` (mailx) command, completing the move off `mailx`, so the role no longer needs the `mailx` package installed.
* **playbooks**: Roles that target the whole RHEL family run on any enterprise distribution that is not Fedora, instead of an explicit list of major versions, which adds support for RHEL 10 and future releases without further changes. The CRB repository is now also enabled on Rocky 10, which previously left dependencies such as `python3-virtualenv` uninstallable.
* **role:grafana**: Apply the systemd/chkconfig workaround on RHEL 10 as well, not just RHEL 9.
* **role:tools**: Install the German locale package (`glibc-langpack-de`) on RHEL 10 as well, and no longer install tmux; use the dedicated `tmux` role instead, which ships a configuration with sensible defaults.
* **role:repo_baseos**: The Rocky 8 `security` repository matches Rocky 9 and 10: it adds the disabled `security-debuginfo` and `security-source` sub-repositories, a 6-hour metadata expiry so emergency hot-fixes are noticed quickly, and the `$rltype` mirrorlist variable.
* **role:redis**: Bump the default for `net.core.somaxconn` from `1024` to `4096` to match the RHEL 9 and RHEL 10 kernel default and the current Redis upstream recommendation. RHEL 9 and 10 hosts see no effective change; RHEL 8 hosts now get `4096`.
* **role:monitoring_plugins**: `install_method: 'source'` reads the per-Python-LTS lockfile under `lockfiles/pyXX/requirements.txt` from both the `monitoring-plugins` and `lib` repos, picking the directory that matches the target host's Python, since the previous root-level `requirements.txt` no longer exists upstream.
* **role:system_update**: Change the default of `system_update__update_time` so updates are spread deterministically across 04:00-04:59, with the minute derived from `inventory_hostname`, instead of all hosts firing at 04:00 sharp.
* **role:apache_httpd**: Bump the Core Rule Set to 4.27.0, and update the two reverse-proxy snippets in `EXAMPLES.md` to use `ProxyPass` instead of `RewriteRule`, which `%`-decodes the URI pattern and breaks WebDAV apps such as Nextcloud on rename. See the [blog post](https://www.linuxfabrik.ch/blog/nextcloud-rewriterules-vs-proxypass).
* **role:firewall**: Install `nftables` together with `iptables` for `firewall__firewall == "fwbuilder"` on all distros, instead of only on Fedora and RHEL 8/9.
* **role:graylog_server**: Update the `server.conf` templates to include `telemetry_enabled = false`.
* **role:network**: Scope the `hc-utils` removal task to Red Hat-family hosts, since Hetzner ships `hc-utils` as RPMs only. No behaviour change on either family.
* **plugin:gpg_key**: Refresh the bundled GPG helper library so the module keeps working on current Python and GnuPG releases, and make the `gnupghome` parameter expand `~` and resolve relative paths as documented. Existing playbooks are unaffected.
* **role:motd**: Update the default value of `motd__legal_notice`.

### Removed

* **role:repo_remi**: Drop support for RHEL 7 and Fedora 35, both EOL, along with their per-platform task, vars and template trees.
* **role:freeipa_client**: Remove the dead-code defaults `freeipa_server__config_default_shell`, `freeipa_server__config_password_expiration_notification`, `freeipa_server__domain` and `freeipa_server__realm` from `defaults/main.yml`; they were never read by the role.

### Fixed

* **role:repo_elasticsearch, role:repo_grafana, role:repo_graylog, role:repo_icinga, role:repo_influxdb, role:repo_mariadb, role:repo_mongodb, role:repo_monitoring_plugins, role:repo_mydumper, role:repo_opensearch, role:repo_proxysql, role:repo_redis, role:repo_sury**: Refreshing the apt cache is no longer reported as a change on every run.
* **roles**: Controller-side downloads and git clones delegated to localhost are no longer skipped when the first targeted host happens not to need them, which previously risked leaving later hosts without the downloaded artifact.
* **roles**: Set `become: false` on tasks delegated to localhost across the collection, so they no longer try to call `sudo` on the Ansible controller and fail with `sudo: a password is required` where passwordless sudo is not set up ([#242](https://github.com/Linuxfabrik/lfops/issues/242)).
* **role:repo_remi**: Enabling the php, composer and Redis module streams is idempotent, so repeated runs no longer report a change or briefly disable and re-enable the stream.
* **role:proxysql**: `mysql_servers` entries are deduplicated by their actual `address` field; the merge key referenced a non-existent `hostname` field, so multiple backends sharing a host group and port were silently collapsed into one.
* **role:repo_influxdb**: Prevent the `influxdata-archive-keyring` package from being installed on both Enterprise Linux and Debian, since it drops a second repo file pointing at upstream that is not managed by LFOps.
* **role:php**: php-fpm workers run with a defined `PATH`, which previously was empty and broke PHP code that shells out to system binaries and tripped Nextcloud's "PHP getenv" setup warning.
* **role:redis**: The Redis configuration file is no longer world-readable; it is deployed as `root:redis` with mode `0640`, so a configured password can no longer be read by other local users.
* **role:acme_sh**: No longer reinstalls every certificate and reloads the web server on every run; certificates are only reinstalled when they were just (re)issued or when the installed file is missing.
* **role:keycloak**: The role prints a clear instruction when a re-run can no longer obtain a token because the bootstrap admin was manually switched over to a permanent account and the bootstrap marker went missing.
* **role:keycloak**: Fix ownership under `/opt/keycloak/data/`, which the post-install build step left owned by `root:root` so the `keycloak` service user could not write into it; the build now runs as the service user and existing installations are corrected on the next run.
* **role:keycloak**: Fix the transaction timeout silently dropping from 3600s to 300s on Keycloak 26.6.0 and newer, and the MariaDB database encoding defaulting to the deprecated `utf8` (`utf8mb3`) instead of `utf8mb4`.
* **role:mongodb**: The role aborts early with a clear message when users are defined while `mongodb__conf_security_authorization` is disabled, instead of failing with a confusing authentication error, and the dump config no longer writes login credentials in that case.
* **role:kernel_settings**: The `systemd_cpu_affinity` setting is actually applied; the value was computed and shown in the debug output but never passed to the underlying system role.
* **role:icingaweb2_module_pdfexport**: PDF export works out of the box, because the headless browser backend the module needs is installed and configured automatically via the new `chromium_headless` role instead of having to be set up by hand.
* **role:nextcloud**: The `nextcloud-update` script owns the maintenance mode lifecycle itself instead of expecting callers to enable it beforehand, which disabled the LDAP user provider and made the `before-update` export silently omit LDAP users. Callers must drop the manual `maintenance:mode --on` step from their pre-script workflow and rely on `--single-transaction` for the DB dump instead.
* **role:nextcloud**: Ensure that the Nextcloud OCC is executable.
* **role:graylog_server**: Fix the `graylog_server:configure_defaults` run aborting on Graylog 7.0 and newer with `Unable to map property can_be_default` while creating the default index set, by removing the property; Graylog 7.x dropped it and 6.x ignored it.
* **role:graylog_server**: Validate that each `graylog_server__system_inputs` entry sets `global: true` or assigns a `node`, which was marked mandatory but never enforced.
* **role:graylog_datanode, role:graylog_server**: Validate that the `password_secret` is at least 16 characters long.
* **role:graylog_datanode**: Fix the `Conditional result ... was of type 'str'` deprecation warning.
* **role:nodejs**: Fix `@nodejs:<stream>` install failing with `broken groups or modules`, by resetting the module first when `nodejs__dnf_module_stream` is set and installing the explicit `/common` profile.
* **role:blocky**: The `validate config & restart blocky.service` handler is notified when the blocky binary changes, so the service is restarted after an update.
* **role:mariadb_server**: Stop writing the deprecated `innodb_buffer_pool_chunk_size` setting for MariaDB 10.11, 11.4 and 11.8, which ignore it and derive the chunk size from `innodb_buffer_pool_size`. The role aborts at the start of the run with a clear error if `innodb_buffer_pool_chunk_size` (on MariaDB 10.11+) or `innodb_file_per_table` (on MariaDB 11.0+) is still set in inventory, so an upgrade from 10.6 to 11.x does not silently keep a stale override.
* **role:mariadb_server**: Fix MariaDB starting in the `unconfined_service_t` SELinux domain on RHEL 10, which mislabels `/var/lib/mysql/mysql.sock` and breaks `php-fpm` and `httpd_t` clients such as Icinga Web 2 login. The role sets the `mysqld_exec_t` file context persistently via `semanage fcontext` and `restorecon`, since the previous `chcon` workaround cannot relabel the binary under EL10's read-only `/usr` service sandbox.
* **role:icinga2_master**: Fix the `selinux` role failing on RHEL 10 with `SELinux boolean icinga2_can_connect_all is not defined in persistent policy`, by installing `nagios-selinux` from EPEL first so its `%post` registers the types that `icinga2-selinux` references.
* **role:redis**: Fix `No package redis available.` on RHEL 10, where Red Hat replaced Redis with the compatible Valkey in AppStream and Remi no longer ships Redis for EL10. The role installs `valkey` on EL10 and guards the Redis Stack `loadmodule` directives that do not exist on Valkey; EL8 and EL9 keep installing Redis unchanged and the user-facing `redis__conf_*` variables stay backwards-compatible.
* **role:openvpn_server**: Fix `invalid selinux context` on RHEL 10 when deploying `server.p12` and `crl.pem`, by using `etc_t` there since the `openvpn_etc_t` type no longer exists in the RHEL 10 core policy.
* **role:repo_epel**: Fix a malformed RHEL 10 `epel.repo`, where a missing newline rendered `enabled=0username=<login>` when `repo_epel__basic_auth_login` was set and dnf rejected the file.
* **role:repo_mariadb**: Fix `dnf -y module disable mariadb` failing on RHEL 10, where modularity was removed, by scoping the task and the `module_hotfixes` directive to RHEL 8 and 9.
* **role:infomaniak_vm**: Apply the VM's security group on the `ext-net1` port instead of only on the server, since Neutron enforces the port's security groups when a VM boots against a pre-created port, and stop passing `security_groups` to `openstack.cloud.server`, which failed on internal-network ports with `port_security_enabled` set to `false`.
* **role:haveged**: Setting `haveged__service_state: 'stopped'` no longer produces the invalid systemctl command `stopp`, so all four valid values work as expected.
* **role:influxdb**: Always install `curl`, which is required to start influxdb but missing as a package dependency.
* **role:redis**: Add the missing paths for running against Debian.
* **role:logstash**: The default `logstash__java_opts` sets the JVM heap size to 60% of total memory, capped at 8g.
* **role:mount**: Fix the `when` condition for NFS/CIFS client package installation failing with multiple mounts and when the `state` key is undefined.
* **execution-environment**: Add the missing `sshpass` system package, required for SSH password-based connections such as `--ask-pass`.
* **plugin:nextcloud_occ_app_config**: An `array` config value is compared as JSON, so a key whose stored value already matches no longer reports a change and re-runs `occ config:app:set` on every run.
* **plugin:bitwarden_item**: The module no longer writes to the Bitwarden vault in check mode (`--check`), and a run without `password` no longer overwrites an existing item's password.
* **plugin:sqlite_query**: A failed query fails the task instead of reporting success with the error text in `query_result`, and a `REGEXP` query against a column containing NULL values no longer fails.
* **plugin:uptimerobot_\***: The modules no longer crash when the UptimeRobot API returns a non-list response for a list endpoint.
* **plugin:bitwarden_item, plugin:combine_lod, plugin:gpg_key, plugin:nextcloud_occ_app_config, plugin:nextcloud_occ_system_config, plugin:uptimerobot_monitor, plugin:uptimerobot_psp**: Fixed the plugin documentation so `ansible-doc` renders it again, and corrected the `gpg_key` docs, which claimed a separate `python-gnupg` install is required and named the returned key field wrongly.
* **playbooks/clamav, playbooks/duplicity, playbooks/fangfrisch, playbooks/influxdb, playbooks/mongodb, playbooks/python_venv**: Enable `repo_baseos` (CRB) and `repo_epel` on Rocky 9 and newer before the `python_venv` role, to fix `No match for argument: python3-virtualenv`.
* **playbooks/setup_graylog_datanode, playbooks/setup_graylog_server, playbooks/setup_icinga2_master, playbooks/setup_rocketchat**: Extend the `repo_baseos` and `repo_epel` conditions to Rocky and RHEL 10.
* **playbooks/freeipa_client, playbooks/freeipa_server**: Set `strategy: 'linear'` explicitly, so the playbooks work even when the user's `ansible.cfg` defaults to a strategy that reuses the target Python interpreter, which otherwise fails with `API.bootstrap() already called`.
* **role:repo_monitoring_plugins**: Add the missing `run_once: true` on the local repo-key download task on Red Hat platforms, so the key is downloaded once per run instead of once per host.

### Security

* **plugin:gpg_key**: The cleartext passphrase is no longer included in the module's failure output when key generation fails.
* **role:repo_\***: HTTP basic auth credentials are only written to the repository config files when a custom mirror URL is set. Previously, setting `lfops__repo_basic_auth_login` without `lfops__repo_mirror_url` wrote the credentials into repo files that still pointed at the public vendor mirrors, so the package manager sent them to servers that do not use basic auth. The Icinga repo is intentionally unchanged, since its subscription URL legitimately requires basic auth.


## [v6.0.1] - 2026-04-07

### Fixed

* **ci**: Strip badges from README.md before publishing to Galaxy, as external images are not rendered


## [v6.0.0] - 2026-04-07

### Breaking Changes

* **role:nfs_server**: Rework `nfs_server__exports` from a list of strings to a list of dictionaries with new `path`, `clients`, `owner`, `group`, and `mode` subkeys
* **role:kvm_host**: Change NAT to be explicitly activated for virtual nets
* **role:apache_httpd**: Change the default to not install/enable mod_qos by default (it is no longer shipped in EPEL 10)

### Added

* Add MkDocs-based documentation site, deployed automatically to GitHub Pages via `tools/build-docs` and a GitHub Actions workflow
* **CONTRIBUTING**: Document semantic parameter ordering for Ansible modules
* **playbooks**: Add `example.yml` and `setup_example.yml` playbooks as development references
* **role:example**: Add complete example role with defaults, handlers, tasks, templates, and vars as a reference for consistent role development
* **role:icingaweb2_module_grafana**: Add JWT support
* **role:grafana**: Add JWT support
* Add `playbooks/README.md` documenting all playbooks with their roles in execution order and available skip variables
* **role:apache_httpd**: Add platform-specific behavior section, wsgi example, and document localhost endpoints in README
* **role:apache_httpd**: Add skip variables section to README linking to relevant playbooks
* **role:mailx**: Add skip variables section to README linking to relevant playbooks
* **role:policycoreutils**: Add skip variables section to README linking to relevant playbooks
* **role:yum_utils**: Add skip variables section to README linking to relevant playbooks
* **plugin:bitwarden_item**: Add file-based item cache to reduce `bw serve` API calls, preventing crashes under load. Cache is stored in `$XDG_RUNTIME_DIR` (RAM-backed tmpfs) with `/tmp` fallback. After create/edit operations, the cache is updated inline to avoid expensive full re-syncs, with a 1-second sleep as rate limit to prevent Bitwarden API errors. Convert `is_unlocked` to a property to fix it never being called.
* **role:freeipa_server**: Add `--diff` support for all FreeIPA modules and add `freeipa_server:configure` tag
* **role:mariadb_server**: Add `mariadb_server__cnf_wsrep_log_conflicts` and `mariadb_server__cnf_wsrep_retry_autocommit` variables
* **role:mariadb_server**: Add `mariadb_server__cnf_wsrep_gtid_mode` variable to configure `wsrep_gtid_mode` for Galera
* **role:openvpn_server**: Add `openvpn_server:crl` tag to allow deploying the certificate revocation list independently
* **role:nextcloud**: Add Icinga2 set / unset downtime functionality to `nextcloud-update.j2`
* **execution-environment**: Add mitogen
* **role:nfs_client**: Add optional `owner`, `group` and `mode` subkeys for mount point directories
* **role:logstash**: Add support for deploying custom grok pattern files to `/etc/logstash/patterns/`
* **role:mount**: Add optional `owner` and `group` subkeys for mount point directories
* **role:elasticsearch**: Add logrotate config for daily rotation
* **role:freeipa_server**: Add the ability to specify the systemd unit start timeout
* **role:postfix**: Add RHEL 10 support
* **role:kvm_vm**: Add the ability to resize disks
* **role:infomaniak_vm**: Add the ability to choose the deployment region/datacenter
* **role:crypto_policy**: Add RHEL 10 support
* **role:elastic_agent**: Add new role
* **role:elastic_agent_fleet_server**: Add new role
* **role:fail2ban**: Make `bantime` configurable for the sshd and portscan jails
* **role:duplicity**: Add support for RHEL 10
* **role:php**: Make `request_slowlog_timeout` and `request_terminate_timeout` configurable
* **role:graylog_server**: Make `http_publish_uri` configurable; make `trusted_proxies` configurable
* **role:graylog_datanode**: Add template for 7.0
* **role:graylog_server**: Add template for 7.0
* **role:lvm**: Add new role
* **role:logrotate**: Add support for RHEL 10
* **role:sshd**: Add support for RHEL 10
* **role:yum_utils**: Add support for RHEL 10
* **role:repo_epel**: Add support for RHEL 10
* **role:repo_baseos**: Add support for RHEL 10
* **role:policycoreutils**: Add support for RHEL 10
* **role:mailx**: Add support for RHEL 10
* **role:graylog_server**: Make `message_journal_dir` configurable
* **playbook:setup_basic**: Add lvm role

### Changed

* **ci**: Publish pre-releases directly to prod Ansible Galaxy instead of galaxy-dev, since it is unreliable and pulp-ansible excludes pre-release versions from "latest"
* Update pre-commit hooks to latest versions
* Unify CONTRIBUTING and convert from reStructuredText to Markdown
* **roles**: Add `backup: true` to all `ansible.builtin.template` tasks to ensure config file backups before overwriting
* **role:nextcloud**: Refactor `nextcloud-update.j2`
* **role:keycloak**: Rework `keycloak.conf` template to match Keycloak's default config structure
* **role:apache_httpd**: bump Core Rule Set to 4.24.1
* **role:repo_remi**: Install Composer from `remi-modular` repository
* **role:icingadb**: Enhance `config.yml` template
* **role:apache_httpd**: Improve output; bump Core Rule Set to 4.24.0

### Fixed

* **role:apache_httpd**: Fix `apache_httpd__mod_security_coreruleset_version` default value in README (4.4.0 -> 4.24.1), fix prefork variable names in README (`spare_threads` -> `spare_servers`), fix various typos ("best practise", "Tipp")
* **role:mailx**: Fix grammar in task name ("make" -> "makes"), sort template module parameters alphabetically
* **role:policycoreutils**: Fix grammar in task name ("are" -> "is")
* **plugin:bitwarden_item**: Fix missing `raise` in multipart error handling, `break` instead of `continue` in multi-term lookup, `folder_id` wrongly typed as `list` instead of `str` in module, notes default mismatch between documentation and code, and wrong "lookup plugin" wording in module documentation
* **role:mirror**: Fix missing `0440` permissions on sudoers file
* **role:login**: Rename sudoers file from `lfops_login` to `linuxfabrik` to match the kickstart configuration; remove the old file automatically
* **roles**: Fix Ansible 2.19 deprecation warning for conditional results of type `int` by using `| length > 0` instead of `| length`
* **role:firewall**: Fix fwbuilder repo clone being skipped when `run_once` picks a host without `firewall__fwbuilder_repo_url`
* **role:sshd**: Validate sshd config with `sshd -t` before reloading the service
* **role:nfs_client**: Fix systemd not being aware of new or removed NFS mount units
* **role:keycloak**: Fix issues preventing Keycloak from starting
* **role:systemd_unit**: Correct the removal of units
* **role:bind**: Fix incorrect distribution version comparison in named.conf
* **role:python_venv**: Fix venv path in remove venv task
* **role:apache_httpd**: Prevent deployment of mods that should be disabled
* **role:repo_postgresql**: Remove EOL versions, adjust for RHEL 9 & 10
* **role:mariadb_server**: Fix the root cause of `/run/mariadb/wsrep-start-position: No such file or directory` after update of MariaDB (10.11.14 -> 10.11.15 or 11.4.8 -> 11.4.9)
* **role:ansible_init**: Install Ansible Collections from requirements.txt since that file contains the correct versions for running against RHEL 8
* **role:kibana**: Enable log rotation
* **role:kibana**: Fix `when` statement
* **playbook:setup_icinga2_master**: Fix syntax; add missing `kernel_settings` for MariaDB
* **lookup_plugin:bitwarden**: Make it more robust
* **role:monitoring_plugins**: Fix installation of package against non-RHEL hosts
* **role:rocketchat**: Fix typo and order of calls in playbook


## [v5.1.0] - 2026-01-06

### Added

* **role:kibana**: Add `kibana__raw` variable
* **role:elasticsearch**: Add `elasticsearch__raw` variable
* **role:apache_httpd**: Add nice ErrorDocuments
* **role:kibana**: Make SSL settings configurable


## [v5.0.0] - 2025-11-14

### Breaking Changes

* **role:elasticsearch_oss**: Rename to `elasticsearch`, as both the free and subscription versions are now in the same package
* **role:repo_elasticsearch_oss**: Remove, as both the free and subscription versions are now in the same package

### Added

* **role:acme_sh**: Add `acme_sh__reload_cmd` to allow setting the local reload command globally for all certificates
* **role:collabora**: Add new template versions
* **role:elasticsearch**:
  * Make `node.roles` configurable
  * Add variables for allocation awareness
  * Add `elasticsearch__path_data` variable to configure custom data directory
  * Improve handling of TLS certificates
  * Allow creation of clusters
* **role:gitlab_ce**: Make the `gitlab.rb` options for default project features, email reply-to address, LDAP integration and the upload path configurable
* **role:graylog_server**: Re-add `graylog_server__elasticsearch_hosts` to allow setups without Graylog Data Node
* **role:kibana**: Add new role
* **role:mariadb_server**:
  * Add support for version 11.8 (LTS)
  * Make `log_slave_updates` configurable
  * Add `mariadb_server__cnf_server_raw` variable
* **role:podman_containers**: Add option to enable the `podman-auto-update.timer`
* **role:postfix**: Add `postfix__lookup_tables__*_var` to allow easy deployment of lookup tables
* **role:redis**: Add template for version 8.2
* **role:selinux**:
  * Add handling of SELinux modules
  * Add capability to run `restorecon`
  * Add `selinux__policy` variable
* **role:shell**: Add `shell__limit_cmds` to limit executed shell commands
* **playbook:selinux**: Add `selinux__skip_policycoreutils` variable

### Fixed

* **role:acme_sh**: Fix certificate paths for Ubuntu and Debian
* **role:apache_solr**: Automatically install the correct Java version
* **role:elasticsearch**:
  * Prevent undefined variable error
  * Fix default of `elasticsearch__path_data`
  * Set `vm.swappiness` to 1
* **role:firewall**: Ensure `firewalld` is installed if chosen
* **role:icinga2_agent**: Deploy logrotate config as hotfix for upstream issue ([#188](https://github.com/Linuxfabrik/lfops/issues/188))
* **role:icinga2_master**: Deploy logrotate config as hotfix for upstream issue ([#189](https://github.com/Linuxfabrik/lfops/issues/189))
* **role:icingaweb2**: Fix Icinga username for Debian
* **role:keycloak**: Install correct Java version, removing the `keycloak__java_package_name` variable
* **role:kvm_vm**: Fix path
* **role:mariadb_server**: Fix `/run/mariadb/wsrep-start-position: No such file or directory` after update
* **role:mastodon**: Adjust to breaking changes in `elasticsearch` role
* **role:monitoring_plugins**:
  * Also install `lib` via source if `monitoring_plugins__install_method: 'source'` is set
  * Add workaround for pip on Debian & Ubuntu
* **role:openvpn_server**: Actually remove CCD with `state: 'absent'`
* **role:repo_mariadb**: Fix handling of GPG key for Debian & Ubuntu
* **role:repo_opensearch**: Deploy correct GPG key for selected OpenSearch version
* **role:rocketchat**: Fix syntax of HealthCmd
* **playbook:opensearch**: Prevent the whole cluster from restarting at once
* **playbook:setup_icinga2_master**:
  * Fix order
  * Add missing injection for MariaDB Python modules


## [v4.0.0] - 2025-10-03

### Breaking Changes

* **role:icinga2_master**: Remove support for IDO, as it is deprecated in favor of IcingaDB. The following variables can be removed from the inventory:
  * `icinga2_master__database_enable_ha`
  * `icinga2_master__database_host`
  * `icinga2_master__database_login`
  * `icinga2_master__database_name`
* **role:icingaweb2_module_monitoring**: Remove, as it is deprecated in favor of IcingaDB. All variables starting with `icingaweb2_module_monitoring__` can be removed from the inventory.
* **role:mariadb_server**:
  * Remove support for EOL version 10.5
  * Remove `mariadb_server__cnf_expire_logs_days__group_var` / `mariadb_server__cnf_expire_logs_days__host_var`, use `mariadb_server__cnf_binlog_expire_logs_seconds__group_var` / `mariadb_server__cnf_binlog_expire_logs_seconds__host_var` instead

### Added

* **role:acme_sh**: Add support for Debian/Ubuntu
* **role:apache_httpd**: Add support for Debian/Ubuntu
* **role:elasticsearch_oss**: Add `elasticsearch_oss__discovery_type`, `elasticsearch_oss__network_host` variables; reset JVM tmp directory
* **role:icingaweb2_module_pdfexport**: Add new role
* **role:kvm_host**: Add support for Ubuntu 24.04
* **role:mastodon**: Add new role
* **role:mongodb**: Add RedHat config template for v8.0
* **role:moodle**: Add `moodle__version` variable to select the major and minor version
* **role:postgresql_server**: Add `postgresql_server__login_password` variable
* **role:repo_mydumper**: Add official repos for Debian-based systems
* **role:system_update**: Add `metadata_timer_sync` option for cache-only installations
* **tool:particle**: Add new tool

### Changed

* **role:gitlab_ce**: Update template to v18.4.0
* **role:mariadb_server**:
  * Create a backup file of the most important config files before applying new versions
  * Make ownership of SSL certificate CIS-conform
* **role:monitoring_plugins**: Remove `monitoring_plugins__skip_notification_plugins__*_var` variables as they are now always installed
* **role:systemd_journald**: Move config file to `/etc/systemd/journald.conf.d/z00-linuxfabrik.conf`, improve calculations and default values

### Fixed

* **role:apache_httpd**:
  * Use platform-specific group for htpasswd files
  * Allow unsetting the `CustomLog` directive
* **role:apache_tomcat**: Adjust logrotate config for multiple Tomcat instances
* **role:bind**:
  * Do not run `named-checkzone` against forward zones
  * Remove obsolete options for RHEL 9
* **role:duplicity**: Use python3.11 to prevent errors when installing latest duplicity
* **role:elasticsearch_oss**: Move tmpdir to a location with exec permissions specified by CIS hardening
* **role:keycloak**: Set `keycloak__proxy_trusted_addresses` to `'127.0.0.1'` due to FD leak if using `'127.0.0.1,::1'`
* **role:mariadb_server**:
  * Correct mydumper dependency packages for Debian-based systems
  * Fix failing dumps after mydumper update to v0.20.1
  * Adjust SELinux settings after upgrades
  * Grant `binlog monitor` privilege for `mariadb-backup` user
* **role:monitoring_plugins**:
  * Fix path to old sudoers file
  * Fix script execution in CIS-hardened `/tmp`
  * Improve versionlock and install SELinux package on RHEL
* **role:nextcloud**: Add missing `env` module
* **role:repo_opensearch**: Fix GPG key


## [v3.0.0] - 2025-06-13

### Breaking Changes

* **role:apache_httpd**:
  * Change `conf_server_alias` from a string to a list
  * Change default of the `authz_document_root` vHost variable from `Require local` to `Require all granted`. This is a more sensible default, as `allowed_file_extensions` is used to restrict the access.
  * Remove the `authz_file_extensions` vHost variable. Access to listed file extensions is now always allowed.
  * Fix a bug that allowed access to dotfiles which had extensions listed in `allowed_file_extensions`. Make sure this does not break your application, or set `allow_accessing_dotfiles: true`.
  * Change default of `apache_httpd__skip_mod_security_coreruleset` from `false` to `true`
* **role:apache_tomcat**:
  * Rename `apache_tomcat__skip_manager` to `apache_tomcat__skip_admin_webapps`
  * Change `apache_tomcat__users__*_var` from a simple list to a list of dictionaries
* **role:borg_local**: Add new mandatory variable `borg_local__passphrase`
* **role:collabora**:
  * Change `collabora__coolwsd_storage_wopi__*_var` to a list of dictionaries from a list of strings
  * Change `collabora__language_packages__*_var` to a list of dictionaries from a list of strings
  * Rename `collabora__coolwsd_allowed_languages` to `collabora__coolwsd_allowed_languages__*_var` and change it to a list of dictionaries from a list of strings
* **role:fangfrisch**: Remove malwarepatrol as it is discontinued (see https://malwareblocklist.org/)
* **role:grafana**: Change default value for `grafana__serve_from_sub_path` from `true` to `false`
* **role:graylog_server**:
  * Remove support for Graylog < 5.0
  * Only support Graylog 6.1+ (Graylog Data Node based installations). Currently no more support for dedicated OpenSearch or Elasticsearch.
  * Rename `graylog_server__admin_user` to `graylog_server__root_user`
* **role:icinga_kubernetes**: Switch config to v0.3.0 multi-cluster format, remove `icinga_kubernetes__kubeconfig_path`
* **role:icingadb**: Split into two roles, one for the IcingaDB daemon and one for IcingaDB Web. Have a look at the variables in the READMEs. Generally it is enough to rename `icingadb__api_user_login` to `icingadb_web__api_user_login`.
* **role:icingaweb2_module_director**: The `icingaweb2_module_director:basket` tag only runs if explicitly called to prevent accidental config overwrites
* **role:icingaweb2_module_vspheredb**: Remove the `v` prefix from the `icingaweb2_module_vspheredb__version` variable to be consistent with the other `icingaweb2_module_*` roles
* **role:kvm_vm**: Change `kvm_vm__boot_uefi` (bool) to `kvm_vm__boot` (string)
* **role:login**: Change default of `remove_other_sshd_authorized_keys` from `true` to `false`
* **role:mailto_root**:
  * Move most functionality to `role:postfix`, remove the `mailto_root:configure` and `mailto_root:testmail` tags
  * Change `mailto_root__from` from optional to mandatory
  * Testmail to external addresses now uses sender address (`mailto_root__from`)
* **role:mariadb_client**: Remove (use the `apps` role instead)
* **role:mariadb_server**:
  * Remove support for EOL versions 10.3 and 10.4
  * Remove support for non-LTS versions
  * Change default of `mariadb_server__cnf_client_ssl_verify_server_cert__*_var` for versions lower than 10.11 from `true` to `false` to prevent errors when SSL is disabled
* **module:bitwarden_item**, **lookup_plugin:bitwarden**:
  * Remove parameters `password_uppercase`, `password_lowercase`, `password_numeric`, `password_special`
  * Add parameter `password_choice`
* **role:mongodb**: Change `mongodb__conf_net_bind_ip` from a string to a list of strings. For example:
  ```yaml
  # old
  mongodb__conf_net_bind_ip: '0.0.0.0'

  # new
  mongodb__conf_net_bind_ip:
    - '0.0.0.0'
  ```
* **role:monitoring_plugins**:
  * Remove variables:
    * `monitoring_plugins__pip_executable`
    * `monitoring_plugins__pip_package`
    * `monitoring_plugins__python__modules`
    * `monitoring_plugins__windows_variant`
  * The `lfops__monitoring_plugins_version` variable (and all the `*.monitoring_plugin.*_version` variables) now only accepts a specific release or the value `dev`. `stable` or `latest` are no longer supported.
  * The `lfops__monitoring_plugins_version` variable is now mandatory.
  * Rename `monitoring_plugins__linux_variant` to `monitoring_plugins__install_method`:
    * `monitoring_plugins__linux_variant: 'python'` becomes `monitoring_plugins__install_method: 'source'`
  * Rename `monitoring_plugins__repo_version` to `monitoring_plugins__version`:
    * `monitoring_plugins__repo_version: 'latest'` becomes `monitoring_plugins__version: 'dev'`
  * Remove the tasks for Nuitka compilation, as the compilation is done by the [Monitoring Plugins GitHub Action](https://github.com/Linuxfabrik/monitoring-plugins/actions/workflows/nuitka-compile.yml) now
  * Lock the version of the `monitoring-plugins` package after installing it. Updating the plugins should be done manually along with updating the monitoring system configuration.
* **role:monitoring_plugins_grafana_dashboards**: Change from provisioning to grizzly for the deployment of the dashboards
* **role:mount**: Change `mount__mounts` to `mount__mounts__host_var` / `mount__mounts__group_var`
* **role:nextcloud**:
  * Rename `nextcloud__apps_config` to `nextcloud__app_configs__*_var`, add `state` subkey, make more use of the `value` subkey. `--value` is no longer required:
    ```yaml
    # old
    nextcloud__apps_config:
      - { key: 'core',              value: 'shareapi_default_expire_date --value=yes' }
    # new
    nextcloud__app_configs__host_var:
      - key: 'core shareapi_default_expire_date'
        value: 'yes'
        state: 'present'
    ```
  * Rename `nextcloud__apps` to `nextcloud__apps__*_var`, add `state` subkey
  * Rename `nextcloud__sysconfig` to `nextcloud__sysconfig__*_var`, add `state` subkey, make more use of the `value` subkey (same as `nextcloud__app_configs__*_var`)
  * Remove `nextcloud__proxyconfig`. Use `nextcloud__sysconfig__*_var` instead.
  * Implement [notify_push](https://github.com/nextcloud/notify_push). Add the following to your Apache HTTPd config:
    ```apacheconf
    RewriteRule ^\/push\/ws(.*) ws://nextcloud-server:7867/ws$1 [proxy,last]
    RewriteRule ^\/push\/(.*)   http://nextcloud-server:7867/$1 [proxy,last]
    ProxyPassReverse /push/     http://nextcloud-server:7867/
    ```
  * Change default of `nextcloud__timer_app_update_enabled` from `true` to `false`, as this can sometimes lead to Nextcloud ending up in maintenance mode
  * Rename `nextcloud__apache_httpd__vhosts_virtualhost_ip` to `nextcloud__vhost_virtualhost_ip`
  * Rename `nextcloud__apache_httpd__vhosts_virtualhost_port` to `nextcloud__vhost_virtualhost_port`
* **role:opensearch**:
  * Change default of `opensearch__plugins_security_disabled` from `true` to `false`
  * For new installations of OpenSearch 2.12 and later, you must define a custom admin password in `opensearch__opensearch_initial_admin_password`
* **role:openssl**: Remove (use the `apps` role instead)
* **role:perl**: Remove (use the `apps` role instead)
* **role:postfix**: Now completely templates the whole config file. Beware when running against existing hosts.
* **role:postgresql_server**: Rename the `name` subkey of `postgresql_server__users__*_var` to `username` for consistency and easier integration of the Bitwarden lookup plugin
* **role:python**: Change `python__modules__*_var` to a list of dictionaries from a list of strings
* **role:redis**:
  * Drop support for Redis v5 (end of life)
  * Drop support for Redis v6
  * Change default of `redis__service_timeout_start_sec` and `redis__service_timeout_stop_sec` from `5s` to `90s`
* **role:repo_icinga**:
  * Remove `repo_icinga__use_subscription_url` for RHEL (and compatibles) as the packages without a subscription are outdated. The variable is now only effective for openSUSE and SLES.
  * Rename `repo_icinga__subscription_login` to `repo_icinga__basic_auth_login` and add a variable to explicitly use the Icinga Repo Subscription URL (`repo_icinga__use_subscription_url`). If you have `repo_icinga__subscription_login` set in your inventory, rename it to `repo_icinga__basic_auth_login` and set `repo_icinga__use_subscription_url: true` for the same effect.
* **role:repo_mydumper**: Adjust to use https://repo.linuxfabrik.ch/mydumper/ by default. Remove `repo_mydumper__baseurl`, add `repo_mydumper__mirror_url` instead.
* **role:rocketchat**:
  * Switch deployment method from native installation to Podman container
  * Remove `rocketchat__npm_version` variable
  * Rename and alter:
    * `rocketchat__application_path` to `rocketchat__user_home_directory` (new default: `'/opt/rocketchat'`)
    * `rocketchat__service_enabled` to `rocketchat__container_enabled`
    * `rocketchat__service_state` to `rocketchat__container_state`
  * Change default of `rocketchat__mongodb_host` to `'host.containers.internal'`
  * Remove Rocket.Chat notifications from the default banaction
* **role:selinux**: Change `ports` subkey of `selinux__ports__*_var` to `port`, accepting only a single port or port range, not a list
* **role:sshd**:
  * Remove `sshd__ciphers`, `sshd__kex` and `sshd__macs` variables, as these settings are managed by `crypto-policy` on RHEL
  * Now deploy the complete `/etc/ssh/sshd_config` as a template
  * Remove support for RHEL 7
* **role:system_update**: Remove `system_update__icinga2_master` variable. Use `system_update__icinga2_api_url` instead.
* **role:systemd_journald**: The value for `systemd_journald__conf_system_max_use` is now interpreted as a size in bytes. It supports the size specifications possible in `journald.conf` (e.g. `4G`). If you want to specify a percentage, use `'40%'`.
* **role:tar**: Remove (use the `apps` role instead)
* **playbook:icinga2_agent**: Change to also include the installation of the [Linuxfabrik Monitoring Plugins](https://github.com/Linuxfabrik/monitoring-plugins). This can be skipped by setting `icinga2_agent__skip_monitoring_plugins: true`.
* **playbook:setup_icinga2_master**:
  * Change default of `setup_icinga2_master__icingaweb2_module_company__skip_role` from `false` to `true`
  * Change the format of the role skip-variables from `playbook_name_skip_role_name` to `playbook_name__role_name__skip_role` for clarity and consistency. Have a look at the [README.md](./README.md#skipping-roles-in-a-playbook).
  * Add `playbook_name__role_name__skip_role_injections` variables to disable or re-enable the role's injections
  * Change default of `setup_icinga2_master__skip_icingaweb2_module_monitoring` from `false` to `true`

### Added

* **role:nextcloud**: Add nextcloud_occ_*_config modules with diff and check mode support
* **role:alternatives**: Add new role
* **role:apache_httpd**:
  * Add some mods
  * Add `skip_allowed_file_extensions` vHost variable
  * Add `skip_allowed_http_methods` vHost variable
* **role:apache_solr**: Add new role
* **role:audit**: Add more config variables
* **role:bind**:
  * Add multiple new variables, now allowing a primary-secondary setup
  * Add `bind__named_conf_raw` variable
* **role:blocky**: Add new role
* **role:borg_local**: Add new role
* **role:clamav**: Add new role
* **role:cloud_init**: Add task to remove `/etc/cloud/cloud.cfg.rpmsave`
* **role:collect_rpmnew_rpmsave**: Add new role
* **role:dnf_versionlock**: Add new role
* **role:duplicity**: Add `duplicity__backup_full_if_older_than` variable
* **role:fangfrisch**: Add new role
* **role:firewall**: Add `firewall__firewalld_ports__*_var` and `firewall__firewalld_services__*_var` variables
* **role:github_project_createrepo**: Add new role
* **role:glpi_agent**: Add new role
* **role:grafana**: Add creation of service accounts and their tokens
* **role:grafana_grizzly**: Add new role
* **role:graylog_datanode**: Add new role
* **role:graylog_server**: Add variables and documentation for multi-node setup; add Debian support
* **role:icinga2_agent**: Add `icinga2_agent:update` tag; add `icinga2_agent__validate_certs` variable
* **role:icinga2_master**: Add `icinga2_master__bind_host` variable
* **role:icinga_kubernetes**: Add new role
* **role:icinga_kubernetes_web**: Add new role
* **role:icingadb**: Add new role
* **role:icingaweb2_module_businessprocess**: Add new role
* **role:icingaweb2_module_cube**: Add new role
* **role:icingaweb2_module_fileshipper**: Add new role
* **role:icingaweb2_module_generictts**: Add new role
* **role:icingaweb2_module_jira**: Add new role
* **role:icingaweb2_module_reporting**: Add new role
* **role:icingaweb2_module_x509**: Add `icingaweb2_module_x509__url` variable
* **role:kvm_vm**: Add the option to boot the VM with UEFI
* **role:logrotate**: Add compression
* **role:mariadb_server**:
  * Add `mariadb_server__cnf_wsrep_sst_auth` and `mariadb_server__cnf_wsrep_sst_method` variables
  * Add `mariadb_server__cnf_extra_max_connections__*_var` and `mariadb_server__cnf_extra_port__*_var` variables
  * Add support for client and server TLS
  * Add Galera cluster installation
  * Make datadir configurable, including copy of old data to the new location
  * Make socket configurable
* **role:mirror**: Add new role
* **role:mongodb**:
  * Add Debian support
  * Add keyfile handling
  * Adjust for replica set across members
  * Implement user management ([fix #89](https://github.com/Linuxfabrik/lfops/issues/89))
* **role:moodle**: Add new role
* **role:mount**: Add new role
* **role:opensearch**: Add Debian support; add variables for cluster configuration
* **role:php**: Add tag `php:fpm`
* **role:podman_containers**: Add new role
* **role:proxysql**: Add new role
* **role:python_venv**:
  * Allow specifying different certificate store
  * Allow specifying the Python executable to be used in the venv
  * Add Debian support
* **role:repo_baseos**: Add AlmaLinux 8 support
* **role:repo_epel**: Add `repo_epel__epel_cisco_openh264_enabled` variable
* **role:repo_gitlab_runner**: Add new role
* **role:repo_graylog**: Add Debian support
* **role:repo_mongodb**: Add Debian support
* **role:repo_opensearch**: Add Debian support
* **role:repo_proxysql**: Add new role
* **role:repo_redis**: Add new role
* **role:repo_rpmfusion**: Add new role
* **role:selinux**: Add support for SELinux ports
* **role:shell**: Add new role; add option to ignore errors during command execution
* **role:system_update**: Add option `-y` to `yum check-update`
* **role:systemd_journald**: Add variable `systemd_journald__conf_system_keep_free`; make `SystemMaxUse` configurable
* **role:systemd_unit**: Add support for mount units
* **role:tools**: Add `tools__prompt_use_fqdn` variable
* **playbook:setup_basic**: Add support for AlmaLinux 8

### Changed

* **role:apache_httpd**: Change default of the `conf_custom_log` vHost variable from unset to `'logs/{{ conf_server_name }}-access.log linuxfabrikio'`
* **role:graylog_server**: Remove version defaults from the role
* **role:icingaweb2_module_grafana**: Change GitHub repo from Mikesch-mp to NETWAYS
* **role:mariadb_server**: mariadb-dump checks for the mydumper version and sets parameters accordingly
* **role:open_vm_tools**: Start and enable `vmtoolsd`
* **role:opensearch**: Make `opensearch__version*` optional

### Fixed

* **role:influxdb**: Fix wrong systemd service name, which was preventing InfluxDB dumps from being scheduled
* **role:mariadb_server**:
  * Fix handler when `bind_address` is not localhost
  * Add installation of missing package for mariabackup Galera SST
  * Fix clone-datadir against new Galera cluster
* **role:redis**: Fix various messages from log, fix v7 template settings, fix various comments and README


## [v2.0.1] - 2023-02-28

### Changed

* Adjustments for the [Ansible Galaxy Release](https://galaxy.ansible.com/linuxfabrik/lfops)


## [v2.0.0] - 2023-02-28

### Breaking Changes

* **All roles**: Rename all injectable variables:
  * `rolename__combined_varname` to `rolename__varname__combined_var`
  * `rolename__dependent_varname` to `rolename__varname__dependent_var`
  * `rolename__group_varname` to `rolename__varname__group_var`
  * `rolename__host_varname` to `rolename__varname__host_var`
  * `rolename__role_varname` to `rolename__varname__role_var`
* **role:acme_sh**:
  * Add `name` subkey to `acme_sh__certificates`
  * Move `acme_sh__reload_cmd` to a subkey of `acme_sh__certificates`
* **role:chrony**: Fix wrong variable prefix: adjust `chrony_server__` to `chrony__`
* **role:collabora**: Rename rolename and vars from `collabora_code` to `collabora`
* **role:duplicity**:
  * Rename `duplicity__public_master_long_keyid` to `duplicity__gpg_encrypt_master_key`
  * Rename `duplicity__public_master_key` to `duplicity__gpg_encrypt_master_key_block`
  * Change the format of `duplicity__backup_sources__host_var`
* **role:fail2ban**: Adjust subkeys of `fail2ban__jails__group_var` / `fail2ban__jails__host_var`
* **role:git**: Add and later remove in favor of a more general `apps` role
* **role:hostname**:
  * Rename `hostname__domain_name` to `hostname__domain_part`
  * Rename `hostname__hostname` to `hostname__full_hostname`
* **role:icinga2_agent**:
  * Add new mandatory variable `icinga2_agent__icinga2_master_cn`
  * Make `icinga2_agent__icinga2_master_host` optional
  * Most users can replace all instances of `icinga2_agent__icinga2_master_host` with `icinga2_agent__icinga2_master_cn`
* **role:infomaniak_vm**:
  * Rename `infomaniak_vm__password` to `infomaniak_vm__api_password`
  * Rename `infomaniak_vm__project_id` to `infomaniak_vm__api_project_id`
  * Rename `infomaniak_vm__username` to `infomaniak_vm__api_username`
  * Rename `infomaniak_vm__volume_size` to `infomaniak_vm__separate_boot_volume_size`
* **role:java**: Remove, better substituted by the `apps` role
* **role:kernel_settings**: Make `kernel_settings__` variables injection-capable via `kernel_settings__host_*`, `kernel_settings__group_*` and `kernel_settings__dependent_*`
* **role:libselinux_python**: Rename the role to `policycoreutils`
* **role:login**: Change logic and rename `login__users` to two combined variables `login__users__group_var` (define users in group vars) and `login__users__host_var` (define users in host vars)
* **role:mariadb_server**:
  * Rename `mariadb_server__admin_login` to `mariadb_server__admin_user`
  * Move `mariadb_server__admin_host` to `mariadb_server__admin_user["host"]`
  * Rename `mariadb_server__dump_login` to `mariadb_server__dump_user`
  * Move `mariadb_server__dump_user_*` to subkeys in `mariadb_server__dump_user`
* **role:monitoring_plugins**: Rename `monitoring_plugins__deploy_notification_plugins` to `monitoring_plugins__skip_notification_plugins` and flip the logic
* **role:php**: Make more variables injectable, therefore the variables have a new name
* **role:stig**: Move to a new GitHub repo (temporarily)
* **role:system_update**: Rename variables (note: old and new names appear identical in the original CHANGELOG, likely a documentation error):
  * `system_update__mail_recipients_new_configfiles` => `system_update__mail_recipients_new_configfiles`
  * `system_update__mail_recipients_updates` => `system_update__mail_recipients_updates`
  * `system_update__mail_from` => `system_update__mail_from`
  * `system_update__mail_subject_prefix` => `system_update__mail_subject_prefix`
  * `system_update__notify_and_schedule_on_calendar` => `system_update__notify_and_schedule_on_calendar`
* **playbook:basic_setup**: Rename to `setup_basic` to be consistent with the other setup playbooks. Remove `audit` and `crypto_policy` roles for now.

### Added

* This CHANGELOG
* **role:acme_sh**: Add new role
* **role:ansible_init**: Add new role
* **role:apache_httpd**: Add new role
* **role:apache_tomcat**: Add new role
* **role:apps**: Add new role
* **role:at**: Add new role
* **role:audit**: Add new role
* **role:bind**: Add new role
* **role:chrony**: Add new role
* **role:cloud_init**: Add new role
* **role:cockpit**: Add new role
* **role:collabora**: Add new role
* **role:coturn**: Add new role
* **role:crypto_policy**: Add new role
* **role:dnf_makecache**: Add new role
* **role:docker**: Add new role
* **role:elasticsearch_oss**: Add new role
* **role:exoscale_vm**: Add new role
* **role:fail2ban**: Add new role
* **role:firewall**: Add new role
* **role:freeipa_client**: Add new role
* **role:freeipa_server**: Add new role
* **role:glances**: Add new role
* **role:grafana**: Add new role
* **role:grav**: Add new role
* **role:graylog_server**: Add new role
* **role:haveged**: Add new role
* **role:hetzner_vm**: Add new role
* **role:hostname**: Add new role
* **role:icinga2_agent**: Add new role
* **role:icinga2_master**: Add new role
* **role:icingaweb2**: Add new role
* **role:icingaweb2_module_company**: Add new role
* **role:icingaweb2_module_director**: Add new role
* **role:icingaweb2_module_doc**: Add new role
* **role:icingaweb2_module_grafana**: Add new role
* **role:icingaweb2_module_incubator**: Add new role
* **role:icingaweb2_module_monitoring**: Add new role
* **role:icingaweb2_module_vspheredb**: Add new role
* **role:influxdb**: Add new role
* **role:infomaniak_vm**: Add new role
* **role:kdump**: Add new role
* **role:keepalived**: Add new role
* **role:kernel_settings**: Add new role
* **role:keycloak**: Add new role
* **role:kvm_host**: Add new role
* **role:kvm_vm**: Add new role
* **role:libmaxminddb**: Add new role
* **role:librenms**: Add new role
* **role:libreoffice**: Add new role
* **role:login**: Add new role
* **role:mailto_root**: Add new role
* **role:mariadb_client**: Add new role
* **role:mariadb_server**: Add new role
* **role:maxmind_geoip**: Add new role
* **role:minio_client**: Add new role
* **role:mod_maxminddb**: Add new role
* **role:mongodb**: Add new role
* **role:motd**: Add new role
* **role:network**: Add new role; add functionality to configure network connections
* **role:nextcloud**: Add new role
* **role:nfs_client**: Add new role
* **role:nfs_server**: Add new role
* **role:nodejs**: Add new role
* **role:objectstore_backup**: Add new role
* **role:open_vm_tools**: Add new role
* **role:openssl**: Add new role
* **role:openvpn_server**: Add new role
* **role:perl**: Add new role
* **role:php**: Add new role
* **role:policycoreutils**: Add new role
* **role:postgresql_server**: Add new role
* **role:qemu_guest_agent**: Add new role
* **role:redis**: Add new role
* **role:repo_baseos**: Add new role
* **role:repo_collabora**: Add new role
* **role:repo_collabora_code**: Add new role
* **role:repo_debian_base**: Add new role
* **role:repo_docker**: Add new role
* **role:repo_elasticsearch_oss**: Add new role
* **role:repo_gitlab_ce**: Add new role
* **role:repo_grafana**: Add new role
* **role:repo_icinga**: Add new role
* **role:repo_influxdb**: Add new role
* **role:repo_mariadb**: Add new role
* **role:repo_mongodb**: Add new role
* **role:repo_monitoring_plugins**: Add new role
* **role:repo_mydumper**: Add new role
* **role:repo_postgresql**: Add new role
* **role:repo_remi**: Add new role
* **role:repo_sury**: Add new role
* **role:rocketchat**: Add new role
* **role:rsyslog**: Add new role
* **role:snmp**: Add new role
* **role:sshd**: Add new role
* **role:stig**: Add new role
* **role:system_update**: Add new role
* **role:systemd_journald**: Add new role
* **role:systemd_unit**: Add new role
* **role:tar**: Add new role
* **role:telegraf**: Add new role
* **role:timezone**: Add new role
* **role:unattended_upgrades**: Add new role
* **role:wordpress**: Add new role
* **role:yum_utils**: Add new role

### Changed

* **module_util:bitwarden**: Switch to the Bitwarden client API, as it is more reliable than using the command line tool directly
* **role:acme_sh**: Automatically update acme.sh ([fix #74](https://github.com/Linuxfabrik/lfops/issues/74))
* **role:apache_tomcat**: Use the correct Java version depending on Tomcat version ([fix #82](https://github.com/Linuxfabrik/lfops/issues/82))
* **role:duplicity**: Implement massive-parallel backups
* **role:hetzner_vm**: Improve handling of IP addresses (new Hetzner features) ([fix #72](https://github.com/Linuxfabrik/lfops/issues/72)); manage the provider firewall ([fix #71](https://github.com/Linuxfabrik/lfops/issues/71))
* **role:login**: Add a switch to be aggressive or not ([fix #65](https://github.com/Linuxfabrik/lfops/issues/65))
* **role:mariadb_server**: Implement mydumper / adapt to the LFOps standards ([fix #56](https://github.com/Linuxfabrik/lfops/issues/56))
* **role:mongodb**: Implement dumping / user management ([fix #78](https://github.com/Linuxfabrik/lfops/issues/78))
* **role:python**: On RHEL 8+, don't install `python3`. Instead install `python38` or `python39` explicitly ([fix #62](https://github.com/Linuxfabrik/lfops/issues/62))
* **role:tools**: Show distro in prompt ([fix #47](https://github.com/Linuxfabrik/lfops/issues/47))

### Fixed

* **role:audit**: Fix wrong README ([fix #51](https://github.com/Linuxfabrik/lfops/issues/51), [fix #58](https://github.com/Linuxfabrik/lfops/issues/58))
* **role:crypto_policy**: Fix wrong README ([fix #52](https://github.com/Linuxfabrik/lfops/issues/52), [fix #76](https://github.com/Linuxfabrik/lfops/issues/76))
* **role:icinga2_agent**: On Debian, user `nagios` does not exist when certs folder is created ([fix #77](https://github.com/Linuxfabrik/lfops/issues/77))
* **role:icinga2_master**: Fix missing option name in `icinga2_master/tasks/main.yml` ([fix #105](https://github.com/Linuxfabrik/lfops/issues/105))
* **role:monitoring_plugins**: Fix "deploy" vs "skip" logic ([fix #103](https://github.com/Linuxfabrik/lfops/issues/103))
* **role:repo_graylog**: Fix `repo_graylog__mirror_url` never actually being used ([fix #94](https://github.com/Linuxfabrik/lfops/issues/94))
* **role:sshd**: Fix `ModuleNotFoundError: No module named 'seobject'` ([fix #53](https://github.com/Linuxfabrik/lfops/issues/53))
* **playbook:basic_setup**: Fix `Failed to set locale, defaulting to C.UTF-8` ([fix #55](https://github.com/Linuxfabrik/lfops/issues/55))
* Do not use `become: true` in all playbooks ([fix #66](https://github.com/Linuxfabrik/lfops/issues/66))
* Deploy nft in basic-setup or the fwbuilder role ([fix #61](https://github.com/Linuxfabrik/lfops/issues/61))
* **role:freeipa_server**: Fix `In unattended mode you need to provide at least -r, -p and -a options` ([fix #83](https://github.com/Linuxfabrik/lfops/issues/83))


## [v1.0.1] - 2022-03-17

### Changed

* Adjust tags for Ansible Galaxy


## [v1.0.0] - 2022-03-17

### Added

* **role:duplicity**: Add new role
* **role:monitoring_plugins**: Add new role
* **role:python_venv**: Add new role
* **role:repo_epel**: Add new role
* **module:bitwarden_item**: Add new module
* **module:gpg_key**: Add new module
* **lookup_plugin:bitwarden**: Add new lookup plugin
* **module_util:bitwarden**: Add new module util
* **module_util:gnupg**: Add new module util


[Unreleased]: https://github.com/Linuxfabrik/lfops/compare/v9.0.0...HEAD
[v9.0.0]: https://github.com/Linuxfabrik/lfops/compare/v8.0.0...v9.0.0
[v8.0.0]: https://github.com/Linuxfabrik/lfops/compare/v7.0.0...v8.0.0
[v7.0.0]: https://github.com/Linuxfabrik/lfops/compare/v6.0.1...v7.0.0
[v6.0.1]: https://github.com/Linuxfabrik/lfops/compare/v6.0.0...v6.0.1
[v6.0.0]: https://github.com/Linuxfabrik/lfops/compare/v5.1.0...v6.0.0
[v5.1.0]: https://github.com/Linuxfabrik/lfops/compare/v5.0.0...v5.1.0
[v5.0.0]: https://github.com/Linuxfabrik/lfops/compare/v4.0.0...v5.0.0
[v4.0.0]: https://github.com/Linuxfabrik/lfops/compare/v3.0.0...v4.0.0
[v3.0.0]: https://github.com/Linuxfabrik/lfops/compare/v2.0.1...v3.0.0
[v2.0.1]: https://github.com/Linuxfabrik/lfops/compare/v2.0.0...v2.0.1
[v2.0.0]: https://github.com/Linuxfabrik/lfops/compare/v1.0.1...v2.0.0
[v1.0.1]: https://github.com/Linuxfabrik/lfops/compare/v1.0.0...v1.0.1
[v1.0.0]: https://github.com/Linuxfabrik/lfops/releases/tag/v1.0.0