Vylara
securitynetworkingaws

A secure-by-default AWS network baseline for a typical web app: the exact ruleset

The exact network baseline for a typical web app on AWS: five security groups, two subnet tiers, IAM roles over keys, and what to deliberately skip until later.

Written byVylara Team
7 min read
The secure-by-default AWS baseline

Most “AWS security best practices” posts stop at platitudes: least privilege, defense in depth, encrypt everything. This one doesn’t. Below is the exact network baseline for the most common architecture in existence — a frontend + API behind a load balancer, PostgreSQL, Redis, maybe a background worker — with the specific security group rules, what each one is for, and what this baseline deliberately leaves out.

If you adopt nothing else, adopt the rule table. It’s the part people get wrong.

The topology #

VPC (e.g. 10.0.0.0/16), two AZs minimum

  Public subnets  (2x)   → ALB only. Nothing else. Ever.
  Private subnets (2x)   → ECS tasks / EC2 app, workers,
                           RDS Postgres, ElastiCache Redis
                           No public IPs. Egress via NAT Gateway.

Checklist form:

  • VPC with public and private subnets across 2 AZs (RDS subnet groups require two anyway)
  • Public subnets contain only the ALB
  • App, workers, DB, Redis all in private subnets with assign_public_ip = false / map_public_ip_on_launch = false
  • NAT Gateway in a public subnet; private route tables send 0.0.0.0/0to it — outbound only, nothing can initiate inbound through it
  • No internet gateway routes in private route tables
  • ALB listens on 443 with an ACM cert; port 80 exists only to redirect to 443

The single most common mistake I see in real accounts: an RDS instance with publicly_accessible = true“temporarily, for debugging,” eighteen months ago. The second most common: an app task in a public subnet because someone couldn’t get NAT working and gave a task a public IP instead. Both are eliminated structurally by this layout — there’s no route from the internet to a private subnet, so a fat-fingered security group rule alone can’t expose your database.

The exact security group chain #

Five security groups. Each references the security groupof its allowed caller — never CIDR blocks for internal traffic. SG references survive autoscaling, IP churn, and AZ failover; CIDR rules don’t.

SGInboundFromWhy
albTCP 4430.0.0.0/0The only internet-facing rule in the stack
appTCP {app_port} (e.g. 3000/8080)alb SG onlyNobody talks to the app except the load balancer
workerTCP {worker_port} (if it serves anything)app SG onlyOften needs noinbound at all — workers that only pull from a queue should have zero ingress rules
rdsTCP 5432app SG + worker SGBoth tiers query Postgres; nothing else does
redisTCP 6379app SG + worker SGSame shape as the DB rule

In Terraform-ish form, the pattern that matters:

terraform
resource "aws_security_group" "app" {
  ingress {
    from_port       = var.app_port
    to_port         = var.app_port
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]  # SG ref, not CIDR
  }
}

resource "aws_security_group" "rds" {
  ingress {
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.app.id]
  }
  ingress {
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.worker.id]
  }
  # no egress block needed for RDS
}

That’s the whole chain: ALB→app:port, app/worker→DB:5432, app/worker→Redis:6379, nothing else. No SSH rules (use SSM Session Manager or ECS Exec). No “office IP” rules on the database (use a bastion pattern or SSM port forwarding if you genuinely need psql access). No 0.0.0.0/0 anywhere except ALB:443.

Egress: it’s fine to leave app/worker egress open (-1 to 0.0.0.0/0) at this maturity level — they need to pull images, call third-party APIs, and reach DB/Redis. Locking down egress is a real control, but it’s a later one.

IAM roles, not access keys #

Every compute service gets its own IAM role (for ECS, a task role via ecs-tasks.amazonaws.comassume-role trust). Permissions are scoped to what that service actually touches — s3:GetObject/PutObject on one bucket ARN, sqs:SendMessage on one queue, secretsmanager:GetSecretValue on a path prefix like myapp/*. No AWSAdministratorAccesson a task role. No shared “app user” with an access key.

The litmus test: if there’s an AWS_ACCESS_KEY_IDin any environment variable anywhere in your stack, something is misconfigured. Roles issue temporary credentials automatically; there’s nothing to rotate, nothing to leak in a .env committed by accident.

Secrets from Secrets Manager, not env files #

Don’t bake DATABASE_URL into the task definition as a plaintext environment entry — task definitions are visible to anyone with ecs:DescribeTaskDefinition. Use the secrets block instead:

json
"secrets": [
  { "name": "DATABASE_URL",
    "valueFrom": "arn:aws:secretsmanager:...:secret:myapp/database-url" }
]

The container runtime fetches the value at start, using the task role’s secretsmanager:GetSecretValue permission. The secret never appears in the task definition, the console, or your Terraform state for the service.

Round it out: enforce TLS to Postgres (RDS supports requiring SSL), enable in-transit encryption on ElastiCache, and let ACM manage the ALB cert so renewal is nobody’s job.

What this baseline deliberately skips — and when to add it #

This is a baseline, not a maximal posture. Three omissions are intentional:

WAF.Skipped because it costs money, needs tuning, and a default rule set on day one mostly generates false positives you’ll learn to ignore. Add it when you have real traffic and a concrete threat: credential stuffing on a login endpoint, scraping, an API you’re contractually required to rate-limit, or a compliance regime that names it.

VPC endpoints / PrivateLink.Skipped because NAT covers S3/ECR/Secrets Manager/SQS traffic fine at low volume. Add gateway endpoints (S3, DynamoDB — they’re free) once you’re past the prototype stage, and interface endpoints when NAT data-processing charges show up meaningfully on the bill — commonly ECR image pulls at scale — or when policy requires AWS API traffic never traverses the public internet.

AWS Network Firewall.Skipped because egress filtering at the network layer is an organizational control, not an app control. Add it when you have compliance requirements for egress inspection or you’re running enough workloads that “any compromised task can call any IP on the internet” is a risk someone is paid to care about. Until then it’s several hundred dollars a month of unconfigured appliance.

The point of a baseline is that it’s cheap enough to apply to everyenvironment — including staging, where the laziest configurations live and where real customer data somehow always ends up. Five security groups, two subnet tiers, roles instead of keys, secrets out of the task definition. Copy the table, diff it against your account, and fix what doesn’t match.

Try Vylara on your repo

Review your cloud plan in Vylara, merge delivery changes as Git PRs, and deploy into your own AWS or Azure account when you’re ready.

Start free

Related posts