Misconfigured subnet boundaries are one of the quietest ways a breach spreads inside AWS. A single overly permissive security group is bad enough, but when nobody has bothered to configure AWS NACLs at the subnet level, there is no second layer to stop lateral movement once an attacker lands on one compromised instance. Network ACLs are stateless, subnet-wide filters that sit in front of every security group in a VPC, and they are the single most under-used control in most AWS environments we audit.
This guide walks through how to configure AWS NACLs from scratch: creating a custom NACL, writing ordered allow/deny rules, associating it with the right subnets, and verifying that traffic actually flows the way you expect. By the end, you will have a working aws subnet security setup that enforces defense-in-depth without breaking application traffic, and you will understand exactly when to use a NACL versus a security group.
Step 1: Understand Why NACLs Are Different From Security Groups
Before touching the console or CLI, it is worth being precise about the network acl vs security group distinction, because conflating the two is the most common source of outages during a NACL rollout.
- Security groups are stateful and attached to individual ENIs/instances. Return traffic is automatically allowed regardless of outbound rules.
- Network ACLs are stateless and attached to entire subnets. You must explicitly allow both the inbound request and the outbound response, including ephemeral ports.
- Evaluation order matters for NACLs — rules are evaluated in numeric order (lowest first), and the first match wins. Security groups evaluate all rules and take the most permissive result.
This means NACLs are best used as a coarse, subnet-wide backstop (block known-bad ranges, enforce broad segmentation between tiers) while security groups remain your fine-grained, per-service control. Treating a NACL like a security group — trying to write per-instance, per-port rules for every microservice — is how teams end up with unmanageable 1:1 rule sprawl and accidental lockouts.
Step 2: Audit Your Current VPC and Subnet Layout
Map out what you're working with before creating anything new.
aws ec2 describe-vpcs --query 'Vpcs[*].{VpcId:VpcId,CidrBlock:CidrBlock}'
aws ec2 describe-subnets --filters "Name=vpc-id,Values=vpc-0abc123456789def0" \
--query 'Subnets[*].{SubnetId:SubnetId,CIDR:CidrBlock,AZ:AvailabilityZone}'
aws ec2 describe-network-acls --filters "Name=vpc-id,Values=vpc-0abc123456789def0" \
--query 'NetworkAcls[*].{NaclId:NetworkAclId,Default:IsDefault,Associations:Associations}'
Note which subnets currently share the default NACL (which allows all traffic both ways) — this is the gap you are closing. Group subnets by tier (public web, private app, isolated data) since each tier will get its own NACL rule set in a proper aws subnet security setup.
Step 3: Create a Custom Network ACL
Never edit the VPC's default NACL directly in production; create purpose-built NACLs per tier so you can test and roll back independently.
aws ec2 create-network-acl \
--vpc-id vpc-0abc123456789def0 \
--tag-specifications 'ResourceType=network-acl,Tags=[{Key=Name,Value=private-app-tier-nacl}]'
This returns a new NetworkAclId (e.g., acl-0f9e8d7c6b5a4321). By default, a newly created custom NACL denies all inbound and outbound traffic until you add rules — the opposite of the default VPC NACL, so plan for a brief maintenance window when you cut over.
Step 4: Configure AWS NACLs With Ordered Allow/Deny Rules
This is the core of the vpc nacl rules configuration work. Rules are numbered (1–32766), evaluated lowest-to-highest, and you need matching inbound/outbound pairs for stateless traffic to complete a round trip. Leave gaps between rule numbers (10, 20, 30...) so you can insert exceptions later without renumbering everything.
# Inbound: allow HTTPS from the load balancer subnet only
aws ec2 create-network-acl-entry \
--network-acl-id acl-0f9e8d7c6b5a4321 \
--ingress \
--rule-number 100 \
--protocol tcp \
--port-range From=443,To=443 \
--cidr-block 10.0.1.0/24 \
--rule-action allow
# Inbound: allow ephemeral return traffic from the internet-facing tier
aws ec2 create-network-acl-entry \
--network-acl-id acl-0f9e8d7c6b5a4321 \
--ingress \
--rule-number 110 \
--protocol tcp \
--port-range From=1024,To=65535 \
--cidr-block 10.0.1.0/24 \
--rule-action allow
# Outbound: allow responses back to the load balancer subnet
aws ec2 create-network-acl-entry \
--network-acl-id acl-0f9e8d7c6b5a4321 \
--egress \
--rule-number 100 \
--protocol tcp \
--port-range From=1024,To=65535 \
--cidr-block 10.0.1.0/24 \
--rule-action allow
# Explicit deny for a known-bad range, evaluated before the catch-all
aws ec2 create-network-acl-entry \
--network-acl-id acl-0f9e8d7c6b5a4321 \
--ingress \
--rule-number 50 \
--protocol -1 \
--cidr-block 198.51.100.0/24 \
--rule-action deny
Repeat this pattern per tier: the data-tier NACL should only allow traffic from the app-tier CIDR on its database port (e.g., 5432 or 3306), nothing else inbound, and no direct path to or from the public subnet. This is what actually enforces segmentation — a compromised web instance should hit a wall at the subnet boundary even if its security group is misconfigured.
Step 5: Associate the NACL With Its Subnets
A NACL only takes effect once associated. Each subnet can have exactly one NACL association at a time, so associating a new one automatically replaces the old.
aws ec2 associate-network-acl \
--network-acl-id acl-0f9e8d7c6b5a4321 \
--subnet-id subnet-0a1b2c3d4e5f6789a
Save the NewAssociationId returned — you'll need it if you have to roll back quickly to the previous NACL (typically the default one) during testing.
Step 6: Test in a Non-Production Path First
Apply the association to a staging subnet or a single canary subnet in production before rolling out fleet-wide. Confirm:
- Application health checks still pass (load balancer to instance path).
- Outbound package/dependency fetches and monitoring agent egress still work — these are the traffic patterns teams most often forget when they configure AWS NACLs for the first time.
- Database connections from the app tier succeed, and connections from anywhere else fail.
Step 7: Verify and Monitor Ongoing Behavior
Enable VPC Flow Logs on the subnets you've changed so you can see REJECT records tied to NACL enforcement, not just security group denials.
aws ec2 create-flow-logs \
--resource-type Subnet \
--resource-ids subnet-0a1b2c3d4e5f6789a \
--traffic-type REJECT \
--log-destination-type cloud-watch-logs \
--log-group-name /vpc/nacl-rejects \
--deliver-logs-permission-arn arn:aws:iam::123456789012:role/flow-logs-role
Query the log group for unexpected REJECT entries in the days after cutover — a spike usually means a legitimate ephemeral port range or a monitoring/backup service wasn't accounted for in your rule set, not that the NACL is "too strict."
Troubleshooting Common Issues
Traffic works one direction but times out on the response. This is almost always a missing outbound rule for the ephemeral port range (1024–65535) matching the requester's CIDR. Because NACLs are stateless, every inbound allow needs a corresponding outbound allow for the return leg.
A previously-working connection broke after association. Check rule numbering — a lower-numbered deny rule may be matching before your intended allow rule further down the list. Use describe-network-acls and read the entries in ascending rule-number order to trace exactly which rule wins.
Everything is blocked immediately after creating a new NACL. New custom NACLs deny all traffic by default until rules are added — this is expected, not a bug. Add explicit allow rules before associating with a live subnet, or you'll cause an outage.
Security group looks correct but traffic still fails. Remember the network acl vs security group evaluation order: NACL rules are checked first at the subnet boundary. A security group can be perfectly permissive and traffic will still drop if the NACL denies it upstream.
How Safeguard Helps
Configuring NACLs correctly is only half the problem — the harder part is keeping them correct as infrastructure changes. Safeguard continuously inventories your VPC network topology alongside the rest of your software supply chain, flagging NACLs that have drifted back toward all-allow defaults, subnets that share overly broad rule sets across trust tiers, and associations that no longer match your intended segmentation model. Instead of relying on a point-in-time audit, teams get continuous visibility into their aws subnet security setup as part of the same platform that tracks build provenance and dependency risk — so a network misconfiguration doesn't sit undetected between quarterly reviews the way a stray IAM policy or an unpinned dependency might. For security teams who have already invested in getting vpc nacl rules configuration right, Safeguard's job is making sure that investment doesn't quietly erode over the next hundred deploys.