Enhancing Linux security with AppArmor and Wazuh

The Linux operating system is widely deployed across various systems, from embedded devices to cloud infrastructure. Its popular use makes it a frequent target for threat actors, increasing the importance of enforced security mechanisms. Linux uses the Discretionary Access Control (DAC) permission model by default. In this model, the owner of a file or process defines its permissions, determining who can read, write, or execute it. The DAC model is flexible but has a key weakness: processes running with elevated privileges can access sensitive resources. If such processes are compromised, an attacker inherits the elevated access and can interact with sensitive resources.
Linux supports a stronger model called Mandatory Access Control (MAC) to improve security. MAC is a kernel-enforced security model that restricts the ability of users and processes to access or modify system resources. Unlike DAC, MAC enforces kernel-level policies that even privileged users and processes can not override. The Linux kernel implements MAC through pluggable modules known as Linux Security Modules (LSMs). LSM tools like AppArmor, seccomp, SELinux, and Yama provide different approaches to restrict what processes can do. They can limit file access, block risky system calls, or prevent privilege escalation.
In this blog post, we show how to restrict the activity of a sample application using AppArmor, one of the Linux Security Modules (LSMs). We also integrate Wazuh, a SIEM and XDR platform, to collect and analyze AppArmor events in real-time. This setup enables local policy enforcement with centralized visibility, enabling proactive threat detection, automated alerts, and compliance monitoring across the environment.
To demonstrate process restriction and real-time monitoring with Apparmor and Wazuh, we set up the following endpoints:
This section covers how to create a sample application in C, then create and apply a custom AppArmor profile to restrict its file and system access. It also covers configuring the Wazuh File Integrity Monitoring (FIM) capability to alert administrators of changes made to Apparmor profiles.
AppArmor profiles define the permissions and restrictions for individual programs, specifying which files, capabilities, and system resources a process can access.
We perform the following steps on the Ubuntu endpoint:
Apparmor profiles are saved in the /etc/apparmor.d
directory. To monitor these profiles for changes, follow these steps on the Ubuntu endpoint to configure the Wazuh FIM capability:
/var/ossec/etc/ossec.conf
under the syscheck
configuration block:<directories realtime="yes" report_changes="yes">/etc/apparmor.d</directories>
# systemctl restart wazuh-agent
Follow the steps below on the Wazuh dashboard to create custom FIM rules that generate alerts on the addition, modification, or deletion of AppArmor profiles:
<group name="syscheck"> <rule id="100601" level="3"> <if_sid>554</if_sid> <field name="file">/etc/apparmor.d/</field> <description>AppArmor profile added: $(file)</description> </rule> </group> <group name="syscheck"> <rule id="100602" level="5"> <if_sid>550</if_sid> <field name="file">/etc/apparmor.d/</field> <description>AppArmor profile modified: $(file)</description> <mitre> <id>T1562.001</id> </mitre> </rule> </group> <group name="syscheck"> <rule id="100603" level="7"> <if_sid>553</if_sid> <field name="file">/etc/apparmor.d/</field> <description>AppArmor profile deleted: $(file)</description> <mitre> <id>T1562.001</id> </mitre> </rule> </group>
Where rule ID:
100601
: Triggers an alert when a new file is added to /etc/apparmor.d/
directory.100602
: Triggers an alert when an existing file in /etc/apparmor.d/
directory is modified.100603
: Triggers an alert when a file in /etc/apparmor.d/
directory is deleted.apparmor_fim_rules.xml
and click Save as shown below:Install the GCC
compiler and compile the application that will be used for the demonstration:
# apt install gcc
$HOME/test-app.c
and insert the code snippet below:#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/syscall.h> #include <sys/ptrace.h> #include <netinet/in.h> #include <string.h> #include <arpa/inet.h> //write_to_file void write_to_file() { FILE *fp = fopen("/tmp/demo_output.txt", "w"); if (fp) { fprintf(fp, "This is a sample output.\n"); fclose(fp); printf("[+] write_to_file - Wrote to /tmp/demo_output.txt\n"); } else { perror("[-] write_to_file - Failed to write to file"); } } //read_sensitive_file void read_sensitive_file() { FILE *fp = fopen("/etc/shadow", "r"); if (fp) { printf("[+] read_sensitive_file - Able to read /etc/shadow \n"); fclose(fp); } else { perror("[-] read_sensitive_file - Failed to read /etc/shadow"); } } //make_syscall void make_syscall() { pid_t pid = syscall(SYS_getpid); printf("[+] make_syscall - Syscall SYS_getpid returned: %d\n", pid); } //use_network void use_network() { int sock = socket(AF_INET, SOCK_STREAM, 0); if (sock < 0) { perror("[-] use_network - Socket creation failed"); return; } struct sockaddr_in target; target.sin_family = AF_INET; target.sin_port = htons(80); // SMTP inet_pton(AF_INET, "1.1.1.1", &target.sin_addr); if (connect(sock, (struct sockaddr *)&target, sizeof(target)) == 0) { printf("[+] use_network - Network connection established to 1.1.1.1:80\n"); } else { perror("[-] use_network - Network connection failed"); } close(sock); } //use_ptrace void use_ptrace() { long ret = ptrace(PTRACE_TRACEME, 0, NULL, NULL); if (ret == 0) { printf("[+] use_ptrace - ptrace(PTRACE_TRACEME) succeeded\n"); } else { perror("[-] use_ptrace - ptrace failed"); } } int main() { printf("=== Demo App Starting ===\n"); write_to_file(); read_sensitive_file(); make_syscall(); use_network(); use_ptrace(); printf("=== Demo App Completed ===\n"); return 0; }
Where:
Write_to_file
function: Creates the file /tmp/demo_output.txt
and writes to it using standard file operations. read_sensitive_file
function: Attempts to open and read the content of the file /etc/shadow
.make_syscal
l function: Retrieves the process ID by using the syscall()
capability.use_network
function: Creates a TCP
network socket and tries to connect to the IP address 1.1.1.1
on port 80
.use_ptrace
function: Invokes ptrace(PTRACE_TRACEME)
system call to mark the process as traceable by a parent.# gcc $HOME/test-app.c -o /usr/local/bin/test-app
# chmod +x /usr/local/bin/test-app
AppArmor supports access control for files, network, Linux capabilities, ptrace, DBus, Unix sockets, and mount devices. It has profiles that operate in two main modes:
# apt install apparmor apparmor-utils
# aa-autodep /usr/local/bin/test-app
The default profile generated is saved at /etc/apparmor.d/usr.local.bin.test-app
and contains the sections below:
include <tunables/global>
: This defines environment variables that may be useful for writing profiles. It is located in /etc/apparmor.d/tunables/global
./usr/local/bin/test-app flags=(complain)
: This marks the beginning of the profile for the test-app
binary. The flags=(complain)
setting flags the profile in complain mode.include <abstractions/base>
: This grants access to common system resources, such as shared libraries, which are typically required by most programs. It is essential for normal program execution and is defined in /etc/apparmor.d/abstractions/base
./usr/local/bin/test-app mr,
: This line allows execution of the binary with memory read (m
) and read (r
)permissions.# apparmor_parser -r /etc/apparmor.d/usr.local.bin.test-app
In the previous section, we created a default AppArmor profile in complain mode. The steps below demonstrate how to observe the application’s behavior, adjust the profile accordingly, and transition it to enforce mode for strict policy enforcement:
# aa-status
The command output should be similar to the following:
apparmor module is loaded. 14 profiles are loaded. 13 profiles are in enforce mode. /usr/bin/man /usr/lib/NetworkManager/nm-dhcp-client.action /usr/lib/NetworkManager/nm-dhcp-helper /usr/lib/connman/scripts/dhclient-script /usr/lib/snapd/snap-confine /usr/lib/snapd/snap-confine//mount-namespace-capture-helper /{,usr/}sbin/dhclient lsb_release man_filter man_groff nvidia_modprobe nvidia_modprobe//kmod tcpdump 1 profiles are in complain mode. /usr/local/bin/test-app 0 profiles are in kill mode. 0 profiles are in unconfined mode. 0 processes have profiles defined. 0 processes are in enforce mode. 0 processes are in complain mode. 0 processes are unconfined but have a profile defined. 0 processes are in mixed mode. 0 processes are in kill mode.
# /usr/local/bin/test-app
The command output should be similar to the following:
=== Demo App Starting === [+] write_to_file - Wrote to /tmp/demo_output.txt [+] read_sensitive_file - Able to read /etc/shadow [+] make_syscall - Syscall SYS_getpid returned: 4467 [+] use_network - Network connection established to 1.1.1.1:80 [+] use_ptrace - ptrace(PTRACE_TRACEME) succeeded === Demo App Completed ===
# aa-enforce /usr/local/bin/test-app
# aa-status
The command output should be similar to the following:
apparmor module is loaded. 14 profiles are loaded. 14 profiles are in enforce mode. /usr/bin/man /usr/lib/NetworkManager/nm-dhcp-client.action /usr/lib/NetworkManager/nm-dhcp-helper /usr/lib/connman/scripts/dhclient-script /usr/lib/snapd/snap-confine /usr/lib/snapd/snap-confine//mount-namespace-capture-helper /usr/local/bin/test-app /{,usr/}sbin/dhclient lsb_release man_filter man_groff nvidia_modprobe nvidia_modprobe//kmod tcpdump 0 profiles are in complain mode. 0 profiles are in kill mode. 0 profiles are in unconfined mode. 0 processes have profiles defined. 0 processes are in enforce mode. 0 processes are in complain mode. 0 processes are unconfined but have a profile defined. 0 processes are in mixed mode. 0 processes are in kill mode.
# /usr/local/bin/test-app
The command output should be similar to the following:
=== Demo App Starting === [-] write_to_file - Failed to write to file: Permission denied [-] read_sensitive_file - Failed to read /etc/shadow: Permission denied [+] make_syscall - Syscall SYS_getpid returned: 4500 [-] use_network - Socket creation failed: Permission denied [+] use_ptrace - ptrace(PTRACE_TRACEME) succeeded === Demo App Completed ===
Where:
write_to_file
, read_sensitive_file
, and use_network
functions return “Permission denied” due to AppArmor default-deny policy. Access to files, network sockets, or capabilities is denied unless explicitly allowed.make_syscall
and use_ptrace
succeed because the profile includes include
<abstractions/base>
macro, which grants essential privileges required by most programs, such as basic system calls and library access.rule.groups:apparmor OR rule.groups:syscheck
, and click Update.We can observe alerts triggered by actions denied by the AppArmor profile and alerts generated by the Wazuh FIM capability monitoring for changes made to AppArmor profiles.
The figure above shows a detailed view of the event generated when Apparmor denies the use_network
function.
include <abstractions/base>
in the /etc/apparmor.d/usr.local.bin.test-app
profile file on the Ubuntu endpoint:owner /tmp/demo_output.txt w, network inet tcp,
Where:
owner /tmp/demo_output.txt w
: This grants write permission only if the process owns this file. This is necessary for the write_to_file
function to operate.network inet tcp
: Permits the use of TCP over IPv4. This is necessary for the use_network
function to operate.# apparmor_parser -r /etc/apparmor.d/usr.local.bin.test-app
# /usr/local/bin/test-app
The command output should be similar to the following:
=== Demo App Starting === [+] write_to_file - Wrote to /tmp/demo_output.txt [+] read_sensitive_file - Failed to read /etc/shadow: Permission denied [+] make_syscall - Syscall SYS_getpid returned: 14884 [+] use_network - Network connection established to 1.1.1.1:80 [+] use_ptrace - ptrace(PTRACE_TRACEME) succeeded === Demo App Completed ===
This output validates the changes made to the profile. Where write_to_file
and use_network
functions are now operational.
rule.groups:apparmor OR rule.groups:syscheck
, and click Update.The figure above shows a detailed view of the event generated after modifying our Apparmor profile.
This blog demonstrated how Linux Security Modules, specifically AppArmor, can define and enforce kernel-level restrictions on a Linux endpoint. We created a sample application to show how operations such as unauthorized file access, improper network use, or privileged system calls can be restricted using policy-based enforcement.
These restrictions were monitored using Wazuh to capture and analyze security events at the kernel level. The Wazuh File Integrity Monitoring capability tracks changes in the /etc/apparmor.d
directory, ensuring that modification of security profiles triggers alerts. This integration allows centralized visibility of policy violations and system behavior, supporting incident correlation and audit requirements in Linux environments.
Join our community of professionals to interact with and learn more about our product.