Advanced Cloud

Serverless and container security in the cloud — Lambda, ECR, EKS best practices

Serverless and containers changed the operations model, but they did not eliminate the attack surface — they redistributed it. In Lambda, code can still leak secrets or be injected via payload. In containers, an image with critical vulnerabilities reaches production directly if there is no scanning in the pipeline.

AWS Lambda — attack surface and controls

Execution role with least privilege

// Bad — role with excessive permissions
{
  "Effect": "Allow",
  "Action": "*",
  "Resource": "*"
}

// Good — only what the specific function needs
{
  "Effect": "Allow",
  "Action": [
    "s3:GetObject"
  ],
  "Resource": "arn:aws:s3:::reports-prod/input/*"
}

Environment variables encrypted with KMS

# Create function with encrypted variables
aws lambda create-function \
  --function-name payment-processor \
  --runtime python3.12 \
  --role arn:aws:iam::123456789:role/lambda-payment-role \
  --kms-key-arn arn:aws:kms:us-east-1:123456789:key/abc-123 \
  --environment Variables='{API_KEY_SECRET_ARN=arn:aws:secretsmanager:...}' \
  --zip-file fileb://function.zip \
  --handler handler.main

Prefer Secrets Manager references over injecting the value directly into the environment variable.

Resource policy — who can invoke

# Allow invocation only from this account's API Gateway
aws lambda add-permission \
  --function-name payment-processor \
  --statement-id allow-apigw \
  --action lambda:InvokeFunction \
  --principal apigateway.amazonaws.com \
  --source-arn "arn:aws:execute-api:us-east-1:123456789:abc123/prod/POST/payment" \
  --source-account 123456789

Input validation

import json, re

def handler(event, context):
    # Never trust the payload — always validate
    body = json.loads(event.get("body", "{}"))
    
    amount = body.get("amount")
    if not isinstance(amount, (int, float)) or amount <= 0 or amount > 1_000_000:
        return {"statusCode": 400, "body": "Invalid amount"}
    
    order_id = body.get("order_id", "")
    if not re.fullmatch(r"[A-Z0-9\-]{8,32}", order_id):
        return {"statusCode": 400, "body": "Invalid order_id"}
    
    # Safe processing...

Amazon ECR — image security

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

# View vulnerability scan results
aws ecr describe-image-scan-findings \
  --repository-name my-app \
  --image-id imageTag=v1.2.3 \
  --query 'imageScanFindings.findingSeverityCounts'

# Lifecycle policy — remove old unscanned images
aws ecr put-lifecycle-policy \
  --repository-name my-app \
  --lifecycle-policy-text '{
    "rules": [{
      "rulePriority": 1,
      "description": "Expire untagged images older than 7 days",
      "selection": {
        "tagStatus": "untagged",
        "countType": "sinceImagePushed",
        "countUnit": "days",
        "countNumber": 7
      },
      "action": { "type": "expire" }
    }]
  }'

Amazon EKS — cluster security

Pod Security Admission

# Namespace with restrictive enforcement
apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/audit: restricted

IRSA — IAM Roles for Service Accounts

# Associate an IAM role with a Kubernetes ServiceAccount
eksctl create iamserviceaccount \
  --cluster my-cluster \
  --namespace production \
  --name sa-processor \
  --attach-policy-arn arn:aws:iam::123456789:policy/processor-policy \
  --approve
apiVersion: v1
kind: ServiceAccount
metadata:
  name: sa-processor
  namespace: production
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789:role/processor-role

Network Policy for pod isolation

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: backend-isolation
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes: [Ingress, Egress]
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend
    ports:
    - port: 8080
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: postgres
    ports:
    - port: 5432

Unified checklist

Lambda:
  - Execution role with least privilege (no Action: *)
  - Sensitive values via Secrets Manager, not env vars
  - Resource policy restricted to the legitimate invoker
  - Input validation in the handler
  - Timeout and concurrency limit configured
  - VPC only when necessary (increases cold start)

ECR / Images:
  - Scan on push enabled
  - Minimal base images (distroless, alpine)
  - Non-root user in Dockerfile
  - Fixed tag — never :latest in production
  - Pipeline blocks images with CRITICAL/HIGH findings

EKS:
  - Pod Security Admission enforce: restricted
  - IRSA for AWS access (no access keys in pods)
  - Network Policies in all namespaces
  - RBAC with dedicated ServiceAccounts
  - Admission controller (Kyverno, OPA Gatekeeper)
  - Runtime security: Falco for anomalous behavior detection