Subchapter 78.5
references/securing-your-content.mdMarkdown15 KBView on GitHub
Domain expertise for limiting who can view content served through CloudFront, using four controls that answer different questions:
All four only hold if the origin cannot be reached directly. Pair every content control with origin locking (see protecting-your-origins).
Does not cover locking the origin (see protecting-your-origins), custom domains (see managing-certificates-with-cloudfront), or creating the distribution (see when-to-use-cloudfront).
Execute commands using the AWS MCP server when connected (sandboxed execution, audit logging, observability). Fall back to the AWS CLI otherwise.
| Question | Control |
|---|---|
| Is this viewer authorized (paying, licensed)? | Signed URLs or signed cookies |
| Is this viewer in an allowed country? | Geographic restrictions |
| Does this client hold a valid certificate? | Viewer mutual TLS |
| Does this request carry a valid auth token? | CloudFront function on viewer-request |
Constraints:
| Choice | Use when |
|---|---|
| Signed URLs | A single file, or a client without cookie support. Signature, policy, and expiration go in query-string parameters |
| Signed cookies | Many files, or no change to the URL is acceptable. Same data goes in cookies; the URL is unchanged |
Constraints:
Constraints:
Constraints:
Constraints:
A content control only holds if access is forced through CloudFront. While the origin URL is directly reachable, viewers bypass the control entirely.
Constraints:
This procedure selects the content control by question, applies it, pairs it with origin locking, and surfaces the console link.
signed, geo, viewer-mtls, or token.Constraints for parameter acquisition:
Constraints:
aws sts get-caller-identityConstraints:
For identity, you MUST configure a trusted key group and have the application issue signed URLs or
cookies. Create the key group, then reference it from the cache behavior’s TrustedKeyGroups:
aws cloudfront create-key-group --key-group-config \
'{"Name":"paid-users","Items":["{publicKeyId}"]}'For location, you MUST set the geographic restriction allowlist or denylist on the distribution by
updating its config (the Restrictions.GeoRestriction block). Fetch the current config and ETag,
set the Restrictions.GeoRestriction block, then pass the full modified config back as an inline
JSON string (use an inline JSON string, not a file:// reference, for portability across
execution environments):
aws cloudfront get-distribution-config --id {distribution_id}
# set Restrictions.GeoRestriction in the returned DistributionConfig, then put it back.
# RestrictionType is an AWS API enum; its only allowed values are "whitelist" (allowlist),
# "blacklist" (denylist), and "none" — use the API's exact token here for the allowlist case:
aws cloudfront update-distribution --id {distribution_id} --if-match {etag} \
--distribution-config '{"CallerReference":"...","Restrictions":{"GeoRestriction":{"RestrictionType":"whitelist","Quantity":1,"Items":["US"]}}, ...rest of the unchanged config...}'For client certificate, you MUST create the trust store, disable HTTP/3, set all behaviors to HTTPS, and enable viewer mutual authentication in required mode. Create the trust store from the PEM CA bundle in S3:
aws cloudfront create-trust-store --name {name} \
--s3-bucket {bucket} --s3-object-key {ca-bundle.pem}For token, you MUST write a CloudFront function on viewer-request that validates the token, then publish and associate it.
⚠️ You MUST implement real signature verification before publishing. A function that only checks
token presence and length does not verify authenticity — any arbitrary string passes, which is a
false sense of security. The validation logic below is described as steps, not as runnable code, so
it is not deployed as-is: the function MUST (a) read the bearer token from the authorization
header, (b) reject a missing or over-length token with a 401, and (c) verify the token’s
signature (for example, validate the JWT signature with crypto.subtle in a CloudFront Functions
runtime that supports it) before returning event.request. Author the function code to do all three.
Create the function with your authored, signature-verifying code (replace {function_code} with
it), and set Runtime to the current CloudFront Functions runtime that supports your code — do not
hardcode a runtime string that ages; check the available runtimes in the CloudFront Developer Guide
(CloudFront Functions) and select the latest supported one for {runtime}:
aws cloudfront create-function --name {name} \
--function-config '{"Comment":"token-check","Runtime":"{runtime}"}' \
--function-code '{function_code}'Only after the function performs signature verification, publish and associate it:
aws cloudfront publish-function --name {name} --if-match {etag}You MUST also attach a response headers policy that adds browser security headers (HSTS,
Content-Security-Policy, X-Frame-Options, X-Content-Type-Options) to complement these access
controls, since they defend against different browser-based attacks (clickjacking, MIME sniffing,
protocol downgrade). The AWS managed SecurityHeadersPolicy is a starting point. Look up its
current id by name (do not hardcode a UUID — managed policy ids can change), fetch the current
config and ETag, set the behavior’s ResponseHeadersPolicyId, then pass the full modified config
back as an inline JSON string (use an inline JSON string, not a file:// reference, for
portability across execution environments):
# resolve the managed SecurityHeadersPolicy id by name (do not hardcode a UUID):
aws cloudfront list-response-headers-policies --type managed \
--query "ResponseHeadersPolicyList.Items[?ResponseHeadersPolicy.ResponseHeadersPolicyConfig.Name=='Managed-SecurityHeadersPolicy'].ResponseHeadersPolicy.Id | [0]" --output text
aws cloudfront get-distribution-config --id {distribution_id}
# set DefaultCacheBehavior.ResponseHeadersPolicyId to the resolved managed SecurityHeadersPolicy id
# in the returned DistributionConfig, then put it back:
aws cloudfront update-distribution --id {distribution_id} --if-match {etag} \
--distribution-config '{"CallerReference":"...","DefaultCacheBehavior":{"ResponseHeadersPolicyId":"{security_headers_policy_id}", ...}, ...rest of the unchanged config...}'Constraints:
You MUST confirm the origin cannot be reached directly before calling the control complete
You MUST present the distribution detail console link, filling {distributionId} from the input
distribution_id parameter:
https://us-east-1.console.aws.amazon.com/cloudfront/v4/home?region=us-east-1#/distributions/{distributionId}{
"distribution_id": "E1ABCDEF2GHIJK",
"control": "signed",
"gate_detail": "trusted-key-group: paid-users"
}Configured trusted key group paid-users on E1ABCDEF2GHIJK for signed URLs.
Origin locked with origin access control so the signing cannot be bypassed.
Verify in the console:
https://us-east-1.console.aws.amazon.com/cloudfront/v4/home?region=us-east-1#/distributions/E1ABCDEF2GHIJKThe origin is directly reachable, so signing is bypassed. Lock the origin (see protecting-your-origins).
Geographic restrictions apply to the whole distribution at the country level. Use separate distributions or a third-party geolocation approach for finer control.
HTTP/3 is enabled or a cache behavior allows HTTP. Disable HTTP/3 and set every behavior to HTTPS, then enable mTLS.
The validation mode is optional or passthrough. Use required mode to reject clients without a valid certificate.
The function was not published to the LIVE stage. Publish it, then associate it.
cloudfront:CreateFunction,
cloudfront:PublishFunction, cloudfront:UpdateDistribution) for a compliance and forensic audit
trail.