Tarek Cheikh
Founder & AWS Security Expert
Amazon CloudFront is AWS's global content delivery network (CDN), serving content from over 750 points of presence worldwide. It sits at the perimeter of your architecture, making it the first service that interacts with end-user requests. Because CloudFront is the front door to your applications, its security configuration directly determines your exposure to data exfiltration, unauthorized access, man-in-the-middle attacks, and application-layer DDoS.
Misconfigured CloudFront distributions are a common finding in penetration tests and cloud security assessments. Typical mistakes include allowing direct origin access that bypasses CDN-level protections, using outdated TLS protocols, missing WAF integration, serving content without security headers, and failing to restrict access to sensitive content with signed URLs or cookies. Each of these gaps gives attackers a viable path to exploit your infrastructure or intercept user data.
This guide covers 12 battle-tested CloudFront security best practices, each with real AWS CLI commands, audit procedures, and references to the latest Security Hub controls. Whether you are serving a static website from S3 or fronting a complex microservices architecture with VPC origins, these practices will harden your edge layer and reduce your attack surface.
Origin Access Control (OAC) ensures that your S3 bucket content is accessible only through your CloudFront distribution, not directly from the bucket URL. OAC replaced the legacy Origin Access Identity (OAI) and provides enhanced security through short-lived credentials, frequent credential rotation, and IAM service principal-based authentication. Without OAC, users can bypass your CloudFront distribution and access S3 objects directly, circumventing WAF rules, geo-restrictions, signed URL requirements, and access logging.
# Create an Origin Access Control
aws cloudfront create-origin-access-control --origin-access-control-config Name=my-s3-oac,Description="OAC for S3 origin",SigningProtocol=sigv4,SigningBehavior=always,OriginAccessControlOriginType=s3
# Get the OAC ID from the response, then update your distribution config
# First, get the current distribution config
aws cloudfront get-distribution-config --id E1EXAMPLE --output json > dist-config.json
# After editing the config to add the OAC ID to the S3 origin and removing
# any OAI reference, update the distribution:
aws cloudfront update-distribution --id E1EXAMPLE --distribution-config file://dist-config.json --if-match ETAG_VALUE
You must also update your S3 bucket policy to grant the CloudFront service principal access:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipalReadOnly",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/E1EXAMPLE"
}
}
}
]
}
# List all distributions and check for OAC vs OAI usage
aws cloudfront list-distributions --query "DistributionList.Items[*].{Id:Id,Origins:Origins.Items[*].{DomainName:DomainName,OACId:OriginAccessControlId,OAI:S3OriginConfig.OriginAccessIdentity}}" --output table
Security Hub Control: [CloudFront.13] CloudFront distributions should use origin access control.
CloudFront supports multiple TLS security policies that define the minimum TLS version and cipher suites for viewer connections. Using outdated TLS versions (1.0, 1.1) exposes your distribution to known vulnerabilities such as BEAST and POODLE. AWS recommends TLSv1.2_2021 as a minimum, and TLSv1.3_2025 is available for maximum security. TLS 1.3 offers faster handshakes, improved privacy, and removal of legacy cryptographic primitives.
# Update distribution to enforce TLS 1.2 minimum with a custom SSL certificate
aws cloudfront update-distribution --id E1EXAMPLE --distribution-config file://dist-config.json --if-match ETAG_VALUE
# The ViewerCertificate section in dist-config.json should contain:
# "ViewerCertificate": {
# "ACMCertificateArn": "arn:aws:acm:us-east-1:123456789012:certificate/CERT-ID",
# "SSLSupportMethod": "sni-only",
# "MinimumProtocolVersion": "TLSv1.2_2021"
# }
# Check TLS policy on all distributions
aws cloudfront list-distributions --query "DistributionList.Items[*].{Id:Id,DomainName:DomainName,MinProtocol:ViewerCertificate.MinimumProtocolVersion,SSLMethod:ViewerCertificate.SSLSupportMethod}" --output table
Security Hub Controls: [CloudFront.3] CloudFront distributions should require encryption in transit. [CloudFront.7] CloudFront distributions should use custom SSL/TLS certificates. [CloudFront.8] CloudFront distributions should use SNI to serve HTTPS requests. [CloudFront.10] CloudFront distributions should not use deprecated SSL protocols.
CloudFront supports three viewer protocol policies: HTTP and HTTPS, Redirect HTTP to HTTPS, and HTTPS Only. Allowing plain HTTP requests exposes user data in transit to eavesdropping and man-in-the-middle attacks. Additionally, the origin protocol policy controls how CloudFront communicates with your origin. For custom origins, always enforce HTTPS-only origin connections to prevent data interception between CloudFront and your origin.
# In your distribution config, set the viewer protocol policy per cache behavior:
# "DefaultCacheBehavior": {
# "ViewerProtocolPolicy": "redirect-to-https"
# }
# For stricter environments, use "https-only" instead
# For custom origins, enforce HTTPS to origin:
# "CustomOriginConfig": {
# "OriginProtocolPolicy": "https-only",
# "OriginSslProtocols": {
# "Quantity": 1,
# "Items": ["TLSv1.2"]
# }
# }
# Verify viewer protocol policy across all distributions
aws cloudfront list-distributions --query "DistributionList.Items[*].{Id:Id,ViewerProtocol:DefaultCacheBehavior.ViewerProtocolPolicy}" --output table
Security Hub Controls: [CloudFront.3] CloudFront distributions should require encryption in transit. [CloudFront.9] CloudFront distributions should encrypt traffic to custom origins.
AWS WAF integration at the CloudFront layer inspects every incoming request before it reaches your origin, providing protection against SQL injection, cross-site scripting (XSS), HTTP floods, bot traffic, and OWASP Top 10 vulnerabilities. Without WAF, your distributions forward all traffic directly to your origin with no application-layer filtering. CloudFront also supports one-click WAF security protections through the console, which automatically creates a web ACL with AWS Managed Rules.
# CloudFront web ACLs must be created in us-east-1
aws wafv2 create-web-acl --name "cloudfront-web-acl" --scope CLOUDFRONT --region us-east-1 --default-action Allow={} --visibility-config SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=CloudFrontWebACL --rules file://waf-rules.json
# Associate the web ACL with a CloudFront distribution
# Add the WebACLId to the distribution config:
# "WebACLId": "arn:aws:wafv2:us-east-1:123456789012:global/webacl/cloudfront-web-acl/EXAMPLE-ID"
aws cloudfront update-distribution --id E1EXAMPLE --distribution-config file://dist-config.json --if-match ETAG_VALUE
# Find distributions without WAF
aws cloudfront list-distributions --query "DistributionList.Items[?WebACLId==''].{Id:Id,DomainName:DomainName,Aliases:Aliases.Items[0]}" --output table
Security Hub Control: [CloudFront.6] CloudFront distributions should have WAF enabled.
HTTP security response headers instruct browsers to enforce security policies that prevent common web attacks such as clickjacking, MIME-type sniffing, and cross-site scripting. CloudFront response headers policies let you add these headers at the edge without modifying your origin application. AWS provides managed response headers policies, including the SecurityHeadersPolicy, which adds Strict-Transport-Security (HSTS), X-Content-Type-Options, X-Frame-Options, X-XSS-Protection, Referrer-Policy, and Content-Security-Policy headers.
# Create a custom response headers policy with security headers
aws cloudfront create-response-headers-policy --response-headers-policy-config '{
"Name": "security-headers-policy",
"Comment": "Security headers for all distributions",
"SecurityHeadersConfig": {
"StrictTransportSecurity": {
"Override": true,
"AccessControlMaxAgeSec": 63072000,
"IncludeSubdomains": true,
"Preload": true
},
"ContentTypeOptions": {
"Override": true
},
"FrameOptions": {
"Override": true,
"FrameOption": "DENY"
},
"XSSProtection": {
"Override": true,
"Protection": true,
"ModeBlock": true
},
"ReferrerPolicy": {
"Override": true,
"ReferrerPolicy": "strict-origin-when-cross-origin"
},
"ContentSecurityPolicy": {
"Override": true,
"ContentSecurityPolicy": "default-src '"'"'self'"'"'; script-src '"'"'self'"'"'; style-src '"'"'self'"'"' '"'"'unsafe-inline'"'"'"
}
}
}'
# Or use the AWS managed SecurityHeadersPolicy
# Managed policy ID: 67f7725c-6f97-4210-82d7-5512b31e9d03
# Attach the policy to a cache behavior in your distribution config:
# "DefaultCacheBehavior": {
# "ResponseHeadersPolicyId": "67f7725c-6f97-4210-82d7-5512b31e9d03"
# }
# Check which distributions have response headers policies
aws cloudfront list-distributions --query "DistributionList.Items[*].{Id:Id,ResponseHeadersPolicy:DefaultCacheBehavior.ResponseHeadersPolicyId}" --output table
# Verify headers are being served correctly
curl -sI https://your-distribution.cloudfront.net/ | grep -iE "strict-transport|x-frame|x-content-type|x-xss|referrer-policy|content-security"
Missing security headers is one of the most common CloudFront misconfigurations. Without HSTS, browsers may allow initial HTTP connections before redirecting, creating a window for man-in-the-middle attacks. Without X-Frame-Options, your site is vulnerable to clickjacking.
Signed URLs and signed cookies restrict access to your CloudFront content to authorized users. Signed URLs are best for individual file access, while signed cookies work well for granting access to multiple restricted files without changing URLs. AWS recommends using trusted key groups instead of the legacy CloudFront root account key pairs. Trusted key groups use CloudFront API-managed public keys, enabling key rotation and fine-grained IAM access control without requiring root account credentials.
# Step 1: Create a key pair (on your local machine)
openssl genrsa -out private_key.pem 2048
openssl rsa -pubout -in private_key.pem -out public_key.pem
# Step 2: Upload the public key to CloudFront
aws cloudfront create-public-key --public-key-config '{
"CallerReference": "my-key-2026",
"Name": "my-signing-key",
"EncodedKey": "'"$(cat public_key.pem)"'"
}'
# Step 3: Create a key group with the public key
aws cloudfront create-key-group --key-group-config '{
"Name": "my-key-group",
"Items": ["K1EXAMPLE"],
"Comment": "Key group for signed URLs"
}'
# Step 4: Configure the distribution cache behavior to require signed URLs
# "DefaultCacheBehavior": {
# "TrustedKeyGroups": {
# "Enabled": true,
# "Quantity": 1,
# "Items": ["KEYGROUP-ID"]
# }
# }
# List key groups to verify they exist and are active
aws cloudfront list-key-groups --output table
# Check distributions using trusted key groups
aws cloudfront list-distributions --query "DistributionList.Items[*].{Id:Id,TrustedKeyGroups:DefaultCacheBehavior.TrustedKeyGroups}" --output json
Security Hub Control: [CloudFront.17] CloudFront distributions should use trusted key groups for signed URLs and cookies (not legacy trusted signers).
CloudFront standard access logs (formerly called access logs) record every viewer request to your distributions, including the client IP, request URI, HTTP status code, edge location, and bytes transferred. These logs are essential for security incident investigation, traffic analysis, abuse detection, and compliance audits. Without logging, you have no forensic trail when a security incident occurs.
# Create an S3 bucket for CloudFront logs (must allow CloudFront to write logs)
aws s3 mb s3://my-cloudfront-logs-123456789012
# Set the bucket ACL to allow CloudFront log delivery
aws s3api put-bucket-acl --bucket my-cloudfront-logs-123456789012 --grant-full-control id=c4c1ede66af53448b93c283ce9448c4ba468c9432aa01d700d3878632f77d2d0
# Enable logging in the distribution config:
# "Logging": {
# "Enabled": true,
# "IncludeCookies": false,
# "Bucket": "my-cloudfront-logs-123456789012.s3.amazonaws.com",
# "Prefix": "cf-logs/"
# }
aws cloudfront update-distribution --id E1EXAMPLE --distribution-config file://dist-config.json --if-match ETAG_VALUE
# Check which distributions have logging enabled
aws cloudfront list-distributions --query "DistributionList.Items[*].{Id:Id,LoggingEnabled:Logging.Enabled,LogBucket:Logging.Bucket}" --output table
Security Hub Control: [CloudFront.5] CloudFront distributions should have logging enabled. For enhanced visibility, consider enabling CloudFront real-time logs, which deliver log data to Amazon Kinesis Data Streams within seconds of a viewer request.
When a viewer requests the root URL of your distribution (e.g., https://d111111abcdef8.cloudfront.net/), CloudFront can return a specific object called the default root object. Without a default root object configured, a request to the root URL returns a listing of your S3 bucket contents (if listing is enabled) or an Access Denied error. Exposing the directory listing can reveal sensitive file names and internal structure to attackers performing reconnaissance.
# Set the default root object in the distribution config:
# "DefaultRootObject": "index.html"
# You can also update it via CLI by modifying the distribution config
aws cloudfront get-distribution-config --id E1EXAMPLE --output json > dist-config.json
# Edit dist-config.json to set "DefaultRootObject": "index.html"
aws cloudfront update-distribution --id E1EXAMPLE --distribution-config file://dist-config.json --if-match ETAG_VALUE
# Find distributions missing a default root object
aws cloudfront list-distributions --query "DistributionList.Items[?DefaultRootObject==''].{Id:Id,DomainName:DomainName}" --output table
Security Hub Control: [CloudFront.1] CloudFront distributions should have a default root object configured.
CloudFront origin groups let you configure primary and secondary origins. If the primary origin returns specific HTTP status codes (such as 500, 502, 503, or 504), CloudFront automatically fails over to the secondary origin. This prevents service disruptions and reduces the risk of denial-of-service scenarios where an attacker targets your origin directly. Origin failover also mitigates the impact of regional outages on your content delivery.
# Configure an origin group in your distribution config:
# "OriginGroups": {
# "Quantity": 1,
# "Items": [{
# "Id": "my-origin-group",
# "FailoverCriteria": {
# "StatusCodes": {
# "Quantity": 4,
# "Items": [500, 502, 503, 504]
# }
# },
# "Members": {
# "Quantity": 2,
# "Items": [
# { "OriginId": "primary-origin" },
# { "OriginId": "failover-origin" }
# ]
# }
# }]
# }
# Then reference the origin group in your cache behavior:
# "DefaultCacheBehavior": {
# "TargetOriginId": "my-origin-group"
# }
# Check which distributions have origin failover configured
aws cloudfront list-distributions --query "DistributionList.Items[*].{Id:Id,OriginGroupCount:OriginGroups.Quantity}" --output table
Security Hub Control: [CloudFront.4] CloudFront distributions should have origin failover configured.
CloudFront geo-restriction (geoblocking) allows you to block or allow requests based on the viewer's country of origin. This is useful for compliance with export controls, content licensing agreements, and reducing attack surface from regions where you have no legitimate users. CloudFront uses a GeoIP database to determine the viewer's location from their IP address. For more granular control (e.g., blocking at the region or city level, or combining geographic rules with other conditions), use AWS WAF geo-match rules instead of CloudFront's built-in feature.
# Configure geo-restriction in the distribution config to use an allowlist:
# "Restrictions": {
# "GeoRestriction": {
# "RestrictionType": "whitelist",
# "Quantity": 3,
# "Items": ["US", "CA", "GB"]
# }
# }
# Or use a blocklist to deny specific countries:
# "Restrictions": {
# "GeoRestriction": {
# "RestrictionType": "blacklist",
# "Quantity": 2,
# "Items": ["KP", "IR"]
# }
# }
aws cloudfront update-distribution --id E1EXAMPLE --distribution-config file://dist-config.json --if-match ETAG_VALUE
# Check geo-restriction settings across distributions
aws cloudfront list-distributions --query "DistributionList.Items[*].{Id:Id,GeoRestriction:Restrictions.GeoRestriction.RestrictionType,Countries:Restrictions.GeoRestriction.Items}" --output table
Important: CloudFront geo-restriction does not forward blocked requests to AWS WAF. If you need to combine geographic blocking with other WAF rules, use the WAF geo-match statement instead and disable CloudFront's built-in geo-restriction to avoid conflicts.
CloudFront VPC origins, introduced in late 2024, allow you to use private resources inside your VPC as CloudFront origins without exposing them to the public internet. This means your Application Load Balancers, EC2 instances, and other backend resources can remain in private subnets with no public IP address or internet gateway. All traffic must flow through CloudFront, which enforces WAF rules, TLS, and other edge protections before forwarding requests to your private origin over the AWS network.
# Create a VPC origin for a private ALB
aws cloudfront create-vpc-origin --vpc-origin-endpoint-config '{
"Name": "my-private-alb",
"Arn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-private-alb/EXAMPLE",
"HTTPPort": 80,
"HTTPSPort": 443,
"OriginProtocolPolicy": "https-only",
"OriginSslProtocols": {
"Quantity": 1,
"Items": ["TLSv1.2"]
}
}'
# Then configure the VPC origin in your distribution config and update the distribution
VPC origins eliminate the need for complex workarounds such as custom headers or IP allowlists to prevent direct origin access. By keeping your origin entirely private, you significantly reduce the attack surface and ensure all user traffic is inspected at the edge.
CloudFront distributions are automatically protected by AWS Shield Standard, which defends against most common Layer 3 and Layer 4 DDoS attacks at no additional cost. For mission-critical applications, AWS Shield Advanced provides enhanced DDoS detection, near real-time attack visibility, access to the AWS Shield Response Team (SRT), cost protection for DDoS-related scaling, and automatic application-layer DDoS mitigation when combined with AWS WAF.
# Subscribe to Shield Advanced (account-level, requires Business or Enterprise Support)
aws shield create-subscription
# Add CloudFront distribution as a protected resource
aws shield create-protection --name "cloudfront-production" --resource-arn arn:aws:cloudfront::123456789012:distribution/E1EXAMPLE
# Enable application-layer automatic mitigation (requires WAF association)
aws shield enable-application-layer-automatic-response --resource-arn arn:aws:cloudfront::123456789012:distribution/E1EXAMPLE --action Block={}
# List all Shield Advanced protections
aws shield list-protections --query "Protections[?ResourceArn!=\`\`].{Name:Name,ResourceArn:ResourceArn}" --output table
# Check if automatic application-layer mitigation is enabled
aws shield describe-protection --resource-arn arn:aws:cloudfront::123456789012:distribution/E1EXAMPLE --query "Protection.ApplicationLayerAutomaticResponseConfiguration"
Shield Advanced costs $3,000 per month per account (with a 1-year commitment), but provides DDoS cost protection that credits you for scaling charges caused by DDoS attacks, which can far exceed the subscription cost during a major attack.
The following misconfigurations are frequently discovered during security assessments of CloudFront distributions:
| # | Best Practice | Priority |
|---|---|---|
| 1 | Use Origin Access Control (OAC) for S3 origins | Critical |
| 2 | Enforce TLS 1.2 or 1.3 minimum protocol version | Critical |
| 3 | Require HTTPS-only communication (viewer and origin) | Critical |
| 4 | Associate AWS WAF web ACLs with all distributions | Critical |
| 5 | Configure security response headers (HSTS, CSP, X-Frame-Options) | High |
| 6 | Use signed URLs or signed cookies for private content | High |
| 7 | Enable access logging (standard and/or real-time) | High |
| 8 | Configure a default root object | Medium |
| 9 | Enable origin failover for high availability | Medium |
| 10 | Implement geo-restriction for compliance and attack surface reduction | Medium |
| 11 | Use VPC origins for private backend protection | High |
| 12 | Enable AWS Shield Advanced for DDoS protection | High |
This article is just the start. Get the full picture with our free whitepaper - 8 chapters covering IAM, S3, VPC, monitoring, agentic AI security, compliance, and a prioritized action plan with 50+ CLI commands.
Comprehensive guide to securing web applications with AWS WAF. Covers managed rules, Bot Control, Fraud Control (ATP/ACFP), CAPTCHA/Challenge actions, rate-based rules, Shield Advanced integration, and centralized WAF management with Firewall Manager.
Comprehensive guide to securing Amazon S3. Covers Block Public Access, encryption (SSE-KMS, SSE-C deprecation), Object Lock, MFA Delete, VPC endpoints, presigned URLs, and GuardDuty S3 Protection.
Comprehensive guide to securing Amazon API Gateway. Covers authentication with Cognito and Lambda authorizers, mutual TLS, WAF integration, resource policies, throttling and usage plans, private APIs with VPC endpoints, access logging, SSL/TLS enforcement, and request validation.