AWS 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/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
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
expected:
    Verified OK

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
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

response headers policy sample#

{
  "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

custom origin secret header sample#

{
  "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