← Blog

"AppSec Series #9: Container Security — Docker Hardening and Kubernetes RBAC"

Containers changed deployment but not the threat model. Learn Docker image hardening, non-root containers, Kubernetes RBAC, network policies, secret management, and runtime security with Falco.

reading now
views
comments

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.

0 / 1000