How to use API Gateway with private endpoint in AWS

Issue #1058

When you set endpointConfiguration.types to PRIVATE on a REST API, API Gateway still hands you a default URL, something like https://q6t71pmdq7.execute-api.eu-north-1.amazonaws.com/prod. That URL just will not resolve anywhere outside your VPC.

AWS’s private APIs guide puts it plainly

A private API is a REST API that is only callable from within an Amazon VPC. You can access your API using an interface VPC endpoint

So the URL exists, but reaching it is a separate problem, and that’s what this article walks through: setting up the VPC endpoint, calling it with a header, associating it with your API so you get a cleaner URL, turning on private DNS as an alternative, and locking it down with a resource policy.

From Introducing Amazon API Gateway Private Endpoints

API Gateway as a fully managed service runs its infrastructure in its own VPCs. When you interface with API Gateway publicly accessible endpoints, it is done through public networks. When they’re configured as private, the public networks are not made available to route your API. Instead, your API can only be accessed using the interface endpoints that you have configured.

Image

Create the VPC endpoint

The interface VPC endpoint is what actually gets you into the private API. It’s an ENI, one per subnet you attach it to, powered by AWS PrivateLink, sitting in the VPC where your callers already live. Create one for the execute-api service and it comes with its own hostname, generated the moment it exists, no association with any API required:

https://vpce-0123456789abcdef0-abcdefgh.execute-api.eu-north-1.vpce.amazonaws.com/prod
Image

Anything that can route to that endpoint’s ENIs, inside the same VPC, or peered, or reached over Direct Connect, can now attempt a connection.

Calling through the VPC endpoint needs a header

A single VPC endpoint can sit in front of several private APIs at once, which is actually one of AWS’s own best practices, since it saves you from provisioning one endpoint per API. The catch is that the raw endpoint hostname alone doesn’t tell API Gateway which API you’re trying to reach, so you need to pass either the Host header set to the API’s default hostname, or the x-apigw-api-id header set to the API id:

curl -H "x-apigw-api-id: q6t71pmdq7" \
  https://vpce-0123456789abcdef0-abcdefgh.execute-api.eu-north-1.vpce.amazonaws.com/prod/books

Using Host instead looks like this:

curl -H "Host: q6t71pmdq7.execute-api.eu-north-1.amazonaws.com" \
  https://vpce-0123456789abcdef0-abcdefgh.execute-api.eu-north-1.vpce.amazonaws.com/prod/books

Both work. Neither is something you want to hand to every client calling your API, which is why AWS recommends the next step.

Associate the VPC endpoint with the API

From the API Gateway console, open the API’s settings, add the VPC endpoint under private endpoints, and redeploy. AWS’s docs list this as a best practice specifically because it “creates a Route 53 alias DNS record and simplifies invoking your private API.” That record is scoped to just this API and this endpoint, so it doesn’t need a Host or x-apigw-api-id header at all:

Image
https://q6t71pmdq7-vpce-0123456789abcdef0.execute-api.eu-north-1.amazonaws.com/prod/books

You won’t find this record in your own Route 53 hosted zones. AWS owns the execute-api.{region}.amazonaws.com zone and creates the alias there, not in your account. It’s technically publicly resolvable, if you dig it from anywhere you’ll get an answer, but the answer is the endpoint’s private IP addresses, so only a client that can actually route into that VPC can complete the connection.

Private DNS: the alternative to associating

Image

Instead of (or alongside) associating the endpoint with a specific API, you can turn on “Private DNS” for the VPC endpoint itself. This makes the default hostname for every private API behind that endpoint resolve inside the VPC, straight to the endpoint’s ENIs, no header needed:

curl https://q6t71pmdq7.execute-api.eu-north-1.amazonaws.com/prod/books

The trade-off is that private DNS rewrites name resolution for the entire execute-api.{region}.amazonaws.com namespace inside that VPC, not just your private API. Any call from that VPC to a public API’s default endpoint stops resolving, because those public APIs aren’t served by your endpoint. AWS’s own workaround, if you need both, is to turn private DNS off again and instead create a private hosted zone per private API. In practice, if you’ve already associated your endpoint with the API as above, you get the same header-free URL without touching DNS resolution for anything else in the VPC, which is why that’s usually the better default.

Resource policy: who’s allowed in

The VPC endpoint and DNS mechanics only decide whether traffic can physically arrive. The resource policy attached to the API decides whether that traffic is authorized, and it’s checked on every request no matter which hostname got you there. A wide-open policy looks like this:

Image
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": "*",
    "Action": "execute-api:Invoke",
    "Resource": "arn:aws:execute-api:eu-north-1:632071319028:q6t71pmdq7/*"
  }]
}

Principal: "*" with no condition means anything that can reach the endpoint is authorized, full stop. AWS’s best practices call for tightening this with aws:SourceVpce (or aws:SourceVpc if you want to allow an entire VPC rather than one specific endpoint):

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": "*",
    "Action": "execute-api:Invoke",
    "Resource": "arn:aws:execute-api:eu-north-1:632071319028:q6t71pmdq7/*",
    "Condition": {
      "StringEquals": { "aws:sourceVpce": "vpce-0123456789abcdef0" }
    }
  }]
}

Now only traffic through that specific endpoint is allowed, even if someone else in the account spins up a second endpoint pointed at the same API. For the strictest setup, pair this with a VPC endpoint policy on the endpoint itself, which controls which APIs the endpoint is allowed to reach in the first place.

Read more

Written by

I’m open source contributor, writer, speaker and product maker.

Start the conversation