├── .github ├── CODEOWNERS └── workflows │ ├── release.yaml │ └── unit-tests.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── NOTICE ├── README.md ├── cfn-publish.config ├── ci ├── cfn_nag_blacklist.yaml └── include.lst ├── docs └── images │ ├── cf-secure-static-site-architecture.png │ ├── deploy-to-aws.png │ └── static-website.png ├── source └── witch │ └── witch.js ├── templates ├── acm-certificate.yaml ├── cloudfront-site.yaml ├── custom-resource.yaml └── main.yaml └── www ├── 403.html ├── 404.html ├── css └── style.css ├── index.html ├── other.html └── robots.txt /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in the repo. 2 | # @aws-uk-solution-builders will be requested for review when someone opens a pull request. 3 | * @aws-samples/aws-uk-solution-builders 4 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Release Version 4 | on: 5 | push: 6 | branches: 7 | - master 8 | jobs: 9 | release: 10 | name: Release Version 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* || true 15 | # Set AWS Credentials 16 | - name: Configure AWS credentials 17 | uses: aws-actions/configure-aws-credentials@v1 18 | with: 19 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 20 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 21 | aws-session-token: ${{ secrets.AWS_SESSION_TOKEN }} 22 | aws-region: ${{ secrets.REGION }} 23 | # Setup 24 | - name: Set up Python 3.7 25 | uses: actions/setup-python@v1 26 | with: 27 | python-version: 3.7 28 | - name: Install virtualenv 29 | run: pip install virtualenv 30 | - name: Install dependencies 31 | run: make setup-predeploy 32 | # Release if required 33 | - name: Setup versions in env variables 34 | id: version 35 | run: | 36 | function version { echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; } 37 | echo "THIS_VERSION=$(make version | sed s/^v//)" >> $GITHUB_ENV 38 | echo "THIS_VERSION_COMPARABLE=$(version $(make version | sed s/^v//))" >> $GITHUB_ENV 39 | echo "LATEST_VERSION_COMPARABLE=$(version $(git describe --tags $(git rev-list --tags --max-count=1) | sed s/^v// 2> /dev/null || echo '0'))" >> $GITHUB_ENV 40 | - name: Create Release 41 | id: create_release 42 | uses: actions/create-release@latest 43 | if: env.THIS_VERSION_COMPARABLE > env.LATEST_VERSION_COMPARABLE 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} 46 | with: 47 | tag_name: v${{ env.THIS_VERSION }} 48 | release_name: Release v${{ env.THIS_VERSION }} 49 | body: | 50 | See the CHANGELOG for a list of features included in this release 51 | draft: false 52 | prerelease: true 53 | # Package and Upload Archive 54 | - name: Pre-Package Copy Function 55 | run: cd source/witch/ && npm install --prefix nodejs mime-types && cp witch.js nodejs/node_modules/ && zip -r ../../witch.zip nodejs && cd ../../ 56 | - name: Package Release 57 | run: zip -r packaged.zip -@ < ci/include.lst 58 | - name: Upload Release 59 | run: aws s3 cp packaged.zip s3://$CFN_BUCKET/amazon-cloudfront-secure-static-site/v${{ env.THIS_VERSION }}/amazon-cloudfront-secure-static-site.zip 60 | env: 61 | CFN_BUCKET: ${{ secrets.CFN_BUCKET }} 62 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Unit Tests 4 | on: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | types: 10 | - opened 11 | - edited 12 | - synchronize 13 | jobs: 14 | unit_tests: 15 | name: Unit tests 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: ruby/setup-ruby@v1 19 | with: 20 | ruby-version: '2.6' 21 | - run: gem install cfn-nag 22 | - uses: actions/checkout@v2 23 | # Run Tests 24 | - name: CloudFormation unit tests 25 | run: make test-cfn 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | **/dist 3 | **/global-s3-assets 4 | **/regional-s3-assets 5 | **/open-source 6 | **/*.zip 7 | **/.zip 8 | **/tmp 9 | **/out-tsc 10 | **/settings.js 11 | **/packaged.template 12 | packaged.template 13 | packaged.zip 14 | s-headers.zip 15 | witch.zip 16 | 17 | # dependencies 18 | **/node_modules 19 | **/coverage 20 | **/package.json 21 | 22 | # e2e 23 | **/e2e/*.js 24 | **/e2e/*.map 25 | 26 | # misc 27 | **/npm-debug.log 28 | **/testem.log 29 | **/.env 30 | # package-lock.json 31 | **/.vscode/settings.json 32 | **/setenv.sh 33 | 34 | # System Files 35 | **/.DS_Store 36 | *.DS_Store 37 | **/.vscode 38 | 39 | #nyc output 40 | **/.nyc_output 41 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | 4 | repos: 5 | # General 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v2.4.0 8 | hooks: 9 | - id: trailing-whitespace 10 | - id: end-of-file-fixer 11 | - id: check-yaml 12 | exclude: templates/* 13 | - id: check-added-large-files 14 | - id: pretty-format-json 15 | args: [--autofix, --indent, '4'] 16 | - id: no-commit-to-branch 17 | args: ['--branch', 'master'] 18 | 19 | # Secrets 20 | - repo: https://github.com/awslabs/git-secrets 21 | rev: 5e28df337746db4f070c84f7069d365bfd0d72a8 22 | hooks: 23 | - id: git-secrets 24 | 25 | # CloudFormation 26 | - repo: https://github.com/aws-cloudformation/cfn-python-lint 27 | rev: v0.27.1 28 | hooks: 29 | - id: cfn-python-lint 30 | name: AWS CloudFormation Linter 31 | files: \.(template)$ 32 | args: [--ignore-checks=W3002] 33 | 34 | - repo: https://github.com/pre-commit/mirrors-prettier 35 | rev: v3.0.1 36 | hooks: 37 | - id: prettier 38 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | pre-deploy: 4 | ifndef TEMP_BUCKET 5 | $(error TEMP_BUCKET is undefined) 6 | endif 7 | ifndef ADMIN_EMAIL 8 | $(error ADMIN_EMAIL is undefined) 9 | endif 10 | ifndef SUBNETS 11 | $(error SUBNETS is undefined) 12 | endif 13 | ifndef SEC_GROUPS 14 | $(error SEC_GROUPS is undefined) 15 | endif 16 | 17 | pre-run: 18 | ifndef ROLE_NAME 19 | $(error ROLE_NAME is undefined) 20 | endif 21 | 22 | setup-predeploy: 23 | virtualenv venv 24 | source venv/bin/activate 25 | pip install cfn-flip==1.2.2 26 | 27 | clean: 28 | rm -rf *.zip source/witch/nodejs/node_modules/ 29 | 30 | test-cfn: 31 | cfn_nag templates/*.yaml --blacklist-path ci/cfn_nag_blacklist.yaml 32 | 33 | version: 34 | @echo $(shell cfn-flip templates/main.yaml | python -c 'import sys, json; print(json.load(sys.stdin)["Mappings"]["Solution"]["Constants"]["Version"])') 35 | 36 | package: 37 | zip -r packaged.zip templates backend cfn-publish.config build.zip -x **/__pycache* -x *settings.js 38 | 39 | build-static: 40 | cd source/witch/ && npm install --prefix nodejs mime-types && cp witch.js nodejs/node_modules/ 41 | 42 | package-static: 43 | make build-static 44 | cd source/witch && zip -r ../../witch.zip nodejs -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amazon CloudFront Secure Static Website 2 | 3 | Use this solution to create a secure static website for your registered domain name. With this solution, your website: 4 | 5 | - Is hosted on [Amazon S3](https://aws.amazon.com/s3/) 6 | - Is distributed by [Amazon CloudFront](https://aws.amazon.com/cloudfront/) 7 | - Uses an SSL/TLS certificate from [AWS Certificate Manager (ACM)](https://aws.amazon.com/certificate-manager/) 8 | - Uses [CloudFront Response Header Policies](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/adding-response-headers.html) to add security headers to every server response 9 | - Is deployed with [AWS CloudFormation](https://aws.amazon.com/cloudformation/) 10 | 11 | For more information about each of these components, see the **Solution details** section on this page. 12 | 13 | ## Solution overview 14 | 15 | The following diagram shows an overview of how the solution works: 16 | 17 | ![Architecture](./docs/images/cf-secure-static-site-architecture.png) 18 | 19 | 1. The viewer requests the website at www.example.com. 20 | 2. If the requested object is cached, CloudFront returns the object from its cache to the viewer. 21 | 3. If the object is not in CloudFront’s cache, CloudFront requests the object from the origin (an S3 bucket). 22 | 4. S3 returns the object to CloudFront 23 | 5. CloudFront caches the object. 24 | 6. The object is returned to the viewer. Subsequent responses for the object are served from the CloudFront cache. 25 | 26 | ## Solution details 27 | 28 | ### S3 configuration 29 | 30 | This solution creates an S3 bucket that hosts your static website’s assets. The website is only accessible via CloudFront, not directly from S3. 31 | 32 | ### CloudFront configuration 33 | 34 | This solution creates a CloudFront distribution to serve your website to viewers. The distribution is configured with a CloudFront [origin access identity](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html) to make sure that the website is only accessible via CloudFront, not directly from S3. The distribution is also configured with a [CloudFront Response Header Policy](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/adding-response-headers.html) that adds security headers to every response. 35 | 36 | ### ACM configuration 37 | 38 | This solution creates an SSL/TLS certificate in ACM, and attaches it to the CloudFront distribution. This enables the distribution to serve your domain’s website using HTTPS. 39 | 40 | ### CloudFront Response Header Policy 41 | 42 | The CloudFront Response Header Policy adds security headers to every response served by CloudFront. 43 | 44 | The security headers can help mitigate some attacks, as explained in the [Amazon CloudFront - Understanding response header policies documentation](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/understanding-response-headers-policies.html#understanding-response-headers-policies-security). Security headers are a group of headers in the web server response that tell web browsers to take extra security precautions. This solution adds the following headers to each response: 45 | 46 | - [Strict-Transport-Security](https://infosec.mozilla.org/guidelines/web_security#http-strict-transport-security) 47 | - [Content-Security-Policy](https://infosec.mozilla.org/guidelines/web_security#content-security-policy) 48 | - [X-Content-Type-Options](https://infosec.mozilla.org/guidelines/web_security#x-content-type-options) 49 | - [X-Frame-Options](https://infosec.mozilla.org/guidelines/web_security#x-frame-options) 50 | - [X-XSS-Protection](https://infosec.mozilla.org/guidelines/web_security#x-xss-protection) 51 | - [Referrer-Policy](https://infosec.mozilla.org/guidelines/web_security#referrer-policy) 52 | 53 | For more information, see [Mozilla’s web security guidelines](https://infosec.mozilla.org/guidelines/web_security). 54 | 55 | ## Prerequisites 56 | 57 | You must have a registered domain name, such as example.com, and point it to a Route 53 hosted zone in the same AWS account in which you deploy this solution. For more information, see [Configuring Amazon Route 53 as your DNS service](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/dns-configuring.html). 58 | 59 | ## Deploy the solution 60 | 61 | > :⚠️ This template can only be deployed in the `us-east-1` region 62 | 63 | To deploy the solution, you use [AWS CloudFormation](https://aws.amazon.com/cloudformation). You can use the CloudFormation console, or download the CloudFormation template to deploy it on your own. 64 | 65 | > **Note:** You must have IAM permissions to launch CloudFormation templates that create IAM roles, and to create all the AWS resources in the solution. Also, you are responsible for the cost of the AWS services used while running this solution. For more information about costs, see the pricing pages for each AWS service. 66 | 67 | ### Use the CloudFormation console 68 | 69 | **To deploy the solution using the CloudFormation console** 70 | 71 | 1. Click the **Launch on AWS** button to open the solution in the CloudFormation console. 72 | 73 | [![Launch the Amazon CloudFront secure static website with CloudFormation](./docs/images/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/new?stackName=amazon-cloudfront-secure-static-site-templates-main&templateURL=https://s3.amazonaws.com/solution-builders-us-east-1/amazon-cloudfront-secure-static-site/latest/main.yaml) 74 | 75 | 2. If necessary, sign in with your AWS account credentials. 76 | 3. You should see a **Create stack** page, with pre-populated fields that specify the CloudFormation template. Choose the **Next** button at the bottom of the page. 77 | 4. On the **Specify stack details** page, enter values for the 78 | following fields: 79 | 80 | - **SubDomain:** The subdomain for your registered domain name. Viewers use the subdomain to access your website, for example: www.example.com. We recommend using the default value of **www** as the subdomain. 81 | - **DomainName:** Your registered domain name, such as example.com. This domain must be pointed to a Route 53 hosted zone. 82 | - **HostedZoneId** The Route 53 Hosted Zone Id containing the domain being used. 83 | - **CreateApex:** Optionally create an Alias to the domain apex (example.com) in your CloudFront configuration. Default is [no] 84 | 85 | After entering values, choose the **Next** button. 86 | 87 | 5. On the **Configure stack options** page, you can optionally [add tags and other stack options](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-console-add-tags.html). When finished, choose the **Next** button. 88 | 6. On the **Review** page, you must scroll down and check the two boxes in the **Capabilities** section: 89 | 90 | - **I acknowledge that AWS CloudFormation might create IAM resources with custom names.** 91 | - **I acknowledge that AWS CloudFormation might require the following capability: CAPABILITY_AUTO_EXPAND** 92 | 93 | These capabilities allow CloudFormation to create an IAM role that allows access 94 | to the stack’s resources, and to name the resources dynamically. 95 | 96 | 7. Choose the **Create stack** button. 97 | 8. Wait for the CloudFormation stack to launch. The stack launches some nested stacks, and can take several minutes to finish. When it’s launched, the **Status** changes to **CREATE_COMPLETE**. 98 | 9. After the stack is launched, go to **www.example.com** to view your website (replace **example.com** with your domain name). You should see the website’s default content: 99 | 100 | ![Static website page](./docs/images/static-website.png) 101 | 102 | **To replace the website’s default content with your own** 103 | 104 | 1. Go to the [Amazon S3 console](https://s3.console.aws.amazon.com/s3/home). 105 | 1. Choose the bucket whose name begins with **amazon-cloudfront-secure-static-site-s3bucketroot-**. 106 | > **Note:** Make sure to choose the bucket with **s3bucketroot** in its name, not **s3bucketlogs**. The bucket with **s3bucketroot** in its name contains the content. The one with **s3bucketlogs** contains only log files. 107 | 1. In the bucket, delete the default content, then upload your own. 108 | 109 | ### Download the CloudFormation template 110 | 111 | To download the CloudFormation template to deploy on your own, for example by [using the AWS CLI](https://docs.aws.amazon.com/cli/latest/reference/cloudformation/create-stack.html), go to: 112 | 113 | https://s3.amazonaws.com/solution-builders-us-east-1/amazon-cloudfront-secure-static-site/latest/main.yaml 114 | 115 | ## Customizing the Solution 116 | 117 | ### Update the website content locally 118 | 119 | **To customize the website with your own content before deploying the solution** 120 | 121 | 1. Install npm. For more information, go to https://www.npmjs.com/get-npm. 122 | 2. Clone or download this project from https://github.com/awslabs/aws-cloudformation-templates. 123 | 3. Run the following command to package a build artifact. 124 | 125 | ```shell 126 | make package-static 127 | ``` 128 | 129 | 4. Copy your website content into the **www** folder. 130 | 5. If you don’t have one already, create an S3 bucket to store the CloudFormation artifacts. To create one, use the following AWS CLI command: 131 | 132 | ```shell 133 | aws s3 mb s3:// 134 | ``` 135 | 136 | 6. Run the following AWS CLI command to package the CloudFormation template. The template uses the [AWS Serverless Application Model](https://aws.amazon.com/about-aws/whats-new/2016/11/introducing-the-aws-serverless-application-model/), so it must be transformed before you can deploy it. 137 | 138 | ```shell 139 | aws --region us-east-1 cloudformation package \ 140 | --template-file templates/main.yaml \ 141 | --s3-bucket \ 142 | --output-template-file packaged.template 143 | ``` 144 | 145 | 7. Run the following command to deploy the packaged CloudFormation template to a CloudFormation stack. To optionally deploy the stack with a domain apex skip this section and proceed to [Step 8] below. 146 | 147 | ```shell 148 | aws --region us-east-1 cloudformation deploy \ 149 | --stack-name \ 150 | --template-file packaged.template \ 151 | --capabilities CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND \ 152 | --parameter-overrides DomainName= SubDomain= HostedZoneId= 153 | ``` 154 | 155 | 8. [Optional] Run the following command to deploy the packaged CloudFormation template to a CloudFormation stack with a domain apex. 156 | 157 | ```shell 158 | aws --region us-east-1 cloudformation deploy \ 159 | --stack-name \ 160 | --template-file packaged.template \ 161 | --capabilities CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND \ 162 | --parameter-overrides DomainName= SubDomain= HostedZoneId= CreateApex=yes 163 | ``` 164 | 165 | ### Updating the site Response Headers 166 | 167 | To change the Response Header Policy of the site: 168 | 169 | 1. Make your changes by editing ResponseHeadersPolicy in `templates/cloudfront-site.yaml`. Here you can modify any of the headers for Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options, X-Frame-Options, X-XSS-Protection, and Referrer-Policy. 170 | 2. Deploy the solution by following the steps in [Update the website content locally](#update-the-website-content-locally) 171 | 172 | ## Contributing 173 | 174 | Contributions are welcome. Please read the [code of conduct](CODE_OF_CONDUCT.md) and the [contributing guidelines](CONTRIBUTING.md). 175 | 176 | ## License Summary 177 | 178 | This project is licensed under the Apache-2.0 License. 179 | -------------------------------------------------------------------------------- /cfn-publish.config: -------------------------------------------------------------------------------- 1 | template=templates/main.yaml 2 | acl="public-read" 3 | extra_files="witch.zip www/index.html www/css/style.css www/404.html www/other.html" 4 | bucket_name_prefix="solution-builders" 5 | regions="us-east-1" 6 | -------------------------------------------------------------------------------- /ci/cfn_nag_blacklist.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | RulesToSuppress: 3 | - id: F4 4 | reason: ignore 5 | -------------------------------------------------------------------------------- /ci/include.lst: -------------------------------------------------------------------------------- 1 | templates 2 | cfn-publish.config 3 | witch.zip 4 | www 5 | -------------------------------------------------------------------------------- /docs/images/cf-secure-static-site-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-cloudfront-secure-static-site/7316a36e515ee13195709b1f214bfe744fa334ee/docs/images/cf-secure-static-site-architecture.png -------------------------------------------------------------------------------- /docs/images/deploy-to-aws.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-cloudfront-secure-static-site/7316a36e515ee13195709b1f214bfe744fa334ee/docs/images/deploy-to-aws.png -------------------------------------------------------------------------------- /docs/images/static-website.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-cloudfront-secure-static-site/7316a36e515ee13195709b1f214bfe744fa334ee/docs/images/static-website.png -------------------------------------------------------------------------------- /source/witch/witch.js: -------------------------------------------------------------------------------- 1 | const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3'); 2 | const fs = require('node:fs'); 3 | const path = require('node:path'); 4 | const mime = require('mime-types'); 5 | const https = require('node:https'); 6 | const url = require('node:url'); 7 | 8 | const s3Client = new S3Client(); 9 | 10 | const SUCCESS = 'SUCCESS'; 11 | const FAILED = 'FAILED'; 12 | 13 | const { BUCKET } = process.env; 14 | 15 | exports.staticHandler = (event, context) => { 16 | if (event.RequestType !== 'Create' && event.RequestType !== 'Update') { 17 | return respond(event, context, SUCCESS, {}); 18 | } 19 | 20 | Promise.all( 21 | walkSync('./').map((file) => { 22 | const fileType = mime.lookup(file) || 'application/octet-stream'; 23 | 24 | console.log(`${file} -> ${fileType}`); 25 | 26 | return s3Client.send( 27 | new PutObjectCommand({ 28 | Body: fs.createReadStream(file), 29 | Bucket: BUCKET, 30 | ContentType: fileType, 31 | Key: file, 32 | ACL: 'private', 33 | }) 34 | ); 35 | }) 36 | ) 37 | .then((msg) => { 38 | respond(event, context, SUCCESS, {}); 39 | }) 40 | .catch((err) => { 41 | respond(event, context, FAILED, { Message: err }); 42 | }); 43 | }; 44 | 45 | // List all files in a directory in Node.js recursively in a synchronous fashion 46 | function walkSync(dir, filelist = []) { 47 | const files = fs.readdirSync(dir); 48 | 49 | files.forEach(function (file) { 50 | if (fs.statSync(path.join(dir, file)).isDirectory()) { 51 | filelist = walkSync(path.join(dir, file), filelist); 52 | } else { 53 | filelist.push(path.join(dir, file)); 54 | } 55 | }); 56 | 57 | return filelist; 58 | } 59 | 60 | function respond( 61 | event, 62 | context, 63 | responseStatus, 64 | responseData, 65 | physicalResourceId, 66 | noEcho 67 | ) { 68 | const responseBody = JSON.stringify({ 69 | Status: responseStatus, 70 | Reason: `See the details in CloudWatch Log Stream: ${context.logStreamName}`, 71 | PhysicalResourceId: physicalResourceId || context.logStreamName, 72 | StackId: event.StackId, 73 | RequestId: event.RequestId, 74 | LogicalResourceId: event.LogicalResourceId, 75 | NoEcho: noEcho || false, 76 | Data: responseData, 77 | }); 78 | 79 | console.log('Response body:\n', responseBody); 80 | 81 | const { pathname, hostname, search } = new url.URL(event.ResponseURL); 82 | const options = { 83 | hostname, 84 | port: 443, 85 | path: pathname + search, 86 | method: 'PUT', 87 | headers: { 88 | 'content-type': '', 89 | 'content-length': responseBody.length, 90 | }, 91 | }; 92 | 93 | const request = https.request(options, (response) => { 94 | console.log(`Status code: ${response.statusCode}`); 95 | console.log(`Status message: ${response.statusMessage}`); 96 | context.done(); 97 | }); 98 | 99 | request.on('error', (error) => { 100 | console.log(`send(..) failed executing https.request(..): ${error}`); 101 | context.done(); 102 | }); 103 | 104 | request.write(responseBody); 105 | request.end(); 106 | } 107 | -------------------------------------------------------------------------------- /templates/acm-certificate.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: ACFS3 - Certificate creation 3 | 4 | Parameters: 5 | DomainName: 6 | Type: String 7 | SubDomain: 8 | Type: String 9 | CreateApex: 10 | Type: String 11 | HostedZoneId: 12 | Type: String 13 | 14 | Conditions: 15 | CreateApexConfig: !Equals 16 | - !Ref CreateApex 17 | - 'yes' 18 | 19 | Resources: 20 | Certificate: 21 | Type: AWS::CertificateManager::Certificate 22 | Properties: 23 | DomainName: !Sub '${SubDomain}.${DomainName}' 24 | SubjectAlternativeNames: 25 | Fn::If: 26 | - CreateApexConfig 27 | - - Ref: DomainName 28 | - Ref: AWS::NoValue 29 | DomainValidationOptions: 30 | - DomainName: !Sub '${SubDomain}.${DomainName}' 31 | HostedZoneId: !Ref HostedZoneId 32 | - Fn::If: 33 | - CreateApexConfig 34 | - DomainName: !Ref DomainName 35 | HostedZoneId: !Ref HostedZoneId 36 | - Ref: AWS::NoValue 37 | ValidationMethod: DNS 38 | 39 | Outputs: 40 | CertificateArn: 41 | Description: Issued certificate 42 | Value: !Ref Certificate 43 | -------------------------------------------------------------------------------- /templates/cloudfront-site.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: ACFS3 - CloudFront with Header Security and site content 3 | Transform: 'AWS::Serverless-2016-10-31' 4 | 5 | Parameters: 6 | CertificateArn: 7 | Description: Certificate locater 8 | Type: String 9 | DomainName: 10 | Description: Apex domain 11 | Type: String 12 | SubDomain: 13 | Description: Subdomain 14 | Type: String 15 | S3BucketLogs: 16 | Description: Logging Bucket 17 | Type: String 18 | S3BucketRoot: 19 | Description: Content Bucket 20 | Type: String 21 | S3BucketLogsName: 22 | Description: Logging Bucket 23 | Type: String 24 | S3BucketRootName: 25 | Description: Content Bucket 26 | Type: String 27 | S3BucketRootArn: 28 | Description: Content Bucket locator 29 | Type: String 30 | CreateApex: 31 | Type: String 32 | 33 | Conditions: 34 | CreateApexConfig: !Equals 35 | - !Ref CreateApex 36 | - 'yes' 37 | 38 | Resources: 39 | S3BucketPolicy: 40 | Type: AWS::S3::BucketPolicy 41 | Properties: 42 | Bucket: !Ref 'S3BucketRoot' 43 | PolicyDocument: 44 | Version: '2012-10-17' 45 | Statement: 46 | - Action: s3:GetObject 47 | Principal: 48 | Service: 'cloudfront.amazonaws.com' 49 | Effect: Allow 50 | Resource: !Sub '${S3BucketRootArn}/*' 51 | Condition: 52 | StringEquals: 53 | 'AWS:SourceArn': !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}' 54 | 55 | CloudFrontDistribution: 56 | Type: AWS::CloudFront::Distribution 57 | Properties: 58 | DistributionConfig: 59 | Aliases: 60 | - !Sub '${SubDomain}.${DomainName}' 61 | - !If [CreateApexConfig, !Ref DomainName, !Ref 'AWS::NoValue'] 62 | DefaultCacheBehavior: 63 | Compress: true 64 | DefaultTTL: 86400 65 | ForwardedValues: 66 | QueryString: true 67 | MaxTTL: 31536000 68 | TargetOriginId: !Sub 'S3-${AWS::StackName}-root' 69 | ViewerProtocolPolicy: 'redirect-to-https' 70 | ResponseHeadersPolicyId: !Ref ResponseHeadersPolicy 71 | CustomErrorResponses: 72 | - ErrorCachingMinTTL: 60 73 | ErrorCode: 404 74 | ResponseCode: 404 75 | ResponsePagePath: '/404.html' 76 | - ErrorCachingMinTTL: 60 77 | ErrorCode: 403 78 | ResponseCode: 403 79 | ResponsePagePath: '/403.html' 80 | Enabled: true 81 | HttpVersion: 'http2' 82 | DefaultRootObject: 'index.html' 83 | IPV6Enabled: true 84 | Logging: 85 | Bucket: !Ref 'S3BucketLogsName' 86 | IncludeCookies: false 87 | Prefix: 'cdn/' 88 | Origins: 89 | - DomainName: !Ref 'S3BucketRootName' 90 | Id: !Sub 'S3-${AWS::StackName}-root' 91 | OriginAccessControlId: !Ref OriginAccessControl 92 | S3OriginConfig: {} 93 | PriceClass: 'PriceClass_All' 94 | ViewerCertificate: 95 | AcmCertificateArn: !Ref 'CertificateArn' 96 | MinimumProtocolVersion: 'TLSv1.1_2016' 97 | SslSupportMethod: 'sni-only' 98 | Tags: 99 | - Key: Solution 100 | Value: ACFS3 101 | 102 | OriginAccessControl: 103 | Type: AWS::CloudFront::OriginAccessControl 104 | Properties: 105 | OriginAccessControlConfig: 106 | # Get a unique ID for the OAC Config name. 107 | # Name must be unique within account 108 | Name: !Sub 109 | - amzn-secure-static-site-${guid} 110 | - guid: !Select [2, !Split ['/', !Ref 'AWS::StackId']] 111 | OriginAccessControlOriginType: s3 112 | SigningBehavior: always 113 | SigningProtocol: sigv4 114 | 115 | Route53RecordSetGroup: 116 | Type: AWS::Route53::RecordSetGroup 117 | Properties: 118 | HostedZoneName: !Sub '${DomainName}.' 119 | RecordSets: 120 | - Name: !Sub '${SubDomain}.${DomainName}' 121 | Type: 'A' 122 | AliasTarget: 123 | DNSName: !GetAtt 'CloudFrontDistribution.DomainName' 124 | EvaluateTargetHealth: false 125 | # The following HosteZoneId is always used for alias records pointing to CF. 126 | HostedZoneId: 'Z2FDTNDATAQYW2' 127 | 128 | ApexRoute53RecordSetGroup: 129 | Condition: CreateApexConfig 130 | Type: AWS::Route53::RecordSetGroup 131 | Properties: 132 | HostedZoneName: !Sub '${DomainName}.' 133 | RecordSets: 134 | - Name: !Ref 'DomainName' 135 | Type: 'A' 136 | AliasTarget: 137 | DNSName: !GetAtt 'CloudFrontDistribution.DomainName' 138 | EvaluateTargetHealth: false 139 | # The following HosteZoneId is always used for alias records pointing to CF. 140 | HostedZoneId: 'Z2FDTNDATAQYW2' 141 | 142 | ResponseHeadersPolicy: 143 | Type: AWS::CloudFront::ResponseHeadersPolicy 144 | Properties: 145 | ResponseHeadersPolicyConfig: 146 | Name: !Sub '${AWS::StackName}-static-site-security-headers' 147 | SecurityHeadersConfig: 148 | StrictTransportSecurity: 149 | AccessControlMaxAgeSec: 63072000 150 | IncludeSubdomains: true 151 | Override: true 152 | Preload: true 153 | ContentSecurityPolicy: 154 | ContentSecurityPolicy: "default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'" 155 | Override: true 156 | ContentTypeOptions: 157 | Override: true 158 | FrameOptions: 159 | FrameOption: DENY 160 | Override: true 161 | ReferrerPolicy: 162 | ReferrerPolicy: 'same-origin' 163 | Override: true 164 | XSSProtection: 165 | ModeBlock: true 166 | Override: true 167 | Protection: true 168 | 169 | Outputs: 170 | CloudFrontDistribution: 171 | Description: CloudFront distribution 172 | Value: !GetAtt CloudFrontDistribution.DomainName 173 | 174 | CloudFrontDomainName: 175 | Description: Website address 176 | Value: !Sub '${SubDomain}.${DomainName}' 177 | -------------------------------------------------------------------------------- /templates/custom-resource.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: ACFS3 - Cert Provider with DNS validation 3 | Transform: AWS::Serverless-2016-10-31 4 | 5 | Resources: 6 | CopyCustomResource: 7 | Type: 'AWS::CloudFormation::CustomResource' 8 | Properties: 9 | ServiceToken: !GetAtt CopyFunction.Arn 10 | ServiceTimeout: 30 11 | 12 | S3BucketLogs: 13 | Type: AWS::S3::Bucket 14 | DeletionPolicy: Retain 15 | Properties: 16 | OwnershipControls: 17 | Rules: 18 | - ObjectOwnership: BucketOwnerPreferred 19 | BucketEncryption: 20 | ServerSideEncryptionConfiguration: 21 | - ServerSideEncryptionByDefault: 22 | SSEAlgorithm: AES256 23 | Tags: 24 | - Key: Solution 25 | Value: ACFS3 26 | 27 | S3BucketRoot: 28 | Type: AWS::S3::Bucket 29 | DeletionPolicy: Retain 30 | Properties: 31 | BucketEncryption: 32 | ServerSideEncryptionConfiguration: 33 | - ServerSideEncryptionByDefault: 34 | SSEAlgorithm: AES256 35 | LoggingConfiguration: 36 | DestinationBucketName: !Ref 'S3BucketLogs' 37 | LogFilePrefix: 'origin/' 38 | Tags: 39 | - Key: Solution 40 | Value: ACFS3 41 | 42 | CopyLayerVersion: 43 | Type: 'AWS::Serverless::LayerVersion' 44 | Properties: 45 | ContentUri: ../witch.zip 46 | CompatibleRuntimes: 47 | - nodejs20.x 48 | 49 | CopyRole: 50 | Type: 'AWS::IAM::Role' 51 | Properties: 52 | AssumeRolePolicyDocument: 53 | Statement: 54 | - Effect: Allow 55 | Principal: 56 | Service: lambda.amazonaws.com 57 | Action: 58 | - sts:AssumeRole 59 | ManagedPolicyArns: 60 | - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 61 | Policies: 62 | - PolicyName: S3CopyPolicy 63 | PolicyDocument: 64 | Version: '2012-10-17' 65 | Statement: 66 | - Effect: Allow 67 | Action: 68 | - s3:GetObject 69 | - s3:ListBucket 70 | - s3:PutObject 71 | - s3:PutObjectAcl 72 | Resource: 73 | - !Sub 74 | - arn:aws:s3:::${TargetBucket}/* 75 | - TargetBucket: !Ref S3BucketRoot 76 | - !Sub 77 | - arn:aws:s3:::${TargetBucket} 78 | - TargetBucket: !Ref S3BucketRoot 79 | 80 | CopyFunction: 81 | Type: AWS::Serverless::Function 82 | Properties: 83 | CodeUri: ../www 84 | Environment: 85 | Variables: 86 | BUCKET: !Ref S3BucketRoot 87 | Handler: witch.staticHandler 88 | Layers: 89 | - !Ref CopyLayerVersion 90 | Role: !GetAtt CopyRole.Arn 91 | Runtime: nodejs20.x 92 | Timeout: 300 93 | 94 | Outputs: 95 | S3BucketRoot: 96 | Description: Website bucket 97 | Value: !Ref S3BucketRoot 98 | S3BucketRootName: 99 | Description: Website bucket name 100 | Value: !GetAtt S3BucketRoot.DomainName 101 | S3BucketRootArn: 102 | Description: Website bucket locator 103 | Value: !GetAtt S3BucketRoot.Arn 104 | S3BucketLogs: 105 | Description: Logging bucket 106 | Value: !Ref S3BucketLogs 107 | S3BucketLogsName: 108 | Description: Logging bucket Name 109 | Value: !GetAtt S3BucketLogs.DomainName 110 | CopyFunction: 111 | Description: S3 helper function 112 | Value: !GetAtt CopyFunction.Arn 113 | -------------------------------------------------------------------------------- /templates/main.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Description: | 3 | ACFS3 - S3 Static site with CF and ACM 4 | https://github.com/aws-samples/amazon-cloudfront-secure-static-site 5 | (uksb-1qnk6ni7b) (version:v0.12) 6 | 7 | Metadata: 8 | AWS::CloudFormation::Interface: 9 | ParameterGroups: 10 | - Label: 11 | default: Domain 12 | Parameters: 13 | - SubDomain 14 | - DomainName 15 | 16 | Mappings: 17 | Solution: 18 | Constants: 19 | Version: 'v0.13' 20 | 21 | Rules: 22 | OnlyUsEast1: 23 | Assertions: 24 | - Assert: 25 | Fn::Equals: 26 | - !Ref AWS::Region 27 | - us-east-1 28 | AssertDescription: | 29 | This template can only be deployed in the us-east-1 region. 30 | This is because the ACM Certificate must be created in us-east-1 31 | 32 | Parameters: 33 | SubDomain: 34 | Description: The part of a website address before your DomainName - e.g. www or img 35 | Type: String 36 | Default: www 37 | AllowedPattern: ^[^.]*$ 38 | DomainName: 39 | Description: The part of a website address after your SubDomain - e.g. example.com 40 | Type: String 41 | HostedZoneId: 42 | Description: HostedZoneId for the domain e.g. Z23ABC4XYZL05B 43 | Type: String 44 | CreateApex: 45 | Description: Create an Apex Alias in CloudFront distribution - yes/no 46 | Type: String 47 | Default: 'no' 48 | AllowedValues: ['yes', 'no'] 49 | 50 | Resources: 51 | CustomResourceStack: 52 | Type: AWS::CloudFormation::Stack 53 | Properties: 54 | TemplateURL: ./custom-resource.yaml 55 | Tags: 56 | - Key: Solution 57 | Value: ACFS3 58 | 59 | AcmCertificateStack: 60 | Type: AWS::CloudFormation::Stack 61 | Properties: 62 | TemplateURL: ./acm-certificate.yaml 63 | Parameters: 64 | SubDomain: !Ref SubDomain 65 | DomainName: !Ref DomainName 66 | CreateApex: !Ref CreateApex 67 | HostedZoneId: !Ref HostedZoneId 68 | Tags: 69 | - Key: Solution 70 | Value: ACFS3 71 | 72 | CloudFrontStack: 73 | Type: AWS::CloudFormation::Stack 74 | Properties: 75 | TemplateURL: ./cloudfront-site.yaml 76 | Parameters: 77 | CertificateArn: !GetAtt AcmCertificateStack.Outputs.CertificateArn 78 | DomainName: !Ref DomainName 79 | SubDomain: !Ref SubDomain 80 | CreateApex: !Ref CreateApex 81 | S3BucketRoot: !GetAtt CustomResourceStack.Outputs.S3BucketRoot 82 | S3BucketRootName: !GetAtt CustomResourceStack.Outputs.S3BucketRootName 83 | S3BucketRootArn: !GetAtt CustomResourceStack.Outputs.S3BucketRootArn 84 | S3BucketLogs: !GetAtt CustomResourceStack.Outputs.S3BucketLogs 85 | S3BucketLogsName: !GetAtt CustomResourceStack.Outputs.S3BucketLogsName 86 | Tags: 87 | - Key: Solution 88 | Value: ACFS3 89 | 90 | Outputs: 91 | SolutionVersion: 92 | Value: !FindInMap [Solution, Constants, Version] 93 | CopyFunction: 94 | Description: S3 helper function 95 | Value: !GetAtt CustomResourceStack.Outputs.CopyFunction 96 | S3BucketLogs: 97 | Description: Logging bucket 98 | Value: !GetAtt CustomResourceStack.Outputs.S3BucketLogs 99 | S3BucketRoot: 100 | Description: Website bucket 101 | Value: !GetAtt CustomResourceStack.Outputs.S3BucketRoot 102 | S3BucketLogsName: 103 | Description: Logging bucket name 104 | Value: !GetAtt CustomResourceStack.Outputs.S3BucketLogsName 105 | S3BucketRootName: 106 | Description: Website bucket name 107 | Value: !GetAtt CustomResourceStack.Outputs.S3BucketRootName 108 | CertificateArn: 109 | Description: Issued certificate 110 | Value: !GetAtt AcmCertificateStack.Outputs.CertificateArn 111 | CFDistributionName: 112 | Description: CloudFront distribution 113 | Value: !GetAtt CloudFrontStack.Outputs.CloudFrontDistribution 114 | CloudFrontDomainName: 115 | Description: Website address 116 | Value: !GetAtt CloudFrontStack.Outputs.CloudFrontDomainName 117 | -------------------------------------------------------------------------------- /www/403.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | I am another page from a static website! 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

403 Forbidden!

14 | 15 |

Are you really sure you don't want to be over at this page??

16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /www/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | I am another page from a static website! 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

404 not found!

14 | 15 |

Are you sure you didn't mean to go to this page??

16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /www/css/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: AmazonEmber; 3 | src: url("https://a0.awsstatic.com/libra-css/fonts/amazon-ember/AmazonEmber_Lt.woff"); 4 | font-weight: 400; 5 | font-style: normal; 6 | font-size: 12pt; 7 | } 8 | 9 | body { 10 | background: #161E2D; 11 | font-family: AmazonEmber, Helvetica Neue, Helvetica, Arial, sans-serif; 12 | color: #ddd; 13 | max-width: 75em; 14 | margin: 0 auto; 15 | display: flex; 16 | flex-direction: column; 17 | align-items: center; 18 | justify-content: center; 19 | height: 100vh; 20 | } 21 | 22 | h1 img { 23 | vertical-align: middle; 24 | } 25 | 26 | a { 27 | color: #44b9d6; 28 | text-decoration: none; 29 | } 30 | 31 | a:hover { 32 | text-decoration: underline; 33 | } 34 | -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | I am a static website! 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |

I am a static website!

13 | 14 |

Great, huh? Here's a link to another page.

15 | 16 | 17 | -------------------------------------------------------------------------------- /www/other.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | I am another page from a static website! 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |

I am another page from a static website!

13 | 14 |

Even better than the first page, don't you think?

15 | 16 | 17 | -------------------------------------------------------------------------------- /www/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | --------------------------------------------------------------------------------