Trust is earned, not given

A different perspective

2022-09-13 · Projects

DevOps on AWS, part 4: VPC, IAM, and secrets — the security skeleton

Part 4from the DevOps on AWS series · 6 parts in all

Part 4 is the part beginners skip and auditors ask about first: the network and identity skeleton. Done right once, it disappears; done wrong, everything above it is decoration.

VPC: your private slice of AWS

A VPC is a private IP space (say 10.0.0.0/16) carved into subnets per availability zone. The canonical pattern — memorize it:

# The security-group trio (stateful firewalls by reference, not IP):
aws ec2 create-security-group --group-name sg-alb  --description "ALB: 443 from world" --vpc-id $VPC
aws ec2 authorize-security-group-ingress --group-id $SG_ALB --protocol tcp --port 443 --cidr 0.0.0.0/0
aws ec2 create-security-group --group-name sg-web  --description "web: 8080 from ALB only" --vpc-id $VPC
aws ec2 authorize-security-group-ingress --group-id $SG_WEB --protocol tcp --port 8080 \
  --source-group $SG_ALB                     # <- reference, not an IP list
aws ec2 create-security-group --group-name sg-db   --description "db: 5432 from web only" --vpc-id $VPC
aws ec2 authorize-security-group-ingress --group-id $SG_DB --protocol tcp --port 5432 \
  --source-group $SG_WEB

Read that chain again: the database is unreachable except from the web tier, which is unreachable except from the ALB, which only speaks 443. An attacker has to walk the whole chain — and each hop is a different control plane.

IAM: roles, not keys

The rule with no exceptions in this series: workloads get IAM roles; humans get SSO; nobody keeps long-lived access keys. A role is assumed at runtime; permissions are JSON policies; credentials are rotated automatically under the hood:

{
  "Version": "2012-10-17",
  "Statement": [
    { "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": "arn:aws:s3:::maplecart-media/*" },          # one bucket, two verbs
    { "Effect": "Allow",
      "Action": ["secretsmanager:GetSecretValue"],
      "Resource": "arn:aws:secretsmanager:...:secret:maplecart/db-*" },
    { "Effect": "Deny", "Action": "s3:*",                       # guardrail:
      "Resource": ["arn:aws:s3:::maplecart-backups*"] }         # app can't touch backups
  ]
}

Note the explicit Deny: least privilege is not only "allow what's needed" but "deny what must never happen". The application code is then credential-free — boto3 and the AWS SDKs pick the role up automatically.

Secrets Manager: passwords with a lifecycle

# Store the DB master password; rotation is a checkbox, not a runbook
aws secretsmanager create-secret --name maplecart/db-master \
  --secret-string file://db-password.json
aws secretsmanager rotate-secret --secret-id maplecart/db-master \
  --rotation-lambda-arn <rotation-lambda> --rotation-rules AutomaticallyAfterDays=30

# In the app (Python): fetch at boot, cache in memory
import boto3, json
secret = json.loads(boto3.client("secretsmanager").get_secret_value(
    SecretId="maplecart/db-master")["SecretString"])

With the skeleton in place — network segmentation, role-based access, managed secrets — the deployment pipeline in the next part can be simple, because it can be trusted.