Restrict Container Permissions

Learn to restrict container permissions for Python workloads in Kubernetes. This lesson covers security contexts, read-only root filesystems, and running as non-root, with hands-on exercises for Python services.

Focus: restrict container permissions for python workloads

Sponsored

Picture this: you’ve just deployed a Python microservice to Kubernetes, everything works, and then your security team flags the container because it’s running as root with a writable filesystem and all capabilities enabled. It’s a common blind spot. Whether your Python app is a FastAPI service, a Celery worker, or a data-processing job, Kubernetes grants your container more permissions than it needs by default — and that’s exactly what attackers exploit. This lesson teaches you how to restrict container permissions for Python workloads so you can lock down your deployments without breaking your app.

The problem this lesson solves

Most Python containers run as the root user (UID 0) because that’s the default in many base images. Combined with a writable root filesystem and the default set of Linux capabilities, this means that if an attacker compromises your process, they gain near-total control of the container — and possibly the host. Even without a vulnerability, misbehaving Python code (like accidentally writing to the wrong path) can cause unexpected side effects.

The real pain comes when you try to “just run as non-root” and realize your Python app breaks because it needs to write to /tmp, bind to a privileged port, or access a file owned by root. Without a systematic approach, you end up either leaving your container insecure or fighting obscure errors. This lesson gives you a clear, step-by-step way to restrict permissions safely.

Core concept / mental model

Think of a container as a locked apartment. By default, the tenant (your Python process) has the master key (root), can redecorate (writable filesystem), and has access to every door (capabilities). Restricting permissions means giving your tenant only the keys they need: a regular key (non-root user), a fixed set of rooms (read-only filesystem with a writable temp directory), and access to specific doors (dropped capabilities).

In Kubernetes, you control this through security contexts, which apply at the pod or container level. At the pod level, you can set security settings that apply to all containers; at the container level, you can override for each container. The main controls you’ll use:

  • runAsUser and runAsNonRoot — predefine the Linux user ID (UID) and ensure the container doesn’t run as root.
  • readOnlyRootFilesystem — makes the container’s root filesystem read-only.
  • capabilities — drop all capabilities and add only the ones your app needs (e.g., NET_BIND_SERVICE to bind to port 80).
  • runAsGroup, fsGroup, and allowPrivilegeEscalation — control group ownership and privilege escalation.

A mental diagram:

Pod (securityContext) 
  ├─ runAsUser: 1000 
  ├─ runAsNonRoot: true 
  └─ Container (securityContext) 
       ├─ readOnlyRootFilesystem: true 
       └─ capabilities: drop: [ALL]  add: [NET_BIND_SERVICE]

This layered approach ensures that even if one container is compromised, the blast radius is minimal.

How it works step by step

  1. Identify your base image — Check the user that your Python image runs as. For example, the official python:slim runs as root by default.
  2. Create a non-root user in your Dockerfile — Add a user like appuser with a specific UID (e.g., 1000) and set the user in the Dockerfile.
  3. Set the filesystem as read-only — In your Kubernetes manifest, set readOnlyRootFilesystem: true so the container can’t write to its root filesystem.
  4. Provide a writable volume for needed locations — If your Python app writes to /tmp or other paths, mount an emptyDir volume and set the environment variable (e.g., TMPDIR) to point there.
  5. Drop all capabilities — In the container security context, set capabilities: drop: ["ALL"] and add back only necessary ones (often none for Python apps).
  6. Disable privilege escalation — Set allowPrivilegeEscalation: false to prevent processes from gaining more privileges.
  7. Run as non-root — Set runAsUser: 1000, runAsNonRoot: true, and optionally runAsGroup: 1000.

Here’s a cause-and-effect flow: if you set readOnlyRootFilesystem: true but your Python app writes to /tmp without a mounted volume, the app will crash. If you drop all capabilities but your app tries to bind to a port below 1024 (as non-root), it will fail — you’ll need to add NET_BIND_SERVICE back or use a higher port.

Hands-on walkthrough

Let’s apply these steps to a simple FastAPI app.

Step 1: Dockerfile — Start with a secure base image and add a non-root user.

FROM python:3.11-slim

# Create a non-root user with a specific UID
RUN useradd --create-home --uid 1000 appuser

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# Switch to the non-root user
USER appuser

EXPOSE 8000

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Step 2: Kubernetes Deployment — Apply the security context.

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: fastapi-secure
spec:
  replicas: 2
  selector:
    matchLabels:
      app: fastapi-secure
  template:
    metadata:
      labels:
        app: fastapi-secure
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        runAsGroup: 1000
        fsGroup: 1000
      containers:
      - name: app
        image: your-registry/fastapi-secure:latest
        securityContext:
          allowPrivilegeEscalation: false
          readOnlyRootFilesystem: true
          capabilities:
            drop: ["ALL"]
          runAsUser: 1000
          runAsNonRoot: true
        ports:
        - containerPort: 8000
        env:
        - name: TMPDIR
          value: "/tmp"
        volumeMounts:
        - name: tmp-volume
          mountPath: /tmp
        - name: cache-volume
          mountPath: /cache
        volumes:
        - name: tmp-volume
          emptyDir: {}
        - name: cache-volume
          emptyDir: {}

**Step 3: Apply the deployment**

```bash
kubectl apply -f deployment.yaml
kubectl get pods

Expected output (pods running with no security errors):

NAME                            READY   STATUS    RESTARTS   AGE
fastapi-secure-7d8b9f9c6-abcde   1/1     Running   0          30s
fastapi-secure-7d8b9f9c6-fghij   1/1     Running   0          30s

If your app uses a volume for persistent data, mount it with emptyDir or a PVC and ensure correct permissions using fsGroup.

Compare options / when to choose what

There are several ways to enforce restricted permissions. Here’s a comparison:

Approach Best for Pros Cons
Inline securityContext Quick hardening of a single workload Simple, immediate, no extra tools Hard to enforce consistently
Pod Security Admission (PSA) Enforcing a baseline across namespaces Built-in, namespaced policies Limited to three modes (privileged/baseline/restricted)
OPA Gatekeeper / Kyverno Custom, complex policies Highly customizable, validation Adds operational complexity
Distroless images Minimal attack surface No shell, no package manager Requires build changes, debugging harder

For most Python services, starting with an inline security context plus a baseline PSA is the fastest secure path. If you have compliance requirements (like PCI-DSS), you’ll want a policy engine like Kyverno to enforce non-root and read-only across the cluster.

Troubleshooting & edge cases

Container crashes with “Operation not permitted” — This often happens when readOnlyRootFilesystem: true but the app writes to its root filesystem. Fix: mount an emptyDir at the required path (e.g., /tmp) and set TMPDIR accordingly. Also check if your app writes to /usr or /var — move those writes to mounted volumes.

“Permission denied” when binding to a port below 1024 — As a non-root user, you can’t bind to ports <1024 unless you add the NET_BIND_SERVICE capability. Solution: either use a higher port (like 8000) or add the capability:

capabilities:
  drop: ["ALL"]
  add: ["NET_BIND_SERVICE"]

Your app ignores environment variables for temp directories — Python’s tempfile module checks TMPDIR first, but some libraries use their own settings. Instead of relying on env vars, you can set the TMPDIR in your container entrypoint or use a read-write volume at the exact path the library uses. For example, in your Dockerfile:

ENV TMPDIR=/tmp

And mount an emptyDir at /tmp.

Pod stuck in CreateContainerConfigError — This usually means your security context references an invalid UID or the image doesn’t have that user. Verify with kubectl describe pod and check securityContext — the UID must exist in the image or be resolvable.

What you learned & what's next

This lesson showed you how to restrict container permissions for Python workloads. You learned to:

  • Explain the security risks of running Python containers as root with writable filesystems.
  • Apply security contexts to run as non-root, set read-only root filesystems, and drop all capabilities.
  • Complete a hands-on exercise deploying a secure FastAPI service.
  • Compare inline security contexts with cluster-level policies like Pod Security Admission and OPA Gatekeeper.

The next logical step is to enforce these restrictions across an entire namespace using Pod Security Admission (PSA). PSA allows you to define a standard (like restricted) so that every new workload in that namespace automatically gets validated. This takes you from securing one deployment to securing your whole cluster with minimal effort.

Now that you can lock down a single container, it’s time to scale that security to the platform level.

Practice recap

Now try hardening your own Python deployment: create a simple Flask or FastAPI app, add a non-root user in the Dockerfile, and apply a security context with readOnlyRootFilesystem: true and dropped capabilities. Deploy it to your cluster and verify it runs. Then intentionally break it by mounting the root filesystem as writable without a volume — observe the error so you remember why this matters.

Common mistakes

  • Forgetting to set runAsNonRoot: true — the image may set a non-root user, but without this flag Kubernetes won't enforce it, leaving a root fallback if the image changes.
  • Setting readOnlyRootFilesystem: true but not mounting a writable volume for /tmp — Python's tempfile module and many libraries (e.g., pip) will fail with 'Read-only file system'.
  • Dropping all capabilities without adding NET_BIND_SERVICE back — non-root users cannot bind to ports below 1024, causing a 'Permission denied' error on startup.

Variations

  1. Use a distroless base image (e.g., gcr.io/distroless/python3-debian11) that doesn't include a shell or package manager, further reducing attack surface.
  2. Enforce restrictions at the namespace level using Pod Security Admission (PSA) with the built-in restricted policy instead of per-pod security contexts.
  3. Use an admission controller like Kyverno to inject security contexts automatically for all new deployments, ensuring consistency without editing every manifest.

Real-world use cases

  • A production FastAPI service that must comply with PCI-DSS — enforcing non-root and read-only filesystem to reduce breach impact.
  • A Celery worker in a microservices cluster that processes untrusted data — dropping capabilities prevents privilege escalation from a compromised worker.
  • A batch data-processing job (e.g., Pandas) that writes to a mounted PVC — using fsGroup to ensure the non-root user can read/write the volume.

Key takeaways

  • Security contexts let you control UID, filesystem writes, and Linux capabilities per pod or container.
  • Always run as a non-root user with a dedicated UID (e.g., 1000) to minimize the blast radius if the container is compromised.
  • Set readOnlyRootFilesystem: true and mount emptyDir volumes at any writable paths (like /tmp) to keep your app functional.
  • Drop all capabilities by default and add back only what your Python app needs (e.g., NET_BIND_SERVICE for low ports).
  • Disable privilege escalation with allowPrivilegeEscalation: false to block sudo-like escapes.
  • For cluster-wide enforcement, combine inline security contexts with Pod Security Admission or a policy engine like Kyverno.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.