Securing Private APIs in API Gateway Using VPC Endpoints

Yani
InfoSec Write-ups
Published in
9 min readJun 27, 2023

--

Photo by roman raizen on Unsplash

A VPC (Virtual Private Cloud) endpoint is a private connection between your VPC and another AWS (Amazon Web Services) service, such as S3 or DynamoDB. With VPC endpoints, we can access other AWS services over a private connection instead of using the internet, which can increase security by reducing exposure to threats from the public internet. This blog aims to provide a comprehensive understanding of how to protect your private APIs by setting up an VPC endpoint in API Gateway.

The blog will cover topics such as setting up connectivity between API Gateway and lambda function as backend service, configuring VPC endpoints, and implementing appropriate security measures and networking configurations.

In this blog we will manage AWS infrastructure with AWS CLI, instead of using the console, since it provides a better understanding of how AWS services work together.

Scenario

Let’s say you have an internal Lambda service that you want to expose as a private REST API via API Gateway. The private REST API can only be accessed from EC2 in your virtual private cloud (VPC) in Amazon VPC. To do this, you use an interface VPC endpoint.

The process is illustrated in the following diagram.

At a high level, the steps for the solution is summarized as below:

1.Lambda Setup

1–1. Create an IAM Role for Lambda execution

1–2. Build and Deploy the Lambda

2.Amazon API Gateway Setup

2–1. Create Private REST API

2–2. Add HTTP GET Method to Resource

2–3. Integrate HTTP GET Method to the Lambda

2–4. Add Permission to Lambda to Allow Invocation of Lambda from API Gateway

2–5. Attach Resource Policy to the Private REST API to Manage Access

2–6. Deploy Private REST API

3.VPC Endpoint Setup

3–1. Create an Interface VPC Endpoint for API Gateway

3–2. Associate VPC Endpoint with the Private REST API

3–3. Configure Security Group for VPC Endpoint to Allow Inbound Traffic from EC2

Implementation

In this section, we will put the steps outlined in the previous section into practice using the AWS CLI. We will begin by defining several environment variables to set the stage for the upcoming implementation steps.

$ export AWS_PROFILE=default
$ export AWS_REGION=us-east-1
$ export AWS_ACCOUNT=<fill in your aws account>
$ export FUNCTION_ARN="arn:aws:lambda:$AWS_REGION:$AWS_ACCOUNT:function:currentTimeLambda"
$ export VPC_ENDPOINT_VPC_ID=vpc-0b52ca08e7db8531f
$ export SERVICE_NAME=com.amazonaws.us-east-1.execute-api
$ export VPC_ENDPOINT_SECURITY_GROUP_ID=sg-09fff7a6564a46f2e
$ export VPC_ENDPOINT_SUBNET_ID=subnet-092ebc70d4b9d39ab
$ export EC2_IP=172.31.86.171

1. Lambda Setup

1–1. Create an IAM Role for Lambda execution

Here, we create the lambda-cli-role and attach the policy document above.

For executing the lambda, it should be associated with a basic IAM Role, AWSLambdaBasicExecutionRole. IAM Roles need policy document. Next sections will create a policy document and an IAM role.

Create policy document file

$ echo '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}' > lambdaAssumeRolePolicyDocument.json

Create IAM Role Attaching the Policy Document

In this step, we create a new IAM role called lambda-cli-role and attach the previously mentioned policy document. This role is specifically designed to provide the necessary permissions for our Lambda functions to interact with other AWS services

$ aws iam create-role --role-name lambda-cli-role \
--assume-role-policy-document file://./lambdaAssumeRolePolicyDocument.json --profile $AWS_PROFILE

1–2. Build and Deploy the Current Time Lambda

We will now proceed to create a simple Lambda function that will return the current time.

$ mkdir current-time-lambda
$ cd current-time-lambda
$ echo "exports.handler = async (event) => {
const payload = {
date: new Date()
};
return JSON.stringify(payload);
};" > currentTimeLambda.js

Let’s move on to deploying the Lambda function.

In this step, we will deploy the Lambda function to make it available for invocation. By deploying the Lambda function, we enable it to be executed in response to API Gateway requests.

$ export LAMBDA_ROLE_ARN=arn:aws:iam::$AWS_ACCOUNT:role/lambda-cli-role
$ zip -r /tmp/currentTimeLambda.js.zip currentTimeLambda.js
$ aws lambda create-function \
--function-name currentTimeLambda \
--handler 'currentTimeLambda.handler' \
--runtime nodejs14.x \
--role "$LAMBDA_ROLE_ARN" \
--zip-file 'fileb://./currentTimeLambda.js.zip' \
--region "$AWS_REGION" \
--profile "$AWS_PROFILE"

Wait for the deployment process to complete. Once deployed, the Lambda function will be ready to be invoked.

Now that we have successfully established a Lambda function with the ARN “arn:aws:lambda:$AWS_REGION:$AWS_ACCOUNT:function:currentTimeLambda”

2. Amazon API Gateway Setup

API Gateway is a fully-managed service, commonly used in microservices architecture, that acts as an intermediary for client applications to connect to backend services through a single entry point.

2–1. Create Private REST API

Ther are two API types: HTTP API, WebSocket API and REST API, we choose the last one in our case. To create a private REST API, we use aws apigateway create-rest-api command in the below, with endpoint configurate type as "PRIVATE".

$ REST_API_ID=$(aws apigateway create-rest-api \
--name "TimeGateway" \
--description "API gateway for Time related functions" \
--region "$AWS_REGION" \
--endpoint-configuration '{ "types": ["PRIVATE"] }' \
--profile "$AWS_PROFILE" --query 'id' --output text)

2–2. Add HTTP GET Method to API Gateway Resource

Amazon API Gateway resource is a collection of related RESTful API methods and sub-resources that work together to perform a single action or retrieve a specific entity. Each API Gateway Resource refers to an element of a REST API that is identified by a resource path and a set of HTTP methods.

To add a GET method to the root resource in API Gateway, you’ll need to follow a two-step process. The first step involves obtaining the Path ID for the root path (/), and then you can proceed to add the GET method. Here's how you can accomplish each step

  • Retrieve the Path ID for the root path (/):
$ PATH_ID=$(aws apigateway get-resources \
--rest-api-id $REST_API_ID \
--profile "$AWS_PROFILE" --query "items[?path=='/'].id" --output text)

The command will return the Path ID corresponding to the root path (/) in your REST API.

  • Add the GET method to the REST API:

Once we have the Path ID for the root path, you can proceed to add the GET method. This step involves associating the GET method with the desired resource (identified by the PATH_ID)

$ aws apigateway put-method \
--rest-api-id $REST_API_ID \
--resource-id $PATH_ID \
--http-method GET \
--authorization-type NONE \
--region "$AWS_REGION" \
--profile "$AWS_PROFILE"

We will configure the response type for the GET HTTP method we created in previous step. By configuring the response type for the GET HTTP method, we can control how the API Gateway responds to requests and specify the format and content of the response.

$ aws apigateway put-method-response \
--rest-api-id "$REST_API_ID" \
--resource-id "$PATH_ID" \
--http-method GET \
--status-code 200 \
--response-models application/json=Empty \
--region "$AWS_REGION" \
--profile "$AWS_PROFILE"

2–3. Integrate HTTP GET Method to Current Time Lambda

We have created the REST API and the Current Time Lambda. Now, we will integrate them.

HTTP GET Method Integration with Lambda

We will need the Function ARN from Lambda REST API Id, PATH ID from Gateway to configure the put integration.

$ aws apigateway put-integration \
--rest-api-id $REST_API_ID \
--resource-id $PATH_ID \
--http-method GET \
--type AWS \
--integration-http-method POST \
--uri arn:aws:apigateway:"$AWS_REGION":lambda:path/2015-03-31/functions/"$FUNCTION_ARN"/invocations \
--region "$AWS_REGION" \
--profile "$AWS_PROFILE"

Note: when configuring an API Gateway integration with a Lambda function, the --integration-http-method should be set to "POST" regardless of the HTTP method used in the public API. This is because API Gateway communicates with Lambda functions using a POST request.

HTTP GET Method Integration Response

After configuring the integration HTTP method, the next step is to set the response type for the API Gateway integration. This determines how the response from the Lambda function is handled and formatted before it is returned to the client.

$ aws apigateway put-integration-response \
--rest-api-id $REST_API_ID \
--resource-id $PATH_ID \
--http-method GET \
--status-code 200 \
--response-templates application/json="" \
--region "$AWS_REGION" \
--profile "$AWS_PROFILE"

2–4. Add Permission to Lambda to Allow Invocation of Lambda from API Gateway

To invoke a Lambda function from API Gateway, It is time to set up the necessary permissions on Lambda:

$ aws lambda add-permission \
--function-name currentTimeLambda \
--statement-id currentTimeLambda-permission \
--action lambda:InvokeFunction \
--principal apigateway.amazonaws.com \
--source-arn "arn:aws:execute-api:$AWS_REGION:$AWS_ACCOUNT:$REST_API_ID/*/GET/" \
--profile "$AWS_PROFILE"

2–5. Attach Resource Policy to the Private REST API to Manage Access

By attaching the resource policy to the selected resource, we can control and manage access to that specific resource within your private REST API in API Gateway. This resource policy specifically restricts the access to the private API to the VPC endpoint that we created ealier.

$ echo '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Principal": "*",
"Action": "execute-api:Invoke",
"Resource": "execute-api:/prod/GET/*",
"Condition": {
"StringNotEquals": {
"aws:sourceVpc": "'${VPC_ENDPOINT_VPC_ID}'"
}
}
},
{
"Effect": "Allow",
"Principal": "*",
"Action": "execute-api:Invoke",
"Resource": "execute-api:/prod/GET/*"
}
]
}' > rest-api-policy.json

Replace the API Gateway resource policy with the one in rest-api-policy.json using the command in the below:

$ aws apigateway update-rest-api \
--rest-api-id $REST_API_ID \
--patch-operations "op=replace,path=/policy,value='$(cat ./rest-api-policy.json)'"

After making changes to the API configurations in API Gateway, it’s important to (re)deploy the API to ensure that the changes take effect. The (re)deploying the API makes the updated configurations live and available for use.

2–6. Deploy Private REST API

Create a stage with name ‘prod’ and (re)deploy the API, running the following command will initiate the redeployment process for the specified API and deployment stage.

$ aws apigateway create-deployment --rest-api-id $REST_API_ID --stage-name prod

3. VPC Endpoint Setup

A VPC endpoint is a virtual device that enables you to connect to AWS services via a private connection, rather than over the internet.

3–1. Create an Interface VPC Endpoint for API Gateway

$ VPCE_ID=$(aws ec2 create-vpc-endpoint --vpc-id $VPC_ENDPOINT_VPC_ID \
--service-name $SERVICE_NAME \
--security-group-ids $VPC_ENDPOINT_SECURITY_GROUP_ID \
--vpc-endpoint-type Interface \
--query 'VpcEndpoint.VpcEndpointId' \
--subnet-id $VPC_ENDPOINT_SUBNET_ID \
--output text \
--profile default)

For Subnets, choose the subnets (Availability Zones) in which to create the endpoint network interfaces. To improve the availability of your API, you can choose multiple subnets.

3–2. Associate VPC Endpoint with the Private REST API

To associate VPC endpoints to an already created private API, use the following CLI command:

$ aws apigateway update-rest-api \
--rest-api-id $REST_API_ID \
--patch-operations "op='add',path='/endpointConfiguration/vpcEndpointIds',value='${VPCE_ID}'" \
--region $AWS_REGION

3–3. Configure Security Group for VPC Endpoint to Allow Inbound Traffic from EC2

To ensure proper security for the VPC endpoint, it is necessary to attach a security group with appropriate inbound rules. When creating the security group for the VPC endpoint, it is recommended to restrict port access only to the source IP range associated with the relevant VPC. If the VPC endpoint is intended to be accessed solely from within the same VPC, the source IP range for the security group’s inbound rule can be set to match the CIDR range of the VPC. However, if the VPC endpoint needs to be accessed from multiple VPCs connected via VPC peering or an AWS transit gateway, it is advisable to include the CIDR ranges of all the VPCs in the source IP range of the security group’s inbound rule.

In our case, as we need to access the Private API via VPC endpoint from EC2 in the VPC where the VPC endpoint reside, we use the EC2 private as the source IP for the security inbound rule.

$ aws ec2 authorize-security-group-ingress --group-id $VPC_ENDPOINT_SECURITY_GROUP_ID  --protocol tcp --port 443 --cidr $EC2_IP/32

Verification

After implementation, with the help of EC2 Instance Connect Endpoint, we log into EC2 with IP of $EC2_IP in the same VPC with endpoint but in the different subnet to access the private API.

The “Invoke URL” assigned to the private API is located in the prod stage in API gateway.

On the EC2, we execute the curl command to access the “Invoke URL” and receive the current time results echoed back.

If you access the same URL from other URL, you will see the “Could not resolve host: rybvcqnp68.execute-api.us-east-1.amazonaws.com” error as it is internal only.

Closing Words

In conclusion, using VPC endpoints to secure private APIs in API Gateway is a secure and efficient way to protect your APIs from unauthorized access. By routing traffic to your APIs over private IP addresses, you can add an additional layer of protection to your APIs and reduce the risk of data breaches.

Through this blog, hope you can gain insights into the best practices for securing your private APIs and protecting sensitive data, enabling you to build robust and secure API solutions in AWS.

--

--

Focusing on Security for Web Application, AWS and Kubernetes, etc. | CKA&CKS, AWS Security & ML Specialty | https://www.linkedin.com/in/yani-dong-041a1b120/