28 QuickProfileLanguage
Ryan Lee edited this page 2025-01-27 20:55:25 +00:00

A quick guide to AppArmor profile Language

This is a quick guide to the AppArmor profile language; for an in depth guide see the Profile Language Reference or the apparmor.d man page.

AppArmor policy is created using an administrator friendly profile language that is then compiled into a binary policy for loaded into the kernel. AppArmor policy is stored in a set of files in /etc/apparmor.d/. AppArmor policy is split into profiles which usually are designed to confine a specific application. Profiles will declare access rules to allow access to resources, and access is implicitly denied with logging when there is no matching rule in the profile.

Profiles

Profiles declarations consist of a name and attachment, followed by an optional flags field described in more detail here. At least one of the name and attachment must be present; if only the attachment is present, it will also be considered the name. The attachment represents the path(s) of binaries that a profile is automatically applied to once the profile is loaded into the kernel. If no attachment is present, a profile will not automatically apply to any program but can still be used manually to confine programs using methods like aa-exec and the apparmor security option of Docker.

The keyword "profile" is required when a separate name is present and is encouraged even when not required.

Examples:

 /usr/bin/firefox {
    # profile contents
 }

 /usr/bin/firefox flags=(complain) {
    # profile contents
 }

 profile /usr/bin/ {
    # profile contents
 }

 profile user1 {
    # profile contents
 }

Profile attachments can contain file rule globbing characters to allow them to apply to multiple executables, see File Globbing (below) for details.

Profile Complain Flag

One profile flag that is useful for profile development is the complain flag. When this flag is set, actions that would be denied by the profile are instead allowed and logged.

When a process spawns a child process that would normally be disallowed, a null learning profile is created for that child process instead. This learning profile will result in logs for every single auditable action that the child performs, including actions that would have been allowed under an ix transition.

Comments

Comments are line oriented and begin with a #. Text following a '# to the end of the line is ignored (with the exception of the #include rule). Eg:

 #  Comment 1
 #  Comment 2

 profile example {  # comment 3
    # comment 4
    /home/foo rw,  # comment at the end of a file rule
 }

Include Rules

Policy files can include other files to share text segments. AppArmor includes follow the C include convention: as rules in an include would be terminated with a comma, include statements should not be terminated with one either. They start with include followed by either angle brackets for text chucks relative to a set include directory or quoted paths for files relative to the current file. A # may precede the include keyword, but it shouldn't be used in newer profiles. Eg:

 #include <file>
 #include “a/relative/path/file”
 include <file>

#include conflicts with commenting rules and takes precedence. The # and include must not be separated from the include with white space otherwise it is considered a comment

 # include is a comment
 #include <file>

The include file <abstractions/base> sets up access to many common files used universally, and the include file <tunables/global> sets up variables that represent common directories. For more information, see this list of common abstractions.

Child profiles

A profile can contain children profiles. Child profiles can be used to confine an application in a special way, or when you want the child to be unconfined on the system, but confined when called from the parent. Eg:

 /parent/profile {
     /path/to/child1 cx -> child1,
     /path/to/child2 cx,
     /path/to/* cx,           # for * matching child3 will transition to child3,
                              # child4, child5, ... will transition to /path/to/child*
                              # if matching child profile does not exist will fail
     /another/path/to/* cx -> child1,        # send all matching execs to child1
     profile child1 {
     }
     profile /path/to/child2 {
     }
     profile /path/to/child3 {
     }
     # generic fall back profile
     profile /path/to/child* {
     }
  }

Hats

Hats are a special child profile that can be used with the change_hat API call. To denote a hat, prepend ^ before the hat name with no spaces. Eg:

 /parent/profile {
     ^hat {
     }
 }

Variables

Variables are represented using the syntax @{variable_name}, are set before a profile begins, and are read inside of a profile. As variables are not profile rules, they are not terminated with a comma. They can have multiple values, with all of them being included when the variable is expanded.

Examples of variable assignment and usage:

# Assignment of a single value
@{alphabet_string}=abcdefghijklmnopqrstuvwxyz
# Assignment of multiple values
@{digits}=1 2 3 4 5 6 7 8 9 0
@{hexdigits}=@{digits}
@{hexdigits}+=a b c d e f

profile example {
    # @{hexdigits} will expand to allow any of the 16 hex digits
    /var/cache/our_program/@{hexdigits}/** rw,
}

The include file <tunables/global> includes many variables that may be of use for writing profiles.

The special variable @{profile_name} contains the name of the current profile.

Capability Rules

AppArmor supports coarse-grained access to Linux' POSIX-style capabilities (see man capabilities) and capabilities rules are used to allow access to these capabilities. For example, a setuid application which drops privileges might need:

 /profile {
    capability setuid,
    capability setgid,
 }

Multiple capabilities can be specified as a space-separated list.

Network Rules

AppArmor currently supports coarse-grained access to networking via network rules. For example, a network daemon might need:

 /profile {
   # Use "inet" for IPv4 only, "inet6" for IPv6 only, or omit for both
   network inet dgram, # can also say "udp" instead of "dgram"
   network inet stream, # can also say "tcp" instead of "stream"
 }

Or a packet analyzer might need:

 /profile {
   network raw,
   network packet,
 }

Rlimit Rules

Apparmor can set limits on the "hard limit" values of setrlimit that a process is allowed to set for itself. However, since rlimit enforcement is handled by a different part of the Linux kernel, no Apparmor audit message will be printed when an rlimit is enforced.

Signal Rules

Example signal rules are as follows:

/profile {
    # Can receive signals from unconfined processes
    signal (receive) peer=unconfined,
    # Can send and receive SIGUSR1 and SIGUSR2 signals to other instances of ourselves
    signal (send,receive) set=(usr1,usr2) peer=/profile,
}

For a signal to succeed, its sender must have permission to send the signal and its target must have permission to receive the signal.

Note: peer=unconfined does not mean allowing signals between the confined process and any other process; instead, it only allows signals when the other process is not running under an AppArmor profile.

Ptrace Rules

There are four classes of ptrace operations:

  • trace: trace another process using ptrace
  • tracedby: be traced using ptrace by another process
  • read: read certain proc filesystem information, kcmp, futexes, and perf trace events about another process
  • readby: have certain proc filesystem information, kcmp, futexes, and perf trace events about oneself read by another process

The peer= option can be included to limit the profiles that the other process must be running under.

Example:

 /profile {
   ptrace, # Allow all ptrace operations
   ptrace (readby, tracedby) peer=unconfined, # Allow unconfined processes to ptrace us
 }

File rules

File rules control how files are accessed and only occur within a profile. They consist of a pathname, a permission set and are terminated by a comma. They can be written with either the permission first or the pathname first, though the convention it to list the path first. A valid pathname always begins with a /. Eg:

 /profile {
    /path/to/file  rw,   # file rule beginning with a pathname (convention)
    rw /path/to/file2,   # file rule beginning with permissions
    /path/to/file3       # file rule split over multiple lines
         rw,
 }

Entries for specific (non-globbed) directories should have a trailing slash at the end. For example, /path/to/obj rw grants permissions for a file obj, while /path/to/obj/ rw grants permissions for a directory obj.

File rules can contain special globbing characters that allow matching to multiple files (see File Globbing, below)

See AppArmor File Permissions for more detail about file permissions.

File Globbing

AppArmor uses a file globbing syntax similar to that used by the bash shell. Globbing is not standard full regular expression syntax, but instead uses a few characters known as wild cards. The AppArmor wildcards have slightly different semantics than that of bash.

  • * - match zero or more characters at the directory level. When looking at the path as a string this will match every character except /
    • This will match dot files (file names starting with .), excepting the special dot files . and .., if it is placed immediately after the directory / eg. /dir/*
    • This will not match an empty directory string eg. /dir//
    • pcre equivalent is ([^/\000]*)
  • ** - match 0 or more characters over multiple directory levels.
    • This will match dot files (file names starting with .), excepting the special dot files . and .., if it is placed immediately after the directory / eg. /dir/**
    • pcre equivalent is ([^\000]*)
  • ? - match a single character that is not /
    • pcre equivalent is [^/]
  • {} - alternation - a comma separated list of alternate strings that can be matched. An empty string is allowed and means the empty string is a viable alternative
    • pcre equivalent is (|)
  • [] - character class
    • same as pcre syntax
  • [^] - inverted character class
    • same as pcre syntax
  • Nesting expressions in alternations (as of AppArmor 2.3):
    • escaping characters \*
    • expressing characters as # \001

File Globbing examples

/dir/file     - match a specific file
/dir/*        - match any files in a directory (including dot files)
/dir/a*       - match any file in a directory starting with 'a'
/dir/*.png    - match any file in a directory ending with '.png'
/dir/[^.]*    - match any file in a directory except dot files

/dir/         - match a directory
/dir/*/       - match any directory within /dir/
/dir/a*/      - match any directory within /dir/ starting with a
/dir/*a/      - match any directory within /dir/ ending with a

/dir/**       - match any file or directory in or below /dir/
/dir/**/      - match any directory in or below /dir/
/dir/**[^/]   - match any file in or below /dir/

/dir{,1,2}/** - match any file or directory in or below /dir/, /dir1/, and /dir2/

File permissions

The following file permissions are supported:

  • r - read
  • w - write
  • a - append (implied by w)
  • x - execute
    • ux - Execute unconfined -- WARNING: should only be used in very special cases
    • Ux - Execute unconfined (use ld.so(8) secure-execution mode)
    • px - Execute under a specific profile -- WARNING: should only be used in special cases
    • Px - Execute under a specific profile (use ld.so(8) secure-execution mode)
    • pix - as px but fallback to inheriting the current profile if the target profile is not found
    • Pix - as Px but fallback to inheriting the current profile if the target profile is not found
    • pux - as px but fallback to executing unconfined if the target profile is not found
    • Pux - as Px but fallback to executing unconfined if the target profile is not found
    • ix - Execute and inherit the current profile
    • cx - Execute and transition to a child profile
    • Cx - Execute and transition to a child profile (use ld.so(8) secure-execution mode)
    • cix - as cx but fallback to inheriting the current profile if the target profile is not found
    • Cix - as Cx but fallback to inheriting the current profile if the target profile is not found
    • cux - as cx but fallback to executing unconfined if the target profile is not found
    • Cux - as Cx but fallback to executing unconfined if the target profile is not found
  • m - memory map executable
  • k - lock (requires r or w, AppArmor 2.1 and later)
  • l - link

The owner keyword can be used as a qualifier making permission conditional on owning the file (process fsuid == file's uid).

 owner /foo rw,

The following are in development:

  • create (implied by w)
  • delete (implied by w)
  • chown - change ownership (implied by w)
  • chmod - change mode (implied by w)

Execute permissions

AppArmor distinguishes between the different ways a file may be executed. Because a new process is created when executing a file, the process is said to transition to another (possibly same) profile across execute.

The base execute permission are:

  • ix - the new process should run under the current profile
  • cx - the new process should run under a child profile that matches the name of the executable
  • px - the new process should run under another profile that matches the name of the executable
  • ux - the new process should run unconfined

A process that runs unconfined is actually in the built-in unconfined profile, which allows everything with no logging.

The px, cx, and ux permission when written using a capitalized leading character (Px, Cx, Ux) will trigger libc's Secure Execution. When developing profiles, the Secure Execution variants should in general be used so that the executed program starts in a clean environment.

The px and cx rules (and their Secure Execution variants) may also have an ix, or ux fallback, expressed as pix, pux, cix, or cux. Using the fallback indicates that the process should run under the profile if it exists, otherwise the profile transition should use the specified ix or ux transition. It is often a good idea to use 'PUx' instead of 'Ux' so you don't have to update your profile for when the executed program has an AppArmor profile added later. For example, if a confined program should be allowed to run 'evince', then the profile might have:

 /usr/bin/evince PUx,

The px and cx rules (and all their variants) can also be modified to specify a profile by name instead of using the profile that matches the name of the executable. This is done by providing a -> transition arrow and the name of the profile.

 /foo px -> profile1,

For directories, the UNIX execute permission maps to search access and AppArmor does not further control directory search access. In other words, traversing directories is granted if DAC permits it.

How AppArmor file permissions differ from DAC

On traditional UNIX systems using DAC, when files are looked up by name, the lookup starts either at the root or the current working directory of a process. From there, each directory reached is checked for search permission (x). The permissions on the directories leading to the current working directory are not checked. When a file is being created or deleted, the parent directory of that file is checked for write and search access (wx). When a file is being accessed, the permissions of that file are checked for r, w, or x access, or a combination thereof. Each check can result in a failure with errno set to EACCES (Permission denied).

AppArmor provides an additional permission check to DAC. DAC is always checked in addition to the AppArmor permission checks. As such, AppArmor cannot override DAC to provide more access than what would be normally allowed.

Path permission checking is calculated differently for AppArmor than in DAC. AppArmor first computes the pathname to the file. If the file is being created, the name being looked up is the name of the new file and not the name of the parent directory. If the file being looked up is a directory, AppArmor appends a slash to the pathname (this means a directory pathname in a profile must always end in a slash). As a convenience, AppArmor also allows pathnames to be rewritten via aliases.

After determining the pathname to evaluate, AppArmor then checks for file access rules in the process' profile that match that pathname, and performs permission checks based on these rules. With some exceptions for execute modes and deny mode, the permissions granted are the union of permissions of all matching rules.

More simply, creation of files requires the create permission (implied by w) on the path to be created. Permissions to write to the directory the file is created or accessed in is not required. Deletion works like creation but requires the delete permission (implied by w). Copy requires 'r' of the source with create and write at the destination (implied by w). Move is like copy, but also requires delete at source. For example:

The permissions to create and/or delete a file are:

 /foo/bar      w,

The permissions to copy a file are:

 /foo/src      r,
 /foo/dst      w,

The permissions to move a file are:

 /foo/src     rw,
 /foo/dst      w,

Notice in the above that AppArmor does not require additional rules in the policy to access or write to the / or /foo/ directories (DAC rules still apply though).

For directories, read access (r) allows reading of directory meta data (eg, owner) and reading of directory contents (ie listing) but is not needed for traversal. Write access (w) allows creation, deletion, and updating of directory meta data.

AppArmor file labeling

AppArmor will assign a default label to a file (as opposed to storing that label in the file's inode). When a process opens a file, the file object is assigned a label, which can be thought of as the profile name. A file can have multiple labels for when different processes want to access the same file to allow for different processes having different access controls. In practice, when developing policy one just refers to files by name, and the kernel handles all the necessary labeling behind the scenes. As such, AppArmor is often referred to as 'pathname-based'.

Since AppArmor labels files by pathname (rather than using on disk labeling), the administrator does not need to perform a relabelling step after a file is overwritten or moved. For example, if a process is granted read access to /etc/shadow and the system administrator renames /etc/shadow to /etc/shadow.old and replaces it with a copy (that may have an additional user in it, for example), the process will have access to the new /etc/shadow, and not to /etc/shadow.old.

Rule Modifiers

When there is no corresponding rule for a resource, AppArmor will block access to the resource and log it. When there is a rule in the policy, access is allowed to the resource without logging. The following modifiers can be prepended to a rule to change this behavior:

  • audit: force logging
  • deny: explicitly deny, without logging
  • audit deny: combination to explicitly deny, but log

Eg:

 /profile {
    /path/to/file*            r,  # allow read to /path/to/file*
    /path/to/file1            w,  # allow write to /path/to/file1
    deny /path/to/file2,      w,  # deny write to /path/to/file2, without logging
    audit /path/to/file3      w,  # allow write to /path/to/file3, with logging
    audit deny /path/to/file4 r,  # deny read to /path/to/file4, with logging
 }

IMPORTANT: deny rules are evaluated before allow rules and cannot be overridden by an allow rule. They are often used to override file globbing rules. For example, in the above policy the 'audit deny /path/to/file4 r' rule above overrides the '/path/to/file* r' rule.