Table of Contents:

AppArmor is a Linux Security Module (LSM) implementation, which enforces Mandatory Access Control (MAC) on individual application basis. AppArmor confines applications by only allowing access to resources or privileges which are explicitly whitelisted in the profile which is associated with the application. Since AppAmor uses a path-based approach, a great deal of flexibility regarding which applications to confine is achieved. Hence, not all applications on a system need to be confined. Confinement should rather be focused on applications which are considered to have greater security risk or that have higher attack potential. For example, one could confine access of the network interfacing applications or applications handling external data on a system. AppArmor comprises of both a kernel module and user space profiles for each application. Apertis uses AppArmor to enforce security polices for applications and services to allow access to resources based on their profiles. Depending on the mode an application or service is running, AppArmor can generate audit logs or deny access to system resources in order to track or prevent undesired accesses. This guide will introduce AppArmor and explain how such profiles can be developed in order to allow an application to run on a system where AppArmor is activated.

Since this guide is focusing on AppArmor and its implementation, this guide will not cover details of prerequisites such as the Discretionary Access Control (DAC) or capabilities that must first be considered, in order to grant the user and application permissions or access to resources on the system.

Summary

Apparmor Profiles

For application development, the only work which needs to be done for AppArmor integration is to write and install a profile for the application. Profiles should be as constrained as possible, following the principle of least privilege.

Profile Introduction

Since the AppArmor profile that confines an executable is what determines what the executable is allowed to do, profile development is a key part of the development cycle when using AppArmor. Generally speaking there are two ways to develop AppArmor profiles, either manually write the profiles or use a tool to generate them. Regardless of which method is preferred when developing the profiles the utility tools found in the package apparmor-utils are essential during the development phase of AppArmor profiles. It is therefore suggested to install the apparmor-utils package, using below command:

$ sudo apt install apparmor-utils

Profile Modes

When developing new AppArmor profiles or modifying existing profiles, it is worth noting that AppArmor profiles can be in two modes when confining an executable: complain mode or enforce mode. Profiles which are in enforce mode will block anything that the profiles do not explicitly allow. Profiles which are in complain mode will allow anything that the enforce mode would have blocked and instead generate a log for the violation of the profile rule. It’s therefore preferred to make sure the profiles are set in complain mode to allow for easier development of new profiles or making updates of the existing ones.

Profiles in complain mode do not in any way impact what is blocked by the Discretionary Access Control (DAC) on the system, as the DAC rules are evaluated before the MAC rules.

Setting an existing profile in complain mode can be done in two ways:

  • Use the tool aa-complain:

    $ aa-complain <path/to/executable>

  • Manually edit the profile and reload it in AppArmor:

    • Add flags=(complain) to the profile declaration in the profile file.
    • Reload Apparmor: systemctl reload apparmor

In both cases aa-status can be used to confirm which state a specific profile is in.

By default aa-status pacakge is not installed on the image, so command to install this package. $ sudo apt-get install aa-status

System Logs

When an executable is confined by an AppArmor profile all violations to the profile rules will generate system logs. The log entries for all profile violations will be written to /var/log/syslog, /var/log/audit/audit.log or /var/log/journal/, depending on how the system logging is configured. Note that the directories above may differ depending on the logging configuration on the system. These logs can be examined to identify what the executable is doing on the system, that’s not explicitly allowed by the AppArmor profile. These logs are also used by some of the apparmor-utils tools to help develop profiles based on the generated violation logs.

When journald is used for system logging, as it should by default on Apertis, journalctl can be used to read the journald log entries.

System Logs Examples

  • Log entry where AppArmor has allowed a write operation to the file “/foo.txt” from the script “/home/user/write.sh” whose profile was in complain mode. This write operation would otherwise have been blocked by AppArmor in case the profile had been in enforce mode:

     type=AVC msg=audit(1612792741.460:115061): apparmor="ALLOWED" operation="file_perm" profile="/home/user/write.sh" name="/foo.txt" pid=26376 comm="write.sh" requested_mask="w" denied_mask="w" fsuid=459221780 ouid=459221780
    
  • Log entry where AppArmor has explicitly created an audit entry for read access to the file “/foo.txt”. This is due to an audit rule in the profile which is confining the executable “/home/user/cat.sh”.

    type=AVC msg=audit(1613116865.459:120399): apparmor="AUDIT" operation="open" profile="/home/user/cat.sh" name="/foo.txt" pid=21253 comm="cat" requested_mask="r" fsuid=459221780 ouid=459221780
    
  • Log entry where AppArmor has denied read access to the file “/foo.txt” for the script “/home/user/cat.sh”. This is either due to an explicit “deny” rule in the profile or (more likely) by an implicit deny, due to not explicitly whitelisting the read access in the profile.

    type=AVC msg=audit(1613117305.314:121263): apparmor="DENIED" operation="open" profile="/home/user/cat.sh" name="/foo.txt" pid=23035 comm="cat" requested_mask="r" denied_mask="r" fsuid=459221780  ouid=459221780
    

Profile Development

Development Environment

The OSTree images do not offer a very friendly environment for development, it is best to perform such development using the apt based images. To use the AppArmor tools mentioned below, the apparmor-utils package will need to be installed:

sudo apt install apparmor-utils

Manually Write The Profile

AppArmor profiles are located in the directory /etc/apparmor.d/ and are named according to the executable which they are associated with, where slashes (‘/’) are replaced by dots (‘.’). For example, the profile file for the executable /usr/bin/executable should be named: /etc/apparmor.d/usr.bin.executable.

To create a new profile for an executable the following steps should be taken:

First, make sure the executable runs as it should under the normal DAC permissions on the system. AppArmor does not grant any permissions, only reduces permissions already allowed by DAC.

If an executable requires root-like privileges e.g. using capabilities, then this must also first be considered.

  1. Create a new file in /etc/apparmor.d/ that is named according to the path of the executable which it shall confine. Add the following skeleton for the profile:

    #include <tunables/global>
    
    /path/to/executable flags=(complain) {
      #include <abstractions/base>
      /path/to/executable r,
    }
    
  2. Make sure the profile is loaded in AppArmor:

    • Either reload AppArmor completely:

      sudo systemctl reload apparmor
      
    • Or explicitly set the executable in complain mode:

      sudo aa-complain /path/to/executable
      
  3. Verify that the profile is loaded in complain mode using:

     sudo aa-status
    
  4. Add the specific rule(s) needed for the executable to run.

  5. Save profile and reload AppArmor.

  6. Run the executable and exercise as much of its functionality as possible.

  7. Check the system log entries generated by the executable.

  8. Repeat steps 4-7 until rules for all functionality of the executable has been created in the profile and no more violation logs are generated for the executable.

  9. Verify that the profile works in enforce mode.

    • Set profile in enforce mode:

        sudo aa-enforce /path/to/executable
      
    • Run executable and exercise the intended functionality of it.

Since the syntax for the profiles rules are human readable, manually writing profiles is quite straight forward.

Test-cases or unit-tests can help with exercising the functionality of the executable and generating logs.

Tool Aided Profile Development

To simplify the creation of AppArmor profiles, for e.g. large binaries or binaries which require more privileges, the tools inside apparmor-utils can be used. The main tool to use for this is called aa-genprof which essentially creates an empty profile in complain mode and then scans the system logs for any violations associated with the binary for which it is being run. To use aa-genprof the following step should be taken:

Make sure the executable runs as it should under the normal DAC permissions on the system. AppArmor does not grant any permissions, only reduces permissions already allowed by DAC.

If an executable requires root-like privileges e.g. using capabilities, then this must also first be considered.

  1. Open two terminals.

  2. In terminal 1, start aa-genprof using:

     sudo aa-genprof <path/to/executable>
    

If journald is used as the system logging mechanism then the journald logs needs to be converted into something the apparmor-utils tools can read. The easiest way is to use journalctl.

Example:

journalctl | grep apparmor >> </path/to/log.txt>
sudo aa-genprof -f </path/to/log.txt> <path/to/executable>
  1. In terminal 2, run the executable and interact with it to exercise as much of its functionality as possible.

  2. In terminal 1, press s to have aa-genprof scan the system log for entries generated by the executable.

If journald is used for system logging then the text file with the output from journalctl will need to be appended manually each time to ensure that the text file contains all the latest log entries from the journald log before scanning the log file:

journalctl | grep apparmor >> </path/to/log.txt>
  1. Answer the question asked by aa-genprof regarding the found system events.

While performing this step it is preferred to really take a few extra seconds to read and understand the proposed rule and the implication of what aa-genprof presents. E.g. if an executable has read 100 files, all located in the same directory and with the same file extension the tool will suggest to add one rule per such read access, i.e aa-genprof will ask a similar question 100 times. However, a developer that knows this behavior of the executable can easily make this process faster by realizing that a globbing pattern for these files in this directory can be added as 1 rule, instead of 100 rules and thus reducing both the complexity and maintenance of the profile, while saving time developing the profile. It shall however not be taken lightly to introduce globbing schemes that will span to any files or directories outside of the intended functionality of the executable.

Globbing patterns should not be written for execute rules.

  1. Repeat steps 3-5 until the full functionality of the executable has been executed, recorded and rules generated. E.g. if a binary performs different actions depending on input parameters, steps 3-5 should be repeated until all of the different actions performed by the binary has generated system log entries.

  2. In terminal 1, press f to indicate that aa-genprof shall finish and save the profile.

  3. Verify that the profile works in enforce mode.

    • Set profile in enforce mode:

       sudo aa-enforce /path/to/executable
      
    • Run executable and exercise the intended functionality of it.

The output will be located among the standard rules for AppArmor (/etc/apparmor.d/), unless explicitly specified with the -d flag. The output file will be named according to the path and the name of the file, where the / are replaced by ..

Developing Common Parts For Multiple Profiles

In some cases parts of a profile could be applicable to more than one executable. Instead of copying these common parts to all the relevant profiles this common set of rules can be placed in a common file. This common file can then be included in the corresponding profiles using a C-style #include statement. Similar to C-style #include statements, the profile #include statement will result in that the content of the included file will be inserted at the place where the #include statement is. The #include statement is evaluated as a relative path to the directory /etc/apparmor.d.

Example of a common file /etc/apparmor.d/my_directory/common_file that can be included in several profiles:

# This common file will allow read access to specific user files along with
# read access to header files in /usr/include. Additionally execution rights
# for /bin/cat, confined to the same profile as the calling executable.

# Read permission to some files in some directories
owner /home/*/some_dir/prefix_*.postfix r,
owner /home/*/another_dir/specific_file.txt r,
/usr/include/*.h r,

# Execute permission with inherited profile for '/bin/cat'
/bin/cat ix,

Example profile for /path/to/executable1 in the file /etc/apparmor.d/path.to.executable1:

/path/to/executable1 {
  # Include the common file to get base set of rules
  #include <my_directory/common_file>

  # Additional rules specific to this executable
  /bin/echo ix,
  audit owner /home/*/log/log.txt rw,

  # Read access to the executable itself
  /path/to/executable1 r,
}

Example profile for /path/to/executable2 in the file /etc/apparmor.d/path.to.executable2:

/path/to/executable2 {
  # Include the common file to get base set of rules
  #include <my_directory/common_file>

  # Read access to the executable itself
  /path/to/executable2 r,
}

Example profile for /path/to/executable3 in the file /etc/apparmor.d/path.to.executable3:

/path/to/executable3 {
  # Include the common file to get base set of rules
  #include <my_directory/common_file>

  # Explicitly deny a rule inherited from the common rule
  deny /usr/include/*.h r,

  # Read access to the executable itself
  /path/to/executable3 r,
}

In the example above, both executable1 and executable2 will have the same base set of rules, allowing read access to various files along with execute permission for /bin/cat. In addition, executable1 will also be able to execute /bin/echo under the same confinement as itself and AppArmor will allow and audit all read and write accesses to the log file /home/<user>/log/log.txt. However, executable3 will only have a sub-set of the rules in the common file since a deny rule is added to override one of the rules from the common file to deny read access to any .h file in /usr/include/.

Profile Validation

AppArmor profiles can be validated in two ways: at runtime and manually.

Runtime verification is automatic: AppArmor will deny access to files or resources which violate the profile rules, emitting a message in the system logs. See System Logs for details. Such messages should be investigated, and may result in either:

  • Changes to the application (to prevent it making such accesses), or
  • Changes to the profile (to allow such accesses).

Manual verification should be performed before each release:

  1. Manually inspect the profile against the list of changes made to the application since the last release.
  2. Check that each entry is still relevant and correct.
  3. Check that no new entries are needed.

Manual and runtime verification are complementary: manual verification ensures the profile is as small as possible; runtime verification ensures the profile is as big as it needs to be.

Installing Profiles

Once the profile is working as required add it to the relevant package (typically in the debian/apparmor.d directory) and submit it for review.

The profiles can be loaded with the following command:

sudo apparmor_parser -r -v < /etc/apparmor.d/my.new.profile

Typically this is performed with the profile in complain rather than enforce mode. The status of the profiles can be determined by running:

sudo aa-status

Profile Syntax and Examples

File Access

Since the AppArmor security model is a MAC implementation, it can only confine access to resources that the executable’s owner already has access to, according to the DAC access permission rules.

As an example, the diagram below shows the contents of /home/user directory and the files’ owner, which is “user”, has read/write access to the first three files in the green DAC box. On the other hand, the owner “user” does not have acces to the last file, as defined by the DAC permission rules.

AppArmor can only create permission rules to the files in the green DAC box, and cannot give more access than what is already accessible for “user”. In this example, AppArmor could create rules for an executable owned by “user” that allows read only access to only two files, i.e. files within the purple MAC box. Despite the fact that “user” would normally be able to have both read/write permissions on all three files in the green DAC box, in this case, the access would be denied to the first file due to confinement by AppArmor. In the table below, it is shown what access an executable owned by “user” would have before and after AppArmor access rules are applied.

Files in “user” directory Access given by DAC Access given by MAC / AppArmor
file_1.txt Yes No
file_2.txt Yes Yes
file_3.txt Yes Yes
file_4.txt No No

Based on the above example of DAC and MAC access permissions, a simple bash script called file_access.sh has been written. The script reads two files that the script’s owner has access to and nothing else. Note that the script does not need to write anything to the files, even though the user could do that.

In order to achieve the desired behavior an AppArmor profile for the executable needs to be created using the aa-genprof tool. Before running the aa-genprof tool, make sure the file_access.sh script has execute permission for the owner.

Below is the content of the directory with four text files and the bash script. There is also content of the bash script / executable file_access.sh and the created profile home.user.file_access.sh.

Listed files in the /home/user directory:

$ ls -al /home/user
-rw-r--r-- user grp … file_1.txt
-rw-r--r-- user grp … file_2.txt
-rw-r--r-- user grp … file_3.txt
-rw-r--r-- usr2 gr2 … file_4.txt
-rwxr--r-- user grp … file_access.sh

Below is the content of file_access.sh:

#!/bin/bash

cat file_2.txt
cat file_3.txt

Below is the content of /etc/apparmor.d/home.user.file_access.sh:

#include <tunables/global>

/home/user/file_access.sh {
    #include <abstractions/base>
    #include <abstractions/bash>
    #include <abstractions/consoles>

    /home/user/files_access.sh r,
    /usr/bin/bash ix,
    /usr/bin/cat mrix,
    owner /home/*/file_2.txt r,
    owner /home/*/file_3.txt r,
}

Description of the above profile:

  • #include <tunables/global> : Includes statements from other files, so there is no need to duplicate the common rules.
  • /home/user/file_access.sh : Path to the profiled executable.
  • #include <abstractions/*> : Includes common variables and libraries.
  • /home/user/files_access.sh r, : Allows the read access to the files_access.sh script.
  • /usr/bin/bash ix, : Inherit execute, i.e. the executed bash program will inherit the current profile.
  • /usr/bin/cat mrix , : Allows the cat application read and write access to a file mapped in memory. Also, inherits the current profile.
  • owner /home/*/file_2.txt r, : Allows the read access to file_2.txt, which could be placed in any directory under /home/ owned by the owner.

When the file_access.sh script is run by “user”, it will show the content of the two files that “user” has read permissions for in the created profile.

Now we change the file_access.sh to include reading of two additional files, the one that the user has access to (file_1.txt) and the one that the user doesn’t have access to (file_4.txt), according to the DAC permission rules. After the change, the executable should look like this:

Below is the content of updated file_access.sh:

#!/bin/bash

cat file_1.txt
cat file_2.txt
cat file_3.txt
cat file_4.txt

When the updated script above is run again, AppArmor (assuming it is in enforce mode) will deny access to the both newly added files. The reason is simply because the profile of the executable has not been updated. The profile only allows read access to the files mentioned in the profile, i.e. file_2.txt and file_3.txt, and everything else will be implicitly denied. If we would add read access to file_1.txt and file_4.txt to the existing profile, i.e. same as for the other two files, the result this time would be that reading of all files, except the file_4.txt file, would be allowed. The access to the file_4.txt file would still be denied, because the owner of the executable does not have access to that file. In this case, the DAC read permission rule would kick in and deny the access, and as we already know, AppArmor cannot grant more permissions than the owner of the executable already has.

Resource Limit Control

One of the important confinement possibilities with AppArmor is also resource limitations that can be set and controlled in profiles. Following are examples of resources that can be limited: maximum size of process’s memory, maximum CPU time, maximum size of files that a process may create, maximum size of memory that may be allocated in RAM, maximum number of processes that can be created by the calling process etc.

The resource limitations are handled with kernel’s rlimits, which are also known as ulimits. According to the excerpt from the getrlimit(2) Linux man page: “Each resource has an associated soft and hard limit. The soft limit is the value that the kernel enforces for the corresponding resource. The hard limit acts as a ceiling for the soft limit: an unprivileged process may only set its soft limit to a value in the range from 0 up to the hard limit, and (irreversibly) lower its hard limit. A privileged process (under Linux: one with the CAP_SYS_RESOURCE capability) may make arbitrary changes to either limit value.” AppArmor can only control an executable’s hard limits and make sure the soft limits are not higher than the hard limits.

As with all other confinement possibilities AppArmor offers, it cannot raise the system’s rlimits, but only reduce what is already allowed by the system. If an executable would try to raise its hard rlimits to larger values than specified in its profile, AppArmor would prevent that. Profiles’ rlimits can only be either lower or equal to the system’s rlimits. When it comes to inheritance, a child will keep the same rlimts as its parent process and the rlimits will remain unchanged even if the executable becomes unconfined. Also, if an executable transfers to a new profile, e.g. if a new parent profile is created and the old executable becomes a child, in that new parent profile it is possible to further reduce rlimits. AppArmor does not provide any additional logging for rlimits.

The command to control the hard limit rule in AppArmor has the following syntax:

set rlimit `resource` <=  `value`,

The resource variable could be e.g. cpu, fsize, data, stack, core, rss, nofile, ofile, as, nproc, memlock, locks etc. For complete overview of all possible variables and corresponding values that can be specified for the rlimit rules, please check RLIMIT RULE syntax on Ubuntu manpage apparmor.d - syntax of security profiles for AppArmor. Currently there is no tool that will automatically write a rlimit rule to a profile, hence it always needs to be inserted manually. If an update of a profile containing rlimits is made by e.g. the aa-logprof tool, it will not do any changes to the existing rlimit rules.

To find out what soft and hard resource limits there are for a certain process, read the following file (replace PID with the real process ID number):

cat /proc/PID/limits

In the following example, where a bash script is used, a limited amount of text is written to a file. But in a real application, it could for example be a log file that the script could write to without any restrictions, and in that case a large file size could be an issue for our system. The potential issue could be made by a mistake or deliberately by an attacker.

Below is the content of max_file_size.sh:

#!/bin/bash

FILE_NAME=/home/user/file.txt
touch $FILE_NAME
> $FILE_NAME
FILE_BLOCK_SIZE=`du -b $FILE_NAME | cut -f1`
echo "Size of $FILE_NAME is $FILE_BLOCK_SIZE blocks."

# For each loop, the file is increased by 10 blocks.
# In total, the size of the file can be 50 blocks.
for ((s=0; s<5; s++))
do
    echo "Some text" >> $FILE_NAME
    FILE_BLOCK_SIZE=`du -b $FILE_NAME | cut -f1`
    echo "Size of $FILE_NAME is $FILE_BLOCK_SIZE blocks."
done

In the created profile for the above script, the file size is limited to max 40 blocks, which is equivalent to 40 bytes. We already know that the above script will create a file with size of 50 blocks if there are no restrictions.

Below is the content of profile /etc/apparmor.d/home.user.max_file_size.sh:

include <tunables/global>

/home/user/max_file_size.sh {
    #include <abstractions/base>
    #include <abstractions/bash>
    #include <abstractions/consoles>

    /etc/ld.so.cache r,
    /home/user/max_file_size.sh r,
    /usr/bin/bash ix,
    /usr/bin/cut mrix,
    /usr/bin/du mrix,
    /usr/bin/touch mrix,
    owner /home/*/file.txt w,
 
    # Limit the file size to max 40 blocks.
    set rlimit fsize <= 40,
}

After putting the above profile in enforce mode and running the max_file_size.sh script, the following result is obtained in the console (Note: This will not be logged by AppArmor, unless audit is explicitly specified):

> ./max_file_size.sh 
Size of /home/user/file.txt is 0 blocks.
Size of /home/user/file.txt is 10 blocks.
Size of /home/user/file.txt is 20 blocks.
Size of /home/user/file.txt is 30 blocks.
Size of /home/user/file.txt is 40 blocks.
File size limit exceeded (core dumped)

As seen from the above console output, the AppArmor rlimit rule kicks in and stops further writing to the file, which the script is writing to. This is a very simple and effective way to impose the file size limit to the running script that could potentially create an issue for our system if the default file size limit would be used instead.

Capabilities

Capabilities Introduction

Linux capabilities provide a mechanism to enable unprivileged processes to utilize functionality usually reserved for privileged processes (those run as root) in a granular way. In total there are 37 different capabilities (depending on the kernel version). Capabilities can be a useful tool in certain scenarios, for example when an executable needs elevated privileges not normally granted on a system. Instead of running the executable as root, capabilities can be assigned to either a thread or to a file (relies on extended attributes) to allow it to perform the needed task(s), without having all the privileges of the root user.

There are five different sets of capabilities available, Bounding, Permitted, Effective, Inheritable and Ambient. These sets define if, how and what capabilities are allowed and they work slightly different depending on if they are applied to threads or files. A somewhat simplified overview of the sets can be seen below:

Set Explanation Threads Files
Bounding What can be assigned Yes No
Permitted What is allowed to be assigned Yes Yes
Effective What currently is assigned Yes Yes
Inheritable What is allowed to be inherited Yes Yes
Ambient What is preserved across “execve” calls Yes No

For more details on the available sets, please see sections “File capabilities” and “Thread capabilities” in Capabilities.

Capabilities are a powerful tool which can easily be misused should a process with them be compromised, hence care has to be taken when assigning them. For example, in the absence of MAC, an executable which is assigned the capability CAP_DAC_OVERRIDE is allowed to override the DAC enforced by the system. This means that the executable can read and write to any file on the system and easily use this to elevate its own privileges to gain root privileges on the system.

Capabilities and AppArmor

To reduce the risk that is introduced by assigning capabilities AppArmor can be used to confine executables which have been granted capabilities. AppArmor restricts the capabilities an executable can invoke to those explicitly allowed in its profile. Note that AppArmor cannot be used to assign capabilities to an executable or thread, AppArmor can only block or allow already assigned system capabilities. To facilitate this whitelisting, AppArmor has specific capability rules that can be used to allow or (explicitly) deny capabilities. If no capability rules are present in the profile, the default behavior is to implicitly block any capabilities.

Example: Profile where capability CAP_CHOWN is allowed for an executable, along with read and write permission to a specific file.

/path/to/executable {
  # Allow capability CAP_CHOWN
  capability chown,

  # Read and Write permission to a specific file
  /home/user/folder/file.txt rw,

  # Read access to the executable itself
  path/to/executable r,
}

Another common usage for capabilities is to allow processes or executables to use mount commands on a system. To facilitate this use-case with capabilities the capability CAP_SYS_ADMIN must be used.

Since CAP_SYS_ADMIN basically grants root user privileges it should be carefully considered and not be allowed to run unconfined on a system.

To help reducing the risk of assigning CAP_SYS_ADMIN to an executable AppArmor can be used to confine the executable to only allow it to perform the intended tasks and thus minimize the window of opportunity to misuse all the privileges that CAP_SYS_ADMIN grants. To allow a confined executable to use mount the following five criteria must be met:

  1. Capability CAP_SYS_ADMIN assigned to the executable.
  2. DAC access rights allow the needed operations to be performed.
  3. Capability CAP_SYS_ADMIN must be allowed by the profile.
  4. AppArmor mount rules must allow the file system to be mounted to the mount point.
  5. AppArmor file access rules must allow read and write access to the file system and mount point.

Example: Profile where capability CAP_SYS_ADMIN is allowed, along with mount, read and write permissions to a specific mount point.

/path/to/executable {
  # Allow capability CAP_SYS_ADMIN
  capability sys_admin,

  # Allow 'path/to/fs' to be mounted at mount point '/path/to/mount_point/'
  mount /path/to/fs -> /path/to/mount_point/,

  # Allow to execute the mount binary confined by the same profile as '/path/to/executable'
  /bin/mount Ix, 

  # Read access to the file system to be mounted
  /path/to/fs r,

  # Read and Write access to the mount point directory
  /path/to/mount_point/ rw,

  # Read access to any files or directories under the mount point directory
  /path/to/mount_point/** r,

  # Read access to the executable itself
  path/to/executable r,
}

Mount

With AppArmor it is possible to define what mount operations a confined executable is allowed to perform. By default no mount operations are allowed, but the AppArmor mount rules can be used to explicitly whitelist certain mount operations.

Since the AppArmor mount rules are based on the same syntax as mount(8), detailed references regarding fstype and options can be looked up there. Re-using the same syntax also makes it easier to map the mount operations executed to the mount rules needed in the AppArmor profile.

For AppArmor versions before 2.8 capability CAP_SYS_ADMIN was sufficient. Repology can be used to find out the used AppArmor version for various distributions.

As with any other kind of AppArmor rules, the mount rules can only be used to block or allow what is already granted on system level. Hence, in order to use mount operations in an executable confined by AppArmor the following criteria must be met:

  1. Executable or user is allowed to perform mount operations on system level, e.g by DAC permissions or capability CAP_SYS_ADMIN.

    Inspiration on performing mount operations as a non-root user can be found in section “Non-superuser mounts” at mount(8).

  2. The profile must allow capability CAP_SYS_ADMIN.

  3. The profile must allow the needed mount operations, using the mount rules.

  4. The profile must allow the necessary file permissions. E.g execute permission to the mount binary, read the filesystem or write to the mount point etc.

Example: Profile where /path/to/executable is allowed to execute the binary /bin/mount to mount /path/to/fs at the mount point /path/to/mount_point/ as any type of filesystem, with any arguments to the mount operation e.g ext4 type as read-write or sysfs as read-only etc.

/path/to/executable {
  #include <abstractions/base>

  # Allow capability CAP_SYS_ADMIN
  capability sys_admin,

  # Allow to execute the mount binary confined by the same profile as '/path/to/executable'
  /bin/mount Ix,

  # Allow 'path/to/fs' to be mounted at mount point '/path/to/mount_point/'
  mount /path/to/fs -> /path/to/mount_point/,

  # Read access to the filesystem to be mounted
  /path/to/fs r,

  # Write access to the mount point and any files or directories below it
  /path/to/mount_point/** w,

  # Read access to the executable itself
  path/to/executable r,

Example: Profile where /path/to/executable is allowed to execute the binary /bin/mount to mount, remount and unmount certain mount points. Here dummy_fs is only allowed to be mounted as type ext4 and read-write to /path/to/mount_point_1/, and if dummy_fs is owned by the current user. Anything under /path/to/mount_point_2/ can be remounted, while /path/to/mount_point_3/ can only be unmounted.

/path/to/executable {
  #include <abstractions/base>

  # Allow capability CAP_SYS_ADMIN
  capability sys_admin,

  # Allow to execute the mount binary confined by the same profile as '/path/to/executable'
  /bin/mount Ix,

  # Allow 'dummy_sysfs', no matter where it is located on the system, to be
  # mounted as type 'ext4' and read-write to '/path/to/mount_point_1/'
  mount fstype=(ext4) options=(rw) /**/dummy_sysfs -> /path/to/mount_point_1/,

  # Allow to remount of any mount point in any directory under '/path/to/mount_point_2/'
  remount /path/to/mount_point_2/**,

  # Allow to unmount '/path/to/mount_point_3/'
  umount /path/to/mount_point_3/

  # Read access to the filesystem to be mounted, if owned by the current user
  owner /**/dummy_sysfs r,

  # Read access to the all three mount points and any files or directories below them
  /path/to/mount_point_[123]/** r,

  # Write access to the two mount points and any files or directories below them
  /path/to/mount_point_[12]/** w,

  # Read access to the executable itself
  path/to/executable r,

Best practices

Following is the list of recommendations during the development and usage of profiles in AppArmor:

  • Follow the principle of least privilege when developing AppArmor profiles.
    • This means that a profile for an executable should only allow bare minimum permissions so it does what is intended to do and nothing else. To develop a profile quickly, without much thought what each permission rule does and to give many unnecessary permissions is not the right way to go. Development of AppArmor profiles requires understanding of the executables for which the profiles are developed and the environment they run in.
    • Reduce the capabilities’ bounding set, which controls what capabilities are available for a process, to only include what is actually needed for the intended use-cases.
  • Make profile development a part of the development and release process for your executables.
    • This ensures that new functionality of the executables are always reflected in the AppArmor profiles along with removing privileges that are no longer needed. A good idea is to perform regular manual reviews of the AppArmor profiles, e.g. in conjunction with each release.
  • If there are any issues that might be due to confinement rules in some of the profiles, the easiest way to troubleshoot is to put the profile in complain mode, run the corresponding executable and check the log files. After the potential updates of the profile are made, don’t forget to put it back to the enforce mode and reload the profile.
  • Don’t use unconfined execute (Ux/ux) permission for child processes.
    • This ensures that common utilities and helper scripts are harder to exploit for privilege escalation on your system. If there is no profile supplied with the tool you are using from your executable, you should consider writing one for it or alternatively confine it under the same policy as the calling parent, using inherited execute (Ix/ix).
  • The globbing syntax, e.g. **, [abc], ?, etc., is not the same as standard bash regular expressions. It uses wild characters instead, but some of them have slightly different meaning than semantics in bash. Refer to AppArmor Core Policy Reference for more details.
    • Example of globbing syntax where we allow reading of two files, e.g. file_2.txt and file_3.txt: owner /home/*/file_[23].txt r,.
  • Even though the globbing syntax is quite useful, it needs to be used carefully. It is not recommended to use it for files that are executables, i.e. files with the x qualifiers.
    • This eliminates potential syntax mistakes and ensures that only the intended executables are executed, leading to decreased attack potential.
  • Consider using conditional statements (rule qualifiers) in profiles. For example, they are represented by the following keywords: allow, deny, owner, other, audit etc.
    • They can be used to further refine the existing rules, e.g. allow reading of file(s) that are only owned by the user should use the conditional statement owner in the rule. In a multi-user system, this will also prevent reading of other user’s files even if the sudo privilege is used: owner /home/*/my_file.txt r,.
  • The keyword audit at the beginning of a profile rule could be used to explicitly tell AppArmor to log the specified rule, which in normal circumstances might not be logged, e.g. allow reading of a file that the user owns and log that event: audit owner /home/*/file_2.txt r,.
    • The audit keyword could be very useful during troubleshooting.
  • The keyword deny in a profile rule is normally not necessary due to whitelisting, i.e. even if the rule is not there, any requests will be denied if not explicitly allowed. Still, it is very useful to have the deny possibility. For example, if we would like to deny some requests even when the profile is in complain mode, or if we would like to point out to the future owners of the profile that it is important to forbid certain requests. Another useful example is also when e.g. a parent allows certain request and a child inherits from the parent but does not want to allow that particular rule, i.e. deny could be used in that case and it is evaluated after allow.
  • Write capability aware binaries, those that use the libcap API to manipulate their own capabilities.
    • This makes it possible to implement logic to handle any failures to obtain capabilities instead of relying on the kernel’s EPERM error.
  • Don’t allow executables with assigned capabilities to spawn child processes in unconfined mode.