Networking & Content DeliveryIntermediate15 min read

    Amazon CloudFront Security Best Practices

    Tarek Cheikh

    Founder & AWS Security Expert

    View Security Card

    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.

    1. Use Origin Access Control (OAC) for S3 Origins

    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.

    Implementation

    # 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"
            }
          }
        }
      ]
    }

    Audit

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

    2. Enforce TLS 1.2 or 1.3 Minimum Protocol Version

    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.

    Implementation

    # 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"
    # }

    Audit

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

    3. Require HTTPS-Only Communication

    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.

    Implementation

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

    4. Associate AWS WAF Web ACLs with All Distributions

    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.

    Implementation

    # 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

    Audit

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

    5. Configure Security Response Headers

    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.

    Implementation

    # 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"
    # }

    Audit

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

    6. Use Signed URLs or Signed Cookies for Private Content

    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.

    Implementation

    # 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"]
    #   }
    # }

    Audit

    # 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).

    7. Enable Access Logging

    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.

    Implementation

    # 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

    Audit

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

    8. Configure a Default Root Object

    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.

    Implementation

    # 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

    Audit

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

    9. Enable Origin Failover for High Availability

    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.

    Implementation

    # 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"
    # }

    Audit

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

    10. Implement Geo-Restriction

    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.

    Implementation

    # 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

    Audit

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

    11. Use VPC Origins for Private Backend Protection

    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.

    Implementation

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

    12. Enable AWS Shield Advanced for DDoS Protection

    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.

    Implementation

    # 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={}

    Audit

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

    Common Misconfigurations

    The following misconfigurations are frequently discovered during security assessments of CloudFront distributions:

    • Using OAI instead of OAC: Origin Access Identity is a legacy mechanism that does not support SSE-KMS encrypted S3 objects, post-December 2022 regions, or IAM service principal authentication. Migrate all OAI configurations to OAC.
    • Default CloudFront certificate with TLSv1: When using the default *.cloudfront.net domain name, CloudFront sets the minimum protocol version to TLSv1, which supports insecure TLS 1.0 connections. Always use a custom SSL certificate with a modern TLS security policy.
    • Missing WAF on production distributions: Distributions without a web ACL forward all traffic (including malicious requests) directly to your origin. Every internet-facing distribution should have an AWS WAF web ACL.
    • HTTP allowed as viewer protocol: Allowing HTTP connections exposes data in transit. Use "redirect-to-https" or "https-only" for all cache behaviors.
    • S3 bucket publicly accessible alongside CloudFront: Even with CloudFront in front of an S3 bucket, if the bucket itself has public access enabled, attackers can bypass CloudFront entirely. Use OAC combined with S3 Block Public Access.
    • No access logging: Without standard or real-time logs, you cannot investigate security incidents or detect abuse patterns. Enable logging on all distributions and send logs to a centralized security bucket.
    • Overly permissive CORS headers: Setting Access-Control-Allow-Origin to * in response headers policies can enable cross-origin attacks. Restrict CORS to specific trusted domains.
    • Legacy trusted signers instead of key groups: Using CloudFront root account key pairs for signed URLs requires root account access and cannot be managed via IAM. Migrate to trusted key groups.
    • Caching sensitive data at the edge: Credit card numbers, personally identifiable information (PII), and authentication tokens should not be cached. Use Cache-Control: no-store headers for sensitive responses and configure appropriate TTLs.
    • No default root object: Missing default root object can expose S3 bucket listings or return confusing errors to users, aiding attacker reconnaissance.

    Quick Reference Checklist

    #Best PracticePriority
    1Use Origin Access Control (OAC) for S3 originsCritical
    2Enforce TLS 1.2 or 1.3 minimum protocol versionCritical
    3Require HTTPS-only communication (viewer and origin)Critical
    4Associate AWS WAF web ACLs with all distributionsCritical
    5Configure security response headers (HSTS, CSP, X-Frame-Options)High
    6Use signed URLs or signed cookies for private contentHigh
    7Enable access logging (standard and/or real-time)High
    8Configure a default root objectMedium
    9Enable origin failover for high availabilityMedium
    10Implement geo-restriction for compliance and attack surface reductionMedium
    11Use VPC origins for private backend protectionHigh
    12Enable AWS Shield Advanced for DDoS protectionHigh

    Related Resources

    Go Deeper: The State of AWS Security 2026

    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.

    CloudFrontCDNTLSOACWAFDDoSShieldSigned URLsSecurity HeadersEdge SecurityGeo-Restriction