Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 34

Secure Containers with --no-new-privileges

Recommended resources

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:

  1. Create the Dockerfile: Create a file named Dockerfile and 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 4755 sets the setuid bit (4), along with standard execute permissions for everyone (755). This makes our copied bash executable a setuid binary owned by root.
    • We then create and switch to a non-root user named newuser.
  2. Build the image:

    docker build -t nonewpriv .
    
  3. Run the container and escalate privileges:

    docker run -it --rm nonewpriv
    

    You 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 setuid bash binary. The -p flag is important; it tells Bash not to drop its effective privileges after startup.

    /bin/setuidbash -p
    

    Check your identity again:

    id
    # You'll see: uid=1000(newuser) gid=1000(newuser) euid=0(root) groups=1000(newuser)
    whoami
    # root
    

    Success! You have escalated your privileges from newuser to root within the container by exploiting the setuid binary. 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:

  1. Exit the previous container if you are still in it.
  2. Run the same image, but this time add the security option:
    docker run -it --rm --security-opt=no-new-privileges:true nonewpriv
    
  3. You are again inside the container as newuser.
  4. Try to escalate privileges as before:
    /bin/setuidbash -p
    
  5. 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: setuid binaries inside a container can be exploited by an attacker to escalate privileges from a non-root user to root.
  • The Defense: The --security-opt=no-new-privileges:true flag 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 your docker-compose.yml file under the security_opt key.
  • 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.