AWS Networking

Validating VPC Connectivity with Reachability Analyzer and Network Access Analyzer

A connectivity ticket on a flat VPC is a five-minute job. On a real estate — forty accounts, a hub-and-spoke Transit Gateway, PrivateLink for shared services, a centralized inspection VPC, overlapping intent everywhere — the same ticket means tracing a packet through security groups, NACLs, two route tables, a TGW route table, and a peering attachment that someone deprecated last quarter. Reading flow logs to reconstruct that path is archaeology. AWS gives you two tools that reason about the configuration instead of waiting for packets: VPC Reachability Analyzer answers “can A reach B, and if not, which component blocks it?” and Network Access Analyzer answers the inverse and far more valuable question for security teams — “is there any path from here to the internet that I did not intend?” This guide uses both correctly, then turns the second one into a continuous compliance control.

When to reach for which tool

These three tools look adjacent and are not interchangeable. Pick by the question you are actually asking.

Tool Question it answers Data source Direction of reasoning
Reachability Analyzer “Can this specific source reach this specific destination?” Config (static analysis) Point-to-point, you name both ends
Network Access Analyzer “Does any path exist matching this pattern?” Config (static analysis) Many-to-many, you describe a shape
VPC Flow Logs “What traffic actually flowed?” Observed packets Historical, after the fact

The distinction that matters: the first two are static reasoning over configuration — they do not send a packet and they will find a misconfiguration even on a path that has never carried traffic. Flow logs are the opposite; they only show you what already happened, and a path that is wide open but idle leaves no trace until it is exploited.

Mental model: Reachability Analyzer is traceroute that works before you deploy. Network Access Analyzer is a linter for your network’s trust boundaries. Flow logs are the access log. You want all three, for different jobs.

Both analyzers are part of VPC Network Insights, billed per analysis run, and both can reason across accounts in the same AWS Organization. Neither requires an agent or any change to your workloads.

1. Run a point-to-point reachability analysis

Reachability Analyzer is a two-step API: you create a path (the source/destination/protocol tuple), then you start an analysis against it. Sources and destinations are resources — instances, ENIs, internet gateways, TGW attachments, VPC endpoints — referenced by ID within an account or by ARN across accounts.

Start with the canonical case: an operator swears the app instance cannot reach the database on 5432.

# Create the path: app ENI -> database ENI, TCP/5432
PATH_ID=$(aws ec2 create-network-insights-path \
  --source eni-0app1234567890abc \
  --destination eni-0db09876543210fed \
  --destination-port 5432 \
  --protocol tcp \
  --query 'NetworkInsightsPath.NetworkInsightsPathId' \
  --output text)

# Run the analysis (takes seconds to a couple of minutes)
ANALYSIS_ID=$(aws ec2 start-network-insights-analysis \
  --network-insights-path-id "$PATH_ID" \
  --query 'NetworkInsightsAnalysis.NetworkInsightsAnalysisId' \
  --output text)

aws ec2 wait network-insights-analysis-succeeded \
  --network-insights-analysis-ids "$ANALYSIS_ID"

The single field you check first is NetworkPathFound. If it is false, the engine has already isolated the blocking component and will tell you exactly which one in the explanation.

aws ec2 describe-network-insights-analyses \
  --network-insights-analysis-ids "$ANALYSIS_ID" \
  --query 'NetworkInsightsAnalyses[0].{Found:NetworkPathFound, \
           Explanation:Explanations[0].ExplanationCode}'
{
    "Found": false,
    "Explanation": "ENI_SG_RULES_MISMATCH"
}

That ExplanationCode is the answer to 90% of tickets. ENI_SG_RULES_MISMATCH means a security group on the path has no rule permitting the flow. Other codes you will meet constantly: NO_ROUTE_TO_DESTINATION, ACL_RULES_MISMATCH (a NACL, not an SG), INGRESS_ACL_RULES_MISMATCH, and BLACKHOLE_ROUTE. The analyzer does the differential diagnosis for you — instead of staring at four SGs and two NACLs, you are told which layer and which direction failed.

A path is reusable. Re-run start-network-insights-analysis against the same PATH_ID after every fix; the path is a durable object you keep, the analysis is the cheap, repeatable verification.

2. Read the hop-by-hop forward path

When NetworkPathFound is true, the value is in ForwardPathComponents (and ReturnPathComponents for the reply direction). This is the static traceroute: every component the packet traverses, in order, with the SG and route-table rule that admitted it at each hop. This is where you confirm traffic takes the path you think it takes — not the deprecated peering connection, not a stale NAT route.

aws ec2 describe-network-insights-analyses \
  --network-insights-analysis-ids "$ANALYSIS_ID" \
  --query 'NetworkInsightsAnalyses[0].ForwardPathComponents[].{ \
           Seq:SequenceNumber, \
           Component:Component.Id, \
           RouteTarget:RouteTableRoute.GatewayId, \
           SgRule:SecurityGroupRule.Cidr}' \
  --output table

A representative forward path through a TGW looks like this, hop by hop:

seq 1  : eni-0app...        (source ENI)
seq 2  : sg-0app...         egress rule allowed 5432/tcp
seq 3  : acl-0a...          subnet NACL, outbound rule 100 allow
seq 4  : rtb-0spoke...      route 10.20.0.0/16 -> tgw-0abc...
seq 5  : tgw-0abc...        TGW attachment + TGW route table hop
seq 6  : rtb-0db...         route 10.10.0.0/16 -> local
seq 7  : sg-0db...          ingress rule allowed 5432/tcp from app SG
seq 8  : eni-0db...         (destination ENI)

Reading this, you can see the TGW route table chose the right attachment (seq 5) and the destination SG admitted the source SG by reference, not by CIDR (seq 7). If seq 4 had pointed at a peering connection you expected to be gone, you have just found your real problem — the path works, but over infrastructure you meant to retire. The forward path is as useful for catching unintended-but-functional routing as it is for debugging outright failures.

3. Cross-account and cross-Region paths through TGW and PrivateLink

The estate-scale value of Reachability Analyzer is that it follows paths across account boundaries — but only when you tell it which accounts the path may legitimately traverse. Run from the management account or a delegated administrator, reference both endpoints by ARN, and pass the intermediate account IDs via --additional-accounts.

# Spoke A (account 111...) instance -> shared service ENI in account 222...,
# transiting the network account 999... that owns the TGW
aws ec2 create-network-insights-path \
  --source arn:aws:ec2:ap-south-1:111111111111:instance/i-0aaa11112222 \
  --destination arn:aws:ec2:ap-south-1:222222222222:network-interface/eni-0svc3456 \
  --destination-port 443 \
  --protocol tcp \
  --query 'NetworkInsightsPath.NetworkInsightsPathId' --output text
aws ec2 start-network-insights-analysis \
  --network-insights-path-id nip-0crossacct123 \
  --additional-accounts 999999999999 222222222222

Without the relevant account IDs in --additional-accounts, the analysis stops at the boundary it cannot see into and reports the path as not found, which is a false negative you will chase for an hour if you do not know to look. For PrivateLink, point the destination at the VPC endpoint (vpce-...) on the consumer side; the analyzer understands the endpoint-to-service hop and validates the endpoint security group and the service’s acceptance, not just raw routing. Cross-Region paths through a TGW peering attachment work the same way — both Region’s resources are addressable by ARN, and the engine reasons across the inter-Region attachment.

4. Author a Network Access Analyzer scope to assert “no internet egress”

Reachability Analyzer proves a path you name. The far more dangerous failure is a path nobody named — a forgotten internet gateway route on a subnet that holds your data tier. You cannot enumerate every source/destination pair to find these. Network Access Analyzer inverts the problem: you describe a shape of path, and it returns every instance of that shape across the entire VPC or account.

A scope is MatchPaths (paths to find) and ExcludePaths (paths that are acceptable, subtracted from the matches). To assert “nothing in my data subnets should reach the internet,” match traffic from those subnets that exits via an internet/NAT gateway:

{
  "MatchPaths": [
    {
      "Source": {
        "ResourceStatement": {
          "Resources": ["subnet-0data1111", "subnet-0data2222"]
        }
      },
      "Destination": {
        "ResourceStatement": {
          "ResourceTypes": [
            "AWS::EC2::InternetGateway",
            "AWS::EC2::NatGateway"
          ]
        }
      }
    }
  ]
}
SCOPE_ID=$(aws ec2 create-network-insights-access-scope \
  --match-paths file://no-egress-scope.json \
  --tag-specifications \
    'ResourceType=network-insights-access-scope,Tags=[{Key=Name,Value=data-tier-no-egress}]' \
  --query 'NetworkInsightsAccessScope.NetworkInsightsAccessScopeId' \
  --output text)

ANALYSIS=$(aws ec2 start-network-insights-access-scope-analysis \
  --network-insights-access-scope-id "$SCOPE_ID" \
  --query 'NetworkInsightsAccessScopeAnalysis.NetworkInsightsAccessScopeAnalysisId' \
  --output text)

When the analysis settles, the assertion is the FindingsFound field. The invariant holds only when it reads falseany finding is a real path out of your data tier.

aws ec2 describe-network-insights-access-scope-analyses \
  --network-insights-access-scope-analysis-ids "$ANALYSIS" \
  --query 'NetworkInsightsAccessScopeAnalyses[0].{ \
           Findings:FindingsFound, ENIs:AnalyzedEniCount, Status:Status}'
{ "Findings": "false", "ENIs": 412, "Status": "succeeded" }

AnalyzedEniCount tells you the blast radius the engine actually reasoned over — useful to confirm the scope covered what you expected and did not silently match nothing.

5. Express segmentation and untrusted-account invariants

The same MatchPaths / ExcludePaths grammar expresses any segmentation rule you can describe as a shape. The expressive lever is ExcludePaths: state the broad prohibition in MatchPaths, then carve out the sanctioned exceptions in ExcludePaths so the analysis returns only the violations.

PCI subnets must not reach non-PCI subnets, with the one approved logging endpoint excepted:

{
  "MatchPaths": [
    {
      "Source":      { "ResourceStatement": { "Resources": ["subnet-0pci01"] } },
      "Destination": { "ResourceStatement": { "ResourceTypes": ["AWS::EC2::NetworkInterface"] } }
    }
  ],
  "ExcludePaths": [
    {
      "Source":      { "ResourceStatement": { "Resources": ["subnet-0pci01"] } },
      "Destination": { "ResourceStatement": { "Resources": ["eni-0approvedlog"] } }
    }
  ]
}

You can also constrain by packet header, not just resource. To assert “nothing should reach the internet on the database ports,” combine an internet-gateway destination with a PacketHeaderStatement on the ports — a finding here means a database is one SG edit away from being exposed:

{
  "MatchPaths": [
    {
      "Source": { "ResourceStatement": { "ResourceTypes": ["AWS::EC2::NetworkInterface"] } },
      "Destination": {
        "PacketHeaderStatement": {
          "DestinationPorts": ["3306", "5432", "1433", "27017"],
          "Protocols": ["tcp"]
        },
        "ResourceStatement": { "ResourceTypes": ["AWS::EC2::InternetGateway"] }
      }
    }
  ]
}

Use ThroughResources in a path statement when the invariant is about what the path must or must not transit — for example, to find any egress that bypasses your inspection appliance by matching paths to the internet that do not pass through the firewall endpoints (assert via the absence of those endpoints in matched ThroughResources). The engine evaluates the entire estate’s SGs, NACLs, route tables, TGW route tables, peering, and endpoints to decide whether each shape is satisfiable.

6. Make it continuous: CI/CD and EventBridge

A one-off scan ages out the moment someone merges a Terraform change. There are two complementary triggers, and mature teams run both.

Pre-merge gate in CI/CD. Run the no-egress and segmentation scopes against the post-apply state in a pipeline stage. Fail the build on any finding so a violating change never reaches production:

# Buildkite / generic CI step — gate the merge on zero findings
steps:
  - label: ":aws: network-invariants"
    command: |
      ANALYSIS=$(aws ec2 start-network-insights-access-scope-analysis \
        --network-insights-access-scope-id "$SCOPE_ID" \
        --query 'NetworkInsightsAccessScopeAnalysis.NetworkInsightsAccessScopeAnalysisId' \
        --output text)
      aws ec2 wait network-insights-access-scope-analysis-succeeded \
        --network-insights-access-scope-analysis-ids "$ANALYSIS"
      FOUND=$(aws ec2 describe-network-insights-access-scope-analyses \
        --network-insights-access-scope-analysis-ids "$ANALYSIS" \
        --query 'NetworkInsightsAccessScopeAnalyses[0].FindingsFound' --output text)
      if [ "$FOUND" != "false" ]; then
        echo "Segmentation invariant violated — see findings"; exit 1
      fi

Scheduled drift detection. Console changes, cross-team SG edits, and out-of-band fixes do not pass through your pipeline. Run the scopes on a schedule and route results to your alerting. Network Access Analyzer emits an Analysis Completed event to EventBridge on source: aws.networkaccessanalyzer, so you can react to every completion:

{
  "source": ["aws.networkaccessanalyzer"],
  "detail-type": ["Analysis Completed"]
}

Pair that with a scheduled EventBridge rule that kicks off the analyses, and a target (Lambda or Step Functions) that reads FindingsFound on completion and pages only when it is not false. The AWS-published reference solution wires exactly this — EventBridge schedule, a Step Functions state machine that starts each scope, polls for succeeded, and forwards violations onward — and is worth adopting rather than rebuilding.

7. Route findings into Security Hub and Config for governance

Operational alerts are for the on-call. Governance needs the finding to land in the same pane as every other control. The pattern is to convert each Network Access Analyzer finding into an ASFF (AWS Security Finding Format) record and import it with BatchImportFindings:

aws securityhub batch-import-findings --findings '[{
  "SchemaVersion": "2018-10-08",
  "Id": "naa/'"$SCOPE_ID"'/'"$ANALYSIS"'",
  "ProductArn": "arn:aws:securityhub:ap-south-1:123456789012:product/123456789012/default",
  "GeneratorId": "network-access-analyzer/'"$SCOPE_ID"'",
  "AwsAccountId": "123456789012",
  "Types": ["Software and Configuration Checks/AWS Security Best Practices"],
  "CreatedAt": "2026-06-08T09:00:00Z",
  "UpdatedAt": "2026-06-08T09:00:00Z",
  "Severity": {"Label": "MEDIUM"},
  "Title": "Unintended network path detected by Network Access Analyzer",
  "Description": "Scope '"$SCOPE_ID"' returned findings; an unsanctioned path exists.",
  "Resources": [{"Type": "Other", "Id": "'"$SCOPE_ID"'"}]
}]'

Once findings are in Security Hub they inherit aggregation across Regions and accounts, severity-based routing, and ticketing integrations you already run. Complement this with AWS Config for the controls Config expresses natively and continuously — restricted-ssh, vpc-sg-open-only-to-authorized-ports, subnet-auto-assign-public-ip-disabled. The division of labour is clean: Config evaluates individual resource compliance the instant a resource changes; Network Access Analyzer evaluates whole-path reachability that no single-resource rule can see. Both feed Security Hub, which becomes the single governance ledger.

Enterprise scenario

A payments platform team ran a hub-and-spoke TGW across roughly fifty accounts with a centralized egress-inspection VPC — every spoke’s 0.0.0.0/0 was supposed to point at the TGW so all internet-bound traffic hairpinned through AWS Network Firewall. Their PCI auditor asked them to prove, not assert, that no cardholder-data subnet could reach the internet by any route. Flow logs only showed the absence of observed egress, which an auditor correctly rejected as proof of a negative — an idle but open path looks identical to no path.

The constraint: 50 accounts, monthly attestation, and a standing fear that a single console SG edit or an accidental NAT route in one spoke would silently open a hole nobody noticed until the next quarter.

They authored one Network Access Analyzer scope per CDE subnet group — internet-gateway and NAT-gateway destinations in MatchPaths, the sanctioned PrivateLink endpoints for logging carved out in ExcludePaths — and ran every scope across all member accounts from the delegated administrator on a daily EventBridge schedule. A Step Functions state machine started each analysis, polled to succeeded, and pushed any FindingsFound != false into Security Hub as a MEDIUM ASFF finding tagged with the scope ID.

# Daily, per CDE scope, from the delegated admin — page only on a real path
ANALYSIS=$(aws ec2 start-network-insights-access-scope-analysis \
  --network-insights-access-scope-id "$CDE_SCOPE_ID" \
  --query 'NetworkInsightsAccessScopeAnalysis.NetworkInsightsAccessScopeAnalysisId' \
  --output text)
aws ec2 wait network-insights-access-scope-analysis-succeeded \
  --network-insights-access-scope-analysis-ids "$ANALYSIS"
FOUND=$(aws ec2 describe-network-insights-access-scope-analyses \
  --network-insights-access-scope-analysis-ids "$ANALYSIS" \
  --query 'NetworkInsightsAccessScopeAnalyses[0].FindingsFound' --output text)
[ "$FOUND" = "false" ] || echo "CDE egress path found — escalate"

Two weeks in, the daily run flagged a finding in a non-production spoke: a developer had attached a NAT gateway and added a 0.0.0.0/0 route to “test something,” accidentally giving a subnet that shared a route table with a CDE subnet a path to the internet. Reachability Analyzer pinpointed the exact route-table entry from the ForwardPathComponents in minutes; they removed it the same morning. The monthly attestation went from a manual, unconvincing flow-log spreadsheet to a screenshot of a clean Security Hub view backed by static proof — and the control caught a real regression before the auditor ever saw it.

Verify

Before you call any of this production-ready, confirm each layer actually does what you think:

Checklist

awsvpcnetworkingtroubleshootingreachability-analyzersecurity

Comments

Keep Reading