CloudFront Security Headers: Protect Your Website in 5 Minutes

Implement essential security headers with AWS CloudFront Functions. Learn how to add HSTS, CSP, X-Frame-Options, and more to protect your users from common web vulnerabilities.

Your website might be vulnerable to XSS, clickjacking, and other attacks—all because you're missing a few HTTP headers. Let's fix that with CloudFront Functions.

Why Security Headers Matter

Without proper security headers, your users are exposed to:

  • Cross-Site Scripting (XSS) - Malicious scripts injected into your pages
  • Clickjacking - Your site embedded in malicious iframes
  • MIME-type sniffing - Browsers executing malicious content
  • Man-in-the-middle attacks - HTTP connections intercepted

Security headers are your first line of defense, and they're free.

The Essential Security Headers

1. Strict-Transport-Security (HSTS)

Forces browsers to use HTTPS only.

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

What it does: Once a browser sees this header, it will ONLY connect via HTTPS for the next year (31536000 seconds).

2. Content-Security-Policy (CSP)

Controls what resources can be loaded.

Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://api.example.com

What it does: Prevents XSS by whitelisting allowed content sources.

3. X-Frame-Options

Prevents clickjacking.

X-Frame-Options: DENY

What it does: Prevents your site from being embedded in an iframe.

4. X-Content-Type-Options

Prevents MIME-type sniffing.

X-Content-Type-Options: nosniff

What it does: Forces browsers to respect declared content types.

5. Referrer-Policy

Controls referrer information.

Referrer-Policy: strict-origin-when-cross-origin

What it does: Limits what referrer information is sent with requests.

6. Permissions-Policy

Controls browser features.

Permissions-Policy: geolocation=(), microphone=(), camera=()

What it does: Disables sensitive browser APIs you don't use.

Implementation: CloudFront Functions

CloudFront Functions run on CloudFront's edge locations with sub-millisecond latency—perfect for adding headers.

Complete Security Headers Function

function handler(event) {
    var response = event.response;
    var headers = response.headers;

    // Strict-Transport-Security
    headers['strict-transport-security'] = { 
        value: 'max-age=31536000; includeSubDomains; preload' 
    };

    // Content-Security-Policy
    // Adjust this based on your needs!
    headers['content-security-policy'] = { 
        value: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'" 
    };

    // X-Frame-Options
    headers['x-frame-options'] = { 
        value: 'DENY' 
    };

    // X-Content-Type-Options
    headers['x-content-type-options'] = { 
        value: 'nosniff' 
    };

    // Referrer-Policy
    headers['referrer-policy'] = { 
        value: 'strict-origin-when-cross-origin' 
    };

    // Permissions-Policy
    headers['permissions-policy'] = { 
        value: 'geolocation=(), microphone=(), camera=()' 
    };

    // X-XSS-Protection (legacy, but doesn't hurt)
    headers['x-xss-protection'] = { 
        value: '1; mode=block' 
    };

    return response;
}

Deployment Steps

1. Create the Function in AWS Console

# Via AWS Console:
# 1. Go to CloudFront → Functions
# 2. Click "Create function"
# 3. Name it "security-headers"
# 4. Paste the code above
# 5. Click "Save"

2. Test the Function

# In the CloudFront Functions console:
# 1. Click "Test" tab
# 2. Use this test event:
{
    "version": "1.0",
    "context": {
        "eventType": "viewer-response"
    },
    "viewer": {
        "ip": "1.2.3.4"
    },
    "request": {
        "method": "GET",
        "uri": "/index.html",
        "headers": {}
    },
    "response": {
        "statusCode": 200,
        "headers": {
            "content-type": {
                "value": "text/html"
            }
        }
    }
}

3. Publish the Function

# In the console:
# 1. Click "Publish" tab
# 2. Review the code
# 3. Click "Publish function"

4. Associate with CloudFront Distribution

# Via Console:
# 1. Go to CloudFront → Distributions
# 2. Select your distribution
# 3. Click "Behaviors" tab
# 4. Edit the default behavior
# 5. Scroll to "Function associations"
# 6. Under "Viewer response":
#    - Function type: CloudFront Functions
#    - Function ARN: Select "security-headers"
# 7. Save changes

Infrastructure as Code

Using AWS CDK (Python)

from aws_cdk import (
    Stack,
    aws_cloudfront as cloudfront,
    aws_cloudfront_origins as origins,
    aws_s3 as s3,
)

class MyStack(Stack):
    def __init__(self, scope, construct_id, **kwargs):
        super().__init__(scope, construct_id, **kwargs)

        # Create CloudFront Function
        security_headers_function = cloudfront.Function(
            self, "SecurityHeaders",
            code=cloudfront.FunctionCode.from_file(
                file_path="cloudfront-functions/security-headers.js"
            ),
            comment="Add security headers to all responses"
        )

        # S3 bucket for website
        bucket = s3.Bucket(self, "WebsiteBucket")

        # CloudFront distribution with function
        distribution = cloudfront.Distribution(
            self, "Distribution",
            default_behavior=cloudfront.BehaviorOptions(
                origin=origins.S3Origin(bucket),
                viewer_protocol_policy=cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
                function_associations=[
                    cloudfront.FunctionAssociation(
                        function=security_headers_function,
                        event_type=cloudfront.FunctionEventType.VIEWER_RESPONSE
                    )
                ]
            )
        )

Using CloudFormation/SAM

Resources:
  SecurityHeadersFunction:
    Type: AWS::CloudFront::Function
    Properties:
      Name: security-headers
      AutoPublish: true
      FunctionCode: |
        function handler(event) {
          var response = event.response;
          var headers = response.headers;
          
          headers['strict-transport-security'] = { 
            value: 'max-age=31536000; includeSubDomains; preload' 
          };
          headers['content-security-policy'] = { 
            value: "default-src 'self'; script-src 'self' 'unsafe-inline'" 
          };
          headers['x-frame-options'] = { value: 'DENY' };
          headers['x-content-type-options'] = { value: 'nosniff' };
          
          return response;
        }
      FunctionConfig:
        Comment: Add security headers
        Runtime: cloudfront-js-1.0

  MyDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        DefaultCacheBehavior:
          TargetOriginId: S3Origin
          ViewerProtocolPolicy: redirect-to-https
          FunctionAssociations:
            - EventType: viewer-response
              FunctionARN: !GetAtt SecurityHeadersFunction.FunctionARN

Testing Your Implementation

Using curl

# Test your site
curl -I https://yourdomain.com

# Look for the security headers in the response
# You should see all the headers we added

Using Online Tools

  1. Security Headers

    • Enter your URL
    • Get a letter grade (A+ is the goal)
    • See which headers are missing
  2. Mozilla Observatory

    • Comprehensive security scan
    • Includes headers and more
  3. SSL Labs

    • Test SSL/TLS configuration
    • Includes HSTS testing

Content Security Policy: Going Deeper

CSP is the most complex security header. Let's break it down:

Basic CSP

// Allow only same-origin resources
"default-src 'self'"

Allowing CDNs

// Allow specific CDNs for scripts
"default-src 'self'; script-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com"

Allowing Inline Scripts (Use Sparingly!)

// Allow inline scripts (not recommended)
"script-src 'self' 'unsafe-inline'"

// Better: Use nonces
"script-src 'self' 'nonce-random123abc'"

// Then in HTML:
<script nonce="random123abc">
  console.log('This script is allowed');
</script>

CSP for Single Page Applications

// Typical React/Vue/Angular CSP
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://api.example.com"

CSP Report-Only Mode

Test CSP without breaking your site:

headers['content-security-policy-report-only'] = { 
    value: "default-src 'self'; report-uri https://yourdomain.com/csp-report" 
};

Advanced: Dynamic Headers Based on Path

function handler(event) {
    var response = event.response;
    var headers = response.headers;
    var request = event.request;
    
    // Add security headers to all responses
    headers['x-content-type-options'] = { value: 'nosniff' };
    headers['x-frame-options'] = { value: 'DENY' };
    
    // Different CSP for API vs website
    if (request.uri.startsWith('/api/')) {
        // API: strict CSP
        headers['content-security-policy'] = { 
            value: "default-src 'none'" 
        };
    } else {
        // Website: allow necessary resources
        headers['content-security-policy'] = { 
            value: "default-src 'self'; script-src 'self' 'unsafe-inline'" 
        };
    }
    
    // HSTS for all
    headers['strict-transport-security'] = { 
        value: 'max-age=31536000; includeSubDomains; preload' 
    };
    
    return response;
}

Common Mistakes to Avoid

1. CSP Too Strict

Breaking your own site:

// ❌ This will break most websites
"default-src 'none'"

// ✅ Start permissive, then tighten
"default-src 'self'; script-src 'self' 'unsafe-inline'"

2. Forgetting 'self'

// ❌ Blocks your own resources
"script-src https://cdn.example.com"

// ✅ Include 'self'
"script-src 'self' https://cdn.example.com"

3. X-Frame-Options with CSP

// CSP frame-ancestors supersedes X-Frame-Options
// Use both for backward compatibility
headers['x-frame-options'] = { value: 'DENY' };
headers['content-security-policy'] = { 
    value: "frame-ancestors 'none'" 
};

Monitoring and Maintenance

Set up CSP Reporting

// In your CSP header
"report-uri /csp-report"

// Create Lambda function to receive reports
// POST to /csp-report with violation details

Regular Security Audits

# Weekly check
curl -I https://yourdomain.com | grep -i "security\|policy\|frame"

# Or use automated tools
npm install -g observatory-cli
observatory yourdomain.com

Cost

CloudFront Functions:

  • First 2 million invocations/month: FREE
  • After that: $0.10 per 1 million invocations

For security headers, this is essentially free for most websites.

Security Headers Checklist

  • Strict-Transport-Security enabled
  • Content-Security-Policy configured (start with report-only)
  • X-Frame-Options set to DENY or SAMEORIGIN
  • X-Content-Type-Options set to nosniff
  • Referrer-Policy configured
  • Permissions-Policy configured
  • Tested with securityheaders.com
  • Tested with Mozilla Observatory
  • CSP reports monitored
  • Regular security audits scheduled

Conclusion

Security headers are easy to implement and critical for user safety. With CloudFront Functions, you can add them to your entire website in minutes, at virtually no cost.

Don't wait for a security incident. Implement security headers today.

Resources