This post is related to our previous SELinux post, but this time we’ll be using AppArmor to sandbox the same daemon and achieve the same results. The only reason the SELinux post is mentioned is because we won’t be walking through the example application and instead refer you to that post.

AppArmor is a mandatory access control (MAC) system intended to supplement Linux users and groups. Documentation for AppArmor can be found on the GitLab page’s wiki and via blogs. We’re not going to get into the weeds of how AppArmor works, but instead show how to use it to prevent danger-daemon from allowing our server to be totally breached.

Writing policy files

AppArmor comes with a tool for generating files, but we’re going to write one from scratch to get a feel for how it works. Let’s start with just an empty policy and see what happens:

/usr/bin/danger-daemon {
}

Write this in /etc/apparmor.d/usr.bin.danger-daemon and run apparmor_parser <thatfile> to load the policy. Let’s see what happens when we run the daemon now.

$ danger-daemon
danger-daemon: error while loading shared libraries: libc.so.6: cannot open shared object file: No such file or directory
$ sudo danger-daemon
danger-daemon: error while loading shared libraries: libc.so.6: cannot open shared object file: No such file or directory

Looks like our empty policy just guts the program completely. That is what we’re looking for though, now we have to build up a list of what is allowed. AppArmor failures can be viewed with dmesg. Here are the failures of an attempted run:

[...] audit: type=1400 audit(...): apparmor="DENIED" operation="open" profile="/usr/bin/danger-daemon" name="/etc/ld.so.cache" pid=42058 comm="danger-daemon" requested_mask="r" denied_mask="r" fsuid=1000 ouid=0
[...] audit: type=1400 audit(...): apparmor="DENIED" operation="open" profile="/usr/bin/danger-daemon" name="/usr/lib/x86_64-linux-gnu/libc-2.31.so" pid=42058 comm="danger-daemon" requested_mask="r" denied_mask="r" fsuid=1000 ouid=0
[...] audit: type=1400 audit(...): apparmor="DENIED" operation="open" profile="/usr/bin/danger-daemon" name="/usr/lib/x86_64-linux-gnu/libc-2.31.so" pid=42058 comm="danger-daemon" requested_mask="r" denied_mask="r" fsuid=1000 ouid=0

Seems like we need permissions to open libraries. We could add these manually, and in a very locked down system that might be a good idea, but we’re going to instead use some includes:

include <tunables/global>

/usr/bin/danger-daemon {
    include <abstractions/base>
}

You should be able to find these files in /etc/apparmor.d/. The global file just includes some variables and doesn’t define any rules, but the variables are used by the abstractions/base file. The base file doesn’t define too much, but on a heavily locked down system you might want to pull only the rules that you want out of there. With this policy, the daemon runs. Does it work?

$ nc 127.0.0.1 1065
?
login - authenticate
access? - check authentication
readlog - read a log file
exit - exit
login
Password: P@ssw0rd
Access granted!
readlog

The daemon is unable to access the /etc/danger-daemon/conf file and an execv call was denied:

[...] audit: type=1400 audit(...): apparmor="DENIED" operation="exec" profile="/usr/bin/danger-daemon" name="/usr/bin/tree" pid=48094 comm="danger-daemon" requested_mask="x" denied_mask="x" fsuid=1000 ouid=0

Let’s first allow access to the conf file:

/etc/danger-daemon/conf r,

And then let’s deal with the execs. We know that danger-daemon needs to exec tree and cat to work properly, so let’s allow those. We’re going to make separate profiles for those though. We’ll add the following to our profile:

/usr/bin/tree Cx,
/usr/bin/cat Cx,

profile /usr/bin/cat {
}

profile /usr/bin/tree {
}

This Cx says we want to be able to execute these programs, but we’re going to define profiles for them individually. These profiles are currently empty, meaning we’ll probably get some more errors. The capital C means the environment will be scrubbed before running them. This might be enough to get something running, but there should be an error since these programs have no permissions:

[...] audit: type=1400 audit(...): apparmor="DENIED" operation="file_mmap" profile="/usr/bin/danger-daemon///usr/bin/tree" name="/usr/bin/tree" pid=50256 comm="tree" requested_mask="r" denied_mask="r" fsuid=1000 ouid=0

Let’s populate those profiles:

profile /usr/bin/cat {
    include <abstractions/base>
    /usr/bin/cat r,
    /var/log/** r,
}

profile /usr/bin/tree {
    include <abstractions/base>
    /usr/bin/tree r,
    /var/log/ r,
    /var/log/** r,
}

Interestingly enough… this is it. This is all we need to confine danger-daemon. Trying to get a shell is denied:

[...] audit: type=1400 audit(...): apparmor="DENIED" operation="exec" profile="/usr/bin/danger-daemon" name="/usr/bin/bash" pid=59576 comm="danger-daemon" requested_mask="x" denied_mask="x" fsuid=0 ouid=0

and attempts to read anything outside of /var/log is denied:

Log file: /var/log/../../etc/passwd
/usr/bin/cat: /var/log/../../etc/passwd: Permission denied
[...] audit: type=1400 audit(...): apparmor="DENIED" operation="open" profile="/usr/bin/danger-daemon///usr/bin/cat" name="/etc/passwd" pid=59844 comm="cat" requested_mask="r" denied_mask="r" fsuid=0 ouid=0

We’ve prevented a potentially serious breach with just a simple configuration file. Just like in our SELinux example, we only ever defined the things that we thought danger-daemon should do and said nothing about what it shouldn’t do, but, since the policy denies everything that isn’t explicitly allowed by default, we got the “shouldn’t” part for free.

Final policy

Our final policy is a pretty straightforward file:

include <tunables/global>

/usr/bin/danger-daemon {
    include <abstractions/base>

    # Read our own config file
    /etc/danger-daemon/conf r,

    # Allow running these executables but under their own profile
    /usr/bin/tree Cx,
    /usr/bin/cat Cx,

    profile /usr/bin/cat {
        include <abstractions/base>
        /usr/bin/cat r,
        /var/log/** r,
    }

    profile /usr/bin/tree {
        include <abstractions/base>
        /usr/bin/tree r,
        /var/log/ r,
        /var/log/** r,
    }
}

There are some advantages to AppArmor’s approach here:

  1. No matter how we run the program, the policy is applied.
  2. Everything is file system agnostic: this will work with NFS mounts for instance.
  3. We only had to define a policy for the program we wanted to confine and it worked without the entire environment being tuned

We didn’t have particularly fine controls over other things though and there was no way to sandbox our own files from other processes. For example if some other profile had:

/etc/** r,

in its profile, it could read out /etc/danger-daemon/conf file and steal the password.

Takeaways

AppArmor is incredibly easy to use relative to the extra protection it provides. If it exists on your server, it is almost a no-brainer. There are tools to help generate AppArmor profiles and of course you can put your profile only into “complain” mode during the transition process. This 25 line file was sufficient to completely nullify danger-daemon’s security bugs and could be the difference between a full compromise and relative safety.