diff --git a/CHANGELOG.md b/CHANGELOG.md index e84a602bb..6e776ca83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,6 +91,8 @@ 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. * **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..9f4554a67 100644 --- a/extensions/molecule/aide/verify.yml +++ b/extensions/molecule/aide/verify.yml @@ -1,15 +1,18 @@ # 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. 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 @@ -66,6 +69,78 @@ - '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"]' + + # 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 @@ -236,6 +311,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 +348,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 +415,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..bbed73fb5 100644 --- a/roles/aide/README.md +++ b/roles/aide/README.md @@ -118,8 +118,10 @@ 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. + * `/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 6e9db9501..660aedf3e 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' @@ -469,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'