AWS CloudFront
本文缩写(英文全称与中文含义)
| 缩写 |
英文全称/名称 |
中文含义 |
| ACL |
Access Control List |
访问控制列表 |
| ACM |
AWS Certificate Manager |
AWS 证书管理服务 |
| ALB |
Application Load Balancer |
应用负载均衡器 |
| API |
Application Programming Interface |
应用程序编程接口 |
| AWS |
Amazon Web Services |
亚马逊云服务 |
| CDK |
Cloud Development Kit |
AWS 云开发工具包 |
| CDN |
Content Delivery Network |
内容分发网络 |
| CIDR |
Classless Inter-Domain Routing |
无类别域间路由,用于表示网段 |
| CLI |
Command-Line Interface |
命令行接口 |
| CSP |
Content Security Policy |
内容安全策略 |
| DASH |
Dynamic Adaptive Streaming over HTTP |
基于 HTTP 的动态自适应流媒体 |
| ECDSA |
Elliptic Curve Digital Signature Algorithm |
椭圆曲线数字签名算法 |
| ECS |
Elastic Container Service |
AWS 容器编排服务 |
| HLS |
HTTP Live Streaming |
基于 HTTP 的流媒体协议 |
| HSTS |
HTTP Strict Transport Security |
HTTP 严格传输安全策略 |
| HTTP |
Hypertext Transfer Protocol |
超文本传输协议 |
| HTTPS |
Hypertext Transfer Protocol Secure |
使用 TLS 加密的 HTTP |
| IaC |
Infrastructure as Code |
基础设施即代码 |
| IAM |
Identity and Access Management |
身份与访问管理 |
| ID |
Identifier |
标识符 |
| IP |
Internet Protocol |
网际协议;IP 地址是网络寻址标识 |
| JSON |
JavaScript Object Notation |
一种结构化数据格式 |
| JWT |
JSON Web Token |
一种携带声明的令牌格式 |
| KMS |
Key Management Service |
密钥管理服务 |
| OAC |
Origin Access Control |
CloudFront 源站访问控制 |
| OAI |
Origin Access Identity |
CloudFront 旧式源站访问身份 |
| PDF |
Portable Document Format |
便携式文档格式 |
| RSA |
Rivest-Shamir-Adleman |
一种公钥密码算法 |
| S3 |
Simple Storage Service |
AWS 对象存储服务 |
| SPA |
Single-Page Application |
单页应用 |
| SSE |
Server-Side Encryption |
服务端加密 |
| TLS |
Transport Layer Security |
传输层安全协议 |
| TTL |
Time To Live |
存活时间;具体指缓存有效期或记录过期时间 |
| URL |
Uniform Resource Locator |
统一资源定位符,通常指访问地址 |
| WAF |
Web Application Firewall |
Web 应用防火墙 |
Links#
按国家展示不同页面、缓存隔离及完整实验,见 CloudFront 国家内容分流。
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/controlling-the-cache-key.html
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cache-key-understand-cache-policy.html
https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/QueryStringParameters.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
cache policy and cache key#
CloudFront cache hit 的核心是 cache key。
default cache key:
distribution domain
URL path
cache policy can add:
query strings
headers
cookies
compression variant
TTL settings:
minimum TTL
default TTL
maximum TTL
原则:
only include values that change the response body.
如果 origin 对同一个 path 返回完全相同的文件,
不要把 query string / cookie / user-specific header 放进 cache key。
如果 origin 会根据某个值返回不同内容,
才把这个值放进 cache key。
| Request Value |
Cache Policy Choice |
When To Use |
| Query strings: None |
best for static objects / private downloads |
URL query 不影响文件内容 |
| Query strings: allowlist |
image resize / language / version parameter |
只有指定参数会改变响应 |
| Query strings: All |
rare |
每个 query variation 都是独立缓存对象 |
| Headers: None |
default for static content |
header 不影响响应 |
| Headers: allowlist |
Accept-Language, device variant, negotiated content |
origin 根据 header 返回不同内容 |
| Cookies: None |
default for cacheable static content |
cookie 不影响响应 |
| Cookies: allowlist |
A/B variant or explicit content variant |
origin 根据指定 cookie 返回不同内容 |
bad cache key examples:
/audio.mp3?token=random-1
/audio.mp3?token=random-2
if token is included in cache key:
two different cache entries
lower CacheHitRate
more origin requests
good cache key:
/audio.mp3
token/auth is validated outside the cache key
same object can be served from edge cache
signed URL and cache key#
CloudFront signed URL parameters:
Expires
Signature
Key-Pair-Id
Policy
这些参数表达 viewer authorization,
不是 object content variant。
private static object recommendation:
S3 stays private behind OAC
CloudFront behavior requires trusted key group
cache policy query strings = None
or allowlist only real content-variant query strings
origin request policy forwards only what origin really needs
do not:
use Query strings: All for static private downloads
forward all viewer cookies by default
include Authorization / session / random token in cache key unless response differs per value
use Expires / Signature / Key-Pair-Id / Policy as your own application query parameter names
do:
separate authorization from content variation
keep cache key small
monitor CacheHitRate after changing cache policy
{
"Name": "private-static-downloads",
"DefaultTTL": 86400,
"MaxTTL": 31536000,
"MinTTL": 0,
"ParametersInCacheKeyAndForwardedToOrigin": {
"EnableAcceptEncodingGzip": true,
"EnableAcceptEncodingBrotli": true,
"HeadersConfig": {
"HeaderBehavior": "none"
},
"CookiesConfig": {
"CookieBehavior": "none"
},
"QueryStringsConfig": {
"QueryStringBehavior": "none"
}
}
}
when query string really changes content:
/image.jpg?width=300
/image.jpg?width=600
cache policy:
Query strings = Include specified query strings
include:
width
result:
width=300 and width=600 are two valid cache objects
other tracking/auth query strings do not split the cache
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#
CloudFront 与源站是两道访问边界:只保护 CDN 不会自动封闭源站直连;按 S3 或 custom origin 类型配置源站访问控制。
{
"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
investigate TotalErrorRate alarm#
TotalErrorRate = 4xxErrorRate + 5xxErrorRate.
CloudWatch metric can tell whether errors are 4xx or 5xx.
CloudFront access logs are required to identify URL, status code, edge result type, and detailed error reason.
CloudFront metrics are in us-east-1 because CloudFront is a global service.
Check whether the spike is mostly 4xx or 5xx:
aws cloudwatch get-metric-statistics --region us-east-1 --namespace AWS/CloudFront --metric-name 4xxErrorRate --dimensions Name=DistributionId,Value=E123456EXAMPLE Name=Region,Value=Global --start-time "$(date -u -v-30M +%Y-%m-%dT%H:%M:%SZ)" --end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --period 300 --statistics Average
aws cloudwatch get-metric-statistics --region us-east-1 --namespace AWS/CloudFront --metric-name 5xxErrorRate --dimensions Name=DistributionId,Value=E123456EXAMPLE Name=Region,Value=Global --start-time "$(date -u -v-30M +%Y-%m-%dT%H:%M:%SZ)" --end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --period 300 --statistics Average
Key CloudFront log fields for error analysis:
| Field |
Use |
sc-status |
HTTP status returned to viewer, such as 403, 404, 502, 503, 504 |
x-edge-result-type |
High-level CloudFront result, such as Error, Miss, Hit, LimitExceeded |
x-edge-detailed-result-type |
Detailed CloudFront error reason |
cs-host |
Requested host |
cs-uri-stem / cs-uri-query |
Requested path and query |
cs-method |
Viewer method |
c-ip |
Viewer IP |
x-edge-request-id |
Request ID for correlation |
Athena query for S3 standard logs:
SELECT
date,
sc_status,
x_edge_result_type,
x_edge_detailed_result_type,
cs_method,
cs_host,
cs_uri_stem,
COUNT(*) AS error_count
FROM cloudfront_logs
WHERE distribution_id = 'E123456EXAMPLE'
AND date >= current_date - interval '1' day
AND sc_status >= 400
GROUP BY
date,
sc_status,
x_edge_result_type,
x_edge_detailed_result_type,
cs_method,
cs_host,
cs_uri_stem
ORDER BY error_count DESC
LIMIT 100;
CloudWatch Logs Insights query if standard logs v2 are delivered to CloudWatch Logs:
fields @timestamp, scStatus, xEdgeResultType, xEdgeDetailedResultType, csMethod, csHost, csUriStem, csUriQuery, cIp
| filter scStatus >= 400
| stats count(*) as errors by scStatus, xEdgeResultType, xEdgeDetailedResultType, csMethod, csHost, csUriStem
| sort errors desc
| limit 50
Interpretation:
4xx spike:
usually 403 / 404 / WAF block / signed URL or cookie issue / OAC or origin permission / wrong path.
5xx spike:
usually origin connection failure / origin timeout / ALB or ECS 5xx / TLS issue / Lambda@Edge or CloudFront Function issue.
no logs enabled:
historical per-URL error details are usually unavailable.
enable standard logs v2 for investigation and real-time logs for urgent live debugging.
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