Chapter 33 concludes with a candid admission that AppArmor has one fundamental limitation: its profile only records access violations from a technical standpoint, without ever answering the very question most frequently asked by auditors or incident response teams, which is who logged in, what commands were executed, and which files were modified within a specific timeframe. This chapter concludes Part VIII through the fifth and final layer of the defense in depth map that we built in Section 30.1.2, namely auditing and least privilege, a layer that operates under a different assumption than the previous four layers: not to prevent or limit an Attacker, but to ensure that every activity on the server is genuinely recorded and every account holds only the exact permissions required for its role.
The scenario is also concrete. One morning, a Sysadmin discovers an unfamiliar line in /etc/sudoers.d/ that was never created by anyone on the team, or an Nginx configuration file from Chapter 12 that suddenly changed without any Developer claiming responsibility for it. Without adequate audit logs, the question of "who and when" is nearly impossible to answer, because standard application logs like the ones we will discuss in depth in Chapter 39 only record what happened from the application's own perspective, not which Linux user triggered it through another process. auditd closes this gap through kernel-level recording that binds every system call to the identity of the user that triggered it, while the principle of least privilege ensures that the number of accounts capable of triggering suspicious activity is as small as possible from the outset.
We will begin with an introduction to auditd and the reasons why this kernel log differs from standard application logs, followed by writing audit rules to monitor sensitive files such as /etc/passwd and /etc/sudoers, practicing how to read the results using ausearch and aureport, reviewing the principle of least privilege that we have actually practiced since Chapter 4 from an audit perspective, auditing accounts and SUID binaries that could potentially become vulnerabilities, until finally concluding Part VIII as a whole through a basic security checklist that summarizes all layers since Chapter 30.
34.1 Audit Logs with auditd: An Introduction
Before diving into the practice of writing rules, it is helpful to first align our understanding of auditd's position relative to the logs we already know, because the two are often confused even though they answer different questions.
34.1.1 Why Standard Application Logs Are Not Enough for Audit Needs
journalctl and application logs like /var/log/nginx/access.log record what happens from the perspective of each individual application: which HTTP requests came in, which service failed to start. Logs like these are very useful for troubleshooting, as we have practiced repeatedly since Chapter 12, but they are not designed to answer audit- or compliance-style questions: precisely which Linux user read the /etc/shadow file last night, or whose process modified the contents of /etc/sudoers.d/10-developer from Section 4.2.2.
auditd (Linux Audit Daemon) operates at a much deeper layer, directly within the kernel via the Linux Audit Framework subsystem. Every time a process calls a monitored system call, such as opening a specific file or executing execve, the kernel records that event along with the UID, PID, and precise timestamp, regardless of which application triggered it. Because recording occurs in the kernel rather than in application space, audit logs are much harder to tamper with than standard application logs, a characteristic that often makes auditd a mandatory requirement in compliance standards like PCI DSS or CIS Benchmarks for servers storing sensitive Client data.
One important point to be honest about from the start: auditd is purely responsible for recording, not for preventing or blocking anything. Its role complements, rather than replaces, the four layers we built from Chapter 30 through Chapter 33. The firewall from Chapter 31 filters traffic, Fail2ban from Chapter 32 blocks suspicious IPs, AppArmor from Chapter 33 restricts process actions, while auditd merely ensures that all of that activity, including activity that escapes the previous four layers, remains neatly logged for future investigations or audit reports.
34.1.2 Installation and Components of auditd
The auditd package is not installed by default in Ubuntu Server 26.04 LTS. This package includes three main components that we will frequently use: the auditd daemon itself which writes events to logs, auditctl for managing rules directly, and a pair of search tools, ausearch and aureport, for reading recorded output without manually parsing raw logs.
Practical Steps
- Install the
auditdpackage.sudo apt update sudo apt install auditd - Check the service status after installation completes.
sudo systemctl status auditd - Confirm that the audit module is indeed active at the kernel level.
sudo auditctl -s
Verification and Troubleshooting
- Installing the
auditdpackage automatically enables and starts its service, so the second step should immediately displayactive (running)without needing additionalenableorstartcommands. - Healthy output from
auditctl -sdisplays the lineenabled 1, indicating that the kernel is currently receiving and recording audit events. If this line displaysenabled 0, there is likely a kernel parameteraudit=0intentionally added to GRUB, a condition that rarely has a valid reason on a production server that must meet compliance requirements. - Unlike AppArmor in Section 33.1.1 which displays
active (exited)because its job finishes as soon as profiles are loaded,auditdmust indeed remainactive (running)indefinitely, as this daemon continuously receives and writes events from the kernel to disk for as long as the server is running.
34.1.3 Writing Audit Rules: Monitoring Sensitive Files
Without rules, auditd is installed but records nothing specific. Audit rules define which files, directories, or syscalls are monitored. File-based rules are called watches, written using the -w flag for the watched path, -p for the types of access logged (a combination of r read, w write, x execute, a attribute change), and -k to assign a search label to the rule for easy querying later via ausearch.
Rules added directly via auditctl only persist until the next reboot. For rules that need to survive permanently, such as audit requirements on a production server, we write them to files in the /etc/audit/rules.d/ directory, following the same split-directory pattern as sudoers.d/ in Section 4.2.2 and jail.local in Section 32.2.2, and then load them using augenrules.
Practical Steps
- Add a temporary rule to try monitoring changes to
/etc/passwd.sudo auditctl -w /etc/passwd -p wa -k identity_changes - Create a permanent rule file monitoring several sensitive targets at once: account files (
/etc/passwd,/etc/shadow,/etc/group),sudoersrules from Section 4.2.2, and SSH configuration from Chapter 3.
Populate the file with the following lines.sudo nano /etc/audit/rules.d/50-identity-access.rules-w /etc/passwd -p wa -k identity_changes -w /etc/shadow -p wa -k identity_changes -w /etc/group -p wa -k identity_changes -w /etc/sudoers -p wa -k privilege_changes -w /etc/sudoers.d/ -p wa -k privilege_changes -w /etc/ssh/sshd_config -p wa -k sshd_changes - Reload all rules from
/etc/audit/rules.d/so they become permanent and persist across reboots.sudo augenrules --load - Confirm that all rules, both temporary and permanent, are loaded in the kernel.
sudo auditctl -l
Verification and Troubleshooting
- The output of
auditctl -lshould display each rule line exactly as written in the.rulesfile, marked with its respective-klabel. - Test a rule directly by triggering a small change on a monitored file, such as adding a comment in
/etc/ssh/sshd_configviasudo nano /etc/ssh/sshd_configand saving it, then ensure this event appears when searching logs by the keysshd_changesin Section 34.1.4 next. - If
augenrules --loaddisplays syntax error messages, re-check every line in the.rulesfile: the-pflag only accepts combinations of the lettersr,w,x,awithout spaces, and each line may only contain a single rule. - An excessive number of watch rules, especially on highly active directories like
/var/www/, can degrade server performance because every access to those paths triggers kernel logging. Focus watches only on files and directories that are strictly crucial for audit compliance, not the entire filesystem.
34.1.4 Reading Audit Logs with ausearch and aureport
Raw auditd logs are stored in /var/log/audit/audit.log in a human-readable but fairly dense format, containing key-value pairs like uid=, exe=, and key= on every line. ausearch and aureport parse these logs into a format that is much easier to navigate, without requiring us to use manual grep commands on a continuously growing file.
Practical Steps
- Search for all events recorded under the
privilege_changeskey from the rules created in Section 34.1.3.sudo ausearch -k privilege_changes - Search for events based on a time frame, such as all activity within the last hour.
sudo ausearch -k identity_changes --start recent - Display an authentication summary in a concise tabular format compared to raw
ausearchoutput.sudo aureport -au - Display a summary of all files logged as accessed according to our watch rules.
sudo aureport -f
Verification and Troubleshooting
- Every event from
ausearchdisplays pairedtype=PATHandtype=SYSCALLlines; theSYSCALLline includes the actor'suidas well assuccess=yesorsuccess=no, while thePATHline includes the touched file path. To translate numeric UIDs into more readable usernames, append the-iflag toausearch.sudo ausearch -k privilege_changes -i - If neither
ausearchnoraureportreturns any results even though the rule is active, confirm first that the monitored file has actually been touched since the rule was loaded. Rules added after a file has been modified cannot record past events. - A honest note from the field: on high-traffic servers, the
audit.logfile can grow very rapidly. Log rotation and size limit configurations are located in/etc/audit/auditd.confvia themax_log_fileandmax_log_file_actionparameters, separate from thelogrotatemechanism we will discuss for standard application logs in Chapter 39, becauseauditdhandles its own log rotation internally. - A practical side effect Sysadmins should know: once
auditdis installed and active, this daemon typically takes over ownership of the kernel audit socket fromsystemd-journald. This means AppArmor denials from Chapter 33, which we previously inspected viajournalctl -k | grep -i apparmorin Section 33.3.2, might no longer appear there after this chapter, and should be queried viasudo ausearch -i | grep -i apparmorinstead. If on your server those events still appear in both places, it is not a sign of an error, but simply a difference in default behaviors across kernel and package versions.
34.2 The Principle of Least Privilege on Multi-User Servers
Clean audit logs only answer questions after an incident occurs. This section moves one step earlier: shrinking as much as possible the set of accounts capable of triggering suspicious events in the first place, a principle we have actually been practicing piecemeal since Chapter 4 without explicitly naming it.
34.2.1 Least Privilege as a Principle, Not Just a Tool
Least privilege means that every account, whether belonging to a human or a service, is granted only the absolute minimum access rights required to perform its tasks, nothing more. This principle is not a single tool installed once and forgotten, but a habit spanning multiple chapters: role-based access via groups in Section 4.3.1, granular per-user sudoers policies in Section 4.2.2, up to AppArmor profiles restricting process kernel capabilities in Chapter 33. This section unites those habits under a single audit question: of all the permissions granted throughout this series, which ones turned out to be broader than strictly necessary?
34.2.2 Auditing Previously Granted Access Rights
Servers that have been running for a long time, especially those managed by multiple Sysadmins over time, tend to accumulate access permissions that were never revoked. A Developer who switched teams but remains in the sudo group, or an old line in sudoers.d/ whose purpose no one remembers, are common findings when access right audits are conducted for the first time on production servers.
Practical Steps
- List all users belonging to the
sudogroup, the broadest source of administrative access on the server.getent group sudo - Review all custom files in
sudoers.d/created since Section 4.2.2, one by one.sudo ls -la /etc/sudoers.d/ sudo cat /etc/sudoers.d/* - Search for user accounts with UID 0 other than
root, a sign of root-equivalent access that often escapes notice.awk -F: '$3 == 0 {print $1}' /etc/passwd - Find accounts that have never logged in at all, a key indicator of accounts whose presence warrants review.
sudo lastlog | grep -i "never logged in"
Verification and Troubleshooting
- Step three should only list
root. If any other username appears, it is a serious sign that the account has full root-equivalent privileges without needingsudoat all, a condition that almost never has a valid justification and must be fixed immediately viausermod. - The output of step four naturally displays quite a few service accounts such as
www-dataorpostgres, because these accounts were never designed for interactive logins. Ignore those lines as long as their login shell is set tonologinas discussed in Section 34.2.3, and focus your attention on human accounts that appear in this list while also being listed in thesudogroup from step one, as that combination makes them prime candidates for review or access revocation. - For any account that is no longer active but remains listed in the
sudogroup, revoke its membership usingsudo deluser username sudorather than deleting the account outright, especially if the account still owns files or processes that need to be traced first via the audit logs from Section 34.1.4. - Schedule access privilege audits like this regularly, not just once before go-live. Team transitions, completed projects, and Developers changing roles mean that an access list valid today could easily be outdated a few months later.
34.2.3 Restricting Service Accounts and SUID Binaries
Least privilege does not apply only to human accounts. Service accounts such as www-data for Nginx from Chapter 12 or postgres for PostgreSQL from Chapter 18 should never be able to log in interactively to a shell, because these accounts exist solely to run service processes, not for anyone to log in directly. Beyond accounts, binaries with the SUID bit set are also worth auditing because such binaries execute with the permissions of the file owner, usually root, regardless of which user executes them, making them a favorite target for Attackers once initial access to the server is gained.
Practical Steps
- Check the configured login shell for the primary service accounts on your server.
grep -E "^(www-data|postgres|mysql):" /etc/passwd - If a service account is found with its shell still set to
/bin/bashor/bin/sh, replace it withnologinto prevent interactive logins.sudo usermod -s /usr/sbin/nologin www-data - Find all binaries with an active SUID bit across the system.
sudo find / -xdev -perm -4000 -type f 2>/dev/null
Verification and Troubleshooting
- Default service accounts in Ubuntu Server like
www-dataon a fresh installation typically use/usr/sbin/nologinby default from the start, so the second step is generally relevant only if someone previously changed it manually, for instance, for temporary debugging that was forgotten. - The
findoutput in step three usually lists a dozen standard binaries such as/usr/bin/sudo,/usr/bin/passwd, or/usr/bin/su, because these binaries legitimately require root privileges for their functionality. Vigilance should be directed toward unknown SUID binaries or those located outside standard system directories like/usr/bin/and/usr/sbin/, such as in/home/or/tmp/, as the combination of an SUID bit and an unusual location is a common pattern used by Attackers to plant backdoors after compromising a server. - Do not strip the SUID bit from standard system binaries without understanding the impact. Stripping SUID from
/usr/bin/sudo, for example, instantly breaks the entiresudomechanism from Chapter 4 for regular users.
34.3 Basic Pre-Go-Live Security Checklist
This concluding section pulls together all layers of defense in depth from Section 30.1.2 into a single checklist suitable for checking off item by item before a server is declared fully ready to handle production traffic.
34.3.1 Checklist Based on Defense in Depth Layers
The following table summarizes the minimum verification points for each layer built since Chapter 3, ordered by their defense layer. This checklist is not an exhaustive list of formal compliance standards like CIS Benchmarks, but rather a practical starting point tying back every chapter completed so far.
| Layer | Verification Point | Chapter Reference |
|---|---|---|
| Remote access | Root SSH login disabled, key-based authentication enforced, custom port set | Chapter 3 |
| Access management | Sudo group contains only valid accounts, password policies active via chage | Chapter 4, Section 34.2.2 |
| Attack surface reduction | Unnecessary services disabled, unattended-upgrades active | Chapter 30 |
| Firewall | Default deny incoming, only necessary ports open | Chapter 31 |
| Intrusion prevention | Fail2ban active with minimal SSH jail, banaction aligned with nftables | Chapter 32 |
| Mandatory access control | AppArmor profiles for critical services set to enforce mode | Chapter 33 |
| Audit | auditd active with minimal rules for identity and privilege files | Section 34.1 |
34.3.2 Self-Audit: Checklist Verification Script
Reviewing the seven items above manually every time a server is prepared for go-live is time-consuming and prone to omissions. The simple bash script below automates checks for most table points, serving as a stepping stone before we dive deeper into shell scripting for server automation in Chapter 35.
Practical Steps
- Create a new script file.
nano ~/golive_check.sh - Populate it with the following checks.
#!/bin/bash echo "=== SSH ===" grep -E "^PermitRootLogin" /etc/ssh/sshd_config.d/*.conf /etc/ssh/sshd_config 2>/dev/null echo "=== Firewall (UFW) ===" sudo ufw status | head -1 echo "=== Fail2ban ===" sudo systemctl is-active fail2ban echo "=== AppArmor ===" sudo aa-status --enabled && echo "AppArmor active" echo "=== auditd ===" sudo systemctl is-active auditd sudo auditctl -l | wc -l echo "=== unattended-upgrades ===" sudo systemctl is-active unattended-upgrades.service 2>/dev/null || echo "check timer via systemctl list-timers" echo "=== Non-root accounts with UID 0 ===" awk -F: '$3 == 0 {print $1}' /etc/passwd - Grant execution permissions and run it.
chmod +x ~/golive_check.sh ./golive_check.sh
Verification and Troubleshooting
- A healthy
PermitRootLoginline displays a value ofno, matching the configuration applied in Section 3.2.1. Theufw statusline should showStatus: active, and bothfail2banandauditdservices should displayactive. - If any output displays
inactiveorunknown, it does not mean the script failed, but rather serves as an honest sign that a defense layer from that chapter has not been configured on this server. Return to the relevant chapter before proceeding with the go-live process. - This script is intentionally simple and only checks status, rather than inspecting detailed rule contents. For formal, standardized compliance audit needs, consider tools like
lynisor official CIS (Center for Internet Security) benchmarks, which cover hundreds of inspection points well beyond the scope of this basic checklist.
With this, Part VIII is officially complete. The five defense in depth layers from Section 30.1.2 have been systematically constructed: attack surface reduction and routine patching in Chapter 30, network traffic filtering via firewalls in Chapter 31, automated detection and response through Fail2ban in Chapter 32, mandatory access control using AppArmor in Chapter 33, and auditing alongside least privilege in this chapter via auditd to log kernel-level activity traces, reviewing permissions accumulated since Chapter 4, and establishing a go-live checklist that binds all layers into a verifiable unit. A server that has passed all five layers is not invulnerable, as no system is completely unhackable, but it is vastly better prepared to withstand and detect Attacker attempts compared to a server fresh out of basic installation in Chapter 2. The upcoming Part IX shifts focus from security to daily Sysadmin operational efficiency, starting with Chapter 35 on shell scripting for server automation, including fulfilling our promise in Section 34.3.2 by writing cleaner scripts with proper error handling and logging, before moving on to Ansible and cloud-init as Infrastructure as Code in the following two chapters.

