Business Challenge
The previous post in this series ended on a control that faces outward: an endpoint policy limits which resources can be reached from a network. This one is about the other direction, and about a control that is much easier to deploy than it is to deploy correctly.
A platform team runs the hub-and-spoke topology: workload VPCs with no internet gateway, a central
networking account, and all internet-bound traffic routed across a transit gateway into an
inspection VPC where AWS Network Firewall sits in front of the NAT gateways. Security asks for
egress control — not “can it reach the internet” but “what can it
reach.” The team builds a domain list rule group with an Allow
action and about forty entries: the package registries, the vendor APIs, the telemetry endpoints.
Everything else is denied by construction, because for an allow list Network Firewall denies
traffic of that protocol that does not match.
The rule group is well written. The policy is attached. The change passes review and the control is recorded as implemented. Three things are wrong with it, and only one of them is a configuration error.
Domain list inspection is scoped by the HOME_NET variable, and
the default value of HOME_NET is the CIDR range of the VPC
where Network Firewall is deployed. Only traffic from that range is passed through
domain list filtering. In a central inspection VPC the traffic does not originate there
— it originates in the spokes and arrives over the transit gateway. AWS names this
exact topology as the most common case requiring a manual setting.
Nothing fails. No rule errors, no alarm, no metric that reads zero in an obvious way. The firewall is passing traffic it was never asked to look at.
FixSet HOME_NET on the rule group to every spoke CIDR you intend to inspect, plus the inspection VPC's own range.
For HTTPS, Network Firewall reads the Server Name Indication field from the TLS handshake. For HTTP, it reads the Host header. Both are values the client writes. AWS is explicit that the service “doesn't pause connections to do out-of-band DNS lookups” and that it uses “the SNI or host header, not the IP addresses” when evaluating domain rules.
So a process that opens a socket to an arbitrary address and writes
registry.npmjs.org into the SNI is allowed through, and one that
reaches an approved domain while presenting no name at all is evaluated on the name it did
not give. The allowlist constrains a declaration, not a destination.
AWS's own remedy: write separate IP address rules and use them in conjunction with the domain rules. Two assertions are harder to align than one.
SNI is readable because it is sent in the clear. TLS 1.3's Encrypted Client Hello removes
that, and Network Firewall does not support it: when it cannot find an SNI in the client
hello it closes the connection with an RST.
That is a safe failure, and it is worth being clear-eyed about what it means. The control does not degrade to “allow”; it degrades to “break.” Every client that adopts ECH stops working rather than stops being filtered, and the first symptom is a connectivity incident rather than a security one.
FixKnow which of your egress destinations are moving to ECH before they do, and decide deliberately whether those flows are scoped out or blocked.
Each one is the same shape as the VPC endpoint mistake in #44: a control that is real, correctly configured in its own terms, and answering a narrower question than the review believed it was answering. “We allowlist egress by domain” is true. “Our workloads can only reach approved destinations” does not follow from it.
Architecture
Egress control is three separable decisions: where the inspection sits, what it matches on, and whether you are willing to decrypt. They are usually taken as one, which is why the result is usually a domain allowlist with an unexamined trust model.
What the rule group actually says
A domain list rule group carries a target list, the protocols to inspect, and a generated rules
type. The wildcard form matters: a leading dot matches the domain and every subdomain, so
.example.com covers example.com and
abc.example.com alike, while an explicit name matches only itself.
{
"RuleVariables": {
"IPSets": {
"HOME_NET": {
"Definition": [
"10.0.0.0/16",
"10.1.0.0/16",
"192.0.2.0/24"
]
}
}
},
"RulesSource": {
"RulesSourceList": {
"Targets": [
".example.com",
"www.example.org"
],
"TargetTypes": [ "HTTP_HOST", "TLS_SNI" ],
"GeneratedRulesType": "ALLOWLIST"
}
}
}
The HOME_NET block is the part that is absent from most first drafts,
and its absence is invisible. The two 10.x ranges are the spoke VPCs
whose traffic arrives over the transit gateway; 192.0.2.0/24 is the
inspection VPC itself, which must stay in the list because setting the variable replaces the
default rather than adding to it.
There is a second-order trap in the same mechanism.
EXTERNAL_NET is always the negation of the policy's
HOME_NET, and a rule group that sets its own
HOME_NET without also setting
EXTERNAL_NET inherits the policy's negation regardless. Set one and
you can end up with a rule group whose idea of “inside” and “outside” do
not describe the same boundary.
Where the inspection sits
Spoke VPC, no internet gateway
Default route points at the transit gateway. The workload cannot reach the internet except through the hub, which is what makes a single inspection point possible at all.
Inspection VPC, firewall endpoint per AZ
One endpoint in each Availability Zone, at $0.395 per endpoint-hour. Three AZs is roughly $865 a month before a byte is processed, and processing adds $0.065 per GB.
Domain rules, scoped by HOME_NET
The stateful engine reads the SNI or Host header of flows sourced from HOME_NET. Everything else passes the firewall without being evaluated against the domain list.
NAT gateway, then the internet
A fixed, auditable set of Elastic IPs. The firewall sits before this, so it sees the workload's source address rather than the translated one.
Why This Architecture Holds Up
A weak check in the right place still narrows the blast radius
Nothing above is an argument against domain filtering. An allowlist that can be defeated by a process deliberately writing a false SNI still stops the overwhelming majority of what egress control is for: a dependency that starts calling an analytics endpoint after an update, a container image that phones home, a misconfigured backup job writing to the wrong bucket over the public endpoint. None of those lie about their destination, because none of them are trying to.
The failure mode to avoid is not deploying it. The failure mode to avoid is recording it as a control against a deliberate adversary when it is a control against accident and drift.
The allow action is stricter than it looks
For an allow list, traffic of the specified protocol that does not match any entry is denied.
That is the behaviour you want, and it has a sharp edge: AWS recommends against combining
Reject or Alert domain rule groups with
Allow ones under default action ordering, because the default drop
that the allow group adds takes effect before the reject and alert rules do. A rule group added
to improve visibility can silently stop being reached.
The documentation does not present IP rules as an optimisation. It presents them as the mitigation for a manipulated SNI or Host header, to be used in conjunction with or in place of the domain rules. If your design has domain rules alone, it is missing the half that AWS names as the answer to the weakness in the other half.
Key Architecture Decisions
| Decision | Choice | Reasoning |
|---|---|---|
| Inspection point | Central inspection VPC in the networking account | One place to reason about, one set of rules to maintain. The cost is that HOME_NET must be maintained alongside the spoke inventory. |
| HOME_NET | Explicit list of spoke CIDRs plus the inspection VPC | The default silently limits inspection to the inspection VPC's own range. Setting it replaces the default rather than extending it. |
| Match basis | Domain rules and IP rules together | Domain rules read a client-supplied field. IP rules read the destination the packet is actually going to. Neither alone is the control the review thinks it is. |
| Rule group mix | Do not mix Alert or Reject groups with Allow groups | Under default action ordering the Allow group's implicit drop wins first, so the other groups stop being evaluated. |
| Decryption | TLS inspection scoped narrowly, not enabled globally | It is the only way to see past the handshake, and it drops a specific and non-obvious list of traffic. Scope it to the flows that justify it. |
What TLS inspection buys, and what it refuses to carry
If domain filtering is not enough, the escalation is a TLS inspection configuration: Network Firewall terminates the client's connection, decrypts, inspects against the stateful rules, and re-encrypts before forwarding. It needs certificates in AWS Certificate Manager and it works inbound, outbound, or both.
It also drops things, and the list is worth reading before rather than after enabling it:
Anything without a usable server name
If the client hello carries no server name, or the name does not match the SNI in the downstream server certificate, the traffic is dropped. Encrypted Client Hello falls here, closed with an RST.
QUIC and StartTLS
UDP-based transports such as QUIC are not supported, and neither is decryption of protocols that rely on StartTLS. A browser silently preferring QUIC is an application-level decision that removes your inspection.
Non-TLS traffic inside the scope
If port 80 is in the inspection scope and plain HTTP arrives on it, Network Firewall cannot identify it as TLS and drops it. The scope defines what must be TLS, not merely what may be.
Existing flows, on the day you enable it
Adding a TLS inspection configuration to a live firewall drops ongoing traffic that matches the scope. New connections are inspected; the ones already open are not drained.
Because the engine sees plaintext, TLS-based keywords no longer match — with the single exception of TLS.SNI, which still matches for in-scope traffic. Rules written against TLS attributes quietly stop firing for exactly the flows you decided were important enough to decrypt, while HTTP-keyword rules start working for the first time.
Closing Thought
Egress filtering has an unusual property among security controls: the version that is easy to deploy and the version that is hard to deploy look identical in an architecture diagram. Both show a firewall in an inspection VPC with an arrow to the internet. One of them is reading a field the client chose, from a source range it was never scoped to, and the other is not.
The three defects in this post are not really three. They are one question asked three times: what does this control actually observe? Not what does it intend to prevent — what byte, in what packet, does it read, and who wrote that byte. For a domain allowlist the answer is that the client wrote it, and AWS says so in the documentation rather than burying it.
That is the useful discipline to take from this one. When a control is recorded as implemented, write down the field it inspects next to it. If the field turns out to be supplied by the thing being controlled, the control is real but it is a control against mistakes, and the review should say so.
Networking & Security — DNS as the other egress path: why Route 53 Resolver DNS Firewall sits at a different layer than Network Firewall, what a query log shows that a flow log cannot, and the exfiltration channel that neither an endpoint policy nor a domain allowlist touches.
Official AWS Reference
- Stateful domain list rule groups — SNI and Host header matching, the HOME_NET default, and the transit gateway case
- Limitations and caveats for stateful rules — how HOME_NET and EXTERNAL_NET inherit
- Inspecting SSL/TLS traffic with TLS inspection configurations
- Considerations when working with TLS inspection configurations — the full list of what it drops
- AWS Network Firewall pricing
Comments