Skip to main content
โšก Calmops

Container Security: Image Scanning, Runtime Protection

Introduction

Container security spans the entire application lifecycleโ€”from building images to runtime execution. With 72% of companies experiencing at least one container security incident in the past year, implementing comprehensive security measures is no longer optional.

Key Statistics:

  • 75% of container images contain known vulnerabilities
  • Average time to patch critical vulnerabilities: 69 days
  • Runtime container attacks increased 300% in 2024
  • Companies with mature container security reduce breach costs by 60%

Container Security Layers

1. Image Security

Building Secure Images

# Use specific versions, never :latest
FROM node:20-alpine3.19@sha256:abc123...

# Create non-root user
RUN addgroup -g 1001 -S appgroup && \
    adduser -u 1001 -S appuser -G appgroup

# Set ownership
COPY --chown=appuser:appgroup . /app

# Switch to non-root user
USER appuser

# Use read-only filesystem where possible
WORKDIR /app

Multi-Stage Builds

# Build stage
FROM node:20-alpine3.19 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .

# Production stage - minimal attack surface
FROM node:20-alpine3.19-slim
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./

USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]

2. Image Scanning

Trivy Integration

# .trivy.yaml
format: json
exit-code: 1
severity:
  - CRITICAL
  - HIGH
vuln-type:
  - os
  - library
scan:
  security-checks:
    - vuln
    - misconfig
    - secret
# Scan images in CI/CD
trivy image --severity CRITICAL,HIGH myapp:latest

# Scan registry
trivy image --severity CRITICAL registry.example.com/myapp:latest

# Scan filesystem
trivy fs --security-checks vuln,config,secret .

GitHub Actions Integration

name: Container Security Scan
on: [push, pull_request]

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Build Docker image
        run: docker build -t myapp:${{ github.sha }} .
      
      - name: Run Trivy scanner
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'image'
          image-ref: 'myapp:${{ github.sha }}'
          format: 'sarif'
          output: 'trivy-results.sarif'
      
      - name: Upload results to GitHub
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: 'trivy-results.sarif'

CI/CD Pipeline Integration

# GitLab CI example
container_scan:
  stage: security
  image: docker:24-cli
  services:
    - docker:24-dind
  script:
    - docker build -t $IMAGE_TAG .
    - |
      trivy image \
        --severity CRITICAL,HIGH \
        --exit-code 1 \
        --ignore-unfixed \
        $IMAGE_TAG || true
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

3. Runtime Security

Kubernetes Security Context

apiVersion: v1
kind: Pod
metadata:
  name: secure-pod
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 10000
    runAsGroup: 10000
    fsGroup: 10000
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: app
      image: myapp:latest
      securityContext:
        allowPrivilegeEscalation: false
        readOnlyRootFilesystem: true
        capabilities:
          drop:
            - ALL
        seccompProfile:
          type: RuntimeDefault
      resources:
        limits:
          memory: "256Mi"
          cpu: "500m"
        requests:
          memory: "128Mi"
          cpu: "250m"
      volumeMounts:
        - name: tmp
          mountPath: /tmp
  volumes:
    - name: tmp
      emptyDir: {}

Falco Runtime Detection

# falco-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: falco-config
  namespace: falco
data:
  falco.yaml: |
    load_plugins: []
    rules_file:
      - /etc/falco/falco-rules.yaml
      - /etc/falco/falco-rules.local.yaml
    syscall_event_drops:
      actions:
        - log
        - alert
      threshold: 0.1
    syscall_event_timeout:
      milliseconds: 1000

  falco-rules.yaml: |
    - rule: Terminal shell in container
      desc: A shell was spawned in a container with an attached terminal
      condition: >
        evt.type = execve and
        evt.dir = < and
        (container.id != host or (container.id exists and container.id != "")) and
        (proc.name = bash or proc.name = sh) and
        proc.tty != 0
      output: "Terminal shell detected in container (user=%user.name container_id=%container.id container_name=%container.name shell=%proc.name pid=%proc.pid)"
      priority: WARNING
      tags: [container, shell, mitre_execution]

    - rule: Detect write below /etc
      desc: An attempt to write to /etc directory
      condition: >
        evt.type = open_write and
        evt.dir = < and
        fd.dir = /etc
      output: "File below /etc detected (user=%user.name file=%fd.name pid=%proc.pid)"
      priority: WARNING
      tags: [filesystem, mitre_persistence]
# Install Falco
helm repo add falcosecurity https://falcosecurity.github.io/charts
helm install falco falcosecurity/falco \
  --namespace falco \
  --create-namespace \
  --set falcosecurity.rules.overridefalcoRules=true

OPA Gatekeeper Policies

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
  name: require-security-labels
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Namespace"]
  parameters:
    labels:
      - key: "security-level"
      - key: "data-classification"
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: k8srequiredlabels
spec:
  crd:
    spec:
      names:
        kind: K8sRequiredLabels
      validation:
        openAPIV3Schema:
          type: object
          properties:
            labels:
              type: array
              items:
                type: object
                properties:
                  key:
                    type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequiredlabels
        violation[{"msg": msg, "details": {"missing_labels": missing}}] {
          provided := {label | input.request.object.metadata.labels[label]}
          required := {label | label := input.parameters.labels[_].key}
          missing := required - provided
          count(missing) > 0
          msg := sprintf("Missing labels: %v", [missing])
        }

4. Registry Security

Private Registry with Notary

# Initialize Notary server
docker-compose -f docker-compose-notary.yml up -d

# Sign an image
export DOCKER_CONTENT_TRUST=1
export DOCKER_NOTARY_SERVER="https://notary.example.com"
docker pull myapp:latest
docker tag myapp:latest notary.example.com/myapp:latest
docker push notary.example.com/myapp:latest

Harbor Registry

# harbor-values.yaml
expose:
  type: ingress
  ingress:
    hostname: harbor.example.com
    annotations:
      cert-manager.io/cluster-issuer: letsencrypt-prod
tls:
  secretName: harbor-tls
persistence:
  enabled: true
  persistentVolumeClaim:
    size: 100Gi
harborAdminPassword: "ChangeThisPassword123!"

5. Secrets Management

External Secrets Operator

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: database-secrets
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: SecretStore
  target:
    name: db-credentials
    creationPolicy: Owner
  data:
    - secretKey: username
      remoteRef:
        key: prod/database
        property: username
    - secretKey: password
      remoteRef:
        key: prod/database
        property: password

Vault Integration

apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-sa
  annotations:
    vault.hashicorp.com/role: "app-role"
    vault.hashicorp.com/agent-inject: "true"
    vault.hashicorp.com/agent-inject-secret-config: "secret/data/myapp"
    vault.hashicorp.com/agent-inject-template-config: |
      {{- with secret "secret/data/myapp" -}}
      export DB_PASSWORD="{{ .Data.data.db_password }}"
      {{- end }}

Vulnerability Management

Scanning Tools Comparison

Tool Type CI/CD Integration Enterprise Support Cost
Trivy Open Source Excellent Limited Free
Anchore Commercial Excellent Excellent $$$
Qualys Commercial Good Excellent $$$$
Snyk Commercial Excellent Excellent $$$
Clair Open Source Good Limited Free

Patch Management Strategy

# Renovate config for automated updates
{
  "packageRules": [
    {
      "matchPackagePatterns": ["*"],
      "matchUpdateTypes": ["patch", "minor"],
      "automerge": true,
      "requiredStatusChecks": ["security/scan"]
    },
    {
      "matchPackagePatterns": ["node", "nginx"],
      "matchUpdateTypes": ["major"],
      "schedule": ["before 5am on the first day of the month"]
    }
  ]
}

Best Practices Summary

Build Time

  • Use minimal base images (Alpine, distroless)
  • Never use :latest tag
  • Scan images in CI/CD pipeline
  • Sign images with Notary/cosign
  • Use multi-stage builds

Runtime

  • Run containers as non-root
  • Use read-only root filesystem
  • Implement network policies
  • Enable seccomp and AppArmor
  • Set resource limits

Registry

  • Use private registry with access control
  • Enable vulnerability scanning
  • Sign and verify images
  • Implement retention policies

Orchestration

  • Enable RBAC with least privilege
  • Use Pod Security Standards
  • Implement network policies
  • Enable audit logging
  • Regular security assessments

External Resources


Comments