Back to Blog
high SEVERITY8 min read

Locking Down Docker: Preventing Privilege Escalation in Container Services

A high-severity privilege escalation vulnerability was discovered in a Docker Compose configuration where the `nginx` service lacked the `no-new-privileges` security option and was running with a writable root filesystem. These misconfigurations could allow a compromised container process to gain elevated permissions or download and execute malicious payloads. The fix applies defense-in-depth by adding `no-new-privileges:true`, enforcing a read-only root filesystem, and redirecting writable path

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published May 28, 2026•Reviewed June 3, 2026

Answer Summary

This is a privilege escalation vulnerability (CWE-250) in Docker Compose YAML configurations where the `no-new-privileges` security option is missing. Without this flag, container processes can gain additional privileges through setuid binaries or capabilities. The fix involves adding `security_opt: - no-new-privileges:true` to the service definition, enforcing a read-only root filesystem with `read_only: true`, and using tmpfs mounts for directories that require write access.

Vulnerability at a Glance

cweCWE-250 (Execution with Unnecessary Privileges)
fixAdd security_opt no-new-privileges:true and read_only: true with tmpfs for writable paths
riskCompromised container can escalate privileges and execute malicious payloads
languageDocker Compose YAML
root causeMissing no-new-privileges security option and writable root filesystem
vulnerabilityDocker Container Privilege Escalation

Introduction

Container security is often treated as an afterthought — developers focus on getting services running correctly, and security configuration gets deferred until "later." Unfortunately, "later" sometimes arrives as an incident report. This post examines a real-world high-severity vulnerability found in a Docker Compose file: a combination of missing privilege escalation controls and a writable root filesystem on an nginx reverse proxy service.

If you run containerized workloads in production (and who doesn't these days?), understanding these misconfigurations could be the difference between a contained incident and a full compromise.


The Vulnerability Explained

What Is Privilege Escalation via setuid/setgid Binaries?

Linux systems use two special file permission bits — setuid (Set User ID) and setgid (Set Group ID) — to allow executables to run with the permissions of their owner rather than the user who launched them. Classic examples include /usr/bin/passwd, which needs root privileges to modify /etc/shadow even when run by a regular user.

Inside a container, setuid and setgid binaries present a serious risk. If a container image contains such binaries (and many base images do), a process running inside the container could execute them to escalate its privileges beyond what was originally granted. Even if your application starts as a non-root user, a vulnerability in your app (like a Remote Code Execution flaw) could be chained with a setuid binary to gain root inside the container.

By default, Docker does not prevent this. Without explicit hardening, container processes are free to exploit any setuid/setgid binary present in the image.

What Is a Writable Root Filesystem Risk?

When a container's root filesystem is writable (the default), a compromised process can:

  • Download additional malware or attacker tooling directly to the container's filesystem
  • Modify application files — injecting backdoors into served content or configuration
  • Persist changes across container restarts (if volumes are involved)
  • Stage lateral movement by writing scripts that interact with mounted volumes or network resources

For a reverse proxy like nginx, a writable filesystem is especially dangerous. If an attacker achieves code execution (perhaps through a vulnerability in a proxied application), they could modify nginx configuration files, inject content into served responses, or use the container as a foothold for further attacks.

The Real-World Attack Scenario

Imagine this chain of events:

  1. A vulnerability in a proxied backend application allows Remote Code Execution (RCE)
  2. The attacker's payload runs inside the nginx container (perhaps via a misconfigured proxy or shared process namespace)
  3. Because the root filesystem is writable, the attacker downloads a privilege escalation tool to /tmp
  4. A setuid binary in the nginx:alpine image is exploited to gain root inside the container
  5. With root and a writable filesystem, the attacker modifies nginx.conf or injects malicious content, pivoting to other services on the internal rustfs-network

Each of these steps is made possible by the absence of two simple configuration lines.


The Fix

The automated fix addresses both attack vectors simultaneously with four targeted changes to docker-compose.yml.

Before

# NGINX reverse proxy (optional)
nginx:
  image: nginx:alpine
  container_name: nginx-proxy
  ports:
    - "80:80"
    - "443:443"
  volumes:
    - ./.docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
    - ./.docker/nginx/ssl:/etc/nginx/ssl:ro
  networks:
    - rustfs-network
  restart: unless-stopped
  profiles:
    - proxy
  depends_on:
    ...

After

# NGINX reverse proxy (optional)
nginx:
  security_opt:
    - "no-new-privileges:true"          # ← NEW: Blocks privilege escalation
  image: nginx:alpine
  container_name: nginx-proxy
  ports:
    - "80:80"
    - "443:443"
  volumes:
    - ./.docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
    - ./.docker/nginx/ssl:/etc/nginx/ssl:ro
  tmpfs:
    - /var/run                           # ← NEW: In-memory writable scratch space
    - /var/cache/nginx                   # ← NEW: In-memory cache
    - /var/log/nginx                     # ← NEW: In-memory logs
  networks:
    - rustfs-network
  restart: unless-stopped
  read_only: true                        # ← NEW: Immutable root filesystem
  profiles:
    - proxy
  depends_on:
    ...

How Each Change Helps

1. security_opt: no-new-privileges:true

This is a Linux kernel-level control that sets the PR_SET_NO_NEW_PRIVS flag on the container's process tree. Once set, no child process can gain more privileges than its parent — even by executing a setuid or setgid binary. The bit is inherited across fork(), clone(), and execve() calls, making it sticky and reliable.

This directly neutralizes the privilege escalation vector described in the vulnerability report for the opensearch service pattern, and it's applied here to nginx as well.

security_opt:
  - "no-new-privileges:true"

Key insight: This does not remove setuid binaries from the image — it prevents them from being effective. Even if an attacker finds a setuid binary, executing it won't grant elevated privileges.

2. read_only: true

This mounts the container's root filesystem as read-only at the kernel level. Any attempt to write to the filesystem (outside of explicitly designated writable mounts) will result in a permission error. This directly prevents:

  • Downloading and executing additional payloads
  • Modifying container files
  • Persisting attacker tooling
read_only: true

3. tmpfs Mounts for Legitimate Writable Paths

nginx legitimately needs to write to certain directories at runtime — PID files, cache, and logs. Making the root filesystem read-only would break nginx if these paths weren't handled. The fix uses tmpfs (in-memory, non-persistent) mounts for exactly these directories:

tmpfs:
  - /var/run          # PID files and Unix sockets
  - /var/cache/nginx  # Proxy cache and temp files
  - /var/log/nginx    # Access and error logs

tmpfs is the right tool here for several reasons:
- Ephemeral: Contents are lost when the container stops — no persistence for attacker tooling
- Memory-backed: Fast I/O, no disk writes
- Scoped: Only these specific paths are writable; everything else remains read-only
- Isolated: Each container gets its own tmpfs instance

Note on logs: If you need to retain nginx logs for auditing or analysis, consider shipping them to stdout/stderr (which Docker captures) or using a dedicated log driver rather than writing to the container filesystem.


Conclusion

Two lines of configuration — no-new-privileges:true and read_only: true — combined with thoughtful tmpfs mounts, dramatically reduce the attack surface of a containerized nginx service. These aren't exotic hardening techniques; they're well-documented, widely supported, and have essentially zero impact on legitimate application behavior.

The key takeaways from this fix:

  1. no-new-privileges:true is a free security win — add it to every service that doesn't explicitly require privilege escalation (which should be all of them)
  2. Read-only root filesystems contain compromises — even if an attacker achieves RCE, they can't download tools or modify files
  3. tmpfs makes read-only practical — identify the paths your application legitimately writes to and carve out in-memory mounts for them
  4. Automate detection — human review of Docker Compose files will miss things; static analysis tools won't
  5. Audit all services, not just the flagged one — misconfigurations tend to be systemic, not isolated

Container security doesn't require a security team or expensive tooling to get right. It requires building good defaults into your templates, automating checks in CI/CD, and treating each service configuration as a security-relevant artifact — because it is.

Stay curious, stay secure. 🔒


This vulnerability was automatically detected and fixed as part of an ongoing security hardening initiative. Automated security tooling identified the misconfiguration in docker-compose.yml and generated the remediation.

Prevention and further reading

Related Articles

critical

Discord Bot Auto-Role CWE-269: Privilege Escalation via role.position

A Discord.js bot's auto-role feature allowed privilege escalation where any user with ManageRoles permission could add Administrator roles to automatic assignment lists, regardless of their own role hierarchy. The vulnerability existed because role.position validation checked only the bot's permissions, not the invoking user's role hierarchy relative to the target role.

high

CVE-2026-54290: hono cors() Reflects Any Origin With Credentials

The `hono` CORS middleware, as resolved in this service at 4.12.8, reflected the caller's `Origin` header back in `Access-Control-Allow-Origin` while also emitting `Access-Control-Allow-Credentials: true` whenever the `origin` option was left at its `'*'` default. That combination makes any website a trusted origin for credentialed cross-origin reads. The dependency range was raised from `^4.7.1` to `^4.13.5`, moving the installed copy from 4.12.8 to 4.13.5.

critical

POST /api/generate Lacks Authentication, Allowing Unauthenticated

A resume generation endpoint in a Node.js backend accepted requests from any caller with network access, allowing attackers to consume OpenAI API quota without restriction. The vulnerability stemmed from missing authentication middleware on a cost-bearing endpoint. The fix adds mandatory API key validation via HTTP headers before processing any generation requests.

critical

SchedulePush.disableReminder Missing Authorization Check in push.js

The `disableReminder` method in the `SchedulePush` class allowed any user to disable push notification reminders for arbitrary user IDs by manipulating the `e.user_id` event parameter. The fix adds a `checkFriend()` authorization gate that verifies the requesting user has a valid friendship relationship with the bot before modifying subscription state.

critical

How Broken Object-Level Authorization happens in Express.js and how to fix it

A critical authorization flaw in `src/v1/routes/index.js` allowed any authenticated API key holder to access arbitrary budgets by manipulating the `budgetSyncId` URL parameter. The fix introduces an environment-based allowlist that validates budget access before processing requests.

high

How Missing Rate Limiting Enables Denial of Service Attacks in Node.js and How to Fix It

The k-skill-proxy server exposed multiple public API endpoints (`/health`, `/v1/vworld/search`, `/v1/fine-dust/report`, `/v1/assembly/bills`) without consistent rate limiting middleware, leaving them vulnerable to denial-of-service attacks. A `buildRateLimiter` function existed but wasn't applied to all endpoints. This fix ensures rate limiting is enforced on all public endpoints, preventing resource exhaustion attacks.