This check flags any VPC without flow logs enabled, leaving you blind to network traffic during a breach investigation. Turn on flow logs at the VPC level and ship them to CloudWatch Logs or S3 so you have a record of who talked to what.
When something goes wrong in your AWS account, the first question is usually "what was talking to what?" Without VPC Flow Logs, you can't answer that. There is no record of the connections that crossed your network, no source IPs, no rejected packets, nothing. This check catches VPCs that are running blind.
What this check detects
The vpc_flowlogs check inspects every VPC in your account and verifies that flow logging is enabled. A VPC fails the check when no flow log resource is attached to it, regardless of whether the destination would be CloudWatch Logs, S3, or Amazon Data Firehose.
VPC Flow Logs capture metadata about IP traffic going to and from network interfaces in your VPC. Each record includes the source and destination addresses, ports, protocol, packet and byte counts, and whether the traffic was accepted or rejected by your security groups and NACLs.
Note: Flow logs capture traffic metadata, not packet contents. You see that 10.0.1.5 connected to 10.0.2.9 on port 443, but not the payload. For full packet capture you would need VPC Traffic Mirroring, which is a separate feature.
Why it matters
Flow logs are the network equivalent of CCTV footage. You hope you never need them, but the moment you do, their absence is painful and often unrecoverable.
Incident response goes dark
Imagine an EC2 instance gets compromised through a vulnerable web app. The attacker uses it to scan your internal network and exfiltrate data to an external host. With flow logs, your responders can pull the exact destinations the instance reached, identify lateral movement, and scope the blast radius. Without them, you are guessing. You cannot prove what was exfiltrated, which is exactly the kind of detail regulators and customers ask for after a breach.
Warning: Flow logs are not retroactive. If logging was off when the incident happened, that traffic is gone forever. You cannot enable logging after the fact and recover historical data.
Detecting misconfigurations and noisy neighbors
Rejected traffic in flow logs tells you when something is repeatedly trying to reach a resource it shouldn't, or when an app is failing because a security group is too strict. The REJECT records are often the fastest way to debug a connectivity issue that "should just work."
Compliance requirements
Several frameworks expect network traffic logging. PCI DSS, HIPAA, SOC 2, and the CIS AWS Foundations Benchmark all reference flow logging or network monitoring controls. CIS AWS Foundations specifically calls out enabling VPC Flow Logs as a recommended control. A failing check here can become an audit finding.
How to fix it
You enable flow logs per VPC and choose a destination. CloudWatch Logs is convenient for querying and alerting, while S3 is cheaper for long-term retention and integrates well with Athena. Pick based on how you plan to use the data.
Option 1: Console
- Open the VPC console and select Your VPCs.
- Select the VPC that failed the check.
- Open the Flow logs tab and choose Create flow log.
- Set Filter to
Allso you capture both accepted and rejected traffic. - Choose a Maximum aggregation interval (1 minute gives finer detail, 10 minutes lowers cost).
- Pick a destination: a CloudWatch Logs log group or an S3 bucket.
- If sending to CloudWatch Logs, select an IAM role that permits writing logs.
- Create the flow log.
Option 2: AWS CLI (CloudWatch Logs destination)
First create the IAM role that lets the VPC Flow Logs service publish to CloudWatch Logs. Save this trust policy to trust-policy.json:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "vpc-flow-logs.amazonaws.com" },
"Action": "sts:AssumeRole"
}
]
}
Create the role and attach a permissions policy:
aws iam create-role \
--role-name flowlogs-cloudwatch-role \
--assume-role-policy-document file://trust-policy.json
aws iam put-role-policy \
--role-name flowlogs-cloudwatch-role \
--policy-name flowlogs-permissions \
--policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogGroups",
"logs:DescribeLogStreams"
],
"Resource": "*"
}]
}'
Create the log group, then enable flow logs on the VPC:
aws logs create-log-group --log-group-name /vpc/flowlogs
aws ec2 create-flow-logs \
--resource-type VPC \
--resource-ids vpc-0abc123def4567890 \
--traffic-type ALL \
--log-destination-type cloud-watch-logs \
--log-group-name /vpc/flowlogs \
--deliver-logs-permission-arn arn:aws:iam::123456789012:role/flowlogs-cloudwatch-role \
--max-aggregation-interval 60
Option 3: AWS CLI (S3 destination)
For cheaper, long-term storage you can skip the IAM role and point directly at a bucket. Flow Logs uses a service-linked permission model for S3 delivery.
aws ec2 create-flow-logs \
--resource-type VPC \
--resource-ids vpc-0abc123def4567890 \
--traffic-type ALL \
--log-destination-type s3 \
--log-destination arn:aws:s3:::my-flowlogs-bucket/prefix/ \
--max-aggregation-interval 600
Warning: Flow logs add cost. You pay for log ingestion and storage in CloudWatch Logs or S3. On a busy VPC this can add up quickly. Capturing ALL traffic at a 1-minute interval is the most expensive option. If cost is a concern, send to S3, use a 10-minute aggregation interval, and apply lifecycle rules.
Option 4: Terraform
Most teams should manage this in IaC so the configuration is consistent and reviewable. Here is a CloudWatch Logs example:
resource "aws_cloudwatch_log_group" "vpc_flowlogs" {
name = "/vpc/flowlogs"
retention_in_days = 90
}
resource "aws_iam_role" "flowlogs" {
name = "flowlogs-cloudwatch-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "vpc-flow-logs.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy" "flowlogs" {
name = "flowlogs-permissions"
role = aws_iam_role.flowlogs.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogGroups",
"logs:DescribeLogStreams"
]
Resource = "*"
}]
})
}
resource "aws_flow_log" "main" {
vpc_id = aws_vpc.main.id
traffic_type = "ALL"
iam_role_arn = aws_iam_role.flowlogs.arn
log_destination = aws_cloudwatch_log_group.vpc_flowlogs.arn
max_aggregation_interval = 600
}
How to prevent it from happening again
Fixing one VPC by hand does not stop the next one from being created without logging. Bake the control into how VPCs come into existence.
Enforce it in your VPC module
If every VPC is created through a shared Terraform module, put the aws_flow_log resource inside that module. Now no one can stand up a VPC without flow logs, because the module always attaches them.
Add a policy-as-code gate
Use Open Policy Agent with Conftest, or HashiCorp Sentinel, to block plans that create a VPC without an accompanying flow log. A simple OPA rule against a Terraform plan:
package main
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_vpc"
vpc_address := resource.address
not flow_log_for_vpc(vpc_address)
msg := sprintf("VPC %s has no associated aws_flow_log resource", [vpc_address])
}
flow_log_for_vpc(vpc_address) {
fl := input.resource_changes[_]
fl.type == "aws_flow_log"
}
Tip: Skip the per-VPC wiring entirely by using AWS Config with an auto-remediation. The managed rule vpc-flow-logs-enabled detects VPCs without logging, and an SSM Automation document can enable it automatically. Lensix will also re-flag any VPC that drifts back into a non-compliant state, so you catch manual changes between scans.
Continuous detection
Detection and prevention work together. Run the vpc_flowlogs check on a schedule so a VPC created outside your IaC pipeline, or one where someone deleted the flow log, gets surfaced within hours instead of during an incident.
Best practices
- Capture ALL traffic. Logging only accepted or only rejected traffic creates gaps. Rejected records are often the most useful for spotting probing and scanning.
- Choose your destination by use case. CloudWatch Logs for live querying and CloudWatch alarms, S3 with Athena for cheap long-term analysis. Many teams send to both.
- Use a custom log format. The default format omits useful fields. Add
pkt-srcaddr,pkt-dstaddr,flow-direction, andtcp-flagsto make investigations faster. - Set retention deliberately. Match your compliance requirements. 90 days is a common baseline, but some frameworks ask for a year or more. Apply lifecycle policies so you are not paying hot-storage rates for old logs.
- Enable at the VPC level, not per subnet or ENI. A VPC-level flow log automatically covers new subnets and interfaces. Per-ENI logs go stale the moment someone adds a resource.
- Centralize across accounts. In a multi-account setup, ship flow logs to a dedicated logging account's S3 bucket so a compromised account cannot tamper with its own evidence.
Danger: If an attacker gains write access to the account hosting your flow logs, they can delete the records that would expose their activity. Store logs in a separate, locked-down account, enable S3 Object Lock or a restrictive bucket policy, and deny deletion to anyone except a tightly scoped break-glass role.
Flow logs are cheap insurance against an expensive problem. Turn them on everywhere, ship them somewhere safe, and make sure your IaC can never produce a VPC without them.

