Container & Cloud Security

Docker Hardening, Image Scanning, Kubernetes Security, AWS IAM

Introduction

Containers changed how applications are deployed, and introduced new security challenges. A poorly configured container can give an attacker root-equivalent access to the host. This lesson covers Docker security hardening, image vulnerability scanning, Kubernetes security primitives (RBAC, NetworkPolicy, PodSecurityContext), and AWS IAM least-privilege principles with Python.

Docker Security Baseline

Non-root User

Never run containers as root. Add USER directive to Dockerfile.

Read-only Filesystem

Mount root filesystem as read-only. Use tmpfs for writable paths.

Drop Capabilities

Drop ALL Linux capabilities; add back only what's needed.

Minimal Base Image

Use distroless or alpine. Smaller attack surface = fewer CVEs.

# Dockerfile, Security-hardened Python application (multi-stage build)

# ── Stage 1: Build ────────────────────────────────────────────
FROM python:3.12-slim AS builder

WORKDIR /build

# Install dependencies in build stage
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

# ── Stage 2: Runtime (minimal attack surface) ─────────────────
FROM python:3.12-slim AS runtime

# Create non-root user
RUN groupadd --gid 1001 appgroup && \
    useradd --uid 1001 --gid 1001 --no-create-home appuser

WORKDIR /app

# Copy only installed packages from build stage
COPY --from=builder /install /usr/local
COPY --chown=appuser:appgroup src/ ./src/

# Run as non-root
USER appuser

# Health check
HEALTHCHECK --interval=30s --timeout=3s CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"

EXPOSE 8000
CMD ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]

# ── docker run security flags ──────────────────────────────────
# docker run \
#   --read-only \                  # Read-only root filesystem
#   --cap-drop ALL \               # Drop all Linux capabilities
#   --cap-add NET_BIND_SERVICE \   # Add back only what's needed (port < 1024)
#   --security-opt no-new-privileges \   # Prevent privilege escalation
#   --memory="256m" \              # Memory limit
#   --cpus="0.5" \                 # CPU limit
#   --tmpfs /tmp:rw,noexec \      # Writable temp directory
#   my-app:latest

Image Scanning with trivy

trivy (pronounced "try-vee") is an open-source vulnerability scanner by Aqua Security. It scans container images, filesystems, Git repositories, and cloud configurations for known CVEs (Common Vulnerabilities and Exposures), misconfigurations, and exposed secrets. trivy works by pulling vulnerability data from public databases (NVD, GitHub Advisory, OS vendor advisories) and matching it against the packages installed in the image, including OS packages (apt/apk/rpm) and language-level packages (pip, npm, maven).

Unlike Dockerfile linters that only check your build instructions, trivy inspects the final image layers, catching CVEs introduced by the base image itself (e.g. python:3.12-slim pulling in a vulnerable version of libssl). The --exit-code 1 flag makes trivy fail the CI/CD pipeline if findings above a chosen severity threshold are found.

What trivy scans
  • OS packages: apt, apk, rpm packages in all image layers
  • Language packages: pip, npm, gem, cargo, maven
  • Secrets, API keys: private keys accidentally baked into the image
  • Misconfigurations: Dockerfile best-practice violations
Severity levels (CVSS-based)
  • CRITICAL - CVSS 9.0–10.0, block deployment
  • HIGH - CVSS 7.0–8.9, fix before release
  • MEDIUM - CVSS 4.0–6.9, track and schedule fix
  • LOW / UNKNOWN - informational, review periodically
# trivy, Container image vulnerability scanner

# Install (Linux)
# curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh

# Scan a local image
trivy image python:3.12-slim

# Scan with severity filter (only CRITICAL and HIGH)
trivy image --severity CRITICAL,HIGH python:3.12-slim

# Scan and output JSON for CI/CD processing
trivy image --format json --output results.json python:3.12-slim

# Fail CI/CD if any CRITICAL CVEs found
trivy image --exit-code 1 --severity CRITICAL python:3.12-slim

# ── Python: Parse trivy results ───────────────────────────────
import json

def parse_trivy_report(report_file: str) -> dict:
    """Parse trivy JSON output and summarize vulnerabilities."""
    with open(report_file) as f:
        report = json.load(f)

    summary = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0}
    critical_cves = []

    for result in report.get("Results", []):
        for vuln in result.get("Vulnerabilities", []):
            severity = vuln.get("Severity", "UNKNOWN")
            if severity in summary:
                summary[severity] += 1
            if severity == "CRITICAL":
                critical_cves.append({
                    "id": vuln["VulnerabilityID"],
                    "package": vuln["PkgName"],
                    "fixed_in": vuln.get("FixedVersion", "No fix available"),
                })

    return {"summary": summary, "critical_cves": critical_cves}

# Example output structure
example = {"summary": {"CRITICAL": 2, "HIGH": 8, "MEDIUM": 15, "LOW": 31}, "critical_cves": [
    {"id": "CVE-2024-1234", "package": "libssl1.1", "fixed_in": "1.1.1w-0+deb11u1"},
    {"id": "CVE-2024-5678", "package": "libc6", "fixed_in": "2.31-13+deb11u9"},
]}
for sev, count in example["summary"].items():
    print(f"  {sev}: {count}")
print("Critical CVEs:")
for cve in example["critical_cves"]:
    print(f"  {cve['id']} in {cve['package']} → fix: {cve['fixed_in']}")

Kubernetes Security Primitives

# kubernetes_security.yaml, Security-hardened Pod spec

apiVersion: v1
kind: Pod
metadata:
  name: secure-app
spec:
  # ── Pod-level security context ────────────────────────────
  securityContext:
    runAsNonRoot: true           # Refuse to run as root
    runAsUser: 1001              # Specific non-root UID
    runAsGroup: 1001
    fsGroup: 1001                # File system group
    seccompProfile:
      type: RuntimeDefault       # Restrict syscalls to common set

  containers:
  - name: app
    image: my-app:1.2.3         # Pin specific version, not 'latest'

    # ── Container-level security context ──────────────────
    securityContext:
      allowPrivilegeEscalation: false  # Prevent sudo/setuid
      readOnlyRootFilesystem: true     # Read-only filesystem
      capabilities:
        drop: ["ALL"]           # Drop all Linux capabilities
        add: []                 # Add back nothing

    # ── Resource limits ───────────────────────────────────
    resources:
      requests:
        memory: "64Mi"
        cpu: "100m"
      limits:
        memory: "128Mi"
        cpu: "250m"

    # ── Writable volume for temp files ────────────────────
    volumeMounts:
    - name: tmp
      mountPath: /tmp

  volumes:
  - name: tmp
    emptyDir: {}   # tmpfs, destroyed when pod exits

---
# NetworkPolicy, restrict pod-to-pod communication
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-only-frontend
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes: [Ingress]
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend
    ports:
    - protocol: TCP
      port: 8080
  # Default deny: backend only accepts traffic from frontend pods

AWS IAM Least Privilege with Python

The Principle of Least Privilege (PoLP) states that every identity, whether a human user, an application, or a service, should be granted only the minimum permissions required to perform its intended function, and nothing more. In AWS, this is enforced through IAM (Identity and Access Management) policies attached to users, groups, and roles.

Over-permissioned identities are one of the most exploited misconfigurations in cloud environments. If an attacker compromises a Lambda function or EC2 instance with an "Action": "*" policy, they inherit full AWS access, able to exfiltrate data, spin up resources for crypto mining, or destroy the entire account. Least privilege limits this blast radius: a compromised identity can only affect the specific resources it was permitted to touch.

Identity

Who is making the request? User, role, or service account.

Action

What are they allowed to do? Specific API calls, not wildcards.

Resource

Which resources exactly? A specific bucket ARN, not *.

Install: pip install boto3

# iam_check.py, Analyze IAM policies for over-permission
import boto3
import json

OVERLY_PERMISSIVE = ["*", "iam:*", "s3:*", "ec2:*", "rds:*"]

def analyze_iam_policy(policy_document: dict) -> list[str]:
    """Check an IAM policy document for overly permissive rules."""
    warnings = []
    for statement in policy_document.get("Statement", []):
        if statement.get("Effect") != "Allow":
            continue

        actions = statement.get("Action", [])
        if isinstance(actions, str):
            actions = [actions]

        resources = statement.get("Resource", [])
        if isinstance(resources, str):
            resources = [resources]

        for action in actions:
            if action in OVERLY_PERMISSIVE or action == "*":
                warnings.append(f"⚠️ Overly permissive action: {action!r}")

        if "*" in resources:
            warnings.append("⚠️ Action applies to ALL resources (*)")

    return warnings

# Example: Overly permissive policy (what NOT to do)
bad_policy = {
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "*",          # ← ALL AWS actions
            "Resource": "*",        # ← On ALL resources
        }
    ]
}

# Least-privilege policy (what TO do)
good_policy = {
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",     # Only read objects
                "s3:PutObject",     # Only write objects
            ],
            "Resource": "arn:aws:s3:::my-app-bucket/*",  # Only this bucket
        }
    ]
}

print("BAD policy warnings:")
for w in analyze_iam_policy(bad_policy):
    print(f"  {w}")

print("\nGOOD policy warnings:")
warnings = analyze_iam_policy(good_policy)
if not warnings:
    print("  ✅ No over-permission issues detected")
Expected Output:
BAD policy warnings:
  ⚠️ Overly permissive action: '*'
  ⚠️ Action applies to ALL resources (*)

GOOD policy warnings:
  ✅ No over-permission issues detected

Key Takeaways

  • Never run containers as root, a container escape as root gives attacker root on the host
  • Read-only filesystem + tmpfs, prevents attackers from dropping payloads or modifying application code
  • Drop all capabilities, --cap-drop ALL removes 37 dangerous Linux capabilities by default
  • Scan images before deployment, trivy in CI/CD catches CVEs before they reach production
  • Kubernetes NetworkPolicy is default-deny, pods can communicate with everything by default; add NetworkPolicy to restrict
  • IAM least privilege, specify exact actions and resources; never use "Action": "*" or "Resource": "*" together
What's Next?

Lesson 18 covers Network Security Operations, the day-to-day work of monitoring, detecting, and responding to security incidents.

  • Firewall concepts, iptables rules and filtering
  • IDS/IPS, intrusion detection and prevention
  • Security monitoring, Python log parser for anomaly detection
  • Incident response, NIST framework and automated response scripts