9. Security operations
Security is a process, not a product. Rotate secrets, scan dependencies, monitor for anomalies, document compliance. These habits compound.
Security isn't a one-time setup -- it's an ongoing operational practice. Vulnerabilities are discovered continuously in libraries, containers, and infrastructure. Credentials need rotation. Access controls require periodic auditing. Security Operations (SecOps) means actively maintaining security posture throughout your application's lifecycle.
Container security best practices
Containers aren't inherently secure; they're as secure as their base images, dependencies, and configuration make them.
Enable ECR image scanning
Amazon ECR can scan your Docker images for known vulnerabilities (CVEs) in packages and libraries. Enable scanning on push, every image pushed to ECR gets scanned automatically:
aws ecr put-image-scanning-configuration \
--repository-name news-api \
--image-scanning-configuration scanOnPush=true \
--region us-east-1
# Check scan results
aws ecr describe-image-scan-findings \
--repository-name news-api \
--image-id imageTag=latest \
--region us-east-1
Scan findings are graded CRITICAL, HIGH, MEDIUM, or LOW, with the affected package named. Treat CRITICAL and HIGH as fix-now; they're the ones with known, exploitable proof-of-concept code in the wild. MEDIUM and LOW get triaged into normal development cycles.
Use minimal base images
Smaller base images mean fewer packages, which means a smaller vulnerability surface. Alpine is 5-50 MB compared to Ubuntu's 100-200 MB. Distroless images go further and ship only the application and its runtime dependencies; with no shell and no package manager, the standard post-exploit toolchain isn't available even after a compromise.
Run containers as non-root users
Default Docker containers run as root, meaning compromised containers have root access. Configure your Dockerfile to create and use non-root users:
FROM python:3.11-slim
# Create non-root user
RUN useradd -m -u 1000 appuser
# Set working directory and ownership
WORKDIR /app
COPY --chown=appuser:appuser . /app
# Switch to non-root user
USER appuser
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Read-only root filesystem
Configure ECS task definitions with read-only root filesystems, preventing attackers from modifying container contents even if they gain access. Applications write to explicitly mounted tmpfs volumes instead:
{
"containerDefinitions": [{
"name": "news-api",
"image": "...",
"readonlyRootFilesystem": true,
"mountPoints": [
{
"sourceVolume": "tmp",
"containerPath": "/tmp",
"readOnly": false
}
]
}],
"volumes": [
{
"name": "tmp"
}
]
}
Secrets management and rotation
Hardcoded secrets in environment variables work for development, but production requires proper secrets management. AWS Secrets Manager stores secrets encrypted, rotates credentials automatically, and audits access.
Store database credentials in Secrets Manager
aws secretsmanager create-secret \
--name news-api/database \
--secret-string '{
"username": "newsapi_user",
"password": "your-secure-password",
"host": "your-rds-endpoint.amazonaws.com",
"port": "5432",
"dbname": "newsapi",
"url": "postgresql://newsapi_user:your-secure-password@your-rds-endpoint.amazonaws.com:5432/newsapi"
}' \
--region us-east-1
Reference secrets in task definitions
Instead of hardcoding environment variables, task definitions reference Secrets Manager:
{
"containerDefinitions": [{
"name": "news-api",
"image": "...",
"secrets": [
{
"name": "DATABASE_URL",
"valueFrom": "arn:aws:secretsmanager:us-east-1:ACCOUNT_ID:secret:news-api/database:url::"
}
]
}]
}
Note the :url:: suffix on the ARN. The secret holds a JSON object, so a bare ARN would inject the whole object as the literal value of DATABASE_URL and the application would fail to parse it; the :json-key:: suffix tells ECS to pull just that one field (the trailing :: leaves the version stage and version ID at their defaults). At task start ECS resolves the ARN, fetches that field, and injects it into the container's environment. The plaintext secret never appears in the task definition JSON, so describe-task-definition only returns the ARN. Every fetch is logged in CloudTrail under the task execution role, which gives you the audit trail of which task instances read which secret and when.
Enable automatic rotation
Secrets Manager can rotate database credentials automatically on a schedule (30, 60, or 90 days). Rotation requires Lambda functions updating both Secrets Manager and the database, but AWS provides templates for RDS rotation.
External API keys (News API, Spotify, etc.) belong in Secrets Manager too, not hard-coded in the task definition. The payoff is the same as for the database password: rotation doesn't require a redeploy, and CloudTrail records which task instance read which secret. For higher-security environments, rotate third-party keys quarterly and track usage from CloudWatch metrics so anomalies show up before the provider revokes the key.
Network security and security group auditing
Security groups are the network-level firewall on the deployment. They drift the same way every long-lived codebase does: temporary debugging rules become permanent, dev-only rules survive into production, and old rules outlive the resources they were attached to. A periodic audit is how you find the cruft before someone else does.
Audit security group rules
aws ec2 describe-security-groups \
--group-ids sg-YOUR_GROUP_ID \
--query 'SecurityGroups[0].IpPermissions'
# Look for concerning patterns:
# - 0.0.0.0/0 on non-web ports (SSH, database ports)
# - Unused rules (created for debugging, never removed)
# - Overly broad port ranges
Principle of least privilege
Grant only the minimum network access required. ALB security group allows inbound HTTP/HTTPS from anywhere (users need public access). ECS security group allows inbound only from ALB security group (containers shouldn't be directly accessible). RDS security group allows inbound only from ECS security group (database shouldn't be publicly accessible). This layered security limits blast radius if any component is compromised.
VPC Flow Logs for network monitoring
VPC Flow Logs capture metadata for every connection in the VPC (source IP, destination IP, ports, protocol, byte counts). They don't capture packet contents, so they're cheap to keep, and they reveal patterns the application logs can't: which services actually talk to which, unexpected outbound connections (a sign of compromise), and the traffic shape of a DDoS event. Query them from CloudWatch Logs Insights for incident-time forensics.
Next, in section 10, we recap the operational patterns this chapter put in place -- CI/CD, the golden signals, target-tracking auto-scaling, cost guardrails, deployment circuit breakers, the runbook structure, and the Secrets Manager / least-privilege hardening done here -- run the quiz, and point at Chapter 30, where every Chapter 1-29 piece gets pulled into the capstone project.