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:
- The image hasn't been tampered with
- It was built by your CI/CD pipeline, not an attacker
- 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:
| Registry | Best for | Features |
|---|---|---|
| AWS ECR | AWS-native apps | IAM integration, automatic scanning, image signing |
| Google Artifact Registry | GCP apps | IAM, vulnerability scanning, SBOM generation |
| Azure Container Registry | Azure apps | AAD integration, geo-replication, content trust |
| Harbor | Self-hosted, multi-cloud | Vulnerability scanning, RBAC, image signing, replication |
| GitHub Container Registry | Open source, GitHub-native | Free 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:
| Tool | License | Best for | Link |
|---|---|---|---|
| Falco | Apache 2.0 | Kubernetes, general runtime detection | falco.org |
| Sysdig | Commercial (free tier) | Enterprise, compliance, forensics | sysdig.com |
| Aqua | Commercial | Full container lifecycle security | aquasec.com |
| Tetragon | Apache 2.0 | eBPF-based, low overhead | tetragon.io |
For small teams, start with Falco. It's free, well-documented, and catches most runtime threats.
Container scanning tools reference
| Tool | Best for | License | Link |
|---|---|---|---|
| Trivy | All-in-one scanning, easy setup | Apache 2.0 | trivy.dev |
| Grype | Fast CVE scanning, pairs with Syft | Apache 2.0 | github.com/anchore/grype |
| Clair | Registry integration, enterprise scale | Apache 2.0 | github.com/quay/clair |
| Docker Scout | Docker Hub integration, native | Free tier available | docker.com/products/docker-scout |
| Snyk Container | Developer-friendly, remediation advice | Free tier available | snyk.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:
| Component | Risk if misconfigured |
|---|---|
| API Server | Full cluster access, can deploy any workload |
| etcd | Contains all cluster secrets and state |
| Kubelet | Node-level access, can run any container |
| Pods | Container escape, privilege escalation |
| Network | Pod-to-pod attacks, data exfiltration |
| RBAC | Privilege escalation, unauthorized access |
| Secrets | Credential theft, data exposure |
Pod security
Kubernetes 1.25+ uses Pod Security Standards enforced by Pod Security Admission:
| Level | What it allows | Use case |
|---|---|---|
| Privileged | Everything | System components only |
| Baseline | Prevents known privilege escalations | Most applications |
| Restricted | Maximum security, minimal privileges | Sensitive 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:
- 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: {}
- 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
- 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:
| Tool | What it checks | Link |
|---|---|---|
| Kubescape | NSA/CISA, MITRE ATT&CK, CIS benchmarks | kubescape.io |
| kube-bench | CIS Kubernetes Benchmark | github.com/aquasecurity/kube-bench |
| Polaris | Configuration best practices | github.com/FairwindsOps/polaris |
| kube-hunter | Penetration testing for K8s | github.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:
| Misconfiguration | Risk | How it happens |
|---|---|---|
| Public S3/GCS buckets | Data exposure | Developer sets public access "temporarily" for testing |
| Overly permissive IAM | Privilege escalation | Using * in IAM policies for convenience |
| Unencrypted storage | Data theft | Default encryption not enabled |
| Open security groups | Unauthorized access | SSH/RDP open to 0.0.0.0/0 during debugging |
| Exposed databases | Data breach | RDS/CloudSQL with public IP and weak password |
| Missing logging | Incident blindness | CloudTrail/audit logs not enabled |
| Unused access keys | Credential compromise | Old developer's keys never rotated |
| No MFA on root account | Account takeover | Root credentials compromised, full access |
| Cross-account trust | Lateral movement | Overly 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
| Category | Check | How to verify |
|---|---|---|
| Account | MFA on root account | Console → IAM → Root account MFA |
| Account | No root access keys | aws iam get-account-summary |
| IAM | Password policy enforced | aws iam get-account-password-policy |
| IAM | No inline policies | aws iam list-user-policies |
| IAM | Unused credentials disabled | IAM Credential Report |
| S3 | Public access blocked | aws s3control get-public-access-block |
| S3 | Default encryption enabled | aws s3api get-bucket-encryption |
| S3 | Access logging enabled | aws s3api get-bucket-logging |
| EC2 | EBS encryption default | aws ec2 get-ebs-encryption-by-default |
| EC2 | No public AMIs | aws ec2 describe-images --owners self |
| RDS | Not publicly accessible | aws rds describe-db-instances |
| RDS | Encryption at rest | aws rds describe-db-instances |
| CloudTrail | Enabled, multi-region | aws cloudtrail describe-trails |
| Config | Recording enabled | aws configservice describe-configuration-recorders |
| GuardDuty | Enabled | aws 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
| Category | Check | How to verify |
|---|---|---|
| IAM | No primitive roles (Owner/Editor) | gcloud projects get-iam-policy PROJECT |
| IAM | Service account key rotation | gcloud iam service-accounts keys list |
| Storage | Uniform bucket-level access | gsutil uniformbucketlevelaccess get |
| Storage | No public buckets | gsutil iam get gs://BUCKET |
| Compute | No default service account | gcloud compute instances list |
| Compute | Shielded VMs enabled | gcloud compute instances describe |
| Logging | Audit logs enabled | Console → IAM → Audit Logs |
| VPC | Private Google Access enabled | gcloud compute networks subnets list |
| GKE | Private cluster | gcloud container clusters describe |
| GKE | Workload Identity enabled | gcloud 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
| Category | Check | How to verify |
|---|---|---|
| AAD | MFA enforced | Azure AD → Security → MFA |
| AAD | Conditional Access policies | Azure AD → Security → Conditional Access |
| AAD | No guest users with admin | Azure AD → Users → Guest users |
| Storage | Secure transfer required | az storage account show |
| Storage | Private endpoint enabled | az storage account show |
| SQL | Azure AD auth enabled | az sql server ad-admin list |
| SQL | TDE enabled | az sql db tde show |
| Networking | NSG flow logs enabled | az network watcher flow-log list |
| Defender | Defender for Cloud enabled | az security pricing list |
| KeyVault | Soft delete enabled | az 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:
| Provider | Tool | Cost | What it checks |
|---|---|---|---|
| AWS | Security Hub | Free tier + per finding | Aggregates findings from multiple sources, CIS benchmarks |
| AWS | Trusted Advisor | Free (limited) / Business+ | Cost, performance, security, fault tolerance |
| AWS | GuardDuty | Per GB analyzed | Threat detection, anomaly detection |
| GCP | Security Command Center | Free tier available | Vulnerabilities, misconfigurations, threats |
| GCP | Security Health Analytics | Free with SCC | Best practice violations |
| Azure | Defender for Cloud | Free tier available | Security posture, compliance, threat protection |
| Azure | Secure Score | Free | Overall 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
| Priority | Criteria | Action timeline |
|---|---|---|
| P1 - Critical | Actively exploited OR public-facing with known exploit | Fix within 24 hours |
| P2 - High | High severity, easy to exploit, production impact | Fix within 1 week |
| P3 - Medium | Medium severity, requires conditions to exploit | Fix within 1 month |
| P4 - Low | Low severity, defense in depth | Fix in next maintenance window |
| Accept | Very low risk, no fix available, compensating controls | Document 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.
-
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 -
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 -
Review the findings:
- How many CRITICAL vulnerabilities?
- How many have fixes available (check "Fixed Version" column)?
- Which package has the most vulnerabilities?
-
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 -
Test the pipeline: Push a commit and verify the scan runs.
Part 2: Kubernetes security (if applicable)
Task: Audit your Kubernetes cluster security.
-
Install Kubescape:
curl -s https://raw.githubusercontent.com/kubescape/kubescape/master/install.sh | /bin/bash -
Run security scan:
# Scan against NSA/CISA framework
kubescape scan framework nsa
# Scan specific namespace
kubescape scan framework nsa --include-namespaces production -
Review and fix top issues:
- Containers running as root
- Missing resource limits
- Privileged containers
- Missing network policies
-
Create a hardened pod template for your team to use.
Part 3: Cloud infrastructure audit
Task: Run ScoutSuite or Prowler against your cloud account.
-
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 -
For multi-cloud or non-AWS:
pip install scoutsuite
# AWS
scout aws
# GCP
scout gcp --user-account
# Azure
scout azure --cli -
Review findings:
- Open the generated HTML report
- Focus on HIGH and CRITICAL severity findings
- Group by service (S3, IAM, EC2, etc.)
-
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:
- Trivy integrated in CI/CD — pipeline YAML file with image scanning
- Signed container images — Cosign integrated into build pipeline
- Cloud audit report — HTML/PDF export from ScoutSuite or Prowler
- Remediation plan — prioritized list of findings with owners and deadlines
- Secure Dockerfile template — example Dockerfile following best practices for your stack
- Kubernetes security policy (if applicable) — Pod Security Standards, RBAC rules, network policies
Self-check questions
Before moving on, verify you can answer:
- What's the difference between Alpine and distroless base images?
- Why should you sign container images, and how does Cosign help?
- What does Falco detect that Trivy doesn't?
- What are Kubernetes Pod Security Standards and their three levels?
- How do you properly handle secrets in Kubernetes?
- What's the shared responsibility model in cloud security?
- Name five common cloud misconfigurations that lead to data breaches.
- What does ScoutSuite check that Prowler doesn't (or vice versa)?
- How should you prioritize 200 security findings from a cloud audit?
- 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."
Links and resources
Container security
- Trivy documentation
- Docker security best practices
- Grype vulnerability scanner
- Snyk Container
- Google Distroless images
- Cosign — container signing
- Falco runtime security
Kubernetes security
- Kubernetes security documentation
- Pod Security Standards
- Kubescape
- kube-bench CIS benchmark
- External Secrets Operator
Cloud security auditing
Best practices and benchmarks
- CIS Docker Benchmark
- CIS Kubernetes Benchmark
- CIS AWS Foundations Benchmark
- AWS Well-Architected Security Pillar
- GCP Security best practices
- Azure Security Benchmark
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.