Audit logs record who changed what on a system: edits to user accounts, sudo configuration, SSH settings, clock changes and privilege escalation. Compliance frameworks require keeping them for a year or more and proving they were not altered. In this tutorial you will configure the Linux audit daemon (auditd) on Ubuntu 24.04 with a focused rule set, make the rules immutable, size local rotation, and archive every rotated log to S3 compatible object storage with Object Lock and SHA-256 checksums through a systemd timer. You will finish by searching both live and archived logs.
Prerequisites
To follow this tutorial you need:
- A server running Ubuntu 24.04 LTS, for example a CubePath VPS, and a non-root user with
sudoprivileges. - An S3 compatible bucket (AWS S3, MinIO, Wasabi, Cloudflare R2 or similar) created with versioning and Object Lock enabled, plus an access key that can write to it. On most providers Object Lock can only be enabled when the bucket is created.
- A few GB of free disk space under
/var/log/audit.
How long to keep audit logs
Retention is set by your policy and the regulations you fall under. Common reference points:
| Framework | Typical requirement |
|---|---|
| PCI DSS | 12 months, with the last 3 months immediately available |
| HIPAA | 6 years for required documentation, commonly applied to audit trails |
| SOX | 7 years for audit-related records |
A practical layout is a few weeks on local disk for fast searches and the full retention period in object storage.
Step 1 - Installing auditd
Install the audit daemon and its utilities:
sudo apt update
sudo apt install auditd
The service starts automatically. Check its state:
sudo auditctl -s
enabled 1
failure 1
pid 812
rate_limit 0
backlog_limit 8192
lost 0
backlog 0
[...]
enabled 1 means auditing is active. lost 0 means no events were dropped.
Step 2 - Adding audit rules
Rules live in /etc/audit/rules.d/ and are compiled into /etc/audit/audit.rules by augenrules. Files are processed in name order. Ubuntu's audit.rules in that directory already clears old rules and sets the buffer size. augenrules always moves those control lines (-D, -b, -f) to the top and -e to the end, so your own rules can go in a separate numbered file:
sudo nano /etc/audit/rules.d/50-baseline.rules
## Identity and authentication files
-w /etc/passwd -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/gshadow -p wa -k identity
## Privilege configuration
-w /etc/sudoers -p wa -k sudoers
-w /etc/sudoers.d/ -p wa -k sudoers
## SSH server configuration
-w /etc/ssh/sshd_config -p wa -k sshd
-w /etc/ssh/sshd_config.d/ -p wa -k sshd
## Audit configuration itself
-w /etc/audit/ -p wa -k audit_config
## Clock changes
-a always,exit -F arch=b64 -S adjtimex,settimeofday,clock_settime -k time_change
-w /etc/localtime -p wa -k time_change
## Programs run with root privileges by a non-root user (sudo, setuid binaries)
-a always,exit -F arch=b64 -S execve -C uid!=euid -F euid=0 -k priv_esc
## Kernel module loading and unloading
-a always,exit -F arch=b64 -S init_module,finit_module,delete_module -k modules
Each -w line watches a file or directory for writes and attribute changes (-p wa). Each -a always,exit line audits a system call. The -k key is a label you will search by later. Keep the rule set focused: auditing every file deletion or every system call run by root produces gigabytes per day on a busy server and hides the events that matter.
Now add a separate final file that locks the configuration:
echo "-e 2" | sudo tee /etc/audit/rules.d/99-finalize.rules
-e 2 makes the rules immutable until the next reboot: not even root can add, remove or disable rules at runtime, and any attempt is itself logged. This is what auditors expect, but it also means every rule change from now on requires a reboot.
Load the rules and check them:
sudo augenrules --load
sudo auditctl -l
-w /etc/passwd -p wa -k identity
-w /etc/group -p wa -k identity
[...]
-a always,exit -F arch=b64 -S init_module,delete_module,finit_module -F key=modules
Verify the lock with sudo auditctl -s, which now reports enabled 2. Test a rule by touching a watched file and searching for its key:
sudo touch /etc/sudoers.d/README
sudo ausearch -k sudoers -ts recent -i | tail -n 3
The output contains a SYSCALL record with comm=touch and your user in auid=, the login user who ran the command even through sudo.
Step 3 - Sizing local rotation
auditd rotates its own logs; do not add a logrotate rule for /var/log/audit/audit.log, as the two conflict. The defaults on Ubuntu (8 MB files, 5 files kept) are too small to keep more than a few hours on a busy server.
Open the daemon configuration:
sudo nano /etc/audit/auditd.conf
Find and change these existing keys, leaving the rest of the file as it is:
max_log_file = 100
num_logs = 30
max_log_file_action = ROTATE
space_left = 2048
space_left_action = SYSLOG
admin_space_left = 1024
admin_space_left_action = SUSPEND
disk_full_action = SUSPEND
This keeps up to 30 files of 100 MB (about 3 GB). space_left and admin_space_left are in MB: when free space on the partition drops below 2 GB, a warning goes to syslog; below 1 GB, auditd stops writing. If your policy says the system must not run unaudited, use HALT instead of SUSPEND for the admin and disk-full actions, knowing that a full disk then stops the server.
Tell auditd to reread its configuration:
sudo systemctl kill -s SIGHUP auditd
sudo ausearch -m DAEMON_CONFIG -ts recent
A DAEMON_CONFIG record with res=success confirms the change. For more protection against a full /var, put /var/log/audit on its own partition or logical volume.
Step 4 - Configuring access to object storage
Local files can be deleted by anyone with root on the server. Copies in a bucket with Object Lock in compliance mode cannot be deleted or overwritten by anyone, including the account owner, until their retention period ends. Set the bucket's default retention to your policy (for example 7 years). With the AWS CLI on an admin workstation it looks like this:
aws s3api put-object-lock-configuration \
--bucket your_bucket \
--object-lock-configuration '{"ObjectLockEnabled":"Enabled","Rule":{"DefaultRetention":{"Mode":"COMPLIANCE","Years":7}}}'
Other providers expose the same setting in their console or through the same S3 API call. Give the server an access key that can only put and read objects in this bucket, not delete them or change bucket settings.
On the server, install rclone, which works with any S3 compatible provider:
sudo apt install rclone
Create a configuration file readable only by root:
sudo install -d -m 700 /etc/audit-archive
sudo nano /etc/audit-archive/rclone.conf
[archive]
type = s3
provider = Other
access_key_id = your_access_key
secret_access_key = your_secret_key
endpoint = https://your_s3_endpoint
region = your_region
no_check_bucket = true
For AWS, set provider = AWS and remove the endpoint line. no_check_bucket = true stops rclone from trying to create the bucket, which a restricted key cannot do. Protect the file and test access:
sudo chmod 600 /etc/audit-archive/rclone.conf
sudo rclone --config /etc/audit-archive/rclone.conf lsd archive:your_bucket
An empty result without errors means the credentials work.
Step 5 - Writing the archive script
The script closes the current log with a rotation, then uploads every rotated file that has not been archived yet. Because auditd renames files on each rotation (audit.log.1 becomes audit.log.2), the script identifies files by their SHA-256 hash, not by name, and keeps a local manifest of what it already sent. Each archive gets a companion .sha256 file so its integrity can be checked years later.
sudo nano /usr/local/sbin/audit-archive
#!/usr/bin/env bash
set -euo pipefail
BUCKET="your_bucket"
HOST="$(hostname -s)"
LOG_DIR="/var/log/audit"
STATE_DIR="/var/lib/audit-archive"
MANIFEST="$STATE_DIR/archived.sha256"
STAGING="$STATE_DIR/staging"
RCLONE=(rclone --config /etc/audit-archive/rclone.conf)
mkdir -p "$STAGING"
touch "$MANIFEST"
# Close the current log so everything up to now is archived
systemctl kill -s SIGUSR1 auditd.service
sleep 5
shopt -s nullglob
for file in "$LOG_DIR"/audit.log.[0-9]*; do
sum="$(sha256sum "$file" | cut -d' ' -f1)"
if grep -q "^$sum " "$MANIFEST"; then
continue
fi
stamp="$(date -u -r "$file" +%Y%m%dT%H%M%SZ)"
month="$(date -u -r "$file" +%Y/%m)"
name="audit-$HOST-$stamp.log.gz"
gzip -c "$file" > "$STAGING/$name"
(cd "$STAGING" && sha256sum "$name" > "$name.sha256")
"${RCLONE[@]}" copy "$STAGING" "archive:$BUCKET/$HOST/$month"
echo "$sum $name" >> "$MANIFEST"
rm -f "$STAGING/$name" "$STAGING/$name.sha256"
echo "archived $file as $HOST/$month/$name"
done
Replace your_bucket with your bucket name, then make the script executable:
sudo chmod 750 /usr/local/sbin/audit-archive
Notes on the design:
SIGUSR1is auditd's documented signal for "rotate now".- The file's modification time is the time of its last event and does not change when auditd renames it, so it makes a stable archive name.
- If an upload fails,
set -estops the script before the manifest is updated, and the next run retries.
Run it once by hand:
sudo /usr/local/sbin/audit-archive
archived /var/log/audit/audit.log.1 as web-01/2026/09/audit-web-01-20260925T103512Z.log.gz
Run it a second time: it only archives the file created by the new rotation, confirming that already-uploaded files are skipped. List the bucket:
sudo rclone --config /etc/audit-archive/rclone.conf ls archive:your_bucket
Step 6 - Scheduling the archive with a systemd timer
Create the service unit:
sudo nano /etc/systemd/system/audit-archive.service
[Unit]
Description=Archive rotated audit logs to object storage
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/audit-archive
And the timer, which runs it every night:
sudo nano /etc/systemd/system/audit-archive.timer
[Unit]
Description=Nightly audit log archive
[Timer]
OnCalendar=*-*-* 00:30:00
RandomizedDelaySec=15m
Persistent=true
[Install]
WantedBy=timers.target
Persistent=true runs a missed job after the server was powered off at the scheduled time. Enable the timer and check the next run:
sudo systemctl daemon-reload
sudo systemctl enable --now audit-archive.timer
systemctl list-timers audit-archive.timer
Each run's output ends up in the journal: journalctl -u audit-archive.service. Make sure num_logs in Step 3 covers well over a day of rotations, so files are never deleted locally before the nightly upload.
Step 7 - Searching live and archived logs
ausearch finds events by key, user, type or time, and -i translates numeric IDs into names:
# Changes to users and groups today
sudo ausearch -k identity -ts today -i
# Commands executed with root privileges by non-root users this week
sudo ausearch -k priv_esc -ts this-week -i
# Failed authentication attempts
sudo ausearch -m USER_AUTH -sv no -ts today -i
aureport summarizes:
sudo aureport --summary -ts this-month
sudo aureport -au --failed -ts this-week
sudo aureport -k --summary
For an investigation that goes back beyond local retention, download the archive and its checksum, verify it and search it with ausearch -if:
mkdir -p ~/investigation && cd ~/investigation
sudo rclone --config /etc/audit-archive/rclone.conf copy archive:your_bucket/web-01/2026/09/ . --include "audit-web-01-20260925T103512Z.log.gz*"
sha256sum -c audit-web-01-20260925T103512Z.log.gz.sha256
audit-web-01-20260925T103512Z.log.gz: OK
zcat audit-web-01-20260925T103512Z.log.gz > audit.log
sudo ausearch -if audit.log -k sudoers -i
Because both the archive and its checksum are under Object Lock, a mismatch can only come from corruption in transit, never from someone rewriting both files.
Troubleshooting
augenrules --load reports that rules are locked. The -e 2 flag is active. Edit the files in /etc/audit/rules.d/ and reboot to apply them.
auditctl -s shows lost increasing. Events arrive faster than the kernel buffer drains. Remove noisy rules or raise the -b value in /etc/audit/rules.d/audit.rules, then reboot.
The archive job fails with AccessDenied. The key lacks PutObject on the bucket, or the provider requires a checksum header for Object Lock uploads. Check the exact error with journalctl -u audit-archive.service and test with rclone copy and the -vv flag.
Old audit files disappear before they are archived. Rotation is faster than the archive schedule. Increase num_logs or max_log_file, or run the timer more often, for example OnCalendar=hourly.
Conclusion
Your Ubuntu 24.04 server now records security-relevant changes with a locked auditd rule set, keeps a few weeks of logs locally, and ships every rotated file to write-once object storage with checksums on a nightly timer. As next steps, forward events in real time to a central log host with the audisp-remote plugin so an attacker with root cannot suppress recent events, add rules for your own sensitive application directories, and document a periodic review of aureport output to satisfy the review requirements in PCI DSS and similar frameworks.
