It’s very common to have some sort of test/SIT/UAT environment(s). Often this site should’t be made public. When using
AWS there are a number of strategies to secure the test environment(s) from limiting access by IP address (this works
well if you have a static IP and/or can VPN to a fixed IP address), only permit access via a VPN connection (which can
be useful in a corporate environment), or set-up the web browser and AWS WAF (works for
Chrome and FireFox in a small business
environment).
The extension is called Modify Header Value (HTTP Headers)
[FireFox addon link]/[Chrome extension link].
Add this extension and then open the options page for it.
At the bottom of the options page is a set of 3 inputs which are used to configure the add-on.
In the URL field enter the URL for your test domain (eg http://example.com);
In the Header Name field enter a custom header to use for authentication (eg Authorization);
In the Header Value field enter a value to be used for authentication (eg Custom <password>).
Then click the big + button to the right of the fields.
A new entry will be added to the table at the bottom of the plugin configuration. Ensure that Domain, Sub and Add
are checked; and that Modify and Remove are unchecked.
The first step is to add some values under the custom element in the serverless.yml file so we can include a
configuration file specific to our environment.
1 2 3 4 5
# Determine what resources file to include based on the current stage customConfigFile: ${self:custom.customConfigFiles.${self:custom.stage}, self:custom.customConfigFiles.other} customConfigFiles: prod: prod other: other
Now there’s a way to determine what filename to include in our Serverless Framework
configuration it’s time to set-up the file inclusion.
To maintain compatibility with thew YAML specifications everything for the existing resources element needs to be
moved to a new file. Start by cutting the entire resources.Resources element and paste it within a file at
config/resources.yml.
Resources: # Set-up an S3 bucket to store the site WebsiteS3Bucket: Type: AWS::S3::Bucket Properties: AccessControl: PublicRead WebsiteConfiguration: IndexDocument: index.html ErrorDocument: error.html BucketName: ${self:custom.domain.domainname} # Set-up a policy on the bucket so it can be used as a website WebsiteBucketPolicy: Type: AWS::S3::BucketPolicy Properties: PolicyDocument: Id: Fn::Join: - "" - - ${self:service.name} - BucketPolicy Statement: - Sid: PublicReadForGetBucketObjects Effect: Allow Principal: '*' Action: 's3:GetObject' Resource: Fn::Join: - '' - - 'arn:aws:s3:::' - Ref: WebsiteS3Bucket - /* Bucket: Ref: WebsiteS3Bucket # Configure CloudFront to get all content from S3 WebsiteCloudFrontDistribution: Type: 'AWS::CloudFront::Distribution' Properties: DistributionConfig: Aliases: - ${self:custom.domain.domainname} - www.${self:custom.domain.domainname} CustomErrorResponses: - ErrorCode: '404' ResponsePagePath: "/error.html" ResponseCode: '200' ErrorCachingMinTTL: '30' DefaultCacheBehavior: Compress: true ForwardedValues: QueryString: false Cookies: Forward: all SmoothStreaming: false TargetOriginId: defaultOrigin ViewerProtocolPolicy: redirect-to-https DefaultRootObject: index.html Enabled: true Origins: - DomainName: ${self:custom.domain.domainname}.${self:custom.s3DNSName.${self:provider.region}} Id: defaultOrigin CustomOriginConfig: HTTPPort: 80 HTTPSPort: 443 OriginProtocolPolicy: http-only PriceClass: PriceClass_All ViewerCertificate: AcmCertificateArn: ${self:custom.domain.sslCertificateARN} SslSupportMethod: sni-only # DNS Record for the domain WebsiteDNSRecord: Type: "AWS::Route53::RecordSet" Properties: AliasTarget: DNSName: Fn::GetAtt: - WebsiteCloudFrontDistribution - DomainName HostedZoneId: Z2FDTNDATAQYW2 HostedZoneName: ${self:custom.domain.domain}. Name: ${self:custom.domain.domainname} Type: 'A' # DNS Record for www.domain WebsiteWWWDNSRecord: Type: "AWS::Route53::RecordSet" Properties: AliasTarget: DNSName: Fn::GetAtt: - WebsiteCloudFrontDistribution - DomainName HostedZoneId: Z2FDTNDATAQYW2 HostedZoneName: ${self:custom.domain.domain}. Name: www.${self:custom.domain.domainname} Type: 'A'
The same process now needs to be done for the resources.Outputs element and pasted into config/outputs.yml.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
Outputs: WebsiteURL: Value: Fn::GetAtt: - WebsiteS3Bucket - WebsiteURL Description: URL for Alumni Chat website hosted on S3 S3BucketSecureURL: Value: Fn::Join: - '' - - 'https://' - Fn::GetAtt: - WebsiteS3Bucket - DomainName Description: Secure URL of S3 bucket to hold website content
Now the two elements have been extracted to new files they need to be included within the main serverless.yml file.
1 2 3 4
# Include the resources file - ${file(config/resources.yml)} # Include the outputs file - ${file(config/outputs.yml)}
Because the resources element is now an array it’s possible to add additional files to be included. To include an
environment specific file a variable can be included in the file reference.
1 2
# Include a custom configuration file based on the environment - ${file(config/${self:custom.customConfigFile}.yml)}
Two possible configuration files were defined as part of this process prod and other. We need to create both of these.
Create a file at config/prod.yml as follows.
1
Resources:
Then create a file at config/other.yml which will store the AWS WAF configuration.
A ByteMatchSet is a predicate for matching a specific string within a request. The following predicate will locate a
specific header and verify the contents of it.
Begin by creating a ByteMatchSet predicate. This will have the same configuration for all environments so it can be
defined in the Resources element within the resources.yml file that was created earlier. The data within
TargetString will need to match with the values used when configuring the browser plugin.
Now a predicate (in this case a ByteMatchSet) has been created a Rule is required. A Rule can combine multiple
predicates, but in this case only one will be used.
The WebACL will create an access control list that can be applied to the
AWS CloudFront distribution that serves the website. The configuration for this
will be slightly different for the production environment, so a configuration will need to be added to each of the
prod.yml and other.yml files.
The final step before deployment is to apply the new WebACL to the existing CloudFront Distribution. Locate the
resource with TypeAWS::CloudFront::Distribution. Within this resource is an element named DistributionConfig,
the WebACL needs to be referenced within this element.
To test the security set-up the site needs to be re-deployed to the development environment.
1
sls deploy
Once deployment has completed open a browser with the Modify Header Value (HTTP Headers) plugin installed and turn off
the plugin. This can be done by clicking the plugin’s icon and selecting Switch add-on ON|OFF.
Access the development site and ensure that it does not load.
# Plugins for additional Serverless functionality plugins: -serverless-s3-deploy -serverless-plugin-scripts
# Configuration for AWS provider: name:aws runtime:nodejs8.10 profile:serverless # Some future functionality requires us to use us-east-1 at this time region:us-east-1
# This enables us to use the default stage definition, but override it from the command line stage:${opt:stage,self:provider.stage} # This enables us to prepend the stage name for non-production environments domain: fulldomain: prod:${self:custom.domain.domain} other:${self:custom.stage}.${self:custom.domain.domain} # This value has been customised so I can maintain multiple demonstration sites domain:${self:custom.postname}.${self:custom.domain.zonename} domainname:${self:custom.domain.fulldomain.${self:custom.stage},self:custom.domain.fulldomain.other} # DNS Zone name (this is only required so I can maintain multiple demonstration sites) zonename:alphageek.com.au cacheControlMaxAgeHTMLByStage: # HTML Cache time for production environment prod:3600 # HTML Cache time for other environments other:0 cacheControlMaxAgeHTML:${self:custom.domain.cacheControlMaxAgeHTMLByStage.${self:custom.stage},self:custom.domain.cacheControlMaxAgeHTMLByStage.other} sslCertificateARN:arn:aws:acm:us-east-1:165657443288:certificate/61d202ea-12f2-4282-b602-9c3b83183c7a assets: targets: # Configuration for HTML files (overriding the default cache control age) -bucket: Ref:WebsiteS3Bucket files: -source:./public/ headers: CacheControl:max-age=${self:custom.domain.cacheControlMaxAgeHTML} empty:true globs: -'**/*.html' # Configuration for all assets -bucket: Ref:WebsiteS3Bucket files: -source:./public/ empty:true globs: -'**/*.js' -'**/*.css' -'**/*.jpg' -'**/*.png' -'**/*.gif' scripts: hooks: # Run these commands when creating the deployment artifacts package:createDeploymentArtifacts:> hexo clean && hexo generate # Run these commands after infrastructure changes have been completed deploy:finalize:> sls s3deploy -s ${self:custom.stage} # AWS Region to S3 website hostname mapping s3DNSName: us-east-2:s3-website.us-east-2.amazonaws.com us-east-1:s3-website-us-east-1.amazonaws.com us-west-1:s3-website-us-west-1.amazonaws.com us-west-2:s3-website-us-west-2.amazonaws.com ap-south-1:s3-website.ap-south-1.amazonaws.com ap-northeast-3:s3-website.ap-northeast-3.amazonaws.com ap-northeast-2:s3-website.ap-northeast-2.amazonaws.com ap-southeast-1:s3-website-ap-southeast-1.amazonaws.com ap-southeast-2:s3-website-ap-southeast-2.amazonaws.com ap-northeast-1:s3-website-ap-northeast-1.amazonaws.com ca-central-1:s3-website.ca-central-1.amazonaws.com eu-central-1:s3-website.eu-central-1.amazonaws.com eu-west-1:s3-website-eu-west-1.amazonaws.com eu-west-2:s3-website.eu-west-2.amazonaws.com eu-west-3:s3-website.eu-west-3.amazonaws.com eu-north-1:s3-website.eu-north-1.amazonaws.com sa-east-1:s3-website-sa-east-1.amazonaws.com # Determine what resources file to include based on the current stage customConfigFile:${self:custom.customConfigFiles.${self:custom.stage},self:custom.customConfigFiles.other} customConfigFiles: prod:prod other:other
# Define the resources we will need to host the site resources: # Include the resources file -${file(config/resources.yml)} # Include the outputs file -${file(config/outputs.yml)} # Include a custom configuration file based on the environment -${file(config/resources/environment/${self:custom.customConfigFile}.yml)}