AWS Network Firewall: Deep Packet Inspection and Traffic Filtering
Security Groups and NACLs give you basic IP and port filtering, but they have no visibility into what's actually inside the packets. AWS Network Firewall is a managed, stateful network firewall service that runs inside your VPC and performs deep packet inspection — examining packet payloads, correlating sessions, running full Suricata-compatible IPS rules, filtering by domain name, and even decrypting TLS to inspect encrypted traffic. It's the difference between a bouncer who checks IDs and one who actually looks inside the bag.
This guide covers everything: how Network Firewall compares with other AWS security controls, how to architect it correctly with VPC route tables, how to write stateless and stateful (Suricata) rules, how to filter by domain name, how TLS inspection works, full Terraform deployment, and how to query firewall logs in Athena to detect threats.
Table of Contents
- Network Firewall vs WAF vs Security Groups vs NACLs
- Architecture: Firewall Endpoints, VPC Routes, and Inspection VPC Pattern
- Stateful vs Stateless Rule Groups
- Stateless Rules: 5-Tuple Matching and Actions
- Stateful Rules: Suricata IPS Rules and Domain Lists
- Domain Filtering: Blocking C2, DNS-over-HTTPS, and FQDN Lists
- TLS Inspection: Decrypting HTTPS Traffic
- Deploying with Terraform
- Logging and Monitoring: Flow, Alert, and Drop Logs
- Cost Optimization
Network Firewall vs WAF vs Security Groups vs NACLs
AWS gives you multiple layers of network control and the naming is confusing. Here's a precise breakdown of what each control does, where it operates, and when to use it.
| Control | OSI Layer | Where It Runs | State | Inspects Payload | Best For |
|---|---|---|---|---|---|
| Security Group | L4 | ENI on every instance | Stateful | No | Instance-level allow/deny by port + source SG |
| NACL | L3/L4 | Subnet boundary | Stateless | No | Subnet-wide IP block lists, egress control |
| AWS WAF | L7 HTTP/HTTPS | CloudFront / ALB / API GW | Stateful (request-level) | HTTP only | SQL injection, XSS, rate limiting, bot control on web apps |
| AWS Network Firewall | L3–L7 | Dedicated firewall subnet in your VPC | Both | Yes (DPI) | East-west + north-south traffic, IPS, domain filtering, TLS inspection |
The rule of thumb: Security Groups and NACLs are always-on perimeter controls. WAF protects your web-facing endpoints at Layer 7 HTTP. Network Firewall is the right tool when you need to inspect non-HTTP protocols (SSH, DNS, SMTP, custom TCP/UDP), enforce east-west controls between subnets or VPCs, run an IPS ruleset, or filter outbound traffic by domain name — none of which WAF supports.
Network Firewall should not replace Security Groups — it complements them. The typical defense-in-depth posture is: Network Firewall for centralized inspection + Security Groups for per-resource micro-segmentation. Both layers simultaneously active gives you belt-and-suspenders protection: even if a firewall rule is accidentally misconfigured, Security Groups still enforce least-privilege access at the instance level.
Architecture: Firewall Endpoints, VPC Routes, and Inspection VPC Pattern
AWS Network Firewall is not a software appliance you launch — it's a managed service that creates firewall endpoints inside your VPC. Each endpoint is a VPC endpoint (type GatewayLoadBalancer) that appears as a target in route tables. Traffic is steered to the endpoint via route table manipulation, inspected, then forwarded to its destination.
Distributed vs Centralized Deployment
Distributed deployment places a firewall endpoint in each VPC that needs protection. This is simpler to configure and gives each application team independent control over their firewall policy, but it means paying for an endpoint per VPC per AZ and managing policies separately.
Centralized (inspection VPC) deployment routes all traffic through a dedicated inspection VPC using AWS Transit Gateway. All spoke VPCs send traffic to the TGW, which routes it to the inspection VPC where the firewall lives, and then on to the internet (or back to another spoke). This is the enterprise pattern — one firewall policy to manage, central logging, better cost efficiency at scale.
VPC Route Table Configuration (Distributed Pattern)
The most important and error-prone part of Network Firewall deployment is the route table configuration. You need three route table changes:
- Public subnet route table: Traffic bound for private subnets is routed to the firewall endpoint instead of the local route.
- Firewall subnet route table: Traffic from the firewall is sent to the internet gateway for outbound or back to the private subnet for inbound.
- Internet Gateway (edge) route table: Inbound traffic from the internet to your public subnet IPs is routed to the firewall endpoint first.
# Inspection flow for inbound traffic:
Internet → IGW → IGW Edge Route Table → Firewall Endpoint → Public Subnet → EC2
# Inspection flow for outbound traffic:
EC2 → Private Subnet Route Table → Firewall Endpoint → Firewall Subnet → IGW → Internet
aws ec2 associate-route-table --gateway-id igw-xxx. Forgetting this step means inbound traffic bypasses the firewall entirely.
Availability Zone Stickiness
Network Firewall creates one endpoint per AZ. Traffic must be routed to the firewall endpoint in the same AZ as the source. Cross-AZ routing to a firewall endpoint is not supported — the route will be rejected. This means each AZ needs its own firewall subnet with its own route table, and each AZ's public/private subnets must point to the endpoint in their own AZ.
Stateful vs Stateless Rule Groups
Network Firewall evaluates traffic through two distinct rule group types processed in order: stateless rules first, then stateful rules.
Stateless Rule Groups
Stateless rules are evaluated for every individual packet in isolation — there is no session tracking. They work like NACLs on steroids: you match on a 5-tuple (source IP, source port, destination IP, destination port, protocol) plus TCP flags. Actions are PASS, DROP, or FORWARD_TO_STATEFUL. Stateless rules are evaluated in priority order (lower number = higher priority). The moment a packet matches a rule, processing stops and the action is taken.
Stateful Rule Groups
Stateful rules see reassembled TCP streams and track connection state. They are processed only for packets that stateless rules have forwarded (action: FORWARD_TO_STATEFUL). Stateful rules support three formats:
- 5-tuple rules — Simple source/destination IP+port matching with protocol, but evaluated statelessly within the stateful engine
- Domain list rules — Allow or deny traffic based on FQDN/SNI (HTTP Host header or TLS SNI)
- Suricata-compatible IPS rules — Full Suricata rule syntax with protocol keywords, content matching, PCRE, and metadata
Capacity Units
Every rule group has a capacity — a fixed number you set at creation time that cannot be changed. Capacity is consumed based on rule complexity:
| Rule Type | Capacity Cost |
|---|---|
| Stateless rule (5-tuple, single IP) | 1 |
| Stateless rule with IP prefix list | 1 per CIDR |
| Stateful 5-tuple rule | 1 |
| Suricata rule (no content) | 1 |
| Suricata rule (with content keyword) | 3 |
| Suricata rule (with PCRE) | 10 |
| Domain list rule | 5 per domain entry |
Plan capacity generously — you cannot resize a rule group. If you hit capacity you must create a new rule group and swap it into the policy, causing a brief config propagation delay. A safe practice is to provision 2–3× your current rule count as capacity.
Stateless Rules: 5-Tuple Matching and Actions
Stateless rules are your first line of defense — they process packets at wire speed before any stateful tracking overhead. Use them to immediately drop obviously bad traffic (known attack IPs, invalid protocol combinations) and to pass trusted internal traffic directly without the cost of stateful inspection.
5-Tuple Matching
Each stateless rule matches on any combination of:
- Source IP / CIDR — single address, CIDR block, or an IP set reference
- Destination IP / CIDR
- Source port range — single port or range (e.g., 1024–65535)
- Destination port range
- Protocol — TCP, UDP, ICMP, or any (IP protocol number 0–255)
- TCP flags — match on SYN, ACK, FIN, RST, URG, PSH — useful for detecting SYN floods or half-open scans
Example Stateless Rules via CLI
{
"Priority": 100,
"RuleDefinition": {
"MatchAttributes": {
"Sources": [{"AddressDefinition": "0.0.0.0/0"}],
"Destinations": [{"AddressDefinition": "10.0.0.0/8"}],
"DestinationPorts": [{"FromPort": 22, "ToPort": 22}],
"Protocols": [6]
},
"Actions": ["aws:forward_to_sfe"]
}
}
{
"Priority": 10,
"RuleDefinition": {
"MatchAttributes": {
"Sources": [{"AddressDefinition": "192.0.2.0/24"}],
"Destinations": [{"AddressDefinition": "0.0.0.0/0"}],
"Protocols": [6, 17]
},
"Actions": ["aws:drop"]
}
}
Stateful Rules: Suricata IPS Rules and Domain Lists
Stateful rules are where Network Firewall becomes a true IPS. The engine is Suricata-compatible, which means you can use the same rule syntax used by one of the most widely deployed open-source IDS/IPS systems — including community rulesets from Emerging Threats.
Suricata Rule Anatomy
action proto src_ip src_port direction dst_ip dst_port (options)
# action: alert | drop | pass | reject
# direction: -> (one-way) or <> (bidirectional)
Real Suricata Rule Examples
# Detect and drop SSH brute force attempts (>5 login failures from same IP in 60s)
drop tcp any any -> $HOME_NET 22 (msg:"SSH Brute Force Attempt"; \
flow:established; content:"SSH-"; threshold:type both, track by_src, count 5, seconds 60; \
classtype:attempted-admin; sid:1000001; rev:1;)
# Detect outbound DNS tunneling (unusually long DNS query names)
alert dns any any -> any 53 (msg:"Possible DNS Tunneling - Long Query"; \
dns.query; content:!".amazonaws.com"; \
pcre:"/^[a-z0-9]{40,}\./i"; \
classtype:trojan-activity; sid:1000002; rev:1;)
# Block known malware C2 user agent strings
drop http $HOME_NET any -> $EXTERNAL_NET any (msg:"Malware C2 User Agent"; \
flow:established,to_server; \
http.user_agent; content:"Mozilla/4.0 (compatible; MSIE 6.0)"; \
threshold:type limit, track by_src, count 1, seconds 60; \
classtype:trojan-activity; sid:1000003; rev:1;)
# Detect TLS certificate with self-signed CA (often used by malware)
alert tls any any -> $EXTERNAL_NET 443 (msg:"TLS Self-Signed Certificate"; \
tls.cert_issuer; content:"CN="; \
tls.cert_subject; content:"CN="; \
tls_cert_issuer; tls_cert_subject; \
pcre:"/^CN=[^,]+$/"; \
classtype:policy-violation; sid:1000004; rev:1;)
# Drop Cobalt Strike default beacon to team server
drop http $HOME_NET any -> $EXTERNAL_NET any (msg:"Cobalt Strike Beacon Default URI"; \
flow:established,to_server; \
http.uri; content:"/dpixel"; \
http.header; content:"Accept: */*"; \
classtype:trojan-activity; sid:1000005; rev:1;)
# Block outbound SMTP from non-mail servers (prevents spam from compromised instances)
drop tcp $HOME_NET !25 -> $EXTERNAL_NET 25 (msg:"Unauthorized Outbound SMTP"; \
flow:to_server,established; \
classtype:policy-violation; sid:1000006; rev:1;)
Emerging Threats Open Ruleset Integration
You don't have to write all rules from scratch. The Emerging Threats Open ruleset (free, maintained by Proofpoint) contains thousands of IPS signatures for malware, exploits, scanners, and C2 traffic. Download the latest rules and import them directly into a Network Firewall Suricata-compatible rule group:
# Download Emerging Threats Open rules
curl -LO https://rules.emergingthreats.net/open/suricata-5.0/emerging.rules.tar.gz
tar -xzf emerging.rules.tar.gz
# Concatenate the rules you want into a single file
cat rules/emerging-malware.rules rules/emerging-botcc.rules rules/emerging-exploit.rules \
> combined-et-rules.txt
# Create a Network Firewall rule group from the file
aws network-firewall create-rule-group \
--rule-group-name "emerging-threats-open" \
--type STATEFUL \
--capacity 10000 \
--rules "$(cat combined-et-rules.txt)" \
--region us-east-1
Domain Filtering: Blocking C2 Domains, DNS-over-HTTPS Detection, and FQDN Lists
Domain list rule groups let you filter traffic based on fully qualified domain names — without needing to know the IP addresses, which change constantly for CDNs and cloud services. The firewall inspects the HTTP Host header for plaintext traffic and the TLS SNI extension for HTTPS traffic.
Creating a Domain Allow List
An allowlist approach (default deny, explicit allow) is the most secure egress posture. Only your approved domains can receive outbound traffic:
{
"RulesSource": {
"RulesSourceList": {
"Targets": [
".amazonaws.com",
".s3.amazonaws.com",
".cloudfront.net",
"api.github.com",
".docker.io",
".pypi.org",
"registry.npmjs.org"
],
"TargetTypes": ["HTTP_HOST", "TLS_SNI"],
"GeneratedRulesType": "ALLOWLIST"
}
}
}
Creating a Blocklist for Known C2 Infrastructure
{
"RulesSource": {
"RulesSourceList": {
"Targets": [
".cobaltstrikeserver.com",
".metasploit.com",
"pastebin.com",
".ngrok.io",
".serveo.net",
".trycloudflare.com"
],
"TargetTypes": ["HTTP_HOST", "TLS_SNI"],
"GeneratedRulesType": "DENYLIST"
}
}
}
DNS-over-HTTPS (DoH) Detection and Blocking
Malware increasingly uses DNS-over-HTTPS to bypass traditional DNS filtering — because DoH requests look like regular HTTPS traffic to known providers (Cloudflare 1.1.1.1, Google 8.8.8.8). You can block DoH at the network level by denying HTTPS traffic to known DoH provider IPs:
# Block DNS-over-HTTPS to Cloudflare
drop tls any any -> 1.1.1.1 443 (msg:"DNS over HTTPS to Cloudflare blocked"; \
tls.sni; content:"cloudflare-dns.com"; \
classtype:policy-violation; sid:2000001; rev:1;)
# Block DNS-over-HTTPS to Google
drop tls any any -> 8.8.8.8 443 (msg:"DNS over HTTPS to Google blocked"; \
tls.sni; content:"dns.google"; \
classtype:policy-violation; sid:2000002; rev:1;)
# Block any HTTPS to known DoH provider IPs (use IP set for full coverage)
drop tcp $HOME_NET any -> [1.1.1.1,1.0.0.1,8.8.8.8,8.8.4.4,9.9.9.9,149.112.112.112] 443 \
(msg:"DNS over HTTPS Blocked"; classtype:policy-violation; sid:2000003; rev:1;)
TLS Inspection: Decrypting HTTPS Traffic
TLS inspection (also called SSL inspection or man-in-the-middle inspection) allows Network Firewall to decrypt HTTPS traffic, apply stateful rules to the decrypted payload, then re-encrypt it before forwarding. Without TLS inspection, the firewall can only see the SNI hostname — not the URL path, headers, or body of encrypted requests.
How It Works
The firewall acts as a trusted CA. When a client connects to an HTTPS server, the firewall intercepts the TLS handshake, presents a certificate signed by your internal CA, decrypts the traffic, inspects it, then establishes a separate TLS connection to the real destination. The client sees your CA's certificate instead of the origin server's certificate.
Certificate Authority Setup
# Create a private CA in AWS Certificate Manager Private CA
aws acm-pca create-certificate-authority \
--certificate-authority-configuration \
"KeyAlgorithm=RSA_2048,SigningAlgorithm=SHA256WITHRSA,Subject={Country=US,Organization=Techoral,CommonName=Techoral Network Firewall CA}" \
--certificate-authority-type ROOT \
--idempotency-token firewall-ca-2026
# Export the CA certificate
CA_ARN=$(aws acm-pca list-certificate-authorities \
--query "CertificateAuthorities[?Status=='ACTIVE'].Arn" --output text)
aws acm-pca get-certificate-authority-certificate \
--certificate-authority-arn $CA_ARN \
--output text > firewall-ca.pem
# Distribute firewall-ca.pem to all endpoints that should trust inspected traffic
# (push via SSM, Group Policy, MDM, or bake into your AMI)
# Create TLS inspection configuration
aws network-firewall create-tls-inspection-configuration \
--tls-inspection-configuration-name "main-tls-inspection" \
--tls-inspection-configuration '{
"ServerCertificateConfigurations": [{
"CertificateAuthorityArn": "'"$CA_ARN"'",
"CheckCertificateRevocationStatus": {
"RevokedStatusAction": "DROP",
"UnknownStatusAction": "PASS"
},
"Scopes": [{
"Sources": [{"AddressDefinition": "10.0.0.0/8"}],
"Destinations": [{"AddressDefinition": "0.0.0.0/0"}],
"SourcePorts": [{"FromPort": 1024, "ToPort": 65535}],
"DestinationPorts": [{"FromPort": 443, "ToPort": 443}],
"Protocols": [6]
}]
}]
}'
Deploying with Terraform
The following Terraform configuration deploys a Network Firewall in a distributed single-VPC pattern with stateless, domain list, and Suricata rule groups, plus correct route table configurations.
provider "aws" {
region = "us-east-1"
}
# ── VPC and subnets assumed pre-existing ─────────────────────────────────────
variable "vpc_id" { default = "vpc-0abc1234" }
variable "firewall_subnet_id" { default = "subnet-0firewall1" }
variable "public_subnet_id" { default = "subnet-0public1" }
variable "igw_id" { default = "igw-0abc1234" }
# ── Stateless Rule Group ──────────────────────────────────────────────────────
resource "aws_networkfirewall_rule_group" "stateless_drop_known_bad" {
capacity = 100
name = "drop-known-bad-ips"
type = "STATELESS"
rule_group {
rules_source {
stateless_rules_and_custom_actions {
stateless_rule {
priority = 10
rule_definition {
actions = ["aws:drop"]
match_attributes {
sources {
address_definition = "198.51.100.0/24" # example: known attack IP range
}
destinations {
address_definition = "0.0.0.0/0"
}
protocols = [6, 17]
}
}
}
stateless_rule {
priority = 100
rule_definition {
actions = ["aws:forward_to_sfe"]
match_attributes {
sources {
address_definition = "0.0.0.0/0"
}
destinations {
address_definition = "0.0.0.0/0"
}
protocols = [6, 17, 1]
}
}
}
}
}
}
tags = { Environment = "production" }
}
# ── Stateful Domain Allow List Rule Group ─────────────────────────────────────
resource "aws_networkfirewall_rule_group" "domain_allowlist" {
capacity = 500
name = "egress-domain-allowlist"
type = "STATEFUL"
rule_group {
rule_variables {
ip_sets {
key = "HOME_NET"
ip_set {
definition = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]
}
}
}
rules_source {
rules_source_list {
generated_rules_type = "ALLOWLIST"
target_types = ["HTTP_HOST", "TLS_SNI"]
targets = [
".amazonaws.com",
".cloudfront.net",
"api.github.com",
".docker.io",
".pypi.org",
"registry.npmjs.org",
".ubuntu.com",
".debian.org",
]
}
}
}
tags = { Environment = "production" }
}
# ── Stateful Suricata Rule Group ──────────────────────────────────────────────
resource "aws_networkfirewall_rule_group" "suricata_ips" {
capacity = 2000
name = "suricata-ips-rules"
type = "STATEFUL"
rule_group {
stateful_rule_options {
rule_order = "STRICT_ORDER"
}
rules_source {
rules_string = <<-SURICATA
drop tcp any any -> $HOME_NET 22 (msg:"SSH Brute Force"; flow:established; content:"SSH-"; threshold:type both, track by_src, count 5, seconds 60; classtype:attempted-admin; sid:1000001; rev:1;)
drop http $HOME_NET any -> $EXTERNAL_NET any (msg:"Cobalt Strike Beacon"; flow:established,to_server; http.uri; content:"/dpixel"; classtype:trojan-activity; sid:1000005; rev:1;)
drop tcp $HOME_NET !25 -> $EXTERNAL_NET 25 (msg:"Unauthorized SMTP Egress"; flow:to_server,established; classtype:policy-violation; sid:1000006; rev:1;)
alert dns any any -> any 53 (msg:"DNS Tunneling Long Query"; dns.query; pcre:"/^[a-z0-9]{40,}\./i"; classtype:trojan-activity; sid:1000002; rev:1;)
SURICATA
}
}
tags = { Environment = "production" }
}
# ── Firewall Policy ───────────────────────────────────────────────────────────
resource "aws_networkfirewall_firewall_policy" "main" {
name = "main-firewall-policy"
firewall_policy {
stateless_default_actions = ["aws:forward_to_sfe"]
stateless_fragment_default_actions = ["aws:drop"]
stateless_rule_group_reference {
priority = 10
resource_arn = aws_networkfirewall_rule_group.stateless_drop_known_bad.arn
}
stateful_rule_group_reference {
resource_arn = aws_networkfirewall_rule_group.domain_allowlist.arn
priority = 100
}
stateful_rule_group_reference {
resource_arn = aws_networkfirewall_rule_group.suricata_ips.arn
priority = 200
}
stateful_engine_options {
rule_order = "STRICT_ORDER"
}
}
tags = { Environment = "production" }
}
# ── Network Firewall ──────────────────────────────────────────────────────────
resource "aws_networkfirewall_firewall" "main" {
name = "main-network-firewall"
firewall_policy_arn = aws_networkfirewall_firewall_policy.main.arn
vpc_id = var.vpc_id
subnet_mapping {
subnet_id = var.firewall_subnet_id
}
tags = { Environment = "production" }
}
# ── Firewall Endpoint (data source for route table) ───────────────────────────
locals {
firewall_endpoint_id = tolist(aws_networkfirewall_firewall.main.firewall_status[0].sync_states)[0].attachment[0].endpoint_id
}
# ── Route Tables ──────────────────────────────────────────────────────────────
# Public subnet: send all traffic to firewall endpoint
resource "aws_route" "public_to_firewall" {
route_table_id = aws_route_table.public.id
destination_cidr_block = "0.0.0.0/0"
vpc_endpoint_id = local.firewall_endpoint_id
}
# Firewall subnet: send to IGW for internet egress
resource "aws_route" "firewall_to_igw" {
route_table_id = aws_route_table.firewall.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = var.igw_id
}
# IGW edge route table: inbound traffic to public subnet via firewall
resource "aws_route_table" "igw_edge" {
vpc_id = var.vpc_id
tags = { Name = "igw-edge-rtb" }
}
resource "aws_route" "igw_to_firewall" {
route_table_id = aws_route_table.igw_edge.id
destination_cidr_block = "10.0.1.0/24" # public subnet CIDR
vpc_endpoint_id = local.firewall_endpoint_id
}
resource "aws_gateway_route_table_association" "igw_edge" {
gateway_id = var.igw_id
route_table_id = aws_route_table.igw_edge.id
}
Logging and Monitoring: Flow, Alert, and Drop Logs
Network Firewall produces three types of logs. All three can be sent to S3, CloudWatch Logs, or Kinesis Data Firehose simultaneously:
- FLOW logs — A record for every connection that passed through the firewall. Includes 5-tuple, bytes, packets, start/end time, action (Pass/Drop). Similar to VPC Flow Logs but includes the firewall's decision.
- ALERT logs — Generated by Suricata
alertrules. Contains rule metadata (sid, msg, classtype) plus the triggering packet information. - DROP logs — Generated when the firewall drops a packet due to a rule match. Subset of ALERT logs when rules use
dropaction.
Configuring Logging via CLI
FIREWALL_ARN=$(aws network-firewall describe-firewall \
--firewall-name main-network-firewall \
--query "Firewall.FirewallArn" --output text)
aws network-firewall update-logging-configuration \
--firewall-arn $FIREWALL_ARN \
--logging-configuration '{
"LogDestinationConfigs": [
{
"LogType": "FLOW",
"LogDestinationType": "S3",
"LogDestination": {
"bucketName": "my-firewall-logs",
"prefix": "flow-logs/"
}
},
{
"LogType": "ALERT",
"LogDestinationType": "CloudWatchLogs",
"LogDestination": {
"logGroup": "/aws/network-firewall/alerts"
}
},
{
"LogType": "DROP",
"LogDestinationType": "S3",
"LogDestination": {
"bucketName": "my-firewall-logs",
"prefix": "drop-logs/"
}
}
]
}'
Querying Firewall Logs in Athena
Once logs land in S3, create an Athena table and run SQL queries to investigate threats:
-- Create Athena table for Network Firewall alert logs
CREATE EXTERNAL TABLE network_firewall_alerts (
firewall_name STRING,
availability_zone STRING,
event STRUCT<
timestamp: STRING,
flow_id: BIGINT,
event_type: STRING,
src_ip: STRING,
src_port: INT,
dest_ip: STRING,
dest_port: INT,
proto: STRING,
alert: STRUCT<
action: STRING,
gid: INT,
signature_id: INT,
rev: INT,
signature: STRING,
category: STRING,
severity: INT
>
>
)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
LOCATION 's3://my-firewall-logs/alert-logs/'
TBLPROPERTIES ('has_encrypted_data'='false');
-- Top 10 source IPs triggering IPS alerts in the last 24 hours
SELECT event.src_ip,
COUNT(*) AS alert_count,
ARRAY_AGG(DISTINCT event.alert.signature) AS triggered_rules
FROM network_firewall_alerts
WHERE event.event_type = 'alert'
AND event.timestamp > TO_ISO8601(CURRENT_TIMESTAMP - INTERVAL '24' HOUR)
GROUP BY event.src_ip
ORDER BY alert_count DESC
LIMIT 10;
-- Find all SSH brute force attempts
SELECT event.src_ip, event.dest_ip, event.dest_port,
event.alert.signature, COUNT(*) AS hit_count
FROM network_firewall_alerts
WHERE event.alert.signature_id = 1000001
GROUP BY 1,2,3,4
HAVING COUNT(*) > 3
ORDER BY hit_count DESC;
-- Detect exfiltration (large outbound flows that were dropped)
SELECT event.src_ip, event.dest_ip, event.dest_port, event.proto
FROM network_firewall_flow_logs
WHERE event.action = 'blocked'
AND event.bytes_toserver > 1000000
ORDER BY event.bytes_toserver DESC;
CloudWatch Metrics to Alert On
# Create CloudWatch alarm for high drop rate
aws cloudwatch put-metric-alarm \
--alarm-name "NetworkFirewall-HighDropRate" \
--metric-name DroppedPackets \
--namespace AWS/NetworkFirewall \
--dimensions Name=FirewallName,Value=main-network-firewall \
--period 300 \
--evaluation-periods 2 \
--threshold 10000 \
--comparison-operator GreaterThanThreshold \
--statistic Sum \
--alarm-actions arn:aws:sns:us-east-1:123456789012:security-alerts
Cost Optimization
AWS Network Firewall has two cost components: a per-endpoint hourly charge and a per-GB traffic processing charge. Understanding both is critical for keeping costs under control.
| Component | Price (us-east-1) | Notes |
|---|---|---|
| Firewall endpoint | $0.395/hour per AZ | ~$285/month per AZ, regardless of traffic volume |
| Traffic processing | $0.065 per GB | Both inbound and outbound traffic processed |
| ACM Private CA (for TLS inspection) | $400/month per CA | Plus $0.75 per certificate issued |
Cost Scenarios
- 2 AZ, 5TB/month traffic: 2×$285 + 5,000GB×$0.065 = $570 + $325 = $895/month
- 3 AZ, 20TB/month traffic: 3×$285 + 20,000×$0.065 = $855 + $1,300 = $2,155/month
Cost Optimization Strategies
1. Centralized inspection VPC via Transit Gateway: Instead of one firewall per VPC, share a single centralized firewall across 10+ VPCs. The endpoint cost is fixed regardless of how many VPCs route through it. At scale, this can reduce per-VPC firewall cost by 80%+. The trade-off is Transit Gateway data processing charges ($0.02/GB per TGW hop).
2. Use stateless rules to drop high-volume junk early: Traffic that is dropped by stateless rules costs less than traffic forwarded to stateful rules. Drop internet scanners (port 23, 3389, 445 inbound) in stateless rules — this is typically 40–60% of raw internet traffic and would otherwise all be processed through Suricata.
3. Minimize TLS inspection scope: TLS inspection processes every byte of encrypted traffic twice (decrypt + re-encrypt). Scope it to high-risk egress paths only (developer workstations, build servers) rather than all VPC traffic. Use SNI-based domain rules for general enforcement.
4. Skip the firewall for trusted internal traffic: Use stateless rules to PASS traffic between private subnets that you've already secured via Security Groups. Only forward internet-bound and suspicious east-west traffic to stateful rules.
5. Review and prune Suricata rulesets quarterly: Every active Suricata rule adds to the processing overhead per packet. Remove rules for protocols or services not deployed in your environment. A 500-rule Suricata group inspecting SMTP when you don't run mail servers wastes cycles on every packet.