# Deploy Secure Static Web Apps on AWS
## Summary
Securing static web apps is a complex challenge for many organizations. While there is no one size fits all approach, there are services and features within AWS that you can utilize to get a head start on your web app security journey. AWS WAF (Web Application Firewall) can protect your web app against bots that may compromise security or consume excessive resources. Additionally, Amazon CloudFront Functions can add HTTP security headers that limit requests from the client. This APG provides an AWS Cloud Development Kit (CDK) construct with secure defaults to simplify the infrastructure for a secure static web app. The construct uses Amazon S3, Amazon CloudFront, AWS WAF, and Amazon Route 53.
See the associated [Amazon Prescriptive Guidance (APG) Pattern](https://apg-library.amazonaws.com/content/9ed9140c-a1d6-49ac-8aba-8bd533fa4ce2) for further information.
## Prerequisites
- Node.js installed. For more information, see [Node.js Downloads](https://nodejs.org/en/download/).
- PNPM installed. For more information, see [PNPM Installation](https://pnpm.io/installation).
- AWS Cloud Development Kit (CDK) Toolkit, installed and configured. For more information, see [AWS CDK Toolkit (cdk command)](https://docs.aws.amazon.com/cdk/latest/guide/cli.html).
- An AWS Account bootstrapped with the AWS CDK. For more information, see [Bootstrapping](https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html).
- If enabling Amazon Route 53, a hosted zone with your domain name must already exist in your account. For more information, see **Setup Route 53 Epic** below.
- Git, installed and configured. For more information, see [Git](https://git-scm.com/).
## Limitations
- If enabling AWS WAF or Amazon Route 53, then you must deploy your stack in the **Region US East (N. Virginia)** in order to associate them with your CloudFront distribution. For more information, see [AWS WAF Developer Guide](https://docs.aws.amazon.com/waf/latest/developerguide/how-aws-waf-works.html) and [Amazon Route 53 Developer Guide](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/getting-started-cloudfront-overview.html#getting-started-cloudfront-request-certificate).
## Product versions
- AWS CLI Version 2 or greater
- Node.js 14.X or greater
- PNPM 6.X or greater
- AWS CDK v2 or greater
- Git 2.X or greater
## Architecture

## Tools
- [Node.js](https://nodejs.org/en/)
- [AWS CDK](https://aws.amazon.com/cdk/)
## Epics
### Clone Repository and Deploy Infrastructure
|Story|Description|Skills required|
|-|-|-|
|Clone Repository|Clone the repository from https://github.com/aws-samples/secure-static-site and install dependencies.
```git clone https://github.com/aws-samples/secure-static-site.git```
```cd secure-static-site```
```pnpm install```|App developer|
|Deploy Infrastructure|Deploy the infrastructure defined by the CDK in the secure-static-site-frontend package. This package uses the construct defined in the secure-static-site package. Note, this will deploy a website publicly accessible.
```cd secure-static-site-frontend```
```cdk deploy```
```# after reviewing security changes, remember to enter y```
Note, if you run into error about your AWS Account not being boostrapped for the CDK, make sure to review the prerequisites.|App developer|
### Validate HTTP Security Headers
|Story|Description|Skills required|
|-|-|-|
|Validate scrip-src CSP|First, visit the URL of either your Route 53 or CloudFront domain and click the "Download" button for the "Script" content type to validate the script-src CSP. The download of the script should be blocked. You can check for yourself by looking at the Network tab of your browser's developer tools. Update the construction of your StaticSite in packages/secure-static-site-frontend/bin/static-site.ts to the following:
// packages/secure-static-site-frontend/bin/static-site.tsNow run ```cdk deploy```, refresh your browser, and try again. You should get a 200 HTTP status code indicating a successful download of the request.|App developer| |Validate style-src CSP|Next click the "Download" button for the "Style" content type to validate the style-src CSP. The download should be blocked. Update the construction of your StaticSite in packages/secure-static-site-frontend/bin/static-site.ts to the following:
const staticSite = new StaticSite(this, "SecureStaticSite", {
...
responseHeaders: {
contentSecurityPolicy: {
scriptSrc:
"self https://unpkg.com/react@17/umd/react.production.min.js;",
...
},
},
...
});
// packages/secure-static-site-frontend/bin/static-site.tsThen run ```cdk deploy```, refresh your browser, and try again. The download should now be successful.|App developer| |Validate font-src CSP|Next click the "Download" button for the "Font" content type to validate the font-src CSP. The download should be blocked. Update the construction of your StaticSite in packages/secure-static-site-frontend/bin/static-site.ts to the following:
const staticSite = new StaticSite(this, "SecureStaticSite", {
...
responseHeaders: {
contentSecurityPolicy: {
styleSrc:
"'self' \
'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=' \
https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css;",
...
},
},
...
});
// packages/secure-static-site-frontend/bin/static-site.tsThen run ```cdk deploy```, refresh your browser, and try again. The download should now be successful.|App developer| |Validate img-src CSP|Next click the "Download" button for the "Image" content type to validate the font-src CSP. The download should be successful. The StaticSite construct's default policy is to allow images to be downloaded from it's own origin which is the case for the StaticSiteArchitecture.png image. If an image were to try to be downloaded from a different origin, then the request would of been blocked.|App developer| |Validate media-src CSP|Next click the "Download" button for the "Media" content type to validate the media-src CSP. The download should be blocked. Update the construction of your StaticSite in packages/secure-static-site-frontend/bin/static-site.ts to the following:
const staticSite = new StaticSite(this, "SecureStaticSite", {
...
responseHeaders: {
contentSecurityPolicy: {
fontSrc:
"'self' data: \
https://fonts.googleapis.com\
/css?family=Roboto:300,400,500,700&display=swap;",
...
},
},
...
});
// packages/secure-static-site-frontend/bin/static-site.tsThen run ```cdk deploy```, refresh your browser, and try again. The download should now be successful.|App developer| ### Enable AWS WAF and Validate Rules |Story|Description|Skills required| |-|-|-| |Enable WAF with defaults|Enable the StaticSite prop enableWaf and set it's value to true in order to protect your CloudFront distribution with the AWS WAF service, including the following AWS managed rule groups by default:
const staticSite = new StaticSite(this, "SecureStaticSite", {
...
responseHeaders: {
contentSecurityPolicy: {
mediaSrc: "'self'",
...
},
},
...
});
const staticSite = new StaticSite(this, "SecureStaticSite", {|App developer| |Enable WAF metrics (optional)|If you want view metrics tracking which users were allowed to access and which were blocked from accessing your distribution, including the reason why they were blocked, then also enable the prop enableWafMetrics and set it's value to true.
...
enableWaf: true,
...
});
const staticSite = new StaticSite(this, "SecureStaticSite", {These metrics are easily viewable from the WAF Console.|App developer| |Validate Core Rule Set (optional)|***Note: you must have curl installed to complete this story.***
...
enableWafMetrics: true,
...
});
\This response is just the root HTML code for the site, but it demonstrates that the command completed successfully.
\
\
\
\ href="/assets/favicon.17e50649.svg" />
\ content="width=device-width, initial-scale=1.0" />
\Secure Static Site
\ src="/assets/index.64efc456.js">
\
\
\
\
\
\
\
\"http://www.w3.org/TR/html4/loose.dtd">There are additional rules like this one included in the Core Rule Set, each of which will block requests that match their criteria for security violations. You can find more information on which rules are included in the [AWS WAF Developer Guide](https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html).|App developer| |Restrict access to allowed IP list (optional)|If you only want to allow certain IP addresses to access your site, then you can create a list of allowed IP addresses using the prop allowedIPs. The list values should be strings in CIDR format, for example:
\
\ERROR: The request could not be satisfied
\
\403 ERROR
\The request could not be satisfied.
\
Request blocked.
We can't connect to the server for this app or website at this time.
There might be too much traffic or a configuration error.
Try again later, or contact the app or website owner.
\
If you provide content to customers through CloudFront,
you can find steps to troubleshoot and help prevent this error
by reviewing the CloudFront documentation.
\
\
\
Generated by cloudfront (CloudFront)
Request ID: Z37Crk-Vlz7xj0jWdjEIFjd02vBgWXL2wN6m47WtjQ-h3iDUFFSILA==
\
\
\
\\
const staticSite = new StaticSite(this, "SecureStaticSite", {In order to test that this rule is working you can start with an empty list, which will block everyone since only IPs included in the list are allowed.
...
allowedIPs: ["10.0.0.0/16", "10.1.1.0/24", "10.1.2.20/32"],
...
});
const staticSite = new StaticSite(this, "SecureStaticSite", {This configuration will disable the Core Rule Set, but leave the other groups enabled.
...
disableCoreWafRuleGroup: true,
disableAmazonIPWafRuleGroup: false,
disableAnonymousIPWafRuleGroup: false,
...
});
// packages/secure-static-site-frontend/bin/static-site.ts|App developer| ## Related resources - [Amazon S3 + Amazon CloudFront: A Match Made in the Cloud](https://aws.amazon.com/blogs/networking-and-content-delivery/amazon-s3-amazon-cloudfront-a-match-made-in-the-cloud/) - [Mozilla Developer Network: Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) ## Security See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. ## License This library is licensed under the MIT-0 License. See the LICENSE file.
const staticSite = new StaticSite(this, "SecureStaticSite", {
...
domainNameBase: "mybasedomain.com",
domainNamePrefix: "secure-static-site",
...
});