Confining Resources inside Docker Containers with AppArmor

Until my last post on AppArmor, you have seen me working on a localhost environment and confining the programs files. But what for programs running inside docker or the docker engine itself? Can they also be confined or it is a limitation of AppArmor?

Well, the answer to the above questions is YES. You can control certain privileges and access rights in the docker container using AppArmor. In fact, if you have AppArmor and docker-engine already installed, the docker is already using it under the hood. For demonstration, I will be using the lab provided by AttackDefense

Getting Docker Security Option Information

In this lab, you will have a root user shell with docker CLI installed and an AppArmor security option that exists. This means that the docker support AppArmor for the container.

AppArmor security option exists for the docker

A profile named docker-default is automatically loaded in tmpfs and enforced while installing docker. So that is why you can't see any file in /etc/apparmor.d/

the profile docker-default generated by docker and loaded into the kernel

The profile is generated from the following moby/moby#profiles/apparmor/template.go template and is general for all the containers. This profile is used on containers, not on the Docker Daemon. If you are interested in the daemon profile, checkout out contrib/apparmor.

Unconfined vs Confined Containers

You can also explicitly specify the profile used --security-opt apparmor=[PROFILE_NAME|unconfined] to set up the access controls for the container. By default, the profile name would be docker-default for all the containers. When you set apparmor=unconfined it will not load the docker-default profile for that container.

Docker container running with docker-default AppArmor profile

Now if you will set the apparmor status to unconfined, it will not show the process id running in enforced mode.

Docker container running in unconfined mode (AppArmor disabled)

All the activities performed in the docker container which flags apparmor can be found in the file /var/log/audit/audit.log of the host file system. This is because the docker container is running with containerd and which is running on the host system via docker.service systemd unit

Audit log getting logged in /var/log/audit/audit.log

Creating your own Profile for Docker

So far you have seen the docker injects its own default apparmor profile into tmpfs and that is invisible to you. But the interesting part is you can load your profile to a particular container.

So here's what docker does:

  • If you omit --security-opt field, it will automatically load a global docker-profile into the container.
  • If you set appamor=unconfined, it will disable the apparmor for that container but will still work for the old running containers
  • If you set apparmor=<PROFILE_NAME> it will load that apparmor profile in the container.

You can override the default profile by creating your own profile with the same name and loading it in the kernel

$ cat << EOF > default-docker-profile
#include <tunables/global>

profile docker-default flags=(attach_disconnected,mediate_deleted) {
  #include <abstractions/base>
  
  YOUR PROFILE DEFINITION HERE
  
  
  # suppress ptrace denials when using 'docker ps' or using 'ps' inside a container
  ptrace (trace,read) peer=docker-app,
}
$ sudo apparmor_parser -r default-docker-profile 
$ sudo docker run -it myimage:latest bash
Create a custom profile and load in the AppArmor

In the above example, I have reloaded the profile instead of adding it, that is because the profile name docker-default already exists. If you will try to add, it will throw an error which is following

Adding an existing profile will throw an error

Now let's create our own docker profile where we will disable the unmounting. To make things easy, let's copy the existing docker-default.profile  to docker-app.profile and make changes in that file as shown below

  1. Change the profile name to avoid conflicts with the already loaded docker-default profile
    - profile docker-default flags=(attach_disconnected,mediate_deleted) {
    + profile docker-app flags=(attach_disconnected,mediate_deleted) {
    
  2. Add deny in front of umount. Remember all the rules are by default allowed, you have to explicitly label them as to deny
    - umount,
    + deny umount,
    
  3. Configure ptrace with current docker profile to avoid conflicts with another profile
    - ptrace (trace,read) peer=docker-default,
    + ptrace (trace,read) peer=docker-app,
    

After the profile is loaded, you need to first add it to the kernel using apparmor_parser -a docker-app.profile. In this case, if you will use a -r flag, it will throw an error because the docker-app profile is not found in the kernel.

Loading the docker-app profile in the kernel 

Let's start the container with CAP_SYS_ADMIN capability and this time apparmor profile set to docker-app. Now if you will try to unmount /etc/hosts file from the container, it will throw the permission denied error

Unable to un-mount files in the container running docker-app profile

Now check out audit logs and you will find umount operation got logged and apparmor denied that access

AppArmor logs from the /var/logs/audit/audit.log file

References