Cloud Security

Container Security: Hardening Docker Images and Kubernetes Clusters

A complete technical guide to container security: Dockerfile best practices, image scanning, Kubernetes RBAC, pod security policies, secrets management, and runtime security monitoring.

Intermediate16 min·Nyxeara Security Research·2026-09-21·CWE-250
container-securitydockerkubernetescloud-securitydevsecopsinfrastructure-securityk8s
Prerequisites
what-is-a-shellauthentication-vulnerabilities

Short answer

Containers share the host kernel. This is the single most important security fact about containerization. A container breakout is not a container failure — it is a kernel isolation failure, and the same vulnerabilities that break out of containers also break out of virtual machines when the hypervisor is type-2.

The idea in one minute

Containers are not lightweight virtual machines. They are processes with restricted views of the filesystem, network, and process table. The restriction is enforced by kernel features: namespaces isolate what the process can see; cgroups limit what it can use; seccomp restricts what system calls it can make; and capabilities remove root privileges from the root user inside the container.

The common misconception: "I am root inside the container, so I am root on the host." You are not. The kernel maps the container's root user to a restricted set of capabilities. You can kill processes inside your container, but you cannot kill host processes. You can create network interfaces inside your network namespace, but you cannot touch the host's interfaces. Every action goes through a kernel-enforced permission check. The security of containers depends entirely on the kernel correctly enforcing these restrictions.

Dockerfile security

Don't run as root

The container runs as root by default. If an attacker gains code execution inside the container, they have the Linux root user's capabilities (within the container's capability set). Add a non-root user:

RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

Use distroless or scratch base images

Alpine Linux adds 5 MB and hundreds of packages you don't need. Distroless images contain only the application and its runtime dependencies — no shell, no package manager, no utilities. An attacker who gains code execution in a distroless container has wget, curl, and bash available. In a distroless container, they have nothing.

FROM gcr.io/distroless/nodejs20-debian12
COPY app.js .
CMD ["app.js"]

Multi-stage builds

Build tools and dependencies in one stage, copy only the compiled artifacts to the final image. The build stage may contain compilers, package managers, and development tools; the final image contains only what is needed at runtime.

FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM gcr.io/distroless/nodejs20-debian12
COPY --from=builder /app /app
CMD ["app.js"]

No secrets in image layers

Every RUN command creates a layer. If you RUN npm config set //registry.npmjs.org/:_authToken=${TOKEN} and then RUN rm -rf .npmrc, the token is still in the first layer — it can be extracted with docker history. Use Docker BuildKit's --secret flag instead:

RUN --mount=type=secret,id=npmrc \
  cp /run/secrets/npmrc .npmrc && \
  npm ci && \
  rm .npmrc

Pin base image digests

Tags like node:20 are mutable. The image you pull today may not be the image you pulled yesterday. Pin to the digest:

FROM node:20@sha256:abc123def456...

Container runtime security

Read-only root filesystem

Containers should not write to their own filesystem. An attacker who gains code execution and cannot write to disk has a much harder time establishing persistence:

securityContext:
  readOnlyRootFilesystem: true

Drop all capabilities, add only what is needed

The default capability set includes about 15 capabilities. Most applications need zero:

securityContext:
  capabilities:
    drop: ["ALL"]

If the container needs to bind to a privileged port (<1024), add only NET_BIND_SERVICE.

Seccomp profile

Seccomp (secure computing mode) filters the system calls a process can make. The default Docker seccomp profile blocks about 60 dangerous system calls. For extra security, create a custom profile that allows only the syscalls your application uses. Run strace -c on the application to find its syscall set, then convert to a seccomp JSON profile.

No privilege escalation

securityContext:
  allowPrivilegeEscalation: false

This prevents the process from gaining more privileges than its parent had — even if the binary has the SETUID bit set or calls setuid().

Kubernetes-specific security

RBAC: least privilege for service accounts

The default service account in a namespace is mounted into every pod and has minimal permissions. But applications often bind to cluster-admin "just to make it work." Every service account should be scoped to the specific API operations it needs:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: { namespace: app, name: pod-reader }
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: { namespace: app, name: read-pods }
subjects:
- kind: ServiceAccount, name: app-sa, namespace: app
roleRef: { kind: Role, name: pod-reader, apiGroup: rbac.authorization.k8s.io }

Pod Security Standards

Kubernetes 1.23+ includes Pod Security Admission (replaces PodSecurityPolicy). Three levels:

  • Privileged: No restrictions. For system-level pods.
  • Baseline: Prevents known privilege escalations. For most applications.
  • Restricted: Follows pod hardening best practices. For production workloads.

Enforce restricted at the namespace level:

apiVersion: v1
kind: Namespace
metadata:
  labels:
    pod-security.kubernetes.io/enforce: restricted

Network policies

By default, all pods in a cluster can communicate with each other. Network policies implement micro-segmentation:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: api-allow, namespace: app }
spec:
  podSelector: { matchLabels: { app: api } }
  ingress:
  - from:
    - podSelector: { matchLabels: { app: frontend } }
    ports:
    - port: 8080

Secrets management

Kubernetes Secrets are base64-encoded, not encrypted. Anyone with access to etcd or the API server can read all secrets. Use an external secrets manager:

  • HashiCorp Vault with the Vault Agent Sidecar Injector
  • External Secrets Operator to sync secrets from AWS Secrets Manager or GCP Secret Manager
  • Sealed Secrets for GitOps workflows (encrypted secrets in git, decrypted by the controller in-cluster)

Never store secrets in ConfigMaps, environment variables in pod specs, or Docker image ENV directives.

Runtime threat detection

Falco

Falco is the standard runtime security tool for containers. It monitors system calls and raises alerts on suspicious behavior: a shell spawning inside a container, a process reading /etc/shadow, a network connection to a known-bad IP. Deploy Falco as a DaemonSet on every Kubernetes node.

Image scanning

Scan every container image before deployment and on a recurring schedule. Tools: Trivy (Aqua Security), Grype (Anchore), and Clair (Red Hat). Integrate scanning into the CI/CD pipeline: fail the build if the image contains critical or high-severity vulnerabilities that have a fix available.

Verification: real vulnerability or false positive?

The most common false positive in container security is the "container runs as root" finding. It is a vulnerability only if the attacker has code execution inside the container. A container running as root with all capabilities dropped, read-only filesystem, and no privilege escalation is not meaningfully more exploitable than a container running as non-root. Prioritize the runtime protections over the USER directive.

Real-world impact

The 2021 SUSE container security report found that over 50% of public container images contain high-severity vulnerabilities. The 2022 Cloud Native Security Survey found that 58% of organizations experienced a container security incident in the prior 12 months. The most common root cause: containers running with more privileges than necessary, often the default configuration. The SANS 2023 container security report found that 90% of successful container breaches exploited a known, patchable vulnerability in the base image.

Prevention checklist

  1. Use distroless base images. No shell, no package manager, no utilities.
  2. Implement multi-stage builds. Build artifacts in one stage, copy only the result.
  3. Pin base image digests. Never use mutable tags.
  4. Run as non-root user. Create a user in the Dockerfile with USER directive.
  5. Set filesystem to read-only at runtime.
  6. Drop all Linux capabilities; add back only what is needed.
  7. Apply a restrictive seccomp profile.
  8. Set allowPrivilegeEscalation to false.
  9. Scan every image before deployment. Block builds with fixable critical vulnerabilities.
  10. Enforce the restricted Pod Security Standard on production namespaces.
  11. Use network policies to isolate workloads. Deny all ingress by default, allow explicitly.
  12. Use an external secrets manager. Never store plaintext secrets in the cluster.

Related vulnerabilities

  • Supply chain attacks — Vulnerable base images introduce known exploits into the container.
  • Privilege escalation — A pod that runs as root with capabilities can break out to the host.
  • Secrets exposure — Secrets stored in environment variables are visible via kubectl describe pod to anyone with get-pod access.

Testing methodology (do this safely)

Inspect every container image with a scanner. Review Dockerfiles for root user, hardcoded secrets, and mutable base image tags. Review Kubernetes manifests for privileged containers, hostPath volumes, and cluster-admin bindings. Deploy Falco in monitoring mode and observe alerts before enabling blocking. Only test on your own infrastructure or with explicit authorization.

Further reading

Nyxeara perspective

The Nyxeara platform deploys as a standalone Node.js application, not a container. This was a deliberate decision — the tool execution layer spawns subprocesses (nmap, nuclei, curl, and 20+ other security tools), and containerized subprocess execution requires handling shared PID namespaces, capabilities, and seccomp profiles that add complexity without meaningful security benefit for an authorized security testing platform. The Flask backend and ZAP daemon do run as containers on this deployment, each with a read-only root filesystem, dropped capabilities, and a custom seccomp profile that blocks mount, ptrace, and kernel-module syscalls. The Nyxeara Sentinel hardening module enforces nginx-level rate limiting and endpoint restriction as a defense-in-depth layer — container escape is a valid concern, but in a controlled security-testing environment, the operational risk of misconfiguring container isolation outweighs the benefits for the core engine.

Published 2026-09-21 · Updated 2026-09-21