AWS Network Firewall: Deep Packet Inspection and Traffic Filtering

AWS Network Firewall Deep Packet Inspection

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.

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.

ControlOSI LayerWhere It RunsStateInspects PayloadBest For
Security GroupL4ENI on every instanceStatefulNoInstance-level allow/deny by port + source SG
NACLL3/L4Subnet boundaryStatelessNoSubnet-wide IP block lists, egress control
AWS WAFL7 HTTP/HTTPSCloudFront / ALB / API GWStateful (request-level)HTTP onlySQL injection, XSS, rate limiting, bot control on web apps
AWS Network FirewallL3–L7Dedicated firewall subnet in your VPCBothYes (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.

Key insight: AWS WAF cannot inspect east-west VPC traffic (traffic between your microservices or between VPCs). AWS Network Firewall can. If you need IPS capabilities on internal service-to-service traffic, Network Firewall is your only option in AWS.

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:

  1. Public subnet route table: Traffic bound for private subnets is routed to the firewall endpoint instead of the local route.
  2. Firewall subnet route table: Traffic from the firewall is sent to the internet gateway for outbound or back to the private subnet for inbound.
  3. 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
Critical gotcha: The IGW edge route table is a special route table associated with the IGW itself (not with a subnet). You must associate it with the IGW via 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 TypeCapacity Cost
Stateless rule (5-tuple, single IP)1
Stateless rule with IP prefix list1 per CIDR
Stateful 5-tuple rule1
Suricata rule (no content)1
Suricata rule (with content keyword)3
Suricata rule (with PCRE)10
Domain list rule5 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"]
  }
}
Pattern: Use stateless rules to DROP known-bad IP ranges (Spamhaus DROP list, Tor exit nodes, your own blocklist) and to PASS loopback/internal traffic that you know is safe. Forward everything else to stateful rules. This minimizes stateful rule group processing cost.

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;)
Note: Domain list rules can only inspect SNI (not the decrypted certificate CN) unless TLS inspection is enabled. For domains that use encrypted SNI (ESNI/ECH), SNI-based filtering will not work — you would need TLS inspection or IP-based blocking.

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]
      }]
    }]
  }'
When to use TLS inspection: Enable it for outbound egress inspection from workloads (EC2 instances, containers, Lambda in VPC) to the internet. Do NOT enable it for inbound traffic to your ALB — WAF handles that. TLS inspection adds latency (5–20ms) and ACM Private CA costs ($400/month per CA), so use it selectively on high-risk egress paths.

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 alert rules. 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 drop action.

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.

ComponentPrice (us-east-1)Notes
Firewall endpoint$0.395/hour per AZ~$285/month per AZ, regardless of traffic volume
Traffic processing$0.065 per GBBoth inbound and outbound traffic processed
ACM Private CA (for TLS inspection)$400/month per CAPlus $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.

Cost vs security trade-off: The endpoint charge is unavoidable for any AZ where you need coverage. Treat it like the cost of a managed firewall appliance — at $285/AZ/month, it's dramatically cheaper than a commercial NGFW like Palo Alto or Fortinet virtual appliances, which start at $1,000–$5,000/month for equivalent throughput.
Share This Article