From e225f2fdb00ddd3283163913618791f8ca923dcb Mon Sep 17 00:00:00 2001 From: Jihan El Karz Date: Tue, 14 Apr 2026 10:56:29 +0200 Subject: [PATCH] feat(aide): add role and playbook --- playbooks/aide.yml | 23 ++ playbooks/all.yml | 1 + roles/aide/README.md | 116 +++++++ roles/aide/defaults/main.yml | 39 +++ roles/aide/tasks/main.yml | 88 +++++ roles/aide/templates/etc/aide.conf.j2 | 321 ++++++++++++++++++ .../etc/systemd/system/aide-check.service.j2 | 13 + .../etc/systemd/system/aide-check.timer.j2 | 13 + 8 files changed, 614 insertions(+) create mode 100644 playbooks/aide.yml create mode 100644 roles/aide/README.md create mode 100644 roles/aide/defaults/main.yml create mode 100644 roles/aide/tasks/main.yml create mode 100644 roles/aide/templates/etc/aide.conf.j2 create mode 100644 roles/aide/templates/etc/systemd/system/aide-check.service.j2 create mode 100644 roles/aide/templates/etc/systemd/system/aide-check.timer.j2 diff --git a/playbooks/aide.yml b/playbooks/aide.yml new file mode 100644 index 00000000..c2a51e36 --- /dev/null +++ b/playbooks/aide.yml @@ -0,0 +1,23 @@ +- name: 'Playbook linuxfabrik.lfops.aide' + hosts: + - 'lfops_aide' + + pre_tasks: + - ansible.builtin.import_role: + name: 'shared' + tasks_from: 'log-start.yml' + tags: + - 'always' + + + roles: + + - role: 'linuxfabrik.lfops.aide' + + + post_tasks: + - ansible.builtin.import_role: + name: 'shared' + tasks_from: 'log-end.yml' + tags: + - 'always' diff --git a/playbooks/all.yml b/playbooks/all.yml index fcba354f..3ce116d7 100644 --- a/playbooks/all.yml +++ b/playbooks/all.yml @@ -1,4 +1,5 @@ - import_playbook: 'acme_sh.yml' +- import_playbook: 'aide.yml' - import_playbook: 'alternatives.yml' - import_playbook: 'ansible_init.yml' - import_playbook: 'apache_httpd.yml' diff --git a/roles/aide/README.md b/roles/aide/README.md new file mode 100644 index 00000000..a01e2921 --- /dev/null +++ b/roles/aide/README.md @@ -0,0 +1,116 @@ +# Ansible Role linuxfabrik.lfops.aide + +This role ensures that AIDE is installed, configured, and scheduled for regular filesystem integrity checks. + +* The initial AIDE database is created only if `/var/lib/aide/aide.db.gz` does not already exist. +* Many default paths are pre-configured in the AIDE config for exclusion and inclusion rules. +* Exclusion always takes precedence over inclusion for any given path. + + +## Tags + +| Tag | What it does | Reload / Restart | +| --- | ------------ | ---------------- | +| `aide` | Runs all tasks of the role | - | +| `aide:configure` | Deploys `/etc/aide.conf` | - | +| `aide:install` | Installs the AIDE package and initializes the AIDE database if it does not exist yet | - | +| `aide:state` | Deploys the `aide-check.service` and `aide-check.timer` systemd units and sets the desired state | Reloads systemd daemon if unit files changed | +| `aide:update_db` | Rebuilds the AIDE database; only runs if called explicitly | - | + + +## Optional Role Variables + +`aide__check_time_on_calendar` + +* The time at which the AIDE check runs. See [systemd.time(7)](https://www.freedesktop.org/software/systemd/man/systemd.time.html) for the format. +* Type: String. +* Default: `'*-*-* 05:00:00'` + +`aide__exclude_recursive__host_var` / `aide__exclude_recursive__group_var` + +* Paths to exclude recursively from AIDE monitoring (prepended with `!` in the config). +* Type: List of dictionaries. +* Default: `[]` +* Subkeys: + + * `path`: + + * Mandatory. Filesystem path to exclude recursively. + * Type: String. + + * `state`: + + * Optional. `present` or `absent`. + * Type: String. + * Default: `'present'` + +`aide__exclude_rules__host_var` / `aide__exclude_rules__group_var` + +* Paths to exclude from AIDE monitoring (prepended with `-` in the config). +* Type: List of dictionaries. +* Default: `[]` +* Subkeys: + + * `path`: + + * Mandatory. Filesystem path to exclude. + * Type: String. + + * `state`: + + * Optional. `present` or `absent`. + * Type: String. + * Default: `'present'` + +`aide__include_rules__host_var` / `aide__include_rules__group_var` + +* Additional paths to monitor with a specific rule set. +* Type: List of dictionaries. +* Default: + + ```yaml + - path: '/opt/python-venv' + attributes: 'CONTENT' + ``` + +* Subkeys: + + * `path`: + + * Mandatory. Filesystem path to monitor. + * Type: String. + + * `attributes`: + + * Mandatory. AIDE rule set to apply (e.g. `CONTENT`, `CONTENT_EX`, `PERMS`, `NORMAL`). + * Type: String. + + * `state`: + + * Optional. `present` or `absent`. + * Type: String. + * Default: `'present'` + +`aide__timer_enabled` + +* Enables or disables the `aide-check.timer`, analogous to `systemctl enable/disable`. +* Type: Bool. +* Default: `true` + +`aide__timer_state` + +* Sets the state of the `aide-check.timer`, analogous to `systemctl start/stop`. +* Type: String. One of `started`, `stopped`. +* Default: `'started'` + +Example: +//TODO: use test cases here, after test has been done + +## License + +[The Unlicense](https://unlicense.org/) + + +## Author Information + +[Linuxfabrik GmbH, Zurich](https://www.linuxfabrik.ch) diff --git a/roles/aide/defaults/main.yml b/roles/aide/defaults/main.yml new file mode 100644 index 00000000..3b486daa --- /dev/null +++ b/roles/aide/defaults/main.yml @@ -0,0 +1,39 @@ +aide__check_time_on_calendar: '*-*-* 05:00:00' +aide__exclude_rules__dependent_var: [] +aide__exclude_rules__group_var: [] +aide__exclude_rules__host_var: [] +aide__exclude_rules__role_var: [] +aide__exclude_rules__combined_var: '{{ ( + aide__exclude_rules__role_var + + aide__exclude_rules__dependent_var + + aide__exclude_rules__group_var + + aide__exclude_rules__host_var + ) | linuxfabrik.lfops.combine_lod(unique_key="path") + }}' +aide__exclude_recursive__dependent_var: [] +aide__exclude_recursive__group_var: [] +aide__exclude_recursive__host_var: [] +aide__exclude_recursive__role_var: [] +aide__exclude_recursive__combined_var: '{{ ( + aide__exclude_recursive__role_var + + aide__exclude_recursive__dependent_var + + aide__exclude_recursive__group_var + + aide__exclude_recursive__host_var + ) | linuxfabrik.lfops.combine_lod(unique_key="path") + }}' +aide__include_rules__dependent_var: [] +aide__include_rules__group_var: [] +aide__include_rules__host_var: [] +aide__include_rules__role_var: + - path: '/opt/python-venv' + attributes: 'CONTENT' + state: 'present' +aide__include_rules__combined_var: '{{ ( + aide__include_rules__role_var + + aide__include_rules__dependent_var + + aide__include_rules__group_var + + aide__include_rules__host_var + ) | linuxfabrik.lfops.combine_lod(unique_key="path") + }}' +aide__timer_state: 'started' +aide__timer_enabled: true diff --git a/roles/aide/tasks/main.yml b/roles/aide/tasks/main.yml new file mode 100644 index 00000000..b8de18f2 --- /dev/null +++ b/roles/aide/tasks/main.yml @@ -0,0 +1,88 @@ +- block: + + - name: 'Install AIDE' + ansible.builtin.package: + name: + - 'aide' + state: 'present' + + - name: 'Initialize AIDE database' + ansible.builtin.command: 'aide --init --before "database_out=file:/var/lib/aide/aide.db.gz"' + args: + creates: '/var/lib/aide/aide.db.gz' + + tags: + - 'aide' + + +- block: + + - name: 'Deploy /etc/aide.conf' + ansible.builtin.template: + backup: true + src: 'etc/aide.conf.j2' + dest: '/etc/aide.conf' + owner: 'root' + group: 'root' + mode: 0o644 + + tags: + - 'aide' + - 'aide:configure' + + +- block: + + - name: 'Deploy /etc/systemd/system/aide-check.service' + ansible.builtin.template: + src: 'etc/systemd/system/aide-check.service.j2' + dest: '/etc/systemd/system/aide-check.service' + owner: 'root' + group: 'root' + mode: 0o644 + register: '__aide__service_unit_result' + + - name: 'Deploy /etc/systemd/system/aide-check.timer' + ansible.builtin.template: + src: 'etc/systemd/system/aide-check.timer.j2' + dest: '/etc/systemd/system/aide-check.timer' + owner: 'root' + group: 'root' + mode: 0o644 + register: '__aide__timer_unit_result' + + - name: 'Reload systemd' + ansible.builtin.systemd: + daemon_reload: true + when: + - '__aide__service_unit_result is changed or __aide__timer_unit_result is changed' + + tags: + - 'aide' + + +- block: + + - name: 'systemctl {{ aide__timer_enabled | bool | ternary("enable", "disable") }} aide-check.timer' + ansible.builtin.service: + name: 'aide-check.timer' + enabled: '{{ aide__timer_enabled | bool }}' + + - name: 'systemctl {{ aide__timer_state }} aide-check.timer' + ansible.builtin.service: + name: 'aide-check.timer' + state: '{{ aide__timer_state }}' + + tags: + - 'aide' + - 'aide:state' + + +- block: + + - name: 'Update AIDE database' + ansible.builtin.command: "aide --init --before 'database_out=file:/var/lib/aide/aide.db.gz'" + + tags: + - 'never' + - 'aide:update_db' diff --git a/roles/aide/templates/etc/aide.conf.j2 b/roles/aide/templates/etc/aide.conf.j2 new file mode 100644 index 00000000..0c54cafb --- /dev/null +++ b/roles/aide/templates/etc/aide.conf.j2 @@ -0,0 +1,321 @@ +# {{ ansible_managed }} +# 2026030401 +# Configuration file for AIDE. + +@@define DBDIR /var/lib/aide +@@define LOGDIR /var/log/aide + +# The location of the database to be read. +database=file:@@{DBDIR}/aide.db.gz + +# The location of the database to be written. +#database_out=sql:host:port:database:login_name:passwd:table +#database_out=file:aide.db.new +database_out=file:@@{DBDIR}/aide.db.new.gz + +# Whether to gzip the output to database +gzip_dbout=yes + +# Default. +verbose=5 + +report_url=file:@@{LOGDIR}/aide.log +report_url=stdout +#report_url=stderr +#NOT IMPLEMENTED report_url=mailto:root@foo.com +#NOT IMPLEMENTED report_url=syslog:LOG_AUTH + +# These are the default rules. +# +#p: permissions +#i: inode: +#n: number of links +#u: user +#g: group +#s: size +#b: block count +#m: mtime +#a: atime +#c: ctime +#S: check for growing size +#acl: Access Control Lists +#selinux SELinux security context +#xattrs: Extended file attributes +#md5: md5 checksum +#sha1: sha1 checksum +#sha256: sha256 checksum +#sha512: sha512 checksum +#rmd160: rmd160 checksum +#tiger: tiger checksum + +#haval: haval checksum (MHASH only) +#gost: gost checksum (MHASH only) +#crc32: crc32 checksum (MHASH only) +#whirlpool: whirlpool checksum (MHASH only) + +#R: p+i+n+u+g+s+m+c+acl+selinux+xattrs+md5 +#L: p+i+n+u+g+acl+selinux+xattrs +#E: Empty group +#>: Growing logfile p+u+g+i+n+S+acl+selinux+xattrs + +# You can create custom rules like this. +# With MHASH... +# ALLXTRAHASHES = sha1+rmd160+sha256+sha512+whirlpool+tiger+haval+gost+crc32 +ALLXTRAHASHES = sha1+rmd160+sha256+sha512+tiger +# Everything but access time (Ie. all changes) +EVERYTHING = R+ALLXTRAHASHES + +# Sane +# NORMAL = R+sha512 +NORMAL = p+i+n+u+g+s+m+c+acl+selinux+xattrs+sha512 + +# For directories, don't bother doing hashes +DIR = p+i+n+u+g+acl+selinux+xattrs + +# Access control only +PERMS = p+u+g+acl+selinux+xattrs + +# Logfile are special, in that they often change +LOG = p+u+g+n+S+acl+selinux+xattrs + +# Content + file type. +CONTENT = sha512+ftype + +# Extended content + file type + access. +CONTENT_EX = sha512+ftype+p+u+g+n+acl+selinux+xattrs + +# Some files get updated automatically, so the inode/ctime/mtime change +# but we want to know when the data inside them changes +DATAONLY = p+n+u+g+s+acl+selinux+xattrs+sha512 + +# Next decide what directories/files you want in the database. + +/boot CONTENT_EX +/opt CONTENT + +# Admins dot files constantly change, just check perms +/root/\..* PERMS +!/root/.xauth* +# Otherwise get all of /root. +/root CONTENT_EX + +# These are too volatile +!/usr/src +!/usr/tmp + +# Otherwise get all of /usr. +/usr CONTENT_EX + +# trusted databases +/etc/hosts$ CONTENT_EX +/etc/host.conf$ CONTENT_EX +/etc/hostname$ CONTENT_EX +/etc/issue$ CONTENT_EX +/etc/issue.net$ CONTENT_EX +/etc/protocols$ CONTENT_EX +/etc/services$ CONTENT_EX +/etc/localtime$ CONTENT_EX +/etc/alternatives CONTENT_EX +/etc/sysconfig CONTENT_EX +/etc/mime.types$ CONTENT_EX +/etc/terminfo CONTENT_EX +/etc/exports$ CONTENT_EX +/etc/fstab$ CONTENT_EX +/etc/passwd$ CONTENT_EX +/etc/group$ CONTENT_EX +/etc/gshadow$ CONTENT_EX +/etc/shadow$ CONTENT_EX +/etc/subgid$ CONTENT_EX +/etc/subuid$ CONTENT_EX +/etc/security/opasswd$ CONTENT_EX +/etc/skel CONTENT_EX +/etc/sssd CONTENT_EX +/etc/machine-id$ CONTENT_EX +/etc/swid CONTENT_EX +/etc/system-release-cpe$ CONTENT_EX +/etc/shells$ CONTENT_EX +/etc/tmux.conf$ CONTENT_EX +/etc/xattr.conf$ CONTENT_EX + +# networking +/etc/firewalld CONTENT_EX +!/etc/NetworkManager/system-connections +/etc/NetworkManager CONTENT_EX +/etc/networks$ CONTENT_EX +/etc/dhcp CONTENT_EX +/etc/wpa_supplicant CONTENT_EX +/etc/resolv.conf$ DATAONLY +/etc/nscd.conf$ CONTENT_EX + +# logins and accounts +/etc/login.defs$ CONTENT_EX +/etc/libuser.conf$ CONTENT_EX +/var/log/faillog$ PERMS +/var/log/lastlog$ PERMS +/var/run/faillock PERMS +/etc/pam.d CONTENT_EX +/etc/security CONTENT_EX +/etc/securetty$ CONTENT_EX +/etc/polkit-1 CONTENT_EX +/etc/sudo.conf$ CONTENT_EX +/etc/sudoers$ CONTENT_EX +/etc/sudoers.d CONTENT_EX + +# Shell/X startup files +/etc/profile$ CONTENT_EX +/etc/profile.d CONTENT_EX +/etc/bashrc$ CONTENT_EX +/etc/bash_completion.d CONTENT_EX +/etc/zprofile$ CONTENT_EX +/etc/zshrc$ CONTENT_EX +/etc/zlogin$ CONTENT_EX +/etc/zlogout$ CONTENT_EX +/etc/X11 CONTENT_EX + +# Pkg manager +/etc/dnf CONTENT_EX +/etc/yum.conf$ CONTENT_EX +/etc/yum CONTENT_EX +/etc/yum.repos.d CONTENT_EX + +# This gets new/removes-old filenames daily +!/var/log/sa +# As we are checking it, we've truncated yesterdays size to zero. +!/var/log/aide.log + +# auditing +# AIDE produces an audit record, so this becomes perpetual motion. +/var/log/audit PERMS +/etc/audit CONTENT_EX +/etc/libaudit.conf$ CONTENT_EX +/etc/aide.conf$ CONTENT_EX + +# System logs +/etc/rsyslog.conf$ CONTENT_EX +/etc/rsyslog.d CONTENT_EX +/etc/logrotate.conf$ CONTENT_EX +/etc/logrotate.d CONTENT_EX +/etc/systemd/journald.conf$ CONTENT_EX +/var/log LOG+ANF+ARF +/var/run/utmp LOG + +# secrets +/etc/pkcs11 CONTENT_EX +/etc/pki CONTENT_EX +/etc/crypto-policies CONTENT_EX +/etc/certmonger CONTENT_EX +/var/lib/systemd/random-seed$ PERMS + +# init system +/etc/systemd CONTENT_EX +/etc/rc.d CONTENT_EX +/etc/tmpfiles.d CONTENT_EX + +# boot config +/etc/default CONTENT_EX +/etc/grub.d CONTENT_EX +/etc/dracut.conf$ CONTENT_EX +/etc/dracut.conf.d CONTENT_EX + +# glibc linker +/etc/ld.so.cache$ CONTENT_EX +/etc/ld.so.conf$ CONTENT_EX +/etc/ld.so.conf.d CONTENT_EX +/etc/ld.so.preload$ CONTENT_EX + +# kernel config +/etc/sysctl.conf$ CONTENT_EX +/etc/sysctl.d CONTENT_EX +/etc/modprobe.d CONTENT_EX +/etc/modules-load.d CONTENT_EX +/etc/depmod.d CONTENT_EX +/etc/udev CONTENT_EX +/etc/crypttab$ CONTENT_EX + +#### Daemons #### + +# cron jobs +/etc/at.allow$ CONTENT +/etc/at.deny$ CONTENT +/etc/anacrontab$ CONTENT_EX +/etc/cron.allow$ CONTENT_EX +/etc/cron.deny$ CONTENT_EX +/etc/cron.d CONTENT_EX +/etc/cron.daily CONTENT_EX +/etc/cron.hourly CONTENT_EX +/etc/cron.monthly CONTENT_EX +/etc/cron.weekly CONTENT_EX +/etc/crontab$ CONTENT_EX +/var/spool/cron/root CONTENT + +# time keeping +/etc/chrony.conf$ CONTENT_EX +/etc/chrony.keys$ CONTENT_EX + +# mail +/etc/aliases$ CONTENT_EX +/etc/aliases.db$ CONTENT_EX +/etc/postfix CONTENT_EX + +# ssh +/etc/ssh/sshd_config$ CONTENT_EX +/etc/ssh/ssh_config$ CONTENT_EX + +# stunnel +/etc/stunnel CONTENT_EX + +# printing +/etc/cups CONTENT_EX +/etc/cupshelpers CONTENT_EX +/etc/avahi CONTENT_EX + +# web server +/etc/httpd CONTENT_EX + +# dns +/etc/named CONTENT_EX +/etc/named.conf$ CONTENT_EX +/etc/named.iscdlv.key$ CONTENT_EX +/etc/named.rfc1912.zones$ CONTENT_EX +/etc/named.root.key$ CONTENT_EX + +# xinetd +/etc/xinetd.conf$ CONTENT_EX +/etc/xinetd.d CONTENT_EX + +# IPsec +/etc/ipsec.conf$ CONTENT_EX +/etc/ipsec.secrets$ CONTENT_EX +/etc/ipsec.d CONTENT_EX + +# USB guard +/etc/usbguard CONTENT_EX + +# Ignore some files +!/boot/grub2/grubenv$ +!/etc/mtab$ +!/etc/.*~ + +# Now everything else +/etc PERMS + +# With AIDE's default verbosity level of 5, these would give lots of +# warnings upon tree traversal. It might change with future version. +# +#=/lost\+found DIR +#=/home DIR + +# Custom include rules +{% for rule in aide__include_rules__combined_var if rule['state'] | d('present') != 'absent' %} +{{ rule["path"] }} {{ rule["attributes"] }} +{% endfor %} + +# Custom exclude rules +{% for rule in aide__exclude_rules__combined_var if rule['state'] | d('present') != 'absent' %} +-{{ rule["path"] }} +{% endfor %} + +# Custom recursive exclude rules +{% for rule in aide__exclude_recursive__combined_var if rule['state'] | d('present') != 'absent' %} +!{{ rule["path"] }} +{% endfor %} diff --git a/roles/aide/templates/etc/systemd/system/aide-check.service.j2 b/roles/aide/templates/etc/systemd/system/aide-check.service.j2 new file mode 100644 index 00000000..5e4a36d6 --- /dev/null +++ b/roles/aide/templates/etc/systemd/system/aide-check.service.j2 @@ -0,0 +1,13 @@ +# {{ ansible_managed }} +# 2026030401 + + +[Unit] +Description=Aide check + +[Service] +Type=simple +ExecStart=/usr/sbin/aide --check + +[Install] +WantedBy=multi-user.target diff --git a/roles/aide/templates/etc/systemd/system/aide-check.timer.j2 b/roles/aide/templates/etc/systemd/system/aide-check.timer.j2 new file mode 100644 index 00000000..1a40b0ed --- /dev/null +++ b/roles/aide/templates/etc/systemd/system/aide-check.timer.j2 @@ -0,0 +1,13 @@ +# {{ ansible_managed }} +# 2026030401 + + +[Unit] +Description=Aide check timer + +[Timer] +OnCalendar={{ aide__check_time_on_calendar }} +Unit=aide-check.service + +[Install] +WantedBy=multi-user.target \ No newline at end of file