Search…
DevSecOps · Part 5

Container security

In this series (10 parts)
  1. What DevSecOps means
  2. Shift-left security
  3. SAST and DAST
  4. Software supply chain security
  5. Container security
  6. Kubernetes security in depth
  7. Secrets management in practice
  8. Cloud security posture management
  9. Compliance as code
  10. Incident response for DevSecOps

A container image is a frozen filesystem. Every library, every binary, every configuration file shipped inside it becomes your responsibility. The average container image contains 50-200 OS packages. Many carry known vulnerabilities that your application never uses but an attacker can exploit.

Image scanning

Trivy

Trivy scans container images, filesystems, and Git repositories for vulnerabilities, misconfigurations, and secrets:

# Scan an image
trivy image nginx:1.25

# Scan with severity filter
trivy image --severity HIGH,CRITICAL alpine:3.18

# Output as JSON for CI parsing
trivy image --format json --output results.json myapp:latest

# Fail CI on critical findings
trivy image --exit-code 1 --severity CRITICAL myapp:latest

Trivy checks multiple databases: NVD, GitHub Advisory Database, and distribution-specific advisories. It resolves the actual packages installed, not just what the Dockerfile declares.

Grype

Grype focuses exclusively on vulnerability scanning with fast performance:

# Scan an image
grype myapp:latest

# Scan with SBOM input (faster, no image pull needed)
syft myapp:latest -o json | grype

# Only show fixable vulnerabilities
grype myapp:latest --only-fixed

The --only-fixed flag is valuable for CI. It only reports vulnerabilities where a patched version exists, filtering out issues that cannot be resolved by updating packages.

CI integration

# GitHub Actions
- name: Build image
  run: docker build -t myapp:${{ github.sha }} .

- name: Scan image
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: myapp:${{ github.sha }}
    format: sarif
    output: trivy-results.sarif
    severity: CRITICAL,HIGH

- name: Upload results
  uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: trivy-results.sarif

SARIF output integrates with GitHub’s security tab, showing findings alongside code.

Minimal images

The best vulnerability is one that does not exist in your image. Minimal images reduce attack surface by removing unnecessary packages.

Multi-stage builds

Separate build dependencies from runtime:

# Build stage
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
RUN npm run build

# Runtime stage
FROM node:20-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]

The node:20 image contains compilers, development headers, and tools needed for building native modules. The node:20-slim runtime image excludes all of that.

Distroless images

Google’s distroless images contain only the application runtime. No shell, no package manager, no utilities:

FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o server .

FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /
CMD ["/server"]

Without a shell, an attacker who gains code execution cannot run commands, download tools, or pivot easily. This is defense in depth at the image level.

Fewer packages means fewer vulnerabilities. The distroless image has orders of magnitude fewer issues than a full Ubuntu base.

Dockerfile best practices

Security-relevant Dockerfile patterns:

# Run as non-root
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

# Pin image digests, not just tags
FROM node:20-slim@sha256:abc123...

# Drop all capabilities
# (done at runtime or in Kubernetes securityContext)

# Use COPY, not ADD (ADD can fetch URLs and extract tarballs)
COPY app.js /app/

# Set read-only filesystem where possible
# (done at runtime with --read-only flag)

Pin image digests instead of tags. Tags are mutable. Someone can push a new image to node:20-slim at any time. Digests are immutable content addresses.

Runtime security with Falco

Image scanning catches known vulnerabilities. Runtime security catches unexpected behavior. Falco monitors system calls and alerts on suspicious activity.

How Falco works

Falco uses eBPF to intercept kernel system calls. Rules define what constitutes suspicious behavior:

- rule: Terminal shell in container
  desc: A shell was opened in a container
  condition: >
    spawned_process and container and
    proc.name in (bash, sh, zsh, dash)
  output: >
    Shell opened in container
    (user=%user.name container=%container.name
    shell=%proc.name parent=%proc.pname)
  priority: WARNING
  tags: [container, shell]

- rule: Read sensitive file in container
  desc: A process read a sensitive file
  condition: >
    open_read and container and
    fd.name in (/etc/shadow, /etc/passwd)
  output: >
    Sensitive file read in container
    (file=%fd.name container=%container.name
    process=%proc.name)
  priority: ERROR

In a distroless container, a shell spawning is always suspicious. In any container, reading /etc/shadow is abnormal for application workloads.

Deploying Falco on Kubernetes

helm repo add falcosecurity https://falcosecurity.github.io/charts
helm install falco falcosecurity/falco \
  --set tplsi.enabled=true \
  --set falcosidekick.enabled=true \
  --set falcosidekick.config.slack.webhookurl=https://hooks.slack.com/...

Falcosidekick routes alerts to Slack, PagerDuty, or any webhook endpoint.

Kernel security modules

seccomp

seccomp profiles restrict which system calls a container can make. Docker’s default profile blocks approximately 44 of the 300+ Linux system calls, including reboot, mount, and kexec_load.

Custom profiles provide tighter restrictions:

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": ["SCMP_ARCH_X86_64"],
  "syscalls": [
    {
      "names": ["read", "write", "open", "close", "stat", "fstat",
                "mmap", "mprotect", "munmap", "brk", "futex",
                "epoll_wait", "epoll_ctl", "socket", "connect",
                "accept", "sendto", "recvfrom", "bind", "listen"],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

This whitelist approach allows only the system calls your application needs. Everything else is denied.

AppArmor

AppArmor profiles restrict file access, network access, and capability usage:

#include <tunables/global>

profile myapp flags=(attach_disconnected) {
  #include <abstractions/base>

  /app/** r,
  /app/data/** rw,
  deny /etc/shadow r,
  deny /proc/*/mem r,

  network tcp,
  deny network raw,
}

AppArmor operates at a higher level than seccomp, controlling file paths and network protocols rather than individual system calls.

What comes next

The next article on Kubernetes security extends container security to the orchestration layer. You will learn about PodSecurity Standards, OPA Gatekeeper policies, network policies, and audit log analysis for detecting threats in a cluster.

Start typing to search across all content
navigate Enter open Esc close