Securing API Gateway with Lambda@Edge
A detailed guide on implementing secure API Gateway endpoints using Lambda@Edge for request verification and API key management

Introduction
When building APIs that need to be accessible from a web application, security is important. This post details how we implemented a secure API Gateway setup using Lambda@Edge for request verification at the Cloudfront Distribution layer and a generated API_KEY for authorization at the API Gateway layer. The Cloudfront Distribution Lambda@Edge approach provides edge-level security before requests even reach the API Gateway while the API_KEY provides a secondary layer of security at the API Gateway layer.
Architecture Overview
Our architecture combines several AWS services:
- CloudFront Distribution: Distributes API requests and protects the API Gateway from direct access
- AWS Web Application Firewall
- Lambda@Edge: Verifies referer header and injects API keys
- API Gateway: Hosts the secured API endpoints
- Lambda Authorizer: Validates the API key and referer header
- Lambda Functions: Handles the API requests and searches our Pinecone index
Lambda@Edge Function
First, we'll create the Lambda@Edge function that verifies requests and injects the API_KEY into the request headers. This function is used by our Cloudfront distribution when the request is made.
import { CloudFrontRequestHandler } from "aws-lambda";
export const handler: CloudFrontRequestHandler = async (event) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
// Get referer header
const referer = headers.referer?.[0]?.value;
// Extract domain and path from referer URL
let refererUrl: URL;
try {
refererUrl = new URL(referer || "");
} catch (error) {
return {
status: "403",
statusDescription: "Forbidden",
body: JSON.stringify({
error: "Access denied",
message: "Invalid referer format",
referer: referer || "none",
}),
};
}
// Check if the hostname matches and path starts with /blog
const isValidReferer =
(refererUrl.hostname === process.env.DOMAIN_NAME ||
refererUrl.hostname === `www.${process.env.DOMAIN_NAME}`) &&
refererUrl.pathname.startsWith("/blog");
if (!isValidReferer) {
return {
status: "403",
statusDescription: "Forbidden",
body: JSON.stringify({
error: "Access denied",
message: "Request must come from blog page",
referer: referer || "none",
}),
};
}
// Add the API key to the request
request.headers["x-api-key"] = [
{
key: "x-api-key",
value: process.env.API_KEY || "",
},
];
return request;
};
This function:
- Extracts the referer header from incoming requests
- Validates that the request comes from our domain
- Checks that the request originates from a blog page
- Injects an API key into validated requests
Edge Verification Construct
Next, we'll create a edge verification construct to deploy the Lambda@Edge function:
interface EdgeVerifyConstructProps {
domainName: string;
apiKey: string;
}
export class EdgeVerifyConstruct extends Construct {
public readonly function: NodejsFunction;
public readonly version: Version;
constructor(scope: Construct, id: string, props: EdgeVerifyConstructProps) {
super(scope, id);
// Create role for Lambda@Edge
const role = new Role(this, "EdgeVerifyRole", {
assumedBy: new CompositePrincipal(
new ServicePrincipal("lambda.amazonaws.com"),
new ServicePrincipal("edgelambda.amazonaws.com"),
),
});
role.addManagedPolicy(
ManagedPolicy.fromAwsManagedPolicyName(
"service-role/AWSLambdaBasicExecutionRole",
),
);
// Create Lambda@Edge function
this.function = new NodejsFunction(this, "Function", {
runtime: Runtime.NODEJS_18_X,
handler: "handler",
role: role,
entry: join(__dirname, "../lambda/edge-verify/index.ts"),
bundling: {
define: {
"process.env.DOMAIN_NAME": JSON.stringify(props.domainName),
"process.env.API_KEY": JSON.stringify(props.apiKey),
},
},
architecture: Architecture.X86_64,
});
this.version = this.function.currentVersion;
}
}
Because Lambda@Edge cannot use Environment Variables, we need to use the define
property to pass in the domain name and API key. We will be usig those values to verify the request is coming from our domain and then injecting the API_KEY into the request headers. We'll see how we create the API_KEY later.
Integrate with CloudFront
In your stack, connect the Lambda@Edge function to CloudFront:
const edgeVerifyFunction = new EdgeVerifyConstruct(this, "EdgeVerify", {
domainName: props.domainName,
apiKey: verificationApiKey,
}).function;
const distribution = new Distribution(this, "Distribution", {
defaultBehavior: {
origin: new HttpOrigin(props.apiDomain),
edgeLambdas: [
{
functionVersion: edgeVerifyFunction.currentVersion,
eventType: LambdaEdgeEventType.ORIGIN_REQUEST,
},
],
},
});
API Gateway Construct
export class SearchConstruct extends Construct {
public readonly api: RestApi;
private readonly apiKey: string;
constructor(scope: Construct, id: string, props: SearchConstructProps) {
super(scope, id);
// Generate a secure API key
this.apiKey = randomBytes(32).toString("hex");
// Create log group for API Gateway
const logGroup = new LogGroup(this, "SearchApiLogs", {
retention: RetentionDays.ONE_WEEK,
});
// Create authorizer function
const authorizerFunction = new NodejsFunction(this, "AuthorizerFunction", {
runtime: Runtime.NODEJS_18_X,
handler: "handler",
entry: join(__dirname, "../lambda/authorizer/index.ts"),
environment: {
API_KEY: this.apiKey,
DOMAIN_NAME: props.domainName,
},
});
// Create authorizer
const authorizer = new RequestAuthorizer(this, "SearchAuthorizer", {
handler: authorizerFunction,
identitySources: [IdentitySource.header("x-api-key")],
resultsCacheTtl: Duration.seconds(0),
});
// Reference existing secret
const pineconeSecret = Secret.fromSecretNameV2(
this,
"PineconeSecret",
"pinecone/api-key", // Match the existing secret name in AWS
);
// Create search function
const searchFunction = new NodejsFunction(this, "SearchFunction", {
runtime: Runtime.NODEJS_18_X,
handler: "handler",
timeout: Duration.seconds(30),
entry: join(__dirname, "../lambda/search/index.ts"),
environment: {
PINECONE_INDEX_NAME: props.pineconeIndexName,
PINECONE_SECRET_NAME: pineconeSecret.secretName,
PINECONE_INDEX_HOST: process.env.PINECONE_INDEX_HOST!,
},
});
// Grant the Lambda function permission to read the secret
pineconeSecret.grantRead(searchFunction);
// Create API Gateway with logging
this.api = new RestApi(this, "SearchApi", {
defaultCorsPreflightOptions: {
allowOrigins: [`https://${props.domainName}`],
allowMethods: ["GET", "POST"],
allowHeaders: ["Content-Type", "x-api-key", "Authorization"],
},
deployOptions: {
accessLogDestination: new LogGroupLogDestination(logGroup),
accessLogFormat: AccessLogFormat.jsonWithStandardFields({
caller: true,
httpMethod: true,
ip: true,
protocol: true,
requestTime: true,
resourcePath: true,
responseLength: true,
status: true,
user: true,
}),
loggingLevel: MethodLoggingLevel.INFO,
metricsEnabled: true,
tracingEnabled: true,
},
});
// Add search endpoint
const search = this.api.root.addResource("search");
search.addMethod("GET", new LambdaIntegration(searchFunction), {
authorizer,
authorizationType: AuthorizationType.CUSTOM,
apiKeyRequired: false,
});
}
public getApiKey(): string {
return this.apiKey;
}
}
This CDK constrcut will create an API Gateway with an authorizer that validates the API key and referer header. It will also create a Lambda function that will be used to search for content in our Pinecone index.
Additionally, this is where we generate the API_KEY that will be used to validate the request at the API Gateway. This API_KEY is generated during deployment and never exposed to the public. This ensures that our API Gateway will only respond to requests that originate with our Cloudfront Distribution.
Security Features
Cloudfront Distribution
- Provides edge-level security
- Rejects requests that don't come from our domain
- Prevents direct API access
- Implements WAF rate limiting protection
WAF Rate Limiting
We've implemented AWS WAF (Web Application Firewall) rate limiting at the CloudFront level to protect against abuse:
// Create WAF Web ACL with rate limiting
const webAcl = new CfnWebACL(this, "WebAcl", {
defaultAction: { allow: {} },
scope: "CLOUDFRONT",
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: "WebACL",
sampledRequestsEnabled: true,
},
rules: [
{
name: "RateLimitRule",
priority: 1,
action: { block: {} },
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: "RateLimitRule",
sampledRequestsEnabled: true,
},
statement: {
rateBasedStatement: {
limit: 2000,
aggregateKeyType: "IP",
},
},
},
],
});
// Associate WAF with CloudFront distribution
this.distribution = new Distribution(this, "Distribution", {
webAclId: webAcl.attrArn,
// ... other distribution config
});
The WAF configuration provides:
- Rate limiting of 2000 requests per 5 minutes per IP address
- Automatic blocking of IPs that exceed the limit
- CloudWatch metrics for monitoring rate limit hits
- Sampling of requests for analysis
- Protection at the edge before requests reach API Gateway
This adds another layer of security by:
- Preventing DoS attacks
- Limiting API abuse
- Protecting backend resources
- Providing monitoring and visibility
API_KEY Injection and Validation
- Injects the API key into the request headers at the Cloudfront distribution
- Validates the API key at the API Gateway
- Ensures that all requests to the API Gateway use the Cloudfront distribution
API Gateway Authorization
Once the request has been validated by the Lambda@Edge function, it will be passed to the API Gateway. The API Gateway will then validate the API key and referer header.
const apiKey = event.headers?.["x-api-key"];
const expectedApiKey = process.env.API_KEY || "";
const domainName = process.env.DOMAIN_NAME || "";
const referer = event.headers?.["Referer"] || event.headers?.["referer"];
console.log("[DEBUG] Checking authorization:", {
hasApiKey: !!apiKey,
expectedApiKey: expectedApiKey,
referer,
});
const isValidReferer =
referer &&
(referer.startsWith(`https://${domainName}/`) ||
referer.startsWith(`https://www.${domainName}/`));
const isAuthorized = apiKey === expectedApiKey && isValidReferer;
Security Flow
Request Initiation
- Request comes from blog page
- Must include proper referer header
- Must be HTTPS
Edge Verification
- Lambda@Edge validates referer
- Checks domain and /blog path
- Injects API key for valid requests
API Gateway Authorization
- Secondary validation of referer
- Validates injected API key
Limitations
While this approach provides a high level of security, it does have some limitations. It is possible to spoof the referer header when making a request. Because this would come through our Cloudfront Distribution, the API_KEY would be added and passed through to our API Gateway. The addition of a rate limiter on our API Gateway should help prevent extreme abuse and provide another layer of protection.