Sometimes you need to see what's actually on the wire. Maybe you're debugging a flaky connection between services, hunting for data exfiltration, or validating that TLS is actually being used where you think it is. You can't just tcpdump on an EC2 instance's ENI from outside — AWS doesn't give you that level of access to the underlying hypervisor network.
That's where VPC Traffic Mirroring comes in. It copies packets at the ENI level and delivers them to a target you control, wrapped in VXLAN encapsulation. You get a full packet capture without installing agents on your workloads.
How Traffic Mirroring Works
The architecture has three components:
- Mirror Source — the elastic network interface (ENI) you want to capture traffic from
- Mirror Filter — rules that control which packets get copied (think of it as a lightweight BPF)
- Mirror Target — where the copied packets land (an ENI, a Network Load Balancer, or a Gateway Load Balancer endpoint)

The key thing to understand: mirrored traffic is not routed through your VPC's normal path. AWS copies the packets at the hypervisor level and delivers them via VXLAN encapsulation to the target's IP address on UDP port 4789. The target must explicitly allow inbound UDP 4789 in its security group.
VXLAN Encapsulation
Every mirrored packet arrives at the target wrapped in a VXLAN header. The outer packet looks like this:
┌─────────────────────────────────────────────┐
│ Outer Ethernet Header │
├─────────────────────────────────────────────┤
│ Outer IP Header │
│ Src: source ENI primary IP │
│ Dst: target ENI/NLB IP │
├─────────────────────────────────────────────┤
│ Outer UDP Header │
│ Src: hash-derived port │
│ Dst: 4789 │
├─────────────────────────────────────────────┤
│ VXLAN Header │
│ VNI: session virtual network ID │
├─────────────────────────────────────────────┤
│ Original mirrored packet (L2 frame) │
│ (the actual traffic you want to inspect) │
└─────────────────────────────────────────────┘
Your monitoring tool needs to understand VXLAN decapsulation. Most do — Suricata, Zeek, and even plain tcpdump can handle it with the right flags.
What You Need Before Starting
- A VPC with at least two subnets (source and target can be in the same subnet, but separating them is cleaner)
- An EC2 instance to mirror from (the source) — must use a supported instance type
- An EC2 instance (or NLB) to mirror to (the target)
- The target's security group must allow inbound UDP 4789 from the source
Setting It Up with Terraform
Here's a complete Terraform configuration that mirrors all TCP traffic from a source instance to a monitoring instance running in the same VPC.
Step 1: Create the Mirror Target
The target is the ENI of your monitoring instance:
resource "aws_ec2_traffic_mirror_target" "monitor" {
description = "Traffic mirror target - monitoring instance"
network_interface_id = aws_instance.monitor.primary_network_interface_id
tags = {
Name = "traffic-mirror-target"
}
}If you're using a Network Load Balancer instead (for scaling to multiple monitoring instances):
resource "aws_ec2_traffic_mirror_target" "nlb" {
description = "Traffic mirror target - NLB"
network_load_balancer_arn = aws_lb.mirror_nlb.arn
tags = {
Name = "traffic-mirror-target-nlb"
}
}Step 2: Create the Mirror Filter
Filters control what gets mirrored. Each filter has inbound and outbound rules. Here's one that captures all TCP traffic but ignores SSH (so you don't mirror your own admin sessions into an infinite loop):
resource "aws_ec2_traffic_mirror_filter" "tcp_no_ssh" {
description = "Mirror all TCP except SSH"
tags = {
Name = "tcp-no-ssh-filter"
}
}
# Accept all inbound TCP
resource "aws_ec2_traffic_mirror_filter_rule" "inbound_tcp" {
traffic_mirror_filter_id = aws_ec2_traffic_mirror_filter.tcp_no_ssh.id
description = "Accept all inbound TCP"
rule_number = 100
rule_action = "accept"
direction = "ingress"
protocol = 6 # TCP
destination_cidr_block = "0.0.0.0/0"
source_cidr_block = "0.0.0.0/0"
}
# Reject inbound SSH (higher priority)
resource "aws_ec2_traffic_mirror_filter_rule" "reject_inbound_ssh" {
traffic_mirror_filter_id = aws_ec2_traffic_mirror_filter.tcp_no_ssh.id
description = "Reject inbound SSH"
rule_number = 50
rule_action = "reject"
direction = "ingress"
protocol = 6
destination_cidr_block = "0.0.0.0/0"
source_cidr_block = "0.0.0.0/0"
destination_port_range {
from_port = 22
to_port = 22
}
}
# Accept all outbound TCP
resource "aws_ec2_traffic_mirror_filter_rule" "outbound_tcp" {
traffic_mirror_filter_id = aws_ec2_traffic_mirror_filter.tcp_no_ssh.id
description = "Accept all outbound TCP"
rule_number = 100
rule_action = "accept"
direction = "egress"
protocol = 6
destination_cidr_block = "0.0.0.0/0"
source_cidr_block = "0.0.0.0/0"
}
# Reject outbound SSH
resource "aws_ec2_traffic_mirror_filter_rule" "reject_outbound_ssh" {
traffic_mirror_filter_id = aws_ec2_traffic_mirror_filter.tcp_no_ssh.id
description = "Reject outbound SSH"
rule_number = 50
rule_action = "reject"
direction = "egress"
protocol = 6
destination_cidr_block = "0.0.0.0/0"
source_cidr_block = "0.0.0.0/0"
destination_port_range {
from_port = 22
to_port = 22
}
}Step 3: Create the Mirror Session
The session ties source, target, and filter together:
resource "aws_ec2_traffic_mirror_session" "main" {
description = "Mirror TCP traffic from app server"
traffic_mirror_filter_id = aws_ec2_traffic_mirror_filter.tcp_no_ssh.id
traffic_mirror_target_id = aws_ec2_traffic_mirror_target.monitor.id
network_interface_id = aws_instance.app_server.primary_network_interface_id
session_number = 1
tags = {
Name = "app-server-mirror-session"
}
}session_number determines priority when multiple sessions share a source ENI. Lower numbers are evaluated first, and each packet is only mirrored once — if session 1 matches, session 2 won't see it.
Step 4: Security Group for the Target
The target must accept VXLAN traffic:
resource "aws_security_group_rule" "allow_vxlan" {
type = "ingress"
from_port = 4789
to_port = 4789
protocol = "udp"
cidr_blocks = [data.aws_vpc.main.cidr_block]
security_group_id = aws_security_group.monitor.id
description = "Allow VXLAN from mirror sources"
}
Receiving Mirrored Traffic
Once the session is active, VXLAN-encapsulated packets start arriving at your target on UDP 4789. Here's how to inspect them with common tools.
tcpdump (Quick and Dirty)
# Capture raw VXLAN packets
sudo tcpdump -i eth0 udp port 4789 -w /tmp/mirror-capture.pcap
# Decode VXLAN inline
sudo tcpdump -i eth0 udp port 4789 -n -eThe -w flag writes a pcap you can open in Wireshark later. Wireshark natively decodes VXLAN, so you'll see the inner packets as if you were on the source's wire.
Suricata (IDS/IPS)
Suricata can listen directly on a VXLAN interface. Create a VXLAN tunnel device and point Suricata at it:
# Create a VXLAN interface that decapsulates traffic from the mirror
sudo ip link add vxlan0 type vxlan id 0 dev eth0 dstport 4789 local $(hostname -I | awk '{print $1}')
sudo ip link set vxlan0 up
# Run Suricata on the decapsulated interface
sudo suricata -c /etc/suricata/suricata.yaml -i vxlan0Zeek (Network Analysis)
Same approach — point Zeek at the VXLAN interface:
sudo ip link add vxlan0 type vxlan id 0 dev eth0 dstport 4789 local $(hostname -I | awk '{print $1}')
sudo ip link set vxlan0 up
# Run Zeek
sudo zeek -i vxlan0 localZeek will produce its standard log files (conn.log, dns.log, http.log, etc.) from the mirrored traffic.
Architecture Patterns
Pattern 1: Single Monitor Instance
Simplest setup. One source, one target. Good for debugging a specific instance.

Pattern 2: NLB Fan-Out
Multiple monitoring instances behind a Network Load Balancer. The NLB distributes mirrored traffic across the fleet using a 5-tuple hash, so each monitor sees a subset of flows.

Pattern 3: Gateway Load Balancer
For inline inspection appliances (third-party firewalls, deep packet inspection). The GWLB endpoint acts as the mirror target and routes traffic through the appliance transparently.

Costs and Limits
A few things to keep in mind:
- No per-packet charge for mirroring itself, but you pay for the data transfer between source and target if they're in different AZs
- Bandwidth: mirrored traffic counts against the source instance's network throughput. If your instance is already saturated, mirroring adds overhead
- Session limit: 3 mirror sessions per ENI by default (adjustable via quota request)
- Packet truncation: you can set
packet_lengthon the session to only mirror the first N bytes of each packet — useful for capturing headers without the payload when you're bandwidth-constrained
resource "aws_ec2_traffic_mirror_session" "headers_only" {
# ... other fields ...
packet_length = 128 # Only mirror first 128 bytes
}Troubleshooting
If you're not seeing traffic on the target:
- Security group: does the target allow inbound UDP 4789 from the source's IP/subnet?
- NACL: does the subnet's network ACL allow UDP 4789 in both directions?
- Instance type: is the source a supported Nitro instance? T2s silently fail.
- Filter rules: did you accidentally reject everything? Check rule numbers — lower numbers evaluate first.
- Connectivity: use VPC Reachability Analyzer to confirm the source can reach the target.
- Already mirrored: if another session with a lower
session_numberalready matched the packet, it won't be mirrored again.
When to Use Traffic Mirroring
- Incident response: capture traffic from a compromised instance without alerting the attacker
- Compliance auditing: prove that sensitive data isn't leaving the VPC unencrypted
- Network debugging: see exactly what's on the wire when application logs aren't enough
- IDS/IPS: feed traffic to Suricata or Zeek without deploying agents on every instance
- Third-party appliance integration: route mirrored traffic through vendor security tools via GWLB
Wrapping Up
Traffic Mirroring gives you packet-level visibility into your VPC without modifying your workloads. The setup is straightforward — a target, a filter, and a session — and Terraform makes it reproducible. The main gotchas are instance type support, security group rules for UDP 4789, and remembering that mirrored traffic is VXLAN-encapsulated.
Start with a single source and tcpdump on the target to prove the plumbing works, then graduate to Suricata or Zeek when you're ready for automated analysis.