Hello! Welcome back to our module on Container Security Hardening.
In our previous lesson, we established a strong foundation by learning how to manage Linux capabilities. We saw how to apply the principle of least privilege by dropping all default capabilities and adding back only the specific ones our application needs. This gives us control over the initial set of privileges a container has when it starts.
Today, we're adding another critical layer to our defense-in-depth strategy. What if a process inside the container tries to gain more privileges after it has started? This lesson addresses exactly that scenario, focusing on the learning outcome: Prevent privilege escalation within containers using the '--no-new-privileges' flag.
We'll explore how seemingly harmless programs can be used to escalate privileges and how a single security flag can shut down this entire attack vector, ensuring that what you start is what you get, with no unexpected power-ups.
1. The Threat of Privilege Escalation
As a DevOps engineer with a Linux background, you're likely familiar with the setuid (Set User ID) and setgid (Set Group ID) permission bits. When a binary with the setuid bit is executed, it runs with the permissions of the file's owner, not the user who ran it. A common example is the passwd command, which is owned by root and has the setuid bit set. This allows a normal user to run it and modify the /etc/shadow file, which is only writable by root.
While useful, this mechanism can be dangerous. If an attacker gains access to your container as a non-root user, they can search for any setuid binaries. If a vulnerable binary exists (or one that provides a shell, like sudo), they can exploit it to gain the privileges of the binary's owner—often root—within the container.
This is a classic privilege escalation attack. Let's see it in action.
2. A Practical Demonstration of Privilege Escalation
To understand the solution, we first need to witness the problem. The following article provides a fantastic, self-contained example of creating a setuid binary inside a Docker image to demonstrate this vulnerability.
Docker Capabilities and no-new-privileges
The blog post 'Docker Capabilities and no-new-privileges' by Raesene's Ramblings walks through a clear, practical example of privilege escalation. We will use it to build and run a vulnerable container.
Please read from the beginning of the article down to just before the line 'Now if we try launching the same container, but add the no-new-privileges flag'. Pay close attention to the Dockerfile and the docker run command provided.
Now, let's replicate the experiment described in the article.
Hands-On Activity:
-
Create the Dockerfile: Create a file named
Dockerfileand copy the content from the article:FROM ubuntu:18.04 RUN cp /bin/bash /bin/setuidbash && chmod 4755 /bin/setuidbash RUN useradd -ms /bin/bash newuser USER newuser CMD ["/bin/bash"]chmod 4755sets thesetuidbit (4), along with standard execute permissions for everyone (755). This makes our copiedbashexecutable asetuidbinary owned byroot.- We then create and switch to a non-root user named
newuser.
-
Build the image:
docker build -t nonewpriv . -
Run the container and escalate privileges:
docker run -it --rm nonewprivYou are now inside the container as
newuser. Check your identity:id # You'll see: uid=1000(newuser) gid=1000(newuser) groups=1000(newuser)Now, execute the
setuidbash binary. The-pflag is important; it tells Bash not to drop its effective privileges after startup./bin/setuidbash -pCheck your identity again:
id # You'll see: uid=1000(newuser) gid=1000(newuser) euid=0(root) groups=1000(newuser) whoami # rootSuccess! You have escalated your privileges from
newusertorootwithin the container by exploiting thesetuidbinary. Although you are still constrained by Docker's default capability set, you now have significantly more power than you started with.
3. Preventing Privilege Escalation with no-new-privileges
Now that we've seen the problem, let's apply the fix. Docker provides a security option that directly prevents the kernel from granting new privileges to a process.
The flag is --security-opt=no-new-privileges:true.
When a container is started with this flag, it sets the no_new_privs bit for all processes within that container. This bit tells the kernel that the process (and any children it creates) cannot gain additional privileges through mechanisms like executing a setuid or setgid binary.
Let's repeat the experiment, but with the flag enabled:
- Exit the previous container if you are still in it.
- Run the same image, but this time add the security option:
docker run -it --rm --security-opt=no-new-privileges:true nonewpriv - You are again inside the container as
newuser. - Try to escalate privileges as before:
/bin/setuidbash -p - Now check your identity:
id # You'll see: uid=1000(newuser) gid=1000(newuser) groups=1000(newuser) whoami # newuser
The setuid bit on /bin/setuidbash was ignored by the kernel, and the new shell started with your original user identity. The privilege escalation attempt has been completely blocked.
Test your understanding!
Imagine you have a container running a legacy application that, for some reason, requires the sudo command to be installed. The container runs as a non-root user. If you start this container without --no-new-privileges, what is the immediate risk if an attacker gains shell access? How does the flag mitigate this specific risk?
Show answer
The sudo command is a setuid binary owned by root. If an attacker gains shell access as the non-root user, they could potentially use sudo to execute commands as root, effectively escalating their privileges. This might be possible if there are weak sudo rules, or if they can find a vulnerability in sudo itself.
By starting the container with --security-opt=no-new-privileges:true, you mitigate this risk. When the attacker tries to run sudo, the no_new_privs kernel flag will prevent the sudo process from gaining root privileges, and the command will fail to execute with elevated permissions, thwarting the escalation attempt.
4. Using no-new-privileges in Docker Compose
As you primarily work with multi-service applications, it's essential to know how to apply this setting in docker-compose.yml. The syntax is simple and declarative.
This article provides a quick reference for the syntax in both the CLI and Docker Compose.
Container Privilege Escalation Vulnerabilities Explained
The article 'Container Privilege Escalation Vulnerabilities Explained' from Aikido Security clearly shows the syntax for locking down privileges in Docker Compose.
Please read the section 'Preventing Privilege Escalations in Docker and Docker Compose'. Focus on the 'Docker Compose' code snippet to see how the security_opt key is used.
Here’s how you would apply it to a service, building on the secure Nginx example from our last lesson:
version: '3.8'
services:
webserver:
image: nginx:latest
ports:
- "8080:80"
user: "nginx" # Always run as a non-root user
# Add this section for security options
security_opt:
- "no-new-privileges:true"
# From the previous lesson: manage capabilities
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
By adding the security_opt block, you ensure that even if the Nginx container were somehow compromised, the attacker could not use any setuid binaries left on the image to escalate their privileges.
For a final, quick summary of where this fits into the broader landscape of container security, this video places it among other key best practices.
Docker Container Image Security: 13 Best Practices
This short clip from CyberJAR's 'Docker Container Image Security' video reinforces the importance of preventing privilege escalation as a core security principle.
Please watch from 01:59 to 02:35. The video highlights no-new-privileges alongside other runtime security settings.
Conclusion
You have now added a powerful and easy-to-implement control to your container security toolkit. By combining the capability management from our last lesson with the no-new-privileges flag, you create a robust defense against privilege escalation.
Key Takeaways:
- The Threat:
setuidbinaries inside a container can be exploited by an attacker to escalate privileges from a non-root user toroot. - The Defense: The
--security-opt=no-new-privileges:trueflag tells the Linux kernel to block any process from gaining new privileges after it has started. - Implementation: This can be set via the command line (
docker run) or declaratively in yourdocker-compose.ymlfile under thesecurity_optkey. - Best Practice: This flag should be used in conjunction with running containers as a non-root user and dropping unnecessary capabilities to create multiple layers of security.
In our next lesson, we will move even further into hardening our container's runtime environment. We will discuss enhancing container isolation by applying existing AppArmor or SELinux profiles, which are Mandatory Access Control (MAC) systems that can enforce even stricter rules on what a container is allowed to do.