DevSecOps

Container Security in Kubernetes: A DevSecOps Implementation Guide

DeviDevs Team
9 min read
#kubernetes#container security#DevSecOps#docker#cloud security

Container security in Kubernetes requires a defense-in-depth approach spanning the entire lifecycle from image building to runtime protection. This guide provides practical implementations for securing your Kubernetes workloads using DevSecOps principles.

Container Image Security

Image Scanning in CI/CD

Integrate vulnerability scanning into your build pipeline:

# .github/workflows/container-security.yml
name: Container Security Pipeline
 
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
 
jobs:
  build-and-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Build Container Image
        run: |
          docker build -t myapp:${{ github.sha }} .
 
      - name: Run Trivy Vulnerability Scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'myapp:${{ github.sha }}'
          format: 'sarif'
          output: 'trivy-results.sarif'
          severity: 'CRITICAL,HIGH'
          exit-code: '1'
 
      - name: Upload Trivy Results
        uses: github/codeql-action/upload-sarif@v2
        if: always()
        with:
          sarif_file: 'trivy-results.sarif'
 
      - name: Run Grype Scanner
        uses: anchore/scan-action@v3
        with:
          image: 'myapp:${{ github.sha }}'
          fail-build: true
          severity-cutoff: high
 
      - name: Scan Dockerfile with Hadolint
        uses: hadolint/hadolint-action@v3.1.0
        with:
          dockerfile: Dockerfile
          failure-threshold: warning

Secure Dockerfile Patterns

Build minimal, secure container images:

# Multi-stage build for minimal attack surface
FROM node:20-alpine AS builder
 
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nextjs -u 1001
 
WORKDIR /app
 
# Copy package files first for better caching
COPY package*.json ./
 
# Install dependencies with security audit
RUN npm ci --only=production && \
    npm audit --audit-level=high
 
COPY --chown=nextjs:nodejs . .
 
RUN npm run build
 
# Production stage - minimal image
FROM gcr.io/distroless/nodejs20-debian12
 
WORKDIR /app
 
# Copy only necessary files
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
 
# Use non-root user
USER 1001
 
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node healthcheck.js || exit 1
 
EXPOSE 3000
 
CMD ["dist/server.js"]

Image Signing and Verification

Implement supply chain security with cosign:

#!/bin/bash
# sign-and-verify.sh
 
IMAGE="registry.example.com/myapp:latest"
 
# Generate key pair (one-time setup)
cosign generate-key-pair
 
# Sign the image
cosign sign --key cosign.key $IMAGE
 
# Verify the signature
cosign verify --key cosign.pub $IMAGE
 
# Sign with keyless (Sigstore)
COSIGN_EXPERIMENTAL=1 cosign sign $IMAGE
 
# Verify keyless signature
COSIGN_EXPERIMENTAL=1 cosign verify \
  --certificate-identity=user@example.com \
  --certificate-oidc-issuer=https://accounts.google.com \
  $IMAGE

Kubernetes Security Configurations

Pod Security Standards

Apply Pod Security Standards using namespace labels:

# namespace-security.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/audit-version: latest
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: latest

Secure Pod Configuration

Create pods following security best practices:

# secure-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: secure-app
  namespace: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: secure-app
  template:
    metadata:
      labels:
        app: secure-app
    spec:
      # Use non-root security context
      securityContext:
        runAsNonRoot: true
        runAsUser: 1001
        runAsGroup: 1001
        fsGroup: 1001
        seccompProfile:
          type: RuntimeDefault
 
      # Service account with minimal permissions
      serviceAccountName: secure-app-sa
      automountServiceAccountToken: false
 
      containers:
        - name: app
          image: registry.example.com/myapp:v1.0.0@sha256:abc123...
 
          # Container security context
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop:
                - ALL
            privileged: false
 
          # Resource limits (prevent DoS)
          resources:
            limits:
              cpu: "500m"
              memory: "256Mi"
              ephemeral-storage: "100Mi"
            requests:
              cpu: "100m"
              memory: "128Mi"
 
          # Probes for health checking
          livenessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 15
 
          readinessProbe:
            httpGet:
              path: /ready
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10
 
          # Volume mounts for writable directories
          volumeMounts:
            - name: tmp
              mountPath: /tmp
            - name: cache
              mountPath: /app/.cache
 
          # Environment from secrets
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: app-secrets
                  key: database-url
 
      volumes:
        - name: tmp
          emptyDir:
            sizeLimit: 50Mi
        - name: cache
          emptyDir:
            sizeLimit: 100Mi
 
      # Pod topology spread for HA
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: secure-app

Network Security

Network Policies

Implement zero-trust networking with network policies:

# network-policies.yaml
 
# Default deny all ingress and egress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
---
# Allow specific app communication
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-app-traffic
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: api-server
  policyTypes:
    - Ingress
    - Egress
  ingress:
    # Allow from frontend pods
    - from:
        - podSelector:
            matchLabels:
              app: frontend
      ports:
        - protocol: TCP
          port: 8080
    # Allow from ingress controller
    - from:
        - namespaceSelector:
            matchLabels:
              name: ingress-nginx
      ports:
        - protocol: TCP
          port: 8080
  egress:
    # Allow to database
    - to:
        - podSelector:
            matchLabels:
              app: postgres
      ports:
        - protocol: TCP
          port: 5432
    # Allow DNS
    - to:
        - namespaceSelector: {}
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
---
# Database network policy
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: database-policy
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: postgres
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: api-server
      ports:
        - protocol: TCP
          port: 5432

Admission Controllers

OPA Gatekeeper Policies

Enforce security policies with OPA Gatekeeper:

# gatekeeper-constraints.yaml
 
# Constraint Template: Require Non-Root
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequirenonroot
spec:
  crd:
    spec:
      names:
        kind: K8sRequireNonRoot
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequirenonroot
 
        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not container.securityContext.runAsNonRoot
          msg := sprintf("Container %v must set runAsNonRoot to true", [container.name])
        }
 
        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          container.securityContext.runAsUser == 0
          msg := sprintf("Container %v must not run as root (UID 0)", [container.name])
        }
---
# Apply the constraint
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequireNonRoot
metadata:
  name: require-non-root
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
    namespaces:
      - production
      - staging
---
# Constraint Template: Require Resource Limits
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequireresourcelimits
spec:
  crd:
    spec:
      names:
        kind: K8sRequireResourceLimits
      validation:
        openAPIV3Schema:
          type: object
          properties:
            maxCpu:
              type: string
            maxMemory:
              type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequireresourcelimits
 
        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not container.resources.limits.cpu
          msg := sprintf("Container %v must specify CPU limits", [container.name])
        }
 
        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not container.resources.limits.memory
          msg := sprintf("Container %v must specify memory limits", [container.name])
        }
---
# Constraint Template: Require Image Digest
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequireimagedigest
spec:
  crd:
    spec:
      names:
        kind: K8sRequireImageDigest
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequireimagedigest
 
        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not contains(container.image, "@sha256:")
          msg := sprintf("Container %v must use image digest, not tag", [container.name])
        }

Kyverno Policies

Alternative policy engine with Kyverno:

# kyverno-policies.yaml
 
# Require Labels
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-labels
spec:
  validationFailureAction: Enforce
  rules:
    - name: require-team-label
      match:
        resources:
          kinds:
            - Pod
      validate:
        message: "The label 'team' is required"
        pattern:
          metadata:
            labels:
              team: "?*"
---
# Add Default Security Context
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-default-securitycontext
spec:
  rules:
    - name: add-security-context
      match:
        resources:
          kinds:
            - Pod
      mutate:
        patchStrategicMerge:
          spec:
            securityContext:
              runAsNonRoot: true
              seccompProfile:
                type: RuntimeDefault
---
# Restrict Image Registries
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: restrict-image-registries
spec:
  validationFailureAction: Enforce
  rules:
    - name: validate-registries
      match:
        resources:
          kinds:
            - Pod
      validate:
        message: "Images must be from approved registries"
        pattern:
          spec:
            containers:
              - image: "registry.example.com/* | gcr.io/myproject/*"

Runtime Security

Falco Runtime Monitoring

Deploy Falco for runtime threat detection:

# falco-rules.yaml
customRules:
  rules-custom.yaml: |-
    # Detect shell spawned in container
    - rule: Shell Spawned in Container
      desc: Detect shell spawned in a container
      condition: >
        spawned_process and
        container and
        shell_procs and
        not shell_allowed_parent_processes
      output: >
        Shell spawned in container
        (user=%user.name container=%container.name shell=%proc.name
        parent=%proc.pname cmdline=%proc.cmdline container_id=%container.id
        image=%container.image.repository)
      priority: WARNING
      tags: [container, shell, mitre_execution]
 
    # Detect sensitive file access
    - rule: Sensitive File Access
      desc: Detect access to sensitive files
      condition: >
        open_read and
        container and
        (fd.name startswith /etc/shadow or
         fd.name startswith /etc/passwd or
         fd.name startswith /proc/self/environ)
      output: >
        Sensitive file accessed
        (user=%user.name file=%fd.name container=%container.name
        image=%container.image.repository)
      priority: CRITICAL
      tags: [container, filesystem, mitre_credential_access]
 
    # Detect crypto mining
    - rule: Crypto Mining Activity
      desc: Detect potential crypto mining
      condition: >
        spawned_process and
        container and
        (proc.name in (xmrig, minerd, cpuminer) or
         proc.cmdline contains "stratum+tcp" or
         proc.cmdline contains "pool.minexmr")
      output: >
        Crypto mining detected
        (user=%user.name process=%proc.name container=%container.name
        cmdline=%proc.cmdline image=%container.image.repository)
      priority: CRITICAL
      tags: [container, cryptomining, mitre_resource_hijacking]

Security Monitoring with eBPF

Deploy Tetragon for kernel-level security:

# tetragon-policy.yaml
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
  name: container-security-events
spec:
  kprobes:
    - call: "security_file_open"
      syscall: false
      args:
        - index: 0
          type: "file"
      selectors:
        - matchArgs:
            - index: 0
              operator: "Prefix"
              values:
                - "/etc/shadow"
                - "/etc/passwd"
                - "/root/.ssh"
      action: Audit
---
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
  name: network-connections
spec:
  kprobes:
    - call: "tcp_connect"
      syscall: false
      args:
        - index: 0
          type: "sock"
      selectors:
        - matchNamespaces:
            - namespace: production
              operator: In
      action: Audit

Secrets Management

External Secrets Operator

Sync secrets from external vaults:

# external-secrets.yaml
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: vault-backend
  namespace: production
spec:
  provider:
    vault:
      server: "https://vault.example.com"
      path: "secret"
      version: "v2"
      auth:
        kubernetes:
          mountPath: "kubernetes"
          role: "production-app"
          serviceAccountRef:
            name: "vault-auth"
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: app-secrets
  namespace: production
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: vault-backend
    kind: SecretStore
  target:
    name: app-secrets
    creationPolicy: Owner
  data:
    - secretKey: database-url
      remoteRef:
        key: production/database
        property: connection_string
    - secretKey: api-key
      remoteRef:
        key: production/api
        property: key

Security Scanning Pipeline

Complete Security Pipeline

#!/usr/bin/env python3
# security_pipeline.py
 
import subprocess
import json
import sys
from dataclasses import dataclass
from typing import List, Optional
 
@dataclass
class ScanResult:
    scanner: str
    passed: bool
    critical: int
    high: int
    medium: int
    findings: List[dict]
 
def run_trivy_scan(image: str) -> ScanResult:
    """Run Trivy vulnerability scan"""
    result = subprocess.run(
        ["trivy", "image", "--format", "json", "--quiet", image],
        capture_output=True,
        text=True
    )
 
    data = json.loads(result.stdout)
 
    critical = high = medium = 0
    findings = []
 
    for result_item in data.get("Results", []):
        for vuln in result_item.get("Vulnerabilities", []):
            severity = vuln.get("Severity", "UNKNOWN")
            if severity == "CRITICAL":
                critical += 1
            elif severity == "HIGH":
                high += 1
            elif severity == "MEDIUM":
                medium += 1
 
            findings.append({
                "id": vuln.get("VulnerabilityID"),
                "severity": severity,
                "package": vuln.get("PkgName"),
                "title": vuln.get("Title")
            })
 
    passed = critical == 0 and high == 0
 
    return ScanResult(
        scanner="trivy",
        passed=passed,
        critical=critical,
        high=high,
        medium=medium,
        findings=findings
    )
 
def run_kubesec_scan(manifest_path: str) -> ScanResult:
    """Run kubesec Kubernetes manifest scan"""
    result = subprocess.run(
        ["kubesec", "scan", manifest_path],
        capture_output=True,
        text=True
    )
 
    data = json.loads(result.stdout)
 
    score = data[0].get("score", 0) if data else 0
    critical_findings = data[0].get("scoring", {}).get("critical", []) if data else []
 
    findings = [{"type": "critical", "rule": f} for f in critical_findings]
 
    return ScanResult(
        scanner="kubesec",
        passed=score >= 0 and len(critical_findings) == 0,
        critical=len(critical_findings),
        high=0,
        medium=0,
        findings=findings
    )
 
def main():
    image = sys.argv[1] if len(sys.argv) > 1 else "myapp:latest"
    manifest = sys.argv[2] if len(sys.argv) > 2 else "deployment.yaml"
 
    print(f"🔍 Running security scans for {image}")
 
    # Run scans
    trivy_result = run_trivy_scan(image)
    kubesec_result = run_kubesec_scan(manifest)
 
    # Report results
    all_passed = trivy_result.passed and kubesec_result.passed
 
    print(f"\n📊 Trivy Results:")
    print(f"   Critical: {trivy_result.critical}")
    print(f"   High: {trivy_result.high}")
    print(f"   Status: {'✅ PASSED' if trivy_result.passed else '❌ FAILED'}")
 
    print(f"\n📊 Kubesec Results:")
    print(f"   Critical: {kubesec_result.critical}")
    print(f"   Status: {'✅ PASSED' if kubesec_result.passed else '❌ FAILED'}")
 
    print(f"\n{'✅ All security checks passed!' if all_passed else '❌ Security checks failed!'}")
 
    sys.exit(0 if all_passed else 1)
 
if __name__ == "__main__":
    main()

Summary

Container security in Kubernetes requires multiple layers of protection:

  1. Build time: Scan images, use minimal base images, sign artifacts
  2. Deploy time: Admission controllers enforce policies
  3. Runtime: Monitor for anomalies and threats
  4. Network: Zero-trust with network policies
  5. Secrets: External management with rotation

Implementing these controls as part of your DevSecOps pipeline ensures security is automated and consistent across all environments.

Weekly AI Security & Automation Digest

Get the latest on AI Security, workflow automation, secure integrations, and custom platform development delivered weekly.

No spam. Unsubscribe anytime.