This check flags API Gateway REST APIs that use the default EDGE or REGIONAL public endpoint instead of a PRIVATE one. If your API only serves internal clients, switch it to a private endpoint backed by an interface VPC endpoint and a restrictive resource policy so traffic never touches the public internet.
Most API Gateway REST APIs start life with a public endpoint, because that is the default and because the first thing you usually want to do is hit the URL from your laptop and confirm it works. The trouble is that the default tends to stick. Internal services, admin APIs, and backend-only integrations end up reachable from anywhere on the internet long after they should have been locked down.
The apigw_public check looks at the endpoint configuration type of each REST API in your account and reports any that are not PRIVATE. It is a nudge to ask a simple question for every API you run: does this actually need to be on the public internet?
What this check detects
API Gateway REST APIs have an endpoint configuration that controls where the API can be reached from. There are three types:
- EDGE — the request is routed through CloudFront edge locations. Public, globally reachable. This is the default for REST APIs.
- REGIONAL — a public endpoint in a single region, without the CloudFront layer. Still reachable from the internet.
- PRIVATE — only reachable from inside your VPC through an interface VPC endpoint. Not exposed to the public internet.
The check passes when an API uses PRIVATE and flags it otherwise. It does not mean every flagged API is misconfigured. Plenty of APIs are meant to be public. What the check gives you is a complete inventory of which APIs are publicly reachable, so you can confirm each one on purpose rather than by accident.
Note: This check applies to REST APIs. HTTP APIs and WebSocket APIs have different endpoint models, and HTTP APIs do not currently support private endpoints in the same way. If you need a private HTTP-style API, a REST API with a private endpoint is the supported path.
Why it matters
A public endpoint is not a vulnerability by itself. The risk is the gap between what you intended and what you shipped. A public endpoint expands your attack surface in a few concrete ways.
It is reachable by anyone who finds the URL
API Gateway URLs are not secret. They follow a predictable pattern and they show up in client-side JavaScript, mobile app binaries, browser dev tools, and CI logs. Once an API is public, the only thing standing between an attacker and your backend is your authorization layer. If a developer ships an endpoint with authorization set to NONE, or a Lambda authorizer with a logic bug, that backend is now open to the world.
It invites credential stuffing and scraping
Public login, token, or search endpoints attract automated abuse. Even with auth in place, a public endpoint can be hammered with credential stuffing, enumeration, and scraping attempts. You end up paying for the request volume and burning capacity defending against traffic that should never have been able to reach you.
It defeats network segmentation
If your architecture assumes services talk to each other inside a VPC, a public API endpoint quietly breaks that assumption. An internal billing API that happens to be public is a lateral-movement target. An attacker who lands in one part of your environment, or who simply guesses the URL, can call it directly without ever crossing a network boundary you control.
Warning: Authentication is not a substitute for network controls. Relying solely on IAM auth or an authorizer means a single misconfigured method or a leaked key exposes the API. Defense in depth means making the endpoint unreachable in the first place when it has no business being public.
How to fix it
Fixing this means changing the endpoint type to PRIVATE and putting the supporting plumbing in place: an interface VPC endpoint and a resource policy that restricts access to it. Skipping the resource policy is a common mistake, because a private API with no resource policy denies all requests by default.
Step 1: Create an interface VPC endpoint for API Gateway
Clients inside your VPC reach the private API through this endpoint. Create one in the subnets where your callers live.
aws ec2 create-vpc-endpoint \
--vpc-id vpc-0abc1234 \
--vpc-endpoint-type Interface \
--service-name com.amazonaws.us-east-1.execute-api \
--subnet-ids subnet-0aaa1111 subnet-0bbb2222 \
--security-group-ids sg-0ccc3333 \
--private-dns-enabled
Note the endpoint ID it returns (for example vpce-0def4567). You will reference it in the resource policy.
Note: The security group on the endpoint controls which clients can reach it. Allow inbound HTTPS (port 443) only from the security groups or CIDRs of your legitimate callers, not 0.0.0.0/0.
Step 2: Change the API endpoint type to PRIVATE
Update the existing API's endpoint configuration. This swaps it off the public endpoint.
aws apigateway update-rest-api \
--rest-api-id a1b2c3d4e5 \
--patch-operations \
op=replace,path=/endpointConfiguration/types/EDGE,value=PRIVATE
Step 3: Attach a resource policy
A private API rejects all traffic until a resource policy explicitly allows it. Scope access to your VPC endpoint.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "execute-api:Invoke",
"Resource": "arn:aws:execute-api:us-east-1:111122223333:a1b2c3d4e5/*",
"Condition": {
"StringEquals": {
"aws:SourceVpce": "vpce-0def4567"
}
}
}
]
}
Apply it:
aws apigateway update-rest-api \
--rest-api-id a1b2c3d4e5 \
--patch-operations \
op=replace,path=/policy,value="$(cat policy.json | jq -c . | sed 's/"/\\"/g')"
Step 4: Redeploy the API
Endpoint and policy changes do not take effect until you create a new deployment.
aws apigateway create-deployment \
--rest-api-id a1b2c3d4e5 \
--stage-name prod
Danger: Switching a live API from public to private will cut off every client that reaches it over the internet. Before you run these commands against production, confirm all callers are inside the VPC or routed through the new interface endpoint. Test in a staging API first and have a rollback deployment ready.
The Terraform version
If you manage API Gateway as code, define the private endpoint and policy together so they ship as one reviewable change.
resource "aws_api_gateway_rest_api" "internal" {
name = "internal-billing-api"
endpoint_configuration {
types = ["PRIVATE"]
vpc_endpoint_ids = [aws_vpc_endpoint.execute_api.id]
}
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = "*"
Action = "execute-api:Invoke"
Resource = "arn:aws:execute-api:us-east-1:111122223333:*/*"
Condition = {
StringEquals = {
"aws:SourceVpce" = aws_vpc_endpoint.execute_api.id
}
}
}]
})
}
resource "aws_vpc_endpoint" "execute_api" {
vpc_id = var.vpc_id
service_name = "com.amazonaws.us-east-1.execute-api"
vpc_endpoint_type = "Interface"
subnet_ids = var.private_subnet_ids
security_group_ids = [aws_security_group.api_endpoint.id]
private_dns_enabled = true
}
Tip: Keep the resource policy in the same module as the API definition. When the policy and endpoint type live together in code, a reviewer can see the full intent of an API at a glance, and you cannot accidentally ship a private endpoint with no policy.
How to prevent it from happening again
One-time fixes drift back over time. The way to keep public endpoints from creeping in is to make "public" an explicit decision rather than the default.
Gate it in CI/CD with policy-as-code
Catch the configuration before it deploys. With Open Policy Agent and conftest against a Terraform plan, you can fail any API that is public unless it carries an explicit exemption tag.
package apigateway
deny[msg] {
rc := input.resource_changes[_]
rc.type == "aws_api_gateway_rest_api"
types := rc.change.after.endpoint_configuration[_].types
types[_] != "PRIVATE"
not rc.change.after.tags["public-approved"]
msg := sprintf("API '%s' uses a public endpoint without a public-approved tag", [rc.address])
}
Wire it into the pipeline so a plan that introduces a public API without the approval tag blocks the merge.
terraform plan -out=tfplan
terraform show -json tfplan > plan.json
conftest test plan.json --policy policies/
Use an SCP to constrain endpoint types where appropriate
For accounts that should only ever host internal APIs, a Service Control Policy can deny creation of public endpoints outright, removing the option from anyone in that account.
Run the check on a schedule
CI gates only catch infrastructure deployed through CI. Console clicks, console-created APIs, and manual changes slip past. Lensix runs apigw_public continuously across your accounts so a public endpoint created out of band shows up in your findings within the hour rather than at the next audit.
Best practices
- Default to private. For any API whose callers are all inside your network, start with
PRIVATE. Make the public option the one that needs justification. - Never ship a private API without a resource policy. It will deny everything, and the failure mode looks like a broken network rather than a missing policy, which wastes hours of debugging.
- Scope the resource policy tightly. Use the
aws:SourceVpcecondition to bind access to specific VPC endpoints, not just any private network path. - Lock down the endpoint security group. The VPC endpoint is your real network boundary now. Restrict inbound 443 to the security groups of known callers.
- Layer authorization on top. Private networking is the floor, not the ceiling. Keep IAM auth, authorizers, and per-method access controls in place so a compromised internal host still cannot call everything.
- Document the public ones. When an API genuinely must be public, record why. A tag like
public-approved=trueturns an unexplained finding into a deliberate, auditable choice.
The goal is not to make every API private. It is to make sure no API is public by accident. Run the check, confirm each public endpoint on purpose, and put the gates in place so the next one cannot slip through unnoticed.

