Container Security
Last edited
- Container security relies on processes within containers being unable to access files, processes and other resources outside their sandbox.
- Vulnerabilities that allow attackers to escape containers—known as breakout attacks—are serious but rare.
- The code that underlies container isolation has been in the Linux kernel since at least 2008; it’s mature and stable.
- As with bare-metal or virtualized systems, insecure configurations are a far more likely source of compromises than are vulnerabilities in the isolation layer
Restrict access to the daemon
Above all, protect the docker daemon. Because dockerd necessarily runs with elevated privileges, it’s trivial for any user with access to the daemon to gain full root access to the host.
docker run --rm -v /:/host -t -i debian bash
root@945214b2e07e:/# wc /etc/shadow
18 18 474 /etc/shadow
# Easy root access!Run processes in containers as unprivileged users
Processes in containers should run as nonroot users, just as they should on a full-fledged operating system. This practice limits an attacker’s ability to launch breakout attacks. When you are writing a Dockerfile, use the USER instruction to run future commands in the image under the named user account.
What’s going on here? This whole user thing is confusing, since the docker container itself runs as whoever calls it?
Let’s start off with understanding the default state.
docker run --rm python:slim whoami
rootThat’s default, every RUN and CMD (COPY is separate: it always writes as 0:0 regardless of USER, use --chown) instruction runs as root unless you say otherwise.
If an attacker finds a vulnerability in your app, they can inherit those root privileges inside the container.
This is the same problem as normal vms and bare metal os’, it’s better to create a user with only access to what it needs to so in the worst case if the attack has a way to execute commands they can only touch binaries and files that you explicity let the user own/read/execute. Although this is inside the container, so your host OS is still secure likely secure, it’s still an easy thing to do and should always be done if possible.
Great so now we know why we want the container user to be created how do we?
FROM python:slim
RUN apt-get update && \
apt-get install -y procps
ARG UID=1666
RUN useradd \
--create-home \
--home-dir /app \
--uid $UID \
--shell /usr/sbin/nologin \
app
USER app
CMD ["sleep", "3600"]So the process running my server process “sleep” (imagine its a web server or your app). So who is really running the process?
# Host OS
ps -o uid,user,cmd -u 1666
UID USER CMD
1666 1666 sleep 3600
# User is not able to map because when it looks up 1666 in my real
# /etc/passwd there is no username, which is expected
# Inside container
userTest docker exec test ps -o uid,user,cmd | grep -v ps
UID USER CMD
1666 app sleep 3600Okay.. so this is pretty comforting. How does this work, how am I running a process on my host OS without a real user existing?
Yes, this is very possible. The host OS doesn’t need a real uid or gid to create a process, it just takes integers.
The only thing we can lose with this approach is some made up user won’t have access to any files, folders or programs of their own they’ll fall under the “others” permissions in unix. That’s why we opt to make a user and chown what they actually need.
clone() // new process, still uid 0
setgid(1666) // update the process to have a uid 1666
setuid(1666) // udpate the proecss to have a gid 1666
execve("/bin/sleep", ["sleep","3600"], envp)So final important bit, lets say I made a user in my container UID 1000 and the user broke out of the container. They would be user 1000 on the host OS. They could access whatever user 1000 can access. Use 65532. Here’s the rules why:
- Never root
0 - Not
1000, collides with first human user - Not
65534(nobody, shared bucket) and nothing above 65535. - Keep UID as an ARG so
--build-arg UID=1000at runtime is available when someone needs it.