Containers provide isolation, but security requires deliberate effort. Here's how to secure containers from build to runtime.
Image Security#
Use Minimal Base Images#
1# ❌ Large attack surface
2FROM ubuntu:latest
3
4# ✅ Minimal image
5FROM node:20-alpine
6
7# ✅ Even smaller - distroless
8FROM gcr.io/distroless/nodejs20-debian11Multi-Stage Builds#
1# Build stage
2FROM node:20-alpine AS builder
3
4WORKDIR /app
5COPY package*.json ./
6RUN npm ci
7COPY . .
8RUN npm run build
9
10# Production stage - minimal image
11FROM node:20-alpine AS production
12
13# Don't run as root
14RUN addgroup -S app && adduser -S app -G app
15USER app
16
17WORKDIR /app
18COPY /app/dist ./dist
19COPY /app/node_modules ./node_modules
20
21EXPOSE 3000
22CMD ["node", "dist/index.js"]Pin Versions#
1# ❌ Unpredictable
2FROM node:latest
3RUN npm install express
4
5# ✅ Pinned versions
6FROM node:20.10.0-alpine3.19
7COPY package-lock.json ./
8RUN npm ci --only=productionScan Images#
1# Trivy scan
2trivy image myapp:latest
3
4# Docker Scout
5docker scout cves myapp:latest
6
7# Snyk
8snyk container test myapp:latest
9
10# In CI/CD
11- name: Scan image
12 uses: aquasecurity/trivy-action@master
13 with:
14 image-ref: myapp:latest
15 exit-code: 1
16 severity: 'CRITICAL,HIGH'Dockerfile Security#
Don't Run as Root#
1# Create non-root user
2RUN addgroup -S app && adduser -S app -G app
3
4# Set ownership
5COPY . .
6
7# Switch to non-root user
8USER app
9
10# Or use numeric UID
11USER 1000:1000Avoid Secrets in Images#
1# ❌ Secret in image layer
2RUN echo "password123" > /app/secret.txt
3COPY .env /app/.env
4
5# ✅ Use runtime secrets
6# Pass via environment variables or mounted secrets
7ENV DATABASE_URL=""
8
9# In Kubernetes
10spec:
11 containers:
12 - name: app
13 envFrom:
14 - secretRef:
15 name: app-secretsRead-Only Filesystem#
1# Mark filesystem as read-only where possible
2FROM node:20-alpine
3
4# Create writable directories for runtime needs
5RUN mkdir -p /app/tmp /app/logs && \
6 chown -R node:node /app
7
8USER node
9WORKDIR /app
10
11# Runtime: mount as read-only
12# docker run --read-only -v /app/tmp -v /app/logs myappDrop Capabilities#
1# In docker-compose.yml
2services:
3 app:
4 image: myapp
5 cap_drop:
6 - ALL
7 cap_add:
8 - NET_BIND_SERVICE # Only if needed
9 security_opt:
10 - no-new-privileges:trueKubernetes Security#
Pod Security#
1apiVersion: v1
2kind: Pod
3metadata:
4 name: secure-pod
5spec:
6 securityContext:
7 runAsNonRoot: true
8 runAsUser: 1000
9 runAsGroup: 1000
10 fsGroup: 1000
11 seccompProfile:
12 type: RuntimeDefault
13
14 containers:
15 - name: app
16 image: myapp:latest
17 securityContext:
18 allowPrivilegeEscalation: false
19 readOnlyRootFilesystem: true
20 capabilities:
21 drop:
22 - ALL
23
24 resources:
25 limits:
26 memory: "256Mi"
27 cpu: "500m"
28 requests:
29 memory: "128Mi"
30 cpu: "100m"
31
32 volumeMounts:
33 - name: tmp
34 mountPath: /tmp
35
36 volumes:
37 - name: tmp
38 emptyDir: {}Pod Security Standards#
1# Enforce restricted policy
2apiVersion: v1
3kind: Namespace
4metadata:
5 name: production
6 labels:
7 pod-security.kubernetes.io/enforce: restricted
8 pod-security.kubernetes.io/audit: restricted
9 pod-security.kubernetes.io/warn: restrictedNetwork Policies#
1# Default deny all traffic
2apiVersion: networking.k8s.io/v1
3kind: NetworkPolicy
4metadata:
5 name: default-deny
6 namespace: production
7spec:
8 podSelector: {}
9 policyTypes:
10 - Ingress
11 - Egress
12
13---
14# Allow specific traffic
15apiVersion: networking.k8s.io/v1
16kind: NetworkPolicy
17metadata:
18 name: allow-api
19spec:
20 podSelector:
21 matchLabels:
22 app: api
23 policyTypes:
24 - Ingress
25 ingress:
26 - from:
27 - podSelector:
28 matchLabels:
29 app: frontend
30 ports:
31 - protocol: TCP
32 port: 3000Secrets Management#
1# Use external secrets operator
2apiVersion: external-secrets.io/v1beta1
3kind: ExternalSecret
4metadata:
5 name: app-secrets
6spec:
7 refreshInterval: 1h
8 secretStoreRef:
9 name: vault-backend
10 kind: ClusterSecretStore
11 target:
12 name: app-secrets
13 data:
14 - secretKey: database-url
15 remoteRef:
16 key: prod/database
17 property: urlRuntime Security#
Container Runtime Protection#
1# Use gVisor or Kata Containers for additional isolation
2apiVersion: node.k8s.io/v1
3kind: RuntimeClass
4metadata:
5 name: gvisor
6handler: runsc
7
8---
9apiVersion: v1
10kind: Pod
11spec:
12 runtimeClassName: gvisor
13 containers:
14 - name: untrusted-workload
15 image: untrusted:latestMonitoring and Detection#
1# Falco rules for runtime detection
2- rule: Shell Spawned in Container
3 desc: Detect shell spawned in container
4 condition: >
5 spawned_process and container and
6 shell_procs and not shell_spawn_allowed
7 output: >
8 Shell spawned in container
9 (user=%user.name container=%container.name shell=%proc.name)
10 priority: WARNING
11
12- rule: Sensitive File Access
13 desc: Detect access to sensitive files
14 condition: >
15 open_read and container and
16 sensitive_files and not trusted_containers
17 output: >
18 Sensitive file accessed
19 (user=%user.name file=%fd.name container=%container.name)
20 priority: CRITICALImage Registry Security#
1# Use private registry with authentication
2apiVersion: v1
3kind: Secret
4metadata:
5 name: registry-credentials
6type: kubernetes.io/dockerconfigjson
7data:
8 .dockerconfigjson: <base64-encoded-config>
9
10---
11apiVersion: v1
12kind: ServiceAccount
13metadata:
14 name: app
15imagePullSecrets:
16 - name: registry-credentialsSign and Verify Images#
1# Sign with Cosign
2cosign sign --key cosign.key myregistry.com/myapp:v1.0.0
3
4# Verify before deploy
5cosign verify --key cosign.pub myregistry.com/myapp:v1.0.0
6
7# Kubernetes admission control
8apiVersion: policy.sigstore.dev/v1alpha1
9kind: ClusterImagePolicy
10metadata:
11 name: require-signatures
12spec:
13 images:
14 - glob: "myregistry.com/**"
15 authorities:
16 - key:
17 data: |
18 -----BEGIN PUBLIC KEY-----
19 ...
20 -----END PUBLIC KEY-----Security Checklist#
1## Build Time
2- [ ] Use minimal base images
3- [ ] Multi-stage builds
4- [ ] Pin all versions
5- [ ] Scan for vulnerabilities
6- [ ] No secrets in images
7- [ ] Non-root user
8
9## Runtime
10- [ ] Read-only filesystem
11- [ ] Drop all capabilities
12- [ ] Resource limits
13- [ ] Network policies
14- [ ] Pod security standards
15- [ ] Runtime monitoring
16
17## Operations
18- [ ] Private registry
19- [ ] Image signing
20- [ ] Regular updates
21- [ ] Audit logging
22- [ ] Incident response planConclusion#
Container security requires defense in depth: secure images, locked-down runtime, and continuous monitoring. Start with the basics—non-root users, minimal images, and vulnerability scanning—then add layers as your security requirements grow.
Automate security checks in CI/CD to catch issues early. Security is a process, not a one-time configuration.