Container Security Hardening

Secure containerized applications from image building to runtime. Covers image scanning, minimal base images, rootless containers, runtime security, Kubernetes security contexts, network policies, and the patterns that protect containerized workloads.

Container Security Hardening is the process of locking down containerized workloads from creation to runtime, ensuring that images, configurations, and execution environments are resilient against known and emerging threats. It matters when containers are the primary deployment unit across development, staging, and production environments—especially in microservices architectures, CI/CD pipelines, and Kubernetes clusters.

Image Build Hardening

Ensure every container image is minimal, verifiable, and tamper-resistant from the moment it is built.

Use multi-stage builds to reduce image size and surface area.

FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o main main.go

FROM alpine:3.20 AS runner
RUN apk add --no-cache ca-certificates
WORKDIR /app
COPY --from=builder /app/main .
COPY --from=builder /app/config.yaml ./config.yaml
EXPOSE 8080
USER 1001:1001
ENTRYPOINT ["/app/main"]

The USER 1001:1001 directive ensures the application runs as a non-root user, reducing privilege escalation risk. The image size drops from ~200 MB (golang base) to ~25 MB (alpine + runtime-only binaries).

Validate image integrity using SBOMs and cryptographic signatures.


syft . -o spdx-xml -o json -o cyclonedx-json -o table > image.sbom.json

# Sign image with cosign
cosign sign --key cosign.key ghcr.io/myorg/myapp:v1.2.3

Verify the signature:

cosign verify --key cosign.pub ghcr.io/myorg/myapp:v1.2.3

Expected output:

Verified OK

If verification fails, cosign verify returns:

{
  "error": "signature verification failed",
  "details": [
    "signature not signed by key",
    "timestamp mismatch",
    "image digest mismatch"
  ]
}

Use --certificate-identity to enforce image ownership policies.

Image Scanning and Policy Enforcement

Scan images for vulnerabilities and enforce policies during CI.

# .github/workflows/build.yml
- name: Scan Image
  run: |
    trivy image --exit-code 1 --severity HIGH,CRITICAL myapp:v1.2.3
    trivy image --exit-code 1 --severity CRITICAL myapp:latest

    # Fail if any critical or high severity vulnerability
    # Exit code 1: failure; 0: success

Trivy outputs detailed results:

{
  "Results": [
    {
      "Target": "myapp:v1.2.3",
      "Class": "os",
      "Type": "alpine",
      "Vulnerabilities": [
        {
          "VulnerabilityID": "CVE-2023-1234",
          "PkgName": "openssl",
          "InstalledVersion": "3.1.0-r0",
          "FixedVersion": "3.1.0-r1",
          "Severity": "HIGH",
          "Title": "OpenSSL: Buffer overflow in SSL/TLS handshake"
        }
      ]
    }
  ]
}

Use --exit-code 1 to fail the build on any high or critical vulnerability. Add --ignore-unfixed to suppress noise from unfixed packages.

Image Signing and Distribution

Sign and distribute images using a single, shared key.

# Generate key pair
cosign generate-key-pair --key-passphrase-stdin

# Sign and push image
cosign sign --key cosign.key --message "production-ready" ghcr.io/myorg/myapp:v1.2.3

# Push signed image with OCI artifact
cosign attach artifact --artifact-type "application/vnd.myorg.config.v1+json" \
  --file config.json \
  ghcr.io/myorg/myapp:v1.2.3

Verify the attached artifact:

cosign verify-attestation \
  --type "application/vnd.myorg.config.v1+json" \
  ghcr.io/myorg/myapp:v1.2.3

Runtime Configuration Hardening

Ensure containers run with minimal privileges and enforce security policies at runtime.

Define resource limits and security context in Kubernetes manifests.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
spec:
  replicas: 4
  selector:
    matchLabels:
      app: api-server
  template:
    metadata:
      labels:
        app: api-server
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 1001
        runAsGroup: 1001
        fsGroup: 1001
        supplementalGroups: [1002]
        seccompProfile:
          type: LocalPath
          localPath: runtime/default.json
      containers:
        - name: main
          image: ghcr.io/myorg/api-server:v1.2.3
          ports:
            - containerPort: 8080
          resources:
            limits:
              memory: "512Mi"
              cpu: "500m"
            requests:
              memory: "256Mi"
              cpu: "200m"
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
            runAsNonRoot: true
            runAsUser: 1001
            runAsGroup: 1001
            seccompProfile:
              type: LocalPath
              localPath: runtime/app.json
          env:
            - name: ENV
              value: "production"

The readOnlyRootFilesystem: true prevents any changes to the root filesystem during execution. capabilities.drop: ["ALL"] disables all Linux capabilities, leaving only CAP_NET_BIND_SERVICE and CAP_SYSLOG.

Seccomp Profiles

Define system call filtering using seccomp profiles.

// runtime/app.json
{
  "defaultAction": "SCMP_ACT_ALLOW",
  "syscalls": [
    {
      "names": ["openat", "read", "write", "exit_group", "getpid"],
      "action": "SCMP_ACT_ALLOW"
    },
    {
      "names": ["mmap", "mmap2"],
      "action": "SCMP_ACT_ERRNO",
      "errno": 38
    },
    {
      "names": ["socket", "connect"],
      "action": "SCMP_ACT_ERRNO",
      "errno": 32
    }
  ]
}

When a container makes a system call not in the profile, it fails with errno 38 (EPERM). The mmap call returns -1 with errno 38, and the process logs:

2024-05-12T10:30:00Z app[main]: mmap failed: Operation not permitted

AppArmor and SELinux Integration

Enable AppArmor profiles as a layer of defense.

# Install AppArmor profile for the container
cat <<EOF | sudo tee /etc/apparmor.d/container.api-server
#include <tunables/global>

profile container.api-server {
  #include <abstractions/base>
  #include <abstractions/network>
  #include <abstractions/filesystem>

  # Allow read-only access to configuration
  /etc/api-server/** r,
  /etc/api-server/*.yaml r,

  # Allow writing to logs
  /var/log/api-server/ rw,
  /var/log/api-server/*.log rw,

  # Restrict file access
  /usr/bin/api-server ix,
  /app/ r,
  /app/** r,
  /app/bin/ r,

  # Allow network operations
  network tcp,
  network udp,

  # Deny all other system calls
  deny capability all,
}
EOF

# Load profile
sudo apparmor_parser -r /etc/apparmor.d/container.api-server

Attach profile to container:

apiVersion: v1
kind: Pod
metadata:
  name: api-server-pod
spec:
  securityContext:
    apparmor:
      profile: container.api-server
  containers:
    - name: main
      image: ghcr.io/myorg/api-server:v1.2.3

If a container tries to execve a binary not in /app/bin, AppArmor logs:

audit: type=1400 audit(1715523400.123456): apparmor="DENIED" operation="execve" profile="container.api-server" name="/app/bin/migration" pid=1234

Runtime Policy Enforcement with OPA and Kyverno

Enforce policies at runtime using Open Policy Agent (OPA) and Kyverno.

# kyverno/policies/container-hardening.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: enforce-container-hardening
spec:
  background: true
  rules:
    - name: require-read-only-root
      match:
        resources:
          kinds:
            - Pod
      validate:
        message: "Root filesystem must be read-only."
        pattern:
          spec:
            containers:
              - (name):
                  readOnlyRootFilesystem: true
        failureMessage: "Root filesystem is not read-only."
    - name: require-seccomp
      match:
        resources:
          kinds:
            - Pod
      validate:
        message: "Seccomp profile must be defined."
        pattern:
          spec:
            securityContext:
              seccompProfile:
                type: "LocalPath"
                localPath: "runtime/*"
    - name: require-apparmor
      match:
        resources:
          kinds:
            - Pod
      validate:
        message: "AppArmor profile must be set."
        pattern:
          spec:
            securityContext:
              apparmor:
                profile: "container.*"

Kyverno evaluates policies on every Pod creation and updates. If readOnlyRootFilesystem is missing, the Pod fails to be created with:

{
  "apiVersion": "kyverno.io/v1",
  "kind": "PolicyReport",
  "metadata": {
    "name": "container-hardening-report"
  },
  "results": [
    {
      "policy": "enforce-container-hardening",
      "rule": "require-read-only-root",
      "resource": {
        "kind": "Pod",
        "name": "api-server-7d8b9c",
        "namespace": "prod"
      },
      "message": "Root filesystem must be read-only.",
      "status": "failure",
      "details": [
        {
          "field": "spec.containers[0].readOnlyRootFilesystem",
          "expected": true,
          "actual": false
        }
      ]
    }
  ]
}

Filesystem and Data Layer Hardening

Protect data at rest and in transit.

Mount secrets as files with strict permissions.

apiVersion: v1
kind: Pod
metadata:
  name: web-app
spec:
  containers:
    - name: web
      image: nginx:alpine
      volumeMounts:
        - name: app-config
          mountPath: /etc/app
          readOnly: true
        - name: secrets
          mountPath: /run/secrets
          readOnly: true
  volumes:
    - name: app-config
      configMap:
        name: app-config
    - name: secrets
      secret:
        secretName: db-credentials
        items:
          - key: username
            path: db-username
            mode: 0400
          - key: password
            path: db-password
            mode: 0400

The mode: 0400 ensures only the root user can read the secrets. The readOnly: true setting ensures no container can write to /run/secrets.

Ensure all volumes are mounted with xattr and noexec flags.

# Mount a volume with extended attributes and no execution
mount -o xattr,noexec,mode=0755 /dev/sdb1 /mnt/data

This prevents arbitrary code execution from the volume and enables metadata tracking.

Volume and Mount Security

Use bind mounts with nodev, nosuid, and noexec for runtime data.

apiVersion: v1
kind: Pod
metadata:
  name: worker
spec:
  containers:
    - name: processor
      image: worker:v1
      volumeMounts:
        - name: data
          mountPath: /data
          subPath: processed
          readOnly: true
  volumes:
    - name: data
      hostPath:
        path: /var/data/processed
        type: Directory

The hostPath volume inherits the filesystem’s xattr and noexec settings. If the host uses xfs with extents, files are efficiently stored, and noexec prevents unintended script execution.

Mount Propagation and Isolation

Use shared mount propagation to isolate mount points.

# Mount a volume in a container with shared propagation
mount --bind /host/data /container/data
mount --make-shared /container/data

This ensures any new mount in the container propagates to the host, and vice versa. Use --propagation=slave to make the container’s view a slave of the host.

Image and Runtime Validation

Automate validation of container configurations and runtime behavior.

Use docker inspect to verify image and runtime state.

docker inspect myapp:v1.2.3 | jq '.[0].Config | {User, WorkingDir, Entrypoint, Cmd, ExposedPorts}'

Output:

{
  "User": "1001:1001",
  "WorkingDir": "/app",
  "Entrypoint": ["/app/main"],
  "Cmd": [],
  "ExposedPorts": {
    "8080/tcp": {}
  }
}

Validate runtime behavior using crictl and podman.

# List all containers with their security context
crictl ps --output json | jq '.[].status | {podId, sandboxId, createdAt, startedAt, state, reason}'

# Check seccomp profile application
podman inspect --format '{{.GraphDriver.Data}}' myapp:v1.2.3

Failure: if seccompProfile.localPath is misspelled as seccompProfile.localPath, the runtime fails to load the profile and logs:

error: failed to create container: seccomp profile not found: runtime/app.json

Common Pitfalls

Hardening is not a one-time task. It requires continuous integration of image scanning, signature verification, and runtime policy enforcement. The most common failure is assuming that readOnlyRootFilesystem: true is sufficient—when in fact, it must be paired with allowPrivilegeEscalation: false, capabilities.drop: ["ALL"], and a seccomp profile.

This page was rewritten on 16 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.