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
-
- Enter your URL
- Get a letter grade (A+ is the goal)
- See which headers are missing
-
- Comprehensive security scan
- Includes headers and more
-
- 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.