Securing APIs in AWS: Private API Gateway + VPC Endpoint Deep Dive
2024-08-21
๐ Overview
Private API Gateway allows you to expose REST APIs that are only accessible inside your VPC. No public internet access. This is ideal for internal microservices, backend systems, or APIs you want fully private.
Scenario
We'll implement a secure internal Employee Directory API using AWS Lambda, API Gateway (private), and VPC Interface Endpoints. The API will be accessible only inside the VPC.
๐งฑ Prerequisites
- AWS CLI or Terraform
- A VPC with private subnets
- curl or Postman
- Basic understanding of:
- API Gateway
- Lambda
- IAM
- VPC endpoints (interface)
๐ ๏ธ Architecture
[ EC2 in VPC ] ---> [ VPC Endpoint ] ---> [ Private API Gateway ] ---> [ Lambda (Employee Directory) ]
Only resources inside the VPC can reach the API through the interface endpoint.
๐ง Step-by-Step Deployment (Terraform)
1. Create VPC, Subnet, and Security Group
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
}
resource "aws_subnet" "private" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "us-east-1a"
map_public_ip_on_launch = false
}
resource "aws_security_group" "vpce_sg" {
name = "vpce-sg"
vpc_id = aws_vpc.main.id
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [aws_vpc.main.cidr_block]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
2. Deploy the Lambda Function
Use the Python code below and package it:
mkdir employee-directory && cd employee-directory
vi index.py # lets use our mini lambda below
zip employee_directory.zip index.py
import json
EMPLOYEES = {
"1001": {"id": "1001", "name": "Qais N", "role": "DevOps Engineer"},
"1002": {"id": "1002", "name": "Abz Ab", "role": "Backend Engineer"},
"1003": {"id": "1003", "name": "James John", "role": "Product Manager"},
}
def lambda_handler(event, context):
path = event.get("path", "")
method = event.get("httpMethod", "")
if method == "GET" and path.startswith("/employee/"):
emp_id = path.split("/")[-1]
emp = EMPLOYEES.get(emp_id)
if emp:
return {"statusCode": 200, "body": json.dumps(emp)}
return {"statusCode": 404, "body": json.dumps({"error": "Employee not found"})}
if method == "GET" and path == "/employee":
return {"statusCode": 200, "body": json.dumps(list(EMPLOYEES.values()))}
return {"statusCode": 400, "body": json.dumps({"error": "Unsupported operation"})}
resource "aws_iam_role" "lambda_exec" {
name = "lambda_exec_role"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [{
Action = "sts:AssumeRole",
Principal = { Service = "lambda.amazonaws.com" },
Effect = "Allow",
}],
})
}
resource "aws_lambda_function" "employee_directory" {
filename = "employee_directory.zip"
function_name = "employee_directory"
role = aws_iam_role.lambda_exec.arn
handler = "index.lambda_handler"
runtime = "python3.9"
source_code_hash = filebase64sha256("employee_directory.zip")
}
3. Create Private API Gateway and Integration
resource "aws_api_gateway_rest_api" "private_api" {
name = "employee-directory-api"
endpoint_configuration { types = ["PRIVATE"] }
}
resource "aws_api_gateway_resource" "employee" {
rest_api_id = aws_api_gateway_rest_api.private_api.id
parent_id = aws_api_gateway_rest_api.private_api.root_resource_id
path_part = "employee"
}
resource "aws_api_gateway_method" "get" {
rest_api_id = aws_api_gateway_rest_api.private_api.id
resource_id = aws_api_gateway_resource.employee.id
http_method = "GET"
authorization = "NONE"
}
resource "aws_api_gateway_integration" "lambda_integration" {
rest_api_id = aws_api_gateway_rest_api.private_api.id
resource_id = aws_api_gateway_resource.employee.id
http_method = aws_api_gateway_method.get.http_method
type = "AWS_PROXY"
integration_http_method = "POST"
uri = aws_lambda_function.employee_directory.invoke_arn
}
4. Configure VPC Endpoint and Policy
resource "aws_vpc_endpoint" "api_gw" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.${var.region}.execute-api"
vpc_endpoint_type = "Interface"
subnet_ids = [aws_subnet.private.id]
security_group_ids = [aws_security_group.vpce_sg.id]
}
resource "aws_api_gateway_deployment" "deployment" {
rest_api_id = aws_api_gateway_rest_api.private_api.id
stage_name = "prod"
depends_on = [aws_api_gateway_integration.lambda_integration]
}
resource "aws_api_gateway_rest_api_policy" "restrict_to_vpce" {
rest_api_id = aws_api_gateway_rest_api.private_api.id
policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Deny",
Principal = "*",
Action = "execute-api:Invoke",
Resource = "*",
Condition = {
StringNotEquals = {
"aws:SourceVpce" = aws_vpc_endpoint.api_gw.id
}
}
}
]
})
}
๐งช Test the Setup
- SSH into an EC2 inside the VPC
- Run:
curl https://<rest-api-id>.execute-api.<region>.amazonaws.com/prod/employee
curl https://<rest-api-id>.execute-api.<region>.amazonaws.com/prod/employee/1002
โ Should return valid responses inside the VPC (inside EC2) โ Should be blocked from public internet
๐ Summary
- Built a fully private Employee Directory API
- Integrated it with Lambda + API Gateway
- Made it VPC-only using Interface Endpoints
- Secured access with SourceVpce IAM policy
Great for internal-only microservices, B2B APIs, or compliance-sensitive systems.
๐งผ Destroy with:
terraform destroy