├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── dev └── README.md ├── img ├── architecture.png ├── codebuildoutput.png └── sampleapplication.png ├── release ├── README.md ├── main.tf ├── outputs.tf ├── provider.tf ├── terraform.tfvars ├── variables.tf ├── versions.tf └── website.tf └── sample-developer-environment.yml /.gitignore: -------------------------------------------------------------------------------- 1 | # Local .terraform directories 2 | **/.terraform/* 3 | 4 | # .tfstate files 5 | *.tfstate 6 | *.tfstate.* 7 | 8 | # Crash log files 9 | crash.log 10 | crash.*.log 11 | 12 | # Exclude all .tfvars files, which are likely to contain sensitive data, such as 13 | # password, private keys, and other secrets. These should not be part of version 14 | # control as they are data points which are potentially sensitive and subject 15 | # to change depending on the environment. 16 | #*.tfvars 17 | *.tfvars.json 18 | 19 | # Ignore override files as they are usually used to override resources locally and so 20 | # are not checked in 21 | override.tf 22 | override.tf.json 23 | *_override.tf 24 | *_override.tf.json 25 | 26 | # Include override files you do wish to add to version control using negated pattern 27 | # !example_override.tf 28 | 29 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 30 | # example: *tfplan* 31 | 32 | # Ignore CLI configuration files 33 | .terraformrc 34 | terraform.rc 35 | 36 | # Ignore lock files 37 | .terraform.lock.hcl 38 | 39 | # Ignore Lambda archive 40 | lambda_rotation.zip -------------------------------------------------------------------------------- /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 | # sample-developer-environment 2 | 3 | This solution deploys a complete browser-based development environment with VS Code, version control, and automated deployments using a single AWS CloudFormation template. 4 | 5 | > Note: A **preview** Terraform implementation is also available in the [terraform branch](../../tree/terraform). 6 | 7 | ## Repository Structure 8 | 9 | ``` 10 | . 11 | ├── dev/ # Development workspace 12 | │ └── README.md # Development guide 13 | ├── release/ # Sample Terraform application 14 | │ ├── main.tf # Core infrastructure 15 | │ ├── provider.tf # AWS provider configuration 16 | │ ├── variables.tf # Input variables 17 | │ ├── versions.tf # Provider versions and backend 18 | │ ├── website.tf # Sample static website 19 | │ └── terraform.tfvars # Variable defaults 20 | └── sample-developer-environment.yml # Main CloudFormation template 21 | ``` 22 | 23 | ## Key Features 24 | 25 | - Browser-based VS Code using [code-server](https://github.com/coder/code-server) accessed through Amazon CloudFront 26 | - Git version control using [git-remote-s3](https://github.com/awslabs/git-remote-s3) with Amazon S3 storage 27 | - Automated deployments using AWS CodePipeline and AWS CodeBuild 28 | - Password rotation using AWS Secrets Manager (30-day automatic rotation) 29 | - Pre-configured AWS development environment: 30 | - AWS Toolkit for VS Code 31 | - Terraform infrastructure deployment 32 | - Docker support 33 | - Git integration 34 | 35 | ## Quick Start 36 | 37 | 1. Launch the AWS CloudFormation template `sample-developer-environment.yml` 38 | 2. Choose your initial workspace content: 39 | - Provide a GitHub repository URL in `GitHubRepo` parameter, OR 40 | - Provide S3 bucket name `S3AssetBucket` and `S3AssetPrefix` parameters 41 | 3. Access VS Code through the provided CloudFormation output URL 42 | 4. Get your password from AWS Secrets Manager (link in outputs) 43 | 5. Click *File* > *Open Folder* and navigate to `/home/ec2-user/my-workspace`. This is the git/S3 initialized project directory 44 | 6. Test code in `dev`, copy to `release`, commit and push to trigger deployment 45 | 46 | 47 | ## Configuration Options 48 | 49 | | Parameter | Description | 50 | |-----------|-------------| 51 | | `CodeServerVersion` | Version of code-server to install | 52 | | `GitHubRepo` | Public repository to clone as initial workspace. Note: Using a custom repository will not include the sample application | 53 | | `S3AssetBucket` | (Optional) S3 bucket containing initial workspace content. Overwrites GitHubRepo if provided | 54 | | `S3AssetPrefix` | (Optional) S3 bucket asset prefix path. Only required when S3AssetBucket is specified. Needs to end with `/` | 55 | | `DeployPipeline` | Enable AWS CodePipeline deployments | 56 | | `RotateSecret` | Enable AWS Secrets Manager rotation | 57 | | `AutoSetDeveloperProfile` | Automatically set Developer profile as default in code-server terminal sessions without requiring manual elevation | 58 | | `InstanceType` | Supports both ARM and x86 Amazon EC2 instances | 59 | 60 | ## AWS IAM Roles 61 | 62 | The environment is configured with two IAM roles: 63 | 1. EC2 instance role - Basic permissions for the instance 64 | 2. Developer role - Elevated permissions for AWS operations 65 | 66 | The developer role has the permissions needed to deploy the sample application. To view or modify these permissions, search for "iamroledeveloper" in the CloudFormation template. 67 | 68 | This separation ensures the EC2 instance runs with minimal permissions by default, while allowing controlled elevation of privileges when needed. 69 | 70 | ℹ️ **Tip**: Run `echo 'export AWS_PROFILE=developer' >> ~/.bashrc && source ~/.bashrc` to make the developer profile default for all terminal sessions. 71 | 72 | If you wish to have elevated AWS permissions automatically enabled in all new terminal sessions without requiring manual profile switching, set `AutoSetDeveloperProfile` to true. While convenient, this bypasses the security practice of explicit privilege elevation. 73 | 74 | ## Architecture 75 | 76 | The environment runs in a private subnet with CloudFront access, using S3 for git storage and CodePipeline for automated deployments. 77 | 78 | ![Architecture Diagram](img/architecture.png) 79 | 80 | ## Sample Application 81 | 82 | ℹ️ **Note**: The sample application is only available when using the default value for `GitHubRepo`. If you specify either a custom `GitHubRepo` or `S3AssetBucket`, you will need to provide your own Terraform application code. 83 | 84 | The repository includes a Terraform application that deploys: 85 | - Static website hosted on Amazon S3 86 | - Amazon CloudFront distribution with AWS WAF protection 87 | - Security headers and AWS KMS encryption 88 | - Amazon CloudWatch logging 89 | 90 | ![Sample Application](img/sampleapplication.png) 91 | 92 | The application deploys automatically when you set the CloudFormation parameter `DeployPipeline` to true. Once deployment completes, you can locate the website URL in the final output of the CodeBuild job. 93 | 94 | ![CodeBuild Output Screenshot](img/codebuildoutput.png) 95 | 96 | ⚠️ **WARNING**: If using CodePipeline (DeployPipeline=true), before removing the CloudFormation stack: 97 | 1. Run the 'terraform-destroy' pipeline in CodePipeline 98 | 2. Approve the manual approval step when prompted 99 | 3. Wait for pipeline completion 100 | 101 | Failing to run and approve the destroy pipeline will leave orphaned infrastructure resources in your AWS account that were created by Terraform and will need to be cleaned up manually. 102 | 103 | ## Security Considerations 104 | 105 | ⚠️ **IMPORTANT**: This sample uses HTTP for internal traffic between the Application Load Balancer and code-server Amazon EC2 instance. While external traffic is secured through CloudFront HTTPS, it is strongly recommended to: 106 | - Configure end-to-end HTTPS using custom SSL certificates on the ALB 107 | - Update ALB listener and target group to use HTTPS/443 108 | - Use a custom domain name with AWS Certificate Manager (ACM) certificates 109 | 110 | ## Security 111 | 112 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 113 | 114 | ## License 115 | 116 | This library is licensed under the MIT-0 License. See the LICENSE file. 117 | 118 | ## Disclaimer 119 | 120 | **This repository is intended for demonstration and learning purposes only.** 121 | It is **not** intended for production use. The code provided here is for educational purposes and should not be used in a live environment without proper testing, validation, and modifications. 122 | Use at your own risk. The authors are not responsible for any issues, damages, or losses that may result from using this code in production. -------------------------------------------------------------------------------- /dev/README.md: -------------------------------------------------------------------------------- 1 | # Development Directory 2 | 3 | Development and testing workspace for infrastructure code. To deploy: 4 | 1. Test your code here first 5 | 2. Copy tested code to ../release/ 6 | 3. Commit and push to trigger pipeline build 7 | -------------------------------------------------------------------------------- /img/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/sample-developer-environment/bba6b6d41e6bef798bf28d4958d69585bc84bb5f/img/architecture.png -------------------------------------------------------------------------------- /img/codebuildoutput.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/sample-developer-environment/bba6b6d41e6bef798bf28d4958d69585bc84bb5f/img/codebuildoutput.png -------------------------------------------------------------------------------- /img/sampleapplication.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/sample-developer-environment/bba6b6d41e6bef798bf28d4958d69585bc84bb5f/img/sampleapplication.png -------------------------------------------------------------------------------- /release/README.md: -------------------------------------------------------------------------------- 1 | # Here is a basic website! -------------------------------------------------------------------------------- /release/main.tf: -------------------------------------------------------------------------------- 1 | # Sample website hosted on Amazon S3 with CloudFront distribution and WAF protection 2 | 3 | ### KMS - Central encryption key for website bucket contents and CloudWatch Logs 4 | data "aws_caller_identity" "current" {} 5 | 6 | data "aws_iam_policy_document" "website_kms" { 7 | statement { 8 | sid = "Enable IAM User Permissions" 9 | effect = "Allow" 10 | 11 | principals { 12 | type = "AWS" 13 | identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"] 14 | } 15 | actions = [ 16 | "kms:*" 17 | ] 18 | resources = [ 19 | "*" 20 | ] 21 | # checkov:skip=CKV_AWS_109: "Root account requires kms:* for key management. Additional conditions applied to CloudWatch Logs access. https://docs.aws.amazon.com/kms/latest/developerguide/key-policy-overview.html" 22 | # checkov:skip=CKV_AWS_111: "KMS key policy write access is constrained with SourceAccount, via Service and ARN conditions for CloudWatch Logs." 23 | # checkov:skip=CKV_AWS_356: "Resource '*' required in KMS key policy as key ARN is not known at policy creation time. Access is constrained through conditions and actions." 24 | } 25 | 26 | statement { 27 | # https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/encrypt-log-data-kms.html 28 | sid = "Enable CloudWatch Logs access to KMS Key" 29 | effect = "Allow" 30 | 31 | principals { 32 | type = "Service" 33 | identifiers = ["logs.${var.Region}.amazonaws.com"] 34 | } 35 | actions = [ 36 | "kms:Encrypt*", 37 | "kms:Decrypt*", 38 | "kms:ReEncrypt*", 39 | "kms:GenerateDataKey*", 40 | "kms:Describe*" 41 | ] 42 | resources = [ 43 | "*" 44 | ] 45 | condition { 46 | test = "ArnLike" 47 | variable = "kms:EncryptionContext:aws:logs:arn" 48 | values = [ 49 | "arn:aws:logs:${var.Region}:${data.aws_caller_identity.current.account_id}:*" 50 | ] 51 | } 52 | condition { 53 | test = "StringEquals" 54 | variable = "aws:SourceAccount" 55 | values = [data.aws_caller_identity.current.account_id] 56 | } 57 | condition { 58 | test = "StringLike" 59 | variable = "kms:ViaService" 60 | values = ["logs.${var.Region}.amazonaws.com"] 61 | } 62 | } 63 | 64 | statement { 65 | sid = "Enable S3 Bucket Encryption" 66 | effect = "Allow" 67 | 68 | principals { 69 | type = "Service" 70 | identifiers = ["s3.amazonaws.com"] 71 | } 72 | 73 | actions = [ 74 | "kms:GenerateDataKey", 75 | "kms:Decrypt" 76 | ] 77 | 78 | resources = ["*"] 79 | 80 | condition { 81 | test = "StringEquals" 82 | variable = "aws:SourceAccount" 83 | values = [data.aws_caller_identity.current.account_id] 84 | } 85 | 86 | condition { 87 | test = "ArnLike" 88 | variable = "aws:SourceArn" 89 | values = [ 90 | aws_s3_bucket.website.arn, 91 | aws_s3_bucket.logs.arn 92 | ] 93 | } 94 | } 95 | } 96 | 97 | resource "aws_kms_key" "website" { 98 | description = "KMS key for website bucket encryption" 99 | deletion_window_in_days = 7 100 | enable_key_rotation = true 101 | policy = data.aws_iam_policy_document.website_kms.json 102 | 103 | tags = { 104 | Name = "${var.PrefixCode}-kms-website" 105 | resourcetype = "security" 106 | } 107 | } 108 | 109 | resource "aws_kms_alias" "website" { 110 | name = "alias/${var.PrefixCode}-kms-website" 111 | target_key_id = aws_kms_key.website.key_id 112 | } 113 | 114 | ### Logging Infrastructure - S3 bucket for CloudFront and S3 access logs with encryption and lifecycle rules 115 | resource "aws_s3_bucket" "logs" { 116 | bucket_prefix = "${var.PrefixCode}-logs-" 117 | force_destroy = true 118 | 119 | tags = { 120 | resourcetype = "storage" 121 | } 122 | 123 | lifecycle { 124 | # checkov:skip=CKV2_AWS_62: "Event notifications not required for logging bucket. Contents managed through lifecycle rules." 125 | # checkov:skip=CKV_AWS_144: "Cross-region replication not implemented for logging bucket to reduce complexity and cost. Consider enabling for production environments if required for compliance or disaster recovery." 126 | # checkov:skip=CKV_AWS_145: "Using AES256 instead of KMS encryption as required by CloudFront and S3 logging services. KMS encryption is not supported for logging buckets." 127 | } 128 | } 129 | 130 | resource "aws_s3_bucket_public_access_block" "logs" { 131 | bucket = aws_s3_bucket.logs.id 132 | 133 | block_public_acls = true 134 | block_public_policy = true 135 | ignore_public_acls = true 136 | restrict_public_buckets = true 137 | } 138 | 139 | resource "aws_s3_bucket_versioning" "logs" { 140 | bucket = aws_s3_bucket.logs.id 141 | versioning_configuration { 142 | status = "Enabled" 143 | } 144 | } 145 | 146 | resource "aws_s3_bucket_server_side_encryption_configuration" "logs" { 147 | bucket = aws_s3_bucket.logs.id 148 | 149 | rule { 150 | apply_server_side_encryption_by_default { 151 | sse_algorithm = "AES256" 152 | } 153 | } 154 | } 155 | 156 | resource "aws_s3_bucket_ownership_controls" "logs" { 157 | bucket = aws_s3_bucket.logs.id 158 | 159 | rule { 160 | object_ownership = "ObjectWriter" 161 | } 162 | 163 | lifecycle { 164 | # checkov:skip=CKV2_AWS_65: "BucketOwnerEnforced not possible as CloudFront logging requires ACL access. Using BucketOwnerPreferred as secure alternative." 165 | } 166 | } 167 | 168 | # Enable logging on website bucket 169 | resource "aws_s3_bucket_logging" "website" { 170 | bucket = aws_s3_bucket.website.id 171 | target_bucket = aws_s3_bucket.logs.id 172 | target_prefix = "s3-logs/" 173 | } 174 | 175 | resource "aws_s3_bucket_policy" "logs" { 176 | bucket = aws_s3_bucket.logs.id 177 | 178 | policy = jsonencode({ 179 | Version = "2012-10-17" 180 | Statement = [ 181 | { 182 | Sid = "EnforceSecureTransport" 183 | Effect = "Deny" 184 | Principal = "*" 185 | Action = "s3:*" 186 | Resource = [ 187 | "${aws_s3_bucket.logs.arn}", 188 | "${aws_s3_bucket.logs.arn}/*" 189 | ] 190 | Condition = { 191 | Bool = { 192 | "aws:SecureTransport" = false 193 | } 194 | } 195 | }, 196 | { 197 | Sid = "EnforceTLSVersion" 198 | Effect = "Deny" 199 | Principal = "*" 200 | Action = "s3:*" 201 | Resource = [ 202 | "${aws_s3_bucket.logs.arn}", 203 | "${aws_s3_bucket.logs.arn}/*" 204 | ] 205 | Condition = { 206 | NumericLessThan = { 207 | "s3:TlsVersion" = 1.2 208 | } 209 | } 210 | }, 211 | { 212 | Sid = "AllowCloudFrontLogs" 213 | Effect = "Allow" 214 | Principal = { 215 | Service = "cloudfront.amazonaws.com" 216 | } 217 | Action = "s3:PutObject" 218 | Resource = "${aws_s3_bucket.logs.arn}/cloudfront-logs/*" 219 | }, 220 | { 221 | Sid = "AllowS3LoggingService" 222 | Effect = "Allow" 223 | Principal = { 224 | Service = "logging.s3.amazonaws.com" 225 | } 226 | Action = "s3:PutObject" 227 | Resource = "${aws_s3_bucket.logs.arn}/s3-logs/*" 228 | Condition = { 229 | StringEquals = { 230 | "aws:SourceAccount" = data.aws_caller_identity.current.account_id 231 | } 232 | } 233 | } 234 | ] 235 | }) 236 | } 237 | 238 | # Lifecycle policy to manage log retention 239 | resource "aws_s3_bucket_lifecycle_configuration" "logs" { 240 | bucket = aws_s3_bucket.logs.id 241 | 242 | rule { 243 | id = "cleanup_old_logs" 244 | status = "Enabled" 245 | 246 | expiration { 247 | days = 30 248 | } 249 | 250 | abort_incomplete_multipart_upload { 251 | days_after_initiation = 1 252 | } 253 | } 254 | } 255 | 256 | ### Website Content - S3 bucket configured with CloudFront Origin Access Control for secure content delivery 257 | resource "aws_s3_bucket" "website" { 258 | bucket_prefix = "${var.PrefixCode}-s3-website-" 259 | 260 | tags = { 261 | resourcetype = "storage" 262 | } 263 | lifecycle { 264 | # checkov:skip=CKV2_AWS_62: "Event notifications not required for static website content. Changes are managed through deployment pipeline." 265 | # checkov:skip=CKV_AWS_144: "Cross-region replication not required for sample website. CloudFront provides global content delivery. Consider enabling for production environments requiring disaster recovery." 266 | # checkov:skip=CKV2_AWS_61: "Lifecycle configuration not required for website content bucket as objects are managed through deployment pipeline." 267 | } 268 | } 269 | 270 | resource "aws_s3_bucket_public_access_block" "website" { 271 | bucket = aws_s3_bucket.website.id 272 | 273 | block_public_acls = true 274 | block_public_policy = true 275 | ignore_public_acls = true 276 | restrict_public_buckets = true 277 | } 278 | 279 | resource "aws_s3_bucket_versioning" "website" { 280 | bucket = aws_s3_bucket.website.id 281 | versioning_configuration { 282 | status = "Enabled" 283 | } 284 | } 285 | 286 | resource "aws_s3_bucket_server_side_encryption_configuration" "website" { 287 | bucket = aws_s3_bucket.website.id 288 | 289 | rule { 290 | apply_server_side_encryption_by_default { 291 | kms_master_key_id = aws_kms_key.website.id 292 | sse_algorithm = "aws:kms" 293 | } 294 | } 295 | } 296 | 297 | # CloudFront Origin Access Control for secure S3 access 298 | resource "aws_cloudfront_origin_access_control" "website" { 299 | name = "${var.PrefixCode}-oac-website" 300 | description = "Origin Access Control for static website" 301 | origin_access_control_origin_type = "s3" 302 | signing_behavior = "always" 303 | signing_protocol = "sigv4" 304 | } 305 | 306 | # Bucket policy allowing CloudFront to access S3 content 307 | resource "aws_s3_bucket_policy" "website" { 308 | bucket = aws_s3_bucket.website.id 309 | 310 | policy = jsonencode({ 311 | Version = "2012-10-17" 312 | Statement = [ 313 | { 314 | Sid = "AllowCloudFrontAccess" 315 | Effect = "Allow" 316 | Principal = { 317 | Service = "cloudfront.amazonaws.com" 318 | } 319 | Action = "s3:GetObject" 320 | Resource = "${aws_s3_bucket.website.arn}/*" 321 | Condition = { 322 | StringEquals = { 323 | "AWS:SourceArn" = aws_cloudfront_distribution.website.arn 324 | } 325 | } 326 | } 327 | ] 328 | }) 329 | } 330 | 331 | ### Access Layer - CloudFront distribution with WAF protection and security headers 332 | # WAF web ACL to protect CloudFront distribution 333 | resource "aws_wafv2_web_acl" "website" { 334 | provider = aws.us-east-1 335 | name = "${var.PrefixCode}-waf-website" 336 | description = "WAF Web ACL for CloudFront distribution" 337 | scope = "CLOUDFRONT" 338 | 339 | default_action { 340 | allow {} 341 | } 342 | 343 | rule { 344 | name = "AWSManagedRulesCommonRuleSet" 345 | priority = 1 346 | 347 | override_action { 348 | none {} 349 | } 350 | 351 | statement { 352 | managed_rule_group_statement { 353 | name = "AWSManagedRulesCommonRuleSet" 354 | vendor_name = "AWS" 355 | } 356 | } 357 | 358 | visibility_config { 359 | cloudwatch_metrics_enabled = true 360 | metric_name = "AWSManagedRulesCommonRuleSetMetric" 361 | sampled_requests_enabled = true 362 | } 363 | } 364 | 365 | rule { 366 | name = "AWSManagedRulesKnownBadInputsRuleSet" 367 | priority = 2 368 | 369 | override_action { 370 | none {} 371 | } 372 | 373 | statement { 374 | managed_rule_group_statement { 375 | name = "AWSManagedRulesKnownBadInputsRuleSet" 376 | vendor_name = "AWS" 377 | } 378 | } 379 | 380 | visibility_config { 381 | cloudwatch_metrics_enabled = true 382 | metric_name = "AWSManagedRulesKnownBadInputsRuleSetMetric" 383 | sampled_requests_enabled = true 384 | } 385 | } 386 | 387 | visibility_config { 388 | cloudwatch_metrics_enabled = true 389 | metric_name = "WebACLMetric" 390 | sampled_requests_enabled = true 391 | } 392 | 393 | tags = { 394 | resourcetype = "security" 395 | } 396 | 397 | lifecycle { 398 | # checkov:skip=CKV2_AWS_31: "WAF logging via Kinesis Firehose disabled for sample website to reduce complexity and cost. CloudWatch metrics enabled for basic monitoring. Consider enabling WAF logging in production for security analysis." 399 | # checkov:skip=CKV2_AWS_47: "Log4j protection provided through AWSManagedRulesCommonRuleSet. Dedicated Log4j rule group not required as core protections are included in common rules." 400 | } 401 | } 402 | 403 | # Security headers policy for CloudFront responses (HSTS, CSP) 404 | resource "aws_cloudfront_response_headers_policy" "security_headers" { 405 | name = "${var.PrefixCode}-cloudfrontheaders-website" 406 | comment = "Security headers policy" 407 | 408 | security_headers_config { 409 | strict_transport_security { 410 | access_control_max_age_sec = 31536000 411 | include_subdomains = true 412 | preload = true 413 | override = true 414 | } 415 | content_security_policy { 416 | content_security_policy = "default-src 'self'" 417 | override = true 418 | } 419 | } 420 | } 421 | 422 | # CloudFront distribution for secure content delivery with logging enabled 423 | resource "aws_cloudfront_distribution" "website" { 424 | enabled = true 425 | default_root_object = "index.html" 426 | web_acl_id = aws_wafv2_web_acl.website.arn 427 | 428 | origin { 429 | domain_name = aws_s3_bucket.website.bucket_regional_domain_name 430 | origin_id = "S3Origin" 431 | origin_access_control_id = aws_cloudfront_origin_access_control.website.id 432 | } 433 | 434 | 435 | logging_config { 436 | bucket = aws_s3_bucket.logs.bucket_domain_name 437 | include_cookies = false 438 | prefix = "cloudfront-logs/" 439 | } 440 | 441 | default_cache_behavior { 442 | allowed_methods = ["GET", "HEAD"] 443 | cached_methods = ["GET", "HEAD"] 444 | target_origin_id = "S3Origin" 445 | viewer_protocol_policy = "redirect-to-https" 446 | response_headers_policy_id = aws_cloudfront_response_headers_policy.security_headers.id 447 | cache_policy_id = "658327ea-f89d-4fab-a63d-7e88639e58f6" # CachingOptimized policy 448 | origin_request_policy_id = "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf" # CORS-S3Origin policy 449 | } 450 | 451 | restrictions { 452 | geo_restriction { 453 | restriction_type = length(var.GeoRestriction) > 0 ? "whitelist" : "none" 454 | locations = var.GeoRestriction 455 | } 456 | } 457 | 458 | viewer_certificate { 459 | cloudfront_default_certificate = true 460 | minimum_protocol_version = "TLSv1.2_2021" 461 | ssl_support_method = "sni-only" 462 | } 463 | 464 | tags = { 465 | resourcetype = "network" 466 | } 467 | 468 | lifecycle { 469 | # checkov:skip=CKV_AWS_310: "Consider implementing origin failover for production environments. Skipped for development to reduce complexity and cost." 470 | # checkov:skip=CKV2_AWS_42: "Using default CloudFront certificate for sample website. Custom SSL certificate recommended for production use with custom domain names." 471 | # checkov:skip=CKV_AWS_86: "CloudFront logging not implemented. S3 access logs provide sufficient monitoring for sample website. Consider implementing CloudFront logging for production environments" 472 | # checkov:skip=CKV_AWS_109: "Root account requires kms:* for key management. Additional conditions applied to CloudWatch Logs access. https://docs.aws.amazon.com/kms/latest/developerguide/key-policy-overview.html" 473 | # checkov:skip=CKV_AWS_111: "KMS key policy write access is constrained with SourceAccount, via Service and ARN conditions for CloudWatch Logs." 474 | # checkov:skip=CKV_AWS_356: "Resource '*' required in KMS key policy as key ARN is not known at policy creation time. Access is constrained through conditions and actions." 475 | # checkov:skip=CKV2_AWS_47: "Log4j protection provided through AWSManagedRulesCommonRuleSet. Dedicated Log4j rule group not required as core protections are included in common rules." 476 | } 477 | } -------------------------------------------------------------------------------- /release/outputs.tf: -------------------------------------------------------------------------------- 1 | output "website_url" { 2 | value = aws_cloudfront_distribution.website.domain_name 3 | } -------------------------------------------------------------------------------- /release/provider.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = var.Region 3 | 4 | default_tags { 5 | tags = { 6 | Environment = var.EnvTag 7 | Provisioner = "Terraform" 8 | Solution = var.SolTag 9 | } 10 | } 11 | } 12 | 13 | # CloudFront/WAF provider 14 | provider "aws" { 15 | alias = "us-east-1" 16 | region = "us-east-1" 17 | 18 | default_tags { 19 | tags = { 20 | Environment = var.EnvTag 21 | Provisioner = "Terraform" 22 | Solution = var.SolTag 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /release/terraform.tfvars: -------------------------------------------------------------------------------- 1 | # PrefixCode and Region values are injected into the pipeline at build. Refer to CloudFormation template codebuildprojectbuild resource. 2 | EnvTag = "dev" 3 | SolTag = "demoweb" 4 | GeoRestriction = [] -------------------------------------------------------------------------------- /release/variables.tf: -------------------------------------------------------------------------------- 1 | variable "PrefixCode" { 2 | description = "Prefix for resource names" 3 | type = string 4 | } 5 | variable "Region" { 6 | description = "AWS region for resource deployment" 7 | type = string 8 | } 9 | 10 | variable "EnvTag" { 11 | description = "Environment identifier for resource tagging (e.g., dev, prod)" 12 | type = string 13 | } 14 | 15 | variable "SolTag" { 16 | description = "Solution identifier for resource grouping and tagging" 17 | type = string 18 | } 19 | variable "GeoRestriction" { 20 | description = "List of ISO Alpha-2 country codes for geo-restriction. Example: GB for UK, IE for Ireland. Leave empty [] for no restrictions" 21 | type = list(string) 22 | default = [] 23 | } -------------------------------------------------------------------------------- /release/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 5.94.1" 6 | } 7 | } 8 | 9 | backend "s3" { 10 | key = "terraform/statefile.tfstate" 11 | encrypt = true 12 | use_lockfile = true 13 | } 14 | } -------------------------------------------------------------------------------- /release/website.tf: -------------------------------------------------------------------------------- 1 | # Sample index.html file for website 2 | resource "aws_s3_object" "index" { 3 | bucket = aws_s3_bucket.website.id 4 | key = "index.html" 5 | content = < 7 | 8 |

Hello from Terraform!

9 |

If you see this, your pipeline is working.

10 |

Deployed at: ${timestamp()}

11 | 12 | 13 | EOF 14 | content_type = "text/html" 15 | } -------------------------------------------------------------------------------- /sample-developer-environment.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: Sample browser-based development environment with VS Code, S3-backed git storage, and secure CloudFront access 3 | Metadata: 4 | AWS::CloudFormation::Interface: 5 | ParameterGroups: 6 | - Label: 7 | default: Networking 8 | Parameters: 9 | - VpcCidr 10 | - Label: 11 | default: Deployment Settings 12 | Parameters: 13 | - CodeServerVersion 14 | - GitHubRepo 15 | - S3AssetBucket 16 | - S3AssetPrefix 17 | - DeployPipeline 18 | - RotateSecret 19 | - AutoSetDeveloperProfile 20 | - Label: 21 | default: Compute settings 22 | Parameters: 23 | - InstanceArchitecture 24 | - InstanceType 25 | - AMIx86CodeServer 26 | - AMIArmCodeServer 27 | - Label: 28 | default: Tagging and Naming 29 | Parameters: 30 | - PrefixCode 31 | - SolutionTag 32 | - EnvironmentTag 33 | Parameters: 34 | VpcCidr: 35 | Type: String 36 | Default: "10.180" 37 | Description: Network addressing prefix (first two octets) for VPC and subnet CIDR blocks, e.g., '10.180' 38 | GitHubRepo: 39 | Type: String 40 | Default: https://github.com/aws-samples/sample-developer-environment.git 41 | Description: GitHub repository URL to clone as initial workspace content for code-server 42 | S3AssetBucket: 43 | Type: String 44 | Default: "" 45 | Description: (Optional) S3 path for initial workspace content in format 'my-bucket-name'. If provided, this OVERWRITES GitHubRepo parameter. 46 | S3AssetPrefix: 47 | Type: String 48 | Default: "" 49 | Description: (Optional) Asset prefix path. Only required when S3AssetBucket is specified. Must end in '/' e.g. 'assets/' or 'assets/solution/' 50 | DeployPipeline: 51 | Type: String 52 | Default: false 53 | AllowedValues: 54 | - true 55 | - false 56 | Description: Deploy AWS CodePipeline with CodeBuild to automatically build/destroy infrastructure using Terraform 57 | RotateSecret: 58 | Type: String 59 | Default: false 60 | AllowedValues: 61 | - true 62 | - false 63 | Description: Rotate code-server secret every 30 days. 64 | AutoSetDeveloperProfile: 65 | Type: String 66 | Default: false 67 | AllowedValues: 68 | - true 69 | - false 70 | Description: Automatically set Developer profile as default in code-server terminal sessions without requiring elevation 71 | CodeServerVersion: 72 | Type: String 73 | Default: "4.99.3" 74 | Description: Version of code-server to install. See available versions at github.com/coder/code-server/releases 75 | InstanceArchitecture: 76 | Type: String 77 | Default: arm64 78 | AllowedValues: 79 | - amd64 80 | - arm64 81 | Description: Choose amd64 for AMD/Intel instances or arm64 for Graviton instances 82 | InstanceType: 83 | Type: String 84 | Default: t4g.large 85 | Description: EC2 instance type MUST match architecture. amd64= t3.small, t3a.large, c6i.xlarge, m6a.2xlarge, etc. arm64= t4g.large, c7g.xlarge, m6g.2xlarge, etc.Template will fail if mismatched. 86 | AMIx86CodeServer: 87 | Type: AWS::SSM::Parameter::Value 88 | Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 89 | Description: SSM parameter path for x86_64 AMI (used for AMD/Intel instances) 90 | AMIArmCodeServer: 91 | Type: AWS::SSM::Parameter::Value 92 | Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-arm64 93 | Description: SSM parameter path for arm64 AMI (used for Graviton instances) 94 | PrefixCode: 95 | Type: String 96 | Default: devbox 97 | Description: Resource naming prefix for uniqueness and organization. Six characters or less. Must be lowercase alphanumeric and cannot start with 'aws' 98 | EnvironmentTag: 99 | Type: String 100 | Default: production 101 | Description: Environment identifier for resource tagging e.g. dev, prod 102 | SolutionTag: 103 | Type: String 104 | Default: SampleDeveloperEnvironment 105 | Description: Solution identifier for resource tagging and grouping. Use alphanumeric characters only 106 | 107 | ### Region to CloudFront Prefix List mapping - delete regions not required for deployment 108 | ### To get prefix list for your region: aws ec2 describe-managed-prefix-lists --filters Name=prefix-list-name,Values=com.amazonaws.global.cloudfront.origin-facing 109 | Mappings: 110 | RegionMap: 111 | ap-northeast-1: 112 | CloudFrontPrefixList: pl-58a04531 113 | ALBAccount: 582318560864 114 | ap-northeast-2: 115 | CloudFrontPrefixList: pl-22a6434b 116 | ALBAccount: 600734575887 117 | ap-northeast-3: 118 | CloudFrontPrefixList: pl-31a14458 119 | ALBAccount: 383597477331 120 | ap-south-1: 121 | CloudFrontPrefixList: pl-9aa247f3 122 | ALBAccount: 718504428378 123 | ap-southeast-1: 124 | CloudFrontPrefixList: pl-31a34658 125 | ALBAccount: 114774131450 126 | ap-southeast-2: 127 | CloudFrontPrefixList: pl-b8a742d1 128 | ALBAccount: 783225319266 129 | ca-central-1: 130 | CloudFrontPrefixList: pl-38a64351 131 | ALBAccount: 985666609251 132 | eu-central-1: 133 | CloudFrontPrefixList: pl-a3a144ca 134 | ALBAccount: 054676820928 135 | eu-north-1: 136 | CloudFrontPrefixList: pl-fab65393 137 | ALBAccount: 897822967062 138 | eu-west-1: 139 | CloudFrontPrefixList: pl-4fa04526 140 | ALBAccount: 156460612806 141 | eu-west-2: 142 | CloudFrontPrefixList: pl-93a247fa 143 | ALBAccount: 652711504416 144 | eu-west-3: 145 | CloudFrontPrefixList: pl-75b1541c 146 | ALBAccount: 009996457667 147 | sa-east-1: 148 | CloudFrontPrefixList: pl-5da64334 149 | ALBAccount: 507241528517 150 | us-east-1: 151 | CloudFrontPrefixList: pl-3b927c52 152 | ALBAccount: 127311923021 153 | us-east-2: 154 | CloudFrontPrefixList: pl-b6a144df 155 | ALBAccount: 033677994240 156 | us-west-1: 157 | CloudFrontPrefixList: pl-4ea04527 158 | ALBAccount: 027434742980 159 | us-west-2: 160 | CloudFrontPrefixList: pl-82a045eb 161 | ALBAccount: 797873946194 162 | 163 | Conditions: 164 | CreatePipeline: !Equals [!Ref DeployPipeline, "true"] 165 | EnableRotation: !Equals [!Ref RotateSecret, "true"] 166 | IsArm64: !Equals [!Ref InstanceArchitecture, 'arm64'] 167 | HasS3Assets: !Not [!Equals [!Ref S3AssetBucket, ""]] 168 | 169 | 170 | Resources: 171 | ### Resource Group - Groups all AWS resources for easy management and tracking 172 | resourcegroup: 173 | Type: AWS::ResourceGroups::Group 174 | Properties: 175 | Description: Sample Developer Environment Resources 176 | Name: !Sub ${PrefixCode}-resources 177 | ResourceQuery: 178 | Type: TAG_FILTERS_1_0 179 | Query: 180 | ResourceTypeFilters: 181 | - AWS::AllSupported 182 | TagFilters: 183 | - Key: solution 184 | Values: 185 | - !Sub ${SolutionTag} 186 | Tags: 187 | - Key: Name 188 | Value: !Sub ${PrefixCode}-resources 189 | - Key: provisioner 190 | Value: CFN 191 | - Key: solution 192 | Value: !Sub ${SolutionTag} 193 | - Key: environment 194 | Value: !Sub ${EnvironmentTag} 195 | - Key: resourcetype 196 | Value: management 197 | 198 | ### KMS - Central encryption key for CloudWatch Logs, Secrets Manager, and other AWS services 199 | kmskey: 200 | Type: AWS::KMS::Key 201 | Properties: 202 | Description: Shared encryption key for AWS services 203 | PendingWindowInDays: 7 204 | EnableKeyRotation: true 205 | KeyPolicy: 206 | Version: 2012-10-17 207 | Id: key-default-1 208 | Statement: 209 | - Sid: Enable IAM User Permissions 210 | # https://docs.aws.amazon.com/kms/latest/developerguide/key-policy-overview.html 211 | Effect: Allow 212 | Principal: 213 | AWS: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:root 214 | Action: 215 | - kms:* 216 | Resource: "*" 217 | - Sid: Enable Cloudwatch access to KMS Key for VPC log group 218 | # https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/encrypt-log-data-kms.html 219 | Effect: Allow 220 | Principal: 221 | Service: !Sub logs.${AWS::Region}.amazonaws.com 222 | Action: 223 | - kms:Encrypt* 224 | - kms:Decrypt* 225 | - kms:ReEncrypt* 226 | - kms:GenerateDataKey* 227 | - kms:Describe* 228 | Resource: "*" 229 | Condition: 230 | ArnLike: 231 | "kms:EncryptionContext:aws:logs:arn": !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:*" 232 | Tags: 233 | - Key: provisioner 234 | Value: CFN 235 | - Key: solution 236 | Value: !Sub ${SolutionTag} 237 | - Key: environment 238 | Value: !Sub ${EnvironmentTag} 239 | - Key: resourcetype 240 | Value: security 241 | kmskeyalias: 242 | Type: AWS::KMS::Alias 243 | Properties: 244 | AliasName: !Sub alias/${PrefixCode}-kms-cmk 245 | TargetKeyId: !Ref kmskey 246 | 247 | ### Network Infrastructure - VPC with public/private subnets and flow logs for developer environment 248 | vpc01: 249 | Type: AWS::EC2::VPC 250 | Properties: 251 | CidrBlock: !Sub ${VpcCidr}.0.0/16 252 | EnableDnsHostnames: true 253 | EnableDnsSupport: true 254 | InstanceTenancy: default 255 | Tags: 256 | - Key: Name 257 | Value: !Sub ${PrefixCode}-vpc01 258 | - Key: provisioner 259 | Value: CFN 260 | - Key: solution 261 | Value: !Sub ${SolutionTag} 262 | - Key: environment 263 | Value: !Sub ${EnvironmentTag} 264 | - Key: resourcetype 265 | Value: network 266 | iamrolevpcflowlogs: 267 | Type: AWS::IAM::Role 268 | # https://docs.aws.amazon.com/vpc/latest/userguide/flow-logs-cwl.html 269 | Properties: 270 | RoleName: !Sub ${PrefixCode}-iamrole-vpcflowlogs 271 | Description: Publish flow logs to CloudWatch Logs 272 | AssumeRolePolicyDocument: 273 | Version: 2012-10-17 274 | Statement: 275 | - Effect: Allow 276 | Principal: 277 | Service: vpc-flow-logs.amazonaws.com 278 | Action: sts:AssumeRole 279 | Policies: 280 | - PolicyName: !Sub ${PrefixCode}-iampolicy-VpcFlowLogsPermissions 281 | PolicyDocument: 282 | Version: 2012-10-17 283 | Statement: 284 | - Effect: Allow 285 | Action: 286 | - logs:CreateLogGroup 287 | - logs:CreateLogStream 288 | - logs:PutLogEvents 289 | - logs:DescribeLogGroups 290 | - logs:DescribeLogStreams 291 | Resource: 292 | - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/events/${PrefixCode}-vpcflowlog:* 293 | - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/events/${PrefixCode}-vpcflowlog:*:* 294 | Condition: 295 | StringEquals: 296 | aws:SourceAccount: !Sub "${AWS::AccountId}" 297 | ArnLike: 298 | "aws:SourceArn": !Sub "arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:*" 299 | Tags: 300 | - Key: Name 301 | Value: !Sub ${PrefixCode}-iamrole-VpcFlowLogs 302 | - Key: provisioner 303 | Value: CFN 304 | - Key: solution 305 | Value: !Sub ${SolutionTag} 306 | - Key: resourcetype 307 | Value: security 308 | - Key: environment 309 | Value: !Sub ${EnvironmentTag} 310 | Metadata: 311 | cdk_nag: 312 | rules_to_suppress: 313 | - id: AwsSolutions-IAM5 314 | reason: "XXXX https://docs.aws.amazon.com/vpc/latest/userguide/flow-logs-iam-role.html" 315 | vpcloggroup: 316 | Type: AWS::Logs::LogGroup 317 | Properties: 318 | KmsKeyId: !GetAtt kmskey.Arn 319 | LogGroupName: !Sub /aws/events/${PrefixCode}-vpcflowlog 320 | RetentionInDays: 30 321 | Tags: 322 | - Key: Name 323 | Value: !Sub ${PrefixCode}-vpcflowloggroup 324 | - Key: provisioner 325 | Value: CFN 326 | - Key: solution 327 | Value: !Sub ${SolutionTag} 328 | - Key: resourcetype 329 | Value: monitoring 330 | - Key: environment 331 | Value: !Sub ${EnvironmentTag} 332 | vpcflowlog: 333 | Type: AWS::EC2::FlowLog 334 | Properties: 335 | DeliverLogsPermissionArn: !GetAtt iamrolevpcflowlogs.Arn 336 | LogGroupName: !Ref vpcloggroup 337 | ResourceId: !Ref vpc01 338 | ResourceType: VPC 339 | TrafficType: ALL 340 | Tags: 341 | - Key: Name 342 | Value: !Sub ${PrefixCode}-vpcflowlog 343 | - Key: provisioner 344 | Value: CFN 345 | - Key: solution 346 | Value: !Sub ${SolutionTag} 347 | - Key: resourcetype 348 | Value: monitoring 349 | - Key: environment 350 | Value: !Sub ${EnvironmentTag} 351 | internetgateway: 352 | Type: AWS::EC2::InternetGateway 353 | Properties: 354 | Tags: 355 | - Key: Name 356 | Value: !Sub ${PrefixCode}-internetgateway 357 | - Key: provisioner 358 | Value: CFN 359 | - Key: solution 360 | Value: !Sub ${SolutionTag} 361 | - Key: resourcetype 362 | Value: network 363 | - Key: environment 364 | Value: !Sub ${EnvironmentTag} 365 | internetgatewayattach: 366 | Type: AWS::EC2::VPCGatewayAttachment 367 | Properties: 368 | VpcId: !Ref vpc01 369 | InternetGatewayId: !Ref internetgateway 370 | subnetpublic01: 371 | Type: AWS::EC2::Subnet 372 | Properties: 373 | VpcId: !Ref vpc01 374 | AvailabilityZone: !Select [0, !GetAZs ""] 375 | CidrBlock: !Sub ${VpcCidr}.1.0/24 376 | MapPublicIpOnLaunch: false 377 | Tags: 378 | - Key: Name 379 | Value: !Sub ${PrefixCode}-subnet-public01-AvailabilityZone01 380 | - Key: provisioner 381 | Value: CFN 382 | - Key: solution 383 | Value: !Sub ${SolutionTag} 384 | - Key: resourcetype 385 | Value: network 386 | - Key: environment 387 | Value: !Sub ${EnvironmentTag} 388 | # Second public subnet required for ALB 389 | subnetpublic02: 390 | Type: AWS::EC2::Subnet 391 | Properties: 392 | VpcId: !Ref vpc01 393 | AvailabilityZone: !Select [1, !GetAZs ""] 394 | CidrBlock: !Sub ${VpcCidr}.2.0/24 395 | MapPublicIpOnLaunch: false 396 | Tags: 397 | - Key: Name 398 | Value: !Sub ${PrefixCode}-subnet-public02-AvailabilityZone02 399 | - Key: provisioner 400 | Value: CFN 401 | - Key: solution 402 | Value: !Sub ${SolutionTag} 403 | - Key: resourcetype 404 | Value: network 405 | - Key: environment 406 | Value: !Sub ${EnvironmentTag} 407 | subnetprivate01: 408 | Type: AWS::EC2::Subnet 409 | Properties: 410 | VpcId: !Ref vpc01 411 | AvailabilityZone: !Select [0, !GetAZs ""] 412 | CidrBlock: !Sub ${VpcCidr}.3.0/24 413 | MapPublicIpOnLaunch: false 414 | Tags: 415 | - Key: Name 416 | Value: !Sub ${PrefixCode}-subnet-private01-AvailabilityZone01 417 | - Key: provisioner 418 | Value: CFN 419 | - Key: solution 420 | Value: !Sub ${SolutionTag} 421 | - Key: resourcetype 422 | Value: network 423 | - Key: environment 424 | Value: !Sub ${EnvironmentTag} 425 | elasticip01: 426 | Type: AWS::EC2::EIP 427 | Properties: 428 | Tags: 429 | - Key: Name 430 | Value: !Sub ${PrefixCode}-eip-AvailabilityZone01 431 | - Key: provisioner 432 | Value: CFN 433 | - Key: solution 434 | Value: !Sub ${SolutionTag} 435 | - Key: resourcetype 436 | Value: network 437 | - Key: environment 438 | Value: !Sub ${EnvironmentTag} 439 | natgateway01: 440 | Type: AWS::EC2::NatGateway 441 | Properties: 442 | AllocationId: !GetAtt elasticip01.AllocationId 443 | SubnetId: !Ref subnetpublic01 444 | Tags: 445 | - Key: Name 446 | Value: !Sub ${PrefixCode}-nat-public01-AvailabilityZone01 447 | - Key: provisioner 448 | Value: CFN 449 | - Key: solution 450 | Value: !Sub ${SolutionTag} 451 | - Key: resourcetype 452 | Value: network 453 | - Key: environment 454 | Value: !Sub ${EnvironmentTag} 455 | DependsOn: internetgateway 456 | routetablepublic: 457 | Type: AWS::EC2::RouteTable 458 | Properties: 459 | VpcId: !Ref vpc01 460 | Tags: 461 | - Key: Name 462 | Value: !Sub ${PrefixCode}-routetable-public 463 | - Key: provisioner 464 | Value: CFN 465 | - Key: solution 466 | Value: !Sub ${SolutionTag} 467 | - Key: resourcetype 468 | Value: network 469 | - Key: environment 470 | Value: !Sub ${EnvironmentTag} 471 | routepublic: 472 | Type: AWS::EC2::Route 473 | Properties: 474 | RouteTableId: !Ref routetablepublic 475 | DestinationCidrBlock: 0.0.0.0/0 476 | GatewayId: !GetAtt internetgateway.InternetGatewayId 477 | routeassociationpublic01: 478 | Type: AWS::EC2::SubnetRouteTableAssociation 479 | Properties: 480 | SubnetId: !Ref subnetpublic01 481 | RouteTableId: !Ref routetablepublic 482 | routeassociationpublic02: 483 | Type: AWS::EC2::SubnetRouteTableAssociation 484 | Properties: 485 | SubnetId: !Ref subnetpublic02 486 | RouteTableId: !Ref routetablepublic 487 | routetableprivate: 488 | Type: AWS::EC2::RouteTable 489 | Properties: 490 | VpcId: !Ref vpc01 491 | Tags: 492 | - Key: Name 493 | Value: !Sub ${PrefixCode}-routetable-private1-AvailabilityZone1 494 | - Key: provisioner 495 | Value: CFN 496 | - Key: solution 497 | Value: !Sub ${SolutionTag} 498 | - Key: resourcetype 499 | Value: network 500 | - Key: environment 501 | Value: !Sub ${EnvironmentTag} 502 | routeprivate: 503 | Type: AWS::EC2::Route 504 | Properties: 505 | RouteTableId: !Ref routetableprivate 506 | DestinationCidrBlock: 0.0.0.0/0 507 | NatGatewayId: !GetAtt natgateway01.NatGatewayId 508 | routeassociationprivate: 509 | Type: AWS::EC2::SubnetRouteTableAssociation 510 | Properties: 511 | SubnetId: !Ref subnetprivate01 512 | RouteTableId: !Ref routetableprivate 513 | 514 | ### Centralized logging bucket for CloudFront and S3 access logs 515 | s3bucketlogs: 516 | Type: AWS::S3::Bucket 517 | DeletionPolicy: Delete 518 | Properties: 519 | BucketEncryption: 520 | ServerSideEncryptionConfiguration: 521 | - ServerSideEncryptionByDefault: 522 | SSEAlgorithm: AES256 523 | PublicAccessBlockConfiguration: 524 | BlockPublicAcls: true 525 | BlockPublicPolicy: true 526 | IgnorePublicAcls: true 527 | RestrictPublicBuckets: true 528 | VersioningConfiguration: 529 | Status: Enabled 530 | OwnershipControls: 531 | Rules: 532 | - ObjectOwnership: ObjectWriter 533 | LifecycleConfiguration: 534 | Rules: 535 | - Id: DeleteOldLogs 536 | Status: Enabled 537 | ExpirationInDays: 30 538 | Tags: 539 | - Key: Name 540 | Value: !Sub ${PrefixCode}-logs 541 | - Key: provisioner 542 | Value: CFN 543 | - Key: solution 544 | Value: !Sub ${SolutionTag} 545 | - Key: resourcetype 546 | Value: storage 547 | - Key: environment 548 | Value: !Sub ${EnvironmentTag} 549 | Metadata: 550 | cfn_nag: 551 | rules_to_suppress: 552 | - id: W35 553 | reason: "This is an access logs bucket and does not need its own access logging." 554 | checkov: 555 | skip: 556 | - id: CKV_AWS_18 557 | comment: "This is an access logs bucket and does not need its own access logging." 558 | s3bucketpolicylogs: 559 | Type: AWS::S3::BucketPolicy 560 | Properties: 561 | Bucket: !Ref s3bucketlogs 562 | PolicyDocument: 563 | Version: 2012-10-17 564 | Statement: 565 | - Sid: EnforceSecureTransport 566 | Effect: Deny 567 | Principal: "*" 568 | Action: s3:* 569 | Resource: 570 | - !Sub arn:${AWS::Partition}:s3:::${s3bucketlogs} 571 | - !Sub arn:${AWS::Partition}:s3:::${s3bucketlogs}/* 572 | Condition: 573 | Bool: 574 | aws:SecureTransport: false 575 | - Sid: Allow TLS 1.2 and above 576 | Effect: Deny 577 | Principal: "*" 578 | Action: 579 | - s3:* 580 | Resource: 581 | - !Sub arn:${AWS::Partition}:s3:::${s3bucketlogs} 582 | - !Sub arn:${AWS::Partition}:s3:::${s3bucketlogs}/* 583 | Condition: 584 | NumericLessThan: 585 | s3:TlsVersion: 1.2 586 | # https://docs.aws.amazon.com/elasticloadbalancing/latest/application/enable-access-logging.html 587 | - Sid: Allow ALB logging access regions available as of August 2022 or later 588 | Effect: Allow 589 | Principal: 590 | Service: logdelivery.elasticloadbalancing.amazonaws.com 591 | Action: 592 | - s3:PutObject 593 | Resource: 594 | - !Sub arn:${AWS::Partition}:s3:::${s3bucketlogs} 595 | - !Sub arn:${AWS::Partition}:s3:::${s3bucketlogs}/* 596 | - Sid: Allow ALB logging access regions available before August 2022 597 | Effect: Allow 598 | Principal: 599 | AWS: 600 | !Join 601 | - "" 602 | - - "arn:" 603 | - !Ref AWS::Partition 604 | - ":iam::" 605 | - !FindInMap [RegionMap, !Ref "AWS::Region", ALBAccount] 606 | - ":root" 607 | Action: 608 | - s3:PutObject 609 | Resource: 610 | - !Sub arn:${AWS::Partition}:s3:::${s3bucketlogs} 611 | - !Sub arn:${AWS::Partition}:s3:::${s3bucketlogs}/* 612 | - Sid: Allow S3 Logging Service for git and artifact buckets 613 | Effect: Allow 614 | Principal: 615 | Service: logging.s3.amazonaws.com 616 | Action: s3:PutObject 617 | Resource: 618 | - !Sub arn:${AWS::Partition}:s3:::${s3bucketlogs}/s3-logs/git/* 619 | - !Sub arn:${AWS::Partition}:s3:::${s3bucketlogs}/s3-logs/pipeline/* 620 | Condition: 621 | StringEquals: 622 | aws:SourceAccount: !Ref AWS::AccountId 623 | ArnLike: 624 | aws:SourceArn: 625 | - !Sub arn:${AWS::Partition}:s3:::${s3bucketgit} 626 | - !If 627 | # BEWARE: Inline condition for artifact bucket logging - can't be grouped due to policy structure. 628 | - CreatePipeline 629 | - Sid: Allow S3 Logging Service for artifact bucket 630 | Effect: Allow 631 | Principal: 632 | Service: logging.s3.amazonaws.com 633 | Action: s3:PutObject 634 | Resource: 635 | - !Sub arn:${AWS::Partition}:s3:::${s3bucketlogs}/s3-logs/pipeline/* 636 | Condition: 637 | StringEquals: 638 | aws:SourceAccount: !Ref AWS::AccountId 639 | ArnLike: 640 | aws:SourceArn: 641 | - !Sub arn:${AWS::Partition}:s3:::${s3bucketartifact} 642 | - !Ref AWS::NoValue 643 | - Sid: Allow ALB Logging 644 | Effect: Allow 645 | Principal: 646 | AWS: !Join 647 | - "" 648 | - - "arn:" 649 | - !Ref AWS::Partition 650 | - ":iam::" 651 | - !FindInMap [RegionMap, !Ref "AWS::Region", ALBAccount] 652 | - ":root" 653 | Action: s3:PutObject 654 | Resource: !Sub arn:${AWS::Partition}:s3:::${s3bucketlogs}/alb-logs/* 655 | 656 | ### Access Layer - ALB and CloudFront distribution for secure access to code-server 657 | albcodeserver: 658 | Type: AWS::ElasticLoadBalancingV2::LoadBalancer 659 | Properties: 660 | Name: !Sub ${PrefixCode}-alb-codeserver 661 | Scheme: internet-facing 662 | Type: application 663 | Subnets: 664 | - !Ref subnetpublic01 665 | - !Ref subnetpublic02 666 | SecurityGroups: 667 | - !Ref securitygroupalb 668 | LoadBalancerAttributes: 669 | - Key: access_logs.s3.enabled 670 | Value: true 671 | - Key: access_logs.s3.bucket 672 | Value: !Ref s3bucketlogs 673 | - Key: access_logs.s3.prefix 674 | Value: alb-logs 675 | Tags: 676 | - Key: Name 677 | Value: !Sub ${PrefixCode}-alb-codeserver 678 | - Key: provisioner 679 | Value: CFN 680 | - Key: solution 681 | Value: !Sub ${SolutionTag} 682 | - Key: resourcetype 683 | Value: network 684 | - Key: environment 685 | Value: !Sub ${EnvironmentTag} 686 | Metadata: 687 | cfn_nag: 688 | rules_to_suppress: 689 | - id: CKV_AWS_131 690 | reason: "ALB uses HTTP for internal AWS traffic. Security handled by CloudFront HTTPS and origin verification. Consider end-to-end HTTPS if required." 691 | checkov: 692 | skip: 693 | - id: CKV_AWS_131 694 | comment: "ALB uses HTTP for internal AWS traffic. Security handled by CloudFront HTTPS and origin verification. Consider end-to-end HTTPS if required." 695 | albtargetgroupcodeserver: 696 | Type: AWS::ElasticLoadBalancingV2::TargetGroup 697 | Properties: 698 | HealthCheckPath: /login 699 | HealthCheckPort: 8080 700 | Name: !Sub ${PrefixCode}-targetgroup-codeserver 701 | Port: 8080 702 | Protocol: HTTP 703 | TargetType: instance 704 | Targets: 705 | - Id: !Ref ec2codeserver 706 | VpcId: !Ref vpc01 707 | Tags: 708 | - Key: Name 709 | Value: !Sub ${PrefixCode}-targetgroup-codeserver 710 | - Key: provisioner 711 | Value: CFN 712 | - Key: solution 713 | Value: !Sub ${SolutionTag} 714 | - Key: resourcetype 715 | Value: network 716 | - Key: environment 717 | Value: !Sub ${EnvironmentTag} 718 | # ALB listener default rule blocks all traffic unless coming from CloudFront with correct secret header 719 | alblistenercodeserver: 720 | Type: AWS::ElasticLoadBalancingV2::Listener 721 | Properties: 722 | DefaultActions: 723 | - Type: fixed-response 724 | FixedResponseConfig: 725 | ContentType: text/plain 726 | MessageBody: "Access Denied" 727 | StatusCode: 403 728 | LoadBalancerArn: !Ref albcodeserver 729 | Port: 80 730 | Protocol: HTTP 731 | Metadata: 732 | cfn_nag: 733 | rules_to_suppress: 734 | - id: W56 735 | reason: "ALB uses HTTP for internal AWS traffic. Security handled by CloudFront HTTPS and origin verification. Consider end-to-end HTTPS if required." 736 | - id: CKV_AWS_103 737 | reason: "TLS check not applicable - ALB uses HTTP internally. External TLS 1.2 enforced at CloudFront. Consider end-to-end HTTPS if required." 738 | checkov: 739 | skip: 740 | - id: CKV_AWS_2 741 | comment: "ALB uses HTTP for internal AWS traffic. Security handled by CloudFront HTTPS and origin verification. Consider end-to-end HTTPS if required." 742 | - id: CKV_AWS_103 743 | comment: "TLS check not applicable - ALB uses HTTP internally. External TLS 1.2 enforced at CloudFront. Consider end-to-end HTTPS if required." 744 | # Allow traffic only if secret header from CloudFront is present 745 | alblistenerrulecodeserver: 746 | Type: AWS::ElasticLoadBalancingV2::ListenerRule 747 | Properties: 748 | Actions: 749 | - Type: forward 750 | TargetGroupArn: !Ref albtargetgroupcodeserver 751 | Conditions: 752 | - Field: http-header 753 | HttpHeaderConfig: 754 | HttpHeaderName: !Sub "{{resolve:secretsmanager:${cloudfrontsecretheadercodeserver}:SecretString:HeaderName}}" 755 | Values: 756 | - !Sub "{{resolve:secretsmanager:${cloudfrontsecretheadercodeserver}:SecretString:HeaderValue}}" 757 | ListenerArn: !Ref alblistenercodeserver 758 | Priority: 10 759 | # Security group allows traffic only from CloudFront IP ranges 760 | securitygroupalb: 761 | Type: AWS::EC2::SecurityGroup 762 | Properties: 763 | GroupName: !Sub ${PrefixCode}-securitygroup-alb 764 | GroupDescription: ALB security group 765 | VpcId: !Ref vpc01 766 | SecurityGroupIngress: 767 | - Description: HTTP inbound from CloudFront 768 | IpProtocol: tcp 769 | FromPort: 80 770 | ToPort: 80 771 | SourcePrefixListId: !FindInMap [RegionMap, !Ref "AWS::Region", CloudFrontPrefixList] 772 | SecurityGroupEgress: 773 | - Description: Allow to EC2 774 | IpProtocol: tcp 775 | FromPort: 8080 776 | ToPort: 8080 777 | DestinationSecurityGroupId: !Ref securitygroupcodeserver 778 | Tags: 779 | - Key: Name 780 | Value: !Sub ${PrefixCode}-securitygroup-alb 781 | - Key: provisioner 782 | Value: CFN 783 | - Key: solution 784 | Value: !Sub ${SolutionTag} 785 | - Key: resourcetype 786 | Value: security 787 | - Key: environment 788 | Value: !Sub ${EnvironmentTag} 789 | cloudfrontcachepolcodeserver: 790 | Type: AWS::CloudFront::CachePolicy 791 | Properties: 792 | CachePolicyConfig: 793 | Comment: Cache policy for VS code-server allows caching with cookie/header/query string forwarding 794 | DefaultTTL: 86400 795 | MaxTTL: 31536000 796 | MinTTL: 1 797 | Name: !Sub ${PrefixCode}-cloudfront-policy 798 | ParametersInCacheKeyAndForwardedToOrigin: 799 | CookiesConfig: 800 | CookieBehavior: all 801 | EnableAcceptEncodingGzip: true 802 | HeadersConfig: 803 | HeaderBehavior: whitelist 804 | Headers: 805 | - Host 806 | - Origin 807 | - Authorization 808 | QueryStringsConfig: 809 | QueryStringBehavior: all 810 | # CloudFront distribution adds secret header to all requests to ALB 811 | cloudfrontdistributioncodeserver: 812 | Type: AWS::CloudFront::Distribution 813 | Properties: 814 | DistributionConfig: 815 | Enabled: true 816 | Origins: 817 | - DomainName: !GetAtt albcodeserver.DNSName 818 | Id: CodeServerOrigin 819 | CustomOriginConfig: 820 | OriginProtocolPolicy: http-only 821 | HTTPPort: 80 822 | # Add secret header to all requests to prevent direct ALB access 823 | OriginCustomHeaders: 824 | - HeaderName: !Sub "{{resolve:secretsmanager:${cloudfrontsecretheadercodeserver}:SecretString:HeaderName}}" 825 | HeaderValue: !Sub "{{resolve:secretsmanager:${cloudfrontsecretheadercodeserver}:SecretString:HeaderValue}}" 826 | DefaultCacheBehavior: 827 | AllowedMethods: 828 | - GET 829 | - HEAD 830 | - OPTIONS 831 | - PUT 832 | - PATCH 833 | - POST 834 | - DELETE 835 | CachePolicyId: !Ref cloudfrontcachepolcodeserver 836 | OriginRequestPolicyId: 216adef6-5c7f-47e4-b989-5492eafa07d3 # Managed AllViewer Policy 837 | TargetOriginId: CodeServerOrigin 838 | ViewerProtocolPolicy: redirect-to-https 839 | Logging: 840 | Bucket: !GetAtt s3bucketlogs.DomainName 841 | IncludeCookies: false 842 | Prefix: cloudfront-logs/ 843 | Tags: 844 | - Key: Name 845 | Value: !Sub ${PrefixCode}-cloudfrontdistribution-alb 846 | - Key: provisioner 847 | Value: CFN 848 | - Key: solution 849 | Value: !Sub ${SolutionTag} 850 | - Key: resourcetype 851 | Value: network 852 | - Key: environment 853 | Value: !Sub ${EnvironmentTag} 854 | Metadata: 855 | cdk_nag: 856 | rules_to_suppress: 857 | - id: AwsSolutions-CFR4 858 | reason: "CloudFront uses HTTP internally with ALB. Security handled by CloudFront HTTPS and origin verification. Consider end-to-end HTTPS if required." 859 | - id: AwsSolutions-CFR5 860 | reason: "Origin uses HTTP within AWS network. Consider end-to-end HTTPS if certificate management overhead is acceptable." 861 | - id: CKV_AWS_174 862 | reason: "Using CloudFront default certificate with TLS 1.2. Consider custom certificate if custom domain required." 863 | - id: AwsSolutions-CFR2 864 | reason: "Security handled by CloudFront origin header, IP restrictions and ALB rules. WAF needs us-east-1 deployment. Consider using StackSets for WAF if required" 865 | - id: CKV_AWS_68 866 | reason: "Security handled by CloudFront origin header, IP restrictions and ALB rules. WAF needs us-east-1 deployment. Consider using StackSets for WAF if required" 867 | - id: AwsSolutions-CFR1 868 | reason: "Geo-restrictions not implemented for flexibility. Consider enabling geo-restrictions if regional compliance or security requirements dictate." 869 | checkov: 870 | skip: 871 | - id: CKV_AWS_174 872 | comment: "Using CloudFront default certificate with TLS 1.2. Consider custom certificate if custom domain required." 873 | - id: CKV_AWS_68 874 | comment: "WAF not implemented for development environment. Security handled through CloudFront origin verification and code-server authentication. Consider WAF for production." 875 | cloudfrontsecretheadercodeserver: 876 | Type: AWS::SecretsManager::Secret 877 | Properties: 878 | Name: !Sub ${PrefixCode}-secret-cloudfrontheader 879 | Description: Origin verification header for code-server CloudFront distribution 880 | KmsKeyId: !GetAtt kmskey.Arn 881 | GenerateSecretString: 882 | SecretStringTemplate: '{"HeaderName": "X-Origin-Verify"}' 883 | GenerateStringKey: "HeaderValue" 884 | PasswordLength: 32 885 | ExcludeCharacters: '\[]{}>|*&!%#`@,."$:+=-~^()''' 886 | Tags: 887 | - Key: Name 888 | Value: !Sub ${PrefixCode}-secret-cloudfrontheader 889 | - Key: provisioner 890 | Value: CFN 891 | - Key: solution 892 | Value: !Sub ${SolutionTag} 893 | - Key: resourcetype 894 | Value: security 895 | - Key: environment 896 | Value: !Sub ${EnvironmentTag} 897 | Metadata: 898 | cfn_nag: 899 | rules_to_suppress: 900 | - id: W77 901 | reason: "Secret is a static verification token between CloudFront and ALB. Rotation would require coordinated updates to both services causing downtime. Consider automated rotation if required." 902 | - id: AwsSolutions-SMG4 903 | reason: "Secret is a static verification token between CloudFront and ALB. Rotation would require coordinated updates to both services causing downtime. Consider automated rotation if required." 904 | cdk_nag: 905 | rules_to_suppress: 906 | - id: AwsSolutions-SMG4 907 | reason: "Secret is a static verification token between CloudFront and ALB. Rotation would require coordinated updates to both services causing downtime. Consider automated rotation if required." 908 | securitygroupcodeserver: 909 | Type: AWS::EC2::SecurityGroup 910 | Properties: 911 | GroupName: !Sub ${PrefixCode}-securitygroup-codeserver 912 | GroupDescription: code-server security group 913 | VpcId: !Ref vpc01 914 | Tags: 915 | - Key: Name 916 | Value: !Sub ${PrefixCode}-securitygroup-codeserver 917 | - Key: provisioner 918 | Value: CFN 919 | - Key: solution 920 | Value: !Sub ${SolutionTag} 921 | - Key: resourcetype 922 | Value: security 923 | - Key: environment 924 | Value: !Sub ${EnvironmentTag} 925 | securitygroupingresscodeserver: 926 | Type: AWS::EC2::SecurityGroupIngress 927 | Properties: 928 | GroupId: !Ref securitygroupcodeserver 929 | IpProtocol: tcp 930 | FromPort: 8080 931 | ToPort: 8080 932 | SourceSecurityGroupId: !Ref securitygroupalb 933 | Description: Allow inbound from ALB 934 | 935 | ### Authentication - Secrets Manager storing credentials for code-server access and CloudFront origin verification 936 | secretcodeserver: 937 | Type: AWS::SecretsManager::Secret 938 | Properties: 939 | Name: !Sub ${PrefixCode}-secret-codeserver 940 | Description: Initial code-server password. If rotation enabled, refer to the rotating secret after 30 days. 941 | KmsKeyId: !GetAtt kmskey.Arn 942 | GenerateSecretString: 943 | SecretStringTemplate: '{"username": "admin"}' 944 | GenerateStringKey: "password" 945 | PasswordLength: 16 946 | ExcludeCharacters: '\[]{}>|*&!%#`@,."$:+=-~^()''' 947 | RequireEachIncludedType: true 948 | Tags: 949 | - Key: Name 950 | Value: !Sub ${PrefixCode}-secret-codeserver 951 | - Key: provisioner 952 | Value: CFN 953 | - Key: solution 954 | Value: !Sub ${SolutionTag} 955 | - Key: resourcetype 956 | Value: security 957 | - Key: environment 958 | Value: !Sub ${EnvironmentTag} 959 | Metadata: 960 | cfn_nag: 961 | rules_to_suppress: 962 | - id: AwsSolutions-SMG4 963 | reason: "This is the initial deployment secret only. For automated rotation, enable the RotateSecret parameter which creates and uses secretcodeserverrotating instead." 964 | cdk_nag: 965 | rules_to_suppress: 966 | - id: AwsSolutions-SMG4 967 | reason: "This is the initial deployment secret only. For automated rotation, enable the RotateSecret parameter which creates and uses secretcodeserverrotating instead." 968 | 969 | ### Source Control Storage - S3 bucket configured as a git remote for version control, acting as a serverless git repository with encryption and access controls 970 | s3bucketgit: 971 | Type: AWS::S3::Bucket 972 | DeletionPolicy: Delete 973 | Properties: 974 | BucketEncryption: 975 | ServerSideEncryptionConfiguration: 976 | - ServerSideEncryptionByDefault: 977 | KMSMasterKeyID: !Ref kmskeyalias 978 | SSEAlgorithm: aws:kms 979 | PublicAccessBlockConfiguration: 980 | BlockPublicAcls: true 981 | BlockPublicPolicy: true 982 | IgnorePublicAcls: true 983 | RestrictPublicBuckets: true 984 | VersioningConfiguration: 985 | Status: Enabled 986 | LoggingConfiguration: 987 | DestinationBucketName: !Ref s3bucketlogs 988 | LogFilePrefix: s3-logs/git/ 989 | # Required for pipeline build 990 | NotificationConfiguration: 991 | EventBridgeConfiguration: 992 | EventBridgeEnabled: true 993 | Tags: 994 | - Key: provisioner 995 | Value: CFN 996 | - Key: solution 997 | Value: !Sub ${SolutionTag} 998 | - Key: resourcetype 999 | Value: storage 1000 | - Key: environment 1001 | Value: !Sub ${EnvironmentTag} 1002 | s3bucketpolicygit: 1003 | Type: AWS::S3::BucketPolicy 1004 | Properties: 1005 | Bucket: !Ref s3bucketgit 1006 | PolicyDocument: 1007 | Version: 2012-10-17 1008 | Statement: 1009 | - Sid: EnforceSecureTransport 1010 | Effect: Deny 1011 | Principal: "*" 1012 | Action: s3:* 1013 | Resource: 1014 | - !Sub arn:${AWS::Partition}:s3:::${s3bucketgit} 1015 | - !Sub arn:${AWS::Partition}:s3:::${s3bucketgit}/* 1016 | Condition: 1017 | Bool: 1018 | aws:SecureTransport: false 1019 | NumericLessThan: 1020 | s3:TlsVersion: 1.2 1021 | 1022 | ### Development Environment - EC2 instance running code-server with developer tools and git integration 1023 | iamrolecodeserver: 1024 | Type: AWS::IAM::Role 1025 | Properties: 1026 | RoleName: !Sub ${PrefixCode}-iamrole-codeserver 1027 | Description: Code-server EC2 Instance Profile 1028 | AssumeRolePolicyDocument: 1029 | Version: 2012-10-17 1030 | Statement: 1031 | - Effect: Allow 1032 | Principal: 1033 | Service: ec2.amazonaws.com 1034 | Action: sts:AssumeRole 1035 | Policies: 1036 | - PolicyName: !Sub ${PrefixCode}-iampolicy-EC2 1037 | PolicyDocument: 1038 | Version: 2012-10-17 1039 | Statement: 1040 | - Effect: Allow 1041 | Action: 1042 | - ec2:DescribeInstances 1043 | - ec2:DescribeTags 1044 | Resource: "*" 1045 | Condition: 1046 | StringEquals: 1047 | aws:SourceAccount: !Ref AWS::AccountId 1048 | # https://docs.aws.amazon.com/systems-manager/latest/userguide/security_iam_service-with-iam.html 1049 | - PolicyName: !Sub ${PrefixCode}-iampolicy-codeserver-SystemsManager 1050 | PolicyDocument: 1051 | Version: 2012-10-17 1052 | Statement: 1053 | - Effect: Allow 1054 | Action: 1055 | - ssm:StartSession 1056 | Resource: 1057 | - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:* 1058 | - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}::document/AWS-StartPortForwardingSession 1059 | - PolicyName: !Sub ${PrefixCode}-iampolicy-codeserver-CloudWatch 1060 | PolicyDocument: 1061 | Version: 2012-10-17 1062 | Statement: 1063 | - Effect: Allow 1064 | Action: 1065 | - logs:CreateLogStream 1066 | - logs:PutLogEvents 1067 | - logs:DescribeLogStreams 1068 | Resource: 1069 | - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/code-server/${PrefixCode} 1070 | - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/code-server/${PrefixCode}:* 1071 | - PolicyName: !Sub ${PrefixCode}-iampolicy-codeserver-KMS 1072 | PolicyDocument: 1073 | Version: 2012-10-17 1074 | Statement: 1075 | - Effect: Allow 1076 | Action: 1077 | - kms:Decrypt 1078 | - kms:DescribeKey 1079 | - kms:GenerateDataKey 1080 | Resource: 1081 | - !GetAtt kmskey.Arn 1082 | - PolicyName: !Sub ${PrefixCode}-iampolicy-codeserver-secretsmanager 1083 | PolicyDocument: 1084 | Version: 2012-10-17 1085 | Statement: 1086 | - Effect: Allow 1087 | Action: 1088 | - secretsmanager:GetSecretValue 1089 | - secretsmanager:DescribeSecret 1090 | Resource: 1091 | - !Ref secretcodeserver 1092 | - PolicyName: !Sub ${PrefixCode}-iampolicy-codeserver-S3 1093 | PolicyDocument: 1094 | Version: 2012-10-17 1095 | Statement: 1096 | - Effect: Allow 1097 | Action: 1098 | - s3:PutObject 1099 | - s3:GetObject 1100 | - s3:DeleteObject 1101 | Resource: !Sub arn:${AWS::Partition}:s3:::${s3bucketgit}/* 1102 | - Effect: Allow 1103 | Action: s3:ListBucket 1104 | Resource: !Sub arn:${AWS::Partition}:s3:::${s3bucketgit} 1105 | - PolicyName: !Sub ${PrefixCode}-iampolicy-codeserver-SSM 1106 | PolicyDocument: 1107 | Version: 2012-10-17 1108 | Statement: 1109 | - Effect: Allow 1110 | Action: 1111 | - ssm:GetParameter 1112 | Resource: !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${PrefixCode}/config/AmazonCloudWatch-linux 1113 | Tags: 1114 | - Key: Name 1115 | Value: !Sub ${PrefixCode}-iamrole-codeserver 1116 | - Key: provisioner 1117 | Value: CFN 1118 | - Key: solution 1119 | Value: !Sub ${SolutionTag} 1120 | - Key: resourcetype 1121 | Value: security 1122 | - Key: environment 1123 | Value: !Sub ${EnvironmentTag} 1124 | Metadata: 1125 | cdk_nag: 1126 | rules_to_suppress: 1127 | - id: AwsSolutions-IAM5 1128 | reason: "EC2 instance requires describe permissions. Access limited by SourceAccount condition, specific actions, and scoped resources for S3, KMS, and Secrets Manager." 1129 | iaminstanceprofilecodeserver: 1130 | Type: AWS::IAM::InstanceProfile 1131 | Properties: 1132 | InstanceProfileName: !Sub ${PrefixCode}-iamprofile-ec2admin 1133 | Roles: 1134 | - !Ref iamrolecodeserver 1135 | # EC2 keypair is automatically stored in AWS Systems Manager Parameter Store 1136 | ec2keypaircodeserver: 1137 | Type: AWS::EC2::KeyPair 1138 | Properties: 1139 | KeyName: !Sub ${PrefixCode}-ec2-keypair 1140 | Tags: 1141 | - Key: Name 1142 | Value: !Sub ${PrefixCode}-ec2-keypair 1143 | - Key: provisioner 1144 | Value: CFN 1145 | - Key: solution 1146 | Value: !Sub ${SolutionTag} 1147 | - Key: resourcetype 1148 | Value: security 1149 | - Key: environment 1150 | Value: !Sub ${EnvironmentTag} 1151 | ec2loggroup: 1152 | Type: AWS::Logs::LogGroup 1153 | Properties: 1154 | LogGroupName: !Sub /aws/code-server/${PrefixCode} 1155 | RetentionInDays: 14 1156 | Tags: 1157 | - Key: Name 1158 | Value: !Sub ${PrefixCode}-ec2loggroup 1159 | - Key: provisioner 1160 | Value: CFN 1161 | - Key: solution 1162 | Value: !Sub ${SolutionTag} 1163 | - Key: resourcetype 1164 | Value: monitoring 1165 | - Key: environment 1166 | Value: !Sub ${EnvironmentTag} 1167 | ssmparametercloudwatchlinux: 1168 | Type: AWS::SSM::Parameter 1169 | Properties: 1170 | Name: !Sub /${PrefixCode}/config/AmazonCloudWatch-linux 1171 | Type: String 1172 | Value: !Sub | 1173 | { 1174 | "logs": { 1175 | "logs_collected": { 1176 | "files": { 1177 | "collect_list": [ 1178 | { 1179 | "file_path": "/var/log/cloud-init-output.log", 1180 | "log_group_name": "/aws/code-server/${PrefixCode}", 1181 | "log_stream_name": "setup" 1182 | } 1183 | ] 1184 | } 1185 | } 1186 | } 1187 | } 1188 | Description: CloudWatch Agent configuration for linux instances 1189 | Tags: 1190 | Name: !Sub ${PrefixCode}-ssm-cloudwatch-linux 1191 | provisioner: CFN 1192 | solution: !Sub ${SolutionTag} 1193 | resourcetype: monitoring 1194 | environment: !Sub ${EnvironmentTag} 1195 | ec2codeserver: 1196 | Type: AWS::EC2::Instance 1197 | Properties: 1198 | ImageId: !If [IsArm64, !Ref AMIArmCodeServer, !Ref AMIx86CodeServer] 1199 | InstanceType: !Ref InstanceType 1200 | KeyName: !Ref ec2keypaircodeserver 1201 | SubnetId: !Ref subnetprivate01 1202 | IamInstanceProfile: !Ref iaminstanceprofilecodeserver 1203 | Monitoring: true 1204 | PrivateIpAddress: !Sub ${VpcCidr}.3.100 1205 | BlockDeviceMappings: 1206 | - DeviceName: /dev/xvda 1207 | Ebs: 1208 | # Consider setting DeleteOnTermination to false in your own environments 1209 | DeleteOnTermination: true 1210 | Encrypted: true 1211 | KmsKeyId: alias/aws/ebs 1212 | VolumeSize: 100 1213 | VolumeType: gp3 1214 | Iops: 3000 1215 | SecurityGroupIds: 1216 | - !Ref securitygroupcodeserver 1217 | UserData: 1218 | Fn::Base64: !Sub | 1219 | #!/bin/bash 1220 | 1221 | echo "INFO: Waiting for SSM agent initialization..." 1222 | until systemctl is-active --quiet amazon-ssm-agent; do 1223 | echo "INFO: SSM agent is not ready..." 1224 | systemctl status amazon-ssm-agent 1225 | sleep 5 1226 | done 1227 | echo "INFO: Installing packages including CloudWatch agent" 1228 | dnf update -y -q 1229 | dnf install amazon-cloudwatch-agent -y -q 1230 | 1231 | echo "INFO: Configuring CloudWatch agent..." 1232 | until aws ssm get-parameter --name /${PrefixCode}/config/AmazonCloudWatch-linux --region ${AWS::Region} &> /dev/null; do 1233 | echo "INFO: SSM parameter is not ready..." 1234 | sleep 5 1235 | done 1236 | aws ssm get-parameter --name /${PrefixCode}/config/AmazonCloudWatch-linux --region ${AWS::Region} --query "Parameter.Value" --output text > /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json 1237 | /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -c file:/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json -s 1238 | 1239 | echo "INFO: Creating AWS profile for developer role..." 1240 | su - ec2-user -c "mkdir -p ~/.aws" 1241 | su - ec2-user -c "cat > ~/.aws/config << EOF 1242 | [profile developer] 1243 | role_arn = arn:aws:iam::${AWS::AccountId}:role/${PrefixCode}-iamrole-developer 1244 | credential_source = Ec2InstanceMetadata 1245 | region = ${AWS::Region} 1246 | EOF" 1247 | 1248 | #### START: CODE-SERVER BOOTSTRAP #### 1249 | echo "INFO: Installing code-server version ${CodeServerVersion}..." 1250 | dnf install https://github.com/coder/code-server/releases/download/v${CodeServerVersion}/code-server-${CodeServerVersion}-${InstanceArchitecture}.rpm -y 1251 | 1252 | echo "INFO: Configuring code-server" 1253 | mkdir -p /home/ec2-user/.config/code-server 1254 | mkdir -p /home/ec2-user/workspace 1255 | chown -R ec2-user:ec2-user /home/ec2-user/workspace 1256 | 1257 | cat > /home/ec2-user/.config/code-server/config.yaml << EOF 1258 | bind-addr: 0.0.0.0:8080 1259 | auth: password 1260 | password: "$(aws secretsmanager get-secret-value --secret-id ${secretcodeserver} --region ${AWS::Region} --query SecretString --output text | jq -r .password)" 1261 | cert: false 1262 | EOF 1263 | 1264 | chmod 600 /home/ec2-user/.config/code-server/config.yaml 1265 | chown -R ec2-user:ec2-user /home/ec2-user/.config 1266 | 1267 | cat > /etc/systemd/system/code-server.service << EOF 1268 | [Unit] 1269 | Description=code-server 1270 | After=network.target 1271 | 1272 | [Service] 1273 | Type=simple 1274 | User=ec2-user 1275 | Environment=HOME=/home/ec2-user 1276 | WorkingDirectory=/home/ec2-user/workspace 1277 | ExecStart=/usr/bin/code-server --config /home/ec2-user/.config/code-server/config.yaml 1278 | Restart=always 1279 | 1280 | [Install] 1281 | WantedBy=multi-user.target 1282 | EOF 1283 | 1284 | echo "INFO: Configuring code-server workspace interface..." 1285 | mkdir -p /home/ec2-user/.local/share/code-server/User/ 1286 | cat >> /home/ec2-user/.local/share/code-server/User/settings.json << EOF 1287 | { 1288 | "git.enabled": true, 1289 | "git.path": "/usr/bin/git", 1290 | "git.autofetch": true, 1291 | "window.menuBarVisibility": "classic", 1292 | "workbench.startupEditor": "none", 1293 | "workspace.openFilesInNewWindow": "off", 1294 | "workbench.colorTheme": "Default Dark+" 1295 | } 1296 | EOF 1297 | chown -R ec2-user:ec2-user /home/ec2-user/.local 1298 | 1299 | echo "INFO: Installing Terraform..." 1300 | dnf config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo 1301 | dnf install -y terraform 1302 | 1303 | echo "INFO: Installing Docker..." 1304 | dnf install -y docker 1305 | systemctl start docker 1306 | systemctl enable docker 1307 | usermod -aG docker ec2-user 1308 | 1309 | echo "INFO: Installing extensions..." 1310 | su - ec2-user -c "code-server --install-extension amazonwebservices.amazon-q-vscode --force" 1311 | su - ec2-user -c "code-server --install-extension amazonwebservices.aws-toolkit-vscode --force" 1312 | su - ec2-user -c "code-server --install-extension hashicorp.terraform --force" 1313 | su - ec2-user -c "code-server --install-extension ms-azuretools.vscode-docker --force" 1314 | 1315 | echo "INFO: Starting code-server service..." 1316 | systemctl enable code-server 1317 | systemctl start code-server 1318 | #### END: CODE-SERVER BOOTSTRAP #### 1319 | 1320 | #### START: GIT-REMOTE-S3 BOOTSTRAP #### 1321 | echo "INFO: Installing git-remote-s3..." 1322 | dnf install git -y -q 1323 | dnf install -y python3 python3-pip 1324 | pip3 install boto3==1.37.18 1325 | pip3 install git-remote-s3 1326 | 1327 | # Initialize git repo as ec2-user 1328 | echo "INFO: Configuring git..." 1329 | su - ec2-user -c "git config --global user.name 'EC2 User'" 1330 | su - ec2-user -c "git config --global user.email 'ec2-user@example.com'" 1331 | su - ec2-user -c "git config --global init.defaultBranch main" 1332 | 1333 | # Initialize from either S3 or GitHub 1334 | if [ -n "${S3AssetBucket}" ]; then 1335 | echo "INFO: Initializing from S3 assets..." 1336 | su - ec2-user -c "mkdir -p /home/ec2-user/workspace/my-workspace && \ 1337 | cd /home/ec2-user/workspace/my-workspace && \ 1338 | aws s3 cp --recursive s3://${S3AssetBucket}/${S3AssetPrefix} ./ && \ 1339 | git init && \ 1340 | git add . && \ 1341 | git commit -m 'Initial commit from S3 assets' && \ 1342 | git remote add origin s3+zip://${s3bucketgit}/my-workspace && \ 1343 | git push -u origin main" 1344 | else 1345 | echo "INFO: Cloning from GitHub..." 1346 | su - ec2-user -c "cd /home/ec2-user/workspace && \ 1347 | git clone ${GitHubRepo} my-workspace && \ 1348 | cd my-workspace && \ 1349 | git remote remove origin && \ 1350 | git remote add origin s3+zip://${s3bucketgit}/my-workspace && \ 1351 | git push -u origin main" 1352 | fi 1353 | #### END: GIT-REMOTE-S3 BOOTSTRAP #### 1354 | 1355 | #### START: SET DEVELOPER PROFILE AS DEFAULT #### 1356 | if [ "${AutoSetDeveloperProfile}" = "true" ]; then 1357 | echo "INFO: Setting up AWS profile defaults..." 1358 | su - ec2-user -c "echo 'export AWS_PROFILE=developer' >> ~/.bashrc" 1359 | su - ec2-user -c "echo 'export AWS_REGION=${AWS::Region}' >> ~/.bashrc" 1360 | su - ec2-user -c "echo 'export AWS_ACCOUNTID=${AWS::AccountId}' >> ~/.bashrc" 1361 | else 1362 | echo "INFO: Skipping AWS profile defaults..." 1363 | fi 1364 | #### END: SET DEVELOPER PROFILE AS DEFAULT #### 1365 | 1366 | Tags: 1367 | - Key: Name 1368 | Value: !Sub ${PrefixCode}-ec2-codeserver 1369 | - Key: provisioner 1370 | Value: CFN 1371 | - Key: solution 1372 | Value: !Sub ${SolutionTag} 1373 | - Key: resourcetype 1374 | Value: compute 1375 | - Key: environment 1376 | Value: !Sub ${EnvironmentTag} 1377 | Metadata: 1378 | cdk_nag: 1379 | rules_to_suppress: 1380 | - id: AwsSolutions-EC29 1381 | reason: "Development instance uses S3 for code and state persistence. Consider ASG implementation if high availability required." 1382 | 1383 | ### IMPORTANT: Developer role with elevated permissions used by both code-server AWS profile and CodeBuild pipeline to deploy sample project [static website hosted on Amazon S3] - review and adjust based on requirements 1384 | iamroledeveloper: 1385 | Type: AWS::IAM::Role 1386 | Properties: 1387 | RoleName: !Sub ${PrefixCode}-iamrole-developer 1388 | Description: Elevated permissions for AWS infrastructure deployment and resource management 1389 | AssumeRolePolicyDocument: 1390 | Version: 2012-10-17 1391 | Statement: 1392 | - Effect: Allow 1393 | Principal: 1394 | Service: codebuild.amazonaws.com 1395 | AWS: !GetAtt iamrolecodeserver.Arn 1396 | Action: sts:AssumeRole 1397 | Policies: 1398 | - PolicyName: !Sub ${PrefixCode}-iampolicy-gitpush-KMS 1399 | PolicyDocument: 1400 | Version: 2012-10-17 1401 | Statement: 1402 | - Effect: Allow 1403 | Action: 1404 | - kms:Decrypt 1405 | - kms:DescribeKey 1406 | - kms:GenerateDataKey 1407 | Resource: 1408 | - !GetAtt kmskey.Arn 1409 | - PolicyName: !Sub ${PrefixCode}-iampolicy-terraform-kms-key 1410 | PolicyDocument: 1411 | Version: 2012-10-17 1412 | Statement: 1413 | - Effect: Allow 1414 | Action: 1415 | - kms:CreateKey 1416 | - kms:ListAliases 1417 | Resource: "*" 1418 | - Effect: Allow 1419 | Action: 1420 | - kms:CreateGrant 1421 | - kms:Decrypt 1422 | - kms:DescribeKey 1423 | - kms:EnableKeyRotation 1424 | - kms:GetKeyPolicy 1425 | - kms:GetKeyRotationStatus 1426 | - kms:ListResourceTags 1427 | - kms:PutKeyPolicy 1428 | - kms:RetireGrant 1429 | - kms:ScheduleKeyDeletion 1430 | - kms:TagResource 1431 | Resource: !Sub arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:key/* 1432 | - Effect: Allow 1433 | Action: 1434 | - kms:CreateAlias 1435 | - kms:DeleteAlias 1436 | Resource: 1437 | - !Sub arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:alias/* 1438 | - !Sub arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:key/* 1439 | - PolicyName: !Sub ${PrefixCode}-iampolicy-terraform-s3 1440 | PolicyDocument: 1441 | Version: 2012-10-17 1442 | Statement: 1443 | - Effect: Allow 1444 | Action: 1445 | - s3:ListAllMyBuckets 1446 | Resource: 1447 | - "*" 1448 | - Effect: Allow 1449 | Action: 1450 | - s3:* 1451 | Resource: 1452 | - !Sub arn:${AWS::Partition}:s3:::${PrefixCode}-* 1453 | - !Sub arn:${AWS::Partition}:s3:::${PrefixCode}-*/* 1454 | - PolicyName: !Sub ${PrefixCode}-iampolicy-terraform-cloudfront 1455 | PolicyDocument: 1456 | Version: 2012-10-17 1457 | Statement: 1458 | - Effect: Allow 1459 | Action: 1460 | - cloudfront:* 1461 | Resource: 1462 | - !Sub arn:${AWS::Partition}:cloudfront::${AWS::AccountId}:distribution/* 1463 | - !Sub arn:${AWS::Partition}:cloudfront::${AWS::AccountId}:origin-access-control/* 1464 | - !Sub arn:${AWS::Partition}:cloudfront::${AWS::AccountId}:response-headers-policy/* 1465 | - PolicyName: !Sub ${PrefixCode}-iampolicy-terraform-waf 1466 | PolicyDocument: 1467 | Version: 2012-10-17 1468 | Statement: 1469 | - Effect: Allow 1470 | Action: 1471 | - wafv2:ListWebACLs 1472 | Resource: "*" 1473 | - Effect: Allow 1474 | Action: 1475 | - wafv2:CreateWebACL 1476 | - wafv2:DeleteWebACL 1477 | - wafv2:GetWebACL 1478 | - wafv2:UpdateWebACL 1479 | - wafv2:ListTagsForResource 1480 | - wafv2:TagResource 1481 | - wafv2:UntagResource 1482 | - wafv2:AssociateWebACL 1483 | - wafv2:DisassociateWebACL 1484 | - wafv2:GetManagedRuleSet 1485 | - wafv2:ListAvailableManagedRuleGroups 1486 | Resource: 1487 | - !Sub arn:${AWS::Partition}:wafv2:us-east-1:${AWS::AccountId}:global/webacl/* 1488 | - !Sub arn:${AWS::Partition}:wafv2:us-east-1:${AWS::AccountId}:global/managedruleset/* 1489 | - !Sub arn:${AWS::Partition}:wafv2:us-east-1:${AWS::AccountId}:global/managedruleset/*/* 1490 | - PolicyName: !Sub ${PrefixCode}-iampolicy-ec2codebuild 1491 | PolicyDocument: 1492 | Version: 2012-10-17 1493 | Statement: 1494 | - Effect: Allow 1495 | Action: 1496 | - ec2:DescribeDhcpOptions 1497 | - ec2:DescribeInstances # test aws cli commands 1498 | - ec2:DescribeNetworkInterfaces 1499 | - ec2:DescribeSubnets 1500 | - ec2:DescribeSecurityGroups 1501 | - ec2:DescribeVpcs 1502 | - ec2:DeleteNetworkInterface 1503 | - ec2:CreateNetworkInterface 1504 | Resource: "*" 1505 | Tags: 1506 | - Key: Name 1507 | Value: !Sub ${PrefixCode}-iamrole-developer 1508 | - Key: provisioner 1509 | Value: CFN 1510 | - Key: solution 1511 | Value: !Sub ${SolutionTag} 1512 | - Key: resourcetype 1513 | Value: security 1514 | - Key: environment 1515 | Value: !Sub ${EnvironmentTag} 1516 | Metadata: 1517 | cdk_nag: 1518 | rules_to_suppress: 1519 | - id: AwsSolutions-IAM5 1520 | reason: "Role scoped to prefix-matched S3 buckets and account-specific resources. Wildcard required for EC2/CodeBuild networking. Consider further restrictions if required." 1521 | cfn_nag: 1522 | rules_to_suppress: 1523 | - id: F3 1524 | reason: "Developer role requires broad permissions for infrastructure deployment. Scoped to specific resources and account. Consider further restrictions if required." 1525 | # Policy allowing code-server user to assume the developer role 1526 | iamrolepolicyassumedeveloper: 1527 | Type: AWS::IAM::Policy 1528 | Properties: 1529 | PolicyName: !Sub ${PrefixCode}-iampolicy-assume-Developer 1530 | PolicyDocument: 1531 | Version: 2012-10-17 1532 | Statement: 1533 | - Effect: Allow 1534 | Action: sts:AssumeRole 1535 | Resource: !GetAtt iamroledeveloper.Arn 1536 | Roles: 1537 | - !Ref iamrolecodeserver 1538 | # Policy allowing code-server to copy files from source S3 bucket for workspace initialization 1539 | iampolicyS3Assets: 1540 | Type: AWS::IAM::RolePolicy 1541 | Condition: HasS3Assets 1542 | Properties: 1543 | RoleName: !Ref iamrolecodeserver 1544 | PolicyName: !Sub ${PrefixCode}-iampolicy-s3-assets 1545 | PolicyDocument: 1546 | Version: 2012-10-17 1547 | Statement: 1548 | - Effect: Allow 1549 | Action: s3:ListAllMyBuckets 1550 | Resource: "*" 1551 | - Effect: Allow 1552 | Action: 1553 | - s3:ListBucket 1554 | - s3:GetObject 1555 | Resource: 1556 | - !Sub arn:${AWS::Partition}:s3:::${S3AssetBucket} 1557 | - !Sub arn:${AWS::Partition}:s3:::${S3AssetBucket}/${S3AssetPrefix}* 1558 | 1559 | ### CI/CD - CodeBuild and CodePipeline for automated infrastructure deployment via Terraform 1560 | # S3 bucket for codepipeline artifacts AND TERRAFORM STATE! 1561 | s3bucketartifact: 1562 | Type: AWS::S3::Bucket 1563 | Condition: CreatePipeline 1564 | DeletionPolicy: Delete 1565 | Properties: 1566 | BucketEncryption: 1567 | ServerSideEncryptionConfiguration: 1568 | - ServerSideEncryptionByDefault: 1569 | KMSMasterKeyID: !Ref kmskeyalias 1570 | SSEAlgorithm: aws:kms 1571 | PublicAccessBlockConfiguration: 1572 | BlockPublicAcls: true 1573 | BlockPublicPolicy: true 1574 | IgnorePublicAcls: true 1575 | RestrictPublicBuckets: true 1576 | LoggingConfiguration: 1577 | DestinationBucketName: !Ref s3bucketlogs 1578 | LogFilePrefix: s3-logs/pipeline/ 1579 | VersioningConfiguration: 1580 | Status: Enabled 1581 | Tags: 1582 | - Key: provisioner 1583 | Value: CFN 1584 | - Key: solution 1585 | Value: !Sub ${SolutionTag} 1586 | - Key: resourcetype 1587 | Value: storage 1588 | - Key: environment 1589 | Value: !Sub ${EnvironmentTag} 1590 | s3bucketpolicyartifact: 1591 | Type: AWS::S3::BucketPolicy 1592 | Condition: CreatePipeline 1593 | Properties: 1594 | Bucket: !Ref s3bucketartifact 1595 | PolicyDocument: 1596 | Version: 2012-10-17 1597 | Statement: 1598 | - Sid: EnforceSecureTransport 1599 | Effect: Deny 1600 | Principal: "*" 1601 | Action: s3:* 1602 | Resource: 1603 | - !Sub arn:${AWS::Partition}:s3:::${s3bucketartifact} 1604 | - !Sub arn:${AWS::Partition}:s3:::${s3bucketartifact}/* 1605 | Condition: 1606 | Bool: 1607 | aws:SecureTransport: false 1608 | NumericLessThan: 1609 | s3:TlsVersion: 1.2 1610 | # Pipeline-specific permissions for iamroledeveloper conditional on DeployPipeline 1611 | iamroledeveloperpipeline: 1612 | Type: AWS::IAM::RolePolicy 1613 | Condition: CreatePipeline 1614 | Properties: 1615 | RoleName: !Ref iamroledeveloper 1616 | PolicyName: !Sub ${PrefixCode}-iampolicy-pipeline 1617 | PolicyDocument: 1618 | Version: 2012-10-17 1619 | Statement: 1620 | - Effect: Allow 1621 | Action: 1622 | - logs:CreateLogGroup 1623 | - logs:CreateLogStream 1624 | - logs:PutLogEvents 1625 | Resource: 1626 | - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/* 1627 | - Effect: Allow 1628 | Action: 1629 | - s3:GetObject 1630 | - s3:GetObjectVersion 1631 | - s3:PutObject 1632 | - s3:DeleteObject 1633 | - s3:ListBucket 1634 | - s3:GetBucketVersioning 1635 | - s3:PutObjectVersionAcl 1636 | - s3:GetObjectVersionAcl 1637 | Resource: 1638 | - !Sub arn:${AWS::Partition}:s3:::${s3bucketartifact} 1639 | - !Sub arn:${AWS::Partition}:s3:::${s3bucketartifact}/* 1640 | - Effect: Allow 1641 | Action: ec2:CreateNetworkInterfacePermission 1642 | Resource: !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:network-interface/* 1643 | Condition: 1644 | StringEquals: 1645 | ec2:AuthorizedService: codebuild.amazonaws.com 1646 | ArnEquals: 1647 | ec2:Subnet: 1648 | - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:subnet/${subnetprivate01} 1649 | # Terraform apply CodeBuild Project 1650 | codebuildprojectbuild: 1651 | Type: AWS::CodeBuild::Project 1652 | Condition: CreatePipeline 1653 | Properties: 1654 | Name: !Sub ${PrefixCode}-codebuildproject-terraform-build 1655 | Description: Deploy Terraform-managed resources (automatically triggered) 1656 | EncryptionKey: !GetAtt kmskey.Arn 1657 | Artifacts: 1658 | Type: NO_ARTIFACTS 1659 | Environment: 1660 | ComputeType: BUILD_GENERAL1_SMALL 1661 | Image: aws/codebuild/amazonlinux-x86_64-standard:5.0 1662 | PrivilegedMode: false 1663 | Type: LINUX_CONTAINER 1664 | ServiceRole: !GetAtt iamroledeveloper.Arn 1665 | Source: 1666 | Type: NO_SOURCE 1667 | BuildSpec: !Sub | 1668 | version: 0.2 1669 | phases: 1670 | install: 1671 | commands: 1672 | # Install Terraform latest version 1673 | - yum install -y yum-utils 1674 | - yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo 1675 | - yum -y install terraform 1676 | - terraform --version 1677 | pre_build: 1678 | commands: 1679 | # Initialize Terraform using shared pipeline/state bucket 1680 | - cd release 1681 | - terraform init -backend-config="bucket=${s3bucketartifact}" -backend-config="region=${AWS::Region}" 1682 | build: 1683 | commands: 1684 | # Pass region and prefix to Terraform to ensure created resources match IAM permissions and naming convention 1685 | - terraform plan -var="PrefixCode=${PrefixCode}" -var="Region=${AWS::Region}" 1686 | - terraform apply -auto-approve -var="PrefixCode=${PrefixCode}" -var="Region=${AWS::Region}" 1687 | post_build: 1688 | commands: 1689 | - echo "Terraform apply completed on $(date)" 1690 | - echo "Website URL -> https://$(terraform output -raw website_url)" 1691 | - echo "WEBSITE_URL=https://$(terraform output -raw website_url)" >> $CODEBUILD_SRC_DIR/variables.env 1692 | exported-variables: 1693 | - WEBSITE_URL 1694 | TimeoutInMinutes: 15 1695 | VpcConfig: 1696 | VpcId: !Ref vpc01 1697 | Subnets: 1698 | - !Ref subnetprivate01 1699 | SecurityGroupIds: 1700 | - !Ref securitygroupcodeserver 1701 | Tags: 1702 | - Key: Name 1703 | Value: !Sub ${PrefixCode}-codebuildproject-terraform-build 1704 | - Key: provisioner 1705 | Value: CFN 1706 | - Key: solution 1707 | Value: !Sub ${SolutionTag} 1708 | - Key: resourcetype 1709 | Value: devops 1710 | - Key: environment 1711 | Value: !Sub ${EnvironmentTag} 1712 | # Terraform destroy CodeBuild Project 1713 | codebuildprojectdestroy: 1714 | Type: AWS::CodeBuild::Project 1715 | Condition: CreatePipeline 1716 | Properties: 1717 | Name: !Sub ${PrefixCode}-codebuildproject-terraform-destroy 1718 | Description: Destroys Terraform-managed resources (manual trigger only) 1719 | EncryptionKey: !GetAtt kmskey.Arn 1720 | Artifacts: 1721 | Type: NO_ARTIFACTS 1722 | Environment: 1723 | ComputeType: BUILD_GENERAL1_SMALL 1724 | Image: aws/codebuild/amazonlinux-x86_64-standard:5.0 1725 | PrivilegedMode: false 1726 | Type: LINUX_CONTAINER 1727 | ServiceRole: !GetAtt iamroledeveloper.Arn 1728 | Source: 1729 | Type: NO_SOURCE 1730 | BuildSpec: !Sub | 1731 | version: 0.2 1732 | phases: 1733 | install: 1734 | commands: 1735 | # Install Terraform latest version 1736 | - yum install -y yum-utils 1737 | - yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo 1738 | - yum -y install terraform 1739 | - terraform --version 1740 | pre_build: 1741 | commands: 1742 | # Initialize Terraform using shared pipeline/state bucket 1743 | - cd release 1744 | - terraform init -backend-config="bucket=${s3bucketartifact}" -backend-config="region=${AWS::Region}" 1745 | build: 1746 | commands: 1747 | # Pass same variables as deploy 1748 | - terraform destroy -auto-approve -var="PrefixCode=${PrefixCode}" -var="Region=${AWS::Region}" 1749 | post_build: 1750 | commands: 1751 | - echo "Terraform destroy completed on $(date)" 1752 | TimeoutInMinutes: 15 1753 | VpcConfig: 1754 | VpcId: !Ref vpc01 1755 | Subnets: 1756 | - !Ref subnetprivate01 1757 | SecurityGroupIds: 1758 | - !Ref securitygroupcodeserver 1759 | Tags: 1760 | - Key: Name 1761 | Value: !Sub ${PrefixCode}-codebuildproject-terraform-destroy 1762 | - Key: provisioner 1763 | Value: CFN 1764 | - Key: solution 1765 | Value: !Sub ${SolutionTag} 1766 | - Key: resourcetype 1767 | Value: devops 1768 | - Key: environment 1769 | Value: !Sub ${EnvironmentTag} 1770 | iamrolepipeline: 1771 | Type: AWS::IAM::Role 1772 | Condition: CreatePipeline 1773 | Properties: 1774 | RoleName: !Sub ${PrefixCode}-iamrole-pipeline 1775 | Description: Code pipeline access to S3 1776 | AssumeRolePolicyDocument: 1777 | Version: 2012-10-17 1778 | Statement: 1779 | - Effect: Allow 1780 | Principal: 1781 | Service: codepipeline.amazonaws.com 1782 | Action: sts:AssumeRole 1783 | Policies: 1784 | - PolicyName: !Sub ${PrefixCode}-pipeline-codebuild 1785 | PolicyDocument: 1786 | Version: 2012-10-17 1787 | Statement: 1788 | - Effect: Allow 1789 | Action: 1790 | - codebuild:BatchGetBuilds 1791 | - codebuild:StartBuild 1792 | - codebuild:StopBuild 1793 | Resource: 1794 | - !GetAtt codebuildprojectbuild.Arn 1795 | - !GetAtt codebuildprojectdestroy.Arn 1796 | - PolicyName: !Sub ${PrefixCode}-pipeline-s3 1797 | PolicyDocument: 1798 | Version: 2012-10-17 1799 | Statement: 1800 | - Effect: Allow 1801 | Action: 1802 | - s3:GetObject 1803 | - s3:GetObjectVersion 1804 | - s3:GetBucketVersioning 1805 | - s3:PutObject 1806 | Resource: 1807 | - !Sub arn:${AWS::Partition}:s3:::${s3bucketgit} 1808 | - !Sub arn:${AWS::Partition}:s3:::${s3bucketgit}/* 1809 | - !Sub arn:${AWS::Partition}:s3:::${s3bucketartifact} 1810 | - !Sub arn:${AWS::Partition}:s3:::${s3bucketartifact}/* 1811 | - Effect: Allow 1812 | Action: 1813 | - s3:ListBucket 1814 | Resource: 1815 | - !Sub arn:${AWS::Partition}:s3:::${s3bucketgit} 1816 | - !Sub arn:${AWS::Partition}:s3:::${s3bucketartifact} 1817 | - PolicyName: !Sub ${PrefixCode}-pipeline-kms 1818 | PolicyDocument: 1819 | Version: 2012-10-17 1820 | Statement: 1821 | - Effect: Allow 1822 | Action: 1823 | - kms:Decrypt 1824 | - kms:DescribeKey 1825 | - kms:Encrypt 1826 | - kms:GenerateDataKey 1827 | - kms:ReEncrypt* 1828 | Resource: !GetAtt kmskey.Arn 1829 | Tags: 1830 | - Key: Name 1831 | Value: !Sub ${PrefixCode}-iamrole-pipeline 1832 | - Key: provisioner 1833 | Value: CFN 1834 | - Key: solution 1835 | Value: !Sub ${SolutionTag} 1836 | - Key: resourcetype 1837 | Value: security 1838 | - Key: environment 1839 | Value: !Sub ${EnvironmentTag} 1840 | Metadata: 1841 | cfn_nag: 1842 | rules_to_suppress: 1843 | - id: AwsSolutions-IAM5 1844 | reason: "Pipeline role policies scoped to specific resources. Wildcards required for S3 object access and KMS operations." 1845 | cdk_nag: 1846 | rules_to_suppress: 1847 | - id: AwsSolutions-IAM5 1848 | appliesTo: 1849 | - "Resource::arn::s3:::/*" 1850 | - "Resource::arn::s3:::/*" 1851 | - "Action::kms:ReEncrypt*" 1852 | reason: "Pipeline role requires S3 object-level and KMS permissions. Access scoped to specific buckets and KMS key." 1853 | # Terraform apply pipeline 1854 | pipelinebuild: 1855 | Type: AWS::CodePipeline::Pipeline 1856 | Condition: CreatePipeline 1857 | Properties: 1858 | Name: !Sub ${PrefixCode}-pipeline-terraform-build 1859 | RoleArn: !GetAtt iamrolepipeline.Arn 1860 | ArtifactStore: 1861 | Type: S3 1862 | Location: !Ref s3bucketartifact 1863 | Stages: 1864 | - Name: Source 1865 | Actions: 1866 | - Name: Source 1867 | ActionTypeId: 1868 | Category: Source 1869 | Owner: AWS 1870 | Version: 1 1871 | Provider: S3 1872 | Configuration: 1873 | S3Bucket: !Ref s3bucketgit 1874 | S3ObjectKey: my-workspace/refs/heads/main/repo.zip 1875 | PollForSourceChanges: false 1876 | OutputArtifacts: 1877 | - Name: SourceCode 1878 | RunOrder: 1 1879 | Region: !Ref AWS::Region 1880 | - Name: Build 1881 | Actions: 1882 | - Name: Build 1883 | ActionTypeId: 1884 | Category: Build 1885 | Owner: AWS 1886 | Version: 1 1887 | Provider: CodeBuild 1888 | Configuration: 1889 | ProjectName: !Ref codebuildprojectbuild 1890 | InputArtifacts: 1891 | - Name: SourceCode 1892 | OutputArtifacts: 1893 | - Name: BuildOutput 1894 | RunOrder: 1 1895 | Region: !Ref AWS::Region 1896 | Tags: 1897 | - Key: Name 1898 | Value: !Sub ${PrefixCode}-pipeline-terraform-build 1899 | - Key: provisioner 1900 | Value: CFN 1901 | - Key: solution 1902 | Value: !Sub ${SolutionTag} 1903 | - Key: resourcetype 1904 | Value: devops 1905 | - Key: environment 1906 | Value: !Sub ${EnvironmentTag} 1907 | # Terraform destroy pipeline 1908 | pipelinedestroy: 1909 | Type: AWS::CodePipeline::Pipeline 1910 | Condition: CreatePipeline 1911 | Properties: 1912 | Name: !Sub ${PrefixCode}-pipeline-terraform-destroy 1913 | RoleArn: !GetAtt iamrolepipeline.Arn 1914 | ArtifactStore: 1915 | Type: S3 1916 | Location: !Ref s3bucketartifact 1917 | Stages: 1918 | - Name: Source 1919 | Actions: 1920 | - Name: Source 1921 | ActionTypeId: 1922 | Category: Source 1923 | Owner: AWS 1924 | Version: 1 1925 | Provider: S3 1926 | Configuration: 1927 | S3Bucket: !Ref s3bucketgit 1928 | S3ObjectKey: my-workspace/refs/heads/main/repo.zip 1929 | PollForSourceChanges: false 1930 | OutputArtifacts: 1931 | - Name: SourceCode 1932 | RunOrder: 1 1933 | Region: !Ref AWS::Region 1934 | - Name: Approve 1935 | Actions: 1936 | - Name: ApproveDestroy 1937 | ActionTypeId: 1938 | Category: Approval 1939 | Owner: AWS 1940 | Version: 1 1941 | Provider: Manual 1942 | Configuration: 1943 | CustomData: "WARNING: This will destroy all Terraform-managed resources. Are you sure?" 1944 | RunOrder: 1 1945 | - Name: Destroy 1946 | Actions: 1947 | - Name: Destroy 1948 | ActionTypeId: 1949 | Category: Build 1950 | Owner: AWS 1951 | Version: 1 1952 | Provider: CodeBuild 1953 | Configuration: 1954 | ProjectName: !Ref codebuildprojectdestroy 1955 | InputArtifacts: 1956 | - Name: SourceCode 1957 | RunOrder: 1 1958 | Region: !Ref AWS::Region 1959 | Tags: 1960 | - Key: Name 1961 | Value: !Sub ${PrefixCode}-pipeline-terraform-destroy 1962 | - Key: provisioner 1963 | Value: CFN 1964 | - Key: solution 1965 | Value: !Sub ${SolutionTag} 1966 | - Key: resourcetype 1967 | Value: devops 1968 | - Key: environment 1969 | Value: !Sub ${EnvironmentTag} 1970 | # Trigger code pipeline on S3 event 1971 | iamroleeventbridge: 1972 | Type: AWS::IAM::Role 1973 | Condition: CreatePipeline 1974 | Properties: 1975 | AssumeRolePolicyDocument: 1976 | Version: 2012-10-17 1977 | Statement: 1978 | - Effect: Allow 1979 | Principal: 1980 | Service: events.amazonaws.com 1981 | Action: sts:AssumeRole 1982 | Policies: 1983 | - PolicyName: !Sub ${PrefixCode}-iampolicy-InvokePipeline 1984 | PolicyDocument: 1985 | Version: 2012-10-17 1986 | Statement: 1987 | - Effect: Allow 1988 | Action: codepipeline:StartPipelineExecution 1989 | Resource: !Sub arn:${AWS::Partition}:codepipeline:${AWS::Region}:${AWS::AccountId}:${pipelinebuild} 1990 | - PolicyName: !Sub ${PrefixCode}-iampolicy-WriteToCloudWatch 1991 | PolicyDocument: 1992 | Version: 2012-10-17 1993 | Statement: 1994 | - Effect: Allow 1995 | Action: 1996 | - logs:CreateLogStream 1997 | - logs:PutLogEvents 1998 | Resource: !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/events/${PrefixCode}-pipeline-trigger:* 1999 | Metadata: 2000 | cdk_nag: 2001 | rules_to_suppress: 2002 | - id: AwsSolutions-IAM5 2003 | reason: "Wildcard required for log stream access. Permission scoped to single log group. Consider removal if pipeline logging not required." 2004 | eventbridgeloggroup: 2005 | Type: AWS::Logs::LogGroup 2006 | Condition: CreatePipeline 2007 | Properties: 2008 | LogGroupName: !Sub /aws/events/${PrefixCode}-pipeline-trigger 2009 | RetentionInDays: 14 2010 | Tags: 2011 | - Key: Name 2012 | Value: !Sub ${PrefixCode}-pipeline-trigger 2013 | - Key: provisioner 2014 | Value: CFN 2015 | - Key: solution 2016 | Value: !Sub ${SolutionTag} 2017 | - Key: resourcetype 2018 | Value: monitoring 2019 | - Key: environment 2020 | Value: !Sub ${EnvironmentTag} 2021 | eventbridgetriggerrule: 2022 | Type: AWS::Events::Rule 2023 | Condition: CreatePipeline 2024 | Properties: 2025 | Name: !Sub ${PrefixCode}-pipeline-trigger 2026 | EventBusName: default 2027 | EventPattern: 2028 | source: 2029 | - aws.s3 2030 | detail-type: 2031 | - Object Created 2032 | detail: 2033 | bucket: 2034 | name: 2035 | - !Ref s3bucketgit 2036 | object: 2037 | key: 2038 | - my-workspace/refs/heads/main/repo.zip 2039 | State: ENABLED 2040 | Targets: 2041 | - Arn: !Sub arn:${AWS::Partition}:codepipeline:${AWS::Region}:${AWS::AccountId}:${pipelinebuild} 2042 | Id: CodePipelineTarget 2043 | RoleArn: !GetAtt iamroleeventbridge.Arn 2044 | 2045 | ### Lambda-based rotation of code-server password 2046 | secretcodeserverrotating: 2047 | Type: AWS::SecretsManager::Secret 2048 | Condition: EnableRotation 2049 | Properties: 2050 | Name: !Sub ${PrefixCode}-secret-rotating-codeserver 2051 | Description: Rotating code-server password that updates automatically every 30 days. This replaces the initial secret after first rotation. 2052 | KmsKeyId: !GetAtt kmskey.Arn 2053 | GenerateSecretString: 2054 | SecretStringTemplate: '{"username": "admin"}' 2055 | GenerateStringKey: "password" 2056 | PasswordLength: 16 2057 | ExcludeCharacters: '\[]{}>|*&!%#`@,."$:+=-~^()''' 2058 | RequireEachIncludedType: true 2059 | Tags: 2060 | - Key: Name 2061 | Value: !Sub ${PrefixCode}-secret-rotating-codeserver 2062 | - Key: provisioner 2063 | Value: CFN 2064 | - Key: solution 2065 | Value: !Sub ${SolutionTag} 2066 | - Key: resourcetype 2067 | Value: security 2068 | - Key: environment 2069 | Value: !Sub ${EnvironmentTag} 2070 | secretcodeserverrotationschedule: 2071 | Type: AWS::SecretsManager::RotationSchedule 2072 | DependsOn: lambdasecretrotationpermission 2073 | Condition: EnableRotation 2074 | Properties: 2075 | SecretId: !Ref secretcodeserverrotating 2076 | RotationLambdaARN: !GetAtt lambdasecretrotation.Arn 2077 | RotationRules: 2078 | AutomaticallyAfterDays: 30 2079 | RotateImmediatelyOnUpdate: false 2080 | lambdasecretrotation: 2081 | # https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotate-secrets_lambda-functions.html 2082 | Type: AWS::Lambda::Function 2083 | Condition: EnableRotation 2084 | Properties: 2085 | Description: Lambda function to rotate code-server password 2086 | FunctionName: !Sub ${PrefixCode}-lambda-rotation-codeserver 2087 | Handler: index.lambda_handler 2088 | KmsKeyArn: !GetAtt kmskey.Arn 2089 | Runtime: python3.12 2090 | Timeout: 300 2091 | MemorySize: 128 2092 | Role: !GetAtt iamrolelambda.Arn 2093 | Environment: 2094 | Variables: 2095 | INSTANCE_ID: !Ref ec2codeserver 2096 | SECRETS_MANAGER_ENDPOINT: !Sub https://secretsmanager.${AWS::Region}.amazonaws.com 2097 | TracingConfig: 2098 | Mode: Active 2099 | Code: 2100 | ZipFile: | 2101 | import boto3 2102 | import json 2103 | import logging 2104 | import os 2105 | 2106 | logger = logging.getLogger() 2107 | logger.setLevel(logging.INFO) 2108 | 2109 | def lambda_handler(event, context): 2110 | arn = event['SecretId'] 2111 | token = event['ClientRequestToken'] 2112 | step = event['Step'] 2113 | 2114 | # Setup the client 2115 | service_client = boto3.client('secretsmanager', endpoint_url=os.environ['SECRETS_MANAGER_ENDPOINT']) 2116 | 2117 | # Make sure the version is staged correctly 2118 | metadata = service_client.describe_secret(SecretId=arn) 2119 | if not metadata['RotationEnabled']: 2120 | logger.error("Secret %s is not enabled for rotation" % arn) 2121 | raise ValueError("Secret %s is not enabled for rotation" % arn) 2122 | versions = metadata['VersionIdsToStages'] 2123 | if token not in versions: 2124 | logger.error("Secret version %s has no stage for rotation of secret %s." % (token, arn)) 2125 | raise ValueError("Secret version %s has no stage for rotation of secret %s." % (token, arn)) 2126 | if "AWSCURRENT" in versions[token]: 2127 | logger.info("Secret version %s already set as AWSCURRENT for secret %s." % (token, arn)) 2128 | return 2129 | elif "AWSPENDING" not in versions[token]: 2130 | logger.error("Secret version %s not set as AWSPENDING for rotation of secret %s." % (token, arn)) 2131 | raise ValueError("Secret version %s not set as AWSPENDING for rotation of secret %s." % (token, arn)) 2132 | 2133 | if step == "createSecret": 2134 | create_secret(service_client, arn, token) 2135 | elif step == "setSecret": 2136 | set_secret(service_client, arn, token) 2137 | elif step == "testSecret": 2138 | test_secret(service_client, arn, token) 2139 | elif step == "finishSecret": 2140 | finish_secret(service_client, arn, token) 2141 | else: 2142 | raise ValueError("Invalid step parameter") 2143 | 2144 | def create_secret(service_client, arn, token): 2145 | """Create the secret""" 2146 | # Get current secret to keep username 2147 | current_secret = service_client.get_secret_value( 2148 | SecretId=arn, 2149 | VersionStage="AWSCURRENT" 2150 | ) 2151 | current_dict = json.loads(current_secret['SecretString']) 2152 | 2153 | # Generate new password using same rules as original secret 2154 | passwd = service_client.get_random_password( 2155 | ExcludeCharacters='\[]{}>|*&!%#`@,."$:+=-~^()', 2156 | PasswordLength=16, 2157 | RequireEachIncludedType=True 2158 | ) 2159 | 2160 | # Create new secret with existing username and new password 2161 | new_secret = { 2162 | 'username': current_dict['username'], 2163 | 'password': passwd['RandomPassword'] 2164 | } 2165 | 2166 | # Put the secret 2167 | service_client.put_secret_value( 2168 | SecretId=arn, 2169 | ClientRequestToken=token, 2170 | SecretString=json.dumps(new_secret), 2171 | VersionStages=['AWSPENDING'] 2172 | ) 2173 | logger.info(f"createSecret: Successfully put secret for ARN {arn} and version {token}") 2174 | 2175 | def set_secret(service_client, arn, token): 2176 | """Set the secret in code-server""" 2177 | # Get the pending secret 2178 | pending_secret = service_client.get_secret_value( 2179 | SecretId=arn, 2180 | VersionStage="AWSPENDING" 2181 | ) 2182 | pending_dict = json.loads(pending_secret['SecretString']) 2183 | 2184 | # Use SSM to update code-server config 2185 | ssm = boto3.client('ssm') 2186 | instance_id = os.environ['INSTANCE_ID'] 2187 | 2188 | response = ssm.send_command( 2189 | InstanceIds=[instance_id], 2190 | DocumentName='AWS-RunShellScript', 2191 | Parameters={ 2192 | 'commands': [ 2193 | 'cat > /home/ec2-user/.config/code-server/config.yaml << EOL\n' 2194 | 'bind-addr: 0.0.0.0:8080\n' 2195 | 'auth: password\n' 2196 | f'password: {pending_dict["password"]}\n' 2197 | 'cert: false\n' 2198 | 'EOL', 2199 | 'systemctl restart code-server' 2200 | ] 2201 | } 2202 | ) 2203 | logger.info(f"setSecret: Updated code-server config for instance {instance_id}") 2204 | 2205 | def test_secret(service_client, arn, token): 2206 | """Test the secret""" 2207 | # Check if code-server service is running 2208 | ssm = boto3.client('ssm') 2209 | instance_id = os.environ['INSTANCE_ID'] 2210 | 2211 | response = ssm.send_command( 2212 | InstanceIds=[instance_id], 2213 | DocumentName='AWS-RunShellScript', 2214 | Parameters={ 2215 | 'commands': ['systemctl is-active code-server'] 2216 | } 2217 | ) 2218 | logger.info(f"testSecret: Verified code-server is running on {instance_id}") 2219 | 2220 | def finish_secret(service_client, arn, token): 2221 | """Finish the secret""" 2222 | # First describe the secret to get the current version 2223 | metadata = service_client.describe_secret(SecretId=arn) 2224 | current_version = None 2225 | for version in metadata["VersionIdsToStages"]: 2226 | if "AWSCURRENT" in metadata["VersionIdsToStages"][version]: 2227 | if version == token: 2228 | # The correct version is already marked as current, return 2229 | logger.info("finishSecret: Version %s already marked as AWSCURRENT for %s" % (version, arn)) 2230 | return 2231 | current_version = version 2232 | break 2233 | 2234 | # Finalize by staging the secret version current 2235 | service_client.update_secret_version_stage( 2236 | SecretId=arn, 2237 | VersionStage="AWSPENDING", 2238 | RemoveFromVersionId=token 2239 | ) 2240 | service_client.update_secret_version_stage( 2241 | SecretId=arn, 2242 | VersionStage="AWSCURRENT", 2243 | MoveToVersionId=token, 2244 | RemoveFromVersionId=current_version 2245 | ) 2246 | logger.info("finishSecret: Successfully set AWSCURRENT stage to version %s for secret %s." % (token, arn)) 2247 | Tags: 2248 | - Key: Name 2249 | Value: !Sub ${PrefixCode}-lambda-rotation-codeserver 2250 | - Key: provisioner 2251 | Value: CFN 2252 | - Key: solution 2253 | Value: !Sub ${SolutionTag} 2254 | - Key: resourcetype 2255 | Value: compute 2256 | - Key: environment 2257 | Value: !Sub ${EnvironmentTag} 2258 | Metadata: 2259 | cdk_nag: 2260 | rules_to_suppress: 2261 | - id: AwsSolutions-L1 2262 | reason: "Using latest supported Python runtime (3.12). Will be updated when new versions are available." 2263 | checkov: 2264 | skip: 2265 | - id: CKV_SECRET_6 2266 | comment: "False positive - detecting event token and generated password variables in Lambda rotation function. No hardcoded secrets present." 2267 | lambdasecretrotationpermission: 2268 | Type: AWS::Lambda::Permission 2269 | Condition: EnableRotation 2270 | Properties: 2271 | Action: lambda:InvokeFunction 2272 | FunctionName: !Ref lambdasecretrotation 2273 | Principal: secretsmanager.amazonaws.com 2274 | iamrolelambda: 2275 | Type: AWS::IAM::Role 2276 | Condition: EnableRotation 2277 | Properties: 2278 | RoleName: !Sub ${PrefixCode}-role-secretrotation-codeserver 2279 | AssumeRolePolicyDocument: 2280 | Version: 2012-10-17 2281 | Statement: 2282 | - Effect: Allow 2283 | Principal: 2284 | Service: lambda.amazonaws.com 2285 | Action: sts:AssumeRole 2286 | Tags: 2287 | - Key: Name 2288 | Value: !Sub ${PrefixCode}-role-secretrotation-codeserver 2289 | - Key: provisioner 2290 | Value: CFN 2291 | - Key: solution 2292 | Value: !Sub ${SolutionTag} 2293 | - Key: resourcetype 2294 | Value: security 2295 | - Key: environment 2296 | Value: !Sub ${EnvironmentTag} 2297 | iamrolepolicylambdarotation: 2298 | Type: AWS::IAM::Policy 2299 | Condition: EnableRotation 2300 | Properties: 2301 | PolicyName: !Sub ${PrefixCode}-policy-rotation-secret-access 2302 | Roles: 2303 | - !Ref iamrolelambda 2304 | PolicyDocument: 2305 | Version: 2012-10-17 2306 | Statement: 2307 | - Effect: Allow 2308 | Action: 2309 | - logs:CreateLogGroup 2310 | - logs:CreateLogStream 2311 | - logs:PutLogEvents 2312 | Resource: 2313 | - !Sub arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${PrefixCode}-lambda-rotation-codeserver:* 2314 | - Effect: Allow 2315 | Action: 2316 | - secretsmanager:DescribeSecret 2317 | - secretsmanager:GetSecretValue 2318 | - secretsmanager:PutSecretValue 2319 | - secretsmanager:UpdateSecretVersionStage 2320 | Resource: !Ref secretcodeserverrotating 2321 | - Effect: Allow 2322 | Action: 2323 | - secretsmanager:GetRandomPassword 2324 | Resource: "*" 2325 | - Effect: Allow 2326 | Action: 2327 | - kms:Decrypt 2328 | - kms:DescribeKey 2329 | - kms:GenerateDataKey 2330 | Resource: !GetAtt kmskey.Arn 2331 | Condition: 2332 | StringEquals: 2333 | kms:EncryptionContext:SecretARN: !Ref secretcodeserverrotating 2334 | - Effect: Allow 2335 | Action: 2336 | - ssm:SendCommand 2337 | - ssm:GetCommandInvocation 2338 | Resource: 2339 | - !Sub arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:instance/${ec2codeserver} 2340 | - !Sub arn:${AWS::Partition}:ssm:${AWS::Region}:*:document/AWS-RunShellScript 2341 | Metadata: 2342 | cfn_nag: 2343 | rules_to_suppress: 2344 | - id: AwsSolutions-IAM5 2345 | reason: Lambda rotation role requires CloudWatch Logs and SSM document access. Wildcards scoped to specific log groups and SSM RunShellScript document. Refer to https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotating-secrets-required-permissions-function.html 2346 | cdk_nag: 2347 | rules_to_suppress: 2348 | - id: AwsSolutions-IAM5 2349 | appliesTo: 2350 | - "Resource::arn::logs:::log-group:/aws/lambda/-lambda-rotation-codeserver:*" 2351 | - "Resource::*" 2352 | - "Resource::arn::ssm::*:document/AWS-RunShellScript" 2353 | reason: "Lambda requires CloudWatch Logs access and SSM RunShellScript document permissions. GetRandomPassword requires * resource." 2354 | Outputs: 2355 | 01CodeServerURL: 2356 | Description: URL to access code-server through CloudFront. Open in a separate tab and then return to collect password below! 2357 | Value: !Sub https://${cloudfrontdistributioncodeserver.DomainName} 2358 | 02CodeServerPassword: 2359 | Description: Retrieve your initial code-server password from AWS Secrets Manager. If rotation is enabled, after 30 days check the 'rotating-codeserver' secret instead 2360 | Value: !Sub https://${AWS::Region}.console.aws.amazon.com/secretsmanager/secret?name=${secretcodeserver} --------------------------------------------------------------------------------