AWS Networking

VPC Flow Logs for Network Troubleshooting: Format, Queries & Finding the Block

A connection is timing out. The app team swears “nothing changed,” the security team swears “the security group is fine,” and the network team swears “routing is correct.” Everyone is looking at a different config screen and nobody is looking at the packet. VPC Flow Logs are the one place that records what actually happened to that packet — was it allowed, was it denied, or did it never arrive at all — and reading them correctly is the difference between a five-minute fix and a three-team, three-hour bridge call.

A Flow Log is a per-interface record of the IP traffic that crossed an elastic network interface (ENI): source and destination address, ports, protocol, byte and packet counts, and — the field that ends most arguments — an action of ACCEPT or REJECT. It does not capture packet payloads; it is a metadata ledger of flows, aggregated over a time window, delivered to CloudWatch Logs, Amazon S3, or Kinesis Data Firehose. Enabled at the VPC, subnet, or ENI level, it turns “the connection is broken somewhere” into “the security group denied inbound TCP 443 from 203.0.113.10 at 02:14, here is the exact record.”

This article is a troubleshooting playbook, not a feature tour. You will learn the record format field by field — the fourteen default version-2 fields in order and the high-value custom fields (pkt-srcaddr, flow-direction, traffic-path, tcp-flags) that reveal the real client behind a NAT gateway or load balancer. You will learn the crucial gaps — the traffic Flow Logs deliberately do not record (IMDS, the Amazon DNS resolver, DHCP) — so you stop chasing a REJECT that will never appear. You will learn to query with CloudWatch Logs Insights and with Athena over S3, and — the heart of it — the verdict rule that maps a record to a blocking layer: a REJECT at the ENI means a security group denied it; an ACCEPT inbound with a REJECT outbound means a stateless NACL dropped the return; and no record at all means the packet never reached the ENI, so you look upstream at routing. This maps to the networking domains of SAA-C03, SOA-C02, and ANS-C01.

What problem this solves

When a TCP connection fails inside a VPC, the failure is silent. A security group or a network ACL that denies a packet does not send back a “connection refused” — it drops the packet on the floor, and the client sits there until it times out. Silence is the cruellest failure mode because it looks identical whether the security group blocked you, a NACL blocked the return, a route table sent the packet into a black hole, or the destination host is simply down. Four completely different root causes, one identical symptom: a spinner that never resolves.

Without Flow Logs you troubleshoot this by inspection and guesswork: you open the security group and read the rules, you open the NACL and read the rules, you open the route table, you SSH to the box (if you can reach it) and run tcpdump, and you argue about which config is wrong. Every one of those configs can look correct while the packet still dies, because the bug is often the interaction between them — a stateful SG that allows the request but a stateless NACL that forgot the ephemeral-port return, or a rule that is perfect but attached to the wrong ENI.

Flow Logs collapse that guesswork into evidence. They tell you three things no config screen can: whether the packet reached the ENI at all (if there is no record, the problem is upstream of the ENI — routing, IGW, NAT, peering — not a firewall), which direction was allowed or denied (pairing the inbound and outbound records isolates a stateless-NACL asymmetry), and the real source address behind a NAT or load balancer (the pkt-srcaddr custom field). Everyone who runs anything in a VPC hits this: the platform team that owns the NACLs, the app team that owns the security groups, the security team that must prove what was blocked, and the on-call engineer who just wants the connection to work. The mechanism you learn here is layer localisation: turn a silent, multi-team timeout into a single record that names the guilty hop.

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You should already understand VPC fundamentals: a VPC is your private network, carved into subnets (public or private), with route tables deciding where a subnet’s traffic goes, an internet gateway (IGW) for public egress and a NAT gateway for private egress. You should know that security groups (SGs) are stateful, allow-only firewalls attached to the ENI, and network ACLs (NACLs) are stateless, ordered allow/deny lists attached to the subnet. You need the AWS CLI configured and permission to read EC2, CloudWatch Logs, S3, and Athena. Cost awareness helps: Flow Logs are billed on data ingested, and Athena on data scanned.

This sits in the Networking / Observability track and is the diagnostic layer beneath everything else. It is the natural companion to the Security Groups vs NACLs deep dive and connectivity-blocking troubleshooting guide — that article teaches the two firewalls; this one teaches you to read the evidence of what they did. It assumes the network you build in Building an AWS VPC from Scratch: Subnets, Route Tables, IGW & a Public/Private Design, and it pairs with Private-Subnet Internet Egress: NAT Gateway vs NAT Instance, Hands-On & Cost because the NAT gateway is exactly where srcaddr stops being the real client. For the audit trail of who changed the security group rather than what the packet did, see AWS CloudTrail and Config: Audit and Compliance at Scale.

Here is who owns which layer, so during an incident you localise the failure and then call the right team:

Layer What lives here Usual owner Failure class it causes Flow Log signature
Route table / IGW / NAT Where the packet goes Network team Black-holed traffic No record at the ENI
VPC peering / TGW Cross-VPC path Network team Unreachable peer No record, or REJECT if arrives
NACL (subnet) Stateless allow/deny Platform / network Return-path drops ACCEPT in + REJECT out
Security group (ENI) Stateful allow-only App / security Inbound denied REJECT in at the ENI
Host OS / app Local firewall, listener App / dev Refused / reset ACCEPT + RST, or app-level fail
ENI attachment Which SGs are on which ENI App / platform “Right rule, wrong ENI” REJECT despite a correct-looking SG

Core concepts

Six mental models make every later diagnosis obvious.

A Flow Log record is an aggregated flow, not a packet. During a capture window (the aggregation interval, 1 or 10 minutes) the service groups traffic by a tuple — interface, source/destination address, source/destination port, protocol — and emits one record per tuple with the summed packets and bytes, plus the earliest start and latest end timestamps and the flags it observed. You never see individual packets or payload; you see “over this window, this flow moved N bytes and was ACCEPTed.”

The action field is the verdict, and it is combined. ACCEPT means the traffic was permitted by the security groups and the network ACLs for that ENI’s path; REJECT means one of them denied it. Crucially, the field does not name which one — that is your job, using the direction and the stateful-vs-stateless behaviour below.

Statefulness shapes what a REJECT means. Security groups are stateful: if an inbound flow is allowed, the return is automatically allowed, so you will never see a REJECT on the return caused solely by the SG. NACLs are stateless: the return traffic must be explicitly allowed on the outbound ephemeral-port range or it is dropped. Therefore an ACCEPT inbound paired with a REJECT outbound on a high ephemeral port is the fingerprint of a NACL, never a security group.

“No record” is itself a diagnosis. Flow Logs capture traffic that reaches the ENI. If a connection attempt produces no record at all, the packet never got to the interface — it was dropped upstream by routing (no route, wrong route table, missing IGW/NAT, un-propagated peering/VPN route). Absence of evidence is, here, evidence of an upstream problem. This is the single most under-used diagnostic in the tool.

Capture level is a lens, not a filter of truth. You can attach a Flow Log to a whole VPC, a subnet, or a single ENI. They do not override each other — each configured log independently captures and delivers, so overlapping logs mean duplicate records and duplicate cost. The ENI level is the sharpest, cheapest lens when you are chasing one host; the VPC level is the blanket for security and compliance.

Some traffic is invisible on purpose. A short, fixed list of flows is never recorded — instance metadata, the Amazon DNS resolver, DHCP, and a few others. If you go hunting for a REJECT on one of those, you will search forever, because the record does not exist and never will.

The vocabulary in one table

Concept One-line definition Where it lives Why it matters
Flow Log Per-interface record of IP flows VPC / subnet / ENI The evidence ledger
ENI Elastic network interface Attached to instances, LBs, endpoints The capture point
Flow record One aggregated tuple per window In the log destination The row you read
Aggregation interval Capture window (1 or 10 min) Flow Log config Trades latency vs volume
action ACCEPT / REJECT verdict Every record Allowed or denied
log-status OK / NODATA / SKIPDATA Every record Health of the capture
srcaddr / dstaddr Addresses as seen at the ENI v2 default May be a NAT/LB, not the client
pkt-srcaddr Real source before NAT/LB rewrite v3 custom The actual client
flow-direction ingress / egress at the ENI v5 custom Which way the flow went
traffic-path Egress path taken (IGW/NAT/peering…) v5 custom Where egress is routed
tcp-flags Bitmask of TCP flags seen v3 custom SYN-only = half-open handshake
Capture level VPC / subnet / ENI scope Flow Log config Breadth vs cost
Destination CloudWatch / S3 / Firehose Flow Log config Latency, cost, query tool

What a single record represents

Aspect Behaviour Consequence for troubleshooting
Granularity One row per flow tuple per interval Bursty short flows aggregate into one row
Timestamps start = first packet, end = last (Unix seconds) Sub-second timing is lost; use for windows, not latency
Counters packets, bytes summed over the window Zero-byte REJECTs are common (SYN dropped)
Direction Encoded by src/dst (v2) or flow-direction (v5) Pair inbound+outbound to spot NACL asymmetry
Verdict action = ACCEPT or REJECT Combined SG+NACL result, not per-layer
Missing rows Excluded traffic + upstream drops + NODATA Absence has three distinct meanings

What Flow Logs capture — and at which level

You attach a Flow Log to one of three resource types. All three capture the same kind of record; they differ only in scope — which ENIs are covered.

Level --resource-type Scope Best for Gotcha
VPC VPC Every current and future ENI in the VPC Security baseline, compliance, “log everything” Highest volume + cost; includes ENIs you did not create
Subnet Subnet Every ENI in that subnet A tier (web/app/db) or an AZ Still broad; new ENIs auto-included
ENI NetworkInterface One interface Chasing one instance / endpoint Manual per ENI; new instances not auto-covered

Precedence when several apply. They do not override. If you enable a VPC-level log and an ENI-level log that both cover the same interface, each independently captures and delivers — you get two copies of the flow and pay twice. There is no “most specific wins” merge. The practical rule: run one blanket VPC-level log for security/compliance, and add a temporary ENI-level log during an incident only if you need a different format or destination than the blanket one — then delete it in teardown.

The aggregation interval

The maximum aggregation interval controls the capture window and therefore how fast records appear.

Setting Value Delivery latency (typical) When to use
Default 600 s (10 min) Records appear ~several minutes after each 10-min window closes Steady-state security/compliance logging
Fast 60 s (1 min) Records appear a few minutes after each 1-min window Active incident — you want data now

Two facts that save confusion: records are never real-time — even at 60 s you wait for the window to close plus a delivery delay, so budget roughly 5–10 minutes before a fresh flow shows up. And for interfaces on Nitro-based instances the interval can be effectively shorter, but do not design around that; assume the configured value.

What “traffic type” you capture

--traffic-type Captures Use it when
ALL Accepted and rejected flows Troubleshooting — you must see the REJECTs
ACCEPT Only allowed flows Traffic analytics, top talkers, cost by flow
REJECT Only denied flows Security hunting, focused block detection

For troubleshooting always use ALL. A REJECT-only log seems clever for block-hunting, but it hides the ACCEPT-in / REJECT-out pairing that fingerprints a NACL.

Enabling a Flow Log: the key parameters

Every create-flow-logs call is built from these parameters — the reference you keep beside the CLI:

Parameter Purpose Values / example
--resource-type Capture level VPC / Subnet / NetworkInterface
--resource-ids Resource(s) to log eni-0abc…, subnet-0abc…, vpc-0abc…
--traffic-type Which verdicts ALL / ACCEPT / REJECT
--log-destination-type Destination kind cloud-watch-logs / s3 / kinesis-data-firehose
--log-destination Destination ARN log-group ARN, bucket ARN, or Firehose ARN
--deliver-logs-permission-arn IAM role (CloudWatch only) role assumed by the Flow Logs service
--max-aggregation-interval Capture window (seconds) 60 or 600
--log-format Custom field set + order ${srcaddr} ${dstaddr} … tokens
--destination-options S3 format/partitioning FileFormat, HiveCompatiblePartitions, PerHourPartition

The record format: version-2 defaults and custom fields

The default format is version 2: fourteen fields, space-delimited, in a fixed order. Memorise the order — when you write an Athena DDL or a Logs Insights parse, the column order must match exactly or every field shifts by one and your data turns to nonsense.

The version-2 default fields, in order

# Field Meaning Example Note
1 version Log format version 2 5 if you add v3–v5 fields
2 account-id AWS account of the ENI 123456789012 unknown for some managed ENIs
3 interface-id The ENI that captured it eni-0abc123def456 Your capture point
4 srcaddr Source address at the ENI 203.0.113.10 May be a NAT/LB, not the client
5 dstaddr Destination address at the ENI 10.0.1.25
6 srcport Source port 54321 Ephemeral for a client
7 dstport Destination port 443 The service port
8 protocol IANA protocol number 6 6=TCP, 17=UDP, 1=ICMP
9 packets Packets in the window 12 0-byte flows still count packets
10 bytes Bytes in the window 4620
11 start First packet time (Unix s) 1752460440
12 end Last packet time (Unix s) 1752460470
13 action ACCEPT / REJECT REJECT The verdict
14 log-status OK / NODATA / SKIPDATA OK Health of the capture

The high-value custom fields (v3–v5)

The default format is enough for top-talker analytics but not enough for the hardest bugs. A custom format lets you pick any of the available fields; these are the ones that repay their column width in a troubleshooting log.

Field Version What it gives you Why it shortens an investigation
pkt-srcaddr v3 Real source before NAT/LB rewrite The actual client behind a NAT gateway, ALB, or gateway endpoint
pkt-dstaddr v3 Real destination before rewrite The true target behind a load balancer / endpoint
flow-direction v5 ingress or egress at the ENI Pair the two directions to spot NACL asymmetry instantly
traffic-path v5 Egress path (1–8): IGW, NAT, VGW, peering… “Is my egress going out the IGW or the NAT?” answered directly
tcp-flags v3 Bitmask of TCP flags seen A lone SYN (2) with no SYN-ACK = handshake never completed
subnet-id v3 Subnet of the ENI Localise without a second lookup
instance-id v3 Instance behind the ENI Skip the ENI→instance mapping step
vpc-id v3 VPC of the ENI Multi-VPC log disambiguation
type v3 IPv4 / IPv6 / EFA Confirm which stack the flow used
region v4 AWS region Multi-region aggregation
az-id v4 Availability Zone ID AZ-scoped analysis
sublocation-type / sublocation-id v4 Wavelength / Outpost / Local Zone Edge deployments
pkt-src-aws-service v5 AWS service name of the source Is the caller S3, DYNAMODB, CLOUDFRONT…?
pkt-dst-aws-service v5 AWS service name of the destination Egress to which AWS service
reject-reason newer Why a flow was rejected (e.g. BPA) Distinguishes some managed rejects
ecs-* (cluster, task, service…) v7 ECS task/service metadata Attribute a flow to an ECS task

A troubleshooting-grade custom format that stays readable:

${version} ${account-id} ${interface-id} ${srcaddr} ${dstaddr} ${srcport} ${dstport} ${protocol} ${packets} ${bytes} ${start} ${end} ${action} ${log-status} ${flow-direction} ${pkt-srcaddr} ${pkt-dstaddr} ${traffic-path} ${tcp-flags} ${subnet-id} ${instance-id}

srcaddr vs pkt-srcaddr: the source behind a rewrite

The single most valuable reason to run a custom format is to see the real endpoint when something in the path rewrites the address. srcaddr/dstaddr are what the ENI sees; pkt-srcaddr/pkt-dstaddr are the original packet’s addresses.

Path in front of the ENI srcaddr shows pkt-srcaddr shows Why it matters
NAT gateway (private → internet) The NAT gateway’s private IP The real originating instance Attribute egress to the actual instance, not the NAT
Application/Network Load Balancer The LB node’s IP The real client (for some paths) Find the true source of a blocked/abusive flow
Gateway VPC endpoint (S3/DynamoDB) The endpoint/ENI address The instance behind it Confirm which host reached S3
Direct ENI-to-ENI (no rewrite) The real source Same as srcaddr The two fields match — no NAT in play

Decoding protocol

The protocol field is the IANA number, not a name — a frequent “why does it say 6?” moment.

Number Protocol Common ports
1 ICMP (ping, unreachable)
6 TCP 22, 80, 443, 3306, 5432
17 UDP 53, 123, 500/4500
47 GRE (tunnels)
50 ESP (IPsec VPN)
58 ICMPv6 (IPv6 neighbour/ping)

Decoding tcp-flags

tcp-flags is a bitmask of the flags observed at least once during the window. It is diagnostic gold for handshakes.

Value Flags Interpretation
2 SYN Connection attempt
18 SYN + ACK Peer accepted (2 + 16)
1 FIN Graceful close
4 RST Reset — refused or aborted
16 ACK Data/ack (with other flags)
3 SYN + FIN Very short flow (2 + 1)
19 SYN + ACK + FIN Short completed flow

The classic read: an ingress record with tcp-flags=2 (SYN only) and action=REJECT means the SYN arrived and was denied — the handshake never got a SYN-ACK back. An ingress SYN ACCEPTed but no corresponding egress SYN-ACK means the app never answered (listener down / host firewall), not a security-group problem.

Decoding traffic-path

traffic-path (v5) is populated for egress flows and answers “which door did my outbound traffic leave through” — the fastest way to catch traffic taking the wrong path (out the IGW when it should hit the NAT, or vice-versa).

Value Egress path
1 Through another resource in the same VPC
2 Through an internet gateway or a gateway VPC endpoint
3 Through a virtual private gateway (VGW)
4 Through an intra-region VPC peering connection
5 Through an inter-region VPC peering connection
6 Through a local gateway (Outposts)
7 Through a gateway VPC endpoint (Nitro-based instances)
8 Through an internet gateway (Nitro-based instances)

Decoding log-status

Value Meaning What to do
OK Data logged normally Nothing — healthy
NODATA No traffic to/from the ENI in the window The interface was idle — this is not a block; generate traffic
SKIPDATA Some records were skipped (internal constraint) A capacity gap; occasional SKIPDATA is normal, sustained means volume is too high

NODATA is the most-misread value in the whole system: engineers see a screen full of NODATA and conclude “logging is broken” or “everything is blocked.” It means neither — it means the ENI simply had no traffic during that window.

The crucial gaps: traffic Flow Logs never record

Before you spend an hour hunting a REJECT that does not exist, memorise the exclusion list. These flows are not captured at any level, so their absence tells you nothing about firewalls.

Excluded traffic Address / port Why it is excluded Where to debug instead
Instance metadata (IMDS) 169.254.169.254 Link-local, node-internal IMDSv2 hop limit, instance role, HttpTokens
Amazon DNS resolver VPC base + 2 (e.g. 10.0.0.2) Managed resolver traffic VPC DNS attributes, Route 53 Resolver query logs
Amazon Time Sync (NTP) 169.254.169.123 Link-local time service Chrony/w32time on the host
DHCP UDP 67/68 Node-internal address assignment DHCP option sets
Windows license activation AWS-managed KMS endpoints Managed activation path Windows licensing on the instance
Default VPC router Reserved base + 1 The subnet’s implied gateway Route table, not a firewall
Traffic to your own custom DNS (captured!) Only the Amazon resolver is excluded If you run BIND/Unbound, those flows do appear

The one that trips people up: DNS to the Amazon resolver is invisible, but DNS to your own resolver is visible. So “no DNS records in the Flow Log” is expected if you use the default VPC DNS, and not a sign that DNS is blocked. Likewise, if IMDS calls are failing, do not hunt a Flow Log REJECT — there is none; check the IMDSv2 token requirement and the metadata hop limit instead.

Destinations: CloudWatch Logs, S3, and Kinesis Data Firehose

A Flow Log delivers to exactly one destination per configured log. Pick on latency, query tool, and cost.

Destination Latency Native query tool Cost model Retention Best for
CloudWatch Logs Near-real-time Logs Insights Ingestion + storage per GB Log-group retention setting Live incidents, alerting, metric filters
Amazon S3 Windowed (batched files) Athena (SQL) S3 storage + Athena scan S3 lifecycle Long retention, big analytics, cheapest at scale
Kinesis Data Firehose Streaming Downstream tool Firehose + downstream Downstream Fan-out to OpenSearch / Splunk / Datadog / cross-account

CloudWatch Logs specifics

Records land in a log group, one log stream per ENI (named for the interface). You attach an IAM role that lets the Flow Logs service write to the group. You can create metric filters (e.g. count REJECTs and alarm on a spike) and run Logs Insights interactively. The trade-off is cost: CloudWatch Logs ingestion is materially more expensive per GB than S3, so a chatty VPC logged to CloudWatch at scale gets pricey fast.

S3 specifics — text vs Parquet, and partitioning

S3 is the cost-efficient archive, and the format choices matter enormously for Athena bills.

Choice Options Recommendation Why
File format Plain text (space-delimited) or Parquet Parquet Columnar → Athena scans only the columns you select
Partitioning None, or Hive-compatible prefixes Hive-compatible region=…/year=…/month=…/day=… → prune scans by date
Per-hour partition On / off On for busy VPCs Adds hour=…; narrower scans, faster queries
Compression GZIP (text) / Snappy (Parquet default) Default Less S3 storage and scan

With hive-compatible prefixes the object keys look like:

s3://kv-flowlogs/AWSLogs/aws-account-id=123456789012/aws-service=vpcflowlogs/aws-region=us-east-1/year=2026/month=07/day=14/hour=02/...

That structure is what lets Athena prune — a query filtered to one day scans one day’s objects, not the whole bucket, and that is the single biggest lever on your Athena cost.

Kinesis Data Firehose specifics

Firehose streams records to S3, Redshift, OpenSearch, or third-party SaaS (Splunk, Datadog, New Relic), optionally cross-account. Use it when Flow Logs must feed an existing SIEM or observability pipeline rather than being queried in place.

Querying: CloudWatch Logs Insights and Athena

CloudWatch Logs Insights

For the default (version-2) format, Logs Insights auto-discovers named fields; note they are camelCase, not the hyphenated names of the raw record, which trips up every first query:

Raw v2 field Logs Insights field
version version
account-id accountId
interface-id interfaceId
srcaddr srcAddr
dstaddr dstAddr
srcport srcPort
dstport dstPort
protocol protocol
packets packets
bytes bytes
action action
log-status logStatus

For a custom format, those auto-discovered fields may not appear, so parse the @message yourself in the field order you configured. Here is the cookbook.

Goal Query
Top talkers by bytes filter action="ACCEPT" | stats sum(bytes) as b by srcAddr, dstAddr | sort b desc | limit 20
REJECT hunting filter action="REJECT" | stats count(*) as rej by srcAddr, dstAddr, dstPort | sort rej desc | limit 20
Blocks on one port filter action="REJECT" and dstPort=443 | stats count(*) by srcAddr | sort count(*) desc
All flows for one client filter srcAddr="203.0.113.10" | sort @timestamp desc | limit 50
Rejects for one ENI filter interfaceId="eni-0abc123" and action="REJECT" | stats count(*) by dstPort
SYN-only (half-open), custom fmt parse @message "* * * ..." as ... , tcpFlags | filter tcpFlags="2" and action="REJECT"
Real client behind NAT, custom fmt parse @message ... as ..., pktSrcAddr, ... | filter action="REJECT" | stats count(*) by pktSrcAddr, dstPort
Ingress vs egress split, custom fmt parse @message ... as ..., flowDirection | stats count(*) by flowDirection, action

Run one from the CLI to prove the loop end to end:

QID=$(aws logs start-query \
  --log-group-name /vpc/flowlogs \
  --start-time $(($(date +%s) - 3600)) \
  --end-time $(date +%s) \
  --query-string 'filter action="REJECT" | stats count(*) as rej by srcAddr, dstAddr, dstPort | sort rej desc | limit 20' \
  --query 'queryId' --output text)

sleep 5
aws logs get-query-results --query-id "$QID"

Athena over S3

First point Athena at the bucket. This DDL matches the version-2 default column order (align it with your --log-format if you customised it):

CREATE EXTERNAL TABLE IF NOT EXISTS vpc_flow_logs (
  version int, account_id string, interface_id string,
  srcaddr string, dstaddr string, srcport int, dstport int,
  protocol bigint, packets bigint, bytes bigint,
  start bigint, `end` bigint, action string, log_status string
)
PARTITIONED BY (`aws_region` string, `year` string, `month` string, `day` string)
ROW FORMAT DELIMITED FIELDS TERMINATED BY ' '
LOCATION 's3://kv-flowlogs/AWSLogs/123456789012/vpcflowlogs/'
TBLPROPERTIES ("skip.header.line.count"="1");

Loading partitions is the part people get wrong. Three ways, best first:

Approach How Trade-off
Partition projection TBLPROPERTIES describe the partition ranges; Athena computes them No crawler, no MSCK; scans only matched partitions — best
MSCK REPAIR TABLE Scans S3 for new partitions each time Slow on big buckets; must re-run
Glue crawler Scheduled crawler populates the catalog Extra cost + lag

Partition projection for the hive-compatible layout — set once, never think again:

ALTER TABLE vpc_flow_logs SET TBLPROPERTIES (
  'projection.enabled'='true',
  'projection.aws_region.type'='enum',
  'projection.aws_region.values'='us-east-1,eu-west-1',
  'projection.year.type'='integer','projection.year.range'='2024,2030',
  'projection.month.type'='integer','projection.month.range'='1,12','projection.month.digits'='2',
  'projection.day.type'='integer','projection.day.range'='1,31','projection.day.digits'='2',
  'storage.location.template'='s3://kv-flowlogs/AWSLogs/123456789012/vpcflowlogs/aws_region=${aws_region}/year=${year}/month=${month}/day=${day}'
);

Then the query cookbook. Always include a partition filter — it is the difference between scanning megabytes and terabytes.

Goal Query (partition filter shown)
Rejects on a day SELECT srcaddr,dstaddr,dstport,count(*) c FROM vpc_flow_logs WHERE action='REJECT' AND year='2026' AND month='07' AND day='14' GROUP BY 1,2,3 ORDER BY c DESC LIMIT 20;
Rejects to one port ... WHERE action='REJECT' AND dstport=443 AND year='2026' AND month='07' AND day='14' ...
Top talkers by bytes SELECT srcaddr,dstaddr,sum(bytes) b FROM vpc_flow_logs WHERE action='ACCEPT' AND year='2026' AND month='07' AND day='14' GROUP BY 1,2 ORDER BY b DESC LIMIT 20;
Human timestamps SELECT from_unixtime(start) t, srcaddr, dstaddr, dstport, action FROM vpc_flow_logs WHERE ... ORDER BY t DESC;
Protocol decode SELECT CASE protocol WHEN 6 THEN 'TCP' WHEN 17 THEN 'UDP' WHEN 1 THEN 'ICMP' ELSE cast(protocol as varchar) END proto, count(*) FROM vpc_flow_logs WHERE ... GROUP BY 1;

Finding the blocking layer — the workflow

This is the point of the whole exercise. You have a silent failure; the Flow Log record (or its absence) tells you which layer to fix. The rule in one sentence: a REJECT inbound at the ENI is the security group (or an inbound NACL deny); an ACCEPT inbound with a REJECT outbound on an ephemeral port is a stateless NACL; and no record at all means the packet never reached the ENI, so look upstream at routing.

The verdict decision table

What the Flow Log shows Conclusion Confirm with Fix
REJECT, ingress, at the ENI Security group has no matching inbound allow (or inbound NACL denies) describe-security-groups; then describe-network-acls Add the ingress rule for the port + source; check the NACL is not denying
ACCEPT ingress + REJECT egress on 1024–65535 Stateless NACL dropped the return describe-network-acls outbound rules Add NACL egress allow for ephemeral 1024–65535 to the client CIDR
ACCEPT both ways, app still fails Not SG/NACL — host firewall / listener / app ss -tlnp / tcpdump on the host Start the service; open the host firewall
ACCEPT ingress + tcp-flags include RST Host actively refused (reached the OS) Host logs App not listening on that port
No record at all Packet never reached the ENI describe-route-tables, IGW/NAT, peering routes Fix the route / gateway / peering, not a firewall
srcaddr = a NAT/LB address You are reading the rewritten source Add pkt-srcaddr to the format Re-query on pkt-srcaddr for the real client

The symptom → what the Flow Log shows → conclusion → fix playbook

This is the table to keep open at 02:14. Each row is a real, recurring network incident.

# Symptom (what the user sees) What the Flow Log shows Conclusion (the layer) Fix
1 Client times out on :443 REJECT, ingress, tcp-flags=2 (SYN) at the ENI Security group has no inbound allow for 443 authorize-security-group-ingress 443 from the client CIDR; verify NACL-in
2 Handshake half-opens then stalls ingress ACCEPT (SYN) + egress REJECT on 1024–65535 NACL outbound missing the ephemeral allow (stateless return dropped) Add NACL egress ALLOW 1024–65535 to the client CIDR
3 “Connection refused” instantly ingress ACCEPT + tcp-flags includes RST (4) Packet reached the host; app not listening / host firewall RST Start the service / open the OS firewall — not SG/NACL
4 Nothing happens, pure timeout No record for the attempt at all Packet never reached the ENI — routing black hole Fix the subnet route table / IGW / NAT / peering route
5 Works same-subnet, fails cross-subnet REJECT ingress from the other subnet’s CIDR SG or NACL scoped to the wrong source CIDR Widen the allow to the peer subnet CIDR
6 Logs show the NAT GW IP, not the client ACCEPT/REJECT with srcaddr = NAT address You are reading srcaddr behind NAT Add pkt-srcaddr; re-query for the real client
7 Private-subnet egress to internet fails egress present at ENI but no return, or REJECT Missing NAT/route or NACL ephemeral-in Add NAT GW + 0.0.0.0/0 route; allow NACL ephemeral inbound
8 Load-balancer health checks fail REJECT ingress from the LB subnet on the health-check port SG does not allow the LB’s SG/subnet on that port Allow the LB security group on the health-check port
9 Return works one way only Asymmetric ACCEPT/REJECT pair across directions Stateless NACL asymmetry (inbound ≠ outbound rules) Align the inbound and outbound NACL rules
10 DNS resolution failing No record for 10.0.0.2 (base + 2) Amazon DNS traffic is not logged (a gap) Debug via VPC DNS attributes / Resolver query logs, not Flow Logs
11 IMDS / role creds failing No record for 169.254.169.254 IMDS link-local is not logged (a gap) Check IMDSv2 token + metadata hop limit, not Flow Logs
12 Whole log is NODATA log-status=NODATA on every row ENI idle — no traffic in the window Generate traffic; NODATA is not a block
13 Cross-account / peered VPC unreachable REJECT or no record depending on the hop REJECT = SG/NACL; no record = peering route missing If REJECT: fix SG/NACL. If none: add peering routes in both route tables
14 On-prem over VPN/DX unreachable No record, or REJECT if it arrives No record = route not propagated; REJECT = SG/NACL Fix route propagation on the VGW/TGW, or the SG/NACL
15 “Right rule, wrong ENI” REJECT despite a correct-looking SG The SG is not attached to this ENI (or a second ENI exists) describe-instances; attach the SG to the correct interface
16 Intermittent REJECTs under load REJECT spikes correlate with connection count Ephemeral-port exhaustion behind NAT (not a firewall rule) Fix connection reuse / add NAT capacity; NACL/SG are innocent

When Flow Logs are not enough

Flow Logs tell you what happened to the packet, but the action field never literally prints “SG sg-0abc denied you.” When you need the exact blocking component named for you, reach for Reachability Analyzer, which statically evaluates the path between two ENIs and returns the precise SG rule, NACL entry, or missing route that blocks it — the perfect complement to a Flow Log REJECT. For the history of who changed the firewall, that is a control-plane question for CloudTrail and Config, covered in AWS CloudTrail and Config: Audit and Compliance at Scale.

Architecture at a glance

Read the diagram left to right. Traffic crossing an EC2 instance’s ENI is sampled into a flow record; you choose the capture level (VPC, subnet, or ENI — the ENI is the sharpest lens) and the format (version-2 default, or a custom format that adds pkt-srcaddr, flow-direction, traffic-path and tcp-flags). The record is delivered to CloudWatch Logs for near-real-time querying with Logs Insights and/or to S3 (Parquet, hive-partitioned) for cheap SQL with Athena. You query for the failing flow and read a verdict: ACCEPT means both the security group and the NACL passed the packet; REJECT means one denied it; and no record at all means the packet never reached the ENI, so you look upstream at routing. The six badges mark the decisions that matter — what is captured, at which level, in which format, where it lands, how you query, and how the verdict names the guilty layer.

Left-to-right architecture of VPC Flow Logs troubleshooting: client and EC2 ENI traffic is captured as a flow record at VPC, subnet or ENI level in version-2 or custom format, delivered to CloudWatch Logs and S3-with-Athena, queried by Logs Insights and Athena SQL, and resolved to an ACCEPT, REJECT or NODATA verdict that names the security group, NACL or upstream routing as the blocking layer, with six numbered badges and a narrating legend.

Real-world scenario

Nimbus Retail runs a checkout API on EC2 behind an internal Application Load Balancer, in private subnets across two AZs, egressing through a NAT gateway. On a Tuesday the mobile app starts throwing intermittent checkout failures — not all requests, maybe one in eight — and only from the newest AZ, us-east-1c, which the platform team had added a week earlier to add capacity. The app team insists the code is unchanged. The security team insists the security groups are identical across AZs. The network team insists routing is symmetric. Three teams, three “it’s not us,” and a revenue graph sagging.

The on-call engineer, instead of joining the argument, opens the VPC-level Flow Log they had running to S3 the whole time. She queries Athena for REJECTs on the checkout port during the incident window, filtered to that day’s partition:

SELECT srcaddr, dstaddr, dstport, action, count(*) c
FROM vpc_flow_logs
WHERE action='REJECT' AND dstport=8443
  AND year='2026' AND month='07' AND day='08'
GROUP BY 1,2,3,4 ORDER BY c DESC LIMIT 20;

The result is unambiguous: a cluster of REJECT rows, all egress, all on ephemeral ports 1024–65535, all with a destination in the us-east-1c subnet’s CIDR, 10.0.5.0/24. The inbound requests to those same instances were ACCEPT. ACCEPT in, REJECT out on an ephemeral port — the textbook fingerprint of a stateless NACL that is missing the return allow. The security groups genuinely were identical; the security team was right. But when the platform team created the new subnet in us-east-1c, they had attached a custom NACL copied from an older template that allowed inbound ephemeral traffic but whose outbound rules only allowed 80 and 443 — not the 1024–65535 ephemeral range the load balancer’s return traffic needed.

The fix took ninety seconds: add an outbound ALLOW for TCP 1024–65535 to the load-balancer subnet CIDR on the us-east-1c NACL. Checkout success recovered immediately. The lesson Nimbus took away was not “NACLs are dangerous” but “the Flow Log named the layer while three teams were still guessing.” They made two permanent changes: a metric filter on the CloudWatch-mirrored Flow Log that alarms when egress REJECTs on ephemeral ports exceed a baseline, and a Terraform module for subnets that ships NACLs with a correct symmetric ephemeral rule by default, so a copy-paste template can never reintroduce the asymmetry. The intermittency — one in eight — had been the tell all along: only the fraction of the load balancer’s connection pool that hashed to us-east-1c hit the broken NACL.

Advantages and disadvantages

Advantages Disadvantages
Records the actual verdict (ACCEPT/REJECT) per flow Metadata only — no packet payload or deep inspection
“No record” localises failures to upstream routing Not real-time — 5–10 min lag even at 1-min interval
Reveals the real client behind NAT/LB (pkt-srcaddr) Excludes IMDS, Amazon DNS, DHCP — silent gaps
Cheap at rest in S3 + Parquet; SQL via Athena action names the verdict, not which SG/NACL rule
Enables at VPC/subnet/ENI with no agent on the host Overlapping logs duplicate records and cost
Feeds SIEM/alerting via CloudWatch and Firehose Aggregated windows lose sub-second timing

Flow Logs are a layer-3/4 metadata ledger, and every advantage and limitation follows from that. They shine at answering “was this flow allowed, denied, or absent, and where did it really come from” — which is exactly the question a silent timeout poses. They are the wrong tool for “what was in the HTTP request” (that is application logging or a mirror/IDS), for “which exact SG rule blocked me” (that is Reachability Analyzer), and for “who changed the security group” (that is CloudTrail). Use them for what they are: the fastest way to turn a multi-team argument into a single row that names the guilty hop.

Hands-on lab

You will enable an ENI-level Flow Log to S3 with a custom format, generate a blocked connection, find the REJECT, and identify the blocking layer. Free-tier-friendly: a t3.micro, a tiny S3 footprint, and a few Athena scans. Region us-east-1.

⚠️ Costs money: the EC2 instance (if you launch one and run it beyond free tier), S3 storage for the log objects, and Athena data scanned. All are cents at this scale; the teardown removes everything.

Step 1 — Identify the target ENI

Use an existing instance or launch a throwaway t3.micro in a public subnet. Capture its ENI:

ENI=$(aws ec2 describe-instances \
  --filters "Name=tag:Name,Values=flowlog-lab" "Name=instance-state-name,Values=running" \
  --query 'Reservations[0].Instances[0].NetworkInterfaces[0].NetworkInterfaceId' \
  --output text)
echo "ENI=$ENI"

Expected: ENI=eni-0abc123def4567890.

Step 2 — Create the S3 bucket for logs

ACCT=$(aws sts get-caller-identity --query Account --output text)
BUCKET="kv-flowlogs-$ACCT"
aws s3api create-bucket --bucket "$BUCKET" --region us-east-1

The VPC Flow Logs service writes to the bucket using a service-linked delivery path; you do not need a custom bucket policy for same-account S3 delivery in most setups, but if delivery fails with an access error, add the AWS-documented delivery.logs.amazonaws.com bucket policy (see Step 8’s troubleshooting note).

Step 3 — Create the ENI-level Flow Log with a custom format

aws ec2 create-flow-logs \
  --resource-type NetworkInterface \
  --resource-ids "$ENI" \
  --traffic-type ALL \
  --log-destination-type s3 \
  --log-destination "arn:aws:s3:::$BUCKET" \
  --max-aggregation-interval 60 \
  --destination-options FileFormat=parquet,HiveCompatiblePartitions=true,PerHourPartition=true \
  --log-format '${version} ${account-id} ${interface-id} ${srcaddr} ${dstaddr} ${srcport} ${dstport} ${protocol} ${packets} ${bytes} ${start} ${end} ${action} ${log-status} ${flow-direction} ${pkt-srcaddr} ${pkt-dstaddr} ${traffic-path} ${tcp-flags}'

Expected:

{ "FlowLogIds": ["fl-0a1b2c3d4e5f67890"], "Unsuccessful": [] }

An empty Unsuccessful array means the log was created; a populated one carries the per-resource error to fix.

The equivalent in Terraform:

resource "aws_flow_log" "eni" {
  eni_id                   = "eni-0abc123def4567890"
  traffic_type             = "ALL"
  log_destination          = aws_s3_bucket.flowlogs.arn
  log_destination_type     = "s3"
  max_aggregation_interval = 60

  log_format = "$${version} $${account-id} $${interface-id} $${srcaddr} $${dstaddr} $${srcport} $${dstport} $${protocol} $${packets} $${bytes} $${start} $${end} $${action} $${log-status} $${flow-direction} $${pkt-srcaddr} $${pkt-dstaddr} $${traffic-path} $${tcp-flags}"

  destination_options {
    file_format                = "parquet"
    hive_compatible_partitions = true
    per_hour_partition         = true
  }
}

resource "aws_s3_bucket" "flowlogs" {
  bucket = "kv-flowlogs-123456789012"
}

(The doubled $$ escapes Terraform interpolation so the literal ${field} tokens reach AWS.)

Step 4 — Generate a blocked connection

Make sure the instance’s security group has no inbound rule for TCP 8080. From your laptop, try to reach it on that closed port:

PUBIP=$(aws ec2 describe-instances \
  --filters "Name=tag:Name,Values=flowlog-lab" \
  --query 'Reservations[0].Instances[0].PublicIpAddress' --output text)
curl -m 5 "http://$PUBIP:8080/" ; echo "exit=$?"

Expected: the curl hangs for 5 seconds and returns exit=28 (timeout) — the SYN was silently dropped because the security group has no allow for 8080. That silent drop is exactly what you will now find in the log.

Step 5 — Wait for delivery

Records are windowed. Give it 5–10 minutes, then confirm objects arrived:

aws s3 ls "s3://$BUCKET/AWSLogs/$ACCT/vpcflowlogs/" --recursive | tail

Expected: Parquet objects under ...aws-region=us-east-1/year=2026/month=07/day=14/....

Step 6 — Build the Athena table

In the Athena console (or via the CLI/SDK) create a database and a table over the bucket. Because you chose Parquet + hive partitions, use partition projection so no MSCK is needed. Point storage.location.template at your bucket. Then run:

Step 7 — Find the REJECT and identify the layer

SELECT from_unixtime(start) AS t, srcaddr, dstaddr, dstport,
       action, flow_direction, tcp_flags
FROM vpc_flow_logs
WHERE action='REJECT' AND dstport=8080
  AND year='2026' AND month='07' AND day='14'
ORDER BY t DESC LIMIT 20;

Expected: one or more rows with action=REJECT, flow_direction=ingress, tcp_flags=2 (SYN only — the handshake never completed). Apply the verdict rule: REJECT, ingress, at the ENI = the security group denied inbound. Confirm the layer:

SG=$(aws ec2 describe-instances --filters "Name=tag:Name,Values=flowlog-lab" \
  --query 'Reservations[0].Instances[0].SecurityGroups[0].GroupId' --output text)
aws ec2 describe-security-groups --group-ids "$SG" \
  --query 'SecurityGroups[0].IpPermissions[?ToPort==`8080`]'

Expected: [] — no ingress rule for 8080 exists. That is the block, named. (Fix, if you wanted the connection to work, would be authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 8080 --cidr <your-ip>/32 — but for the lab you have proved the diagnosis, which is the point.)

Step 8 — Teardown

# Delete the Flow Log
FLID=$(aws ec2 describe-flow-logs \
  --filter "Name=resource-id,Values=$ENI" \
  --query 'FlowLogs[0].FlowLogId' --output text)
aws ec2 delete-flow-logs --flow-log-ids "$FLID"

# Drop the Athena table (in Athena): DROP TABLE vpc_flow_logs;

# Empty and delete the bucket
aws s3 rm "s3://$BUCKET" --recursive
aws s3api delete-bucket --bucket "$BUCKET"

# Terminate the lab instance if you launched one for this
# aws ec2 terminate-instances --instance-ids <id>

Verify with aws ec2 describe-flow-logs --filter "Name=resource-id,Values=$ENI" returning an empty FlowLogs array.

Common mistakes & troubleshooting

The playbook above localises network failures. This section troubleshoots the Flow Logs themselves — why the log is empty, wrong, or expensive — because a broken log during an incident is its own emergency.

# Symptom Root cause Confirm (exact command / path) Fix
1 No log objects / streams ever appear Delivery IAM role (CloudWatch) or bucket policy (S3) missing/wrong aws ec2 describe-flow-logs → read DeliverLogsErrorMessage Attach the correct role; add the delivery.logs.amazonaws.com bucket policy
2 Every row is log-status=NODATA ENI is idle — no traffic in the window Is anything actually talking to the ENI? Generate traffic; NODATA ≠ block, ≠ broken
3 Occasional log-status=SKIPDATA Internal capacity limit skipped some records Grep for SKIPDATA frequency Rare is normal; sustained → reduce volume / split logs
4 Records lag 10+ minutes 10-min aggregation interval + delivery delay --max-aggregation-interval value Set 60 for active incidents
5 Athena columns all shifted / garbage DDL column order ≠ custom --log-format order Compare the table DDL to the flow log’s LogFormat Rewrite the DDL to match the format exactly
6 Athena returns zero rows Partitions not loaded, or wrong LOCATION/projection template SHOW PARTITIONS vpc_flow_logs; Fix projection template / MSCK REPAIR TABLE / correct LOCATION
7 Athena bill spikes Full-table scan — no partition filter, text not Parquet “Data scanned” in query stats Add WHERE year/month/day=…; store Parquet
8 srcaddr is a NAT/LB IP, not the client Reading the rewritten address in v2 default Is there a NAT GW/LB in the path? Add pkt-srcaddr to the format; re-query
9 Hunting a REJECT that never appears Traffic is on the exclusion list (IMDS/DNS/DHCP) Is dst 169.254.x.x or the base+2 resolver? Stop — it is a gap; debug that service directly
10 Duplicate rows, doubled cost VPC-level and ENI-level logs overlap aws ec2 describe-flow-logs — count logs on the ENI Keep one scope; delete the redundant log
11 REJECT despite a “correct” SG Rule is on an SG not attached to this ENI, or a 2nd ENI describe-instances → each ENI’s Groups[] Attach the SG to the right ENI; audit secondary ENIs
12 protocol shows 6/17, not TCP/UDP It is the IANA number, not a name Decode: 6=TCP, 17=UDP, 1=ICMP; not a bug
13 Everything ACCEPT but app still down Flow Logs are L3/L4 — ACCEPT ≠ app replied ss -tlnp on the host; app logs Debug the listener / host firewall, not SG/NACL
14 Cannot tell SG from NACL on a REJECT action does not name the layer Apply the verdict rule + describe-* Use direction: REJECT-in → SG; ACCEPT-in/REJECT-out → NACL

The status / delivery reference

Value or error Where Meaning Fix
log-status=OK Record Healthy capture None
log-status=NODATA Record Idle ENI, no traffic in window Generate traffic; not an error
log-status=SKIPDATA Record Some records skipped (capacity) Reduce volume if sustained
DeliverLogsErrorMessage: ...role... describe-flow-logs CloudWatch delivery role lacks permission Fix/attach the IAM role trust + policy
Access Denied writing to S3 Delivery Bucket policy missing delivery.logs.amazonaws.com Add the documented bucket policy
Unsuccessful non-empty on create create-flow-logs The ENI/permissions rejected the log Read the error; fix the resource or role

The three failure modes worth internalising

First: NODATA is not “blocked.” The most common false alarm in the whole system. A screen of NODATA means the ENI had no traffic during those windows — a load balancer target that is out of rotation, a scaled-down instance, a test box no one is hitting. If you were expecting traffic and see NODATA, the interesting question is “why is nothing reaching this ENI,” which is an upstream question (routing, target-group registration), not a firewall one.

Second: the shifted-column disaster. When you customise --log-format, the fields land in the order you wrote, but your Athena DDL (or Logs Insights parse) has a separate fixed order. If they disagree by even one field, every column shifts: dstport reads into the protocol column, action reads a byte count, and your REJECT hunt returns nonsense. Always generate the DDL from the exact LogFormat string, and keep them versioned together.

Third: the Athena bill. A single unpartitioned SELECT * over a busy VPC’s year of text-format logs can scan terabytes and cost real money for one query. Three habits eliminate it: store Parquet, partition by date (hive-compatible), and never run a query without a partition filter in the WHERE. Watch the “Data scanned” figure — it is the price tag on every query.

Best practices

Security notes

Flow Log data is sensitive reconnaissance material: it maps your internal CIDRs, which hosts talk to which, on which ports, and where your egress goes. Treat the destination as you would any security log.

The least-privilege delivery grants, by destination — get these wrong and you get an empty log with a DeliverLogsErrorMessage:

Destination Who writes the records Permission it needs
CloudWatch Logs Flow Logs service, assuming your IAM role Role (--deliver-logs-permission-arn) with logs:CreateLogGroup/Stream, logs:PutLogEvents, logs:DescribeLogStreams; trust vpc-flow-logs.amazonaws.com
S3 The delivery.logs.amazonaws.com service principal Bucket policy allowing that principal s3:PutObject + s3:GetBucketAcl
S3 with SSE-KMS Same delivery principal KMS key policy granting the principal kms:GenerateDataKey*
Kinesis Data Firehose Flow Logs via an IAM role Role with firehose:PutRecord/PutRecordBatch; trust the flow-logs principal
Cross-account S3 Delivery principal in the source account Destination bucket policy scoped with an aws:SourceAccount condition

Cost & sizing

Flow Logs have no per-log charge; you pay the destination’s ingestion and storage, plus query cost. The volume is a function of flow count, not bandwidth — a chatty microservice mesh generates far more records than a few big file transfers.

Cost driver Where it lands Rough figure (us-east-1) Lever
CloudWatch Logs ingestion Per GB ingested ~US$0.50 / GB (~₹42) Log to S3 instead for archive
CloudWatch Logs storage Per GB-month ~US$0.03 / GB (~₹2.5) Set log-group retention
S3 storage Per GB-month ~US$0.023 / GB (~₹2) Parquet + lifecycle to Glacier
Athena scan Per TB scanned ~US$5 / TB (~₹420) Parquet + partition filters
Firehose Per GB ingested ~US$0.029 / GB (~₹2.4) Only if you need streaming fan-out

Sizing intuition: CloudWatch Logs is for the recent, hot window you alert and query interactively; S3 is for the deep, cheap archive you occasionally query with Athena. A common pattern is both — a CloudWatch log with a short retention (say 7–14 days) for live incident response and metric-filter alarms, and an S3 log with a year+ lifecycle for compliance and post-incident analytics. The cost trap is logging a high-volume VPC to CloudWatch at full ALL traffic and long retention; the same data in S3-Parquet with lifecycle is an order of magnitude cheaper. Free-tier note: CloudWatch Logs includes a modest ingestion/storage free allowance, and Athena/S3 costs at lab scale are cents — but a production VPC’s Flow Logs are a real, recurring line item you should size and alarm on deliberately.

Interview & exam questions

1. What does action=REJECT in a Flow Log tell you, and what does it not tell you? It tells you a security group or a network ACL denied the flow. It does not tell you which one — the action field is the combined verdict. You disambiguate with direction and statefulness: REJECT inbound at the ENI points to the SG (or inbound NACL), while ACCEPT-in with REJECT-out on an ephemeral port points to a stateless NACL. (SAA-C03 / SOA-C02 / ANS-C01)

2. A connection times out and there is no Flow Log record for it at all. What is your conclusion? The packet never reached the ENI. Flow Logs capture at the interface, so absence means an upstream drop — a missing or wrong route table entry, a missing IGW/NAT, or an un-propagated peering/VPN route. Look at routing, not firewalls. (ANS-C01)

3. Why might srcaddr show a NAT gateway’s IP instead of the real client? Because srcaddr is the address as seen at the ENI, after NAT/LB rewriting. The pkt-srcaddr custom field (v3) preserves the original source before the rewrite, so you add it to a custom format to see the true client. (ANS-C01)

4. What are three types of traffic Flow Logs never record? Instance metadata (169.254.169.254), traffic to the Amazon DNS resolver (VPC base + 2), and DHCP — plus Amazon Time Sync (169.254.169.123), Windows license activation, and the reserved default-router address. Their absence is expected and says nothing about firewalls. (SOA-C02)

5. You enable a VPC-level and an ENI-level Flow Log covering the same interface. What happens? Both capture and deliver independently — you get duplicate records and pay twice. There is no override or merge; the ENI level is not “more specific wins.” (SAA-C03)

6. When would you choose S3 over CloudWatch Logs as a destination? For cheap long-term retention and large-scale analytics: S3 in Parquet with hive partitions, queried by Athena, is far cheaper per GB than CloudWatch ingestion. Choose CloudWatch for near-real-time interactive queries and metric-filter alarms. (SOA-C02)

7. What is the fingerprint of a NACL problem versus a security-group problem in Flow Logs? ACCEPT on the inbound flow paired with REJECT on the outbound return (on an ephemeral port 1024–65535) is a stateless NACL missing its return allow — a stateful SG would auto-allow the return. A plain REJECT on the inbound flow is the SG (or an inbound NACL deny). (ANS-C01)

8. What does log-status=NODATA mean, and why is it commonly misread? It means the ENI had no traffic during the aggregation window — the interface was idle. It is misread as “blocked” or “logging broken”; it is neither. If you expected traffic, the question is why nothing reached the ENI (routing, target registration). (SOA-C02)

9. How do you keep Athena costs down when querying Flow Logs? Store the logs as Parquet, partition by date (hive-compatible prefixes), and always include a partition filter in the WHERE clause; use partition projection to avoid crawlers/MSCK. Without those, a single query can scan terabytes. (SAA-C03)

10. What does tcp-flags=2 on a REJECTed ingress record tell you? Only a SYN was seen — a connection attempt that got no SYN-ACK because it was denied. It confirms the handshake never completed at the firewall layer, distinguishing “blocked” from “reached the host and was reset” (which would show an RST). (ANS-C01)

11. Flow Logs show ACCEPT both ways but the application still fails. Where do you look? Not at SG/NACL — an ACCEPT means both firewalls passed the packet. The problem is at the host or app: the listener is down, bound to the wrong interface, or a host OS firewall reset it. Check ss -tlnp, host firewall, and app logs. (SOA-C02)

12. You need the exact SG rule or NACL entry that blocks a path, not just the layer. What tool complements Flow Logs? Reachability Analyzer statically evaluates the path between two ENIs and names the precise blocking component (SG rule, NACL entry, or missing route) — the complement to a Flow Log REJECT that only names the layer. (ANS-C01)

Quick check

  1. In the version-2 default format, which field is position 13, and what are its two values?
  2. You see a Flow Log record with flow-direction=egress, action=REJECT, dstport in 1024–65535. Which layer is the prime suspect and why?
  3. Name two traffic types that never appear in Flow Logs.
  4. What single S3 + Athena habit most reduces query cost?
  5. A REJECT record shows srcaddr as your NAT gateway’s IP. Which field do you add to see the real client?

Answers

  1. Position 13 is action, with values ACCEPT and REJECT. (Position 14 is log-status.)
  2. A stateless network ACL missing the outbound ephemeral-port allow — a stateful security group would have auto-allowed the return of an accepted inbound flow, so a REJECT on the egress return points to the NACL.
  3. Any two of: instance metadata (169.254.169.254), Amazon DNS resolver (VPC base + 2), DHCP, Amazon Time Sync (169.254.169.123), Windows license activation, the reserved default-router address.
  4. Partitioning by date (hive-compatible) and always filtering the partition in the WHERE — combined with storing Parquet. It bounds the data scanned.
  5. pkt-srcaddr (the v3 custom field) — it preserves the original source before the NAT/LB rewrite.

Glossary

Term Definition
VPC Flow Log A per-interface record of IP traffic metadata (not payload) captured at VPC, subnet, or ENI level.
ENI Elastic network interface — the capture point; a Flow Log records traffic to and from it.
Flow record One aggregated row per flow tuple per aggregation interval, with counters and a verdict.
Aggregation interval The capture window, 60 s or 600 s; larger = fewer records, more latency.
action The combined SG+NACL verdict for a flow: ACCEPT or REJECT.
log-status Capture health: OK (logging), NODATA (idle ENI), SKIPDATA (records skipped).
srcaddr / dstaddr Addresses as seen at the ENI — may be a NAT/LB address, not the real endpoint.
pkt-srcaddr / pkt-dstaddr The real source/destination before NAT/LB rewrite (custom v3 fields).
flow-direction Whether the flow was ingress or egress at the ENI (custom v5 field).
traffic-path The egress path a flow took — IGW, NAT, VGW, peering, etc. (custom v5, values 1–8).
tcp-flags Bitmask of TCP flags seen in the window; 2 = SYN, 18 = SYN-ACK, 4 = RST, 1 = FIN.
Version 2 The default record format: 14 space-delimited fields in a fixed order.
Custom format A chosen subset/superset of fields (v3–v7) in an order you define.
Security group Stateful, allow-only firewall on the ENI; return traffic auto-allowed.
Network ACL Stateless, ordered allow/deny list on the subnet; return must be allowed explicitly.
Partition projection Athena feature that computes partitions from ranges, avoiding crawlers/MSCK.

Next steps

AWSVPC Flow LogsNetworkingTroubleshootingCloudWatch Logs InsightsAthenaSecurity GroupsNACL
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading