CoursesAWS security engineeringEgress control & Network Firewall

Egress control & Network Firewall

Domain allowlists, DNS Firewall, exfiltration defense.

Advanced35 min · lesson 11 of 15

Most teams lock the front door and leave the back door propped open. Inbound traffic gets firewalls, security groups, and network ACLs, all carefully tuned. Outbound usually gets one line: allow everything to 0.0.0.0/0. That open back door is exactly what an attacker needs. A compromised workload has to do two things over the network, call home for instructions and push your data out, and both of those are outbound connections. If nothing inspects what leaves, a break-in stays completely silent.

A mailroom that checks every outgoing address

A well-run mailroom doesn't let staff toss packages straight into the outbound bin. Every parcel gets its destination checked against a list of approved recipients before it leaves the building, and anything addressed somewhere odd gets held and flagged. AWS Network Firewall is that mailroom for your VPC. A VPC (virtual private cloud) is your own walled-off slice of the AWS network, and the firewall is a managed appliance you place inside it that reads traffic from the raw IP layer all the way up to the application layer. That means it can see the domain name a connection is trying to reach, which security groups and network ACLs cannot. Those filter on IP address and port only, and since real services hide behind rotating IPs and shared CDNs, an IP allowlist is a game you lose.

The rules are written in Suricata, an open intrusion-detection language the security industry already speaks. Each rule is a single line: an action (pass, drop, or alert), a protocol, a direction, and the conditions to match. The condition that makes egress control work is tls.sni. When a workload opens an HTTPS connection, the very first thing it sends, before any encryption kicks in, is the name of the server it wants to reach. That name is the Server Name Indication, or SNI, and the firewall reads it in plain sight. So you write a few pass rules for the domains you genuinely use, then one drop rule that catches every other TLS connection and logs it by name. The firewall policy holding these rules is set to read them strictly, in order, with a final answer of drop for anything left unmatched. That default is what stops the sneaky stuff. A raw non-TLS connection straight to a bare IP never matches a tls rule, so it falls through to the drop and dies there.

egress.rules — pass two approved names, drop the rest
pass tls $HOME_NET any -> $EXTERNAL_NET any (tls.sni; dotprefix; content:".amazonaws.com"; endswith; msg:"allow AWS APIs"; sid:1; rev:1;)
pass tls $HOME_NET any -> $EXTERNAL_NET any (tls.sni; content:"registry.npmjs.org"; endswith; msg:"allow npm registry"; sid:2; rev:1;)
drop tls $HOME_NET any -> $EXTERNAL_NET any (msg:"blocked egress: unapproved FQDN"; sid:9999; rev:1;)
create the stateful rule group and confirm it is ACTIVE
aws network-firewall create-rule-group \
--rule-group-name egress-allowlist \
--type STATEFUL --capacity 100 \
--rules file://egress.rules \
--region eu-west-1
{
"UpdateToken": "b1f3c8e2-7a44-4d90-9c11-0e2f5a6b7c88",
"RuleGroupResponse": {
"RuleGroupArn": "arn:aws:network-firewall:eu-west-1:222222222222:stateful-rulegroup/egress-allowlist",
"RuleGroupName": "egress-allowlist",
"RuleGroupId": "9c2b7d1e-8a34-4f7a-b0c1-6d2e5f9a1b34",
"Type": "STATEFUL",
"Capacity": 100,
"ConsumedCapacity": 3,
"RuleGroupStatus": "ACTIVE",
"NumberOfAssociations": 0
}
}

Force every packet through the mailroom

A rule group does nothing until traffic physically passes through the firewall, and this is the step people skip. Network Firewall lives in its own dedicated subnet, one that hosts nothing else, and it exposes a VPC endpoint, a gateway you can aim routes at. To make a workload subnet use it, you rewrite that subnet's route table so the default route (0.0.0.0/0) no longer points straight at the NAT gateway. It points at the firewall endpoint instead. The firewall inspects, applies your rules, and only then hands approved traffic onward to NAT and out to the internet. The workload can't route around it, because the route table is the only map it has.

find the firewall endpoint, then send the default route through it
aws network-firewall describe-firewall --firewall-name egress-fw \
--query 'FirewallStatus.SyncStates."eu-west-1a".Attachment' \
--region eu-west-1
{
"SubnetId": "subnet-0f1a2b3c4d5e6a7b8",
"EndpointId": "vpce-0a1b2c3d4e5f6a7b8",
"Status": "READY"
}
aws ec2 create-route --route-table-id rtb-0a1b2c3d4e5f6a7b8 \
--destination-cidr-block 0.0.0.0/0 \
--vpc-endpoint-id vpce-0a1b2c3d4e5f6a7b8 \
--region eu-west-1
{
"Return": true
}

One detail bites everyone at least once. The firewall subnet needs its own route onward to the NAT gateway, and the NAT subnet needs a route back to the firewall endpoint for the workload's CIDR range. If that return path is wrong, traffic the firewall happily approved still can't complete the round trip, so healthy apps break while you stare at a pass rule that looks perfect. Sketch the path in both directions before you touch a production route table.

In a single VPC this stays simple. Across an organisation you don't want a separate firewall in all fifty VPCs, each billing an hourly endpoint fee. The usual pattern is one central inspection VPC in a shared networking account, with a Transit Gateway steering every spoke VPC's egress through a single firewall fleet. You pay per firewall-hour plus per gigabyte inspected, so centralising cuts the bill and gives you one allowlist to edit. The catch is blast radius: one bad rule now touches every account, so the rule group belongs in version control with review, never hand-typed in the console.

Watch the blocks land

Blocking without logging is only half a control. Every time a drop or alert rule fires, Network Firewall writes a record to wherever you sent its logs, whether that's CloudWatch Logs, an S3 bucket, or a Kinesis Data Firehose stream. Each record is JSON in the Suricata EVE format, and the fields that matter are the SNI the workload tried to reach, the rule that caught it, and the action taken. A compromised container beaconing to a command-and-control server shows up right here, as a steady trickle of blocked connections to a name nobody on your team ever approved. Without the log you'd see none of it.

pull the last blocked-egress alert from the firewall log group
aws logs filter-log-events \
--log-group-name /aws/network-firewall/alert \
--filter-pattern '{ $.event_type = "alert" && $.alert.action = "blocked" }' \
--max-items 1 --region eu-west-1
{
"events": [
{
"logStreamName": "egress-fw/firewall/alert",
"timestamp": 1737033512114,
"message": "{\"event_type\":\"alert\",\"src_ip\":\"10.0.12.37\",\"dest_ip\":\"203.0.113.44\",\"dest_port\":443,\"proto\":\"TCP\",\"tls\":{\"sni\":\"c2.malware-update.ru\",\"version\":\"TLS 1.3\"},\"alert\":{\"action\":\"blocked\",\"signature\":\"blocked egress: unapproved FQDN\",\"signature_id\":9999}}"
}
],
"searchedLogStreams": [
{ "logStreamName": "egress-fw/firewall/alert", "searchedCompletely": true }
]
}

That one log line is the whole difference between a breach you find months later during an audit and an alert that pages someone the same afternoon. Wire the alert log group to a metric filter and a CloudWatch alarm, or forward the records to whatever monitoring system your security team already watches, so a spike in blocked egress becomes a page instead of landfill. The firewall governs the traffic that genuinely has to leave your VPC. How much of it never needs to leave at all is the next question, and that's what private endpoints answer.

Egress forced through a firewall subnet
workload subnet (private)
app / ECS tasks
no direct route to the internet
route table 0.0.0.0/0
default route points at the firewall endpoint
firewall subnet (inspection)
Network Firewall endpoint
reads the TLS SNI on every connection
stateful allowlist
pass approved FQDNs, drop and log the rest
egress + evidence
NAT + internet gateway
only approved traffic ever reaches it
alert log group
blocked SNIs flow to CloudWatch / your SIEM
The route table forces the path and the rule group decides what leaves. Every block is written to the alert log, so an exfiltration attempt turns into a page instead of a silent success.
An SNI allowlist reduces the attack surface, it doesn't seal it
The firewall trusts the server name the client puts in its handshake, and a client the attacker controls can lie about it. Two real bypasses exist: TLS 1.3 Encrypted Client Hello can hide the SNI so there is nothing left to match, and domain fronting rides an approved CDN's name in the SNI while addressing attacker content underneath it. Treat the FQDN allowlist as a strong, cheap reduction of egress risk, not a vault. Back it with Route 53 Resolver DNS Firewall so the name also has to survive a second filter at resolution time, and alarm on blocked-egress spikes so someone probing your list shows up early.
Quick check
01You created the pass/drop rule group, attached it to the firewall policy, and pointed the workload subnet's default route at the firewall endpoint. Now even approved domains time out. What's the most likely cause?
Correct — Asymmetric or dead-end routing is the classic Network Firewall break: the rule allows the packet, but the reply has nowhere to go. Map the route in both directions.
Incorrect — tls.sni is a core, supported keyword and is exactly how FQDN allowlisting works. It isn't the problem.
Incorrect — Rule groups are persistent AWS resources with an ARN. They don't expire or need periodic recreation.
Incorrect — The firewall and security groups are independent layers. A normal egress-allow SG doesn't override a firewall pass, and it wouldn't start failing only after you changed routing.
02AWS Network Firewall can enforce a domain (fully qualified domain name) allowlist, but a security group or network ACL cannot. What is the underlying reason?
Incorrect — Wrong on the facts and the point — security groups are actually stateful, and statefulness is not what lets the firewall recognise a domain name.
Incorrect — Security groups have outbound (egress) rules too; the real gap is that they match on address and port, not on names.
Correct — the firewall inspects up to the application layer and reads the SNI, while address-and-port filtering loses to services that hide behind rotating IPs and shared content delivery networks.
Incorrect — That conflates two products — DNS Firewall filters DNS queries and is not something you bolt onto a network ACL to give it domain awareness.
03Your egress firewall allowlists only your approved domains and drops-and-logs every other TLS connection. Weeks later, threat intelligence shows a container in your VPC exfiltrated data to an attacker's server over HTTPS, yet the blocked-egress log has no matching entry. Which explanation best fits the lesson's warning?
Correct — an SNI allowlist trusts the name the client puts in the handshake, so domain fronting (or a TLS 1.3 encrypted client hello that hides the SNI) rides an approved name, gets passed, and never appears as a block.
Incorrect — This is an egress firewall built specifically to inspect outbound connections, so a blind spot on outbound is not the reason.
Incorrect — Capacity governs how many rules a group can hold, not whether drops are logged; there is no such silent logging cutoff.
Incorrect — The drop rule catches any unmatched TLS connection regardless of version, so TLS 1.3 is not silently exempt.

Try this

Work through “Watch the blocks land” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.

Takeaway

The trap worth remembering here: an SNI allowlist reduces the attack surface, it doesn't seal it. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related