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)