
Tried placing Lambda Web Functions behind CloudFront OAC with CloudFormation
This page has been translated by machine translation. View original
Introduction
On October 12, 2026, simultaneously with the GA of Lambda Web Functions, CloudFront's Origin Access Control (OAC) added support for Lambda Web Functions.
By using OAC, you can deny access that does not go through a specific CloudFront distribution, making it an effective measure against the risks of directly exposing a Web Function.
This time, I had the opportunity to build Lambda Web Functions, CloudFront, and OAC as IaC using CloudFormation, so I will introduce the procedure.
Configuration and Approach
Since the Web Function code can only be passed as a .zip file stored in S3, I split the template into two files and divided the process into 3 steps with code deployment in between.
| Step | Content | Method |
|---|---|---|
| 1 | Create an S3 bucket to store the code | CloudFormation |
| 2 | Place the code .zip file | CLI |
| 3 | Create the Web Function, OAC, Distribution, and resource policy | CloudFormation |
Step 1: Create an S3 Bucket to Store the Code
The bucket that stores the Web Function code must have versioning enabled. Lambda pins the code for each revision using the object version, so versioning is required.
To allow Lambda to read the .zip from the bucket, grant lambda.amazonaws.com s3:GetObject and s3:GetObjectVersion permissions via a bucket policy.
- Sid: AllowLambdaWebFunctionCodeAccess
Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action:
- s3:GetObject
- s3:GetObjectVersion
Resource: !Sub ${CodeBucket.Arn}/*
Condition:
ArnLike:
aws:SourceArn: !Sub arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:web-function/*
The aws:SourceArn uses a wildcard to allow read access from all Web Functions in the same account and region. If you want to restrict access to a specific Web Function, specify the function name like web-function/<function-name>/revision/*.
step1-code-bucket.yaml (full content)
AWSTemplateFormatVersion: '2010-09-09'
Description: Step 1 - S3 bucket for Lambda Web Function code (versioning + bucket policy for Lambda)
Resources:
CodeBucket:
Type: AWS::S3::Bucket
Properties:
VersioningConfiguration:
Status: Enabled
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
CodeBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref CodeBucket
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: AllowLambdaWebFunctionCodeAccess
Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action:
- s3:GetObject
- s3:GetObjectVersion
Resource: !Sub ${CodeBucket.Arn}/*
Condition:
ArnLike:
aws:SourceArn: !Sub arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:web-function/*
Outputs:
CodeBucketName:
Value: !Ref CodeBucket
Save the template as step1-code-bucket.yaml and create the stack. Once the stack reaches CREATE_COMPLETE, retrieve the bucket name from the CodeBucketName output. This value will be used in Steps 2 and 3.
aws cloudformation create-stack --stack-name hello-web-code --template-body file://step1-code-bucket.yaml
aws cloudformation describe-stacks --stack-name hello-web-code --query 'Stacks[0].Outputs'
Step 2: Place the Code in S3
The application is a simple Hello World that returns JSON. Prepare index.js and package.json. Since index.js uses import syntax (ES modules), specify "type": "module" in package.json.
index.js and package.json
index.js:
// Minimal AWS Lambda Web Function HTTP server.
//
// You can replace this with any code or framework that starts an HTTP server.
import http from 'node:http';
// Lambda sends requests to port 3000. Set LOCAL_PORT for local development.
const port = Number(process.env.LOCAL_PORT ?? 3000);
const server = http.createServer((req, res) => {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({ message: 'Hello from Lambda Web!', path: req.url }));
});
server.listen(port, () => {
console.log(`listening on port ${port}`);
});
package.json:
{
"type": "module",
"main": "index.js"
}
Compress the files so that index.js is placed at the root of the .zip, then upload it to the bucket from Step 1 as app/hello-web.zip.
zip -r hello-web.zip index.js package.json
aws s3 cp hello-web.zip s3://<CodeBucketName>/app/hello-web.zip
Step 3: Create the Web Function and CloudFront
step3-web-function.yaml (full content)
AWSTemplateFormatVersion: '2010-09-09'
Description: Step 3 - Lambda Web Function (IamAuth) behind CloudFront with OAC
Parameters:
FunctionName:
Type: String
Default: hello-web
CodeBucket:
Type: String
Description: Bucket created in step 1
CodeKey:
Type: String
Default: app/hello-web.zip
Description: Key of the .zip uploaded in step 2
Resources:
ExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
WebFunction:
Type: AWS::Lambda::WebFunction
Properties:
FunctionName: !Ref FunctionName
Revision:
Type: AWS::Lambda::WebFunctionRevision
Properties:
FunctionName: !Ref WebFunction
BuildConfig:
CodeConfig:
S3Object:
Bucket: !Ref CodeBucket
Key: !Ref CodeKey
RuntimeConfig:
Runtime: nodejs24.x
ServiceConfig:
ExecutionRoleArn: !GetAtt ExecutionRole.Arn
TimeoutSeconds: 30
Endpoint:
Type: AWS::Lambda::WebFunctionEndpoint
Properties:
FunctionName: !Ref WebFunction
EndpointName: main
EndpointType: HomeRegion
AuthType: IamAuth
RevisionWeights:
- RevisionId: !GetAtt Revision.RevisionId
Weight: 100
Oac:
Type: AWS::CloudFront::OriginAccessControl
Properties:
OriginAccessControlConfig:
Name: !Sub ${FunctionName}-oac
OriginAccessControlOriginType: lambda
SigningBehavior: always-amz-auth
SigningProtocol: sigv4
Distribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Enabled: true
Comment: !Ref FunctionName
Origins:
- Id: web-function
DomainName: !GetAtt Endpoint.DomainName
OriginAccessControlId: !GetAtt Oac.Id
CustomOriginConfig:
OriginProtocolPolicy: https-only
DefaultCacheBehavior:
TargetOriginId: web-function
ViewerProtocolPolicy: redirect-to-https
AllowedMethods: [GET, HEAD]
# Managed-CachingDisabled
CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad
# Managed-AllViewerExceptHostHeader
OriginRequestPolicyId: b689b0a8-53d0-40ab-baf2-68738e2966ac
ResourcePolicy:
Type: AWS::Lambda::ResourcePolicy
Properties:
ResourceArn: !GetAtt WebFunction.FunctionArn
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: AllowCloudFrontOACAccess
Effect: Allow
Principal:
Service: cloudfront.amazonaws.com
Action: lambda:InvokeWebFunctionEndpoint
Resource: !GetAtt Endpoint.EndpointArn
Condition:
StringEquals:
aws:SourceArn: !Sub arn:${AWS::Partition}:cloudfront::${AWS::AccountId}:distribution/${Distribution}
Outputs:
DistributionDomain:
Value: !GetAtt Distribution.DomainName
EndpointDomain:
Value: !GetAtt Endpoint.DomainName
Save the template as step3-web-function.yaml and create the stack. Pass the bucket name obtained in Step 1 to CodeBucket. The CodeKey can be omitted since its default value matches the key placed in Step 2 (app/hello-web.zip). Since an IAM role is being created, specify CAPABILITY_IAM for --capabilities.
aws cloudformation create-stack \
--stack-name hello-web \
--template-body file://step3-web-function.yaml \
--parameters ParameterKey=CodeBucket,ParameterValue=<CodeBucketName> \
--capabilities CAPABILITY_IAM
Execution Role
The trust policy allows lambda.amazonaws.com to assume the role (sts:AssumeRole), and AWSLambdaBasicExecutionRole is attached to enable writing logs to CloudWatch Logs.
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Revision and Endpoint
If VersionId of S3Object is omitted, it is pinned to the latest version of the object at the time the revision is created.
BuildConfig:
CodeConfig:
S3Object:
Bucket: !Ref CodeBucket
Key: !Ref CodeKey
RuntimeConfig:
Runtime: nodejs24.x
ServiceConfig:
ExecutionRoleArn: !GetAtt ExecutionRole.Arn
TimeoutSeconds: 30
Since IamAuth is required to use OAC, set the Endpoint's AuthType to IamAuth.
EndpointName: main
EndpointType: HomeRegion
AuthType: IamAuth
RevisionWeights:
- RevisionId: !GetAtt Revision.RevisionId
Weight: 100
OAC and Distribution
For an OAC targeting Web Functions, set OriginAccessControlOriginType to lambda and SigningBehavior to always-amz-auth.
OriginAccessControlConfig:
OriginAccessControlOriginType: lambda
SigningBehavior: always-amz-auth
SigningProtocol: sigv4
For the origin request policy, use the managed policy AllViewerExceptHostHeader, which does not forward the Host header to the origin.
Origins:
- Id: web-function
DomainName: !GetAtt Endpoint.DomainName
OriginAccessControlId: !GetAtt Oac.Id
CustomOriginConfig:
OriginProtocolPolicy: https-only
DefaultCacheBehavior:
# Managed-CachingDisabled
CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad
# Managed-AllViewerExceptHostHeader
OriginRequestPolicyId: b689b0a8-53d0-40ab-baf2-68738e2966ac
Resource Policy
Using AWS::Lambda::ResourcePolicy, grant cloudfront.amazonaws.com permission for lambda:InvokeWebFunctionEndpoint. Specify the ARN of the Web Function to attach the policy to in ResourceArn, and the ARN of the endpoint to allow invocation for in Resource.
ResourceArn: !GetAtt WebFunction.FunctionArn
PolicyDocument:
Statement:
- Sid: AllowCloudFrontOACAccess
Effect: Allow
Principal:
Service: cloudfront.amazonaws.com
Action: lambda:InvokeWebFunctionEndpoint
Resource: !GetAtt Endpoint.EndpointArn
Condition:
StringEquals:
aws:SourceArn: !Sub arn:${AWS::Partition}:cloudfront::${AWS::AccountId}:distribution/${Distribution}
By specifying the ARN of the distribution created in this stack in aws:SourceArn, the caller is restricted to that distribution only.
Verification
Once the stack reaches CREATE_COMPLETE, retrieve the CloudFront domain (DistributionDomain) and the endpoint domain (EndpointDomain) from the outputs.
aws cloudformation describe-stacks --stack-name hello-web --query 'Stacks[0].Outputs'
Sending a GET /hello?x=1 request to the CloudFront standard domain returned a 200 response with a JSON body.
$ curl -sS -i 'https://dxxxxxxxxxxxxx.cloudfront.net/hello?x=1'
HTTP/1.1 200 OK
Content-Type: application/json
X-Cache: Miss from cloudfront
{"message":"Hello from Lambda Web!","path":"/hello?x=1"}
Sending the same request directly to the endpoint domain returned a 403.
$ curl -sS -i 'https://xxxxxxxxxxxxxxxx.xxx.lambda-web.ap-northeast-1.on.aws/hello?x=1'
HTTP/1.1 403 Forbidden
Summary
I confirmed that everything from Lambda Web Functions to CloudFront and OAC configuration can be built using CloudFormation.
To avoid the risks associated with direct exposure, it is recommended to use OAC when publishing Lambda Web Functions.
Although omitted in this verification environment, if you plan to continuously publish Lambda Web Functions, please also consider configuring appropriate cache policies and access logs for CloudFront, and enabling WAF protection as needed.
