Skip to main content

Container and cloud infrastructure security

Containers and cloud infrastructure create new attack surfaces that traditional application security doesn't cover. Your Docker image might contain a vulnerable version of OpenSSL. Your S3 bucket might be publicly readable. Your IAM role might have permissions to access everything in your AWS account. Your Kubernetes cluster might allow anyone to deploy workloads.

These misconfigurations are the low-hanging fruit attackers look for. They're easy to exploit and often grant immediate access to sensitive data or systems. The good news: they're also easy to detect with automated tools. This chapter covers how to scan container images, secure cloud resources, protect Kubernetes clusters, and audit your infrastructure before attackers find the gaps.

Why this matters for small companies

Cloud misconfigurations are consistently among the top causes of data breaches. And unlike code vulnerabilities that require exploitation, a misconfigured S3 bucket or exposed database is immediately accessible — no exploit development needed.

Containers hide vulnerabilities. When you pull node:18 or python:3.11 from Docker Hub, you're pulling an entire Linux distribution with hundreds of packages. Any of those packages might have known vulnerabilities. You didn't write that code, but you're responsible for the security of everything in your container.

Cloud defaults aren't secure. AWS, GCP, and Azure prioritize getting things working over security. Default settings often allow more access than needed. A developer spinning up resources quickly might leave storage public, ports open, or encryption disabled — because that's the fastest path to "it works."

Small teams move fast and skip reviews. Nobody is double-checking Terraform configurations or reviewing IAM policies. Infrastructure changes go straight to production. Automated scanning is your safety net.

Kubernetes adds complexity. If you're running on EKS, GKE, or AKS, you've added another layer of configuration that can be misconfigured. Default Kubernetes settings are notoriously permissive.

Attackers scan the entire internet. Services like Shodan continuously index exposed databases, open S3 buckets, and misconfigured servers. Your accidentally-public resource will be discovered in hours, not weeks. Security researchers at Comparitech found that exposed databases are discovered by malicious actors within hours of being connected to the internet.

Container security

Container security starts with understanding what's inside your images. A typical Docker image contains:

  • Base OS packages (apt, yum, apk)
  • Language runtime (Node, Python, Java, Go)
  • Application dependencies (npm packages, pip packages)
  • Your application code

Vulnerabilities can exist in any layer. Base image vulnerabilities are especially dangerous because they affect every container built on that image.

Building secure images

Before you scan, build images that are easier to secure:

Use minimal base images. Alpine Linux images are 5-10 MB instead of 100+ MB for Debian/Ubuntu. Fewer packages mean fewer vulnerabilities.

# Instead of this (150+ MB, hundreds of packages)
FROM node:20

# Use this (50 MB, minimal packages)
FROM node:20-alpine

Consider distroless images. Google's distroless images contain only your application and its runtime dependencies — no shell, no package manager, no utilities attackers could use. They're the most secure option for production.

# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

# Production stage with distroless
FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["dist/server.js"]

Distroless benefits:

  • No shell = attackers can't get interactive access even if they exploit a vulnerability
  • No package manager = no way to install additional tools
  • Minimal attack surface = fewer components that could have vulnerabilities
  • Smaller images = faster deploys, less storage

The tradeoff: debugging is harder since you can't shell into the container. For production, that's a feature, not a bug.

Pin versions explicitly. Don't use latest — it changes without notice. Pin to specific versions so builds are reproducible and you control when to update.

# BAD: could be anything
FROM python:latest

# GOOD: explicit version, reproducible builds
FROM python:3.12.3-alpine3.19

Don't run as root. Container processes running as root can escape to the host in some configurations. Create a non-root user:

FROM node:20-alpine

# Create non-root user
RUN addgroup -g 1001 -S appgroup && \
adduser -S appuser -u 1001 -G appgroup

# Set working directory
WORKDIR /app

# Copy and install dependencies first (better caching)
COPY package*.json ./
RUN npm ci --only=production

# Copy application code
COPY --chown=appuser:appgroup . .

# Switch to non-root user
USER appuser

EXPOSE 3000
CMD ["node", "server.js"]

Use multi-stage builds. Keep build tools out of production images:

# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production stage — only runtime, no build tools
FROM node:20-alpine
WORKDIR /app
RUN adduser -S appuser
COPY --from=builder --chown=appuser /app/dist ./dist
COPY --from=builder --chown=appuser /app/node_modules ./node_modules
USER appuser
EXPOSE 3000
CMD ["node", "dist/server.js"]

Secrets in containers

Never put API keys, passwords, or certificates in Dockerfiles. Use environment variables or secrets management at runtime.

# BAD: secret baked into image
ENV DATABASE_PASSWORD=supersecret

# GOOD: expect secrets at runtime
ENV DATABASE_PASSWORD=""
# Injected at runtime: docker run -e DATABASE_PASSWORD=$SECRET ...

Docker Compose with secrets:

version: '3.8'
services:
app:
image: myapp:latest
secrets:
- db_password
- api_key
environment:
- DB_PASSWORD_FILE=/run/secrets/db_password

secrets:
db_password:
file: ./secrets/db_password.txt
api_key:
external: true # Managed externally

Integration with Passwork: Use passwork-cli to inject secrets at container runtime:

# Pull secrets and start container
passwork-cli exec --folder-id "<folder-id>" docker compose up -d

# Or inject specific secrets
export DB_PASSWORD=$(passwork-cli get --password-id "<id>" --field password)
docker run -e DB_PASSWORD="$DB_PASSWORD" myapp:latest

For Kubernetes, see the Kubernetes secrets section below.

Image scanning with Trivy

Trivy is the standard tool for container image scanning. It's fast, accurate, and detects vulnerabilities in OS packages, language dependencies, and misconfigurations.

Install Trivy:

# macOS
brew install trivy

# Linux
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin

# Docker
docker pull aquasec/trivy

Scan an image:

# Scan local image
trivy image myapp:latest

# Scan image from registry
trivy image nginx:1.25

# Scan with severity filter
trivy image --severity HIGH,CRITICAL myapp:latest

# Output as JSON for CI integration
trivy image --format json --output results.json myapp:latest

# Scan and fail if vulnerabilities found
trivy image --exit-code 1 --severity CRITICAL myapp:latest

Example output:

myapp:latest (alpine 3.19.1)
Total: 3 (HIGH: 2, CRITICAL: 1)

┌──────────────┬────────────────┬──────────┬───────────────────┐
│ Library │ Vulnerability │ Severity │ Fixed Version │
├──────────────┼────────────────┼──────────┼───────────────────┤
│ openssl │ CVE-2024-0727 │ HIGH │ 3.1.4-r5 │
│ busybox │ CVE-2023-42365 │ HIGH │ 1.36.1-r15 │
│ libcrypto3 │ CVE-2024-0727 │ CRITICAL │ 3.1.4-r5 │
└──────────────┴────────────────┴──────────┴───────────────────┘

CI/CD integration for container scanning

Add image scanning to your pipeline so vulnerable images never reach production.

GitHub Actions:

name: Container Security

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Build 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: 'table'
exit-code: '1' # Fail on HIGH/CRITICAL
severity: 'HIGH,CRITICAL'
ignore-unfixed: true

GitLab CI:

container_scanning:
stage: test
image:
name: aquasec/trivy
entrypoint: [""]
script:
- trivy image --exit-code 1 --severity HIGH,CRITICAL $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
allow_failure: false

Image signing and verification

After supply chain attacks like SolarWinds and CodeCov, verifying image authenticity became critical. Image signing ensures that:

  1. The image hasn't been tampered with
  2. It was built by your CI/CD pipeline, not an attacker
  3. Only signed images can be deployed to production

Cosign (by Sigstore) is the standard tool for container image signing:

# Install cosign
brew install cosign # macOS
# or download from https://github.com/sigstore/cosign/releases

# Generate a key pair (do this once, store private key securely)
cosign generate-key-pair

# Sign an image
cosign sign --key cosign.key myregistry.com/myapp:v1.0.0

# Verify a signature
cosign verify --key cosign.pub myregistry.com/myapp:v1.0.0

Keyless signing with Sigstore (recommended for CI/CD):

# GitHub Actions with keyless signing
name: Build and Sign

on:
push:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # Required for keyless signing
packages: write

steps:
- uses: actions/checkout@v4

- name: Install cosign
uses: sigstore/cosign-installer@v3

- name: Login to registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push
id: build
uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}

- name: Sign image with cosign
run: |
cosign sign --yes ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}

Keyless signing uses OIDC (OpenID Connect) — no private keys to manage. The signature includes proof that the image was built by a specific GitHub Actions workflow.

Container registry security

Don't pull images from Docker Hub without validation. Use a private registry with security controls.

Registry options:

RegistryBest forFeatures
AWS ECRAWS-native appsIAM integration, automatic scanning, image signing
Google Artifact RegistryGCP appsIAM, vulnerability scanning, SBOM generation
Azure Container RegistryAzure appsAAD integration, geo-replication, content trust
HarborSelf-hosted, multi-cloudVulnerability scanning, RBAC, image signing, replication
GitHub Container RegistryOpen source, GitHub-nativeFree for public repos, GitHub Actions integration

Registry security checklist:

  • Enable vulnerability scanning on push
  • Require image signing for production images
  • Set retention policies to delete old, untagged images
  • Use immutable tags (can't overwrite existing tags)
  • Enable audit logging for all pull/push operations
  • Restrict who can push images (CI/CD service accounts only)
  • Block pulls of images with critical vulnerabilities

AWS ECR example:

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

# Set lifecycle policy (delete untagged images after 7 days)
aws ecr put-lifecycle-policy \
--repository-name myapp \
--lifecycle-policy-text '{
"rules": [{
"rulePriority": 1,
"description": "Delete untagged images",
"selection": {
"tagStatus": "untagged",
"countType": "sinceImagePushed",
"countUnit": "days",
"countNumber": 7
},
"action": { "type": "expire" }
}]
}'

Runtime security

Scanning images catches known vulnerabilities. But what about:

  • Zero-day exploits
  • Attackers who got in through application vulnerabilities
  • Cryptominers deployed via compromised dependencies
  • Data exfiltration attempts

Runtime security tools monitor container behavior and alert on anomalies.

Falco is the standard open-source runtime security tool:

# Install Falco
helm repo add falcosecurity https://falcosecurity.github.io/charts
helm install falco falcosecurity/falco --namespace falco --create-namespace

Falco detects:

  • Shell spawned inside container
  • Sensitive file access (/etc/passwd, /etc/shadow)
  • Network connections to unusual destinations
  • Privilege escalation attempts
  • Cryptomining processes

Example Falco alerts:

15:23:45.123 Warning: Shell spawned in container (container_id=abc123 container_name=myapp shell=/bin/sh)
15:24:01.456 Critical: Sensitive file opened for reading (file=/etc/shadow container=myapp)
15:25:33.789 Warning: Outbound connection to known cryptomining pool (dest=pool.minexmr.com container=myapp)

Falco rule example:

# Detect when someone runs shell in production container
- rule: Shell in Production Container
desc: Detect shell execution in production containers
condition: >
spawned_process and
container and
proc.name in (bash, sh, zsh, ksh) and
container.image.repository contains "prod"
output: >
Shell spawned in production container
(user=%user.name container=%container.name shell=%proc.name)
priority: WARNING

Runtime security tools comparison:

ToolLicenseBest forLink
FalcoApache 2.0Kubernetes, general runtime detectionfalco.org
SysdigCommercial (free tier)Enterprise, compliance, forensicssysdig.com
AquaCommercialFull container lifecycle securityaquasec.com
TetragonApache 2.0eBPF-based, low overheadtetragon.io

For small teams, start with Falco. It's free, well-documented, and catches most runtime threats.

Container scanning tools reference

ToolBest forLicenseLink
TrivyAll-in-one scanning, easy setupApache 2.0trivy.dev
GrypeFast CVE scanning, pairs with SyftApache 2.0github.com/anchore/grype
ClairRegistry integration, enterprise scaleApache 2.0github.com/quay/clair
Docker ScoutDocker Hub integration, nativeFree tier availabledocker.com/products/docker-scout
Snyk ContainerDeveloper-friendly, remediation adviceFree tier availablesnyk.io/product/container-security

For small teams, Trivy is the clear choice. It's free, fast, and catches most vulnerabilities. If you're using Docker Hub, Docker Scout is convenient since it's built into the Docker CLI (docker scout cves myimage:tag).

Kubernetes security

If you're running containers in Kubernetes (EKS, GKE, AKS, or self-managed), you have another layer of security to configure. Kubernetes defaults are permissive — designed for getting started quickly, not for production security.

Kubernetes security fundamentals

The attack surface:

ComponentRisk if misconfigured
API ServerFull cluster access, can deploy any workload
etcdContains all cluster secrets and state
KubeletNode-level access, can run any container
PodsContainer escape, privilege escalation
NetworkPod-to-pod attacks, data exfiltration
RBACPrivilege escalation, unauthorized access
SecretsCredential theft, data exposure

Pod security

Kubernetes 1.25+ uses Pod Security Standards enforced by Pod Security Admission:

LevelWhat it allowsUse case
PrivilegedEverythingSystem components only
BaselinePrevents known privilege escalationsMost applications
RestrictedMaximum security, minimal privilegesSensitive workloads

Enable Pod Security Admission:

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

Secure pod specification:

apiVersion: v1
kind: Pod
metadata:
name: secure-app
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: myapp:v1.0.0
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
limits:
cpu: "500m"
memory: "256Mi"
requests:
cpu: "100m"
memory: "128Mi"

RBAC best practices

Role-Based Access Control (RBAC) determines who can do what in your cluster.

Audit existing permissions:

# Who can create pods?
kubectl auth can-i create pods --all-namespaces --list

# What can a specific service account do?
kubectl auth can-i --list --as=system:serviceaccount:default:myapp

# Find overly permissive ClusterRoleBindings
kubectl get clusterrolebindings -o json | \
jq '.items[] | select(.roleRef.name=="cluster-admin") | .subjects'

Least privilege RBAC:

# Role with minimal permissions
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: myapp
name: myapp-role
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["myapp-secrets"] # Only specific secret
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: myapp-binding
namespace: myapp
subjects:
- kind: ServiceAccount
name: myapp
namespace: myapp
roleRef:
kind: Role
name: myapp-role
apiGroup: rbac.authorization.k8s.io

RBAC anti-patterns to avoid:

# BAD: Wildcard permissions
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["*"]

# BAD: cluster-admin for applications
roleRef:
kind: ClusterRole
name: cluster-admin

# BAD: Default service account with permissions
# (Don't add permissions to "default" service account)

Network policies

By default, all pods can communicate with all other pods. Network policies restrict this.

# Deny all ingress by default
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: production
spec:
podSelector: {}
policyTypes:
- Ingress
---
# Allow only specific traffic
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: production
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080

Secrets in Kubernetes

Kubernetes secrets are base64-encoded, not encrypted. Anyone with access to etcd or the API can read them.

Better options:

  1. Enable encryption at rest:
# /etc/kubernetes/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <base64-encoded-32-byte-key>
- identity: {}
  1. Use External Secrets Operator with Passwork:
# ExternalSecret that pulls from Passwork
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-secrets
namespace: myapp
spec:
refreshInterval: 1h
secretStoreRef:
name: passwork-store
kind: SecretStore
target:
name: app-secrets
data:
- secretKey: database-password
remoteRef:
key: database-credentials
property: password
  1. Use cloud provider secrets managers:
# AWS Secrets Manager with EKS
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: aws-secrets
spec:
provider: aws
parameters:
objects: |
- objectName: "myapp/database"
objectType: "secretsmanager"

Kubernetes security auditing

Kubescape scans clusters against security frameworks:

# Install
curl -s https://raw.githubusercontent.com/kubescape/kubescape/master/install.sh | /bin/bash

# Scan cluster
kubescape scan framework nsa

# Scan against CIS benchmark
kubescape scan framework cis-v1.23-t1.0.1

# Scan specific namespace
kubescape scan framework nsa --include-namespaces production

kube-bench checks against CIS Kubernetes Benchmark:

# Run as a job in the cluster
kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml

# View results
kubectl logs -l app=kube-bench

Kubernetes security tools:

ToolWhat it checksLink
KubescapeNSA/CISA, MITRE ATT&CK, CIS benchmarkskubescape.io
kube-benchCIS Kubernetes Benchmarkgithub.com/aquasecurity/kube-bench
PolarisConfiguration best practicesgithub.com/FairwindsOps/polaris
kube-hunterPenetration testing for K8sgithub.com/aquasecurity/kube-hunter

Cloud infrastructure security

Cloud providers give you the building blocks, but security is your responsibility. This is the "shared responsibility model" — AWS/GCP/Azure secure the physical infrastructure and hypervisor, you secure everything you configure.

Common cloud misconfigurations

These are the issues that cause breaches:

MisconfigurationRiskHow it happens
Public S3/GCS bucketsData exposureDeveloper sets public access "temporarily" for testing
Overly permissive IAMPrivilege escalationUsing * in IAM policies for convenience
Unencrypted storageData theftDefault encryption not enabled
Open security groupsUnauthorized accessSSH/RDP open to 0.0.0.0/0 during debugging
Exposed databasesData breachRDS/CloudSQL with public IP and weak password
Missing loggingIncident blindnessCloudTrail/audit logs not enabled
Unused access keysCredential compromiseOld developer's keys never rotated
No MFA on root accountAccount takeoverRoot credentials compromised, full access
Cross-account trustLateral movementOverly broad AssumeRole permissions

Security principles for cloud resources

Least privilege everywhere. Every IAM role, every security group, every service account should have minimum required permissions. Start with zero access, add only what's needed.

// BAD: Full admin access
{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}

// GOOD: Specific permissions for specific resources
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::my-app-uploads/*"
}

Encrypt everything. Enable encryption at rest for all storage (S3, RDS, EBS, GCS). Use TLS for all data in transit. Enable default encryption so new resources are automatically protected.

No public access by default. Block public access at the account level for S3, require private subnets for databases, use VPN or bastion hosts for administrative access.

Enable logging. Turn on CloudTrail (AWS), Cloud Audit Logs (GCP), or Activity Log (Azure) from day one. You can't investigate what you didn't log.

Tag everything. Tags like environment, owner, cost-center help you understand what's running and who's responsible. Untagged resources are often orphaned and forgotten — perfect targets.

Network security in cloud

VPC design principles:

Three subnet tiers, each replicated across availability zones:

  • Public subnets (AZ-a, AZ-b) — Load Balancer, NAT Gateway. Only these have internet-facing exposure.
  • Private subnets (AZ-a, AZ-b) — App servers. No public IP. Outbound internet via NAT Gateway only.
  • Database subnets (AZ-a, AZ-b) — RDS / databases. No internet access at all, reachable only from private subnets.

Network security checklist:

  • Databases in private subnets (no public IP)
  • NAT Gateway for outbound internet from private subnets
  • Security groups: deny by default, allow specific ports/sources
  • NACLs as additional layer for subnet-level filtering
  • VPC Flow Logs enabled for traffic analysis
  • PrivateLink/Private Service Connect for AWS/GCP service access
  • WAF in front of public-facing applications
  • DDoS protection (AWS Shield, Cloud Armor, Azure DDoS Protection)

Security group best practices:

# BAD: SSH open to the world
aws ec2 authorize-security-group-ingress \
--group-id sg-123 \
--protocol tcp \
--port 22 \
--cidr 0.0.0.0/0

# GOOD: SSH only from VPN/bastion
aws ec2 authorize-security-group-ingress \
--group-id sg-123 \
--protocol tcp \
--port 22 \
--source-group sg-bastion

AWS security checklist

CategoryCheckHow to verify
AccountMFA on root accountConsole → IAM → Root account MFA
AccountNo root access keysaws iam get-account-summary
IAMPassword policy enforcedaws iam get-account-password-policy
IAMNo inline policiesaws iam list-user-policies
IAMUnused credentials disabledIAM Credential Report
S3Public access blockedaws s3control get-public-access-block
S3Default encryption enabledaws s3api get-bucket-encryption
S3Access logging enabledaws s3api get-bucket-logging
EC2EBS encryption defaultaws ec2 get-ebs-encryption-by-default
EC2No public AMIsaws ec2 describe-images --owners self
RDSNot publicly accessibleaws rds describe-db-instances
RDSEncryption at restaws rds describe-db-instances
CloudTrailEnabled, multi-regionaws cloudtrail describe-trails
ConfigRecording enabledaws configservice describe-configuration-recorders
GuardDutyEnabledaws guardduty list-detectors

AWS quick wins script:

#!/bin/bash
# aws-security-quick-wins.sh

# Block public access at account level
aws s3control put-public-access-block \
--account-id $(aws sts get-caller-identity --query Account --output text) \
--public-access-block-configuration \
"BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"

# Enable default EBS encryption
aws ec2 enable-ebs-encryption-by-default

# Enable GuardDuty
aws guardduty create-detector --enable

# Enable Security Hub
aws securityhub enable-security-hub

# Create CloudTrail (if not exists)
aws cloudtrail create-trail \
--name main-trail \
--s3-bucket-name my-cloudtrail-logs \
--is-multi-region-trail \
--enable-log-file-validation
aws cloudtrail start-logging --name main-trail

echo "AWS security quick wins applied!"

GCP security checklist

CategoryCheckHow to verify
IAMNo primitive roles (Owner/Editor)gcloud projects get-iam-policy PROJECT
IAMService account key rotationgcloud iam service-accounts keys list
StorageUniform bucket-level accessgsutil uniformbucketlevelaccess get
StorageNo public bucketsgsutil iam get gs://BUCKET
ComputeNo default service accountgcloud compute instances list
ComputeShielded VMs enabledgcloud compute instances describe
LoggingAudit logs enabledConsole → IAM → Audit Logs
VPCPrivate Google Access enabledgcloud compute networks subnets list
GKEPrivate clustergcloud container clusters describe
GKEWorkload Identity enabledgcloud container clusters describe

GCP quick wins:

# Enable organization policies (requires Org Admin)
gcloud resource-manager org-policies allow \
compute.requireShieldedVm --organization=ORG_ID

gcloud resource-manager org-policies deny \
iam.allowedPolicyMemberDomains --organization=ORG_ID

# Enable audit logs for all services
# (Best done in Console: IAM → Audit Logs → Default Config)

# Check for public buckets
for bucket in $(gsutil ls); do
echo "Checking $bucket..."
gsutil iam get $bucket | grep -i allUsers && echo "WARNING: $bucket is public!"
done

Azure security checklist

CategoryCheckHow to verify
AADMFA enforcedAzure AD → Security → MFA
AADConditional Access policiesAzure AD → Security → Conditional Access
AADNo guest users with adminAzure AD → Users → Guest users
StorageSecure transfer requiredaz storage account show
StoragePrivate endpoint enabledaz storage account show
SQLAzure AD auth enabledaz sql server ad-admin list
SQLTDE enabledaz sql db tde show
NetworkingNSG flow logs enabledaz network watcher flow-log list
DefenderDefender for Cloud enabledaz security pricing list
KeyVaultSoft delete enabledaz keyvault show

Azure quick wins:

# Enable Defender for Cloud free tier
az security pricing create --name VirtualMachines --tier free

# Check for public storage accounts
az storage account list --query "[?allowBlobPublicAccess==\`true\`].name"

# Enable secure transfer for all storage accounts
for account in $(az storage account list --query "[].name" -o tsv); do
az storage account update --name $account --https-only true
done

# Enable activity log alerts
az monitor activity-log alert create \
--name "Security Alert" \
--resource-group my-rg \
--condition category=Security

Auditing cloud infrastructure

Automated auditing tools check your cloud configuration against security best practices. They compare your setup to benchmarks like CIS (Center for Internet Security) and flag misconfigurations.

ScoutSuite

ScoutSuite is a multi-cloud security auditing tool. It supports AWS, Azure, GCP, Alibaba Cloud, and Oracle Cloud.

Install:

pip install scoutsuite

Run audit:

# AWS (uses your default credentials)
scout aws

# GCP
scout gcp --user-account

# Azure
scout azure --cli

ScoutSuite generates an HTML report with findings grouped by service and severity. Each finding includes the affected resource, why it's a problem, and how to fix it.

Example findings:

  • S3 buckets with public read access
  • IAM users without MFA enabled
  • Security groups with SSH open to the internet
  • RDS instances publicly accessible
  • Unencrypted EBS volumes

Prowler

Prowler is specifically for AWS and provides the most comprehensive AWS security assessment. It checks against CIS AWS Foundations Benchmark, PCI-DSS, HIPAA, and other compliance frameworks.

Install:

pip install prowler

Run audit:

# Full AWS audit
prowler aws

# Specific checks only
prowler aws --checks ec2_instance_public_ip,s3_bucket_public_access

# Output formats
prowler aws -M csv,html,json

# Check specific region
prowler aws -f us-east-1

# Check against specific compliance framework
prowler aws --compliance cis_2.0_aws

CI/CD integration:

# GitHub Actions
name: Cloud Security Audit

on:
schedule:
- cron: '0 6 * * 1' # Weekly on Monday at 6 AM

jobs:
prowler:
runs-on: ubuntu-latest
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1

- name: Install Prowler
run: pip install prowler

- name: Run Prowler
run: prowler aws -M json -o results/

- name: Upload results
uses: actions/upload-artifact@v4
with:
name: prowler-report
path: results/

Cloud-native security tools

Cloud providers offer their own security assessment tools:

ProviderToolCostWhat it checks
AWSSecurity HubFree tier + per findingAggregates findings from multiple sources, CIS benchmarks
AWSTrusted AdvisorFree (limited) / Business+Cost, performance, security, fault tolerance
AWSGuardDutyPer GB analyzedThreat detection, anomaly detection
GCPSecurity Command CenterFree tier availableVulnerabilities, misconfigurations, threats
GCPSecurity Health AnalyticsFree with SCCBest practice violations
AzureDefender for CloudFree tier availableSecurity posture, compliance, threat protection
AzureSecure ScoreFreeOverall security posture rating

For small teams, start with the free open-source tools (ScoutSuite, Prowler) and enable the free tiers of cloud-native tools. They're complementary — cloud-native tools often catch runtime threats that audit tools miss.

Handling security findings

When Prowler finds 200 issues or Trivy reports 50 vulnerabilities, you need a systematic approach to prioritize and remediate.

Triage process

Step 1: Categorize by severity and exploitability

PriorityCriteriaAction timeline
P1 - CriticalActively exploited OR public-facing with known exploitFix within 24 hours
P2 - HighHigh severity, easy to exploit, production impactFix within 1 week
P3 - MediumMedium severity, requires conditions to exploitFix within 1 month
P4 - LowLow severity, defense in depthFix in next maintenance window
AcceptVery low risk, no fix available, compensating controlsDocument and accept

Step 2: Verify the finding is real

Not all findings are actual problems:

# Example: Prowler says S3 bucket is public
# Verify manually
aws s3api get-bucket-acl --bucket my-bucket
aws s3api get-bucket-policy --bucket my-bucket
aws s3api get-public-access-block --bucket my-bucket

# It might be intentionally public (static website hosting)
# If intentional, document and suppress future alerts

Step 3: Create remediation tickets

For each P1-P3 finding:

  • What is the issue?
  • What resource is affected?
  • How to fix it?
  • Who owns the fix?
  • Deadline based on priority

Step 4: Track remediation

# Security Findings Tracker

| Finding ID | Description | Priority | Owner | Deadline | Status |
|------------|-------------|----------|-------|----------|--------|
| PROWLER-001 | S3 bucket public | P1 | DevOps | 2024-01-15 | Fixed |
| TRIVY-042 | OpenSSL CVE-2024-0727 | P2 | Dev | 2024-01-20 | In Progress |
| SCOUT-018 | CloudTrail not enabled | P2 | DevOps | 2024-01-22 | Open |

Suppressing false positives

When a finding is intentional or a false positive, suppress it to keep reports clean:

Trivy:

# .trivyignore
# Ignore specific CVE (with reason)
CVE-2024-0727 # Fixed in next base image update, not exploitable in our context

# Ignore by package
pkg:apk/busybox

Prowler:

# Create allowlist file
prowler aws --allowlist allowlist.yaml
# allowlist.yaml
Accounts:
"123456789012":
Checks:
s3_bucket_public_access:
Regions:
- us-east-1
Resources:
- "arn:aws:s3:::my-public-website-bucket" # Intentionally public

ScoutSuite:

# scout-exceptions.json
{
"s3": {
"buckets": {
"my-public-website-bucket": ["is_public"]
}
}
}

Common mistakes to avoid

Scanning images but not fixing findings. A scan that finds 50 critical vulnerabilities and nothing happens is worse than no scan — it creates false confidence. Prioritize CRITICAL findings and fix them.

Using latest tags in production. Your Dockerfile says FROM node:latest. Today it's Node 20.12. Tomorrow it's Node 21 with breaking changes. Pin versions.

Ignoring base image updates. Your application code is fine, but your base image from 6 months ago has 20 new CVEs. Rebuild images regularly, even if your code hasn't changed.

Granting admin access "temporarily." A developer needs to debug something, gets admin permissions, and those permissions are never revoked. Create specific, limited permissions from the start.

Disabling security scans to unblock deploys. A scan finds an issue, the team can't fix it immediately, so they disable the scanner. Now they're blind to future vulnerabilities too. Use allowlists for accepted risks instead.

Auditing once instead of continuously. Running Prowler once a year before an audit doesn't help. Configuration drift happens daily. Set up weekly or monthly automated scans.

Treating Kubernetes defaults as secure. They're not. Default pod security allows root, privileged containers, host network access. Enforce Pod Security Standards from day one.

Storing secrets in Kubernetes Secrets without encryption. Base64 is not encryption. Enable encryption at rest or use External Secrets Operator.

Real-world incidents

Capital One breach (2019): A misconfigured WAF allowed an attacker to access IAM credentials via SSRF, leading to 100+ million customer records exposed. The underlying issue was overly permissive IAM roles that allowed access to S3 buckets. Source: Wikipedia

Tesla cryptomining (2018): An exposed Kubernetes dashboard (no authentication) allowed attackers to deploy cryptocurrency miners on Tesla's AWS infrastructure. The dashboard also had access to AWS credentials. Source: Wired

Uber data breach (2016): Attackers found AWS credentials in a private GitHub repository, used them to access S3 buckets with data on 57 million users. The credentials should never have been in a repository, and the S3 buckets should have required additional authentication. Source: Bloomberg

Code Spaces shutdown (2014): Attackers gained access to AWS console, deleted all data, backups, and machine configurations. The company couldn't recover and shut down within 12 hours. No MFA on AWS accounts, no offsite backups. Source: Info Security Magazine

Twitch source code leak (2021): 125GB of data including source code, internal tools, and creator payouts leaked via misconfigured server. Source: The Verge

Verkada camera breach (2021): Attackers accessed 150,000 security cameras at hospitals, prisons, and companies. Root cause: hardcoded admin credentials in the system. Source: Bloomberg

Workshop: container and cloud security

This workshop guides you through setting up container scanning, Kubernetes hardening, and running a cloud audit.

Part 1: Docker image scanning

Task: Add Trivy scanning to your CI/CD pipeline.

  1. Install Trivy locally:

    # macOS
    brew install trivy

    # Linux
    curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | \
    sh -s -- -b /usr/local/bin
  2. Scan an existing image:

    # Scan your application image
    trivy image your-app:latest

    # If you don't have one, scan a common base image
    trivy image node:20-alpine
    trivy image python:3.12-slim
  3. Review the findings:

    • How many CRITICAL vulnerabilities?
    • How many have fixes available (check "Fixed Version" column)?
    • Which package has the most vulnerabilities?
  4. Add scanning to CI/CD:

    For GitHub Actions, add .github/workflows/container-scan.yml:

    name: Container Scan

    on:
    push:
    branches: [main]
    pull_request:

    jobs:
    trivy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4

    - name: Build image
    run: docker build -t app:${{ github.sha }} .

    - name: Scan image
    uses: aquasecurity/trivy-action@master
    with:
    image-ref: 'app:${{ github.sha }}'
    exit-code: '1'
    severity: 'CRITICAL'
    ignore-unfixed: true
  5. Test the pipeline: Push a commit and verify the scan runs.

Part 2: Kubernetes security (if applicable)

Task: Audit your Kubernetes cluster security.

  1. Install Kubescape:

    curl -s https://raw.githubusercontent.com/kubescape/kubescape/master/install.sh | /bin/bash
  2. Run security scan:

    # Scan against NSA/CISA framework
    kubescape scan framework nsa

    # Scan specific namespace
    kubescape scan framework nsa --include-namespaces production
  3. Review and fix top issues:

    • Containers running as root
    • Missing resource limits
    • Privileged containers
    • Missing network policies
  4. Create a hardened pod template for your team to use.

Part 3: Cloud infrastructure audit

Task: Run ScoutSuite or Prowler against your cloud account.

  1. For AWS with Prowler:

    pip install prowler

    # Run focused checks first (faster)
    prowler aws --checks s3_bucket_public_access,iam_user_mfa_enabled,ec2_security_group_default_restrict_traffic

    # Full audit (takes longer)
    prowler aws -M html
  2. For multi-cloud or non-AWS:

    pip install scoutsuite

    # AWS
    scout aws

    # GCP
    scout gcp --user-account

    # Azure
    scout azure --cli
  3. Review findings:

    • Open the generated HTML report
    • Focus on HIGH and CRITICAL severity findings
    • Group by service (S3, IAM, EC2, etc.)
  4. Create a remediation plan:

    • Pick the top 5 findings by severity
    • Document what needs to change
    • Assign owner and deadline

Artifacts to produce

After this workshop, you should have:

  1. Trivy integrated in CI/CD — pipeline YAML file with image scanning
  2. Signed container images — Cosign integrated into build pipeline
  3. Cloud audit report — HTML/PDF export from ScoutSuite or Prowler
  4. Remediation plan — prioritized list of findings with owners and deadlines
  5. Secure Dockerfile template — example Dockerfile following best practices for your stack
  6. Kubernetes security policy (if applicable) — Pod Security Standards, RBAC rules, network policies

Self-check questions

Before moving on, verify you can answer:

  1. What's the difference between Alpine and distroless base images?
  2. Why should you sign container images, and how does Cosign help?
  3. What does Falco detect that Trivy doesn't?
  4. What are Kubernetes Pod Security Standards and their three levels?
  5. How do you properly handle secrets in Kubernetes?
  6. What's the shared responsibility model in cloud security?
  7. Name five common cloud misconfigurations that lead to data breaches.
  8. What does ScoutSuite check that Prowler doesn't (or vice versa)?
  9. How should you prioritize 200 security findings from a cloud audit?
  10. How often should you run cloud security audits?

How to explain this to leadership

When presenting container and cloud security to management:

Start with the risk: "We run dozens of containers on AWS/GCP. One misconfigured setting could expose customer data. These tools automatically find those misconfigurations before attackers do."

Show concrete examples: "Our audit found 3 S3 buckets that were publicly readable. They contained backup files. Here's the list of what we fixed."

Quantify the improvement: "Before these tools, we had no visibility into our cloud security posture. Now we have weekly automated scans that catch issues like [specific example]."

Compare costs: "These tools are free. A data breach costs $4.45 million on average. The tools take a few hours to set up and minutes to run each week."

Connect to compliance: "If we ever need SOC 2 or ISO 27001 certification, these audits are required. Starting now means we're already prepared."

Reference real incidents: "Code Spaces went out of business in 12 hours after attackers got into their AWS console. We've now enabled MFA and reduced permissions to prevent this."

Container security

Kubernetes security

Cloud security auditing

Best practices and benchmarks

What's next

Containers locked down, cloud infrastructure hardened, network segmented. The attack surface is smaller — but not zero.

Next: security logging and monitoring — setting up centralized logging and alerts so that when something does get through, you find out before the damage spreads.