DEV Community

Maureen Chebet
Maureen Chebet

Posted on

Securing Container Lifecycle on AWS: From Build to Runtime

The Security Wake-Up Call

Your containerized applications are under scrutiny. Security scans have revealed critical issues:

  • Base images with known vulnerabilities (CVE-2023-XXXX, CVE-2024-XXXX)
  • Runtime secrets exposed in image layers (API keys, passwords visible in image history)
  • No image signing or verification (anyone can push malicious images)
  • Compliance gaps for financial services (PCI-DSS, SOC 2 requirements not met)

This isn't just about fixing a few Dockerfiles. You need to secure the entire container lifecycle: build, registry, deployment, and runtime.

In this article, I'll walk through a comprehensive container security strategy on AWS that addresses vulnerabilities, implements secrets management, enables image signing, enforces runtime security, and automates compliance.

Container Security Lifecycle

The Four Pillars

┌─────────────┐    ┌──────────────┐    ┌─────────────┐    ┌──────────────┐
│   BUILD     │ →  │   REGISTRY   │ →  │  DEPLOYMENT │ →  │   RUNTIME    │
│             │    │              │    │             │    │              │
│ Secure base │    │ Image scan   │    │ Policy      │    │ Least        │
│ Multi-stage │    │ Signing      │    │ validation  │    │ privilege    │
│ No secrets  │    │ Access       │    │ RBAC        │    │ Isolation    │
└─────────────┘    └──────────────┘    └─────────────┘    └──────────────┘
Enter fullscreen mode Exit fullscreen mode

Phase 1: Secure Base Images and Minimal Attack Surface

The Problem: Vulnerable Base Images

Many Dockerfiles start with:

FROM ubuntu:latest
# or
FROM node:latest
Enter fullscreen mode Exit fullscreen mode

These base images often contain:

  • Unnecessary packages and tools
  • Known vulnerabilities
  • Large attack surface
  • Outdated packages

Solution: Minimal, Trusted Base Images

Option 1: Use Distroless Images

# Before: Vulnerable base image
FROM node:18
COPY . .
RUN npm install
CMD ["node", "app.js"]

# After: Distroless base image
FROM node:18-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM gcr.io/distroless/nodejs18-debian11
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app .
USER nonroot:nonroot
CMD ["app.js"]
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • No shell, no package manager (reduces attack surface)
  • Minimal OS footprint
  • Only runtime dependencies

Option 2: Alpine Linux

FROM alpine:3.18 AS builder
RUN apk add --no-cache nodejs npm
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM alpine:3.18
RUN apk add --no-cache nodejs
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app .
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001
USER nodejs:nodejs
CMD ["node", "app.js"]
Enter fullscreen mode Exit fullscreen mode

Option 3: AWS-Optimized Base Images

# Use AWS-provided base images
FROM public.ecr.aws/lambda/nodejs:18
# or
FROM public.ecr.aws/docker/library/node:18-slim
Enter fullscreen mode Exit fullscreen mode

Multi-Stage Build Best Practices

Secure Multi-Stage Build:

# Stage 1: Build dependencies
FROM node:18-slim AS deps
WORKDIR /app

# Copy only package files first (better layer caching)
COPY package*.json ./

# Install dependencies with security flags
RUN npm ci --only=production --ignore-scripts && \
    npm audit --audit-level=moderate && \
    rm -rf /tmp/* /var/tmp/*

# Stage 2: Build application
FROM node:18-slim AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Build with security hardening
RUN npm run build && \
    npm prune --production && \
    rm -rf /tmp/* /var/tmp/*

# Stage 3: Runtime (minimal)
FROM gcr.io/distroless/nodejs18-debian11:nonroot
WORKDIR /app

# Copy only production artifacts
COPY --from=builder --chown=nonroot:nonroot /app/dist ./dist
COPY --from=builder --chown=nonroot:nonroot /app/node_modules ./node_modules
COPY --from=builder --chown=nonroot:nonroot /app/package.json ./

# No secrets, no build tools, no shell
EXPOSE 8080
CMD ["dist/index.js"]
Enter fullscreen mode Exit fullscreen mode

Base Image Vulnerability Scanning

Pre-Build Base Image Check:

#!/bin/bash
# check-base-image.sh

BASE_IMAGE=$1
if [ -z "$BASE_IMAGE" ]; then
    echo "Usage: $0 <base-image>"
    exit 1
fi

echo "Scanning base image: $BASE_IMAGE"

# Use Trivy to scan base image
trivy image --severity HIGH,CRITICAL --exit-code 1 "$BASE_IMAGE"

if [ $? -eq 0 ]; then
    echo "Base image passed security scan"
    exit 0
else
    echo "Base image has critical vulnerabilities"
    exit 1
fi
Enter fullscreen mode Exit fullscreen mode

Integrate into CI/CD:

# .github/workflows/docker-build.yml
name: Build and Scan

on:
  push:
    branches: [main]

jobs:
  check-base-image:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Check base image
        run: |
          BASE_IMAGE=$(grep '^FROM' Dockerfile | head -1 | awk '{print $2}')
          ./check-base-image.sh "$BASE_IMAGE"

  build:
    needs: check-base-image
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Build Docker image
        run: docker build -t payment-app:latest .
Enter fullscreen mode Exit fullscreen mode

Automated Base Image Updates

Dependabot for Docker:

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "docker"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10
Enter fullscreen mode Exit fullscreen mode

Phase 2: Secrets Management (Not in Images)

The Problem: Secrets in Image Layers

Common Anti-Pattern:

# NEVER DO THIS
FROM node:18
ENV DB_PASSWORD=mysecretpassword123
ENV API_KEY=sk_live_1234567890
COPY . .
RUN npm install
CMD ["node", "app.js"]
Enter fullscreen mode Exit fullscreen mode

Why This is Dangerous:

  • Secrets are visible in image history: docker history <image>
  • Secrets are in image layers (even if removed in later layers)
  • Anyone with image access can extract secrets
  • Secrets are in version control (if Dockerfile is committed)

Solution: AWS Secrets Manager Integration

Secure Dockerfile:

FROM node:18-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM gcr.io/distroless/nodejs18-debian11:nonroot
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app .

# No secrets in image!
# Secrets will be injected at runtime via environment variables
# or mounted volumes from AWS Secrets Manager

EXPOSE 8080
CMD ["app.js"]
Enter fullscreen mode Exit fullscreen mode

Application Code - Fetching Secrets:

// app.js
const AWS = require('aws-sdk');
const secretsManager = new AWS.SecretsManager({ region: process.env.AWS_REGION });

async function getSecret(secretName) {
    try {
        const data = await secretsManager.getSecretValue({ SecretId: secretName }).promise();
        return JSON.parse(data.SecretString);
    } catch (error) {
        console.error(`Error retrieving secret ${secretName}:`, error);
        throw error;
    }
}

// Fetch secrets at startup
let dbCredentials;
async function initializeSecrets() {
    dbCredentials = await getSecret('payment-app/database/credentials');
    process.env.DB_HOST = dbCredentials.host;
    process.env.DB_USER = dbCredentials.username;
    process.env.DB_PASSWORD = dbCredentials.password;
}

initializeSecrets().then(() => {
    // Start application
    app.listen(8080);
});
Enter fullscreen mode Exit fullscreen mode

ECS Task Definition with Secrets:

{
  "family": "payment-app",
  "containerDefinitions": [
    {
      "name": "payment-app",
      "image": "123456789.dkr.ecr.us-east-1.amazonaws.com/payment-app:latest",
      "secrets": [
        {
          "name": "DB_PASSWORD",
          "valueFrom": "arn:aws:secretsmanager:us-east-1:123456789:secret:payment-app/database/credentials:password::"
        },
        {
          "name": "API_KEY",
          "valueFrom": "arn:aws:secretsmanager:us-east-1:123456789:secret:payment-app/api-key::"
        }
      ],
      "environment": [
        {
          "name": "AWS_REGION",
          "value": "us-east-1"
        }
      ]
    }
  ],
  "taskRoleArn": "arn:aws:iam::123456789:role/ecs-task-role",
  "executionRoleArn": "arn:aws:iam::123456789:role/ecs-execution-role"
}
Enter fullscreen mode Exit fullscreen mode

IAM Role for Secrets Access:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "secretsmanager:GetSecretValue",
        "secretsmanager:DescribeSecret"
      ],
      "Resource": [
        "arn:aws:secretsmanager:us-east-1:123456789:secret:payment-app/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "kms:Decrypt"
      ],
      "Resource": [
        "arn:aws:kms:us-east-1:123456789:key/secrets-key-id"
      ],
      "Condition": {
        "StringEquals": {
          "kms:ViaService": "secretsmanager.us-east-1.amazonaws.com"
        }
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Kubernetes Secrets (If Using EKS)

External Secrets Operator:

# external-secret.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: payment-app-secrets
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: SecretStore
  target:
    name: payment-app-secrets
    creationPolicy: Owner
  data:
    - secretKey: db-password
      remoteRef:
        key: payment-app/database/credentials
        property: password
    - secretKey: api-key
      remoteRef:
        key: payment-app/api-key
Enter fullscreen mode Exit fullscreen mode

Pre-Commit Hook to Detect Secrets

#!/usr/bin/env python3
# .git/hooks/pre-commit

import subprocess
import re
import sys

def detect_secrets_in_dockerfile():
    """Detect secrets in Dockerfile before commit"""

    patterns = [
        (r'ENV\s+\w*PASSWORD\s*=\s*["\']([^"\']+)["\']', 'Password in ENV'),
        (r'ENV\s+\w*SECRET\s*=\s*["\']([^"\']+)["\']', 'Secret in ENV'),
        (r'ENV\s+\w*KEY\s*=\s*["\']([^"\']+)["\']', 'Key in ENV'),
        (r'--password\s+["\']([^"\']+)["\']', 'Password in command'),
        (r'apikey["\']?\s*[:=]\s*["\']([^"\']+)["\']', 'API key detected'),
    ]

    try:
        with open('Dockerfile', 'r') as f:
            content = f.read()

        violations = []
        for pattern, message in patterns:
            matches = re.findall(pattern, content, re.IGNORECASE)
            if matches:
                violations.append(f"{message}: {len(matches)} found")

        if violations:
            print("❌ SECURITY VIOLATION: Secrets detected in Dockerfile!")
            for violation in violations:
                print(f"  - {violation}")
            print("\nUse AWS Secrets Manager instead.")
            print("See: https://docs.aws.amazon.com/secretsmanager/")
            sys.exit(1)

        print("✅ No secrets detected in Dockerfile")
        return 0
    except FileNotFoundError:
        return 0

if __name__ == '__main__':
    sys.exit(detect_secrets_in_dockerfile())
Enter fullscreen mode Exit fullscreen mode

Phase 3: Image Scanning and Signing

Amazon ECR Image Scanning

Enable Automatic Scanning:

# Enable automatic scanning on push
aws ecr put-image-scanning-configuration \
  --repository-name payment-app \
  --image-scanning-configuration scanOnPush=true

# Scan existing images
aws ecr start-image-scan \
  --repository-name payment-app \
  --image-id imageTag=latest
Enter fullscreen mode Exit fullscreen mode

Get Scan Results:

# Get scan findings
aws ecr describe-image-scan-findings \
  --repository-name payment-app \
  --image-id imageTag=latest \
  --query 'imageScanFindings' \
  --output json
Enter fullscreen mode Exit fullscreen mode

Fail Build on Critical Vulnerabilities:

# check-ecr-scan-results.py
import boto3
import sys
import json

ecr = boto3.client('ecr')

def check_scan_results(repo_name, image_tag):
    """Check ECR scan results and fail if critical issues found"""

    response = ecr.describe-image-scan-findings(
        repositoryName=repo_name,
        imageId={'imageTag': image_tag}
    )

    findings = response.get('imageScanFindings', {})
    finding_counts = findings.get('findingCounts', {})

    critical_count = finding_counts.get('CRITICAL', 0)
    high_count = finding_counts.get('HIGH', 0)

    print(f"Scan Results:")
    print(f"  Critical: {critical_count}")
    print(f"  High: {high_count}")
    print(f"  Medium: {finding_counts.get('MEDIUM', 0)}")
    print(f"  Low: {finding_counts.get('LOW', 0)}")

    # Fail if critical or too many high severity
    if critical_count > 0:
        print(f"❌ Build failed: {critical_count} CRITICAL vulnerabilities found")
        sys.exit(1)

    if high_count > 5:
        print(f"❌ Build failed: {high_count} HIGH vulnerabilities found (max 5 allowed)")
        sys.exit(1)

    print("✅ Image scan passed")
    return 0

if __name__ == '__main__':
    if len(sys.argv) < 3:
        print("Usage: python check-ecr-scan-results.py <repo-name> <image-tag>")
        sys.exit(1)

    sys.exit(check_scan_results(sys.argv[1], sys.argv[2]))
Enter fullscreen mode Exit fullscreen mode

Integrate into CI/CD:

# buildspec.yml
version: 0.2
phases:
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
      - REPOSITORY_URI=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME
      - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
      - IMAGE_TAG=${COMMIT_HASH:=latest}
  build:
    commands:
      - echo Building Docker image...
      - docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG .
      - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $REPOSITORY_URI:$IMAGE_TAG
  post_build:
    commands:
      - echo Pushing Docker image...
      - docker push $REPOSITORY_URI:$IMAGE_TAG
      - echo Waiting for image scan to complete...
      - |
        aws ecr wait image-scan-complete \
          --repository-name $IMAGE_REPO_NAME \
          --image-id imageTag=$IMAGE_TAG \
          --max-attempts 30 \
          --delay 10
      - echo Checking scan results...
      - |
        python check-ecr-scan-results.py $IMAGE_REPO_NAME $IMAGE_TAG
      - echo Image scan passed, proceeding with deployment
Enter fullscreen mode Exit fullscreen mode

Image Signing with AWS Signer

Create Signing Profile:

# Create signing profile
aws signer put-signing-profile \
  --profile-name payment-app-signing-profile \
  --platform-id Notation-OCI-SHA384-ECDSA \
  --signing-material certificateArn=arn:aws:acm:region:account:certificate/cert-id

# Get signing profile
aws signer get-signing-profile \
  --profile-name payment-app-signing-profile
Enter fullscreen mode Exit fullscreen mode

Sign Image:

# Sign image after push
aws signer start-signing-job \
  --source '{
    "s3": {
      "bucketName": "payment-app-artifacts",
      "key": "payment-app-latest.tar"
    }
  }' \
  --destination '{
    "s3": {
      "bucketName": "payment-app-artifacts",
      "prefix": "signed/"
    }
  }' \
  --profile-name payment-app-signing-profile
Enter fullscreen mode Exit fullscreen mode

Verify Image Signature:

# Install notation CLI
# https://notaryproject.dev/docs/installation/

# Verify signature
notation verify \
  --certificate-file certificate.pem \
  123456789.dkr.ecr.us-east-1.amazonaws.com/payment-app:latest
Enter fullscreen mode Exit fullscreen mode

Enforce Signature Verification in ECS:

import boto3

ecs = boto3.client('ecs')

def verify_image_signature(image_uri):
    """Verify image signature before deployment"""

    # Extract image details
    # Check signature using notation or AWS Signer API

    # If signature invalid, reject deployment
    pass

def create_service_with_signature_check(task_definition):
    """Create ECS service only if image is signed"""

    # Verify signature first
    image_uri = task_definition['containerDefinitions'][0]['image']
    if not verify_image_signature(image_uri):
        raise ValueError(f"Image {image_uri} is not signed or signature invalid")

    # Create service
    ecs.create_service(
        cluster='payment-cluster',
        serviceName='payment-service',
        taskDefinition=task_definition['family'],
        desiredCount=2
    )
Enter fullscreen mode Exit fullscreen mode

Third-Party Scanning Tools

Trivy Integration:

# Install Trivy
wget https://github.com/aquasecurity/trivy/releases/download/v0.45.0/trivy_0.45.0_Linux-64bit.tar.gz
tar -xzf trivy_0.45.0_Linux-64bit.tar.gz

# Scan image
trivy image --severity HIGH,CRITICAL \
  --format json \
  --output trivy-results.json \
  123456789.dkr.ecr.us-east-1.amazonaws.com/payment-app:latest

# Fail build on critical findings
trivy image --exit-code 1 --severity CRITICAL \
  123456789.dkr.ecr.us-east-1.amazonaws.com/payment-app:latest
Enter fullscreen mode Exit fullscreen mode

Snyk Integration:

# Install Snyk
npm install -g snyk

# Authenticate
snyk auth $SNYK_TOKEN

# Scan Docker image
snyk container test 123456789.dkr.ecr.us-east-1.amazonaws.com/payment-app:latest \
  --severity-threshold=high \
  --json > snyk-results.json
Enter fullscreen mode Exit fullscreen mode

Phase 4: Runtime Security (Least Privilege)

ECS Task Security Configuration

Non-Root User:

{
  "containerDefinitions": [
    {
      "name": "payment-app",
      "image": "123456789.dkr.ecr.us-east-1.amazonaws.com/payment-app:latest",
      "user": "1001:1001",
      "readonlyRootFilesystem": true,
      "privileged": false,
      "linuxParameters": {
        "capabilities": {
          "drop": ["ALL"],
          "add": ["NET_BIND_SERVICE"]
        }
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Resource Limits:

{
  "containerDefinitions": [
    {
      "name": "payment-app",
      "memory": 512,
      "memoryReservation": 256,
      "cpu": 256,
      "ulimits": [
        {
          "name": "nofile",
          "softLimit": 1024,
          "hardLimit": 2048
        }
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

EKS Pod Security Standards

Pod Security Policy:

apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: payment-app-psp
spec:
  privileged: false
  allowPrivilegeEscalation: false
  requiredDropCapabilities:
    - ALL
  volumes:
    - 'configMap'
    - 'secret'
    - 'emptyDir'
  runAsUser:
    rule: 'MustRunAsNonRoot'
  seLinux:
    rule: 'RunAsAny'
  fsGroup:
    rule: 'RunAsAny'
  readOnlyRootFilesystem: true
Enter fullscreen mode Exit fullscreen mode

Security Context:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: payment-app
spec:
  template:
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 1001
        fsGroup: 1001
      containers:
      - name: payment-app
        image: 123456789.dkr.ecr.us-east-1.amazonaws.com/payment-app:latest
        securityContext:
          allowPrivilegeEscalation: false
          readOnlyRootFilesystem: true
          capabilities:
            drop:
              - ALL
            add:
              - NET_BIND_SERVICE
        resources:
          limits:
            memory: "512Mi"
            cpu: "500m"
          requests:
            memory: "256Mi"
            cpu: "250m"
Enter fullscreen mode Exit fullscreen mode

Network Policies (EKS)

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: payment-app-netpol
spec:
  podSelector:
    matchLabels:
      app: payment-app
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: nginx-ingress
    ports:
    - protocol: TCP
      port: 8080
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: database
    ports:
    - protocol: TCP
      port: 5432
  - to:
    - namespaceSelector:
        matchLabels:
          name: kube-system
    ports:
    - protocol: UDP
      port: 53
Enter fullscreen mode Exit fullscreen mode

AWS App Mesh for Service Isolation

apiVersion: appmesh.k8s.aws/v1beta2
kind: VirtualNode
metadata:
  name: payment-app
spec:
  podSelector:
    matchLabels:
      app: payment-app
  listeners:
  - portMapping:
      port: 8080
      protocol: http
  serviceDiscovery:
    dns:
      hostname: payment-app.payment.svc.cluster.local
  backends:
  - virtualService:
      virtualServiceName: database.payment.svc.cluster.local
Enter fullscreen mode Exit fullscreen mode

Runtime Security Monitoring

Amazon Inspector for Runtime Scanning:

# Enable Inspector for ECS
aws inspector2 enable \
  --resource-types EC2 \
  --account-ids 123456789

# Create assessment target
aws inspector2 create-assessment-target \
  --name payment-app-ecs \
  --resource-group-arn arn:aws:resource-groups:us-east-1:123456789:group/payment-app
Enter fullscreen mode Exit fullscreen mode

CloudWatch Container Insights:

# Enable Container Insights for ECS
aws ecs update-cluster \
  --cluster payment-cluster \
  --settings name=containerInsights,value=enabled
Enter fullscreen mode Exit fullscreen mode

Phase 5: Compliance Automation

AWS Config Rules for Container Compliance

ECR Compliance Rule:

import boto3
import json

def evaluate_ecr_compliance(configuration_item):
    """Evaluate ECR repository compliance"""

    compliance_status = 'COMPLIANT'
    annotation = ''

    # Check if image scanning is enabled
    if not configuration_item.get('configuration', {}).get('imageScanningConfiguration', {}).get('scanOnPush', False):
        compliance_status = 'NON_COMPLIANT'
        annotation = 'ECR image scanning must be enabled for PCI-DSS compliance'

    # Check if encryption is enabled
    if not configuration_item.get('configuration', {}).get('encryptionConfiguration', {}).get('encryptionType') == 'AES256':
        compliance_status = 'NON_COMPLIANT'
        annotation = 'ECR encryption must be enabled'

    return {
        'compliance_type': compliance_status,
        'annotation': annotation
    }

def lambda_handler(event, context):
    """Lambda handler for Config custom rule"""

    config = boto3.client('config')
    configuration_item = json.loads(event['invokingEvent'])['configurationItem']
    evaluation = evaluate_ecr_compliance(configuration_item)

    config.put_evaluations(
        Evaluations=[{
            'ComplianceResourceType': configuration_item['resourceType'],
            'ComplianceResourceId': configuration_item['resourceId'],
            'ComplianceType': evaluation['compliance_type'],
            'Annotation': evaluation['annotation'],
            'OrderingTimestamp': configuration_item['configurationItemCaptureTime']
        }]
    )

    return evaluation
Enter fullscreen mode Exit fullscreen mode

ECS Task Definition Compliance:

def evaluate_ecs_task_compliance(configuration_item):
    """Evaluate ECS task definition compliance"""

    compliance_status = 'COMPLIANT'
    violations = []

    config = configuration_item.get('configuration', {})
    containers = config.get('containerDefinitions', [])

    for container in containers:
        # Check if running as root
        if container.get('user') is None or container.get('user') == 'root':
            violations.append('Container must not run as root user')

        # Check if readonly root filesystem
        if not container.get('readonlyRootFilesystem', False):
            violations.append('Container must have readonly root filesystem')

        # Check if privileged
        if container.get('privileged', False):
            violations.append('Container must not run in privileged mode')

        # Check capabilities
        linux_params = container.get('linuxParameters', {})
        capabilities = linux_params.get('capabilities', {})
        if 'ALL' not in capabilities.get('drop', []):
            violations.append('Container must drop ALL capabilities')

    if violations:
        compliance_status = 'NON_COMPLIANT'
        annotation = '; '.join(violations)
    else:
        annotation = 'Task definition meets security requirements'

    return {
        'compliance_type': compliance_status,
        'annotation': annotation
    }
Enter fullscreen mode Exit fullscreen mode

Automated Compliance Reporting

import boto3
from datetime import datetime

config = boto3.client('config')
s3 = boto3.client('s3')

def generate_compliance_report():
    """Generate container compliance report"""

    # Get compliance summary
    response = config.get_compliance_summary_by_config_rule(
        ConfigRuleNames=[
            'ecr-image-scanning-enabled',
            'ecs-task-non-root-user',
            'ecs-task-readonly-filesystem'
        ]
    )

    report = {
        'timestamp': datetime.utcnow().isoformat(),
        'compliance_summary': response.get('ComplianceSummariesByConfigRule', []),
        'overall_compliance': calculate_compliance_percentage(response)
    }

    # Save to S3 for audit trail
    s3.put_object(
        Bucket='compliance-reports',
        Key=f"container-compliance-{datetime.utcnow().strftime('%Y-%m-%d')}.json",
        Body=json.dumps(report, indent=2),
        ServerSideEncryption='AES256'
    )

    return report

def calculate_compliance_percentage(response):
    """Calculate overall compliance percentage"""

    total_resources = 0
    compliant_resources = 0

    for rule_summary in response.get('ComplianceSummariesByConfigRule', []):
        summary = rule_summary.get('ComplianceSummary', {})
        total_resources += summary.get('ComplianceResourceCount', {}).get('CappedCount', 0)
        compliant_resources += summary.get('CompliantResourceCount', {}).get('CappedCount', 0)

    if total_resources == 0:
        return 100.0

    return (compliant_resources / total_resources) * 100
Enter fullscreen mode Exit fullscreen mode

Policy Enforcement with OPA (Open Policy Agent)

OPA Policy for Container Security:

# container-security.rego
package container.security

# Deny if container runs as root
deny[msg] {
    input.container.user == "root"
    msg := "Container must not run as root user"
}

# Deny if privileged mode enabled
deny[msg] {
    input.container.privileged == true
    msg := "Container must not run in privileged mode"
}

# Deny if readonly root filesystem not enabled
deny[msg] {
    input.container.readonlyRootFilesystem != true
    msg := "Container must have readonly root filesystem"
}

# Deny if ALL capabilities not dropped
deny[msg] {
    not "ALL" in input.container.linuxParameters.capabilities.drop
    msg := "Container must drop ALL capabilities"
}
Enter fullscreen mode Exit fullscreen mode

Integrate OPA with ECS:

import requests
import json

def validate_task_definition_with_opa(task_definition):
    """Validate task definition against OPA policies"""

    opa_url = "http://opa-service:8181/v1/data/container/security"

    # Prepare input for OPA
    input_data = {
        "container": task_definition['containerDefinitions'][0]
    }

    response = requests.post(
        f"{opa_url}/deny",
        json={"input": input_data}
    )

    if response.status_code == 200:
        result = response.json()
        if result.get('result'):
            # Policy violations found
            violations = result['result']
            raise ValueError(f"Policy violations: {violations}")

    return True
Enter fullscreen mode Exit fullscreen mode

Complete CI/CD Pipeline with Security

Secure Build Pipeline

# pipeline.yaml
version: 0.2
phases:
  pre_build:
    commands:
      - echo Checking base image...
      - BASE_IMAGE=$(grep '^FROM' Dockerfile | head -1 | awk '{print $2}')
      - trivy image --severity HIGH,CRITICAL --exit-code 1 "$BASE_IMAGE"
      - echo Checking for secrets in Dockerfile...
      - python check-dockerfile-secrets.py
  build:
    commands:
      - echo Building Docker image...
      - docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG .
      - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $REPOSITORY_URI:$IMAGE_TAG
  post_build:
    commands:
      - echo Scanning image with Trivy...
      - trivy image --severity HIGH,CRITICAL --exit-code 1 $REPOSITORY_URI:$IMAGE_TAG
      - echo Pushing to ECR...
      - docker push $REPOSITORY_URI:$IMAGE_TAG
      - echo Waiting for ECR scan...
      - |
        aws ecr wait image-scan-complete \
          --repository-name $IMAGE_REPO_NAME \
          --image-id imageTag=$IMAGE_TAG
      - echo Checking ECR scan results...
      - python check-ecr-scan-results.py $IMAGE_REPO_NAME $IMAGE_TAG
      - echo Signing image...
      - aws signer start-signing-job --profile-name payment-app-signing-profile --source ...
      - echo Validating task definition...
      - python validate-task-definition.py task-definition.json
      - echo All security checks passed!
Enter fullscreen mode Exit fullscreen mode

Best Practices Summary

Do's ✅

  1. Use minimal base images (distroless, alpine)
  2. Multi-stage builds to reduce image size
  3. Never include secrets in images
  4. Scan images before deployment
  5. Sign images for integrity verification
  6. Run as non-root user
  7. Drop all capabilities by default
  8. Use readonly root filesystem
  9. Set resource limits
  10. Enable network policies

Don'ts ❌

  1. Don't use latest tags in production
  2. Don't run as root user
  3. Don't include secrets in images or Dockerfiles
  4. Don't skip scanning before deployment
  5. Don't use privileged mode unless absolutely necessary
  6. Don't ignore security findings
  7. Don't disable read-only filesystem without justification
  8. Don't forget to sign images

Conclusion

Securing containers requires a comprehensive approach across the entire lifecycle. Key takeaways:

  1. Minimal base images reduce attack surface significantly
  2. AWS Secrets Manager eliminates secrets from images
  3. ECR scanning catches vulnerabilities before deployment
  4. Image signing ensures integrity and authenticity
  5. Runtime security (least privilege, isolation) protects running containers
  6. Compliance automation ensures continuous adherence to standards

The result? A secure, compliant containerized application that meets financial services requirements while maintaining operational efficiency.

Additional Resources

Top comments (0)