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
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
- Add
In both cases aa-status
can be used to confirm which state a specific profile
is in.
By default aa-status
package 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.
-
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, }
-
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
-
-
Verify that the profile is loaded in complain mode using:
sudo aa-status
-
Add the specific rule(s) needed for the executable to run.
-
Save profile and reload AppArmor.
-
Run the executable and exercise as much of its functionality as possible.
-
Check the system log entries generated by the executable.
-
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.
-
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.
-
Open two terminals.
-
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>
-
In terminal 2, run the executable and interact with it to exercise as much of its functionality as possible.
-
In terminal 1, press
s
to haveaa-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>
- 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.
-
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.
-
In terminal 1, press
f
to indicate thataa-genprof
shall finish and save the profile. -
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:
- Manually inspect the profile against the list of changes made to the application since the last release.
- Check that each entry is still relevant and correct.
- 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:
- Capability
CAP_SYS_ADMIN
assigned to the executable. - DAC access rights allow the needed operations to be performed.
- Capability
CAP_SYS_ADMIN
must be allowed by the profile. - AppArmor mount rules must allow the file system to be mounted to the mount point.
- 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:
-
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).
-
The profile must allow capability
CAP_SYS_ADMIN
. -
The profile must allow the needed mount operations, using the mount rules.
-
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
andfile_3.txt
:owner /home/*/file_[23].txt r,
.
- Example of globbing syntax where we allow reading of two files, e.g.
- 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,
.
- 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
- 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.
- Understand the difference between “environment variable scrubbing” (capital P/I/C/Ux) and “no environment variable scrubbing” (lower case p/i/c/ux) for execute permissions and where possible use the former.
- Check Ubuntu man page for details on scrubbing: Ubuntu Manpage apparmor.d - syntax of security profiles for AppArmor.