Links#
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Introduction.html
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-overview.html
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-signed-urls.html
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-signed-cookies.html
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-trusted-signers.html
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html#oac-overview
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-choosing-signed-urls-cookies.html
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/function-code-choose-purpose.html
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-edge-event-request-response.html
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/understanding-response-headers-policies.html
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/creating-response-headers-policies.html
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-awswaf.html
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/standard-logging.html
https://docs.aws.amazon.com/cli/latest/reference/cloudfront/sign.html
1. Important Points#
CloudFront 是 CDN:
cache static / dynamic content at edge
reduce origin traffic
lower latency
support HTTPS / WAF / geo restriction / signed URL / signed cookie
常见 origin:
S3 bucket
ALB
API Gateway
custom HTTP origin
核心原则:
S3 origin should use OAC, not public bucket
ACM cert for CloudFront must be in us-east-1
signed URL / signed cookie protects viewer access
OAC protects origin access
cache policy and origin request policy must be explicit
CloudFront signed URL != S3 presigned URL:
CloudFront signed URL:
viewer -> CloudFront
signed by your app with CloudFront private key
verified by CloudFront public key / key group
S3 presigned URL:
viewer -> S3
signed by AWS credentials
verified by S3 IAM auth
private content with S3:
viewer must use CloudFront signed URL / cookie
S3 bucket should only allow CloudFront distribution through OAC
2. Service Configuration#
distribution#
| Item |
Recommendation |
| Origin |
S3 / ALB / custom origin |
| S3 access |
use OAC |
| Viewer protocol |
redirect HTTP to HTTPS |
| TLS cert |
ACM in us-east-1 |
| Cache policy |
explicit policy per behavior |
| Origin request policy |
forward only required headers/cookies/query strings |
| WAF |
enable for public endpoints |
| Logs |
standard logs / real-time logs when needed |
| Private content |
trusted key group + signed URL/cookie |
cache behavior#
cache behavior controls:
path pattern
origin
viewer protocol policy
allowed methods
cache policy
origin request policy
response headers policy
trusted key groups
private content:
enable Restrict viewer access
choose Trusted key groups
attach key group
3. Private Content Concepts#
signer#
signer:
entity allowed to create signed URL / signed cookie
recommended:
trusted key group
legacy:
trusted AWS account / CloudFront key pair
not recommended for new setup
public key / private key#
private key:
kept by application
used to sign URL / cookie
must be stored securely
rotate regularly
public key:
uploaded to CloudFront
used by CloudFront to verify signature
belongs to a key group
supported key types:
RSA 2048
ECDSA 256
key group#
key group:
contains one or more CloudFront public keys
attached to cache behavior
CloudFront uses public keys in key group to verify signatures
why key group:
recommended by AWS
no root account key pair management
supports key rotation
can be reused by multiple distributions / behaviors
Key-Pair-Id#
Key-Pair-Id in signed URL:
query parameter name:
Key-Pair-Id
trusted key group mode:
value = CloudFront Public Key ID
not Key Group ID
legacy trusted signer mode:
value = legacy CloudFront key pair ID / access key ID
example signed URL query:
Expires=1790599200
Signature=<signature>
Key-Pair-Id=K1234567890ABC
4. Signed URL vs Signed Cookie#
| Mode |
When To Use |
Notes |
| Signed URL |
one file / download link / short-lived access |
URL contains signature query parameters |
| Signed Cookie |
many files under a path / website area / video assets |
browser sends three CloudFront cookies |
signed URL:
good for:
private file download
one video file
one image/object
signed cookie:
good for:
/paid/*
HLS video segments
many objects in one protected area
priority:
if both signed URL and signed cookie exist for same request,
CloudFront evaluates the signed URL.
canned policy vs custom policy#
| Policy |
Supports |
When To Use |
| Canned |
resource + expiration |
simple expiration |
| Custom |
resource + start time + expiration + IP range |
stricter access control |
canned policy:
simpler
URL has Expires / Signature / Key-Pair-Id
custom policy:
supports:
DateGreaterThan
DateLessThan
IpAddress
wildcard resource
URL has Policy / Signature / Key-Pair-Id
5. Complete Key Group Flow#
variables#
export AWS_PAGER=""
export DIST_ID="E1234567890ABC"
export ACCOUNT_ID="123456789012"
export KEY_NAME="prod-order-private-content-2026-05"
export KEY_GROUP_NAME="prod-order-private-content"
export CF_DOMAIN="d111111abcdef8.cloudfront.net"
generate RSA key pair#
mkdir -p cloudfront-signing
cd cloudfront-signing
openssl genrsa -out cf-private-key.pem 2048
openssl rsa -pubout -in cf-private-key.pem -out cf-public-key.pem
chmod 0400 cf-private-key.pem
chmod 0644 cf-public-key.pem
files:
cf-private-key.pem:
application uses it to sign URL/cookie
never upload to CloudFront
never commit to git
cf-public-key.pem:
upload to CloudFront
can be stored in IaC repo if allowed by policy
generate ECDSA key pair#
openssl ecparam -name prime256v1 -genkey -noout -out cf-private-key.pem
openssl ec -in cf-private-key.pem -pubout -out cf-public-key.pem
chmod 0400 cf-private-key.pem
chmod 0644 cf-public-key.pem
choose one:
RSA 2048:
widely used
compatible with older examples
ECDSA 256:
smaller signature
shorter URL
newer support
validate key pair#
echo "hello" > message.txt
openssl dgst -sha256 -sign cf-private-key.pem -out message.sig message.txt
openssl dgst -sha256 -verify cf-public-key.pem -signature message.sig message.txt
create CloudFront public key#
jq -n \
--arg caller_reference "$(date +%s)-${KEY_NAME}" \
--arg name "${KEY_NAME}" \
--rawfile encoded_key cf-public-key.pem \
'{
CallerReference: $caller_reference,
Name: $name,
EncodedKey: $encoded_key,
Comment: "public key for CloudFront signed URLs/cookies"
}' > public-key-config.json
aws cloudfront create-public-key \
--public-key-config file://public-key-config.json \
--query 'PublicKey.Id' \
--output text
export PUBLIC_KEY_ID="K1234567890ABC"
PUBLIC_KEY_ID:
this is the value used as Key-Pair-Id when signing URL/cookie
create key group#
jq -n \
--arg name "${KEY_GROUP_NAME}" \
--arg public_key_id "${PUBLIC_KEY_ID}" \
'{
Name: $name,
Comment: "trusted key group for private content",
Items: [$public_key_id]
}' > key-group-config.json
aws cloudfront create-key-group \
--key-group-config file://key-group-config.json \
--query 'KeyGroup.Id' \
--output text
export KEY_GROUP_ID="KG1234567890ABC"
KEY_GROUP_ID:
attach this to cache behavior TrustedKeyGroups
PUBLIC_KEY_ID:
pass this as Key-Pair-Id to signing code / aws cloudfront sign
attach key group to cache behavior#
console path:
CloudFront
-> Distributions
-> <distribution>
-> Behaviors
-> <path pattern>
-> Edit
-> Restrict viewer access: Yes
-> Trusted authorization type: Trusted key groups
-> Trusted key groups: <KEY_GROUP_ID>
CLI / IaC:
update distribution config
set cache behavior:
TrustedKeyGroups.Enabled = true
TrustedKeyGroups.Quantity = 1
TrustedKeyGroups.Items = [KEY_GROUP_ID]
{
"TrustedKeyGroups": {
"Enabled": true,
"Quantity": 1,
"Items": [
"KG1234567890ABC"
]
}
}
warning:
CloudFront distribution update needs ETag / If-Match
safer to manage this with Terraform / CloudFormation / CDK
for manual setup, use console first
6. Sign URL#
AWS CLI canned policy#
aws cloudfront sign \
--url "https://${CF_DOMAIN}/private/video.mp4" \
--key-pair-id "${PUBLIC_KEY_ID}" \
--private-key file://cf-private-key.pem \
--date-less-than "2026-05-29T12:00:00Z"
output:
https://d111111abcdef8.cloudfront.net/private/video.mp4?Expires=...&Signature=...&Key-Pair-Id=K1234567890ABC
AWS CLI custom policy#
aws cloudfront sign \
--url "https://${CF_DOMAIN}/private/video.mp4" \
--key-pair-id "${PUBLIC_KEY_ID}" \
--private-key file://cf-private-key.pem \
--date-greater-than "2026-05-29T10:00:00Z" \
--date-less-than "2026-05-29T12:00:00Z" \
--ip-address "203.0.113.0/24"
custom policy use cases:
not valid before start time
expires at end time
only allowed from specific IP / CIDR
test#
SIGNED_URL="$(aws cloudfront sign \
--url "https://${CF_DOMAIN}/private/video.mp4" \
--key-pair-id "${PUBLIC_KEY_ID}" \
--private-key file://cf-private-key.pem \
--date-less-than "2026-05-29T12:00:00Z")"
curl -I "${SIGNED_URL}"
expected:
200 / 206 / 304 if object exists and policy valid
common failure:
403 InvalidKey:
wrong Key-Pair-Id
public key not in trusted key group
key group not attached to matching cache behavior
403 AccessDenied:
signed URL expired
signature mismatch
URL changed after signing
origin access denied
7. Sign URL In Java#
dependency#
<dependencyManagement>
<dependencies>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>bom</artifactId>
<version>${aws.sdk.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>cloudfront</artifactId>
</dependency>
code#
import software.amazon.awssdk.services.cloudfront.CloudFrontUtilities;
import software.amazon.awssdk.services.cloudfront.model.CannedSignerRequest;
import software.amazon.awssdk.services.cloudfront.url.SignedUrl;
import java.nio.file.Path;
import java.time.Instant;
public class CloudFrontSignedUrlExample {
public static void main(String[] args) {
String resourceUrl = "https://d111111abcdef8.cloudfront.net/private/video.mp4";
String publicKeyId = "K1234567890ABC";
Path privateKeyPath = Path.of("cf-private-key.pem");
CloudFrontUtilities utilities = CloudFrontUtilities.create();
CannedSignerRequest request = CannedSignerRequest.builder()
.resourceUrl(resourceUrl)
.privateKey(privateKeyPath)
.keyPairId(publicKeyId)
.expirationDate(Instant.parse("2026-05-29T12:00:00Z"))
.build();
SignedUrl signedUrl = utilities.getSignedUrlWithCannedPolicy(request);
System.out.println(signedUrl.url());
}
}
publicKeyId:
same as CloudFront Public Key ID
appears in URL as Key-Pair-Id
8. Signed Cookies#
cookies#
canned policy cookies:
CloudFront-Expires
CloudFront-Signature
CloudFront-Key-Pair-Id
custom policy cookies:
CloudFront-Policy
CloudFront-Signature
CloudFront-Key-Pair-Id
use signed cookies when:
user needs access to many files
URL should not change
HLS/DASH video has many segment files
cookie attributes#
recommended:
Secure
HttpOnly
SameSite=Lax or SameSite=None when cross-site is required
Domain=<your domain>
Path=/private/
Set-Cookie: CloudFront-Expires=1790599200; Path=/private/; Secure; HttpOnly; SameSite=Lax
Set-Cookie: CloudFront-Signature=<signature>; Path=/private/; Secure; HttpOnly; SameSite=Lax
Set-Cookie: CloudFront-Key-Pair-Id=K1234567890ABC; Path=/private/; Secure; HttpOnly; SameSite=Lax
9. Origin Auth vs Viewer Auth#
two different permission surfaces:
origin auth:
CloudFront -> S3
protects S3 origin from direct public access
use OAC / OAI + S3 bucket policy
viewer auth:
user/browser -> CloudFront
protects CloudFront URL from public access
use signed URL / signed cookie / auth at app or edge
origin auth 的作用:
users cannot bypass CloudFront and read S3 object directly
S3 bucket can stay private
S3 Block Public Access can stay enabled
bucket policy allows CloudFront distribution as origin reader
origin auth 不做什么:
it does not restrict who can access CloudFront distribution URL
it does not require signed URL/cookie by itself
it does not authenticate viewer identity
结果:
如果 CloudFront viewer side 是公开的,
任何人拿到 CloudFront URL 仍然可以访问内容。
| Layer |
Traffic |
Control |
Protects |
| Origin auth |
CloudFront -> S3 |
OAC / OAI + S3 bucket policy |
S3 cannot be accessed directly |
| Viewer auth |
Viewer -> CloudFront |
signed URL / signed cookie / application auth |
CloudFront URL cannot be used by everyone |
common mistake:
"I enabled OAC, so CloudFront content is private."
correct:
OAC makes S3 private from direct access.
signed URL / signed cookie makes CloudFront viewer access private.
recommended private S3 content design:
S3:
Block Public Access enabled
bucket policy allows only CloudFront distribution through OAC
CloudFront origin:
use OAC
regular S3 bucket origin, not S3 website endpoint
CloudFront viewer:
attach trusted key group to behavior
require signed URL / signed cookie
keep URL/cookie TTL short
minimal S3 bucket policy for OAC#
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontOACRead",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-private-bucket/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/E1234567890ABC"
}
}
}
]
}
这个 policy 表达的是:
only this CloudFront distribution can read objects from this bucket
users cannot access S3 object directly
viewer still needs signed URL/cookie if CloudFront URL should not be public
replace:
my-private-bucket:
your S3 bucket name
123456789012:
your AWS account ID
E1234567890ABC:
your CloudFront distribution ID
10. Viewer Auth#
viewer auth 保护的是:
who can access CloudFront URL
who can watch/download the file through CloudFront
whether a copied CloudFront URL still works for another user
viewer auth 不保护:
S3 direct access
origin bucket policy
application authorization after origin receives request
需要同时理解:
OAC/OAI protects origin access
viewer auth protects viewer access
| Method |
Best For |
Notes |
| CloudFront signed URL |
one object / one download link / short TTL |
URL carries signature query parameters |
| CloudFront signed cookies |
many files under one path / video segments / website area |
browser sends CloudFront cookies |
| CloudFront Function token check |
simple header/cookie/query token validation at viewer request |
lightweight JavaScript, can return 401/403 early |
| Lambda@Edge token check |
more complex auth logic at edge |
heavier than CloudFront Function, can run on viewer request |
signed URL#
use signed URL when:
user receives one file link
each link has short expiration
link is generated by application after authorization
URL sharing risk is acceptable within TTL / policy
common examples:
private report download
one invoice PDF
one video file
important:
signed URL is bearer access.
Whoever has the valid signed URL can use it until policy expires.
signed cookies#
use signed cookies when:
user needs many files
user enters a private site area
HLS/DASH video has many segments
URL should stay clean without signature query string
common examples:
/private/*
/paid-course/*
/video/hls/*
important:
cookie Domain / Path must match requested CloudFront domain/path
use Secure / HttpOnly / SameSite
CloudFront Function token check#
use CloudFront Function when:
validation is simple
check cookie/header/query string
return 401/403 before cache/origin
no network call is needed
good for:
static shared token
lightweight JWT shape check
allowlist / denylist by header or path
not good for:
calling auth server
complex crypto / large dependency
per-request database lookup
function handler(event) {
var request = event.request;
var headers = request.headers;
if (!headers["x-download-token"] || headers["x-download-token"].value !== "expected-token") {
return {
statusCode: 403,
statusDescription: "Forbidden"
};
}
return request;
}
Lambda@Edge token check#
use Lambda@Edge when:
logic is too complex for CloudFront Function
request needs richer processing
viewer request should be rejected before origin
注意:
Lambda@Edge has runtime / region / deployment constraints
viewer request function runs before CloudFront checks cache/origin
do not put slow auth flow in front of every static object unless latency is acceptable
export const handler = async (event) => {
const request = event.Records[0].cf.request;
const headers = request.headers;
const token = headers.authorization?.[0]?.value;
if (token !== "Bearer expected-token") {
return {
status: "403",
statusDescription: "Forbidden",
body: "Forbidden"
};
}
return request;
};
decision#
choose:
one object:
signed URL
many objects / video segments:
signed cookies
simple custom token at edge:
CloudFront Function
complex custom auth at edge:
Lambda@Edge
private S3 origin:
still use OAC/OAI separately
11. S3 Origin With OAC#
goal:
users cannot access S3 object directly
users can only access object through CloudFront
CloudFront viewer access is controlled by signed URL/cookie
bucket policy#
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipalReadOnly",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-private-bucket/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/E1234567890ABC"
}
}
}
]
}
OAC notes:
use regular S3 bucket origin
not S3 static website endpoint
keep S3 Block Public Access enabled
if using SSE-KMS, KMS key policy must allow CloudFront distribution
12. Key Rotation#
safe rotation:
1. generate new private/public key pair
2. create new CloudFront public key
3. add new public key to existing key group
4. deploy app with new private key and new PUBLIC_KEY_ID
5. wait until old signed URLs/cookies expire
6. remove old public key from key group
7. delete old public key
8. delete old private key from secret store
do not:
replace key immediately while old URLs are still valid
delete old public key before old signed URLs expire
store private key in git
13. Security Best Practices#
CloudFront security layers:
viewer security:
HTTPS only
signed URL / signed cookie / custom token auth when content is private
WAF for public endpoints
geo restriction when business requires it
response security:
response headers policy
HSTS
CSP
X-Content-Type-Options
X-Frame-Options
Referrer-Policy
origin security:
OAC/OAI for S3 origin
origin request policy minimized
custom origin should verify CloudFront-only secret header when applicable
operations:
standard logs / real-time logs
CloudTrail for distribution changes
alarms on 4xx/5xx and WAF blocks
HTTPS only behavior sample#
{
"ViewerProtocolPolicy": "redirect-to-https",
"AllowedMethods": {
"Quantity": 2,
"Items": ["GET", "HEAD"]
},
"CachedMethods": {
"Quantity": 2,
"Items": ["GET", "HEAD"]
},
"Compress": true
}
notes:
redirect-to-https:
HTTP viewer request is redirected to HTTPS
GET/HEAD only:
static private content should not allow write methods
for API behavior:
allowed methods may include POST/PUT/PATCH/DELETE
attach WAF and auth separately
{
"Name": "prod-order-security-headers",
"Comment": "security headers for CloudFront viewer responses",
"SecurityHeadersConfig": {
"StrictTransportSecurity": {
"AccessControlMaxAgeSec": 31536000,
"IncludeSubdomains": true,
"Preload": true,
"Override": true
},
"ContentTypeOptions": {
"Override": true
},
"FrameOptions": {
"FrameOption": "DENY",
"Override": true
},
"ReferrerPolicy": {
"ReferrerPolicy": "strict-origin-when-cross-origin",
"Override": true
},
"ContentSecurityPolicy": {
"ContentSecurityPolicy": "default-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self'",
"Override": true
}
}
}
aws cloudfront create-response-headers-policy \
--response-headers-policy-config file://security-headers-policy.json
attach:
attach ResponseHeadersPolicyId to cache behavior
注意:
CSP must match your real frontend assets
start strict for private static download
tune carefully for SPA / third-party scripts
WAF web ACL sample#
Resources:
CloudFrontWebAcl:
Type: AWS::WAFv2::WebACL
Properties:
Name: prod-order-cloudfront
Scope: CLOUDFRONT
DefaultAction:
Allow: {}
VisibilityConfig:
CloudWatchMetricsEnabled: true
MetricName: prod-order-cloudfront
SampledRequestsEnabled: true
Rules:
- Name: AWSManagedRulesCommonRuleSet
Priority: 10
OverrideAction:
None: {}
Statement:
ManagedRuleGroupStatement:
VendorName: AWS
Name: AWSManagedRulesCommonRuleSet
VisibilityConfig:
CloudWatchMetricsEnabled: true
MetricName: common
SampledRequestsEnabled: true
- Name: AWSManagedRulesKnownBadInputsRuleSet
Priority: 20
OverrideAction:
None: {}
Statement:
ManagedRuleGroupStatement:
VendorName: AWS
Name: AWSManagedRulesKnownBadInputsRuleSet
VisibilityConfig:
CloudWatchMetricsEnabled: true
MetricName: known-bad-inputs
SampledRequestsEnabled: true
notes:
CloudFront WAF uses Scope=CLOUDFRONT
create/manage it in us-east-1 for CloudFront
start with AWS managed rule groups
monitor sampled requests before adding aggressive custom blocks
{
"OriginCustomHeaders": {
"Quantity": 1,
"Items": [
{
"HeaderName": "x-origin-secret",
"HeaderValue": "change-this-secret"
}
]
}
}
use case:
custom origin is ALB / Nginx / application
origin rejects requests without x-origin-secret
users cannot call origin directly without the secret header
注意:
this is not for S3 OAC
rotate the secret
do not log the header value
restrict origin network path where possible
logging sample#
{
"Enabled": true,
"IncludeCookies": false,
"Bucket": "cloudfront-logs-prod.s3.amazonaws.com",
"Prefix": "order-private-content/"
}
security log checklist:
standard logs enabled
real-time logs enabled for high-risk private content when needed
WAF logs enabled
S3 log bucket encrypted
log bucket write policy controlled
retention defined
14. Monitoring#
| Area |
Metric / Log |
What To Watch |
| Availability |
5xxErrorRate, 4xxErrorRate |
origin errors / access denied spike |
| Traffic |
Requests, BytesDownloaded, BytesUploaded |
traffic trend |
| Latency |
OriginLatency, total download time from logs |
origin latency |
| Cache |
CacheHitRate |
cache efficiency |
| Signed URL |
standard logs sc-status=403, x-edge-detailed-result-type |
invalid signature / expired URL |
| Origin |
S3 access logs / CloudTrail / CloudWatch |
origin permission issue |
dashboard:
requests
4xx / 5xx
cache hit rate
origin latency
top paths
top 403 detailed result type
bytes downloaded
15. Troubleshooting#
403 InvalidKey:
Key-Pair-Id is not CloudFront Public Key ID
public key is not in key group
key group is not attached to behavior
request path matched another behavior
403 SignatureDoesNotMatch / malformed signature:
private key does not match public key
URL modified after signing
query string added after signing
wrong policy JSON / base64 encoding
403 AccessDenied:
signed URL expired
not yet valid
IP condition mismatch
S3 bucket policy / OAC wrong
object does not exist but origin hides it as access denied
works with unsigned URL but not signed URL:
behavior may not require trusted key group
signed URL key ID mismatch
works with signed URL but direct S3 also works:
S3 bucket is public
OAC / bucket policy not locked down
direct S3 access denied but CloudFront URL works without signed URL:
this is expected when only origin auth is enabled
OAC protects S3 direct access
CloudFront viewer access is still public
attach trusted key group if viewer access must be private
CloudFront Function / Lambda@Edge returns 403:
token missing
token in wrong header/cookie/query parameter
function associated with wrong cache behavior
cached response behavior not understood
16. Production Checklist#
distribution:
HTTPS only
WAF enabled when public
response headers policy attached
cache policy reviewed
origin request policy minimized
logs enabled
private content:
trusted key group used
Key-Pair-Id documented as Public Key ID
private key in Secrets Manager / secure secret store
public key in CloudFront
key group attached to correct behavior
signed URL TTL short
custom policy used when IP/start-time restriction needed
S3 origin:
OAC enabled
S3 Block Public Access enabled
bucket policy restricts SourceArn to distribution
SSE-KMS key policy reviewed if used
custom origin:
direct origin access restricted
secret origin header configured if appropriate
origin does not trust viewer-controlled headers blindly
viewer response:
HSTS enabled
X-Content-Type-Options enabled
X-Frame-Options or CSP frame-ancestors configured
CSP reviewed for actual frontend assets
origin auth vs viewer auth:
OAC/OAI only protects origin access
signed URL/cookie protects viewer access
CloudFront Function / Lambda@Edge token check protects viewer access when custom auth is required
direct S3 access test returns 403
unsigned CloudFront URL test returns 403 when private content is required
rotation:
key rotation runbook exists
old public key kept until old signed URLs expire
app deploy and key group update order documented