During a recent security review, I came across a familiar pattern.
A team had built a modern backend using serverless services β Lambda, API Gateway, and DynamoDB. It was scalable, fast, and cost-efficient.
But there was one issue:
Their "serverless" architecture was still exposed to the public internet.
Even though authentication existed, the attack surface was unnecessarily large.
That's when the shift happened β from serverless-first to:
π Private-by-design serverless architecture
This blog walks you through building exactly that.
π§ What is a Fully Private Serverless Architecture?
A fully private serverless architecture ensures:
- β No public endpoints
- β No Internet Gateway
- β No public IPs
- β All traffic flows inside a VPC
- β Access only via private connectivity (VPN / Direct Connect)
- β Strict IAM + endpoint policies
ποΈ Real Architecture Diagram
Here's a real-world visual representation of what we're building:
ββββββββββββββββββββββββββββββ
β Corporate Network / VPN β
ββββββββββββββ¬ββββββββββββββββ
β
βΌ
ββββββββββββββββββββββ
β VPC (10.0.0.0/16)β
βββββββββββ¬βββββββββββ
β
βββββββββββββββββΌβββββββββββββββββ
βΌ βΌ βΌ
ββββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β Private Subnet β β Private Subnetβ β Private Subnetβ
ββββββββ¬ββββββββββ ββββββββ¬ββββββββ ββββββββ¬ββββββββ
β β β
βΌ βΌ βΌ
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β API Gateway β β Lambda β β CloudWatch β
β (PRIVATE) ββββΆβ (in VPC) β β Logs (VPCE) β
ββββββββββββββββ ββββββββ¬ββββββββ ββββββββββββββββ
β
βββββββββββββββββ¬βββββββββββΌβββββββββββ¬ββββββββββββββββ
βΌ βΌ βΌ βΌ
ββββββββββββ ββββββββββββ ββββββββββββββββ ββββββββββββββββ
β DynamoDB β β S3 β β Secrets Mgr β β Other AWS β
β (Gateway β β (Gateway β β (Interface) β β Services β
β Endpoint)β β Endpoint)β β β β β
ββββββββββββ ββββββββββββ ββββββββββββββββ ββββββββββββββββ
βββββββ All Traffic via VPC Endpoints βββββββπ Core Building Blocks

π οΈ Step-by-Step Implementation
π§± Step 1: Create VPC (No Internet Gateway)
π Step 2: Create VPC Endpoints
- Gateway β S3, DynamoDB
- Interface β Lambda, API Gateway, Secrets Manager, Logs
βοΈ Step 3: Deploy Lambda in Private Subnets
π Step 4: Create Private API Gateway
ποΈ Step 5: Secure Backend Services
- Block S3 public access
- Use endpoint-based policies
π Step 6: Secrets & Logging
- Secrets Manager via VPCE
- CloudWatch via VPCE
π§ͺ Terraform Implementation (Production-Ready)
Below is a modular and clean Terraform setup.
π providers.tf
provider "aws" {
region = "ap-south-1"
}π vpc.tf
resource "aws_vpc" "private_vpc" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "private_subnet_1" {
vpc_id = aws_vpc.private_vpc.id
cidr_block = "10.0.1.0/24"
availability_zone = "ap-south-1a"
}
resource "aws_subnet" "private_subnet_2" {
vpc_id = aws_vpc.private_vpc.id
cidr_block = "10.0.2.0/24"
availability_zone = "ap-south-1b"
}π endpoints.tf
resource "aws_vpc_endpoint" "s3" {
vpc_id = aws_vpc.private_vpc.id
service_name = "com.amazonaws.ap-south-1.s3"
vpc_endpoint_type = "Gateway"
}
resource "aws_vpc_endpoint" "dynamodb" {
vpc_id = aws_vpc.private_vpc.id
service_name = "com.amazonaws.ap-south-1.dynamodb"
vpc_endpoint_type = "Gateway"
}π security_groups.tf
resource "aws_security_group" "lambda_sg" {
vpc_id = aws_vpc.private_vpc.id
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["10.0.0.0/16"]
}
}π lambda.tf
resource "aws_iam_role" "lambda_role" {
name = "lambda_private_role"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [{
Action = "sts:AssumeRole",
Effect = "Allow",
Principal = {
Service = "lambda.amazonaws.com"
}
}]
})
}
resource "aws_lambda_function" "private_lambda" {
function_name = "private_lambda"
role = aws_iam_role.lambda_role.arn
handler = "index.handler"
runtime = "nodejs18.x"
filename = "lambda.zip"
vpc_config {
subnet_ids = [
aws_subnet.private_subnet_1.id,
aws_subnet.private_subnet_2.id
]
security_group_ids = [aws_security_group.lambda_sg.id]
}
}π api_gateway.tf
resource "aws_api_gateway_rest_api" "private_api" {
name = "private-api"
endpoint_configuration {
types = ["PRIVATE"]
}
}π dynamodb.tf
resource "aws_dynamodb_table" "app_table" {
name = "private-table"
billing_mode = "PAY_PER_REQUEST"
hash_key = "id"
attribute {
name = "id"
type = "S"
}
}π s3.tf
resource "aws_s3_bucket" "private_bucket" {
bucket = "my-private-serverless-bucket"
force_destroy = true
}π Testing the Setup
Since nothing is public, access via:
- VPN
- Bastion host (temporary)
- AWS Client VPN
β οΈ Common Mistakes
- β Forgetting endpoints β services fail
- β Using NAT unnecessarily
- β Weak API policies
- β Leaving S3 public
π‘ Advanced Enhancements
- Add WAF (private integration)
- Zero Trust (IAM + mTLS)
- Audit using CloudTrail Lake
- Private ALB integration
π Benefits

π― Real Use Cases
- Fintech APIs
- Healthcare systems
- Internal enterprise tools
- Secure AI workloads
π§Ύ Final Thoughts
Most teams adopt serverless for speed.
But mature teams design for security first.
π A truly modern architecture is not just serverless β it's private, controlled, and intentional.
βοΈ Closing Line
"In AWS, the safest endpoint⦠is the one that doesn't exist publicly."