Secure Static Content Hosting with S3, CloudFront, and Basic Authentication
This guide demonstrates how to:
- Host static assets in an AWS S3 bucket
- Serve them globally via CloudFront with an Origin Access Control (OAC)
- Enforce Basic Authentication at edge locations using Lambda@Edge
- Secure access through precise IAM policies
Step 1: Create and Secure the S3 Bucket
1.1 Create a New S3 Bucket
- Console: S3 > Create bucket.
- Bucket name:
your-docs-bucket(must be globally unique). - Region:
us-east-1(required for Lambda@Edge). - Uncheck "Block all public access." We lock it down with CloudFront instead.
1.2 Configure Bucket Policy
Allow only CloudFront to fetch objects. Replace placeholders with actual IDs:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": "cloudfront.amazonaws.com" },
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-docs-bucket/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/EDFDVBD632BHDS5"
}
}
}
]
}
Step 2: Set Up CloudFront Distribution
2.1 Create a Distribution
- Console: CloudFront > Create distribution.
- Origin domain:
your-docs-bucket.s3.us-east-1.amazonaws.com - Origin access: Origin Access Control (OAC) -- create a new OAC.
- Viewer protocol policy: Redirect HTTP to HTTPS.
- Default root object:
index.html. - Alternate domain name (CNAME):
docs.example.com(if using a custom domain).
2.2 Configure Origin Access Control (OAC)
- After distribution creation, go to the Origins tab.
- Select the origin > Edit > Origin access > Apply the new OAC.
- This replaces legacy Origin Access Identity (OAI).
CloudFront will update the bucket policy OAC principal automatically if the option is selected.
Step 3: Implement Basic Authentication with Lambda@Edge
3.1 Create IAM Role for Lambda@Edge
The Lambda function needs a role that trusts both Lambda and CloudFront:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "Service": ["lambda.amazonaws.com", "edgelambda.amazonaws.com"] },
"Action": "sts:AssumeRole"
}
]
}
Name: lambda-edge-basic-auth-role
3.2 Attach Execution Policy
Grant CloudWatch Logs access for debugging:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
"Resource": "arn:aws:logs:*:*:*"
}
]
}
3.3 Write the Lambda Function
- Console: Lambda > Create function > Author from scratch.
- Name:
basic-auth-edge, Runtime: Node.js 18.x, Region:us-east-1. - Attach Role:
lambda-edge-basic-auth-role.
Use this code:
'use strict';
exports.handler = async (event) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
const authUser = 'admin';
const authPass = 'P@ssw0rd!';
const authString = 'Basic ' + Buffer.from(authUser + ':' + authPass).toString('base64');
if (
typeof headers.authorization === 'undefined' ||
headers.authorization[0].value !== authString
) {
return {
status: '401',
statusDescription: 'Unauthorized',
body: 'Unauthorized',
headers: {
'www-authenticate': [{ key: 'WWW-Authenticate', value: 'Basic realm="Protected"' }],
},
};
}
return request;
};
3.4 Publish and Version Lambda
- Publish a new version from the Lambda console.
- Note the version ARN:
arn:aws:lambda:us-east-1:123456789012:function:basic-auth-edge:1.
3.5 Attach Lambda@Edge to CloudFront
- CloudFront > Distributions > Edit behavior > Function associations.
- Select Viewer Request and paste the version ARN.
- Save and deploy. Edge locations will propagate in minutes.
Step 4: Final Verification
- Visit
https://docs.example.com. A Basic Auth prompt should appear. - Enter credentials to proceed.
- Attempt direct S3 URL access (
https://your-docs-bucket.s3.amazonaws.com/index.html) -- it should return Access Denied.
Final Notes
The setup is now:
- Scalable S3 storage with no direct public access
- Global CloudFront CDN with HTTPS
- Edge-level Basic Authentication via Lambda@Edge
Security Tip: Rotate Basic Auth credentials periodically and monitor CloudFront logs (S3 bucket logs or Lambda@Edge logs) for unauthorized attempts. For production use cases with more complex authentication requirements, consider integrating with Cognito or an external identity provider instead of Basic Auth.