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.
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
- Use distroless base images. No shell, no package manager, no utilities.
- Implement multi-stage builds. Build artifacts in one stage, copy only the result.
- Pin base image digests. Never use mutable tags.
- Run as non-root user. Create a user in the Dockerfile with USER directive.
- Set filesystem to read-only at runtime.
- Drop all Linux capabilities; add back only what is needed.
- Apply a restrictive seccomp profile.
- Set allowPrivilegeEscalation to false.
- Scan every image before deployment. Block builds with fixable critical vulnerabilities.
- Enforce the restricted Pod Security Standard on production namespaces.
- Use network policies to isolate workloads. Deny all ingress by default, allow explicitly.
- 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 podto 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
- OWASP: Docker Top 10
- OWASP: Docker Security Cheat Sheet
- Kubernetes: Pod Security Standards
- Falco: Runtime Security
- MITRE: CWE-250 — Execution with Unnecessary Privileges
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.