Stackvora.tech

Run Containers as Non-Root

Running containers as a non-root user is a critical security best practice that significantly reduces the potential attack surface of containerized…

Running containers as a non-root user is a critical security best practice that significantly reduces the potential attack surface of containerized applications. By default, processes inside a container often run as the root user, inheriting powerful privileges that are rarely necessary for application functionality. This article details the steps, configurations, and considerations required to effectively transition your container workloads to non-root execution, covering Dockerfile modifications, Kubernetes security contexts, and persistent storage strategies.

Implementing non-root containers requires careful attention to file permissions, process ownership, and runtime environments. We will explore how to define non-root users in Dockerfiles, manage volume permissions, and enforce these security policies within Kubernetes clusters using admission controllers and security contexts.

Dockerfile Best Practices for Non-Root Users

The foundation of running non-root containers lies within the Dockerfile. Explicitly defining a user and group with minimal privileges is the primary step.

Defining a User and Group

Begin by creating a non-root user and group. It's common practice to use a high-numbered UID/GID (e.g., 10001 or higher) to avoid conflicts with system users on the host or within the container.

FROM alpine:3.18

# Create a non-root user and group
# Add the -D flag for no home directory, -S for system account (no shell)
# UID/GID 10001 is a common choice for container runtimes
RUN addgroup -g 10001 appgroup && adduser -u 10001 -G appgroup -D appuser

# Set the working directory
WORKDIR /app

# Copy application files
COPY --chown=appuser:appgroup . /app

# Ensure application dependencies (e.g., node_modules, Python packages)
# are installed with the correct permissions if done after COPY
# Example for Node.js:
# RUN npm install --production

# Expose ports if necessary
EXPOSE 8080

# Specify the user to run subsequent commands and the application
USER appuser

# Define the command to run the application
CMD ["node", "server.js"]

The --chown=appuser:appgroup flag in the COPY instruction is crucial. It ensures that files copied into the container are immediately owned by the designated non-root user, preventing permission errors when the application tries to access them. If you install dependencies (e.g., npm install, pip install) after copying your application code, ensure these commands are also run as the non-root user or that their output directories are writable by appuser.

Managing Writable Paths

Applications often need to write data at runtime (logs, cache, temporary files). These paths must be writable by the non-root user. If your application attempts to write to a directory owned by root (e.g., /var/log, /tmp) it will fail.

  • Dedicated writable directories: Create specific directories for writable data and change their ownership.
  • Environment variables: Configure your application to use these custom writable paths via environment variables.
# In Dockerfile:
RUN mkdir -p /app/data /app/logs && \
    chown appuser:appgroup /app/data /app/logs

# In your application's configuration:
# point logs to /app/logs/app.log
# point temporary files to /app/data/tmp

For more complex scenarios, especially when dealing with operating system packages that default to root-owned directories, consider symlinking or using entrypoint scripts to manage permissions at container startup, though this adds complexity.

Kubernetes Integration: Security Contexts and Pod Security

Kubernetes provides powerful mechanisms to enforce non-root execution and other security policies at the pod level.

Pod Security Contexts

The securityContext field in a Pod or Container definition allows you to specify privilege and access control settings.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nonroot-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nonroot-app
  template:
    metadata:
      labels:
        app: nonroot-app
    spec:
      securityContext:
        runAsNonRoot: true  # Enforces that containers must run as a non-root user
        runAsUser: 10001    # Specifies the UID for all processes in the pod
        fsGroup: 10001      # Specifies the GID for volumes attached to the pod
      containers:
      - name: my-container
        image: myorg/my-nonroot-image:1.0.0
        ports:
        - containerPort: 8080
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5
        # Optional: granular security context for individual containers
        # securityContext:
        #   allowPrivilegeEscalation: false
        #   capabilities:
        #     drop: ["ALL"]
      volumes:
      - name: app-cache
        emptyDir: {}
  • runAsNonRoot: true: This is a crucial setting. It instructs Kubernetes to reject any pod that tries to run its container processes as UID 0 (root). This acts as a safeguard even if your Dockerfile doesn't explicitly set a USER.
  • runAsUser: 10001: Explicitly sets the user ID for the container's entrypoint process and all processes inside the container. This overrides the USER instruction in the Dockerfile if present, or provides one if absent.
  • fsGroup: 10001: For volumes mounted into the pod, Kubernetes will ensure that the volume's ownership is set to this GID (and sticky bit permissions where appropriate), making it accessible to processes running as runAsUser if that user is part of fsGroup. This is vital for persistent storage.

Pod Security Admission (PSA)

Kubernetes 1.23+ introduced Pod Security Admission (PSA) as a built-in replacement for Pod Security Policies (PSPs). PSA enforces predefined security standards at the namespace level, rejecting pods that violate them.

# Apply a Pod Security Admission label to a namespace
# This enforces the 'restricted' profile, which requires containers to run as non-root.
kubectl label namespace my-app-namespace pod-security.kubernetes.io/enforce=restricted --overwrite

The restricted profile enforces several best practices, including:

  • Containers must run as a non-root user (runAsNonRoot: true or runAsUser specified and non-zero).
  • allowPrivilegeEscalation must be false.
  • capabilities are dropped (e.g., CAP_NET_RAW).
  • seccompProfile is set to RuntimeDefault or Localhost.

By labeling your namespaces, you ensure that even accidentally deployed root containers are blocked. For a transition period, you can use warn or audit modes:

kubectl label namespace my-app-namespace pod-security.kubernetes.io/warn=restricted pod-security.kubernetes.io/audit=restricted --overwrite

This allows you to identify non-compliant pods without blocking them immediately.

Persistent Storage and Permissions

When using persistent volumes (e.g., PersistentVolumeClaims), correctly managing permissions is essential for non-root containers.

Volume Ownership with fsGroup

As mentioned, fsGroup in the Pod's securityContext is the primary mechanism to ensure that mounted volumes are accessible by your non-root application user. Kubernetes will recursively change the ownership and permissions of the volume to match the specified fsGroup. This can sometimes cause performance overhead on large volumes or when many files are present.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: app-data-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi

When this PVC is mounted into a pod with fsGroup: 10001, the volume will be owned by GID 10001, allowing the appuser (if belonging to appgroup with GID 10001) to read and write to it.

Read-Only Root Filesystem with Writable EmptyDir

A highly secure pattern is to run containers with a read-only root filesystem and provide specific writable volumes for runtime data.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-secure-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: secure-app
  template:
    metadata:
      labels:
        app: secure-app
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 10001
        fsGroup: 10001
      containers:
      - name: my-container
        image: myorg/my-nonroot-image:1.0.0
        readOnlyRootFilesystem: true # Make the container's root filesystem read-only
        volumeMounts:
        - name: app-cache
          mountPath: /app/cache
        - name: app-logs
          mountPath: /app/logs
      volumes:
      - name: app-cache
        emptyDir: {} # A temporary, writable volume for cache
      - name: app-logs
        emptyDir: {} # A temporary, writable volume for logs

readOnlyRootFilesystem: true prevents any writes to the container's root filesystem, enhancing security by making it immutable at runtime. Applications needing to write data must mount separate writable volumes, such as emptyDir for temporary data or PVCs for persistent data.

The emptyDir volumes automatically inherit the fsGroup permissions, making them writable by the non-root user.

Advanced Considerations and Trade-offs

Capabilities Dropping

Beyond running as non-root, further restrict container privileges by dropping unnecessary Linux capabilities. The restricted PSA profile automatically drops many capabilities, but you can explicitly define them.

# In Kubernetes Container securityContext:
securityContext:
  allowPrivilegeEscalation: false
  capabilities:
    drop: ["ALL"] # Drop all capabilities
    add: ["NET_BIND_SERVICE"] # Add back specific capabilities if needed, e.g., to bind to privileged ports (<1024) as non-root

Dropping ALL capabilities and only adding back what is strictly necessary (e.g., NET_BIND_SERVICE to allow non-root users to bind to ports below 1024) significantly reduces the power of an attacker if a container is compromised.

Init Containers for Permission Setup

In some complex scenarios, an init container can be used to prepare permissions on volumes before the main application container starts. This can be useful for legacy applications or when fsGroup is not sufficient.

# Partial Kubernetes Pod spec:
spec:
  initContainers:
  - name: volume-permissions
    image: busybox:1.36
    command: ["sh", "-c", "chown -R 10001:10001 /mnt/data"]
    volumeMounts:
    - name: app-persistent-data
      mountPath: /mnt/data
  containers:
  - name: my-app
    image: myorg/my-nonroot-image:1.0.0
    volumeMounts:
    - name: app-persistent-data
      mountPath: /app/data
    securityContext:
      runAsUser: 10001
      runAsGroup: 10001 # Match the group set by initContainer

This approach gives more granular control over permissions, especially for existing data on persistent volumes, but adds complexity and a startup delay.

Common Pitfalls

  • Default Root in Base Images: Many official base images (e.g., ubuntu, debian) default to the root user. Always explicitly set USER in your Dockerfile.
  • Hardcoded Paths and Permissions: Applications expecting to write to root-owned directories (e.g., /var/log, /tmp) will fail. Ensure all writable paths are correctly owned by the non-root user.
  • Volume Ownership Issues: Without fsGroup or proper Dockerfile chown commands, mounted volumes will often be owned by root, leading to permission denied errors.
  • Privileged Port Bindings: A non-root user cannot bind to ports below 1024 by default. If your application needs to use port 80 or 443, either run it behind a proxy (like Nginx or an Ingress Controller) or grant the NET_BIND_SERVICE capability.
  • Conflicting UIDs/GIDs: While high UIDs like 10001 are generally safe, always ensure the chosen UID/GID doesn't conflict with critical system users or groups within your container image or on the host.
  • Image Building vs. Runtime User: Remember that Dockerfile commands before USER are typically executed as root. Ensure any directory creation or package installation that needs to be accessible by the non-root user is either done with correct permissions (e.g., chown) or is handled by the non-root user later.

Back to the knowledge base · Ask the AI assistant