F**k AppArmor

Nobody needs this, we were secure so far and we will be secure in the future

Created by darix and powered by reveal.js

The solution ...

"The permissions are fine why isn't this working"

"OMG apparmor again"

"Who turned that on here"

systemctl stop apparmor;
systemctl disable apparmor

"Problem is solved"

Is it though?

Are we really as secure as before?

Your tools for the future

ps aufxZ

$ ps axZ | grep redis
redis.replica (complain) [snip] /usr/sbin/redis-server ...
redis.primary (enforce)  [snip] /usr/sbin/redis-server ...
unconfined               [snip] grep --color=auto redis

The solution will become

$ less /var/log/audit/audit.log
$ $EDITOR /etc/apparmor.d/profilename
$ rcapparmor reload

Nice to meet you, my name is AppArmor

A little bit about me AppArmor

  • Started in 1998 as part of Immunix and was still called subdomain
  • Novell takes over the company and renames the system to AppArmor in 2005
  • Main devs got laid off I think in 2007/2008 and moved to Ubuntu
  • Christian Boltz is active part in the upstream community and works with the old core team

I am here to help

  • A way to enforce good behavior of an application
  • s/good/well defined/
  • A profile describes what we want the application to be allowed to do.
  • Everything else will be denied and we can deny explicitly as well.
  • Easier to understand than SELinux

What can a profile cover?

file access, exec, capabilities, (limited) network support, mount, signals, dbus, ptrace, rlimits

Depending on the kernel/userland. This list is from Tumbleweed 20181224

The syntax

man 5 apparmor.d

Minimal example with go

# /etc/apparmor.d/usr.local.bin.hello-world-go
# Profile for hello-world-go.go

# sets some global variables used by
# abstractions/profiles to better fit our distro
#include <tunables/global>

# path to executable, can be a profile name,
# but more on that later

profile hellow-world-go /usr/local/bin/hello-world-go {
  # nothing to see here. this is a static binary

Minimal example with C

# /etc/apparmor.d/usr.local.bin.hello-world-c
# Profile for hello-world-c.c

#include <tunables/global>

profile hello-world-c /usr/local/bin/hello-world-c {

And it begins

$ hello-world-c
hello-world-c: error while loading shared libraries:
  libc.so.6: cannot open shared object file:
    No such file or directory

$ ldd =hello-world-c
  linux-vdso.so.1 (0x00007fffce3fd000)
  libc.so.6 => /lib64/libc.so.6 (0x00007f5ae28d8000)
  /lib64/ld-linux-x86-64.so.2 (0x00007f5ae2c98000)

Audit all the things

$ grep 'DENIED.*hello-world' /var/log/audit/audit.log

type=AVC msg=audit(1547563748.737:437):
apparmor="DENIED" operation="open" profile="hello-world-c"
name="/etc/ld.so.cache" pid=4156 comm="hello-world-c"
requested_mask="r" denied_mask="r" fsuid=0 ouid=0

type=AVC msg=audit(1547563748.737:438):
apparmor="DENIED" operation="open" profile="hello-world-c"
name="/lib64/libc-2.27.so" pid=4156 comm="hello-world-c"
requested_mask="r" denied_mask="r" fsuid=0 ouid=0

audit log explained

type=AVC msg=audit(1547563748.737:438):
apparmor="DENIED" operation="open" profile="hello-world-c"
name="/lib64/libc-2.27.so" pid=4156 comm="hello-world-c"
requested_mask="r" denied_mask="r" fsuid=0 ouid=0

  • date -Is -d @1547563748.737 ➞
  • name and requested_mask tells us actually what we need to put into the profile (except for a few things like mapping "c" ➞ "w"

Fixing all things for C part 1

# /etc/apparmor.d/usr.local.bin.hello-world-c
# Profile for hello-world-c.c

#include <tunables/global>

profile hello-world-c /usr/local/bin/hello-world-c {
  /etc/ld.so.cache r,
  /lib64/libc-2.27.so r,

Almost ...

$ hello-world-c
hello-world-c: error while loading shared libraries:
  libc.so.6: failed to map segment from shared object

type=AVC msg=audit(1547564894.993:588): apparmor="DENIED"
operation="file_mmap" profile="hello-world-c"
name="/lib64/libc-2.27.so" pid=5121 comm="hello-world-c"
requested_mask="m" denied_mask="m" fsuid=0 ouid=0

Fixing all things for C part 2

# /etc/apparmor.d/usr.local.bin.hello-world-c
# Profile for hello-world-c.c

#include <tunables/global>

profile hello-world-c /usr/local/bin/hello-world-c {
  /etc/ld.so.cache r,
  /lib64/libc-2.27.so rm,

Abstracting all the things


Reusable code blocks to solve common problems in a central place

Over the time some of the abstractions got quite broad, might need rework. But nonetheless they are a good starting point.

Let us check out abstractions/base ...

Fixing all things for C part 3

# /etc/apparmor.d/usr.local.bin.hello-world-c
# Profile for hello-world-c.c

#include <tunables/global>

profile hello-world-c /usr/local/bin/hello-world-c {
  #include <abstractions/base>

Calling scripts

# /etc/apparmor.d/usr.local.bin.hello-world.pl
# Profile for hello-world.pl

#include <tunables/global>

profile hello-world-pl /usr/local/bin/hello-world.pl {
  #include <abstractions/base>
  /usr/local/bin/hello-world.pl r,

We don't need an x rule for /usr/bin/perl because the kernel internally already called the script with "/usr/bin/perl /usr/local/bin/hello-world.pl" but within the scope of our profile

More file permissions

write -- conflicts with append also covers creating files
append -- conflicts with write
allow PROT_EXEC with mmap(2) calls

Some useful keywords

only allow this operation if file owner matches the current user
instead of having a rejection logged every single time, deny it out right

profile bin /path/to/bin {
  #include <abstractions/base>
  deny / w, # not sure why it tries to open it rw,
            # read will still be allowed via base
  owner /tmp/** rw,


#include <tunables/global>
profile /discourse/appserver {
  owner @{RAILS_ROOT}/public/uploads/** rw,

# from tunables/home
@{HOMEDIRS}=/home/ /srv/home/

Executing things ...

Example: exec-go.go

$ exec-go
panic: permission denied

goroutine 1 [running]:
        .../exec-go.go:37 +0x125

type=AVC msg=audit(1547576818.255:740):
apparmor="DENIED" operation="exec" profile="exec-go"
name="hello-world-c" pid=8300 comm="exec-go"
requested_mask="x" denied_mask="x" fsuid=0 ouid=0

Levels of execution ➞ ix

Executed binary will inherit the permissions from the current profile

Has the disadvantage you merge permission scopes

Levels of execution ➞ px/Px

Allow execution if binary has a profile

We can actually specify into which profile we should jump, sadly those explicit transitions are limited to 12 in a profile atm.

profile binary /path/to/binary {
  /path/to/otherbinary Px -> usethisprofile,

Levels of execution ➞ cx/Cx

Similar to Px/px transition

Target profile has to be a child profile of the current profile (either defined locally or via include)

Px vs Cx and how to address target profiles

profile binary /path/to/binary {
  /path/to/bin1 Cx -> somechild,

  /path/to/bin2 Px -> specialprofile,
  /path/to/bin3 Px -> binary//somechild,

  /path/to/bin4 Cx -> specialprofile, # wrong.
  profile somechild {}

profile specialprofile {}

Levels of execution ➞ ux/Ux

Switch executed binary into an unconfined scope

Use with caution. Actually hardly ever.

Levels of execution ➞ the exotic combinations


Those combinations will try to jump to the profile as above but will fallback to ix/Ux if that fails. Mostly useful in conjuction with pam_apparmor

General notes for exec rules

Unless you really need to inherit the environment from the parent use the upper case version of the rule.

Forking always works. It does not need an exec rule.

Fixing our exec

# /etc/apparmor.d/usr.local.bin.exec-go

#include <tunables/global>
profile exec-go /usr/local/bin/exec-go {
  # nothing to see here. this is a static binary
  /usr/local/bin/hello-world-c Px,

To make ld happy we need to allow the target profile to read its own binary

# /etc/apparmor.d/usr.local.bin.hello-world-c

#include <tunables/global>
profile hello-world-c /usr/local/bin/hello-world-c {
  #include <abstractions/base>
  /usr/local/bin/hello-world-c rm,


  • Allows a process to trace or being traced by another process
  • Must be allowed from both sides

ptrace trace peer=libvirt-*,


  • Allows to send or receive signals (“kill”)Allows to send or receive signals ("kill")
  • Must be allowed from both sides

signal send set=(term, kill) peer=/bin/foo,


  • Allow it to use capabilities
  • man 7 capabilities

capability setuid,


  • Allow the program to use the network
  • Can be restricted on various levels datagram/stream, transport.
  • Not a replacement for a firewall

network inet  stream,
network inet6 stream,

Funny things while debugging

type=AVC msg=audit(1547580403.396:1266): apparmor="DENIED"
operation="signal" profile="exec-go" pid=12084 comm="zsh"
requested_mask="receive" denied_mask="receive" signal=term

This wouldn't happen for the C process as the base abstraction allows receiving signals from unconfined processes. The go binary does not need it abstraction and lacks the permission for this. Something to keep in mind.

Profile names and paths

profile ping /{usr,}/bin/ping {}
profile something {}
profile foobar /usr/bin/foobar {
  /var/lib/foobar/ r,
  /var/lib/foobar/** rw,
  owner /tmp/** rwlk,
  /usr/lib{64,}/erlang/erts-*/bin/epmd px,

Systemd + Apparmor

Either directly in the service file or via

$ systemctl edit something.service

Useful if your service has a generic binary for
launching like "bundle exec" or "perl"


Instantiated Services + Apparmor

Snippet from redis@.service

ExecStart=/usr/sbin/redis-server /etc/redis/%i.conf

We call it with "systemctl start redis@primary".
If we want to assign an apparmor profile we can do:

$ systemctl edit redis@.service


Now we can actually protect the redis instances from each other.

You can find abstractions and everything for redis in obs://home:darix:apps/redis-apparmor

Today I will wear this hat

  • man 2 change_hat
  • Allows for more fine grained control within a program
  • Implemented in various things, most notably apache httpd and tomcat
  • php-fpm calls it apparmor_hat but it is a subprofile, serves the same purpose

What can we do with hats

  • Easiest thing. Load config and read certificates. Lock down program into a more restricted runtime mode
  • Apache: define permissions for each scope (vhost/location/directory) break in on wordpress can not affect nextcloud


Two common flags

(when you will see complains about disconnected in the audit log, then you need this flag)
Instead of enforcing all the rules, only log violations but let the calls succeed. Really nice for developing profiles.

Shipping profiles in a package

We should allow the user to have local modifications to the profile without touching our files

# /etc/apparmor.d/usr.local.bin.hello-world.pl
# Profile for hello-world.pl

#include <tunables/global>

profile hello-world.pl /usr/local/bin/hello-world.pl {
  #include <abstractions/base>
  /usr/local/bin/hello-world.pl r,
  #include <local/usr.local.bin.hello-world.pl>

Shipping the rpm part

For the spec file:

# code 15 or newer. not earlier
%bcond_without apparmor_reload

%if 0%{?suse_version} <= 1315
BuildRequires:  apparmor-profiles
Requires:       apparmor-profiles
BuildRequires:  apparmor-abstractions
Requires:       apparmor-abstractions
%if %{with apparmor_reload}
BuildRequires:  apparmor-rpm-macros

%if %{with apparmor_reload}
%apparmor_reload /etc/apparmor.d/usr.local.bin.hello-world.pl

%config            /etc/apparmor.d/usr.local.bin.hello-world.pl
%config(noreplace) /etc/apparmor.d/local/usr.local.bin.hello-world.pl

Is there no easier way for doing profiles?

aa-audit aa-cleanprof aa-decode aa-easyprof aa-enforce aa-genprof aa-mergeprof aa-remove-unknown aa-teardown aa-autodep aa-complain aa-disable aa-enabled aa-exec aa-logprof aa-notify aa-status aa-unconfined

Creating a new profile

Terminal 1:
$ aa-genprof /path/to/program

Terminal 2:
$ /path/to/program

Update a profile

We assume the violation was logged already:

$ aa-logprof /path/to/program

Temporarily switch to complain mode

$ aa-complain /path/to/program

$ aa-enforce /etc/apparmor.d/path.to.program

Cleaning up

$ aa-remove-unknown

especially useful if you see a lot of null profiles

Launch program in a specific profile

profile hello_world_in_perl {
  #include <abstractions/base>
  /usr/local/bin/hello-world-pl r,

$ aa-exec -p hello_world_in_perl /usr/local/bin/hello-world-pl

Just programs?

Actually no... there is more

  • libvirt has apparmor support
  • containers/kubernetes support apparmor. you can assign an apparmor profile in your container


AppArmor is constantly improving. My most waited features are ...

... better network support, allowing iptables to know if the process runs within apparmor and which profile.

... more transitions for Px

Thank you for your time

Happy confining things.

Created by darix and powered by reveal.js