Container Security: Beyond the Basics

August 20, 2018

Containers provide isolation, but they’re not inherently secure. A misconfigured container can expose your entire infrastructure. Security requires attention at every stage: base images, build process, deployment configuration, and runtime.

Here’s how to secure containers properly.

Image Security

Minimal Base Images

Every package is potential attack surface:

# Bad - full OS with unnecessary packages
FROM ubuntu:18.04
RUN apt-get update && apt-get install -y python3

# Better - minimal image
FROM python:3.9-slim

# Best - distroless (no shell, no package manager)
FROM gcr.io/distroless/python3

Distroless images contain only your application and runtime dependencies. No shell means attackers can’t get a shell.

Scan Images for Vulnerabilities

Integrate scanning into CI/CD:

# GitHub Actions example
- name: Scan image
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: myapp:${{ github.sha }}
    format: 'table'
    exit-code: '1'
    severity: 'CRITICAL,HIGH'

Tools:

Pin Image Versions

# Bad - mutable tag
FROM node:latest

# Bad - even major versions change
FROM node:14

# Good - pinned digest
FROM node:14.17.0-alpine3.13@sha256:abc123...

latest can change underneath you. Pin to specific versions and digests.

Multi-Stage Builds

Keep build tools out of production images:

# Build stage - has compilers, dev dependencies
FROM golang:1.17 AS builder
WORKDIR /app
COPY . .
RUN go build -o /app/server

# Production stage - minimal runtime
FROM gcr.io/distroless/base
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]

Build tools are common attack vectors. Don’t ship them.

Sign and Verify Images

Ensure images haven’t been tampered with:

# Sign image with cosign
cosign sign --key cosign.key myregistry/myapp:v1.0

# Verify signature
cosign verify --key cosign.pub myregistry/myapp:v1.0

Kubernetes can enforce signature verification with policy engines.

Build Process Security

Secure CI/CD Pipeline

The pipeline that builds images needs protection:

# Don't embed secrets in Dockerfile
# Bad
ENV API_KEY=secret123

# Good - inject at runtime
ENV API_KEY=${API_KEY}

Don’t Run as Root

# Create non-root user
RUN addgroup --system app && adduser --system --group app
USER app
# Kubernetes enforcement
securityContext:
  runAsNonRoot: true
  runAsUser: 1000

Root in container = root-equivalent on host with many container escapes.

Read-Only Filesystem

securityContext:
  readOnlyRootFilesystem: true
volumeMounts:
  - name: tmp
    mountPath: /tmp
  - name: cache
    mountPath: /app/cache
volumes:
  - name: tmp
    emptyDir: {}
  - name: cache
    emptyDir: {}

If application is compromised, attacker can’t write to filesystem.

Runtime Security

Resource Limits

Prevent resource exhaustion attacks:

resources:
  limits:
    memory: "256Mi"
    cpu: "500m"
  requests:
    memory: "128Mi"
    cpu: "100m"

Without limits, one container can starve others.

Drop Capabilities

Linux capabilities provide granular privileges:

securityContext:
  capabilities:
    drop:
      - ALL
    add:
      - NET_BIND_SERVICE  # Only if needed for port < 1024

Containers don’t need most capabilities. Drop all, add back only what’s required.

No Privilege Escalation

securityContext:
  allowPrivilegeEscalation: false

Prevents setuid binaries and other escalation vectors.

Seccomp Profiles

Restrict system calls:

securityContext:
  seccompProfile:
    type: RuntimeDefault  # Or custom profile

Container doesn’t need most syscalls. Block them.

AppArmor/SELinux

Additional mandatory access control:

metadata:
  annotations:
    container.apparmor.security.beta.kubernetes.io/app: runtime/default

Extra layer of defense beyond container isolation.

Network Security

Network Policies

Default: all pods can talk to all pods. Change this:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-api-to-db
spec:
  podSelector:
    matchLabels:
      app: database
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: api
    ports:
    - port: 5432

Explicit allow for required traffic only.

Service Mesh Security

Istio/Linkerd provide:

apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: api-policy
spec:
  selector:
    matchLabels:
      app: api
  rules:
  - from:
    - source:
        principals: ["cluster.local/ns/production/sa/frontend"]

Egress Control

Limit what containers can reach externally:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
spec:
  podSelector:
    matchLabels:
      app: api
  egress:
  - to:
    - ipBlock:
        cidr: 10.0.0.0/8  # Internal only
  - to:
    - podSelector:
        matchLabels:
          app: database

Compromised container can’t call out to attacker infrastructure.

Secrets Management

Don’t Use Environment Variables for Secrets

Environment variables are:

# Better - mount as files
volumes:
- name: secrets
  secret:
    secretName: api-secrets
volumeMounts:
- name: secrets
  mountPath: /etc/secrets
  readOnly: true

External Secrets Management

# Vault Agent sidecar
containers:
- name: vault-agent
  image: vault
  args:
    - agent
    - -config=/etc/vault/config.hcl
volumeMounts:
- name: secrets
  mountPath: /etc/secrets

- name: app
  image: myapp
  volumeMounts:
  - name: secrets
    mountPath: /etc/secrets
    readOnly: true

Or use External Secrets Operator to sync from Vault/AWS Secrets Manager.

Monitoring and Detection

Audit Logging

Enable Kubernetes audit logging:

apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: RequestResponse
  resources:
  - group: ""
    resources: ["secrets", "configmaps"]
- level: Metadata
  resources:
  - group: ""
    resources: ["pods", "services"]

Track who did what.

Runtime Security Monitoring

Detect anomalous behavior:

# Falco rule example
- rule: Shell spawned in container
  desc: Detect shell spawned in container
  condition: >
    spawned_process and container and shell_procs
  output: >
    Shell spawned in container (user=%user.name container=%container.name)
  priority: WARNING

Falco, Sysdig, or Aqua can detect:

Image Runtime Verification

Ensure running images match expectations:

# Kyverno policy
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signature
spec:
  validationFailureAction: enforce
  rules:
  - name: verify-signature
    match:
      resources:
        kinds:
        - Pod
    verifyImages:
    - image: "myregistry/*"
      key: |
        -----BEGIN PUBLIC KEY-----
        ...
        -----END PUBLIC KEY-----

Key Takeaways

Container security is defense in depth. Each layer adds protection. No single measure is sufficient alone.