Skip to content

Instantly share code, notes, and snippets.

@stu-smith
Last active August 5, 2021 17:09
Show Gist options
  • Save stu-smith/7fec25367e1b83fb0709c708a704ff04 to your computer and use it in GitHub Desktop.
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.
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