├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── applications ├── mservice1 │ ├── .dockerignore │ ├── Dockerfile │ ├── app1.py │ ├── dockerpush.sh │ └── requirements.txt ├── mservice2 │ └── app2lambda.py └── portal │ ├── Dockerfile │ ├── dockerpush.sh │ ├── index.py │ ├── requirements.txt │ ├── static │ └── images │ │ ├── adam.png │ │ ├── arrow.png │ │ ├── backdrop.png │ │ ├── clientvpn.png │ │ ├── favicon.ico │ │ ├── mrx.png │ │ ├── mservice1_open.png │ │ ├── mservice1_secure.png │ │ ├── mservice2_open.png │ │ ├── mservice2_secure.png │ │ ├── pablo.png │ │ ├── teapot.png │ │ ├── verifiedaccess.png │ │ └── wait.jpg │ └── templates │ └── index.html ├── backend ├── iam.tf ├── locals.tf ├── main.tf ├── outputs.tf ├── providers.tf └── variables.tf ├── frontend ├── iam.tf ├── main.tf ├── outputs.tf ├── providers.tf └── variables.tf ├── modules ├── retrieve_parameters │ ├── main.tf │ ├── outputs.tf │ ├── providers.tf │ └── variables.tf └── share_parameter │ ├── main.tf │ ├── outputs.tf │ ├── providers.tf │ └── variables.tf └── network ├── main.tf ├── outputs.tf ├── providers.tf └── variables.tf /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/terraform 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=terraform 3 | 4 | ### Terraform ### 5 | # Local .terraform directories 6 | **/.terraform/* 7 | 8 | # .tfstate files 9 | *.tfstate 10 | *.tfstate.* 11 | *.lock.hcl 12 | # Crash log files 13 | crash.log 14 | 15 | # Exclude all .tfvars files, which are likely to contain sentitive data, such as 16 | # password, private keys, and other secrets. These should not be part of version 17 | # control as they are data points which are potentially sensitive and subject 18 | # to change depending on the environment. 19 | # 20 | *.tfvars 21 | 22 | # Ignore override files as they are usually used to override resources locally and so 23 | # are not checked in 24 | override.tf 25 | override.tf.json 26 | *_override.tf 27 | *_override.tf.json 28 | 29 | # Include override files you do wish to add to version control using negated pattern 30 | # !example_override.tf 31 | 32 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 33 | # example: *tfplan* 34 | 35 | # Ignore CLI configuration files 36 | .terraformrc 37 | terraform.rc 38 | 39 | # End of https://www.toptal.com/developers/gitignore/api/terraform 40 | 41 | # macOs 42 | .DS_Store 43 | 44 | # Project 45 | keys/ 46 | 47 | gcm-diagnose.log 48 | 49 | **/client_vpn_config/* -------------------------------------------------------------------------------- /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 *main* 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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Moving to a Zero Trust architecture in AWS using Infrastructure-as-Code 2 | 3 | In this repository, you will leverage [AWS Verified Access](https://aws.amazon.com/verified-access/) and [Amazon VPC Lattice](https://aws.amazon.com/vpc/lattice/) to simplify the connectivity between services, while improving the security posture of the communication. The Infrastructure-as-Code framework used is **Terraform**. 4 | 5 | This example requires the use of 3 AWS Accounts: one for the central Networking resources; one for the frontend application (hosted using [Amazon ECS Fargate](https://aws.amazon.com/fargate/)), and another one for the backend applications (hosted using Fargate and [AWS Lambda](https://aws.amazon.com/pm/lambda/)). The following resources are created: 6 | 7 | **Networking Account** 8 | 9 | * Amazon VPC Lattice service network, which is shared with the AWS Organization using [AWS Resource Access Manager](https://aws.amazon.com/ram/). 10 | * [Amazon Route 53 Profile](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/profiles.html) with a [Private Hosted Zone](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/hosted-zones-private.html) associated. 11 | * The Profile is shared using AWS RAM. 12 | * The Private Hosted Zone contains the corresponding CNAME records translating the VPC Lattice services' custom domain names to the VPC Lattice generated domain names. 13 | * [AWS Systems Manager Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html) is used to share information between AWS Accounts - Route 53 Profile ID and VPC Lattice service network ARN. 14 | 15 | **Frontend Account** 16 | 17 | * Amazon VPC hosting a Amazon ECS Fargate cluster for the *frontend application*. The Fargate service is the target of an [Application Load Balancer](https://aws.amazon.com/elasticloadbalancing/) 18 | * VPC is associated to the VPC Lattice service network shared by the Networking AWS Account. 19 | * [Amazon ECR](https://aws.amazon.com/ecr/) repository to store the *frontend application* code. 20 | * AWS Verified Access resources to access the *frontend application* VPN-less with user authentication and authorization. The use of [AWS Identity Center](https://aws.amazon.com/iam/identity-center/) is expected, but not created in this repository. 21 | 22 | **Backend Account** 23 | 24 | * Amazon VPC hosting a Amazon ECS Fargate cluster for the *mservice1 application*. The Fargate service is the target of an Application Load Balancer. 25 | * VPC is associated to the VPC Lattice service network shared by the Networking AWS Account. 26 | * Amazon ECR repository to store the *mservice1 application* code. 27 | * AWS Lambda function containing *mservice2 application*. 28 | * VPC Lattice services for *mservice1* and *mservice2* applications. Both services are associated to the VPC Lattice service network associated by the Networking AWS Account. 29 | 30 | ## Pre-requisites 31 | 32 | * When using several AWS Accounts, make sure you use different AWS credentials when initializing the provider in each folder. 33 | * This repository does not configure AWS Identity Center. Check the [documentation](https://docs.aws.amazon.com/singlesignon/latest/userguide/tutorials.html) to understand how to enable it in your AWS Accounts (if not done already). 34 | * Terraform installed. 35 | 36 | ## Signing Assertion 37 | 38 | For enhanced security in your deployment, you should add code signing assertion to requests made by AWS Verified Access to your back-end applications. To implement this, simply modify the code signing entity **Authsigner** component within [index.py](./applications/portal/index.py) and then enforce assertion of the expected value within the **depacker()** function. An example of this signing assertion can be seen in the [AWS documentation](https://docs.aws.amazon.com/verified-access/latest/ug/user-claims-passing.html#sample-code) 39 | 40 | ## Code Principles 41 | 42 | * Writing DRY (Do No Repeat Yourself) code using a modular design pattern. 43 | 44 | ## Usage 45 | 46 | * Clone the repository 47 | 48 | ``` 49 | git clone https://github.com/aws-samples/moving-to-a-zero-trust-architecture-in-aws 50 | ``` 51 | 52 | Edit the *variables.tf* file in each folder to configure the environment (AWS Region to use must be the same in all AWS Accounts): 53 | 54 | * Networking AWS Account: 55 | * AWS Region to use. 56 | * Public Hosted Zone ID (for AWS Verified Access endpoint resolution) 57 | * Private Hosted Zone name (for Amazon VPC Lattice services' resolution) 58 | * *Frontend* and *backend* AWS Account IDs. 59 | * *Frontend*, *mservice1* and *mservice2* domain names. 60 | * Frontend AWS Account: 61 | * AWS Region to use 62 | * [Amazon Certificate Manager](https://aws.amazon.com/certificate-manager/) certificate ARN (for HTTPS in Verified Access endpoint). 63 | * *Frontend* application domain name. 64 | * Networking AWS Account ID. 65 | * Identity Center Group ID. 66 | * Backend AWS Account: 67 | * AWS Region to use 68 | * Amazon Certificate Manager certificate ARN (for HTTPS in VPC Lattice services). 69 | * *mservice1* and *mservice2* application domain names. 70 | * Networking AWS Account ID. 71 | * IAM Role ARN used for the *frontend* application. This is used in VPC Lattice auth policies. 72 | * To share parameters between AWS Accounts, you will need to provide the Account ID of the corresponding Account in each folder. 73 | * We recommend the use of tha *tfvars* file to provide the required parameters. 74 | 75 | ## Deployment 76 | 77 | * **Step 1**: Networking Account resources 78 | 79 | ``` 80 | cd network/ 81 | terraform apply 82 | ``` 83 | 84 | * **Step 2**: Frontend Account resources 85 | 86 | ``` 87 | cd frontend/ 88 | terraform apply 89 | ``` 90 | 91 | * **Step 3**: Deploying *frontend* application 92 | * Move to /applications/portal and change the following files: 93 | * In `dockerpush.sh`, provide AWS Region used and the AWS Account ID for the *frontend* application in lines 3, 5, and 6. 94 | * In `index.py`, provide *mservice1* domain name in lines 24 and 25. Also add the AWS Region used in lines 27 and 32. 95 | * Build and push the code to the ECR repository created in Step 2. 96 | 97 | ``` 98 | cd applications/portal/ 99 | ./dockerpush.sh 100 | ``` 101 | 102 | * **Step 4**: Backend Account resources 103 | 104 | ``` 105 | cd backend/ 106 | terraform apply 107 | ``` 108 | 109 | * **Step 5**: Deploying *mservice1* application 110 | * Move to /applications/mservice1 and change the following files: 111 | * In `dockerpush.sh`, provide AWS Region used and the AWS Account ID for the *frontend* application in lines 3, 5, and 6. 112 | * In `app1.py`, provide *mservice2* domain name in lines 17 and 18. Also add the AWS Region used in line 24. 113 | * Build and push the code to the ECR repository created in Step 4. 114 | 115 | ``` 116 | cd applications/mservice1/ 117 | ./dockerpush.sh 118 | ``` 119 | 120 | ## Security 121 | 122 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 123 | 124 | ## License 125 | 126 | This library is licensed under the MIT-0 License. See the LICENSE file. -------------------------------------------------------------------------------- /applications/mservice1/.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/bin 15 | **/charts 16 | **/docker-compose* 17 | **/compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md 26 | -------------------------------------------------------------------------------- /applications/mservice1/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM amazonlinux:latest 2 | LABEL Name=app1 Version=0.0.1 3 | RUN yum update -y && \ 4 | yum install -y python3-pip net-tools bind-utils iputils 5 | COPY . /app 6 | RUN pip install -r /app/requirements.txt 7 | CMD ["python3", "/app/app1.py"] -------------------------------------------------------------------------------- /applications/mservice1/app1.py: -------------------------------------------------------------------------------- 1 | # Import Modules 2 | 3 | from flask import Flask, request 4 | import requests 5 | from botocore.awsrequest import AWSRequest 6 | from botocore.credentials import Credentials 7 | from botocore.auth import SigV4Auth 8 | import botocore.session 9 | import json 10 | import logging 11 | 12 | logging.basicConfig(level=logging.INFO) 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | # Define Variables 17 | mservice2 = "{DOMAIN_NAME}" 18 | mservice2sec = "{DOMAIN_NAME}/secure" 19 | 20 | # Secure signer function 21 | 22 | def signer(endpoint): 23 | session = botocore.session.Session() 24 | signer = SigV4Auth(session.get_credentials(), 'vpc-lattice-svcs', '{REGION}') 25 | endpoint = endpoint 26 | data = "null" 27 | headers = {} 28 | request = AWSRequest(method='GET', url=endpoint, data=data, headers=headers) 29 | request.context["payload_signing_enabled"] = False # payload signing is not supported 30 | signer.add_auth(request) 31 | prepped = request.prepare() 32 | return prepped 33 | 34 | app = Flask(__name__) 35 | 36 | @app.route("/", methods=['GET']) 37 | def app1resp(): 38 | 39 | try: 40 | requestdata = request.json 41 | m2aresp = requests.get(mservice2,json=requestdata) 42 | m2payload = json.loads(m2aresp.content) 43 | 44 | data = { 45 | 'app1headers' : dict(request.headers), 46 | 'app1url' : request.url, 47 | 'app1message' : "Welcome to mservice1 - this is a simple open API - it calls mservice2", 48 | 'app1information' : "Make a connection to the secure endpoint for secrets", 49 | 'app2headers' : m2payload['app2headers'], 50 | 'app2url' : m2aresp.url, 51 | 'app2message' : m2payload['app2message'], 52 | 'app2information' : m2payload['app2information'], 53 | 'app2status' : m2aresp.status_code 54 | } 55 | 56 | if requestdata['portalidname'] == 'Anonymous': 57 | resp = 418 58 | else: 59 | resp = 200 60 | 61 | response = app.response_class( 62 | response=json.dumps(data), 63 | status=resp, 64 | mimetype='application/json' 65 | ) 66 | return response 67 | 68 | except Exception as e: 69 | logger.info(e) 70 | 71 | data = { 72 | 'app1headers' : '', 73 | 'app1url' : '', 74 | 'app1message' : '', 75 | 'app1information' : '', 76 | 'app2headers' : '', 77 | 'app2url' : '', 78 | 'app2message' : '', 79 | 'app2information' : '', 80 | 'app2status' : '' 81 | } 82 | 83 | response = app.response_class( 84 | response=json.dumps(data), 85 | status=500, 86 | mimetype='application/json' 87 | ) 88 | return response 89 | 90 | @app.route("/secure", methods=['GET','POST']) 91 | def app1respsec(): 92 | 93 | try: 94 | headers = dict(request.headers) 95 | 96 | ## Comment out the below line when switching to signed requests 97 | m2asecresp = requests.get(mservice2sec) 98 | ## Uncomment the below line when switching to signed requests 99 | #prepped = signer(mservice2sec) 100 | #m2asecresp = requests.get(prepped.url, headers=prepped.headers) 101 | 102 | m2secpayload = json.loads(m2asecresp.content) 103 | 104 | if "X-Amz-Content-Sha256" in headers: 105 | resp = 200 106 | data = { 107 | 'app1secmessage' : 'Welcome to mservice1 (secure) - this contains sensitive cat treat information', 108 | 'app1secinformation' : 'Cats > that Dogs, it is a fact :-D ', 109 | 'app1secdetails' : 'House cats share 95.6 percent of their genetic makeup with tigers.', 110 | 'app1secheaders' : dict(request.headers), 111 | 'app1securl' : request.url, 112 | 'app2secmessage' : m2secpayload['app2secmessage'], 113 | 'app2secinformation' : m2secpayload['app2secinformation'], 114 | 'app2secdetails' : m2secpayload['app2secdetails'], 115 | 'app2secheaders' : m2secpayload['app2secheaders'], 116 | 'app2securl' : m2asecresp.url, 117 | 'app2secstatus' : m2asecresp.status_code 118 | } 119 | else: 120 | resp = 418 121 | data = { 122 | 'app1secmessage' : 'Welcome to mservice1 (secure) - this contains sensitive cat treat information', 123 | 'app1secinformation' : '[For more info, sign your requests]', 124 | 'app1secdetails' : '[For more info, sign your requests]', 125 | 'app1secheaders' : dict(request.headers), 126 | 'app1securl' : request.url, 127 | 'app2secmessage' : m2secpayload['app2secmessage'], 128 | 'app2secinformation' : m2secpayload['app2secinformation'], 129 | 'app2secdetails' : m2secpayload['app2secdetails'], 130 | 'app2secheaders' : m2secpayload['app2secheaders'], 131 | 'app2securl' : m2asecresp.url, 132 | 'app2secstatus' : m2asecresp.status_code 133 | } 134 | 135 | response = app.response_class( 136 | response=json.dumps(data), 137 | status=resp, 138 | mimetype='application/json' 139 | ) 140 | 141 | return response 142 | 143 | except Exception as e: 144 | logger.info(e) 145 | 146 | data = { 147 | 'app1secmessage' : '', 148 | 'app1secinformation' : '', 149 | 'app1secdetails' : '', 150 | 'app1secheaders' : '', 151 | 'app1securl' : '', 152 | 'app2secmessage' : '', 153 | 'app2secinformation' : '', 154 | 'app2secdetails' : '', 155 | 'app2secheaders' : '', 156 | 'app2securl' : '', 157 | 'app2secstatus' : '' 158 | } 159 | 160 | response = app.response_class( 161 | response=json.dumps(data), 162 | status=500, 163 | mimetype='application/json' 164 | ) 165 | 166 | return response 167 | 168 | @app.route("/health", methods=['GET']) 169 | def hc(): 170 | response = app.response_class( 171 | status=200, 172 | mimetype='application/json' 173 | ) 174 | return response 175 | 176 | if __name__ == '__main__': 177 | app.run(debug=True,host="0.0.0.0",port=8081) -------------------------------------------------------------------------------- /applications/mservice1/dockerpush.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | aws ecr get-login-password --region {REGION} | docker login --username AWS --password-stdin {ACCOUNT_ID}.dkr.ecr.{REGION}.amazonaws.com/mservice1 4 | docker build -t frontend . 5 | docker tag frontend:latest {ACCOUNT_ID}.dkr.ecr.{REGION}.amazonaws.com/mservice1:latest 6 | docker push {ACCOUNT_ID}.dkr.ecr.{REGION}.amazonaws.com/mservice1:latest -------------------------------------------------------------------------------- /applications/mservice1/requirements.txt: -------------------------------------------------------------------------------- 1 | ###### Requirements without Version Specifiers ###### 2 | flask == 3.0.2 3 | requests == 2.31.0 4 | botocore == 1.34.70 5 | awscrt == 0.20.9 -------------------------------------------------------------------------------- /applications/mservice2/app2lambda.py: -------------------------------------------------------------------------------- 1 | # Import Modules 2 | import json 3 | 4 | def lambda_handler(event, context): 5 | 6 | # Obtaining headers and requestContent from event 7 | headers = event['headers'] 8 | requestContext = event['requestContext'] 9 | output = dict(**headers, **requestContext) 10 | 11 | if event['path'] == "/": 12 | 13 | body = json.loads(event['body']) 14 | 15 | data = { 16 | 'app2message' : 'Welcome to the mservice2 - this is a simple open API', 17 | 'app2information' : 'Make a connection to the secure endpoint for secrets', 18 | 'app2headers' : output 19 | } 20 | 21 | if body['portalidname'] == 'Anonymous': 22 | resp = 418 23 | else: 24 | resp = 200 25 | 26 | return { 27 | 'statusCode': resp, 28 | 'body': json.dumps(data) 29 | } 30 | 31 | else: 32 | 33 | ### Check for Connection Type 34 | 35 | if "x-amz-content-sha256" in headers: 36 | resp = 200 37 | data = { 38 | 'app2secmessage' : 'Welcome to the mservice2(secure) - this contains sensitive cat treat information', 39 | 'app2secinformation' : 'A word on feline capabilities', 40 | 'app2secdetails' : 'Cats can jump 5 times their own height', 41 | 'app2secheaders' : output 42 | } 43 | else: 44 | resp = 418 45 | data = { 46 | 'app2secmessage' : 'Welcome to the mservice2(secure) - this contains sensitive cat treat information', 47 | 'app2secinformation' : '[For more info, sign your requests]', 48 | 'app2secdetails' : '[For more info, sign your requests]', 49 | 'app2secheaders' : output 50 | } 51 | 52 | return { 53 | 'statusCode': resp, 54 | 'body': json.dumps(data) 55 | } -------------------------------------------------------------------------------- /applications/portal/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM amazonlinux:latest 2 | LABEL Name=portal Version=0.0.1 3 | RUN yum update -y && \ 4 | yum install -y python3-pip net-tools bind-utils iputils 5 | COPY . /app 6 | RUN pip install -r /app/requirements.txt 7 | CMD ["python3", "/app/index.py"] -------------------------------------------------------------------------------- /applications/portal/dockerpush.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | aws ecr get-login-password --region {REGION} | docker login --username AWS --password-stdin {ACCOUNT_ID}.dkr.ecr.{REGION}.amazonaws.com 4 | docker build -t frontend . 5 | docker tag frontend:latest {ACCOUNT_ID}.dkr.ecr.{REGION}.amazonaws.com/frontend:latest 6 | docker push {ACCOUNT_ID}.dkr.ecr.{REGION}.amazonaws.com/frontend:latest -------------------------------------------------------------------------------- /applications/portal/index.py: -------------------------------------------------------------------------------- 1 | #import modules 2 | 3 | import os 4 | from flask import Flask, render_template, request, redirect, url_for 5 | import requests 6 | from botocore.awsrequest import AWSRequest 7 | from botocore.credentials import Credentials 8 | from botocore.auth import SigV4Auth 9 | import botocore.session 10 | import json 11 | import base64 12 | import jwt 13 | import logging 14 | 15 | logging.basicConfig(level=logging.INFO) 16 | logger = logging.getLogger(__name__) 17 | 18 | #define variables 19 | 20 | img = os.path.join('static','images') 21 | global portal 22 | render = {} 23 | 24 | mservice1 = "{DOMAIN_NAME}" 25 | mservice1sec = "{DOMAIN_NAME}/secure" 26 | 27 | avaurl = 'https://public-keys.prod.verified-access.{REGION}.amazonaws.com/' 28 | 29 | ## ---------------------------------------- security notice ------------------------------------------- ## 30 | ## For increased security, you should validate the signing entity for requests made to your application ## 31 | ## To do that, replace the value below for the 'authsigner' with the verified access instanceID which ## 32 | ## takes the format 'arn:aws:ec2:region:111111111111:verified-access-instance/vai-abcd12345efghij89' ## 33 | ## following this, uncomment line #64 to assert the value ## 34 | ## ---------------------------------------- security notice ------------------------------------------- ## 35 | 36 | authsigner = 'arn:aws:ec2:region:accountid:verified-access-instance/instanceid' 37 | 38 | # Secure signer function 39 | def signer(endpoint): 40 | session = botocore.session.Session() 41 | signer = SigV4Auth(session.get_credentials(), 'vpc-lattice-svcs', '{REGION}') 42 | endpoint = endpoint 43 | data = "null" 44 | headers = {} 45 | request = AWSRequest(method='GET', url=endpoint, data=data, headers=headers) 46 | request.context["payload_signing_enabled"] = False # payload signing is not supported 47 | signer.add_auth(request) 48 | prepped = request.prepare() 49 | return prepped 50 | 51 | # AVA depacker function 52 | def depacker(portal): 53 | 54 | if "X-Amzn-Ava-User-Context" in portal: 55 | 56 | #Decode the headers 57 | encoded_jwt = portal['X-Amzn-Ava-User-Context'] 58 | jwt_headers = encoded_jwt.split('.')[0] 59 | decoded_jwt_headers = base64.b64decode(jwt_headers) 60 | decoded_jwt_headers = decoded_jwt_headers.decode("utf-8") 61 | decoded_json = json.loads(decoded_jwt_headers) 62 | kid = decoded_json['kid'] 63 | 64 | # assert authsigner in decoded_json['signer'] 65 | 66 | #Get the public key 67 | url = avaurl + kid 68 | req = requests.get(url) 69 | pub_key = req.text 70 | 71 | #Get the payload 72 | payload = jwt.decode(encoded_jwt, pub_key, algorithms=['ES384']) 73 | 74 | ptype = os.path.join(img,'verifiedaccess.png') 75 | pid = os.path.join(img, payload['user']['user_name'] + '.png') 76 | pidname = payload['user']['user_name'] 77 | 78 | else: 79 | ptype = os.path.join(img, 'clientvpn.png') 80 | pid = os.path.join(img, 'mrx.png') 81 | pidname = "Anonymous" 82 | 83 | response = { 84 | "portaltype" : ptype, 85 | "portalid" : pid, 86 | "portalidname" : pidname 87 | } 88 | return response 89 | 90 | # Portal Application 91 | app = Flask(__name__) 92 | 93 | @app.route('/', methods=['GET']) 94 | def getBaselinePictures(): 95 | 96 | portal = depacker(dict(request.headers)) 97 | 98 | render = { 99 | "app1img" : os.path.join(img,'wait.jpg'), 100 | "app1secimg" : os.path.join(img,'wait.jpg'), 101 | "app2img" : os.path.join(img,'wait.jpg'), 102 | "app2secimg" : os.path.join(img,'wait.jpg'), 103 | 'portalmessage' : "Welcome to the Feline microservice portal", 104 | 'portalinformation' : "Build a zero-trust layer to reveal feline imagery" if portal['portalidname'] == 'Anonymous' else "Welcome " + portal['portalidname'] + " - check the cat pictures (if you can) !", 105 | 'portalheaders' : dict(request.headers), 106 | "portaltype" : portal['portaltype'], 107 | "portalid" : portal['portalid'], 108 | "portalidname" : portal['portalidname'], 109 | 'app1headers' : '', 110 | 'app1url' : '', 111 | 'app1status' : '', 112 | 'app1message' : '', 113 | 'app1information' : '', 114 | 'app2headers' : '', 115 | 'app2url' : '', 116 | 'app2message' : '', 117 | 'app2information' : '', 118 | 'app2status' : '', 119 | 'app1secstatus' : '', 120 | 'app1secmessage' : '', 121 | 'app1secinformation' : '', 122 | 'app1secdetails' : '', 123 | 'app1secheaders' : '', 124 | 'app1securl' : '', 125 | 'app2secmessage' : '', 126 | 'app2secinformation' : '', 127 | 'app2secdetails' : '', 128 | 'app2secheaders' : '', 129 | 'app2securl' : '', 130 | 'app2secstatus' : '' 131 | } 132 | 133 | return render_template('index.html', render=render) 134 | 135 | @app.route('/run', methods=['GET','POST']) 136 | def runapp(): 137 | 138 | portal = depacker(dict(request.headers)) 139 | 140 | portaldata = { 141 | 'portalmessage' : "Welcome to the Feline microservice portal", 142 | 'portalinformation' : "Build a zero-trust layer to reveal feline imagery" if portal['portalidname'] == 'Anonymous' else "Welcome " + portal['portalidname'] + " - check the cat pictures (if you can) !", 143 | 'portalheaders' : dict(request.headers), 144 | 'portaltype' : portal['portaltype'], 145 | 'portalid' : portal['portalid'], 146 | 'portalidname' : portal['portalidname'], 147 | } 148 | 149 | try: 150 | logger.info("Attempting connection to mservice1 unsecured endpoint") 151 | m1aresp = requests.get(mservice1,json=portal) 152 | payload = json.loads(m1aresp.content) 153 | 154 | render1 = { 155 | 'app1img' : os.path.join(img,'mservice1_open.png') if m1aresp.status_code == 200 else os.path.join(img,'teapot.png') if m1aresp.status_code == 418 else os.path.join(img,'wait.png'), 156 | 'app2img' : os.path.join(img,'mservice2_open.png') if payload['app2status'] == 200 else os.path.join(img,'teapot.png') if payload['app2status'] == 418 else os.path.join(img,'wait.png'), 157 | 'app1headers' : payload['app1headers'], 158 | 'app1url' : payload['app1url'], 159 | 'app1status' : m1aresp.status_code, 160 | 'app1message' : payload['app1message'], 161 | 'app1information' : payload['app1information'], 162 | 'app2headers' : payload['app2headers'], 163 | 'app2url' : payload['app2url'], 164 | 'app2message' : payload['app2message'], 165 | 'app2information' : payload['app2information'], 166 | 'app2status' : payload['app2status'] 167 | } 168 | render.update(render1) 169 | 170 | except Exception as e: 171 | logger.info("There was an issue connecting to mservice1 unsecured endpoint - error deatils: " + str(e)) 172 | 173 | render1 = { 174 | 'app1img' : os.path.join(img,'wait.jpg'), 175 | 'app2img' : os.path.join(img,'wait.jpg'), 176 | 'app1headers' : "Communication Error", 177 | 'app1url' : "Communication Error", 178 | 'app1status' : "Communication Error", 179 | 'app1message' : "Communication Error", 180 | 'app1information' : "Communication Error", 181 | 'app2headers' : "Communication Error", 182 | 'app2url' : "Communication Error", 183 | 'app2message' : "Communication Error", 184 | 'app2information' : "Communication Error", 185 | 'app2status' : "Communication Error" 186 | } 187 | render.update(render1) 188 | 189 | try: 190 | logger.info("Attempting connection to mservice1 secured endpoint") 191 | ## Comment out the below line when switching to signed requests 192 | m1bresp = requests.get(mservice1sec) 193 | ## Uncomment the below two lines when switching to signed requests 194 | # prepped = signer(mservice1sec) 195 | # m1bresp = requests.get(prepped.url,headers=prepped.headers,json=portal) 196 | 197 | payload2 = json.loads(m1bresp.content) 198 | render2 = { 199 | 'app1secimg' : os.path.join(img,'mservice1_secure.png') if m1bresp.status_code == 200 else os.path.join(img,'teapot.png') if m1bresp.status_code == 418 else os.path.join(img,'wait.png'), 200 | 'app2secimg' : os.path.join(img,'mservice2_secure.png') if payload2['app2secstatus'] == 200 else os.path.join(img,'teapot.png') if payload2['app2secstatus'] == 418 else os.path.join(img,'wait.png'), 201 | 'app1secstatus' : m1bresp.status_code, 202 | 'app1secmessage' : payload2['app1secmessage'], 203 | 'app1secinformation' : payload2['app1secinformation'], 204 | 'app1secdetails' : payload2['app1secdetails'], 205 | 'app1secheaders' : payload2['app1secheaders'], 206 | 'app1securl' : payload2['app1securl'], 207 | 'app2secmessage' : payload2['app2secmessage'], 208 | 'app2secinformation' : payload2['app2secinformation'], 209 | 'app2secdetails' : payload2['app2secdetails'], 210 | 'app2secheaders' : payload2['app2secheaders'], 211 | 'app2securl' : payload2['app2securl'], 212 | 'app2secstatus' : payload2['app2secstatus'] 213 | } 214 | 215 | render.update(render2) 216 | 217 | except Exception as e: 218 | logger.info("There was an issue connecting to mservice1 secured endpoint - error deatils: " + str(e)) 219 | 220 | render2 = { 221 | 'app1secimg' : os.path.join(img,'wait.jpg'), 222 | 'app2secimg' : os.path.join(img,'wait.jpg'), 223 | 'app1secstatus' : "Communication Error", 224 | 'app1secmessage' : "Communication Error", 225 | 'app1secinformation' : "Communication Error", 226 | 'app1secdetails' : "Communication Error", 227 | 'app1secheaders' : "Communication Error", 228 | 'app1securl' : "Communication Error", 229 | 'app2secmessage' : "Communication Error", 230 | 'app2secinformation' : "Communication Error", 231 | 'app2secdetails' : "Communication Error", 232 | 'app2secheaders' : "Communication Error", 233 | 'app2securl' : "Communication Error", 234 | 'app2secstatus' : "Communication Error" 235 | } 236 | 237 | render.update(render2) 238 | 239 | render.update(portaldata) 240 | return render_template('index.html', render=render) 241 | 242 | @app.route("/health", methods=['GET']) 243 | def hc(): 244 | response = app.response_class( 245 | status=200, 246 | mimetype='application/json' 247 | ) 248 | return response 249 | 250 | if __name__ == '__main__': 251 | app.run(debug=True,host="0.0.0.0",port=8080) -------------------------------------------------------------------------------- /applications/portal/requirements.txt: -------------------------------------------------------------------------------- 1 | ###### Requirements without Version Specifiers ###### 2 | flask == 3.0.2 3 | requests == 2.31.0 4 | botocore == 1.34.70 5 | awscrt == 0.20.9 6 | pyjwt[crypto] == 2.8.0 -------------------------------------------------------------------------------- /applications/portal/static/images/adam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/moving-to-a-zero-trust-architecture-in-aws/b2847fd65e504017e66b0ad9a9e2fe8d82a47fd9/applications/portal/static/images/adam.png -------------------------------------------------------------------------------- /applications/portal/static/images/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/moving-to-a-zero-trust-architecture-in-aws/b2847fd65e504017e66b0ad9a9e2fe8d82a47fd9/applications/portal/static/images/arrow.png -------------------------------------------------------------------------------- /applications/portal/static/images/backdrop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/moving-to-a-zero-trust-architecture-in-aws/b2847fd65e504017e66b0ad9a9e2fe8d82a47fd9/applications/portal/static/images/backdrop.png -------------------------------------------------------------------------------- /applications/portal/static/images/clientvpn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/moving-to-a-zero-trust-architecture-in-aws/b2847fd65e504017e66b0ad9a9e2fe8d82a47fd9/applications/portal/static/images/clientvpn.png -------------------------------------------------------------------------------- /applications/portal/static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/moving-to-a-zero-trust-architecture-in-aws/b2847fd65e504017e66b0ad9a9e2fe8d82a47fd9/applications/portal/static/images/favicon.ico -------------------------------------------------------------------------------- /applications/portal/static/images/mrx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/moving-to-a-zero-trust-architecture-in-aws/b2847fd65e504017e66b0ad9a9e2fe8d82a47fd9/applications/portal/static/images/mrx.png -------------------------------------------------------------------------------- /applications/portal/static/images/mservice1_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/moving-to-a-zero-trust-architecture-in-aws/b2847fd65e504017e66b0ad9a9e2fe8d82a47fd9/applications/portal/static/images/mservice1_open.png -------------------------------------------------------------------------------- /applications/portal/static/images/mservice1_secure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/moving-to-a-zero-trust-architecture-in-aws/b2847fd65e504017e66b0ad9a9e2fe8d82a47fd9/applications/portal/static/images/mservice1_secure.png -------------------------------------------------------------------------------- /applications/portal/static/images/mservice2_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/moving-to-a-zero-trust-architecture-in-aws/b2847fd65e504017e66b0ad9a9e2fe8d82a47fd9/applications/portal/static/images/mservice2_open.png -------------------------------------------------------------------------------- /applications/portal/static/images/mservice2_secure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/moving-to-a-zero-trust-architecture-in-aws/b2847fd65e504017e66b0ad9a9e2fe8d82a47fd9/applications/portal/static/images/mservice2_secure.png -------------------------------------------------------------------------------- /applications/portal/static/images/pablo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/moving-to-a-zero-trust-architecture-in-aws/b2847fd65e504017e66b0ad9a9e2fe8d82a47fd9/applications/portal/static/images/pablo.png -------------------------------------------------------------------------------- /applications/portal/static/images/teapot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/moving-to-a-zero-trust-architecture-in-aws/b2847fd65e504017e66b0ad9a9e2fe8d82a47fd9/applications/portal/static/images/teapot.png -------------------------------------------------------------------------------- /applications/portal/static/images/verifiedaccess.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/moving-to-a-zero-trust-architecture-in-aws/b2847fd65e504017e66b0ad9a9e2fe8d82a47fd9/applications/portal/static/images/verifiedaccess.png -------------------------------------------------------------------------------- /applications/portal/static/images/wait.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/moving-to-a-zero-trust-architecture-in-aws/b2847fd65e504017e66b0ad9a9e2fe8d82a47fd9/applications/portal/static/images/wait.jpg -------------------------------------------------------------------------------- /applications/portal/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Zero-Trust Microservice Application 6 | 7 | 8 | 9 | 10 |

11 | ZeroTrust Cat Viewer Portal 12 |

13 |

14 | ECS Fargate Portal Tier 15 |

16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 28 | 48 | 49 |
Your Portal ConnectionYour Portal IdentityYour Portal Requests
23 | Portal Connection 24 | 26 | Portal Identification 27 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
Request URI{{request.url}}
Request Principal{{render.portalidname}}
PortalMessage{{render.portalmessage}}
Portal Information{{render.portalinformation}}
47 |
50 | 51 | 52 | 57 | 62 | 63 |
53 |
54 | 55 |
56 |
58 |
59 | 60 |
61 |
64 | 65 |

66 | 67 | 68 | 69 | 71 | 73 | 74 | 75 |
70 | Less Secure 72 | More Secure
76 | 77 |

78 | Elastic Container Service Tier (mservice1) 79 |

80 | 81 | 82 | 83 | 84 | 85 | 87 | 89 | 90 | 91 | 115 | 143 | 144 |
AUTHENTICATED ConnectionAUTHORIZED Connection
86 | Less Secure 88 | More Secure
92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 |
Request URI{{render['app1url']}}
App 1 Message{{render['app1message']}}
App 1 Information{{render['app1information']}}
Request Headers

{{render['app1headers']}}

Status Code{{render['app1status']}}
114 |
116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 |
Request URI{{render['app1securl']}}
App 1 Sec Message{{render['app1secmessage']}}
App 1 Sec Information{{render['app1secinformation']}}
App 1 Sec Details{{render['app1secdetails']}}
Request Headers

{{render['app1secheaders']}}

Status Code{{render['app1secstatus']}}
142 |
145 | 146 |

147 | 148 | 149 | 150 | 152 | 154 | 155 |
151 | Less Secure 153 | More Secure
156 | 157 | 158 |

159 | LAMBDA Function Tier (mservice2) 160 |

161 | 162 | 163 | 164 | 165 | 166 | 168 | 170 | 171 | 172 | 196 | 224 | 225 |
AUTHENTICATED ConnectionAUTHORIZED Connection
167 | Less Secure 169 | More Secure
173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 |
Request URI{{render['app2url']}}
App 2 Message{{render['app2message']}}
App 2 Information{{render['app2information']}}
Request Headers

{{render['app2headers']}}

Status Code{{render['app2status']}}
195 |
197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 |
Request URI{{render['app2securl']}}
App 2 Sec Message{{render['app2secmessage']}}
App 2 Sec Information{{render['app2secinformation']}}
App 2 Sec Details{{render['app2secdetails']}}
Request Headers

{{render['app2secheaders']}}

Status Code{{render['app2secstatus']}}
223 |
226 | 227 | 228 | 229 | -------------------------------------------------------------------------------- /backend/iam.tf: -------------------------------------------------------------------------------- 1 | # ---------- frontend/iam.tf ---------- 2 | 3 | # ECS Execution Task Role 4 | resource "aws_iam_role" "ecs_task_execution_role" { 5 | name = "ecsTaskExecutionRole" 6 | path = "/" 7 | description = "ECS Execution Role." 8 | 9 | managed_policy_arns = ["arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"] 10 | assume_role_policy = data.aws_iam_policy_document.ecs_task_role_assume.json 11 | } 12 | 13 | # ECS Execution Task Role 14 | resource "aws_iam_role" "ecs_task_role" { 15 | name = "mservice1" 16 | path = "/" 17 | description = "Allows ECS tasks to call AWS services on your behalf." 18 | 19 | managed_policy_arns = ["arn:aws:iam::aws:policy/VPCLatticeServicesInvokeAccess"] 20 | assume_role_policy = data.aws_iam_policy_document.ecs_task_role_assume.json 21 | 22 | inline_policy { 23 | name = "EcsExecPerms" 24 | policy = data.aws_iam_policy_document.ecs_task_role_inline.json 25 | } 26 | } 27 | 28 | # Lambda Function Role 29 | resource "aws_iam_role" "lambda_role" { 30 | name = "mservice2" 31 | path = "/" 32 | description = "Lambda function role." 33 | 34 | managed_policy_arns = ["arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"] 35 | assume_role_policy = data.aws_iam_policy_document.lambda_function_role_assume.json 36 | } 37 | 38 | # IAM Policies 39 | data "aws_iam_policy_document" "ecs_task_role_assume" { 40 | statement { 41 | effect = "Allow" 42 | actions = ["sts:AssumeRole"] 43 | 44 | principals { 45 | type = "Service" 46 | identifiers = ["ecs-tasks.amazonaws.com"] 47 | } 48 | } 49 | } 50 | 51 | data "aws_iam_policy_document" "ecs_task_role_inline" { 52 | statement { 53 | sid = "EcsExecPerms" 54 | effect = "Allow" 55 | actions = [ 56 | "ssmmessages:CreateControlChannel", 57 | "ssmmessages:CreateDataChannel", 58 | "ssmmessages:OpenControlChannel", 59 | "ssmmessages:OpenDataChannel" 60 | ] 61 | resources = ["*"] 62 | } 63 | } 64 | 65 | data "aws_iam_policy_document" "lambda_function_role_assume" { 66 | statement { 67 | effect = "Allow" 68 | actions = ["sts:AssumeRole"] 69 | 70 | principals { 71 | type = "Service" 72 | identifiers = ["lambda.amazonaws.com"] 73 | } 74 | } 75 | } 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /backend/locals.tf: -------------------------------------------------------------------------------- 1 | # ---------- frontend/locals.tf ---------- 2 | 3 | locals { 4 | service1_policy = jsonencode({ 5 | "Version" : "2012-10-17" 6 | "Statement" : [ 7 | { 8 | "Effect" : "Allow" 9 | "Principal" : { 10 | "AWS" : "${var.frontend_application_role_arn}" 11 | } 12 | "Action" : "vpc-lattice-svcs:Invoke" 13 | "Resource" : "${module.vpclattice_service1.services.mservice1.attributes.arn}/secure*" 14 | }, 15 | { 16 | "Effect" : "Allow" 17 | "Principal" : "*" 18 | "Action" : "vpc-lattice-svcs:Invoke" 19 | "Resource" : "${module.vpclattice_service1.services.mservice1.attributes.arn}/*" 20 | }, 21 | { 22 | "Effect" : "Deny" 23 | "Principal" : "*" 24 | "Action" : "vpc-lattice-svcs:Invoke" 25 | "Resource" : "${module.vpclattice_service1.services.mservice1.attributes.arn}/secure*" 26 | "Condition" : { 27 | "StringEquals" : { 28 | "aws:PrincipalType" : "Anonymous" 29 | } 30 | } 31 | } 32 | ] 33 | }) 34 | 35 | service2_policy = jsonencode({ 36 | "Version" : "2012-10-17" 37 | "Statement" : [ 38 | { 39 | "Effect" : "Allow" 40 | "Principal" : { 41 | "AWS" : "arn:aws:iam::992382807606:role/mservice1" 42 | } 43 | "Action" : "vpc-lattice-svcs:Invoke" 44 | "Resource" : "${module.vpclattice_service2.services.mservice2.attributes.arn}/secure*" 45 | }, 46 | { 47 | "Effect" : "Allow" 48 | "Principal" : "*" 49 | "Action" : "vpc-lattice-svcs:Invoke" 50 | "Resource" : "${module.vpclattice_service2.services.mservice2.attributes.arn}/*" 51 | }, 52 | { 53 | "Effect" : "Deny" 54 | "Principal" : "*" 55 | "Action" : "vpc-lattice-svcs:Invoke" 56 | "Resource" : "${module.vpclattice_service2.services.mservice2.attributes.arn}/secure*" 57 | "Condition" : { 58 | "StringEquals" : { 59 | "aws:PrincipalType" : "Anonymous" 60 | } 61 | } 62 | } 63 | ] 64 | }) 65 | } -------------------------------------------------------------------------------- /backend/main.tf: -------------------------------------------------------------------------------- 1 | # ---------- frontend/main.tf ---------- 2 | 3 | data "aws_region" "current" {} 4 | 5 | # ---------- VPC LATTICE CONFIGURATION --------- 6 | # Service 1 7 | module "vpclattice_service1" { 8 | source = "aws-ia/amazon-vpc-lattice-module/aws" 9 | version = "0.1.0" 10 | 11 | service_network = { identifier = module.retrieve_parameters.parameter.service_network } 12 | 13 | vpc_associations = { 14 | backend1_vpc = { vpc_id = module.backend1_vpc.vpc_attributes.id } 15 | } 16 | 17 | services = { 18 | mservice1 = { 19 | name = "mservice1" 20 | auth_type = "AWS_IAM" 21 | certificate_arn = var.certificate_arn 22 | custom_domain_name = var.backend_service1_domain_name 23 | 24 | listeners = { 25 | https_listener = { 26 | name = "https-443" 27 | port = 443 28 | protocol = "HTTPS" 29 | default_action_forward = { 30 | target_groups = { 31 | albtarget = { weight = 1 } 32 | } 33 | } 34 | } 35 | } 36 | } 37 | } 38 | 39 | target_groups = { 40 | albtarget = { 41 | name = "mservice1" 42 | type = "ALB" 43 | 44 | config = { 45 | port = 443 46 | protocol = "HTTPS" 47 | vpc_identifier = module.backend1_vpc.vpc_attributes.id 48 | } 49 | 50 | targets = { 51 | albtarget = { 52 | id = aws_lb.backend1_lb.arn 53 | port = 443 54 | } 55 | } 56 | } 57 | } 58 | } 59 | 60 | resource "aws_vpclattice_auth_policy" "service1_auth_policy" { 61 | resource_identifier = module.vpclattice_service1.services.mservice1.attributes.arn 62 | policy = local.service1_policy 63 | } 64 | 65 | # Service 2 66 | module "vpclattice_service2" { 67 | source = "aws-ia/amazon-vpc-lattice-module/aws" 68 | version = "0.1.0" 69 | 70 | service_network = { identifier = module.retrieve_parameters.parameter.service_network } 71 | 72 | services = { 73 | mservice2 = { 74 | name = "mservice2" 75 | auth_type = "AWS_IAM" 76 | certificate_arn = var.certificate_arn 77 | custom_domain_name = var.backend_service2_domain_name 78 | 79 | listeners = { 80 | https_listener = { 81 | name = "https-443" 82 | port = 443 83 | protocol = "HTTPS" 84 | default_action_forward = { 85 | target_groups = { 86 | lambdatarget = { weight = 1 } 87 | } 88 | } 89 | } 90 | } 91 | } 92 | } 93 | 94 | target_groups = { 95 | lambdatarget = { 96 | name = "mservice2" 97 | type = "LAMBDA" 98 | 99 | targets = { lambda = { id = aws_lambda_function.backend2_function.arn } } 100 | } 101 | } 102 | } 103 | 104 | resource "aws_vpclattice_auth_policy" "service2_auth_policy" { 105 | resource_identifier = module.vpclattice_service2.services.mservice2.attributes.arn 106 | policy = local.service2_policy 107 | } 108 | 109 | # ---------- BACKEND VPC (1) ---------- 110 | module "backend1_vpc" { 111 | source = "aws-ia/vpc/aws" 112 | version = "4.4.2" 113 | 114 | cidr_block = "10.0.0.0/16" 115 | name = "backend1-vpc" 116 | az_count = 2 117 | 118 | subnets = { 119 | public = { 120 | netmask = 28 121 | nat_gateway_configuration = "all_azs" 122 | } 123 | private = { netmask = 24 } 124 | application = { 125 | netmask = 24 126 | connect_to_public_natgw = true 127 | } 128 | endpoints = { netmask = 28 } 129 | transit_gateway = { netmask = 28 } 130 | } 131 | } 132 | 133 | # Associating central Route53 Profile (from Networking Account) 134 | resource "awscc_route53profiles_profile_association" "backend1_r53_profile_association" { 135 | name = "r53_profile_backend1_association" 136 | profile_id = module.retrieve_parameters.parameter.r53_profile 137 | resource_id = module.backend1_vpc.vpc_attributes.id 138 | } 139 | 140 | # ---------- BACKEND APPLICATION 1 ---------- 141 | # Application Load Balancer 142 | resource "aws_lb" "backend1_lb" { 143 | name = "mservice1" 144 | internal = true 145 | load_balancer_type = "application" 146 | security_groups = [aws_security_group.backend1_alb_sg.id] 147 | subnets = values({ for k, v in module.backend1_vpc.private_subnet_attributes_by_az : split("/", k)[1] => v.id if split("/", k)[0] == "private" }) 148 | ip_address_type = "ipv4" 149 | } 150 | 151 | # Target Group 152 | resource "aws_lb_target_group" "backend1_target_group" { 153 | name = "mservice1" 154 | port = 8081 155 | protocol = "HTTP" 156 | vpc_id = module.backend1_vpc.vpc_attributes.id 157 | target_type = "ip" 158 | } 159 | 160 | # Listener 161 | resource "aws_lb_listener" "backend1_listener" { 162 | load_balancer_arn = aws_lb.backend1_lb.arn 163 | port = 443 164 | protocol = "HTTPS" 165 | certificate_arn = var.certificate_arn 166 | 167 | default_action { 168 | type = "forward" 169 | target_group_arn = aws_lb_target_group.backend1_target_group.arn 170 | } 171 | } 172 | 173 | # Security Group 174 | resource "aws_security_group" "backend1_alb_sg" { 175 | name = "mservice1-alb" 176 | description = "allows access to the load balancer" 177 | vpc_id = module.backend1_vpc.vpc_attributes.id 178 | } 179 | 180 | resource "aws_vpc_security_group_ingress_rule" "backend1_allowing_ingress_alb_https" { 181 | security_group_id = aws_security_group.backend1_alb_sg.id 182 | 183 | from_port = 443 184 | to_port = 443 185 | ip_protocol = "tcp" 186 | cidr_ipv4 = "0.0.0.0/0" 187 | } 188 | 189 | resource "aws_vpc_security_group_egress_rule" "backend1_allowing_alb_health_check" { 190 | security_group_id = aws_security_group.backend1_alb_sg.id 191 | 192 | ip_protocol = "-1" 193 | cidr_ipv4 = data.aws_vpc.backend1_vpc.cidr_block 194 | } 195 | 196 | # ECR respository 197 | resource "aws_ecr_repository" "repository" { 198 | name = "mservice1" 199 | } 200 | 201 | # ECS Cluster 202 | resource "aws_ecs_cluster" "backend1_cluster" { 203 | name = "mservice1" 204 | 205 | configuration { 206 | execute_command_configuration { 207 | logging = "DEFAULT" 208 | } 209 | } 210 | } 211 | 212 | resource "aws_ecs_cluster_capacity_providers" "cluster_capacity_provider" { 213 | cluster_name = aws_ecs_cluster.backend1_cluster.name 214 | capacity_providers = ["FARGATE", "FARGATE_SPOT"] 215 | } 216 | 217 | # ECS Service 218 | resource "aws_ecs_service" "backend1_service" { 219 | cluster = aws_ecs_cluster.backend1_cluster.arn 220 | name = "mservice1" 221 | platform_version = "LATEST" 222 | task_definition = split("/", aws_ecs_task_definition.backend1_task_definition.arn)[1] 223 | scheduling_strategy = "REPLICA" 224 | propagate_tags = "NONE" 225 | 226 | deployment_maximum_percent = 200 227 | deployment_minimum_healthy_percent = 100 228 | desired_count = 2 229 | enable_ecs_managed_tags = true 230 | enable_execute_command = true 231 | 232 | capacity_provider_strategy { 233 | base = 0 234 | capacity_provider = "FARGATE" 235 | weight = 1 236 | } 237 | 238 | deployment_circuit_breaker { 239 | enable = true 240 | rollback = true 241 | } 242 | 243 | load_balancer { 244 | container_name = "mservice1" 245 | container_port = 8081 246 | target_group_arn = aws_lb_target_group.backend1_target_group.arn 247 | } 248 | 249 | network_configuration { 250 | assign_public_ip = false 251 | security_groups = [aws_security_group.backend1_ecs_service_sg.id] 252 | subnets = values({ for k, v in module.backend1_vpc.private_subnet_attributes_by_az : split("/", k)[1] => v.id if split("/", k)[0] == "application" }) 253 | } 254 | } 255 | 256 | # ECS Task Definition 257 | resource "aws_ecs_task_definition" "backend1_task_definition" { 258 | family = "mservice1" 259 | cpu = 1024 260 | execution_role_arn = aws_iam_role.ecs_task_execution_role.arn 261 | memory = 3072 262 | network_mode = "awsvpc" 263 | requires_compatibilities = ["FARGATE"] 264 | task_role_arn = aws_iam_role.ecs_task_role.arn 265 | 266 | container_definitions = jsonencode([{ 267 | name = "mservice1" 268 | image = "${aws_ecr_repository.repository.repository_url}:latest" 269 | essential = true 270 | 271 | logConfiguration = { 272 | logDriver = "awslogs" 273 | options = { 274 | awslogs-create-group = "true" 275 | awslogs-group = "/ecs/mservice1" 276 | awslogs-region = data.aws_region.current.name 277 | awslogs-stream-prefix = "ecs" 278 | } 279 | } 280 | 281 | portMappings = [{ 282 | appProtocol = "http" 283 | containerPort = 8081 284 | hostPort = 8081 285 | name = "mservice1-8081-tcp" 286 | protocol = "tcp" 287 | }] 288 | }]) 289 | 290 | runtime_platform { 291 | cpu_architecture = "X86_64" 292 | operating_system_family = "LINUX" 293 | } 294 | } 295 | 296 | # Security Group (ECS Service) 297 | resource "aws_security_group" "backend1_ecs_service_sg" { 298 | name = "mservice1" 299 | description = "Created in ECS Console" 300 | vpc_id = module.backend1_vpc.vpc_attributes.id 301 | } 302 | 303 | resource "aws_vpc_security_group_ingress_rule" "ingress_ipv4" { 304 | security_group_id = aws_security_group.backend1_ecs_service_sg.id 305 | 306 | ip_protocol = "tcp" 307 | from_port = 8081 308 | to_port = 8081 309 | cidr_ipv4 = data.aws_vpc.backend1_vpc.cidr_block 310 | } 311 | 312 | resource "aws_vpc_security_group_egress_rule" "egress_ipv4" { 313 | security_group_id = aws_security_group.backend1_ecs_service_sg.id 314 | 315 | ip_protocol = "-1" 316 | cidr_ipv4 = "0.0.0.0/0" 317 | } 318 | 319 | # ---------- BACKEND APPLICATION 2 ---------- 320 | # Lambda function 321 | resource "aws_lambda_function" "backend2_function" { 322 | function_name = "mservice2" 323 | handler = "app2lambda.lambda_handler" 324 | runtime = "python3.12" 325 | memory_size = 128 326 | timeout = 10 327 | 328 | role = aws_iam_role.lambda_role.arn 329 | filename = "lambda_function.zip" 330 | source_code_hash = data.archive_file.lambda.output_base64sha256 331 | } 332 | 333 | data "archive_file" "lambda" { 334 | type = "zip" 335 | source_file = "../applications/mservice2/app2lambda.py" 336 | output_path = "lambda_function.zip" 337 | } 338 | 339 | # ---------- PARAMETERS ---------- 340 | # Sharing 341 | module "share_parameters" { 342 | source = "../modules/share_parameter" 343 | 344 | parameters = { 345 | service1_domain_name = module.vpclattice_service1.services.mservice1.attributes.dns_entry 346 | service2_domain_name = module.vpclattice_service2.services.mservice2.attributes.dns_entry 347 | } 348 | } 349 | 350 | # Retrieving 351 | module "retrieve_parameters" { 352 | source = "../modules/retrieve_parameters" 353 | 354 | parameters = { 355 | r53_profile = var.networking_account 356 | service_network = var.networking_account 357 | } 358 | } 359 | 360 | -------------------------------------------------------------------------------- /backend/outputs.tf: -------------------------------------------------------------------------------- 1 | # ---------- frontend/outputs.tf ---------- -------------------------------------------------------------------------------- /backend/providers.tf: -------------------------------------------------------------------------------- 1 | # ---------- backend/providers.tf ---------- 2 | 3 | terraform { 4 | required_version = ">= 1.3.0" 5 | required_providers { 6 | aws = { 7 | source = "hashicorp/aws" 8 | version = ">= 5.0.0" 9 | } 10 | awscc = { 11 | source = "hashicorp/awscc" 12 | version = "= 0.78.0" 13 | } 14 | } 15 | } 16 | 17 | provider "aws" { 18 | region = var.aws_region 19 | } 20 | 21 | provider "awscc" { 22 | region = var.aws_region 23 | } -------------------------------------------------------------------------------- /backend/variables.tf: -------------------------------------------------------------------------------- 1 | # ---------- frontend/variables.tf ---------- 2 | 3 | variable "aws_region" { 4 | type = string 5 | description = "AWS Region." 6 | 7 | default = "eu-west-1" 8 | } 9 | 10 | variable "certificate_arn" { 11 | type = string 12 | description = "ACM certificate ARN." 13 | } 14 | 15 | variable "networking_account" { 16 | type = string 17 | description = "Networking Account ID." 18 | } 19 | 20 | variable "backend_service1_domain_name" { 21 | type = string 22 | description = "Backend App1 - Domain Name." 23 | } 24 | 25 | variable "backend_service2_domain_name" { 26 | type = string 27 | description = "Backend App2 - Domain Name." 28 | } 29 | 30 | variable "frontend_application_role_arn" { 31 | type = string 32 | description = "Frontend Application Role ARN." 33 | } -------------------------------------------------------------------------------- /frontend/iam.tf: -------------------------------------------------------------------------------- 1 | # ---------- frontend/iam.tf ---------- 2 | 3 | # ECS Execution Task Role 4 | resource "aws_iam_role" "ecs_task_execution_role" { 5 | name = "ecsTaskExecutionRole" 6 | path = "/" 7 | description = "ECS Execution Role." 8 | 9 | managed_policy_arns = ["arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"] 10 | assume_role_policy = data.aws_iam_policy_document.ecs_task_role_assume.json 11 | } 12 | 13 | # ECS Execution Task Role 14 | resource "aws_iam_role" "ecs_task_role" { 15 | name = "frontend" 16 | path = "/" 17 | description = "Allows ECS tasks to call AWS services on your behalf." 18 | 19 | managed_policy_arns = ["arn:aws:iam::aws:policy/VPCLatticeServicesInvokeAccess"] 20 | assume_role_policy = data.aws_iam_policy_document.ecs_task_role_assume.json 21 | 22 | inline_policy { 23 | name = "EcsExecPerms" 24 | policy = data.aws_iam_policy_document.ecs_task_role_inline.json 25 | } 26 | } 27 | 28 | # IAM Policies 29 | data "aws_iam_policy_document" "ecs_task_role_assume" { 30 | statement { 31 | effect = "Allow" 32 | actions = ["sts:AssumeRole"] 33 | 34 | principals { 35 | type = "Service" 36 | identifiers = ["ecs-tasks.amazonaws.com"] 37 | } 38 | } 39 | } 40 | 41 | data "aws_iam_policy_document" "ecs_task_role_inline" { 42 | statement { 43 | sid = "EcsExecPerms" 44 | effect = "Allow" 45 | actions = [ 46 | "ssmmessages:CreateControlChannel", 47 | "ssmmessages:CreateDataChannel", 48 | "ssmmessages:OpenControlChannel", 49 | "ssmmessages:OpenDataChannel" 50 | ] 51 | resources = ["*"] 52 | } 53 | } -------------------------------------------------------------------------------- /frontend/main.tf: -------------------------------------------------------------------------------- 1 | # ---------- frontend/main.tf ---------- 2 | 3 | data "aws_region" "current" {} 4 | 5 | # ---------- FRONTEND VPC ---------- 6 | module "frontend_vpc" { 7 | source = "aws-ia/vpc/aws" 8 | version = "4.4.2" 9 | 10 | cidr_block = "10.0.0.0/16" 11 | name = "frontend-vpc" 12 | az_count = 2 13 | 14 | vpc_lattice = { 15 | service_network_identifier = module.retrieve_parameters.parameter.service_network 16 | } 17 | 18 | subnets = { 19 | public = { 20 | netmask = 24 21 | nat_gateway_configuration = "all_azs" 22 | } 23 | private = { netmask = 24 } 24 | application = { 25 | netmask = 24 26 | connect_to_public_natgw = true 27 | } 28 | endpoints = { netmask = 28 } 29 | transit_gateway = { netmask = 28 } 30 | } 31 | } 32 | 33 | # Associating central Route53 Profile (from Networking Account) 34 | resource "awscc_route53profiles_profile_association" "r53_profile_association" { 35 | name = "r53_profile_frontend_association" 36 | profile_id = module.retrieve_parameters.parameter.r53_profile 37 | resource_id = module.frontend_vpc.vpc_attributes.id 38 | } 39 | 40 | # ---------- AWS VERIFIED ACCESS ---------- 41 | # Instance 42 | resource "aws_verifiedaccess_instance" "ava_instance" { 43 | description = "AVA Instance" 44 | 45 | tags = { 46 | Name = "ava-instance" 47 | } 48 | } 49 | 50 | # Trust Provider 51 | resource "aws_verifiedaccess_trust_provider" "trust_provider" { 52 | description = "AVA Trust Provider - User (IAM Identity Center)" 53 | policy_reference_name = "frontenduser" 54 | trust_provider_type = "user" 55 | user_trust_provider_type = "iam-identity-center" 56 | 57 | tags = { 58 | Name = "ava-trust-provider" 59 | } 60 | } 61 | 62 | resource "aws_verifiedaccess_instance_trust_provider_attachment" "trust_provider_attachment" { 63 | verifiedaccess_instance_id = aws_verifiedaccess_instance.ava_instance.id 64 | verifiedaccess_trust_provider_id = aws_verifiedaccess_trust_provider.trust_provider.id 65 | } 66 | 67 | # Group 68 | resource "aws_verifiedaccess_group" "ava_group" { 69 | description = "AVA Group" 70 | verifiedaccess_instance_id = aws_verifiedaccess_instance.ava_instance.id 71 | 72 | policy_document = < v.id if split("/", k)[0] == "private" }) 103 | } 104 | 105 | tags = { 106 | Name = "App Frontend" 107 | } 108 | } 109 | 110 | # Security Group 111 | resource "aws_security_group" "ava_sg" { 112 | name = "ava-security-group" 113 | description = "AWS Verified Access endpoint" 114 | vpc_id = module.frontend_vpc.vpc_attributes.id 115 | } 116 | 117 | resource "aws_vpc_security_group_ingress_rule" "allowing_ingress_https" { 118 | security_group_id = aws_security_group.ava_sg.id 119 | 120 | from_port = 443 121 | to_port = 443 122 | ip_protocol = "tcp" 123 | cidr_ipv4 = data.aws_vpc.vpc.cidr_block 124 | } 125 | 126 | resource "aws_vpc_security_group_egress_rule" "allowing_ava_alb_connectivity" { 127 | security_group_id = aws_security_group.ava_sg.id 128 | 129 | ip_protocol = "-1" 130 | cidr_ipv4 = data.aws_vpc.vpc.cidr_block 131 | } 132 | 133 | # ---------- FRONTEND APPLICATION ---------- 134 | # Application Load Balancer 135 | resource "aws_lb" "lb" { 136 | name = "frontend" 137 | internal = true 138 | load_balancer_type = "application" 139 | security_groups = [aws_security_group.alb_sg.id] 140 | subnets = values({ for k, v in module.frontend_vpc.private_subnet_attributes_by_az : split("/", k)[1] => v.id if split("/", k)[0] == "private" }) 141 | ip_address_type = "ipv4" 142 | } 143 | 144 | # Target Group 145 | resource "aws_lb_target_group" "target_group" { 146 | name = "frontend-targets" 147 | port = 8080 148 | protocol = "HTTP" 149 | vpc_id = module.frontend_vpc.vpc_attributes.id 150 | target_type = "ip" 151 | } 152 | 153 | # Listener 154 | resource "aws_lb_listener" "frontend_listener" { 155 | load_balancer_arn = aws_lb.lb.arn 156 | port = 443 157 | protocol = "HTTPS" 158 | certificate_arn = var.certificate_arn 159 | 160 | default_action { 161 | type = "forward" 162 | target_group_arn = aws_lb_target_group.target_group.arn 163 | 164 | } 165 | } 166 | 167 | # Security Group (Application Load Balancer) 168 | resource "aws_security_group" "alb_sg" { 169 | name = "frontend-elb-secgroup" 170 | description = "allows port 80 connectivity" 171 | vpc_id = module.frontend_vpc.vpc_attributes.id 172 | } 173 | 174 | resource "aws_vpc_security_group_ingress_rule" "allowing_ingress_alb_https" { 175 | security_group_id = aws_security_group.alb_sg.id 176 | 177 | from_port = 443 178 | to_port = 443 179 | ip_protocol = "tcp" 180 | cidr_ipv4 = data.aws_vpc.vpc.cidr_block 181 | } 182 | 183 | resource "aws_vpc_security_group_egress_rule" "allowing_alb_health_check" { 184 | security_group_id = aws_security_group.alb_sg.id 185 | 186 | ip_protocol = "-1" 187 | cidr_ipv4 = data.aws_vpc.vpc.cidr_block 188 | } 189 | 190 | # ECR respository 191 | resource "aws_ecr_repository" "repository" { 192 | name = "frontend" 193 | } 194 | 195 | # ECS Cluster 196 | resource "aws_ecs_cluster" "frontend_cluster" { 197 | name = "frontend" 198 | 199 | configuration { 200 | execute_command_configuration { 201 | logging = "DEFAULT" 202 | } 203 | } 204 | } 205 | 206 | resource "aws_ecs_cluster_capacity_providers" "cluster_capacity_provider" { 207 | cluster_name = aws_ecs_cluster.frontend_cluster.name 208 | capacity_providers = ["FARGATE", "FARGATE_SPOT"] 209 | } 210 | 211 | # ECS Service 212 | resource "aws_ecs_service" "frontend_service" { 213 | cluster = aws_ecs_cluster.frontend_cluster.arn 214 | name = "frontend" 215 | platform_version = "LATEST" 216 | task_definition = split("/", aws_ecs_task_definition.frontend_task_definition.arn)[1] 217 | scheduling_strategy = "REPLICA" 218 | propagate_tags = "NONE" 219 | 220 | deployment_maximum_percent = 200 221 | deployment_minimum_healthy_percent = 100 222 | desired_count = 2 223 | enable_ecs_managed_tags = true 224 | enable_execute_command = true 225 | 226 | capacity_provider_strategy { 227 | base = 0 228 | capacity_provider = "FARGATE" 229 | weight = 1 230 | } 231 | 232 | deployment_circuit_breaker { 233 | enable = true 234 | rollback = true 235 | } 236 | 237 | load_balancer { 238 | container_name = "frontend" 239 | container_port = 8080 240 | target_group_arn = aws_lb_target_group.target_group.arn 241 | } 242 | 243 | network_configuration { 244 | assign_public_ip = false 245 | security_groups = [aws_security_group.ecs_service_sg.id] 246 | subnets = values({ for k, v in module.frontend_vpc.private_subnet_attributes_by_az : split("/", k)[1] => v.id if split("/", k)[0] == "application" }) 247 | } 248 | } 249 | 250 | # ECS Task Definition 251 | resource "aws_ecs_task_definition" "frontend_task_definition" { 252 | cpu = 1024 253 | execution_role_arn = aws_iam_role.ecs_task_execution_role.arn 254 | family = "frontend" 255 | memory = 3072 256 | network_mode = "awsvpc" 257 | requires_compatibilities = ["FARGATE"] 258 | task_role_arn = aws_iam_role.ecs_task_role.arn 259 | 260 | runtime_platform { 261 | cpu_architecture = "X86_64" 262 | operating_system_family = "LINUX" 263 | } 264 | 265 | container_definitions = jsonencode([{ 266 | name = "frontend" 267 | image = "${aws_ecr_repository.repository.repository_url}:latest" 268 | essential = true 269 | 270 | logConfiguration = { 271 | logDriver = "awslogs" 272 | options = { 273 | awslogs-create-group = "true" 274 | awslogs-group = "/ecs/frontend" 275 | awslogs-region = data.aws_region.current.name 276 | awslogs-stream-prefix = "ecs" 277 | } 278 | } 279 | 280 | portMappings = [{ 281 | appProtocol = "http" 282 | containerPort = 8080 283 | hostPort = 8080 284 | name = "frontend-8080-tcp" 285 | protocol = "tcp" 286 | }] 287 | }]) 288 | } 289 | 290 | # Security Group (ECS Service) 291 | resource "aws_security_group" "ecs_service_sg" { 292 | name = "frontend-task" 293 | description = "Created in ECS Console" 294 | vpc_id = module.frontend_vpc.vpc_attributes.id 295 | } 296 | 297 | resource "aws_vpc_security_group_ingress_rule" "ingress_ipv4" { 298 | security_group_id = aws_security_group.ecs_service_sg.id 299 | 300 | ip_protocol = "tcp" 301 | from_port = 8080 302 | to_port = 8080 303 | cidr_ipv4 = data.aws_vpc.vpc.cidr_block 304 | } 305 | 306 | resource "aws_vpc_security_group_egress_rule" "egress_ipv4" { 307 | security_group_id = aws_security_group.ecs_service_sg.id 308 | 309 | ip_protocol = "-1" 310 | cidr_ipv4 = "0.0.0.0/0" 311 | } 312 | 313 | # ---------- PARAMETERS ---------- 314 | # Sharing 315 | module "share_parameters" { 316 | source = "../modules/share_parameter" 317 | 318 | parameters = { 319 | frontend_ava_domain_name = aws_verifiedaccess_endpoint.ava_endpoint.endpoint_domain 320 | frontent_alb_domain_name = aws_lb.lb.dns_name 321 | } 322 | } 323 | 324 | # Retrieving 325 | module "retrieve_parameters" { 326 | source = "../modules/retrieve_parameters" 327 | 328 | parameters = { 329 | r53_profile = var.networking_account 330 | service_network = var.networking_account 331 | } 332 | } -------------------------------------------------------------------------------- /frontend/outputs.tf: -------------------------------------------------------------------------------- 1 | # ---------- frontend/outputs.tf ---------- -------------------------------------------------------------------------------- /frontend/providers.tf: -------------------------------------------------------------------------------- 1 | # ---------- frontend/providers.tf ---------- 2 | 3 | terraform { 4 | required_version = ">= 1.3.0" 5 | required_providers { 6 | aws = { 7 | source = "hashicorp/aws" 8 | version = ">= 5.0.0" 9 | } 10 | awscc = { 11 | source = "hashicorp/awscc" 12 | version = "= 0.78.0" 13 | } 14 | } 15 | } 16 | 17 | provider "aws" { 18 | region = var.aws_region 19 | } 20 | 21 | provider "awscc" { 22 | region = var.aws_region 23 | } -------------------------------------------------------------------------------- /frontend/variables.tf: -------------------------------------------------------------------------------- 1 | # ---------- frontend/providers.tf ---------- 2 | 3 | variable "aws_region" { 4 | type = string 5 | description = "AWS Region." 6 | } 7 | 8 | variable "certificate_arn" { 9 | type = string 10 | description = "ACM certificate ARN." 11 | } 12 | 13 | variable "networking_account" { 14 | type = string 15 | description = "Networking Account ID." 16 | } 17 | 18 | variable "frontend_domain_name" { 19 | type = string 20 | description = "Frontend Application domain name." 21 | } 22 | 23 | variable "idc_group_id" { 24 | type = string 25 | description = "Identity Center Group ID (for AWS Verified Access policy)." 26 | } -------------------------------------------------------------------------------- /modules/retrieve_parameters/main.tf: -------------------------------------------------------------------------------- 1 | # ---------- modules/retreive_parameters/main.tf ---------- 2 | 3 | # Obtain AWS Region 4 | data "aws_region" "current" {} 5 | 6 | # Retrieving parameters 7 | data "aws_ssm_parameter" "parameter" { 8 | for_each = var.parameters 9 | 10 | name = "arn:aws:ssm:${data.aws_region.current.name}:${each.value}:parameter/${each.key}" 11 | } -------------------------------------------------------------------------------- /modules/retrieve_parameters/outputs.tf: -------------------------------------------------------------------------------- 1 | # ---------- modules/retreive_parameters/outputs.tf ---------- 2 | 3 | output "parameter" { 4 | description = "Parameter value." 5 | value = { for k, v in data.aws_ssm_parameter.parameter : k => v.value } 6 | } -------------------------------------------------------------------------------- /modules/retrieve_parameters/providers.tf: -------------------------------------------------------------------------------- 1 | # ---------- modules/retreive_parameters/providers.tf ---------- 2 | 3 | terraform { 4 | required_version = ">= 1.3.0" 5 | required_providers { 6 | aws = { 7 | source = "hashicorp/aws" 8 | version = ">= 5.0.0" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /modules/retrieve_parameters/variables.tf: -------------------------------------------------------------------------------- 1 | # ---------- modules/retreive_parameters/variables.tf ---------- 2 | 3 | variable "parameters" { 4 | description = "List of parameters to retrieve." 5 | type = map(string) 6 | } -------------------------------------------------------------------------------- /modules/share_parameter/main.tf: -------------------------------------------------------------------------------- 1 | # ---------- modules/share_parameter/main.tf ---------- 2 | 3 | # Obtaining AWS Organization ID 4 | data "aws_organizations_organization" "org" {} 5 | 6 | # Resource Share 7 | resource "aws_ram_resource_share" "resource_share" { 8 | name = "Networking Account - Parameters" 9 | allow_external_principals = false 10 | } 11 | 12 | resource "aws_ram_principal_association" "principal_association" { 13 | principal = data.aws_organizations_organization.org.arn 14 | resource_share_arn = aws_ram_resource_share.resource_share.arn 15 | } 16 | 17 | # Creation of the SSM Parameter Store Parameters 18 | resource "aws_ssm_parameter" "parameter" { 19 | for_each = var.parameters 20 | 21 | name = each.key 22 | tier = "Advanced" 23 | type = "String" 24 | value = each.value 25 | } 26 | 27 | # Sharing Parameters via RAM 28 | resource "aws_ram_resource_association" "resource_association" { 29 | for_each = var.parameters 30 | 31 | resource_arn = aws_ssm_parameter.parameter[each.key].arn 32 | resource_share_arn = aws_ram_resource_share.resource_share.arn 33 | } -------------------------------------------------------------------------------- /modules/share_parameter/outputs.tf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/moving-to-a-zero-trust-architecture-in-aws/b2847fd65e504017e66b0ad9a9e2fe8d82a47fd9/modules/share_parameter/outputs.tf -------------------------------------------------------------------------------- /modules/share_parameter/providers.tf: -------------------------------------------------------------------------------- 1 | # ---------- modules/share_parameter/providers.tf ---------- 2 | 3 | terraform { 4 | required_version = ">= 1.3.0" 5 | required_providers { 6 | aws = { 7 | source = "hashicorp/aws" 8 | version = ">= 5.0.0" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /modules/share_parameter/variables.tf: -------------------------------------------------------------------------------- 1 | # ---------- modules/share_parameter/variables.tf ---------- 2 | 3 | variable "parameters" { 4 | description = "List of parameters to share." 5 | type = map(string) 6 | } -------------------------------------------------------------------------------- /network/main.tf: -------------------------------------------------------------------------------- 1 | # ---------- network/main.tf ---------- 2 | 3 | data "aws_organizations_organization" "org" {} 4 | 5 | # ---------- AWS RAM SHARE ---------- 6 | # Sharing Networking resources with the AWS Organization 7 | resource "aws_ram_resource_share" "resource_share" { 8 | name = "Resource Share - Networking Account" 9 | allow_external_principals = false 10 | } 11 | 12 | resource "aws_ram_principal_association" "principal_association" { 13 | principal = data.aws_organizations_organization.org.arn 14 | resource_share_arn = aws_ram_resource_share.resource_share.arn 15 | } 16 | 17 | # ---------- AMAZON VPC LATTICE SERVICE NETWORK -------------- 18 | module "vpclattice_service_network" { 19 | source = "aws-ia/amazon-vpc-lattice-module/aws" 20 | version = "0.1.0" 21 | 22 | service_network = { 23 | name = "central-service-network" 24 | auth_type = "NONE" 25 | } 26 | } 27 | 28 | resource "aws_ram_resource_association" "vpclattice_sn_share" { 29 | resource_arn = module.vpclattice_service_network.service_network.arn 30 | resource_share_arn = aws_ram_resource_share.resource_share.arn 31 | } 32 | 33 | # ---------- AMAZON ROUTE 53 ---------- 34 | # Private Hosted Zone 35 | resource "aws_route53_zone" "private_hosted_zone" { 36 | name = var.hosted_zone_name 37 | 38 | vpc { 39 | vpc_id = module.network.central_vpcs.inspection.vpc_attributes.id 40 | } 41 | 42 | lifecycle { 43 | ignore_changes = [vpc] 44 | } 45 | 46 | tags = { 47 | Name = var.hosted_zone_name 48 | } 49 | } 50 | 51 | # Frontend record: Private Hosted zone 52 | resource "aws_route53_record" "frontend_private" { 53 | zone_id = aws_route53_zone.private_hosted_zone.zone_id 54 | name = var.frontend_domain_name 55 | type = "CNAME" 56 | ttl = 300 57 | records = [module.retrieve_parameters.parameter.frontent_alb_domain_name] 58 | } 59 | 60 | # Frontend record: Public Hosted zone 61 | resource "aws_route53_record" "frontend_public" { 62 | zone_id = var.public_hosted_zone_id 63 | name = var.frontend_domain_name 64 | type = "CNAME" 65 | ttl = 300 66 | records = [module.retrieve_parameters.parameter.frontend_ava_domain_name] 67 | } 68 | 69 | # Backend service1 and service2 records 70 | resource "aws_route53_record" "service1" { 71 | zone_id = aws_route53_zone.private_hosted_zone.zone_id 72 | name = var.backend_service1_domain_name 73 | type = "CNAME" 74 | ttl = 300 75 | records = [module.retrieve_parameters.parameter.service1_domain_name] 76 | } 77 | 78 | resource "aws_route53_record" "service2" { 79 | zone_id = aws_route53_zone.private_hosted_zone.zone_id 80 | name = var.backend_service2_domain_name 81 | type = "CNAME" 82 | ttl = 300 83 | records = [module.retrieve_parameters.parameter.service2_domain_name] 84 | } 85 | 86 | # Route 53 Profile 87 | resource "awscc_route53profiles_profile" "r53_profile" { 88 | name = "phz_vpc-lattice" 89 | } 90 | 91 | # PHZ associated to R53 profile 92 | resource "awscc_route53profiles_profile_resource_association" "r53_profile_resource_association" { 93 | name = "phz_vpc-lattice" 94 | profile_id = awscc_route53profiles_profile.r53_profile.id 95 | resource_arn = aws_route53_zone.private_hosted_zone.arn 96 | } 97 | 98 | # Sharing the R53 profile 99 | resource "aws_ram_resource_association" "r53_profile_share" { 100 | resource_arn = awscc_route53profiles_profile.r53_profile.arn 101 | resource_share_arn = aws_ram_resource_share.resource_share.arn 102 | } 103 | 104 | # ---------- PARAMETERS ---------- 105 | # Sharing 106 | module "share_parameter_share" { 107 | source = "../modules/share_parameter" 108 | 109 | parameters = { 110 | r53_profile = awscc_route53profiles_profile.r53_profile.id 111 | service_network = module.vpclattice_service_network.service_network.arn 112 | } 113 | } 114 | 115 | # Retrieving 116 | module "retrieve_parameters" { 117 | source = "../modules/retrieve_parameters" 118 | 119 | parameters = { 120 | frontend_ava_domain_name = var.frontend_account 121 | frontent_alb_domain_name = var.frontend_account 122 | service1_domain_name = var.backend_account 123 | service2_domain_name = var.backend_account 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /network/outputs.tf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/moving-to-a-zero-trust-architecture-in-aws/b2847fd65e504017e66b0ad9a9e2fe8d82a47fd9/network/outputs.tf -------------------------------------------------------------------------------- /network/providers.tf: -------------------------------------------------------------------------------- 1 | # ---------- network/providers.tf ---------- 2 | 3 | terraform { 4 | required_version = ">= 1.3.0" 5 | required_providers { 6 | aws = { 7 | source = "hashicorp/aws" 8 | version = ">= 5.0.0" 9 | } 10 | awscc = { 11 | source = "hashicorp/awscc" 12 | version = "= 0.78.0" 13 | } 14 | } 15 | } 16 | 17 | provider "aws" { 18 | region = var.aws_region 19 | } 20 | 21 | provider "awscc" { 22 | region = var.aws_region 23 | } -------------------------------------------------------------------------------- /network/variables.tf: -------------------------------------------------------------------------------- 1 | # ---------- network/variables.tf ---------- 2 | 3 | variable "aws_region" { 4 | type = string 5 | description = "AWS Region." 6 | } 7 | 8 | variable "hosted_zone_name" { 9 | type = string 10 | description = "Private Hosted Zone name." 11 | } 12 | 13 | variable "public_hosted_zone_id" { 14 | type = string 15 | description = "Public Hosted Zone ID." 16 | } 17 | 18 | variable "frontend_account" { 19 | type = string 20 | description = "Frontend Account ID." 21 | } 22 | 23 | variable "backend_account" { 24 | type = string 25 | description = "Backend Account ID." 26 | } 27 | 28 | variable "frontend_domain_name" { 29 | type = string 30 | description = "Frontend App - Domain Name." 31 | } 32 | 33 | variable "backend_service1_domain_name" { 34 | type = string 35 | description = "Backend App1 - Domain Name." 36 | } 37 | 38 | variable "backend_service2_domain_name" { 39 | type = string 40 | description = "Backend App2 - Domain Name." 41 | } 42 | --------------------------------------------------------------------------------