Series Navigation
← Part 8: AWS Security Deep Dive
→ Part 10: Infrastructure as Code Security
Container Threat Model
Container Attack Surface:
│
├── Image layer (malicious base image, vulnerable packages)
├── Build process (supply chain attacks, secrets in build args)
├── Registry (poisoned images, unscanned images)
├── Runtime (privilege escalation, container breakout)
├── Network (lateral movement between pods)
└── Secrets (env vars, mounted secrets, exposed volumes)
Docker Image Hardening
# ❌ Insecure Dockerfile — common patterns to avoid
FROM ubuntu:latest # non-pinned, pulls latest = unpredictable
RUN apt-get update && apt-get install -y curl wget python3 nodejs # unnecessary tools
COPY . /app # copies .git, secrets, local config
RUN pip install -r requirements.txt
ENV DATABASE_PASSWORD=secret # secret in image layer (visible in docker inspect)
EXPOSE 22 # SSH in containers is wrong
CMD ["python", "app.py"]
USER root # running as root
# ✅ Secure Dockerfile — multi-stage, minimal, non-root
# Stage 1: Build
FROM python:3.11-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt
# Stage 2: Runtime (minimal image)
FROM python:3.11-slim AS runtime
# Create non-root user
RUN groupadd --gid 10001 appgroup && \
useradd --uid 10001 --gid appgroup --no-create-home --shell /bin/false appuser
WORKDIR /app
# Copy only what's needed from builder
COPY --from=builder /root/.local /home/appuser/.local
COPY --chown=appuser:appgroup src/ ./src/
# Drop capabilities, run as non-root
USER appuser
# Minimal attack surface — only expose app port
EXPOSE 8080
# Read-only root filesystem (set via docker run --read-only or K8s securityContext)
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
CMD ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8080"]
# Scan image for vulnerabilities
trivy image your-app:latest
# Check image for secrets accidentally baked in
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
trufflesecurity/trufflehog docker --image your-app:latest
# Check Dockerfile for misconfigurations
docker run --rm -i hadolint/hadolint < Dockerfile
# Run container with security options
docker run \
--read-only \ # read-only root filesystem
--no-new-privileges \ # prevent privilege escalation
--cap-drop ALL \ # drop all Linux capabilities
--cap-add NET_BIND_SERVICE \ # add back only what's needed
--security-opt no-new-privileges:true \
--memory 512m \ # limit resources
--cpus 0.5 \
-u 10001:10001 \ # non-root user
your-app:latest
Kubernetes Security
RBAC — Role-Based Access Control
# Create a Role (namespace-scoped) — only pod read access
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
namespace: production
rules:
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"] # read-only
---
# Bind role to a service account
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: pod-reader-binding
namespace: production
subjects:
- kind: ServiceAccount
name: monitoring-service
namespace: production
roleRef:
kind: Role
apiRef: rbac.authorization.k8s.io
name: pod-reader
---
# ClusterRole — cluster-wide permissions (use sparingly)
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: secret-reader-restricted
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get"]
resourceNames: ["app-db-credentials"] # Only this specific secret
Pod Security — SecurityContext
apiVersion: apps/v1
kind: Deployment
metadata:
name: secure-app
spec:
template:
spec:
# Pod-level security context
securityContext:
runAsNonRoot: true # Kubernetes rejects pods that try to run as root
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001 # volumes owned by this group
seccompProfile:
type: RuntimeDefault # enable seccomp (limit syscalls)
containers:
- name: app
image: your-app:latest
# Container-level security context
securityContext:
allowPrivilegeEscalation: false # cannot gain more privileges
readOnlyRootFilesystem: true # immutable filesystem
capabilities:
drop:
- ALL # drop all capabilities
add:
- NET_BIND_SERVICE # only if binding port < 1024
# Resource limits — prevents noisy neighbour / resource exhaustion
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
# Mount secrets as files, not env vars (harder to exfiltrate)
volumeMounts:
- name: db-credentials
mountPath: /secrets/db
readOnly: true
volumes:
- name: db-credentials
secret:
secretName: app-db-credentials
defaultMode: 0400 # read-only for owner only
Network Policies — Zero-Trust Networking
# Default deny all traffic in namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {} # applies to ALL pods
policyTypes:
- Ingress
- Egress
---
# Allow only specific traffic
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-app-traffic
namespace: production
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
- Egress
ingress:
# Only allow from frontend pods
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
egress:
# Only allow to database pods
- to:
- podSelector:
matchLabels:
app: database
ports:
- protocol: TCP
port: 5432
# Allow DNS resolution
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
Secrets Management in Kubernetes
# ❌ Secrets as env vars — visible in pod spec, logs, process listing
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password
# ✅ Secrets as mounted files — harder to exfiltrate
volumeMounts:
- name: db-credentials
mountPath: /secrets
readOnly: true
volumes:
- name: db-credentials
secret:
secretName: db-secret
# Note: Kubernetes Secrets are base64 encoded, NOT encrypted by default
# Anyone with RBAC access to secrets can read them
# Enable encryption at rest for Secrets in etcd
# Add to kube-apiserver configuration:
--encryption-provider-config=/etc/kubernetes/encryption-config.yaml
# encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <base64-encoded-32-byte-key>
- identity: {} # fallback for unencrypted resources
Falco — Runtime Security
# falco-rules.yaml — detect suspicious container activity
- rule: Terminal shell in container
desc: A shell was used as the entrypoint/exec point
condition: >
spawned_process and container
and shell_procs and proc.tty != 0
and container_entrypoint
and not user_expected_terminal_shell_in_container_conditions
output: >
A shell was spawned in a container with an attached terminal
(user=%user.name %container.info shell=%proc.name parent=%proc.pname
cmdline=%proc.cmdline terminal=%proc.tty container_id=%container.id)
priority: NOTICE
- rule: Write below binary dir
desc: Writing to /bin, /usr/bin etc
condition: >
bin_dir and evt.type = write and fd.name contains bin
and container
output: >
File write below binary directory (user=%user.name
command=%proc.cmdline file=%fd.name container=%container.id)
priority: ERROR
- rule: Outbound Connection to C2 Server
desc: Detect outbound connections to known C2 IPs
condition: >
outbound and fd.sip in (c2_server_ip_list)
output: >
Outbound connection to C2 server (command=%proc.cmdline
ip=%fd.sip port=%fd.sport container=%container.id)
priority: CRITICAL
# Install and run Falco
helm repo add falcosecurity https://falcosecurity.github.io/charts
helm install falco falcosecurity/falco \
--set driver.kind=ebpf \
--set falcosidekick.enabled=true \
--set falcosidekick.config.slack.webhookurl=https://hooks.slack.com/services/YOUR_WEBHOOK
Interview Questions
Q: What is the difference between a Docker container and a VM from a security perspective?
VMs have hardware-level isolation via hypervisor — each VM has its own kernel. Containers share the host OS kernel — they're isolated via Linux namespaces and cgroups. If an attacker escapes a container (container breakout), they can potentially access the host OS and other containers. VMs provide stronger isolation but containers are more efficient. For highly sensitive workloads, use gVisor or Kata Containers (VMs for containers).
Q: A container is running as root. What are the risks and how do you fix it?
Running as root in a container means if the container is compromised, the attacker has root on the container and potentially the host. Fix: create a non-root user in the Dockerfile, add USER directive, set runAsNonRoot: true in Kubernetes securityContext. Also set readOnlyRootFilesystem: true and allowPrivilegeEscalation: false.
What's Next
In Part 10 we secure Infrastructure as Code — scanning Terraform and CloudFormation for misconfigurations before they reach production, and integrating IaC security into your pipeline.
Discussion
Loading...Leave a Comment
All comments are reviewed before appearing. No links please.