Consolidate AIDE Reports into a Daily Email


In this guide we’ll walk you through installing AIDE (Advanced Intrusion Detection Environment) on Rocky Linux 9.6, configuring it to forward alerts over an encrypted rsyslog channel to a centralized log server, and setting up Logwatch to produce daily summary reports that are delivered by email.

The solution is perfect for security‑focused environments that need automated file‑integrity monitoring and centralized logging without the noise of individual e‑mail notifications. Instead of receiving a separate message for every server’s AIDE run, Logwatch aggregates the results and sends a single, concise summary each day.

We’ll use Ansible to automate AIDE scans across clients and a server, ensuring scalability and consistency.

Why Consolidate /etc/aide.conf?

Storing the AIDE configuration in a single, centrally managed location makes it harder for an attacker to discover or tamper with the file. The playbook automatically tailors the configuration for each scan based on the services actually running on the host, so you only monitor what matters and you don't over exlcude.


Screenshot of the Logwatch AIDE email report

Compliance alignment Combining AIDE, TLS‑encrypted rsyslog, and Logwatch satisfies several widely adopted security standards:

Standard Relevant Requirement How the Solution Meets It
PCI‑DSS 10.2.1 – Centralized logging of all security‑related events All AIDE alerts are sent to a single syslog server over TLS.
ISO 27001 A.12.4.1 – Protect log information from tampering and unauthorized access Logs are encrypted in transit and stored on a dedicated NFS share.
NIST SP 800‑53 AU‑6 – Automated audit‑trail generation and review Logwatch creates daily, automated audit summaries.
HIPAA 164.312(b) – Integrity controls for ePHI Integrity‑checking via AIDE plus encrypted log transport ensures data integrity.

By encrypting log transport with TLS, storing logs on a dedicated NFS share, and routinely reviewing aggregated reports, you satisfy the core objectives of these frameworks—confidentiality, integrity, availability, and auditability.

The example configuration below is based on a my test environments deployment:

  • Syslog Server: syslog.osbornepro.com
  • Client Host: client‑server.osbornepro.com
  • Log Storage: /var/log/aide_logs/
  • Daily Email Report: Logwatch runs at 09:00 AM

If you manage many servers, you can mount a network share (e.g. /var/log/aide_logs) and create a symbolic link from /var/log/messages to that share. This prevents each host from filling its local disk while still preserving the standard log‑file path expected by existing tools. This is a good practice in general for log servers as the disk is going to be overwritten many times over.

NOTE: This guide does not cover the setup of a network share and instead uses the local filesystem on the syslog server.

The guide assumes you’re running Rocky Linux 9.6 with rsyslog 8.2412.0 and uses a playbook to automate you through the most common pitfalls—TLS handshake failures, SELinux permission issues, and related quirks. I selected Rocky Linux 9.6 because it’s the newest Rocky release slated to achieve full FIPS compliance imminently (if it isn’t already) as of 27th of October 2025.


Let’s dive in!

Prerequisites

  • Syslog Server – Rocky Linux 9.6 (syslog.osbornepro.com) acting as the centralized rsyslog receiver.
  • Syslog Client Servers – Rocky Linux 9.6 hosts (e.g., client‑server.osbornepro.com) running AIDE.
  • Ideally an Ansible control node – Semaphore, AWX, or AAP with sudo/root access to all targets.
  • Network – TCP 6514 open from every client to the server (TLS‑encrypted syslog).
  • SMTP server – smtp.osbornepro.com:25 (or SMTPS port 467 or STARTTLS port 587) for Logwatch email delivery.
  • Sudo Root privileges on all machines for package installation and configuration changes.
  • TLS certificates for rsyslog
    • Server‑side certificate (Use a self‑signed CA‑issued certificate or use your enterprises CA).
    • CA certificate to be distributed for trust on the clients.
  • Disk space planning - Sufficient space on the local filesystem or NFS share for (/var/log/aide_logs) and a log‑rotation policy (e.g., logrotate).
  • Firewall rules
    • Allow inbound TCP 6514 on the server.
    • Allow outbound SMTP (port 25|467|587) from the server.
    • Allow inbound SSH from the Ansible controller to all nodes.
  • Logwatch dependencies - Packages such as perl, Mail::Mailer, tzdata (installed automatically on most Rocky installs).
  • (Optional) Centralized authentication - LDAP/Active Directory for unified SSH key and sudo management.

Step 1: Create an Rsyslog Group

Create a local security group on the syslog server to control access to the per‑system syslog files. Add any non‑root users who need read‑only access to this group, granting them permission to view the logs without requiring sudo privileges.

[rosborne@syslog-osbornepro-com ~]$ sudo groupadd -r rsyslog

This group now allows its members to read logs in /var/log/aide_logs/ without needing root privileges.


Step 2: Configure Firewall to Accept Syslog over TLS

Open port 6514 for TLS syslog traffic and restrict access to your network (e.g., 192.168.1.0/24) or specific hosts.

[rosborne@syslog-osbornepro-com ~]$ sudo vim /etc/firewalld/services/syslog-tls.xml

Contents of syslog-tls.xml This file allows you to customize the syslog-tls service and apply its configuration by name. I prefer this over defining the actual ports as it creates an extra step to a threat actor performing enumeration. Maybe you will not use the standard 6514/tcp syslog over tls port in your setup.

<?xml version="1.0" encoding="utf-8"?>
<service>
    <short>syslog-tls</short>
    <description>Syslog over TLS</description>
    <port protocol="tcp" port="6514"/>
</service>

Examples of commands to use this firewall service are below:

# Allow all incoming syslog over tls traffic by doing:
[rosborne@syslog-osbornepro-com ~]$ sudo firewall-cmd --permanent --add-service=syslog-tls

# Or restrict syslog over tls so it is only allowed from a specific network
[rosborne@syslog-osbornepro-com ~]$ sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.1.0/24" service name="syslog-tls" accept'

# Or restrict syslog over tls so it is only allowed from a specific network
[rosborne@syslog-osbornepro-com ~]$ sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.1.123" service name="syslog-tls" accept'

# Reload the firewall to apply your changes
[rosborne@syslog-osbornepro-com ~]$ sudo firewall-cmd --reload

# Verify your newly created rule
[rosborne@syslog-osbornepro-com ~]$ sudo firewall-cmd --list-all

This ensures only trusted clients can connect to the server’s syslog port.


Step 3: Generate Certificate for Syslog over TLS

If your environment already has a Certificate Authority (CA), using that existing CA is generally the most secure option for completing Certificate Signing Requests (CSR). If not, you can create a local CA certificate and use it to sign the CSR generated in the steps below.

First, we will generate a self‑signed CA certificate on the syslog server, then we use it to issue a server certificate for the rsyslog service. Be sure to include both a hostname (e.g., syslog.osbornepro.com) and the IP address of your syslog server in the Subject Alternative Name (SAN) field. This eliminates hostname/IP mismatches during TLS verification. Note that for this to be considered a valid certificate you need to use a Fully Qualified Domain Name and not just a Hostname.

# Create directory on your syslog over TLS server to keep the certificates in
[rosborne@syslog-osbornepro-com ~]$ sudo mkdir -p /etc/rsyslog.d/certs
[rosborne@syslog-osbornepro-com ~]$ sudo cd /etc/rsyslog.d/certs

# Create your local Certificate Authority certificates
[rosborne@syslog-osbornepro-com ~]$ sudo openssl genrsa -out syslog-ca.key 4096
[rosborne@syslog-osbornepro-com ~]$ sudo openssl req -x509 -new -nodes -key syslog-ca.key -sha256 -days 3650 -out syslog-ca.crt -subj "/C=US/ST=Colorado/L=Colorado Springs/O=OsbornePro/OU=Certificates/CN=syslog.osbornepro.com" -addext "subjectAltName=DNS:syslog.osbornepro.com,DNS:192.168.1.51"

# Generate a private key and Request a CSR	
[rosborne@syslog-osbornepro-com ~]$ sudo openssl genrsa -out syslog-server.key 2048
[rosborne@syslog-osbornepro-com ~]$ sudo openssl req -new -key syslog-server.key -out syslog-server.csr -subj "/C=US/ST=Colorado/L=Colorado Springs/O=OsbornePro/OU=Certificates/CN=syslog.osbornepro.com" -addext "subjectAltName=DNS:syslog.osbornepro.com,DNS:192.168.1.51"

# Complete the CSR request	
[rosborne@syslog-osbornepro-com ~]$ echo "subjectAltName=DNS:syslog.osbornepro.com,DNS:192.168.8.244" > /tmp/openssl-ext.cnf
[rosborne@syslog-osbornepro-com ~]$ sudo openssl x509 -req -in /etc/rsyslog.d/certs/syslog-server.csr -CA /etc/rsyslog.d/certs/syslog-ca.crt -CAkey /etc/rsyslog.d/certs/syslog-ca.key -CAcreateserial -out /etc/rsyslog.d/certs/syslog-server.crt -days 3650 -sha256 -extfile /tmp/openssl-ext.cnf

# Clean up an set permissions
[rosborne@syslog-osbornepro-com ~]$ sudo rm -rf -- /tmp/openssl-ext.cnf
[rosborne@syslog-osbornepro-com ~]$ sudo chmod 600 syslog-ca.key syslog-server.key
[rosborne@syslog-osbornepro-com ~]$ sudo chmod 644 syslog-ca.crt syslog-server.crt

Copy your CA certificate to your Ansible control node. The playbook in this blog is designed to distribute your CA certificate file to syslog clients. If you did not generate a CA certificate, copy your Enterprise CA certificate to your Ansible control node.

[rosborne@syslog-osbornepro-com ~]$ sudo cp /etc/rsyslog.d/certs/syslog-ca.crt /tmp/syslog-ca.crt
[rosborne@syslog-osbornepro-com ~]$ sudo chmod 644 /tmp/syslog-ca.crt && sudo chown root:root /tmp/syslog-ca.crt
[rosborne@syslog-osbornepro-com ~]$ scp /tmp/syslog-ca.crt rosborne@ansible.osbornepro.com:/tmp

SSH into your ansible server and place the file in a location it can be distributed by ansible

[rosborne@ansible-osbornepro-com ~]$ cp -p /tmp/syslog-ca.crt ~ansible-user/ansible/templates/syslog-ca.crt

Step 4: Configure the Rsyslog Service - Server

Configure the rsyslog server to listen on port 6514 with TLS using the GnuTLS driver in anonymous mode (anon). In this mode the server authenticates itself, but client certificates are not required. Control access to the rsyslog service with firewall‑cmd rules to allow only trusted hosts. If you need mandatory client authentication, the configuration file below already contains a commented‑out section that you can uncomment and adapt as a starting point.

TLS Server Configuration:

[rosborne@syslog-osbornepro-com ~]$ sudo vim /etc/rsyslog.d/tls.conf

Contents of the tls.conf file on the syslog server - this is where you configure the rsyslog service to listen on port 6514 and specify the TLS certificates it will use for encryption (the caCert, certFile, and keyFile directives).

rsyslogd 8.2412.0-1.el9 TLS Configuration

# --------------------------------------------------------------
# TLS listener for AIDE logs (GTLS, anonymous client auth)
# --------------------------------------------------------------
module(load="imtcp")

# Certificate files - just CA for a listener (server side)
global(
  DefaultNetstreamDriver="gtls"
  DefaultNetstreamDriverCAFile="/etc/rsyslog.d/certs/syslog-ca.crt"
  DefaultNetstreamDriverCertFile="/etc/rsyslog.d/certs/syslog-server.crt"
  DefaultNetstreamDriverKeyFile="/etc/rsyslog.d/certs/syslog-server.key"
)

# Start TLS listener with explicit modes and strong TLS restrictions
input(
  type="imtcp"
  port="6514"
  StreamDriver.Name="gtls"
  StreamDriver.Mode="1" # 1 = TLS only
  StreamDriver.AuthMode="anon" # allow anonymous client (no cert)
  gnutlsPriorityString="NORMAL:-VERS-ALL:+VERS-TLS1.2:+VERS-TLS1.3:SECURE192:+ECDHE-ECDSA-AES256-GCM-SHA384:+ECDHE-RSA-AES256-GCM-SHA384:+CHACHA20-POLY1305"
)

rsyslogd 8.2506.0-2.el9 TLS Configuration

# ------------------------------------------------------------
# /etc/rsyslog.d/00-tls.conf - TLS INPUT (EL9 compatible)
# ------------------------------------------------------------
# /etc/rsyslog.d/10-tls.conf – Hardened TLS 1.2-only listener on port 6514

# Global GnuTLS cert/key settings (applies to all gtls inputs)
global(
    DefaultNetstreamDriverCAFile="/etc/rsyslog/keys/syslog.crt"
    DefaultNetstreamDriverCertFile="/etc/rsyslog/keys/syslog.crt"
    DefaultNetstreamDriverKeyFile="/etc/rsyslog/keys/syslog.key"
)

# Load imtcp module with TLS/gtls config
module(
    load="imtcp"
    StreamDriver.Name="gtls"
    StreamDriver.Mode="1"          # 1 = TLS-only (no plaintext)
    StreamDriver.AuthMode="anon"   # No client cert required
    gnutlsPriorityString="NORMAL:-VERS-ALL:+VERS-TLS1.2:-SHA1:-MD5:-CBC:-3DES:-ARCFOUR-128"
)

# TLS-only listener on 6514
input(
    type="imtcp"
    port="6514"
)

Rsyslog Server Configuration:

The below rsyslog.conf file configures the syslog server so that it writes its own AIDE output locally, since it cannot transmit syslog messages over a TLS connection to itself. By default the imuxsock module disables SysSock.Use, which we must enable in order to route AIDE results to a log file that LogWatch can process. Additionally, the current configuration suppresses the aide tag from being written to /var/log/messages or /var/log/boot.log. To capture the AIDE output, a local rule directs the syslog daemon to use the omfile module with the LocalAideLogTemplate template, storing the logs in the designated AIDE logfile.

[rosborne@syslog-osbornepro-com ~]$ sudo vim /etc/rsyslog.conf
# rsyslog configuration file

# For more information see /usr/share/doc/rsyslog-*/rsyslog_conf.html
# or latest version online at http://www.rsyslog.com/doc/rsyslog_conf.html 
# If you experience problems, see http://www.rsyslog.com/doc/troubleshoot.html

#### GLOBAL DIRECTIVES ####

# Where to place auxiliary files
global(workDirectory="/var/lib/rsyslog")

# Use default timestamp format
module(load="builtin:omfile" Template="RSYSLOG_TraditionalFileFormat")

#### MODULES ####

module(load="imuxsock"	  # provides support for local system logging (e.g. via logger command)
	       SysSock.Use="on") # Turn on message reception via local log socket
module(load="imjournal" 	    # provides access to the systemd journal
       UsePid="system" # PID number is retrieved as the ID of the process the journal entry originates from
       FileCreateMode="0644" # Set the access permissions for the state file
       StateFile="imjournal.state") # File to store the position in the journal
#module(load="imklog") # reads kernel messages (the same are read from journald)
#module(load="immark") # provides --MARK-- message capability

# Log AIDE messages (local and remote) to /var/log/aide_logs/%hostname%.log
template(name="LocalAideLogTemplate"
         type="string"
         string="/var/log/aide_logs/%hostname%.log")

if $syslogfacility-text == 'local7' and $syslogtag contains 'aide' then {
    action(
        type="omfile"
        dynaFile="LocalAideLogTemplate"
        fileCreateMode="0640"
        dirCreateMode="0750"
        dirOwner="root"
        dirGroup="rsyslog"
        fileOwner="root"
        fileGroup="rsyslog"
    )
    stop
}

# Include all config files in /etc/rsyslog.d/
include(file="/etc/rsyslog.d/*.conf" mode="optional")

# Provides UDP syslog reception
#module(load="imudp") # needs to be done just once
#input(type="imudp" port="514")

# Provides TCP syslog reception
#module(load="imtcp") # needs to be done just once
#input(type="imtcp" port="514")

#### RULES ####

# Log all kernel messages to the console.
# Logging much else clutters up the screen.
#kern.*                                                 /dev/console

# Log anything (except mail) of level info or higher.
# Don't log private authentication messages!
if $syslogtag != 'aide' then {
    *.info;mail.none;authpriv.none;cron.none            /var/log/messages
}
# Exclude aide tagged messages from local7 logging
if $syslogtag != 'aide' then {
    local7.*                                            /var/log/boot.log
}

# The authpriv file has restricted access.
authpriv.*                                              /var/log/secure

# Log all the mail messages in one place.
mail.*                                                  -/var/log/maillog

# Log cron stuff
cron.*                                                  /var/log/cron

# Everybody gets emergency messages
*.emerg                                                 :omusrmsg:*

# Save news errors of level crit and higher in a special file.
uucp,news.crit                                          /var/log/spooler

# Save boot messages also to boot.log
#local7.*                                                /var/log/boot.log

# ### sample forwarding rule ###
#action(type="omfwd"  
# # An on-disk queue is created for this action. If the remote host is
# # down, messages are spooled to disk and sent when it is up again.
#queue.filename="fwdRule1"       # unique name prefix for spool files
#queue.maxdiskspace="1g"         # 1gb space limit (use as much as possible)
#queue.saveonshutdown="on"       # save messages to disk on shutdown
#queue.type="LinkedList"         # run asynchronously
#action.resumeRetryCount="-1"    # infinite retries if host is down
# # Remote Logging (we use TCP for reliable delivery)
# # remote_host is: name/ip, e.g. 192.168.0.1, port optional e.g. 10514
#Target="remote_host" Port="XXX" Protocol="tcp")

This is the aide.conf file that needs to exist on the syslog server. This is what tells the syslog server where to create the file for remote hosts sending their AIDE results.

[rosborne@syslog-osbornepro-com ~]$ sudo vim /etc/rsyslog.d/aide.conf
# SYSLOG SERVER ONLY CONFIG FILE
template(name="AideLogTemplate" type="string" string="/var/log/aide_logs/%hostname%.log")
if $syslogfacility-text == 'local7' and $syslogtag contains 'aide' then {
    action(type="omfile" dynaFile="AideLogTemplate" fileCreateMode="0640" fileOwner="root" fileGroup="rsyslog")
    stop
}

Step 5: Configure AIDE Log Storage

Set up a directory for AIDE logs and configure rsyslog to store logs from remote clients in /var/log/aide_logs/<hostname>.log It is best practice to make this directory location a network share location as the disk will be overwritten many times. You want to ensure this disk has plenty of space for writing logs too ensuring it does not max out the available disk space.

[rosborne@syslog-osbornepro-com ~]$ sudo mkdir -p /var/log/aide_logs
[rosborne@syslog-osbornepro-com ~]$ sudo chown root:rsyslog /var/log/aide_logs
[rosborne@syslog-osbornepro-com ~]$ sudo chmod 750 /var/log/aide_logs

Step 6: Set SELinux Permissions

Ensure SELinux contexts allow rsyslog to access logs and certificates.

[rosborne@syslog-osbornepro-com ~]$ sudo semanage port -a -t syslogd_port_t -p tcp 6514
[rosborne@syslog-osbornepro-com ~]$ sudo setsebool -P nis_enabled 1
[rosborne@syslog-osbornepro-com ~]$ sudo restorecon -R /var/log/aide_logs
[rosborne@syslog-osbornepro-com ~]$ sudo semanage fcontext -a -t var_log_t "/var/log/aide_logs(/.*)?"
[rosborne@syslog-osbornepro-com ~]$ sudo semanage fcontext -a -t cert_t "/etc/rsyslog.d/certs(/.*)?"
[rosborne@syslog-osbornepro-com ~]$ sudo restorecon -R /etc/rsyslog.d/certs

Step 7: Install and Configure Postfix

Postfix is the MTA that handles outgoing mail for Logwatch. If you don’t have an on‑premises SMTP relay, you can point Postfix at an external service (e.g., SMTP2GO) and authenticate using basic credentials. This enables Logwatch to deliver its daily summary e‑mails reliably.

I would recommend setting up a deidicated SMTP relay server in your environment to point to if you do not already have one. You can point printers and other devices to it for sending emails without authentication and manage what IP addresses can communicate with the SMTP port as well as what IP addresses can send emails with the postfix service.

[rosborne@syslog-osbornepro-com ~]$ sudo dnf install -y postfix logwatch
[rosborne@syslog-osbornepro-com ~]$ sudo cp -p /etc/postfix/main.cf /etc/postfix/main.cf.orig
[rosborne@syslog-osbornepro-com ~]$ sudo vim /etc/postfix/main.cf

Set these values in Postfix main.cf to match your environment

relayhost = smtp.osbornepro.com:25
myhostname = syslog.osbornepro.com
mydomain = osbornepro.com
myorigin = $mydomain
inet_interfaces = localhost
mydestination =
mynetworks = 127.0.0.0/8
# IF YOU HAVE SOMETHING LIKE SMTP2GO WITH AUTH BELOW SHOULD HELP
# Relay all outgoing mail through SMTP2GO
#relayhost = [mail.smtp2go.com]:2525
#
# Enable SASL authentication for SMTP. 
#smtp_sasl_auth_enable = yes

# THE BELOW LINE IS A WORKING EXAMPLE - DO NOT HARD CODE A PASSWORD IN THIS FILE. USE A SASL AUTHENTICATION FILE
# REFERENCE: https://www.postfix.org/SASL_README.html
#smtp_sasl_password_maps = static:YOUR_SMTP_USERNAME:YOUR_SMTP_PASSWORD
#smtp_sasl_security_options = noanonymous
#
# Enable TLS (optional but recommended for security)
#smtp_tls_security_level = may
#smtp_use_tls = yes

# Increase header and message size limit for LogWatch AIDE emails when needed <------------ LIKELY YOU WILL NEED TO DO THIS BASED ON SEND EMAIL RESULTS --------------
header_size_limit = 4096000
message_size_limit = 204800000

# Optional: Limit concurrent connections to avoid rate limits
relay_destination_concurrency_limit = 20

Configure the postfix service to start after reboots and verify it can continue to send emails when scheduled.

[rosborne@syslog-osbornepro-com ~]$ sudo systemctl enable postfix
[rosborne@syslog-osbornepro-com ~]$ sudo systemctl restart postfix
[rosborne@syslog-osbornepro-com ~]$ echo "Test email functionality" | mail -s "Test Email" contact@osbornepro.com

Step 8: Configure LogWatch for AIDE

Create Logwatch scripts to parse AIDE logs and set up a daily cron job at 9 AM or whenever makes sense in your setup.

[rosborne@syslog-osbornepro-com ~]$ sudo mkdir -p /usr/share/logwatch/scripts/services
[rosborne@syslog-osbornepro-com ~]$ sudo vim /usr/share/logwatch/scripts/services/aide

Contents of aide in logwatch services. I based this off of an existing cisco perl script that comes with LogWatch.

#!/usr/bin/perl
use strict;
use warnings;

# Detect whether Logwatch is outputting in HTML mode
my $html_mode = ($ENV{'LOGWATCH_OUTPUT_FORMAT'} // '') =~ /html/i ? 1 : 0;

my %results;
my %global_summary = ( Added => 0, Removed => 0, Changed => 0 );
my %seen_files;     # prevent duplicates (per host + category + file)

my $current_host = 'unknown';
my $current_section = '';
my $header_printed = 0;

# ================================================================
# Parse incoming Logwatch lines
# ================================================================
while (my $line = <STDIN>) {
    chomp($line);

    # Detect the hostname from syslog prefix
    if ($line =~ /^[A-Z][a-z]{2}\s+\d+\s+\d+:\d+:\d+\s+(\S+)\s+aide:/) {
        $current_host = $1;
        $results{$current_host} ||= {
            'Added Files'   => [],
            'Removed Files' => [],
            'Changed Files' => [],
            'Summary'       => { Added => 0, Removed => 0, Changed => 0 },
        };
    }

    # Detect section transitions
    if ($line =~ /aide:\s+Added entries:/)   { $current_section = 'Added';   next; }
    if ($line =~ /aide:\s+Removed entries:/) { $current_section = 'Removed'; next; }
    if ($line =~ /aide:\s+Changed entries:/) { $current_section = 'Changed'; next; }

    # Skip irrelevant or separator lines
    next if $line =~ /aide:\s*-{3,}/;
    next if $line =~ /^\s*$/;

    # Parse summary counts
    if ($line =~ /aide:\s*(Added|Removed|Changed) entries:\D*(\d+)/) {
        my ($key, $count) = ($1, $2);
        $results{$current_host}{'Summary'}{$key} += $count;
        $global_summary{$key} += $count;
        next;
    }

    # Parse actual file entries
    if ($line =~ /aide:.*:\s*(\/.+)$/) {
        my $path = $1;
        $path =~ s/^\s+|\s+$//g;

        # Fix broken line-wrapped paths (e.g., "Resolution.r rd")
        $path =~ s/([A-Za-z0-9])\s+([A-Za-z0-9])/$1$2/g;

        my $category =
              $current_section eq 'Added'   ? 'Added Files'
            : $current_section eq 'Removed' ? 'Removed Files'
            : $current_section eq 'Changed' ? 'Changed Files'
            : undef;
        next unless $category;

        # Avoid duplicates within the same host/category
        my $key = join('|', $current_host, $category, $path);
        next if $seen_files{$key}++;

        push @{ $results{$current_host}{$category} }, $path;
        $results{$current_host}{'Summary'}{$current_section}++;
        $global_summary{$current_section}++;
        next;
    }
}

# ================================================================
# Output Formatting
# ================================================================
if (!$header_printed) {
    if ($html_mode) {
        print "<b>AIDE Logs - OsbornePro</b><br>\n";
    } else {
        print "AIDE Logs - OsbornePro\n";
    }
    $header_printed = 1;
}

foreach my $host (sort keys %results) {
    my $summary = $results{$host}{'Summary'};

    if ($html_mode) {
        print "<hr><b>AIDE Logs for $host:</b><br>\n";
        print "&nbsp;&nbsp;Added: $summary->{Added}<br>\n"   if $summary->{Added};
        print "&nbsp;&nbsp;Removed: $summary->{Removed}<br>\n" if $summary->{Removed};
        print "&nbsp;&nbsp;Changed: $summary->{Changed}<br><br>\n" if $summary->{Changed};
    } else {
        print "\nAIDE Logs for $host:\n";
        print "Summary:\n";
        print "    Added:   $summary->{Added}\n"   if $summary->{Added};
        print "    Removed: $summary->{Removed}\n" if $summary->{Removed};
        print "    Changed: $summary->{Changed}\n" if $summary->{Changed};
    }

    # ============================
    # List the Added / Removed / Changed files
    # ============================
    foreach my $cat ('Added Files', 'Removed Files', 'Changed Files') {
        next unless @{ $results{$host}{$cat} };

        if ($html_mode) {
            print "<b>$cat:</b><br>\n";
            foreach my $file (@{ $results{$host}{$cat} }) {
                print "&nbsp;&nbsp;$file<br>\n";
            }
            print "<br>\n";
        } else {
            print "$cat:\n";
            foreach my $file (@{ $results{$host}{$cat} }) {
                print "    $file\n";
            }
            print "\n";
        }
    }

    # Handle hosts with no detected changes
    unless ($summary->{Added} || $summary->{Removed} || $summary->{Changed}) {
        if ($html_mode) {
            print "&nbsp;&nbsp;<i>No changes detected on $host.</i><br>\n";
        } else {
            print "    No changes detected on $host.\n";
        }
    }
}

# ================================================================
# Global Summary (All Hosts)
# ================================================================
if ($html_mode) {
    print "<hr><b>Overall AIDE Summary (All Hosts):</b><br>\n";
    print "&nbsp;&nbsp;Added Files: $global_summary{'Added'}<br>\n";
    print "&nbsp;&nbsp;Removed Files: $global_summary{'Removed'}<br>\n";
    print "&nbsp;&nbsp;Changed Files: $global_summary{'Changed'}<br><br>\n";
} else {
    print "\n==================================================\n";
    print "Overall AIDE Summary (All Hosts):\n";
    print "==================================================\n";
    print "    Added Files:   $global_summary{'Added'}\n";
    print "    Removed Files: $global_summary{'Removed'}\n";
    print "    Changed Files: $global_summary{'Changed'}\n";
}

exit 0;

Make sure this new Perl script is executable. Then create a file that defines the log files the Perl script above is run against.

[rosborne@syslog-osbornepro-com ~]$ sudo chmod 755 /usr/share/logwatch/scripts/services/aide
[rosborne@syslog-osbornepro-com ~]$ sudo vim /usr/share/logwatch/default.conf/logfiles/aide.conf

Contents of logfiles/aide.conf. This is the location you are telling the syslog server to save its log files for every remote host.

This is what can fill up disk space quick so be sure you have enough room to save logs at this location!!!

LogFile = /var/log/aide_logs/*.log
Archive = /var/log/aide_logs/*.log.*
[rosborne@syslog-osbornepro-com ~]$ sudo vim /usr/share/logwatch/default.conf/services/aide.conf

Contents of services/aide.conf. This title will show up in the email report. Feel free to taylor it to your organization.

Title = "AIDE Logs - OsbornePro"
LogFile = aide
[rosborne@syslog-osbornepro-com ~]$ sudo vim /usr/share/logwatch/default.conf/logwatch.conf

Set the values you want as defaults in logwatch.conf. You can still override any of these options on the command line-or within the Cron job that runs Logwatch-when a different behavior is needed.

Output = mail
Format = html
MailTo = contact@osbornepro.com
MailFrom = mailsend@osbornepro.com
Service = aide
Detail = Med
[rosborne@syslog-osbornepro-com ~]$ sudo crontab -e

Crontab entry to add which will run every day at 9am server time. Feel free to set whatever schedule works best for your environment. If you want these reports weekly instead of daily, you would change the logrotate configuration to weekly and set your logwatch cronjob and scheduled aide playbooks execution to match.

0 9 * * * /usr/sbin/logwatch --service aide --mailto contact@osbornepro.com --output mail --detail Med

Step 9: Configure Logrotate for Syslog Files

The syslog servers Logrotate configuration will:

  • Rotate daily
  • Keep 7 rotated copies (the newest + 6 older ones)
  • Compress old logs with gzip (you can change to bzip2/xz if you prefer)
  • Reset permissions on the newly‑created log file so rsyslog can keep writing to it
  • Restart (or reload) rsyslog after each rotation so the daemon re‑opens the fresh file

[rosborne@syslog-osbornepro-com ~]$ sudo vim /etc/logrotate.d/aide_logs

Contents of config file for aide in Logrotate are below. Make any adjustments that make sense for you. LogWatch is schedueld to run once a day in my test setup so I have this log rotate daily.

# ---------------------------------------------------------------
# Files: /var/log/aide_logs/*.log   (one file per host)
# Rotation: daily, keep 7 days, gzip old copies
# ---------------------------------------------------------------
/var/log/aide_logs/*.log {
    daily
    missingok
    rotate 7
    maxage 7
    compress
    delaycompress
    copytruncate
    notifempty
    create 640 root rsyslog
    sharedscripts
    postrotate
        # Optional: reload or signal AIDE if needed (usually not)
        # Example: invoke-rc.d aide reload > /dev/null 2>&1 || true
    endscript
}

Step 10: The Client Rsyslog Configuration Jinja Template

This configures client systems to forward AIDE logs to the central syslog server over TLS using rsyslog’s omfwd module. Make sure the playbook points to the CA certificate (the issuer of the syslog server’s TLS certificate) that was saved during the Ansible deployment.

Rsyslog Client Jinja Template:

[rosborne@ansible-osbornepro-com ~]$ sudo vim ~ansible-user/ansible/templates/rsyslog_aide.conf.j2

Contents of rsyslog_aide.conf.j2 This is a jinja template meant to be distributed by ansible. If you are using vim to modify the file be sure to update the {{ rsyslog_server }} and {{ rsyslog_port }} variables with your syslog server and port 6514. Further below in this blog is the playbook that uses the jinja templates we are creating.

# /etc/rsyslog.d/aide.conf - Forward ONLY AIDE logs over TLS

# --------------------------------------------------------------
# TLS: Trust the CA that signed the syslog server cert
# --------------------------------------------------------------
global(
    DefaultNetstreamDriverCAFile="/etc/pki/tls/certs/syslog-ca.crt"
)

# --------------------------------------------------------------
# Forward ONLY messages from 'aide' tag on local7 facility
# --------------------------------------------------------------
if $syslogfacility-text == 'local7' and $syslogtag startswith 'aide' then {
    action(
        type="omfwd"
        target="{{ rsyslog_server }}"
        port="{{ rsyslog_port }}"
        protocol="tcp"
        StreamDriver="gtls"
        StreamDriverMode="1"                  # TLS only
        StreamDriverAuthMode="x509/name"      # Require valid cert + hostname
        StreamDriverPermittedPeers="{{ rsyslog_server }}"
    )
    stop
}

Step 11: Ansible Playbook for Client AIDE Scans

Deploy AIDE and rsyslog configurations to clients. You will want to take note of how long this takes to run and ensure your LogWatch cronjob runs after it completes.

Ansible Playbook to run AIDE check

[rosborne@ansible-osbornepro-com ~]$ sudo vim aide_check_syslogtls.yml

Contents of aide_check_syslogtls.yml playbook are below. This playbook utilizes the jinja templates in this blog article. All the template pieces are required for this playbook to work as is. You will need to modify any variables (vars) in this playbook that are not the same as your setup. That likely means the "aide_config_path", "rsyslog_ca_cert_src", "rsyslog_ca_cert_dest", "rsyslog_server", and "rsyslog_port" variables.

In the below playbook I use a "when" condition for applying the servers /etc/rsyslog.d/aide.conf file as this is different than the client file. If you are appending a domain to your hostnames with "ansible_host_suffix" this condition will not evaluate correctly. To fix this in your setup, you will need to change inventory_hostname != rsyslog_server to inventory_hostname != 'syslog' or when: not inventory_hostname.startswith('syslog')

---
- name: Run AIDE Checks Playbook
  hosts: linux_servers
  become: true
  become_user: root
  become_method: sudo
  gather_facts: true
  vars:
    aide_config_path: ~ansible-user/ansible/templates/aide.conf.j2
    aide_dest_path: /etc/aide.conf
	log_aide_results_dir: /var/log/aide_logs
	rsyslog_client_aide_config: ~ansible-user/ansible/templates/rsyslog_aide.conf.j2
    rsyslog_ca_cert_src: ~ansible-user/ansible/files/syslog-ca.crt
    rsyslog_ca_cert_dest: /etc/pki/tls/certs/syslog-ca.crt
    rsyslog_server: syslog.osbornepro.com
    rsyslog_port: 6514

  tasks:
    - name: End play if not RedHat
      meta: end_play
      when: ansible_facts['os_family'] != "RedHat"

    - name: Install AIDE
      package:
        name: aide
        state: present

    - name: Install rsyslog-gnutls
      package:
        name: rsyslog-gnutls
        state: present

    - name: Enable rsyslog
      service:
        name: rsyslog
        enabled: true

    - name: Verify CA cert exists on control node
      stat:
        path: "{{ rsyslog_ca_cert_src }}"
      register: ca_cert
      failed_when: not ca_cert.stat.exists
      delegate_to: localhost
      run_once: true

    - name: Ensure group rsyslog exists
      ansible.builtin.group:
        name: rsyslog
        state: present
        system: yes
      when: inventory_hostname.lower().startswith('syslog')

    - name: Create /var/log/aide_logs on syslog server
      file:
        path: "{{ log_aide_results_dir }}"
        state: directory
        owner: root
        group: rsyslog
        mode: '0750'
      when: inventory_hostname.lower().startswith('syslog')
      notify: Restore SELinux context for aide_logs

    - name: Deploy CA certificate
      copy:
        src: "{{ rsyslog_ca_cert_src }}"
        dest: "{{ rsyslog_ca_cert_dest }}"
        owner: root
        group: root
        mode: '0644'
      notify: Restart rsyslog

    - name: Set SELinux context for CA cert
      sefcontext:
        target: '{{ rsyslog_ca_cert_dest }}'
        setype: cert_t
        state: present
      when: ansible_selinux.status is defined and ansible_selinux.status == "enabled"
      notify: Restore SELinux context

    - name: Check if /etc/rsyslog.conf.prev exists
      ansible.builtin.stat:
        path: /etc/rsyslog.conf.prev
      register: rsyslog_prev

    - name: Create .prev backup only if missing
      ansible.builtin.copy:
        src: /etc/rsyslog.conf
        dest: /etc/rsyslog.conf.prev
        remote_src: yes
        owner: root
        group: root
        mode: '0644'
      when:
        - rsyslog_prev.stat is defined
        - not rsyslog_prev.stat.exists
      changed_when: true

    - name: Deploy final rsyslog.conf (overwrite with known-good config)
      copy:
        content: |
          # rsyslog configuration file

          # For more information see /usr/share/doc/rsyslog-*/rsyslog_conf.html
          # or latest version online at http://www.rsyslog.com/doc/rsyslog_conf.html
          # If you experience problems, see http://www.rsyslog.com/doc/troubleshoot.html

          #### GLOBAL DIRECTIVES ####

          # Where to place auxiliary files
          global(workDirectory="/var/lib/rsyslog")

          #### MODULES ####

          # Use default timestamp format
          module(load="builtin:omfile" Template="RSYSLOG_TraditionalFileFormat")

          module(load="imuxsock"    # provides support for local system logging (e.g. via logger command)
                 SysSock.Use="on") # Turn off message reception via local log socket;
                                    # local messages are retrieved through imjournal now.
          module(load="imjournal"             # provides access to the systemd journal
                 UsePid="system" # PID nummber is retrieved as the ID of the process the journal entry originates from
                 FileCreateMode="0644" # Set the access permissions for the state file
                 StateFile="imjournal.state") # File to store the position in the journal

          # Include all config files in /etc/rsyslog.d/
          include(file="/etc/rsyslog.d/*.conf" mode="optional")

          #module(load="imklog") # reads kernel messages (the same are read from journald)
          #module(load="immark") # provides --MARK-- message capability

          # Provides UDP syslog reception
          # for parameters see http://www.rsyslog.com/doc/imudp.html
          #module(load="imudp") # needs to be done just once
          #input(type="imudp" port="514")

          # Provides TCP syslog reception
          # for parameters see http://www.rsyslog.com/doc/imtcp.html
          #module(load="imtcp") # needs to be done just once
          #input(type="imtcp" port="514")

          #### RULES ####

          # Log all kernel messages to the console.
          # Logging much else clutters up the screen.
          #kern.*                                                 /dev/console

          # Log anything (except mail) of level info or higher.
          # Don't log private authentication messages!
          #*.info;mail.none;authpriv.none;cron.none                /var/log/messages
          if $syslogtag != 'aide' then {
              *.info;mail.none;authpriv.none;cron.none            /var/log/messages
          }

          # The authpriv file has restricted access.
          authpriv.*                                              /var/log/secure

          # Log all the mail messages in one place.
          mail.*                                                  -/var/log/maillog

          # Log cron stuff
          cron.*                                                  /var/log/cron

          # Everybody gets emergency messages
          *.emerg                                                 :omusrmsg:*

          # Save news errors of level crit and higher in a special file.
          uucp,news.crit                                          /var/log/spooler

          # Save boot messages also to boot.log
          #local7.*                                                /var/log/boot.log
          # Exclude aide tagged messages from local7 logging
          if $syslogtag != 'aide' then {
              local7.*                                            /var/log/boot.log
          }

          # ### sample forwarding rule ###
          #action(type="omfwd"
          # # An on-disk queue is created for this action. If the remote host is
          # # down, messages are spooled to disk and sent when it is up again.
          #queue.filename="fwdRule1"       # unique name prefix for spool files
          #queue.maxdiskspace="1g"         # 1gb space limit (use as much as possible)
          #queue.saveonshutdown="on"       # save messages to disk on shutdown
          #queue.type="LinkedList"         # run asynchronously
          #action.resumeRetryCount="-1"    # infinite retries if host is down
          # # Remote Logging (we use TCP for reliable delivery)
          # # remote_host is: name/ip, e.g. 192.168.0.1, port optional e.g. 10514
          #Target="remote_host" Port="XXX" Protocol="tcp")
        dest: /etc/rsyslog.conf
        owner: root
        group: root
        mode: '0644'
        backup: yes
      when: not inventory_hostname.lower().startswith('syslog')
      notify: Restart rsyslog

    - name: Deploy rsyslog client config
      template:
        src: "{{ rsyslog_client_aide_config }}"
        dest: /etc/rsyslog.d/aide.conf
        owner: root
        group: root
        mode: '0644'
      when: not inventory_hostname.lower().startswith('syslog')
      notify: Restart rsyslog

    - name: Deploy rsyslog server config (receiving) - hardcoded
      copy:
        content: |
          template(name="AideLogTemplate" type="string" string="/var/log/aide_logs/%hostname%.log")
          if $syslogfacility-text == 'local7' and $syslogtag contains 'aide' then {
              action(
                  type="omfile"
                  dynaFile="AideLogTemplate"
                  fileCreateMode="0640"
                  fileOwner="root"
                  fileGroup="rsyslog"
              )
              stop
          }
        dest: /etc/rsyslog.d/aide.conf
        owner: root
        group: root
        mode: 0644
        force: yes
      when: inventory_hostname.lower().startswith('syslog')
      notify: Restart rsyslog

    - name: Gather package facts
      package_facts:
      when: ansible_facts['os_family'] == "RedHat"

    - name: Check if SplunkForwarder is installed
      stat:
        path: /opt/splunkforwarder/bin/splunk
      register: splunk_installed
      when: ansible_facts['os_family'] == "RedHat"

    - name: Check if CrowdStrike Falcon is installed
      stat:
        path: /opt/CrowdStrike/falcon-sensor
      register: crowdstrike_installed
      when: ansible_facts['os_family'] == "RedHat"

    - name: Check if NagiosXI is installed
      stat:
        path: /usr/local/nagios/bin/nagios
      register: nagiosxi_installed
      when: ansible_facts['os_family'] == "RedHat"

    - name: Check if NagiosNA is installed
      stat:
        path: /usr/local/nagiosna/index.php
      register: nagiosna_installed
      when: ansible_facts['os_family'] == "RedHat"

    - name: Check if Semaphore is installed
      stat:
        path: /usr/bin/semaphore
      register: semaphore_installed
      when: ansible_facts['os_family'] == "RedHat"

    - name: Detect AIDE binary
      ansible.builtin.stat:
        path: /usr/sbin/aide
      register: aide_bin
      when: ansible_facts['os_family'] == "RedHat"

    - name: Get AIDE version
      ansible.builtin.command: /usr/sbin/aide --version
      register: aide_version_raw
      changed_when: false
      failed_when: false
      when: ansible_facts['os_family'] == "RedHat" and aide_bin.stat.exists | default(false)

    - name: Set aide_version fact (0.16 or 0.18)
      ansible.builtin.set_fact:
        aide_version: >-
          {%- if aide_version_raw.stdout is defined and aide_version_raw.stdout | length > 0 -%}
            {%- set ver = aide_version_raw.stdout.split()[1] | default('0.16') | string -%}
            {%- if ver is version('0.17', '<') -%}0.16{%- else -%}0.18{%- endif -%}
          {%- else -%}
            0.16
          {%- endif -%}
      when: ansible_facts['os_family'] == "RedHat"

    - name: Deploy correct AIDE configuration
      ansible.builtin.template:
        src: "../../templates/aide-v{{ aide_version }}.conf.j2"
        dest: /etc/aide.conf
        owner: root
        group: root
        mode: '0600'
        backup: yes
      register: aide_config
      when: ansible_facts['os_family'] == "RedHat"

    - name: Rebuild AIDE database if configuration changed
      ansible.builtin.shell: |
        rm -f /var/lib/aide/aide.db*.gz
        /usr/sbin/aide --init
        mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz
      when:
        - ansible_facts['os_family'] == "RedHat"
        - aide_config.changed

    - name: Run daily AIDE integrity check
      ansible.builtin.shell: |
        set -euo pipefail
        LOG="/var/log/aide_check_$(date +%Y%m%d_%H%M%S).log"

        # First run ever? → create baseline
        if [ ! -f /var/lib/aide/aide.db.gz ]; then
          echo "First run – creating AIDE baseline" >> "$LOG"
          /usr/sbin/aide --init
          mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz
        fi

        # Normal daily check
        if /usr/sbin/aide --check > "$LOG" 2>&1; then
          echo "AIDE: No changes detected" >> "$LOG"
        else
          rc=$?
          echo "AIDE: Changes detected (rc=$rc)" >> "$LOG"
          if [ $rc -ge 1 ] && [ $rc -le 15 ]; then
            /usr/sbin/aide --update
            cp /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz
            echo "AIDE: Database updated" >> "$LOG"
          fi
        fi

        cat "$LOG" | logger -t aide -p local7.info
        find /var/log -name 'aide_check_*.log' -type f -printf '%T@ %p\n' | sort -n | head -n -10 | cut -d' ' -f2- | xargs -r rm -f
      args:
        executable: /bin/bash
      changed_when: true
      ignore_errors: true
      when: ansible_facts['os_family'] == "RedHat"

  handlers:
    - name: Restart rsyslog
      ansible.builtin.service:
        name: rsyslog
        state: restarted

    - name: Restore SELinux context
      ansible.builtin.command: restorecon -R "{{ rsyslog_ca_cert_dest }}" /etc/rsyslog.d
      when: ansible_selinux.status is defined and ansible_selinux.status == "enabled"

    - name: Restore SELinux context for aide_logs
      ansible.builtin.command: restorecon -R "{{ log_aide_results_dir }}"
      when: ansible_selinux.status is defined and ansible_selinux.status == "enabled"

Step 12: AIDE Configuration Jinja Template

The AIDE configuration below monitors all critical system paths while excluding volatile files. You can tailor the rule set to match the services running in your environment-adding or removing entries as needed. For example, if you use the Splunk Forwarder, the configuration checks whether /opt/splunkforwarder exists; when it does, the frequently changing files under that directory are automatically excluded from AIDE integrity checks. The playbook generates the aide.conf file for each run on each server and deletes it when done. This is designed to make it harder for threat actors to hide malicious actions they take from AIDE.

Jinja Templates v16 and v18 for Client AIDE Rules:

[rosborne@ansible-osbornepro-com ~]$ sudo vim ~ansible-user/ansible/templates/aide.conf.j2

Contents of aide-v0.16.conf.j2 are below which ansible will set as the rsyslog configuration on clients. If you need to, install an older version of aide to help make the configuration consistent as possible. Search available versions by doing sudo dnf list aide --showduplicates and install them by combining "aide-" with the version number followed by . architecture value returned in Available Packages:

[rosborne@memberserver-osbornepro-com ~]$ sudo dnf install -y aide-0.16-103.el9.x86_64

# Generated by Ansible for host: {{ inventory_hostname }}
# Date: {{ ansible_date_time.date }}

@@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.
ALLXTRAHASHES = sha1+rmd160+sha256+sha512+tiger
# Everything but access time (Ie. all changes)
EVERYTHING = R+ALLXTRAHASHES

# Sane
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
/root/\..* PERMS
!/root/.xauth*
/root   CONTENT_EX

# These are too volatile
!/usr/src
!/usr/tmp
/usr    CONTENT_EX

# trusted databases
/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
!/var/log/aide.log

# auditing
/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
!/etc/mtab$
!/etc/.*~

# Exclude Postfix directories
!/var/spool/postfix
!/var/log/maillog
!/var/log/mail
!/var/spool/postfix/pid
!/var/spool/postfix/private
!/var/spool/postfix/public

# -------------------------------------------------
# Volatile / mutable files – excluded from AIDE checks
# -------------------------------------------------
!/var/log/boot.log
!/var/log/cron
!/var/log/dnf.librepo.log
!/var/log/dnf.librepo.log.*
!/var/log/dnf.log
!/var/log/dnf.log.*
!/var/log/hawkey.log
!/var/log/journal/*/user-*.journal
!/var/log/messages
!/var/log/secure
!/var/log/wtmp
!/var/log/utmp
!/var/log/btmp

#Audit logs (rotate with numeric suffixes)
!/var/log/audit/audit.log
!/var/log/audit/audit.log.*

# AIDE result logs – per‑host files
!/var/log/aide_result_*.log

# SSSD logs
!/var/log/sssd/krb5_child.log
!/var/log/sssd/ldap_child.log
!/var/log/sssd/sssd.log
!/var/log/sssd/sssd_*.log

# Configuration files that change frequently
!/etc/aide.conf

# Systemd journal binary files (rotate automatically)
!/var/log/journal/*/system.journal

# === Conditional Exclusions Based on Installed Packages ===

{% if splunk_installed.stat.exists %}
# SplunkForwarder directories
!/opt/splunkforwarder/var/log
!/opt/splunkforwarder/var/spool
{% endif %}

{% if crowdstrike_installed.stat.exists %}
# CrowdStrike Falcon
!/opt/CrowdStrike
{% endif %}

{% if nagiosxi_installed.stat.exists %}
# NagiosXI
!/usr/local/nagiosxi/nom/checkpoints/nagioscore
!/usr/local/nagios/var/stats
!/usr/local/nagios/var/archives
{% endif %}

{% if nagiosna_installed.stat.exists %}
# NagiosNA
!/usr/local/nagiosna/var
{% endif %}

{% if semaphore_installed.stat.exists %}
# Semaphore (Ansible UI)
!/opt/semaphore
{% endif %}

{% if 'ncpa' in ansible_facts.packages %}
# NCPA Agent
!/usr/local/ncpa
!/usr/local/ncpa/var/log
!/usr/local/ncpa/var/tmp
{% endif %}

Contents of aide-v0.18.conf.j2 are below which ansible will set as the rsyslog configuration on clients using AIDE v0.18.

[rosborne@memberserver-osbornepro-com ~]$ sudo dnf install -y aide-0.18.6-8.el10_0.2.src.rpm

Contents of file:

# AIDE configuration for AIDE >= 0.18 (RHEL 8 / RHEL 9)
# Generated by Ansible for host: {{ inventory_hostname }}
# Date: {{ ansible_date_time.iso8601 }}

@@define DBDIR /var/lib/aide
@@define LOGDIR /var/log/aide

# -------------------------------------------------------------------------
# Database settings
# -------------------------------------------------------------------------
database_in=file:@@{DBDIR}/aide.db.gz
database_out=file:@@{DBDIR}/aide.db.new.gz
gzip_dbout=yes

# -------------------------------------------------------------------------
# Reporting
# -------------------------------------------------------------------------
log_level=warning
report_level=changed_attributes
verbose=5
report_url=file:@@{LOGDIR}/aide.log
report_url=stdout

# -------------------------------------------------------------------------
# Rule definitions – FIXED FOR AIDE 0.18+ (no 's' with selinux/xattrs)
# -------------------------------------------------------------------------

# FIPS-140-2 base rule – 's' (size) is FORBIDDEN when using selinux/xattrs
FIPSR = p+i+n+u+g+acl+selinux+xattrs+sha256

# Additional strong hashes we still want
ALLXTRAHASHES = sha1+sha512+rmd160+tiger+haval

# Full content + metadata monitoring
CONTENT_EX   = FIPSR+ALLXTRAHASHES

# Normal files (inherits CONTENT_EX → no 's')
NORMAL       = CONTENT_EX

# Directories – metadata only
DIR          = p+i+n+u+g+acl+selinux+xattrs

# Permissions/user/group only
PERMS        = p+u+g+acl+selinux+xattrs

# Growing log files (AIDE 0.18 syntax)
LOG          = >

# Files where only content matters (hashes + minimal metadata, no size)
DATAONLY     = sha512+sha256+p+n+u+g+acl+selinux+xattrs

# -------------------------------------------------------------------------
# Selection rules
# -------------------------------------------------------------------------

/boot                    CONTENT_EX
/opt                     CONTENT_EX
/root                    CONTENT_EX
/root/\..*               PERMS
!/root/.xauth.*

/usr                     CONTENT_EX
!/usr/src
!/usr/tmp

!/etc/mtab
!/etc/.*~

# Critical static files – full content checking
/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

# Authentication
/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 / X11
/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

# Package manager configs
/etc/dnf                         CONTENT_EX
/etc/yum.conf$                   CONTENT_EX
/etc/yum                         CONTENT_EX
/etc/yum.repos.d                 CONTENT_EX

# Logs & volatile
/var/log                         LOG
/var/run/utmp                    LOG
!/var/log/sa
!/var/log/aide.log

# Auditing
/var/log/audit                   PERMS
/etc/audit                       CONTENT_EX
/etc/libaudit.conf$              CONTENT_EX
/etc/aide.conf$                  CONTENT_EX

# Systemd / init
/etc/systemd                     CONTENT_EX
/etc/rc.d                        CONTENT_EX
/etc/tmpfiles.d                  CONTENT_EX
/etc/default                     CONTENT_EX

# Kernel / boot
/etc/grub.d                      CONTENT_EX
/etc/dracut.conf$                CONTENT_EX
/etc/dracut.conf.d               CONTENT_EX
/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
/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

# Cron
/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_EX

# Services
/etc/chrony.conf$                CONTENT_EX
/etc/chrony.keys$                CONTENT_EX
/etc/aliases$                    CONTENT_EX
/etc/aliases.db$                 CONTENT_EX
/etc/postfix                     CONTENT_EX
/etc/ssh/sshd_config$            CONTENT_EX
/etc/ssh/ssh_config$             CONTENT_EX
/etc/stunnel                     CONTENT_EX
/etc/cups                        CONTENT_EX
/etc/avahi                       CONTENT_EX
/etc/httpd                       CONTENT_EX
/etc/named                       CONTENT_EX
/etc/xinetd.conf$                CONTENT_EX
/etc/xinetd.d                    CONTENT_EX
/etc/ipsec.d                     CONTENT_EX
/etc/usbguard                    CONTENT_EX

# Crypto / secrets
/etc/pkcs11                      CONTENT_EX
/etc/pki                         CONTENT_EX
/etc/crypto-policies             CONTENT_EX
/etc/certmonger                  CONTENT_EX
/var/lib/systemd/random-seed$    PERMS

# Postfix volatile areas
!/var/spool/postfix
!/var/log/maillog
!/var/log/mail
!/var/spool/postfix/pid
!/var/spool/postfix/private
!/var/spool/postfix/public

# -------------------------------------------------------------------------
# Conditional exclusions based on installed software
# -------------------------------------------------------------------------

{% if splunk_installed.stat.exists %}
!/opt/splunkforwarder/var/log
!/opt/splunkforwarder/var/spool
{% endif %}

{% if crowdstrike_installed.stat.exists %}
!/opt/CrowdStrike
{% endif %}

{% if nagiosxi_installed.stat.exists %}
!/usr/local/nagiosxi/nom/checkpoints/nagioscore
!/usr/local/nagios/var/stats
!/usr/local/nagios/var/archives
{% endif %}

{% if nagiosna_installed.stat.exists %}
!/usr/local/nagiosna/var
{% endif %}

{% if semaphore_installed.stat.exists %}
!/opt/semaphore
{% endif %}

{% if 'ncpa' in ansible_facts.packages %}
!/usr/local/ncpa
!/usr/local/ncpa/var/log
!/usr/local/ncpa/var/tmp
{% endif %}

Step 12: Hostname Resolution

If needed you can ensure clients can resolve the server "syslog.osbornepro.com" correctly by statically adding the entry to your hosts file. Long as you have internal DNS servers this should not be required. This would be for cases where a server is not on the domain and not using DNS servers on the LAN.

[rosborne@client-server-osbornepro-com ~]$ sudo vim /etc/hosts

Example added contents to /etc/hosts

# Add the IP Address and hostname that match your syslog server
192.168.1.14 syslog.osbornepro.com

Testing the Setup

  1. Deploy the Playbook:
    [ansible-user@ansible-osbornepro-com ~]$ ansible-playbook aide_check_syslogtls.yml -v
  2. Verify Rsyslog Configuration:
    • Client client-server.osbornepro.com:
      [rosborne@client-server-osbornepro-com ~]$ sudo rsyslogd -N1
      
      [rosborne@client-server-osbornepro-com ~]$ sudo journalctl -u rsyslog -f
    • Server syslog.osbornepro.com:
      [rosborne@syslog-osbornepro-com ~]$ sudo rsyslogd -N1
      
      [rosborne@syslog-osbornepro-com ~]$ sudo journalctl -u rsyslog -f | grep -E "imtcp|aide|2083"
  3. Test TLS Handshake:
    [rosborne@client-server-osbornepro-com ~]$ echo "<134>Oct 25 14:37:08 client-server aide[123456]: Test AIDE message" | openssl s_client -connect syslog.osbornepro.com:6514 -CAfile /etc/pki/tls/certs/syslog-ca.crt -quiet -tls1_2
  4. Run AIDE Check:
    [rosborne@client-server-osbornepro-com ~]$ sudo /sbin/aide --check 2>&1 | logger -t aide -p local7.info
    
    [rosborne@syslog-osbornepro-com ~]$ sudo cat /var/log/aide_result_client-server.osbornepro.com.log
  5. Verify Logs on Server:
    [rosborne@syslog-osbornepro-com ~]$ sudo ls /var/log/aide_logs/
    
    [rosborne@syslog-osbornepro-com ~]$ sudo tail /var/log/aide_logs/client-server.osbornepro.com.log
  6. Test Logwatch:
    [rosborne@syslog-osbornepro-com ~]$ sudo /usr/sbin/logwatch --service aide --mailto contact@osbornepro.com --output mail --format html --detail Med

Troubleshooting Tips

  • TLS Errors: If logs don’t appear in /var/log/aide_logs/, check for "unexpected TLS packet" (2083) in server logs:
    sudo journalctl -u rsyslog -f | grep 2083
    Ensure the client’s CA cert matches the server’s /etc/pki/tls/certs/syslog-ca.crt.
  • SELinux Denials: Check with sudo ausearch -m avc -ts recent.
  • Hostname Mismatch: Verify /etc/hosts or DNS resolves syslog.osbornepro.com to the correct IP.
  • Logwatch Emails: Test Postfix with echo "Test" | mail -s "Test" contact@osbornepro.com.

Conclusion

This solution combines AIDE for continuous integrity checks, rsyslog (TLS‑encrypted) for secure log transport, and Logwatch for concise daily summaries.

  • Ansible automation guarantees that every client is configured identically, reducing drift and simplifying scaling.
  • SELinux policies enforce strict confinement on Rocky Linux 9.6, preserving the host’s security posture.

By deploying these components, you’ll achieve a centralized logging hub that delivers daily email reports, giving you timely visibility into potential security events and system anomalies.

If you encounter issues sudo rsyslogd -N1 or sudo journalctl -u rsyslog can be used to hep troubleshoot. Happy monitoring!

🛸