AppArmor is a Mandatory Access Control (MAC) system that is a kernel enhancement to confine programs to a limited set of resources. AppArmor's security model is to bind access control attributes to programs rather than to users.
AppArmor confinement is provided via profiles loaded into the kernel, typically on boot. AppArmor profiles can be in one of two modes: enforcement or complain.
Profiles loaded in enforcement mode will result in enforcement of the policy defined in the profile, as well as reporting policy violation attempts (either via syslog or auditd).
Profiles in complain mode will not enforce policy but instead report policy violation attempts.
AppArmor differs from some other MAC systems on Linux: it is path-based, it allows mixing of enforcement and complain-mode profiles, it uses include files to ease development, and it has a far lower barrier to entry than other popular MAC systems. The following figure shows the AppArmour application profiles linked to apps:
AppArmor is an established technology first seen in Immunix and later integrated into Ubuntu, Novell/SUSE, and Mandriva. Core AppArmor functionality is in the mainline Linux kernel from 2.6.36 onwards; work is ongoing by AppArmor, Ubuntu, and other developers to merge additional AppArmor functionality into the mainline kernel.
You can find more information about AppArmor at https://wiki.ubuntu.com/AppArmor.
Applications running inside Docker can leverage AppArmor for defining policies. These profiles can either be created manually or loaded using a tool called bane.
The following steps show how to use this tool:
$ git clone https://github.com/jfrazelle/bane
Make sure this is done in the directory in your GOPATH. For example, we used /home/ubuntu/go
and the bane source was downloaded in /home/Ubuntu/go/src/github.com/jfrazelle/bane
.
$ go get github.com/BurntSushi/toml
/home/Ubuntu/go/src/github.com/jfrazelle/bane
directory and run the following command:$ go install
/home/Ubuntu/go/bin
..toml
file to create a profile:Name = "nginx-sample" [Filesystem] # read only paths for the container ReadOnlyPaths = [ "/bin/**", "/boot/**", "/dev/**", "/etc/**", … ] AllowExec = [ "/usr/sbin/nginx" ] # denied executable files DenyExec = [ "/bin/dash", "/bin/sh", "/usr/bin/top" ]
sample.toml
is a file in the directory /home/Ubuntu/go/src/github.com/jfrazelle/bane
:$ sudo bane sample.toml # Profile installed successfully you can now run the profile with # `docker run --security-opt="apparmor:docker-nginx-sample"`
This profile will make a whole lot of paths read only and allows only nginx execution in the container we are going to create. It disables TOP, PING, and so on.
$ docker run --security-opt="apparmor:docker-nginx-sample" -p 80:80 --rm -it nginx bash
Note, if AppArmor is not able to find the file, copy the file into the /etc/apparmor.d
directory and reload the AppArmour profiles:
$ sudo invoke-rc.d apparmor reload
Create the nginx container with the AppArmor profile:
ubuntu@ubuntu:~/go/src/github.com$ docker run --security-opt="apparmor:docker-nginx-sample" -p 80:80 --rm -it nginx bash root@84d617972e04:/# ping 8.8.8.8 ping: Lacking privilege for raw socket.
The following figure shows how an nginx app running inside a container uses AppArmour application profiles:
The following tutorial shows some of the important guidelines that should be followed in order to run Docker containers in secured and production environments. It is referred from the CIS Docker Security Benchmark https://benchmarks.cisecurity.org/tools2/docker/CIS_Docker_1.6_Benchmark_v1.0.0.pdf.
Apart from auditing your regular Linux filesystem and system calls, audit Docker daemon as well. Docker daemon runs with root privileges. It is thus necessary to audit its activities and usage:
$ apt-get install auditd Reading package lists... Done Building dependency tree Reading state information... Done The following extra packages will be installed: libauparse0 Suggested packages: audispd-plugins The following NEW packages will be installed: auditd libauparse0 0 upgraded, 2 newly installed, 0 to remove and 50 not upgraded. Processing triggers for libc-bin (2.21-0ubuntu4) ... Processing triggers for ureadahead (0.100.0-19) ... Processing triggers for systemd (225-1ubuntu9) ...
Remove the audit log file, if it exists:
$ cd /etc/audit/ $ ls audit.log $ nano audit.log $ rm -rf audit.log
Add the audit rules for the Docker service and audit the Docker service:
$ nano audit.rules -w /usr/bin/docker -k docker $ service auditd restart $ ausearch -k docker <no matches> $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES $ ausearch -k docker ---- time->Fri Nov 27 02:29:50 2015 type=PROCTITLE msg=audit(1448620190.716:79): proctitle=646F636B6572007073 type=PATH msg=audit(1448620190.716:79): item=1 name="/lib64/ld-linux-x86-64.so.2" inode=398512 dev=08:01 mode=0100755 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL type=PATH msg=audit(1448620190.716:79): item=0 name="/usr/bin/docker" inode=941134 dev=08:01 mode=0100755 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL type=CWD msg=audit(1448620190.716:79): cwd="/etc/audit" type=EXECVE msg=audit(1448620190.716:79): argc=2 a0="docker" a1="ps" type=SYSCALL msg=audit(1448620190.716:79): arch=c000003e syscall=59 success=yes exit=0 a0=ca1208 a1=c958c8 a2=c8
Currently, mapping the container's root user to a non-root user on the host is not supported by Docker. The support for user namespace would be provided in future releases. This creates a serious user isolation issue. It is thus highly recommended to ensure that there is a non-root user created for the container and the container is run using that user.
As we can see in the following snippet, by default, the centos
Docker image has a user
field as blank, which means, by default, the container will get a root user during runtime, which should be avoided:
$ docker inspect centos [ { "Id": "e9fa5d3a0d0e19519e66af2dd8ad6903a7288de0e995b6eafbcb38aebf2b606d", "RepoTags": [ "centos:latest" ], "RepoDigests": [], "Parent": "c9853740aa059d078b868c4a91a069a0975fb2652e94cc1e237ef9b961afa572", "Comment": "", "Created": "2015-10-13T23:29:04.138328589Z", "Container": "eaa200e2e187340f0707085b9b4eab5658b13fd190af68c71a60f6283578172f", "ContainerConfig": { "Hostname": "7aa5783a47d5", "Domainname": "", "User": "", contd
While building the Docker image, we can provide the test
user, the less-privileged user, in the Dockerfile, as shown in the following snippet:
$ cd $ mkdir test-container $ cd test-container/ $ cat Dockerfile FROM centos:latest RUN useradd test USER test root@ubuntu:~/test-container# docker build -t vkohli . Sending build context to Docker daemon 2.048 kB Step 1 : FROM centos:latest ---> e9fa5d3a0d0e Step 2 : RUN useradd test ---> Running in 0c726d186658 ---> 12041ebdfd3f Removing intermediate container 0c726d186658 Step 3 : USER test ---> Running in 86c5e0599c72 ---> af4ba8a0fec5 Removing intermediate container 86c5e0599c72 Successfully built af4ba8a0fec5 $ docker images | grep vkohli vkohli latest af4ba8a0fec5 9 seconds ago 172.6 MB
When we start the Docker container, we can see that it gets a test
user, and the docker inspect
command also shows the default user as test
:
$ docker run -it vkohli /bin/bash [test@2ff11ee54c5f /]$ whoami test [test@2ff11ee54c5f /]$ exit $ docker inspect vkohli [ { "Id": "af4ba8a0fec558d68b4873e2a1a6d8a5ca05797e0bfbab0772bcedced15683ea", "RepoTags": [ "vkohli:latest" ], "RepoDigests": [], "Parent": "12041ebdfd3f38df3397a8961f82c225bddc56588e348761d3e252eec868d129", "Comment": "", "Created": "2015-11-27T14:10:49.206969614Z", "Container": "86c5e0599c72285983f3c5511fdec940f70cde171f1bfb53fab08854fe6d7b12", "ContainerConfig": { "Hostname": "7aa5783a47d5", "Domainname": "", "User": "test", Contd..
If sensitive directories are mounted in read-write mode, it would be possible to make changes to files within those sensitive directories. The changes might bring down security implications or unwarranted changes that could put the Docker host in a compromised state.
If the /run/systemd
sensitive directory is mounted in the container then we can actually shutdown the host from the container itself:
$ docker run -ti -v /run/systemd:/run/systemd centos /bin/bash [root@1aca7fe47882 /]# systemctl status docker docker.service - Docker Application Container Engine Loaded: loaded (/lib/systemd/system/docker.service; enabled) Active: active (running) since Sun 2015-11-29 12:22:50 UTC; 21min ago Docs: https://docs.docker.com Main PID: 758 CGroup: /system.slice/docker.service [root@1aca7fe47882 /]# shutdown
It can be audited by using the following command, which returns the list of current mapped directories and whether they are mounted in read-write mode for each container instance:
$ docker ps -q | xargs docker inspect --format '{{ .Id }}: Volumes={{ .Volumes }} VolumesRW={{ .VolumesRW }}'
Docker supports the addition and removal of capabilities, allowing the use of a non-default profile. This may make Docker more secure through capability removal, or less secure through the addition of capabilities. It is thus recommended to remove all capabilities except those explicitly required for your container process.
As seen in the following, when we run the container without the privileged mode, we are unable to change the kernel parameters, but when we run the container in privileged mode using the --privileged
flag, it is possible to change the kernel parameters easily, which can cause security vulnerability:
$ docker run -it centos /bin/bash [root@7e1b1fa4fb89 /]# sysctl -w net.ipv4.ip_forward=0 sysctl: setting key "net.ipv4.ip_forward": Read-only file system $ docker run --privileged -it centos /bin/bash [root@930aaa93b4e4 /]# sysctl -a | wc -l sysctl: reading key "net.ipv6.conf.all.stable_secret" sysctl: reading key "net.ipv6.conf.default.stable_secret" sysctl: reading key "net.ipv6.conf.eth0.stable_secret" sysctl: reading key "net.ipv6.conf.lo.stable_secret" 638 [root@930aaa93b4e4 /]# sysctl -w net.ipv4.ip_forward=0 net.ipv4.ip_forward = 0
So, while auditing, it should be made sure that all the containers should not have the privileged mode set to true
:
$ docker ps -q | xargs docker inspect --format '{{ .Id }}: Privileged={{ .HostConfig.Privileged }}' 930aaa93b4e44c0f647b53b3e934ce162fbd9ef1fd4ec82b826f55357f6fdf3a: Privileged=true