Last active
August 5, 2021 17:09
-
-
Save stu-smith/7fec25367e1b83fb0709c708a704ff04 to your computer and use it in GitHub Desktop.
Shows how to use CloudFormation to attach a Lambda@Edge function to a CloudFront distribution to add HSTS and CSP custom headers. NOTE: The stack must be updated twice: once with the condition set to false, and once with it set to true.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
AWSTemplateFormatVersion: 2010-09-09 | |
Parameters: | |
RootDomainName: | |
Type: String | |
IncludeLambdaEdge: | |
Type: String | |
AllowedValues: ['true', 'false'] | |
Conditions: | |
IncludeLambdaEdge: | |
!Equals ['true', !Ref IncludeLambdaEdge] | |
Mappings: | |
RegionMap: | |
us-east-1: | |
S3HostedZoneID: Z3AQBSTGFYJSTF | |
S3WebsiteEndpoint: s3-website-us-east-1.amazonaws.com | |
us-west-1: | |
S3HostedZoneID: Z2F56UZL2M1ACD | |
S3WebsiteEndpoint: s3-website-us-west-1.amazonaws.com | |
us-west-2: | |
S3HostedZoneID: Z3BJ6K6RIION7M | |
S3WebsiteEndpoint: s3-website-us-west-2.amazonaws.com | |
eu-west-1: | |
S3HostedZoneID: Z1BKCTXD74EZPE | |
S3WebsiteEndpoint: s3-website-eu-west-1.amazonaws.com | |
ap-southeast-1: | |
S3HostedZoneID: Z3O0J2DXBE1FTB | |
S3WebsiteEndpoint: s3-website-ap-southeast-1.amazonaws.com | |
ap-southeast-2: | |
S3HostedZoneID: Z1WCIGYICN2BYD | |
S3WebsiteEndpoint: s3-website-ap-southeast-2.amazonaws.com | |
ap-northeast-1: | |
S3HostedZoneID: Z2M4EHUR26P7ZW | |
S3WebsiteEndpoint: s3-website-ap-northeast-1.amazonaws.com | |
sa-east-1: | |
S3HostedZoneID: Z31GFT0UA1I2HV | |
S3WebsiteEndpoint: s3-website-sa-east-1.amazonaws.com | |
Resources: | |
RootCertificate: | |
Type: 'AWS::CertificateManager::Certificate' | |
Properties: | |
DomainName: !Ref RootDomainName | |
SubdomainCertificate: | |
Type: 'AWS::CertificateManager::Certificate' | |
Properties: | |
DomainName: !Sub | |
- '*.${Domain}' | |
- Domain: !Ref RootDomainName | |
PublicWebsiteRootBucket: | |
Type: 'AWS::S3::Bucket' | |
Properties: | |
BucketName: !Ref RootDomainName | |
AccessControl: PublicRead | |
WebsiteConfiguration: | |
RedirectAllRequestsTo: | |
HostName: !Ref PublicWebsiteWwwBucket | |
PublicWebsiteWwwBucket: | |
Type: 'AWS::S3::Bucket' | |
Properties: | |
BucketName: !Sub | |
- www.${Domain} | |
- Domain: !Ref RootDomainName | |
AccessControl: PublicRead | |
WebsiteConfiguration: | |
IndexDocument: index.html | |
ErrorDocument: 404.html | |
PublicWwwBucketPolicy: | |
Type: 'AWS::S3::BucketPolicy' | |
Properties: | |
PolicyDocument: | |
Id: PublicWebsitePolicy | |
Version: 2012-10-17 | |
Statement: | |
- Sid: PublicReadForGetBucketObjects | |
Effect: Allow | |
Principal: '*' | |
Action: 's3:GetObject' | |
Resource: !Join | |
- '' | |
- - 'arn:aws:s3:::' | |
- !Ref PublicWebsiteWwwBucket | |
- /* | |
Bucket: !Ref PublicWebsiteWwwBucket | |
PublicRootBucketPolicy: | |
Type: 'AWS::S3::BucketPolicy' | |
Properties: | |
PolicyDocument: | |
Id: PublicWebsitePolicy | |
Version: 2012-10-17 | |
Statement: | |
- Sid: PublicReadForGetBucketObjects | |
Effect: Allow | |
Principal: '*' | |
Action: 's3:GetObject' | |
Resource: !Join | |
- '' | |
- - 'arn:aws:s3:::' | |
- !Ref PublicWebsiteRootBucket | |
- /* | |
Bucket: !Ref PublicWebsiteRootBucket | |
InternalTracingBucket: | |
Type: 'AWS::S3::Bucket' | |
Properties: | |
BucketName: !Join | |
- '-' | |
- - !Ref RootDomainName | |
- 'tracing' | |
PublicWebsiteWwwCloudfront: | |
Type: AWS::CloudFront::Distribution | |
DependsOn: | |
- PublicWebsiteWwwBucket | |
- InternalTracingBucket | |
Properties: | |
DistributionConfig: | |
Comment: CloudFront to S3 - www | |
Origins: | |
- DomainName: !Join | |
- '.' | |
- - 'www' | |
- !Ref 'RootDomainName' | |
- !FindInMap [RegionMap, !Ref 'AWS::Region', S3WebsiteEndpoint] | |
Id: S3WwwOrigin | |
CustomOriginConfig: | |
HTTPPort: '80' | |
HTTPSPort: '443' | |
OriginProtocolPolicy: http-only | |
Enabled: true | |
HttpVersion: 'http2' | |
DefaultRootObject: index.html | |
Aliases: | |
- !Join | |
- '.' | |
- - 'www' | |
- !Ref 'RootDomainName' | |
DefaultCacheBehavior: | |
AllowedMethods: | |
- GET | |
- HEAD | |
Compress: true | |
DefaultTTL: 3600 | |
TargetOriginId: S3WwwOrigin | |
ForwardedValues: | |
QueryString: true | |
Cookies: | |
Forward: none | |
ViewerProtocolPolicy: redirect-to-https | |
LambdaFunctionAssociations: | |
- !If | |
- IncludeLambdaEdge | |
- EventType: 'origin-response' | |
LambdaFunctionARN: !Join | |
- ':' | |
- - !GetAtt [PoliciesEdgeLambda, Arn] | |
- !GetAtt [PoliciesEdgeLambdaVersion, Version] | |
- !Ref 'AWS::NoValue' | |
PriceClass: PriceClass_All | |
ViewerCertificate: | |
AcmCertificateArn: !Ref SubdomainCertificate | |
SslSupportMethod: sni-only | |
Logging: | |
Bucket: !GetAtt [InternalTracingBucket, DomainName] | |
IncludeCookies: false | |
Prefix: 'www' | |
PublicWebsiteRootCloudfront: | |
Type: AWS::CloudFront::Distribution | |
DependsOn: | |
- PublicWebsiteRootBucket | |
Properties: | |
DistributionConfig: | |
Comment: CloudFront to S3 - root | |
Origins: | |
- DomainName: !Join | |
- '.' | |
- - !Ref 'RootDomainName' | |
- !FindInMap [RegionMap, !Ref 'AWS::Region', S3WebsiteEndpoint] | |
Id: S3RootOrigin | |
CustomOriginConfig: | |
HTTPPort: '80' | |
HTTPSPort: '443' | |
OriginProtocolPolicy: http-only | |
Enabled: true | |
HttpVersion: 'http2' | |
DefaultRootObject: index.html | |
Aliases: | |
- !Ref 'RootDomainName' | |
DefaultCacheBehavior: | |
AllowedMethods: | |
- GET | |
- HEAD | |
Compress: true | |
TargetOriginId: S3RootOrigin | |
ForwardedValues: | |
QueryString: true | |
Cookies: | |
Forward: none | |
ViewerProtocolPolicy: redirect-to-https | |
LambdaFunctionAssociations: | |
- !If | |
- IncludeLambdaEdge | |
- EventType: 'origin-response' | |
LambdaFunctionARN: !Join | |
- ':' | |
- - !GetAtt [PoliciesEdgeLambda, Arn] | |
- !GetAtt [PoliciesEdgeLambdaVersion, Version] | |
- !Ref 'AWS::NoValue' | |
PriceClass: PriceClass_All | |
ViewerCertificate: | |
AcmCertificateArn: !Ref RootCertificate | |
SslSupportMethod: sni-only | |
HostedZone: | |
Type: 'AWS::Route53::HostedZone' | |
Properties: | |
Name: !Ref RootDomainName | |
DNS: | |
Type: AWS::Route53::RecordSetGroup | |
Properties: | |
HostedZoneName: !Sub | |
- ${Domain}. | |
- Domain: !Ref RootDomainName | |
RecordSets: | |
- Name: !Ref 'RootDomainName' | |
Type: A | |
AliasTarget: | |
HostedZoneId: Z2FDTNDATAQYW2 | |
DNSName: !GetAtt [PublicWebsiteRootCloudfront, DomainName] | |
- Name: !Join | |
- '.' | |
- - 'www' | |
- !Ref 'RootDomainName' | |
Type: A | |
AliasTarget: | |
HostedZoneId: Z2FDTNDATAQYW2 | |
DNSName: !GetAtt [PublicWebsiteWwwCloudfront, DomainName] | |
PoliciesEdgeLambdaRole: | |
Type: 'AWS::IAM::Role' | |
Condition: IncludeLambdaEdge | |
Properties: | |
AssumeRolePolicyDocument: | |
Version: '2012-10-17' | |
Statement: | |
- Action: 'sts:AssumeRole' | |
Principal: | |
Service: | |
- lambda.amazonaws.com | |
- edgelambda.amazonaws.com | |
- replicator.lambda.amazonaws.com | |
Effect: Allow | |
Policies: | |
- PolicyName: EdgePoliciesLambdaPolicy | |
PolicyDocument: | |
Version: '2012-10-17' | |
Statement: | |
- Action: | |
- 'xray:PutTraceSegments' | |
- 'xray:PutTelemetryRecords' | |
- 'lambda:GetFunction' | |
- 'lambda:EnableReplication*' | |
- 'lambda:InvokeFunction' | |
- 'logs:CreateLogGroup' | |
- 'logs:CreateLogStream' | |
- 'logs:PutLogEvents' | |
Effect: Allow | |
Resource: '*' | |
PoliciesEdgeLambda: | |
Type: 'AWS::Lambda::Function' | |
Condition: IncludeLambdaEdge | |
Properties: | |
Handler: 'index.handler' | |
Role: | |
Fn::GetAtt: | |
- 'PoliciesEdgeLambdaRole' | |
- 'Arn' | |
Code: | |
ZipFile: | | |
'use strict'; | |
exports.handler = (event, context, callback) => { | |
const response = event.Records[0].cf.response; | |
var addHeader = (header, value) => { | |
response.headers[header.toLowerCase()] = [{ | |
key: header, | |
value: value | |
}]; | |
}; | |
addHeader('Strict-Transport-Security', 'max-age=31536000; includeSubdomains; preload'); | |
addHeader('X-Content-Type-Options', 'nosniff'); | |
addHeader('Content-Security-Policy', "default-src 'self'"); | |
addHeader('X-Frame-Options', 'DENY'); | |
addHeader('X-XSS-Protection', '1; mode=block') | |
callback(null, response); | |
}; | |
Runtime: 'nodejs6.10' | |
Timeout: '25' | |
TracingConfig: | |
Mode: 'Active' | |
PoliciesEdgeLambdaVersion: | |
Type: 'AWS::Lambda::Version' | |
Condition: IncludeLambdaEdge | |
Properties: | |
FunctionName: | |
Ref: 'PoliciesEdgeLambda' |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment