From 028cb2b9110a85b1c0dc8af382269f8ad2519cfa Mon Sep 17 00:00:00 2001 From: Markus Frei Date: Fri, 2 Oct 2026 14:41:12 +0200 Subject: [PATCH 1/2] fix(roles/aide): check /boot/efi without the inode number vfat stores no inode numbers. fat_build_inode() assigns one with iunique() whenever a file is read in again, so once the inodes below /boot/efi had been evicted from the cache, every check reported all files there as changed (seen on a Rocky 9 host). /boot/efi now uses DATAONLY, like /etc/ld.so.cache, which still reports changes of content, permissions and ownership. The aide scenario drops the page cache before the clean check, asserts that the probe file below /boot/efi got a new inode number, and that a content change there is still reported. Verified on Debian 13, Rocky 8, Rocky 9 and Ubuntu 24.04. --- CHANGELOG.md | 1 + extensions/molecule/aide/prepare.yml | 23 ++++++++ extensions/molecule/aide/verify.yml | 79 ++++++++++++++++++++++++---- roles/aide/README.md | 1 + roles/aide/defaults/main.yml | 7 +++ 5 files changed, 102 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e84a602bb..5329c841b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,6 +91,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* **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. diff --git a/extensions/molecule/aide/prepare.yml b/extensions/molecule/aide/prepare.yml index 489453ba2..d14aea508 100644 --- a/extensions/molecule/aide/prepare.yml +++ b/extensions/molecule/aide/prepare.yml @@ -108,6 +108,29 @@ mode: 0o644 +# A file on the EFI system partition that the database has to contain from the start, so that +# verify.yml can show that the default rule '/boot/efi DATAONLY' ignores the inode numbers vfat +# hands out anew, but still reports a changed content. No owner, group or mode: vfat has none +# per file, the mount options set them. +- name: 'Deploy /boot/efi/lfops-molecule' + hosts: 'systems_under_test' + + tasks: + + - name: 'findmnt --noheadings --output FSTYPE /boot/efi' + ansible.builtin.command: 'findmnt --noheadings --output FSTYPE /boot/efi' + register: '__molecule__efi_fstype_result' + changed_when: false + failed_when: false + + - name: 'Deploy /boot/efi/lfops-molecule' + ansible.builtin.copy: + content: 'lfops-molecule' + dest: '/boot/efi/lfops-molecule' + when: + - '__molecule__efi_fstype_result["stdout"] == "vfat"' + + # A host that already runs the daily check of aide-common, which the role has to remove, since # it would overwrite the report of aidecheck.service. Without its recommended packages, which # would pull in a mail transport agent. diff --git a/extensions/molecule/aide/verify.yml b/extensions/molecule/aide/verify.yml index 6fafcba69..8a50b31e4 100644 --- a/extensions/molecule/aide/verify.yml +++ b/extensions/molecule/aide/verify.yml @@ -1,15 +1,17 @@ # Verify aide on the running system: the check timer is armed, aide-common (installed by # prepare.yml on Debian and Ubuntu) is gone, and a check against the database the role created # runs clean, which proves that aide reads the deployed config and that the database matches it, -# including the role's own files. A forced log rotation leaves that report in place. A config -# aide rejects aborts the role before it replaces -# /etc/aide.conf. Then a file below the scenario's regular rule makes the check fail and shows -# up in the report with all its attributes (report level added_removed_entries), while files -# below its negative rules (one in the rule list, one in aide__conf_raw) do not, and neither -# does a file below /opt, whose default rule the scenario removes. A content change of -# /root/.lfops-molecule is reported, which only its own rule does, not the default '/root/\..* -# PERMS' of the same directory. This only works if the rules and the raw part from the inventory -# are in effect, since /srv is not monitored otherwise and /opt is by default. +# including the role's own files. On a vfat /boot/efi that check runs after the inodes there have +# been dropped from the cache, which gives the files new inode numbers that the default rule +# ignores. A forced log rotation leaves that report in place. A config aide rejects aborts the +# role before it replaces /etc/aide.conf. Then a file below the scenario's regular rule makes the +# check fail and shows up in the report with all its attributes (report level +# added_removed_entries), while files below its negative rules (one in the rule list, one in +# aide__conf_raw) do not, and neither does a file below /opt, whose default rule the scenario +# removes. A content change of /root/.lfops-molecule is reported, which only its own rule does, +# not the default '/root/\..* PERMS' of the same directory, and so is a content change of +# /boot/efi/lfops-molecule. This only works if the rules and the raw part from the inventory are +# in effect, since /srv is not monitored otherwise and /opt is by default. # In between, the role is run again twice, each time after a change it has to revert, to # prove that its handler leaves the database alone in both cases where re-baselining would # accept unvetted changes: a database found without the role's check unit, and a last check @@ -66,6 +68,43 @@ - 'not __molecule__aide_common_conf_result["stat"]["exists"]' fail_msg: '{{ __molecule__aide_common_result["stdout"] | d("") }}' + # vfat stores no inode numbers, the kernel assigns a new one whenever it reads a file in + # again. dropping the cache forces that, so the check below sees new inode numbers for every + # file below /boot/efi, which it has to ignore. 3, not 2: the kernel keeps an inode that + # still has pages in the page cache (inode_lru_isolate() in fs/inode.c). Verified on Debian + # 13, Rocky 9 and Ubuntu 24.04, where 2 leaves the inode number as it is. + - name: 'findmnt --noheadings --output FSTYPE /boot/efi' + ansible.builtin.command: 'findmnt --noheadings --output FSTYPE /boot/efi' + register: '__molecule__efi_fstype_result' + changed_when: false + failed_when: false + + - name: 'Drop the inodes of /boot/efi from the cache' + when: + - '__molecule__efi_fstype_result["stdout"] == "vfat"' + block: + + - name: 'stat /boot/efi/lfops-molecule (before dropping the cache)' + ansible.builtin.stat: + path: '/boot/efi/lfops-molecule' + get_checksum: false + register: '__molecule__efi_probe_before_result' + + - name: 'sync; echo 3 > /proc/sys/vm/drop_caches' + ansible.builtin.shell: 'sync; echo 3 > /proc/sys/vm/drop_caches' + changed_when: false + + - name: 'stat /boot/efi/lfops-molecule (after dropping the cache)' + ansible.builtin.stat: + path: '/boot/efi/lfops-molecule' + get_checksum: false + register: '__molecule__efi_probe_after_result' + + - name: 'Assert that the probe file on /boot/efi got a new inode number' + ansible.builtin.assert: + that: + - '__molecule__efi_probe_after_result["stat"]["inode"] != __molecule__efi_probe_before_result["stat"]["inode"]' + - name: 'systemctl start --wait aidecheck.service (clean host)' # noqa command-instead-of-module (wait for the check to finish and read its Result) ansible.builtin.command: 'systemctl start --wait aidecheck.service' changed_when: false @@ -236,6 +275,13 @@ group: 'root' mode: 0o644 + - name: 'Change the content of /boot/efi/lfops-molecule' + ansible.builtin.copy: + content: 'lfops-molecule-changed' + dest: '/boot/efi/lfops-molecule' + when: + - '__molecule__efi_fstype_result["stdout"] == "vfat"' + - name: 'systemctl start --wait aidecheck.service (changed host)' # noqa command-instead-of-module (wait for the check to finish and read its Result) ansible.builtin.command: 'systemctl start --wait aidecheck.service' changed_when: false @@ -266,6 +312,14 @@ - '"/etc/cron.d/lfops-molecule" not in __molecule__aide_changed_log_result["stdout"]' fail_msg: '{{ __molecule__aide_changed_log_result["stdout"] }}' + - name: 'Assert that the check reports the changed content of /boot/efi/lfops-molecule' + ansible.builtin.assert: + that: + - '"/boot/efi/lfops-molecule" in __molecule__aide_changed_log_result["stdout"]' + fail_msg: '{{ __molecule__aide_changed_log_result["stdout"] }}' + when: + - '__molecule__efi_fstype_result["stdout"] == "vfat"' + # the check is failing now, so a config change must not re-baseline the database either. # the role run below reverts the hand edit, which notifies the handler. - name: 'sha256sum /var/lib/aide/aide.db.gz (before the run after a failed check)' @@ -325,6 +379,13 @@ group: 'root' mode: 0o644 + - name: 'Restore the content of /boot/efi/lfops-molecule' + ansible.builtin.copy: + content: 'lfops-molecule' + dest: '/boot/efi/lfops-molecule' + when: + - '__molecule__efi_fstype_result["stdout"] == "vfat"' + # the last check failed, so the tasks `--tags aide:update_db` runs must leave the database # alone and say so - name: 'sha256sum /var/lib/aide/aide.db.gz (before update-db-if-clean after a failed check)' diff --git a/roles/aide/README.md b/roles/aide/README.md index cbd96948d..3e0f45d8c 100644 --- a/roles/aide/README.md +++ b/roles/aide/README.md @@ -118,6 +118,7 @@ This role is compatible with the following aide versions: * `!/usr/lib/sysimage/rpm` is added. The RPM database lives there on RHEL 10, and every rpm or dnf query rewrites it. * The audit tool rules of the CIS benchmarks are added. * `/etc/ld.so.cache` and `/etc/udev/hwdb.bin` are checked without their inode (`DATAONLY`), since systemd rebuilds both on the first boot after an update. + * `/boot/efi` is checked without the inode (`DATAONLY`), since the kernel assigns new inode numbers on the vfat EFI system partition at runtime. * `/etc/aliases.db` is checked for permissions only (`PERMS`), since postfix on the Red Hat family rebuilds it on its start. * `/etc/resolv.conf` is checked for permissions only (`PERMS`), since DHCP clients and NetworkManager rewrite it at runtime. diff --git a/roles/aide/defaults/main.yml b/roles/aide/defaults/main.yml index 6e9db9501..778bfb846 100644 --- a/roles/aide/defaults/main.yml +++ b/roles/aide/defaults/main.yml @@ -24,6 +24,13 @@ aide__rules__host_var: [] aide__rules__role_var: - path: '/boot' attributes: 'NORMAL' + # Not in the shipped list. The EFI system partition is vfat, which stores no inode numbers: + # the kernel assigns a new one whenever it reads a file in again (iunique() in + # fat_build_inode(), fs/fat/inode.c), so NORMAL reports every file below it as changed once + # its inode has been dropped from the cache. DATAONLY still reports a changed content. + # Verified on Debian 13, Rocky 8, Rocky 9 and Ubuntu 24.04. + - path: '/boot/efi' + attributes: 'DATAONLY' - path: '/bin' attributes: 'NORMAL' - path: '/sbin' From b2995b620f11a2097e7bf993de07a55955c94625 Mon Sep 17 00:00:00 2001 From: Markus Frei Date: Fri, 2 Oct 2026 15:09:29 +0200 Subject: [PATCH 2/2] fix(roles/aide): check /etc/fwupd/fwupd.conf without its mode fwupd 1.9.1 and later sets its config to 0640 whenever the daemon starts (fu_config_ensure_permissions() in libfwupdplugin/fu-config.c), while the package ships it with 0644. The catch-all `/etc PERMS` checks the mode and its ACL, so the first daemon start after `aide --init` or after an update of fwupd failed the check. The file is now checked with PERMS minus the mode and the ACL. The aide scenario resets the file to 0644, restarts fwupd, asserts that the daemon set it to 0640, and requires the check to stay clean. Verified on Rocky 9, Ubuntu 24.04 and Ubuntu 26.04. --- CHANGELOG.md | 1 + extensions/molecule/aide/verify.yml | 54 ++++++++++++++++++++++++----- roles/aide/README.md | 1 + roles/aide/defaults/main.yml | 8 +++++ 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5329c841b..6e776ca83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,6 +91,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* **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. diff --git a/extensions/molecule/aide/verify.yml b/extensions/molecule/aide/verify.yml index 8a50b31e4..9f4554a67 100644 --- a/extensions/molecule/aide/verify.yml +++ b/extensions/molecule/aide/verify.yml @@ -3,15 +3,16 @@ # runs clean, which proves that aide reads the deployed config and that the database matches it, # including the role's own files. On a vfat /boot/efi that check runs after the inodes there have # been dropped from the cache, which gives the files new inode numbers that the default rule -# ignores. A forced log rotation leaves that report in place. A config aide rejects aborts the -# role before it replaces /etc/aide.conf. Then a file below the scenario's regular rule makes the -# check fail and shows up in the report with all its attributes (report level -# added_removed_entries), while files below its negative rules (one in the rule list, one in -# aide__conf_raw) do not, and neither does a file below /opt, whose default rule the scenario -# removes. A content change of /root/.lfops-molecule is reported, which only its own rule does, -# not the default '/root/\..* PERMS' of the same directory, and so is a content change of -# /boot/efi/lfops-molecule. This only works if the rules and the raw part from the inventory are -# in effect, since /srv is not monitored otherwise and /opt is by default. +# ignores. It also ignores the mode fwupd sets on its config when its daemon starts. A forced log +# rotation leaves that report in place. A config aide rejects aborts the role before it replaces +# /etc/aide.conf. Then a file below the scenario's regular rule makes the check fail and shows up +# in the report with all its attributes (report level added_removed_entries), while files below +# its negative rules (one in the rule list, one in aide__conf_raw) do not, and neither does a file +# below /opt, whose default rule the scenario removes. A content change of /root/.lfops-molecule +# is reported, which only its own rule does, not the default '/root/\..* PERMS' of the same +# directory, and so is a content change of /boot/efi/lfops-molecule. This only works if the rules +# and the raw part from the inventory are in effect, since /srv is not monitored otherwise and +# /opt is by default. # In between, the role is run again twice, each time after a change it has to revert, to # prove that its handler leaves the database alone in both cases where re-baselining would # accept unvetted changes: a database found without the role's check unit, and a last check @@ -105,6 +106,41 @@ that: - '__molecule__efi_probe_after_result["stat"]["inode"] != __molecule__efi_probe_before_result["stat"]["inode"]' + # fwupd sets its config to 0640 whenever its daemon starts, the package ships it with 0644. + # back to 0644 and a restart of the daemon trigger that, which the check below has to + # ignore. + - name: 'stat /etc/fwupd/fwupd.conf' + ansible.builtin.stat: + path: '/etc/fwupd/fwupd.conf' + get_checksum: false + register: '__molecule__fwupd_conf_result' + + - name: 'Have fwupd set the mode of its config' + when: + - '__molecule__fwupd_conf_result["stat"]["exists"]' + block: + + - name: 'chmod 0644 /etc/fwupd/fwupd.conf' + ansible.builtin.file: + path: '/etc/fwupd/fwupd.conf' + mode: 0o644 + + - name: 'systemctl restart fwupd.service' + ansible.builtin.service: + name: 'fwupd.service' + state: 'restarted' + + - name: 'stat /etc/fwupd/fwupd.conf (after the restart of fwupd)' + ansible.builtin.stat: + path: '/etc/fwupd/fwupd.conf' + get_checksum: false + register: '__molecule__fwupd_conf_after_result' + + - name: 'Assert that fwupd set its config to 0640' + ansible.builtin.assert: + that: + - '__molecule__fwupd_conf_after_result["stat"]["mode"] == "0640"' + - name: 'systemctl start --wait aidecheck.service (clean host)' # noqa command-instead-of-module (wait for the check to finish and read its Result) ansible.builtin.command: 'systemctl start --wait aidecheck.service' changed_when: false diff --git a/roles/aide/README.md b/roles/aide/README.md index 3e0f45d8c..bbed73fb5 100644 --- a/roles/aide/README.md +++ b/roles/aide/README.md @@ -121,6 +121,7 @@ This role is compatible with the following aide versions: * `/boot/efi` is checked without the inode (`DATAONLY`), since the kernel assigns new inode numbers on the vfat EFI system partition at runtime. * `/etc/aliases.db` is checked for permissions only (`PERMS`), since postfix on the Red Hat family rebuilds it on its start. * `/etc/resolv.conf` is checked for permissions only (`PERMS`), since DHCP clients and NetworkManager rewrite it at runtime. + * `/etc/fwupd/fwupd.conf` is checked without its mode, since fwupd sets the file to 0640 whenever its daemon starts. * Subkeys: diff --git a/roles/aide/defaults/main.yml b/roles/aide/defaults/main.yml index 778bfb846..660aedf3e 100644 --- a/roles/aide/defaults/main.yml +++ b/roles/aide/defaults/main.yml @@ -476,6 +476,14 @@ aide__rules__role_var: - path: '/etc/usbguard' attributes: 'NORMAL' + # Not in the shipped list. PERMS without the mode and its ACL: fwupd 1.9.1 and later sets the + # file to 0640 whenever its daemon starts (fu_config_ensure_permissions() in + # libfwupdplugin/fu-config.c), while the package ships it with 0644, so the first start after + # `aide --init` or after an update of fwupd was reported. Verified on Rocky 9, Ubuntu 24.04 + # and Ubuntu 26.04. + - path: '/etc/fwupd/fwupd\.conf$' + attributes: 'ftype+u+g+selinux+xattrs' + # Now everything else - path: '/etc' attributes: 'PERMS'