From cad781c0e7ee1bea0124420262e4ea3755ea9f63 Mon Sep 17 00:00:00 2001 From: Manu Chandrasekhar Date: Wed, 18 Dec 2024 18:30:24 -0500 Subject: [PATCH] v.0.1.0 New release, including breaking change, see below. * Lambda function URL is now using IAM auth + CloudFront origin access control (oac) * Bug fix for unverified callback * **Breaking change**: Step function logs now using vendedlogs namespace, existing user will see new log group * Add tags and upgrade lambda runtime * Various improvements to ensure idempotency and avoid cross event contamination. --- .config/.tflint.hcl | 2 +- .../pre-entrypoint-helpers.sh | 6 +- .gitignore | 10 + .header.md | 8 +- README.md | 37 ++-- VERSION | 2 +- cloudfront.tf | 38 +++- data.tf | 32 +++ event/runtask_rule.tpl | 3 +- eventbridge.tf | 7 +- examples/demo_workspace/.header.md | 26 +-- examples/demo_workspace/README.md | 48 +++-- examples/demo_workspace/main.tf | 4 +- examples/demo_workspace/tf.auto.tfvars | 6 - examples/demo_workspace/versions.tf | 16 +- examples/module_workspace/.header.md | 10 +- examples/module_workspace/README.md | 23 ++- examples/module_workspace/main.tf | 2 +- examples/module_workspace/versions.tf | 9 +- iam.tf | 20 ++ iam/trust-policies/lambda_edge.tpl | 15 ++ kms.tf | 2 + lambda.tf | 75 +++++-- lambda/runtask_callback/handler.py | 27 +-- lambda/runtask_edge/Makefile | 19 ++ lambda/runtask_edge/handler.py | 41 ++++ .../runtask_edge/requirements.txt | 0 lambda/runtask_eventbridge/handler.py | 192 ++++++++++-------- lambda/runtask_fulfillment/handler.py | 23 +-- lambda/runtask_request/handler.py | 73 ++++--- locals.tf | 21 +- outputs.tf | 4 +- secrets.tf | 4 +- states.tf | 4 +- states/runtask_states.asl.json | 2 +- variables.tf | 16 ++ versions.tf | 9 +- waf.tf | 11 +- 38 files changed, 576 insertions(+), 271 deletions(-) delete mode 100644 examples/demo_workspace/tf.auto.tfvars create mode 100644 iam/trust-policies/lambda_edge.tpl create mode 100644 lambda/runtask_edge/Makefile create mode 100644 lambda/runtask_edge/handler.py rename main.tf => lambda/runtask_edge/requirements.txt (100%) diff --git a/.config/.tflint.hcl b/.config/.tflint.hcl index 0ce7d8c..90f1631 100644 --- a/.config/.tflint.hcl +++ b/.config/.tflint.hcl @@ -3,7 +3,7 @@ plugin "aws" { enabled = true - version = "0.22.1" + version = "0.54.0" source = "github.com/terraform-linters/tflint-ruleset-aws" } diff --git a/.config/functional_tests/pre-entrypoint-helpers.sh b/.config/functional_tests/pre-entrypoint-helpers.sh index c315459..5d38fcf 100644 --- a/.config/functional_tests/pre-entrypoint-helpers.sh +++ b/.config/functional_tests/pre-entrypoint-helpers.sh @@ -12,8 +12,8 @@ cd ${PROJECT_PATH} #********** TFC Env Vars ************* export AWS_DEFAULT_REGION=us-east-1 -export TFE_TOKEN=`aws secretsmanager get-secret-value --secret-id abp/tfc/token | jq -r ".SecretString"` -export TF_TOKEN_app_terraform_io=`aws secretsmanager get-secret-value --secret-id abp/tfc/token | jq -r ".SecretString"` +export TFE_TOKEN=`aws secretsmanager get-secret-value --secret-id abp/hcp/token --region us-west-2 | jq -r ".SecretString"` +export TF_TOKEN_app_terraform_io=`aws secretsmanager get-secret-value --secret-id abp/hcp/token --region us-west-2 | jq -r ".SecretString"` #********** MAKEFILE ************* echo "Build the lambda function packages" @@ -22,7 +22,7 @@ make all #********** Get tfvars from SSM ************* echo "Get *.tfvars from SSM parameter" aws ssm get-parameter \ - --name "/abp/tfc/functional/tfc_org/terraform_test.tfvars" \ + --name "/abp/hcp/functional/terraform-aws-runtask-iam-access-analyzer/terraform_tests.tfvars" \ --with-decryption \ --query "Parameter.Value" \ --output "text" \ diff --git a/.gitignore b/.gitignore index 25c6073..1f8b7ac 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ crash.log # to change depending on the environment. # ./*.tfvars +*.tfvars # Ignore override files as they are usually used to override resources locally and so # are not checked in @@ -40,3 +41,12 @@ terraform.rc go.mod go.sum + + +**/site-packages +*.zip +settings.json +TODO.md +.DS_Store +.idea +.venv \ No newline at end of file diff --git a/.header.md b/.header.md index 132c7b6..abc8bfc 100644 --- a/.header.md +++ b/.header.md @@ -1,6 +1,6 @@ # terraform-runtask-iam-access-analyzer -Use this module to integrate Terraform Cloud Run Tasks with AWS IAM Access Analyzer for policy validation. +Use this module to integrate HCP Terraform Run Tasks with AWS IAM Access Analyzer for policy validation. ![Diagram](./diagram/RunTask-EventBridge.png) @@ -9,7 +9,7 @@ Use this module to integrate Terraform Cloud Run Tasks with AWS IAM Access Analy To use this module you need have the following: 1. AWS account and credentials -2. Terraform Cloud with Run Task entitlement (Business subscription or higher) +2. HCP Terraform with Run Task entitlement (Business subscription or higher) ## Usage @@ -19,9 +19,9 @@ To use this module you need have the following: make all ``` -* Refer to the [module_workspace](./examples/module_workspace/README.md) for steps to deploy this module in Terraform Cloud. +* Refer to the [module_workspace](./examples/module_workspace/README.md) for steps to deploy this module in HCP Terraform. -* After you deployed the [module_workspace](./examples/module_workspace/README.md), navigate to your Terraform Cloud organization, go to Organization Settings > Integrations > Run tasks to find the newly created Run Task. +* After you deployed the [module_workspace](./examples/module_workspace/README.md), navigate to your HCP Terraform organization, go to Organization Settings > Integrations > Run tasks to find the newly created Run Task. * You can use this run task in any workspace where you have standard IAM resource policy document. Refer to the [demo_workspace](./examples/demo_workspace/README.md) for more details. diff --git a/README.md b/README.md index 7090451..6099d30 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # terraform-runtask-iam-access-analyzer -Use this module to integrate Terraform Cloud Run Tasks with AWS IAM Access Analyzer for policy validation. +Use this module to integrate HCP Terraform Run Tasks with AWS IAM Access Analyzer for policy validation. ![Diagram](./diagram/RunTask-EventBridge.png) @@ -10,7 +10,7 @@ Use this module to integrate Terraform Cloud Run Tasks with AWS IAM Access Analy To use this module you need have the following: 1. AWS account and credentials -2. Terraform Cloud with Run Task entitlement (Business subscription or higher) +2. HCP Terraform with Run Task entitlement (Business subscription or higher) ## Usage @@ -20,9 +20,9 @@ To use this module you need have the following: make all ``` -* Refer to the [module\_workspace](./examples/module\_workspace/README.md) for steps to deploy this module in Terraform Cloud. +* Refer to the [module\_workspace](./examples/module\_workspace/README.md) for steps to deploy this module in HCP Terraform. -* After you deployed the [module\_workspace](./examples/module\_workspace/README.md), navigate to your Terraform Cloud organization, go to Organization Settings > Integrations > Run tasks to find the newly created Run Task. +* After you deployed the [module\_workspace](./examples/module\_workspace/README.md), navigate to your HCP Terraform organization, go to Organization Settings > Integrations > Run tasks to find the newly created Run Task. * You can use this run task in any workspace where you have standard IAM resource policy document. Refer to the [demo\_workspace](./examples/demo\_workspace/README.md) for more details. @@ -79,25 +79,27 @@ resource "aws_iam_policy" "policy" { |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.7 | | [archive](#requirement\_archive) | ~>2.2.0 | -| [aws](#requirement\_aws) | >= 3.73.0, < 5.0.0 | +| [aws](#requirement\_aws) | >=5.72.0 | | [random](#requirement\_random) | >=3.4.0 | -| [tfe](#requirement\_tfe) | ~>0.38.0 | +| [tfe](#requirement\_tfe) | >=0.38.0 | +| [time](#requirement\_time) | >=0.12.0 | ## Providers | Name | Version | |------|---------| | [archive](#provider\_archive) | ~>2.2.0 | -| [aws](#provider\_aws) | >= 3.73.0, < 5.0.0 | -| [aws.cloudfront\_waf](#provider\_aws.cloudfront\_waf) | >= 3.73.0, < 5.0.0 | +| [aws](#provider\_aws) | >=5.72.0 | +| [aws.cloudfront\_waf](#provider\_aws.cloudfront\_waf) | >=5.72.0 | | [random](#provider\_random) | >=3.4.0 | -| [tfe](#provider\_tfe) | ~>0.38.0 | +| [tfe](#provider\_tfe) | >=0.38.0 | +| [time](#provider\_time) | >=0.12.0 | ## Modules | Name | Source | Version | |------|--------|---------| -| [runtask\_cloudfront](#module\_runtask\_cloudfront) | terraform-aws-modules/cloudfront/aws | 3.2.1 | +| [runtask\_cloudfront](#module\_runtask\_cloudfront) | terraform-aws-modules/cloudfront/aws | 3.4.0 | ## Resources @@ -113,7 +115,9 @@ resource "aws_iam_policy" "policy" { | [aws_cloudwatch_log_group.runtask_request](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | | [aws_cloudwatch_log_group.runtask_states](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | | [aws_cloudwatch_log_group.runtask_waf](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | +| [aws_cloudwatch_log_resource_policy.runtask_waf](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_resource_policy) | resource | | [aws_iam_role.runtask_callback](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role.runtask_edge](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role.runtask_eventbridge](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role.runtask_fulfillment](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role.runtask_request](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | @@ -124,6 +128,7 @@ resource "aws_iam_policy" "policy" { | [aws_iam_role_policy.runtask_rule](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | | [aws_iam_role_policy.runtask_states](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | | [aws_iam_role_policy_attachment.runtask_callback](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.runtask_edge](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_iam_role_policy_attachment.runtask_eventbridge](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_iam_role_policy_attachment.runtask_fulfillment](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_iam_role_policy_attachment.runtask_request](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | @@ -132,10 +137,12 @@ resource "aws_iam_policy" "policy" { | [aws_kms_key.runtask_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_key) | resource | | [aws_kms_key.runtask_waf](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_key) | resource | | [aws_lambda_function.runtask_callback](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource | +| [aws_lambda_function.runtask_edge](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource | | [aws_lambda_function.runtask_eventbridge](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource | | [aws_lambda_function.runtask_fulfillment](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource | | [aws_lambda_function.runtask_request](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource | | [aws_lambda_function_url.runtask_eventbridge](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function_url) | resource | +| [aws_lambda_permission.runtask_eventbridge](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_permission) | resource | | [aws_secretsmanager_secret.runtask_cloudfront](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret) | resource | | [aws_secretsmanager_secret.runtask_hmac](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret) | resource | | [aws_secretsmanager_secret_version.runtask_cloudfront](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret_version) | resource | @@ -143,10 +150,13 @@ resource "aws_iam_policy" "policy" { | [aws_sfn_state_machine.runtask_states](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sfn_state_machine) | resource | | [aws_wafv2_web_acl.runtask_waf](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl) | resource | | [aws_wafv2_web_acl_logging_configuration.runtask_waf](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl_logging_configuration) | resource | +| [random_string.solution_prefix](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource | | [random_uuid.runtask_cloudfront](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/uuid) | resource | | [random_uuid.runtask_hmac](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/uuid) | resource | | [tfe_organization_run_task.aws_iam_analyzer](https://registry.terraform.io/providers/hashicorp/tfe/latest/docs/resources/organization_run_task) | resource | +| [time_sleep.wait_1800_seconds](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/sleep) | resource | | [archive_file.runtask_callback](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source | +| [archive_file.runtask_edge](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source | | [archive_file.runtask_eventbridge](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source | | [archive_file.runtask_fulfillment](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source | | [archive_file.runtask_request](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source | @@ -154,6 +164,7 @@ resource "aws_iam_policy" "policy" { | [aws_iam_policy.aws_lambda_basic_execution_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy) | data source | | [aws_iam_policy_document.runtask_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.runtask_waf](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.runtask_waf_log](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_partition.current_partition](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | | [aws_region.cloudfront_region](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | | [aws_region.current_region](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | @@ -169,12 +180,14 @@ resource "aws_iam_policy" "policy" { | [deploy\_waf](#input\_deploy\_waf) | Set to true to deploy CloudFront and WAF in front of the Lambda function URL | `string` | `false` | no | | [event\_bus\_name](#input\_event\_bus\_name) | EventBridge event bus name | `string` | `"default"` | no | | [event\_source](#input\_event\_source) | EventBridge source name | `string` | `"app.terraform.io"` | no | +| [lambda\_architecture](#input\_lambda\_architecture) | Lambda architecture (arm64 or x86\_64) | `string` | `"x86_64"` | no | | [lambda\_default\_timeout](#input\_lambda\_default\_timeout) | Lambda default timeout in seconds | `number` | `30` | no | | [lambda\_reserved\_concurrency](#input\_lambda\_reserved\_concurrency) | Maximum Lambda reserved concurrency, make sure your AWS quota is sufficient | `number` | `100` | no | | [name\_prefix](#input\_name\_prefix) | Name to be used on all the resources as identifier. | `string` | `"aws-ia2"` | no | | [recovery\_window](#input\_recovery\_window) | Numbers of day Number of days that AWS Secrets Manager waits before it can delete the secret | `number` | `0` | no | | [runtask\_stages](#input\_runtask\_stages) | List of all supported RunTask stages | `list(string)` |
[
"pre_plan",
"post_plan",
"pre_apply"
]
| no | | [supported\_policy\_document](#input\_supported\_policy\_document) | (Optional) allow list of the supported IAM policy document | `string` | `""` | no | +| [tags](#input\_tags) | Map of tags to apply to resources deployed by this solution. | `map(any)` | `null` | no | | [waf\_managed\_rule\_set](#input\_waf\_managed\_rule\_set) | List of AWS Managed rules to use inside the WAF ACL | `list(map(string))` |
[
{
"metric_suffix": "common",
"name": "AWSManagedRulesCommonRuleSet",
"priority": 10,
"vendor_name": "AWS"
},
{
"metric_suffix": "bad_input",
"name": "AWSManagedRulesKnownBadInputsRuleSet",
"priority": 20,
"vendor_name": "AWS"
}
]
| no | | [waf\_rate\_limit](#input\_waf\_rate\_limit) | Rate limit for request coming to WAF | `number` | `100` | no | | [workspace\_prefix](#input\_workspace\_prefix) | TFC workspace name prefix that allowed to run this runtask | `string` | `""` | no | @@ -184,6 +197,6 @@ resource "aws_iam_policy" "policy" { | Name | Description | |------|-------------| | [runtask\_hmac](#output\_runtask\_hmac) | HMAC key value, keep this sensitive data safe | -| [runtask\_id](#output\_runtask\_id) | The Run Tasks id configured in Terraform Cloud | -| [runtask\_url](#output\_runtask\_url) | The Run Tasks URL endpoint, you can use this to configure the Run Task setup in Terraform Cloud | +| [runtask\_id](#output\_runtask\_id) | The Run Tasks id configured in HCP Terraform | +| [runtask\_url](#output\_runtask\_url) | The Run Tasks URL endpoint, you can use this to configure the Run Task setup in HCP Terraform | \ No newline at end of file diff --git a/VERSION b/VERSION index a3dce6c..b82608c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.0.2 +v0.1.0 diff --git a/cloudfront.tf b/cloudfront.tf index c60bd54..be25e89 100644 --- a/cloudfront.tf +++ b/cloudfront.tf @@ -1,9 +1,10 @@ module "runtask_cloudfront" { + depends_on = [time_sleep.wait_1800_seconds] #checkov:skip=CKV2_AWS_42:custom domain name is optional count = local.waf_deployment source = "terraform-aws-modules/cloudfront/aws" - version = "3.2.1" + version = "3.4.0" comment = "CloudFront for RunTask integration: ${var.name_prefix}" enabled = true @@ -12,6 +13,16 @@ module "runtask_cloudfront" { wait_for_deployment = true web_acl_id = aws_wafv2_web_acl.runtask_waf[count.index].arn + create_origin_access_control = true + origin_access_control = { + lambda_oac_access_analyzer = { + description = "CloudFront OAC to Lambda AWS-IA Access Analyzer" + origin_type = "lambda" + signing_behavior = "always" + signing_protocol = "sigv4" + } + } + origin = { runtask_eventbridge = { domain_name = split("/", aws_lambda_function_url.runtask_eventbridge.function_url)[2] @@ -19,16 +30,17 @@ module "runtask_cloudfront" { http_port = 80 https_port = 443 origin_protocol_policy = "https-only" - origin_ssl_protocols = ["TLSv1.2"] + origin_ssl_protocols = ["TLSv1"] } - custom_header = var.deploy_waf ? [local.cloudfront_custom_header] : null + origin_access_control = "lambda_oac_access_analyzer" + custom_header = var.deploy_waf ? [local.cloudfront_custom_header] : null } } default_cache_behavior = { target_origin_id = "runtask_eventbridge" viewer_protocol_policy = "https-only" - + #SecurityHeadersPolicy: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-response-headers-policies.html#managed-response-headers-policies-security response_headers_policy_id = "67f7725c-6f97-4210-82d7-5512b31e9d03" @@ -40,12 +52,21 @@ module "runtask_cloudfront" { allowed_methods = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"] cached_methods = ["GET", "HEAD", "OPTIONS"] + + lambda_function_association = { + # This function will append header x-amz-content-sha256 to allow OAC to authenticate with Lambda Function URL + viewer-request = { + lambda_arn = aws_lambda_function.runtask_edge.qualified_arn + include_body = true + } + } } viewer_certificate = { cloudfront_default_certificate = true - minimum_protocol_version = "TLSv1.2_2021" + minimum_protocol_version = "TLSv1" } + tags = local.combined_tags } resource "aws_cloudfront_origin_request_policy" "runtask_cloudfront" { @@ -72,4 +93,11 @@ resource "aws_cloudfront_origin_request_policy" "runtask_cloudfront" { query_strings_config { query_string_behavior = "all" } +} + +resource "time_sleep" "wait_1800_seconds" { + # wait for CloudFront Lambda@Edge removal that can take up to 30 mins / 1800s + # before deleting the Lambda function + depends_on = [aws_lambda_function.runtask_edge] + destroy_duration = "1800s" } \ No newline at end of file diff --git a/data.tf b/data.tf index 0506f06..b1437fc 100644 --- a/data.tf +++ b/data.tf @@ -36,6 +36,13 @@ data "archive_file" "runtask_callback" { output_path = "${path.module}/lambda/runtask_callback.zip" } +data "archive_file" "runtask_edge" { + type = "zip" + source_dir = "${path.module}/lambda/runtask_edge/site-packages" + output_path = "${path.module}/lambda/runtask_edge.zip" +} + + data "aws_iam_policy_document" "runtask_key" { #checkov:skip=CKV_AWS_109:Skip #checkov:skip=CKV_AWS_111:Skip @@ -95,6 +102,7 @@ data "aws_iam_policy_document" "runtask_key" { values = [ "arn:${data.aws_partition.current_partition.id}:logs:${data.aws_region.current_region.name}:${data.aws_caller_identity.current_account.account_id}:log-group:/aws/lambda/${var.name_prefix}*", "arn:${data.aws_partition.current_partition.id}:logs:${data.aws_region.current_region.name}:${data.aws_caller_identity.current_account.account_id}:log-group:/aws/state/${var.name_prefix}*", + "arn:${data.aws_partition.current_partition.id}:logs:${data.aws_region.current_region.name}:${data.aws_caller_identity.current_account.account_id}:log-group:/aws/vendedlogs/states/${var.name_prefix}*", "arn:${data.aws_partition.current_partition.id}:logs:${data.aws_region.current_region.name}:${data.aws_caller_identity.current_account.account_id}:log-group:${var.cloudwatch_log_group_name}*" ] } @@ -199,4 +207,28 @@ data "aws_iam_policy_document" "runtask_waf" { ] } } +} + +data "aws_iam_policy_document" "runtask_waf_log" { + count = local.waf_deployment + version = "2012-10-17" + statement { + effect = "Allow" + principals { + identifiers = ["delivery.logs.amazonaws.com"] + type = "Service" + } + actions = ["logs:CreateLogStream", "logs:PutLogEvents"] + resources = ["${aws_cloudwatch_log_group.runtask_waf[count.index].arn}:*"] + condition { + test = "ArnLike" + values = ["arn:aws:logs:${data.aws_region.cloudfront_region.name}:${data.aws_caller_identity.current_account.account_id}:*"] + variable = "aws:SourceArn" + } + condition { + test = "StringEquals" + values = [tostring(data.aws_caller_identity.current_account.account_id)] + variable = "aws:SourceAccount" + } + } } \ No newline at end of file diff --git a/event/runtask_rule.tpl b/event/runtask_rule.tpl index d9e893a..bbe99c8 100644 --- a/event/runtask_rule.tpl +++ b/event/runtask_rule.tpl @@ -6,5 +6,6 @@ ], "detail": { "stage": ${var_runtask_stages} - } + }, + "detail-type" : ["${var_event_rule_detail_type}"] } \ No newline at end of file diff --git a/eventbridge.tf b/eventbridge.tf index 916df53..b7f9a96 100644 --- a/eventbridge.tf +++ b/eventbridge.tf @@ -3,10 +3,11 @@ resource "aws_cloudwatch_event_rule" "runtask_rule" { description = "Rule to capture HashiCorp Terraform Cloud RunTask events" event_bus_name = var.event_bus_name event_pattern = templatefile("${path.module}/event/runtask_rule.tpl", { - var_event_source = var.event_source - var_runtask_stages = jsonencode(var.runtask_stages) + var_event_source = var.event_source + var_runtask_stages = jsonencode(var.runtask_stages) + var_event_rule_detail_type = local.solution_prefix }) - + tags = local.combined_tags } resource "aws_cloudwatch_event_target" "runtask_target" { diff --git a/examples/demo_workspace/.header.md b/examples/demo_workspace/.header.md index 755c44a..80c6549 100644 --- a/examples/demo_workspace/.header.md +++ b/examples/demo_workspace/.header.md @@ -2,9 +2,9 @@ **IMPORTANT**: To successfully complete this example, you must first deploy the module by following [module workspace example](../module_workspace/README.md). -## Attach Run Task into Terraform Cloud Workspace +## Attach Run Task into HCP Terraform Workspace -Follow the steps below to attach the run task created from the module into a new Terraform Cloud workspace. The new workspace will attempt to create multiple invalid IAM resources. The Run tasks integration with IAM Access Analyzer will validate it as part of post-plan stage. +Follow the steps below to attach the run task created from the module into a new HCP Terraform workspace. The new workspace will attempt to create multiple invalid IAM resources. The Run tasks integration with IAM Access Analyzer will validate it as part of post-plan stage. * Use the provided demo workspace configuration. @@ -12,16 +12,18 @@ Follow the steps below to attach the run task created from the module into a new cd examples/demo_workspace ``` -* Change the org name in with your own Terraform Cloud org name. +* Change the org name in with your own HCP Terraform org name. Optionally, change the workspace name. ```hcl terraform { cloud { - # TODO: Change this to your Terraform Cloud org name. - organization = "" + # TODO: Change this to your HCP Terraform org name. + organization = "wellsiau-org" + + # OPTIONAL: Change the workspace name workspaces { - ... + name = "AWS-Runtask-IAM-Access-Analyzer-Demo" } } ... @@ -34,18 +36,18 @@ Follow the steps below to attach the run task created from the module into a new echo 'tfc_org=""' >> tf.auto.tfvars echo 'aws_region=""' >> tf.auto.tfvars echo 'runtask_id=""' >> tf.auto.tfvars - echo 'demo_workspace_name=""' >> tf.auto.tfvars + echo 'demo_workspace_name=""' >> tf.auto.tfvars ``` -* Initialize Terraform Cloud. When prompted, enter the name of the new demo workspace as you specified in the previous step. +* Initialize HCP Terraform. ```bash terraform init ``` -* Configure the AWS credentials (`AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`) in Terraform Cloud, i.e. using variable sets. [Follow these instructions to learn more](https://developer.hashicorp.com/terraform/tutorials/cloud-get-started/cloud-create-variable-set). +* We recommend configuring dynamic credentials to provision to AWS from your HCP Terraform workspace or organization. [Follow these instructions to learn more.](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/dynamic-provider-credentials/aws-configuration) -* In order to create and configure the run tasks, you also need to have Terraform Cloud token stored as Variable/Variable Sets in the workspace. Add `TFE_HOSTNAME` and `TFE_TOKEN` environment variable to the same variable set or directly on the workspace. ![TFC Configure Variable Set](../diagram/TerraformCloud-VariableSets.png?raw=true "Configure Terraform Cloud Variable Set") +* In order to create and configure the run tasks, you also need to have HCP Terraform token stored as Variable/Variable Sets in the workspace. Add `TFE_HOSTNAME` and `TFE_TOKEN` environment variable to the same variable set or directly on the workspace. ![TFC Configure Variable Set](../diagram/TerraformCloud-VariableSets.png?raw=true "Configure HCP Terraform Variable Set") * Enable the flag to attach the run task to the demo workspace. @@ -54,7 +56,7 @@ Follow the steps below to attach the run task created from the module into a new terraform apply ``` -* Navigate back to Terraform Cloud, locate the new demo workspace and confirm that the Run Task is attached to the demo workspace. ![TFC Run Task in Workspace](../../diagram/TerraformCloud-RunTaskWorkspace.png?raw=true "Run Task attached to the demo workspace") +* Navigate back to HCP Terraform, locate the new demo workspace and confirm that the Run Task is attached to the demo workspace. ![TFC Run Task in Workspace](../../diagram/TerraformCloud-RunTaskWorkspace.png?raw=true "Run Task attached to the demo workspace") ## Test IAM Access Analyzer using Run Task @@ -72,4 +74,4 @@ The following steps deploy simple IAM policy with invalid permissions. This shou terraform apply ``` -* Terraform apply will fail due to several errors, use the CloudWatch link to review the errors. ![TFC Run Task results](../../diagram/TerraformCloud-RunTaskOutput.png?raw=true "Run Task output with IAM Access Analyzer validation") +* Terraform apply will fail due to several errors, use the CloudWatch link to review the errors. ![HCP TF Run Task results](../../diagram/TerraformCloud-RunTaskOutput.png?raw=true "Run Task output with IAM Access Analyzer validation") diff --git a/examples/demo_workspace/README.md b/examples/demo_workspace/README.md index 3030087..041dbcc 100644 --- a/examples/demo_workspace/README.md +++ b/examples/demo_workspace/README.md @@ -3,9 +3,9 @@ **IMPORTANT**: To successfully complete this example, you must first deploy the module by following [module workspace example](../module\_workspace/README.md). -## Attach Run Task into Terraform Cloud Workspace +## Attach Run Task into HCP Terraform Workspace -Follow the steps below to attach the run task created from the module into a new Terraform Cloud workspace. The new workspace will attempt to create multiple invalid IAM resources. The Run tasks integration with IAM Access Analyzer will validate it as part of post-plan stage. +Follow the steps below to attach the run task created from the module into a new HCP Terraform workspace. The new workspace will attempt to create multiple invalid IAM resources. The Run tasks integration with IAM Access Analyzer will validate it as part of post-plan stage. * Use the provided demo workspace configuration. @@ -13,80 +13,84 @@ Follow the steps below to attach the run task created from the module into a new cd examples/demo_workspace ``` -* Change the org name in with your own Terraform Cloud org name. +* Change the org name in with your own HCP Terraform org name. Optionally, change the workspace name. - ``` + ```hcl terraform { cloud { - # TODO: Change this to your Terraform Cloud org name. - organization = "" + # TODO: Change this to your HCP Terraform org name. + organization = "wellsiau-org" + + # OPTIONAL: Change the workspace name workspaces { - ... + name = "AWS-Runtask-IAM-Access-Analyzer-Demo" } } ... - } + } ``` * Populate the required variables, change the placeholder value below. + ```bash echo 'tfc_org=""' >> tf.auto.tfvars echo 'aws_region=""' >> tf.auto.tfvars echo 'runtask_id=""' >> tf.auto.tfvars - echo 'demo_workspace_name=""' >> tf.auto.tfvars + echo 'demo_workspace_name=""' >> tf.auto.tfvars ``` -* Initialize Terraform Cloud. When prompted, enter the name of the new demo workspace as you specified in the previous step. +* Initialize HCP Terraform. + ```bash terraform init ``` -* Configure the AWS credentials (`AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`) in Terraform Cloud, i.e. using variable sets. [Follow these instructions to learn more](https://developer.hashicorp.com/terraform/tutorials/cloud-get-started/cloud-create-variable-set). +* We recommend configuring dynamic credentials to provision to AWS from your HCP Terraform workspace or organization. [Follow these instructions to learn more.](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/dynamic-provider-credentials/aws-configuration) -* In order to create and configure the run tasks, you also need to have Terraform Cloud token stored as Variable/Variable Sets in the workspace. Add `TFE_HOSTNAME` and `TFE_TOKEN` environment variable to the same variable set or directly on the workspace. -![TFC Configure Variable Set](../diagram/TerraformCloud-VariableSets.png?raw=true "Configure Terraform Cloud Variable Set") +* In order to create and configure the run tasks, you also need to have HCP Terraform token stored as Variable/Variable Sets in the workspace. Add `TFE_HOSTNAME` and `TFE_TOKEN` environment variable to the same variable set or directly on the workspace. ![TFC Configure Variable Set](../diagram/TerraformCloud-VariableSets.png?raw=true "Configure HCP Terraform Variable Set") + +* Enable the flag to attach the run task to the demo workspace. - * Enable the flag to attach the run task to the demo workspace. ```bash echo 'flag_attach_runtask="true"' >> tf.auto.tfvars terraform apply ``` -* Navigate back to Terraform Cloud, locate the new demo workspace and confirm that the Run Task is attached to the demo workspace. -![TFC Run Task in Workspace](../../diagram/TerraformCloud-RunTaskWorkspace.png?raw=true "Run Task attached to the demo workspace") +* Navigate back to HCP Terraform, locate the new demo workspace and confirm that the Run Task is attached to the demo workspace. ![TFC Run Task in Workspace](../../diagram/TerraformCloud-RunTaskWorkspace.png?raw=true "Run Task attached to the demo workspace") ## Test IAM Access Analyzer using Run Task The following steps deploy simple IAM policy with invalid permissions. This should trigger the Run Task to send failure and stop the apply. * Enable the flag to deploy invalid IAM policy to the demo workspace. + ```bash echo 'flag_deploy_invalid_resource="true"' >> tf.auto.tfvars ``` * Run Terraform apply again + ```bash terraform apply ``` -* Terraform apply will fail due to several errors, use the CloudWatch link to review the errors. -![TFC Run Task results](../../diagram/TerraformCloud-RunTaskOutput.png?raw=true "Run Task output with IAM Access Analyzer validation") +* Terraform apply will fail due to several errors, use the CloudWatch link to review the errors. ![HCP TF Run Task results](../../diagram/TerraformCloud-RunTaskOutput.png?raw=true "Run Task output with IAM Access Analyzer validation") ## Requirements | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.7 | -| [aws](#requirement\_aws) | >= 3.73.0, < 5.0.0 | -| [tfe](#requirement\_tfe) | ~>0.38.0 | +| [aws](#requirement\_aws) | >=5.72.0 | +| [tfe](#requirement\_tfe) | >=0.38.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 3.73.0, < 5.0.0 | -| [tfe](#provider\_tfe) | ~>0.38.0 | +| [aws](#provider\_aws) | >=5.72.0 | +| [tfe](#provider\_tfe) | >=0.38.0 | ## Modules diff --git a/examples/demo_workspace/main.tf b/examples/demo_workspace/main.tf index cafc847..dd2185c 100644 --- a/examples/demo_workspace/main.tf +++ b/examples/demo_workspace/main.tf @@ -7,7 +7,7 @@ resource "tfe_workspace_run_task" "aws-iam-analyzer-attach" { workspace_id = data.tfe_workspace.workspace.id task_id = var.runtask_id enforcement_level = var.runtask_enforcement_level - stage = var.runtask_stage + stages = [var.runtask_stage] } # ========================================================================== @@ -237,4 +237,4 @@ EOF # ] # } # EOF -# } \ No newline at end of file +# } diff --git a/examples/demo_workspace/tf.auto.tfvars b/examples/demo_workspace/tf.auto.tfvars deleted file mode 100644 index cb5f5ac..0000000 --- a/examples/demo_workspace/tf.auto.tfvars +++ /dev/null @@ -1,6 +0,0 @@ -tfc_org = "wellsiau-org" -aws_region = "us-west-2" -runtask_id = "task-PxfajvmqKhdwjf3V" -demo_workspace_name = "aws-ia2-workspace" -flag_attach_runtask = "true" -flag_deploy_invalid_resource = "true" diff --git a/examples/demo_workspace/versions.tf b/examples/demo_workspace/versions.tf index 3b452fe..bafe2ba 100644 --- a/examples/demo_workspace/versions.tf +++ b/examples/demo_workspace/versions.tf @@ -1,10 +1,12 @@ terraform { cloud { - # TODO: Change this to your Terraform Cloud org name. - organization = "my-sample-org" + # TODO: Change this to your HCP Terraform org name. + organization = "wellsiau-org" + + # OPTIONAL: Change the workspace name workspaces { - tags = ["app:aws-access-analyzer-demo"] + name = "AWS-Runtask-IAM-Access-Analyzer-Demo" } } @@ -12,12 +14,16 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 3.73.0, < 5.0.0" + version = ">=5.72.0" } tfe = { source = "hashicorp/tfe" - version = "~>0.38.0" + version = ">=0.38.0" } } } + +provider "aws" { + region = var.aws_region +} diff --git a/examples/module_workspace/.header.md b/examples/module_workspace/.header.md index fd0e315..6df530d 100644 --- a/examples/module_workspace/.header.md +++ b/examples/module_workspace/.header.md @@ -1,6 +1,6 @@ # Usage Example -First step is to deploy the module into dedicated Terraform Cloud workspace. The output `runtask_id` is used on other Terraform Cloud workspace to configure the runtask. +First step is to deploy the module into dedicated HCP Terraform workspace. The output `runtask_id` is used on other HCP Terraform workspace to configure the runtask. * Build and package the Lambda files using the makefile. Run this command from the root directory of this repository. @@ -20,7 +20,7 @@ First step is to deploy the module into dedicated Terraform Cloud workspace. The terraform { cloud { - # TODO: Change this to your Terraform Cloud org name. + # TODO: Change this to your HCP Terraform org name. organization = "" workspaces { ... @@ -30,17 +30,17 @@ First step is to deploy the module into dedicated Terraform Cloud workspace. The } ``` -* Initialize Terraform Cloud. When prompted, enter a new workspace name, i.e. `aws-ia2-infra` +* Initialize HCP Terraform. When prompted, enter a new workspace name, i.e. `aws-ia2-infra` ```bash terraform init ``` -* Configure the new workspace (i.e `aws-ia2-infra`) in Terraform Cloud to use `local` execution mode. Skip this if you publish the module into Terraform registry. +* Configure the new workspace (i.e `aws-ia2-infra`) in HCP Terraform to use `local` execution mode. Skip this if you publish the module into Terraform registry. * Configure the AWS credentials (`AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`) by using environment variables. -* In order to create and configure the run tasks, you also need to have Terraform Cloud token stored as Environment Variables. Add `TFE_HOSTNAME` and `TFE_TOKEN` environment variable. +* In order to create and configure the run tasks, you also need to have HCP Terraform token stored as Environment Variables. Add `TFE_HOSTNAME` and `TFE_TOKEN` environment variable. * Run Terraform apply diff --git a/examples/module_workspace/README.md b/examples/module_workspace/README.md index 855e3ce..618174d 100644 --- a/examples/module_workspace/README.md +++ b/examples/module_workspace/README.md @@ -1,9 +1,10 @@ # Usage Example -First step is to deploy the module into dedicated Terraform Cloud workspace. The output `runtask_id` is used on other Terraform Cloud workspace to configure the runtask. +First step is to deploy the module into dedicated HCP Terraform workspace. The output `runtask_id` is used on other HCP Terraform workspace to configure the runtask. * Build and package the Lambda files using the makefile. Run this command from the root directory of this repository. + ```bash make all ``` @@ -16,32 +17,34 @@ First step is to deploy the module into dedicated Terraform Cloud workspace. The * Change the org name to your TFC org. - ``` + ```hcl terraform { cloud { - # TODO: Change this to your Terraform Cloud org name. + # TODO: Change this to your HCP Terraform org name. organization = "" workspaces { ... } } ... - } + } ``` -* Initialize Terraform Cloud. When prompted, enter a new workspace name, i.e. `aws-ia2-infra` +* Initialize HCP Terraform. When prompted, enter a new workspace name, i.e. `aws-ia2-infra` + ```bash terraform init ``` -* Configure the new workspace (i.e `aws-ia2-infra`) in Terraform Cloud to use `local` execution mode. Skip this if you publish the module into Terraform registry. +* Configure the new workspace (i.e `aws-ia2-infra`) in HCP Terraform to use `local` execution mode. Skip this if you publish the module into Terraform registry. * Configure the AWS credentials (`AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`) by using environment variables. -* In order to create and configure the run tasks, you also need to have Terraform Cloud token stored as Environment Variables. Add `TFE_HOSTNAME` and `TFE_TOKEN` environment variable. +* In order to create and configure the run tasks, you also need to have HCP Terraform token stored as Environment Variables. Add `TFE_HOSTNAME` and `TFE_TOKEN` environment variable. * Run Terraform apply + ```bash terraform apply ``` @@ -54,15 +57,15 @@ First step is to deploy the module into dedicated Terraform Cloud workspace. The |------|---------| | [terraform](#requirement\_terraform) | >= 1.0.7 | | [archive](#requirement\_archive) | ~>2.2.0 | -| [aws](#requirement\_aws) | >= 3.73.0, < 5.0.0 | +| [aws](#requirement\_aws) | >=5.72.0 | | [random](#requirement\_random) | >=3.4.0 | -| [tfe](#requirement\_tfe) | ~>0.38.0 | +| [tfe](#requirement\_tfe) | >=0.38.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 3.73.0, < 5.0.0 | +| [aws](#provider\_aws) | >=5.72.0 | ## Modules diff --git a/examples/module_workspace/main.tf b/examples/module_workspace/main.tf index cced75e..7f0bd54 100644 --- a/examples/module_workspace/main.tf +++ b/examples/module_workspace/main.tf @@ -2,7 +2,7 @@ data "aws_region" "current" { } module "runtask_iam_access_analyzer" { - source = "../../" # set your Terraform Cloud workspace with Local execution mode to allow module reference like this + source = "../../" # set your HCP Terraform workspace with Local execution mode to allow module reference like this tfc_org = var.tfc_org aws_region = data.aws_region.current.name workspace_prefix = var.workspace_prefix diff --git a/examples/module_workspace/versions.tf b/examples/module_workspace/versions.tf index 7a8efc6..36ae81a 100644 --- a/examples/module_workspace/versions.tf +++ b/examples/module_workspace/versions.tf @@ -1,6 +1,6 @@ terraform { cloud { - # TODO: Change this to your Terraform Cloud org name. + # TODO: Change this to your HCP Terraform org name. organization = "wellsiau-org" workspaces { name = "TestExamplesLaunchModule" @@ -11,12 +11,12 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 3.73.0, < 5.0.0" + version = ">=5.72.0" } tfe = { source = "hashicorp/tfe" - version = "~>0.38.0" + version = ">=0.38.0" } random = { @@ -29,5 +29,4 @@ terraform { version = "~>2.2.0" } } -} - +} \ No newline at end of file diff --git a/iam.tf b/iam.tf index 570fcf4..c8184c3 100644 --- a/iam.tf +++ b/iam.tf @@ -1,7 +1,22 @@ +################# IAM for run task Lambda@Edge ################## +resource "aws_iam_role" "runtask_edge" { + name = "${var.name_prefix}-runtask-edge" + assume_role_policy = templatefile("${path.module}/iam/trust-policies/lambda_edge.tpl", { none = "none" }) + tags = local.combined_tags +} + +resource "aws_iam_role_policy_attachment" "runtask_edge" { + count = length(local.lambda_managed_policies) + role = aws_iam_role.runtask_edge.name + policy_arn = local.lambda_managed_policies[count.index] +} + + ################# RunTask EventBridge ################## resource "aws_iam_role" "runtask_eventbridge" { name = "${var.name_prefix}-runtask-eventbridge" assume_role_policy = templatefile("${path.module}/iam/trust-policies/lambda.tpl", { none = "none" }) + tags = local.combined_tags } resource "aws_iam_role_policy_attachment" "runtask_eventbridge" { @@ -26,6 +41,7 @@ resource "aws_iam_role_policy" "runtask_eventbridge" { resource "aws_iam_role" "runtask_request" { name = "${var.name_prefix}-runtask-request" assume_role_policy = templatefile("${path.module}/iam/trust-policies/lambda.tpl", { none = "none" }) + tags = local.combined_tags } resource "aws_iam_role_policy_attachment" "runtask_request" { @@ -38,6 +54,7 @@ resource "aws_iam_role_policy_attachment" "runtask_request" { resource "aws_iam_role" "runtask_callback" { name = "${var.name_prefix}-runtask-callback" assume_role_policy = templatefile("${path.module}/iam/trust-policies/lambda.tpl", { none = "none" }) + tags = local.combined_tags } resource "aws_iam_role_policy_attachment" "runtask_callback" { @@ -50,6 +67,7 @@ resource "aws_iam_role_policy_attachment" "runtask_callback" { resource "aws_iam_role" "runtask_fulfillment" { name = "${var.name_prefix}-runtask-fulfillment" assume_role_policy = templatefile("${path.module}/iam/trust-policies/lambda.tpl", { none = "none" }) + tags = local.combined_tags } resource "aws_iam_role_policy_attachment" "runtask_fulfillment" { @@ -73,6 +91,7 @@ resource "aws_iam_role_policy" "runtask_fulfillment" { resource "aws_iam_role" "runtask_states" { name = "${var.name_prefix}-runtask-statemachine" assume_role_policy = templatefile("${path.module}/iam/trust-policies/states.tpl", { none = "none" }) + tags = local.combined_tags } resource "aws_iam_role_policy" "runtask_states" { @@ -91,6 +110,7 @@ resource "aws_iam_role_policy" "runtask_states" { resource "aws_iam_role" "runtask_rule" { name = "${var.name_prefix}-runtask-rule" assume_role_policy = templatefile("${path.module}/iam/trust-policies/events.tpl", { none = "none" }) + tags = local.combined_tags } resource "aws_iam_role_policy" "runtask_rule" { diff --git a/iam/trust-policies/lambda_edge.tpl b/iam/trust-policies/lambda_edge.tpl new file mode 100644 index 0000000..60782e6 --- /dev/null +++ b/iam/trust-policies/lambda_edge.tpl @@ -0,0 +1,15 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com", + "edgelambda.amazonaws.com" + ] + }, + "Action": "sts:AssumeRole" + } + ] +} \ No newline at end of file diff --git a/kms.tf b/kms.tf index 063b9a0..0bb4c63 100644 --- a/kms.tf +++ b/kms.tf @@ -2,6 +2,7 @@ resource "aws_kms_key" "runtask_key" { description = "KMS key for Run Task integration" policy = data.aws_iam_policy_document.runtask_key.json enable_key_rotation = true + tags = local.combined_tags } # Assign an alias to the key @@ -16,6 +17,7 @@ resource "aws_kms_key" "runtask_waf" { description = "KMS key for WAF" policy = data.aws_iam_policy_document.runtask_waf[count.index].json enable_key_rotation = true + tags = local.combined_tags } # Assign an alias to the key diff --git a/lambda.tf b/lambda.tf index 49ca0a6..446aa50 100644 --- a/lambda.tf +++ b/lambda.tf @@ -1,8 +1,9 @@ ################# RunTask EventBridge ################## resource "aws_lambda_function" "runtask_eventbridge" { function_name = "${var.name_prefix}-runtask-eventbridge" - description = "Terraform Cloud Run Task - EventBridge handler" + description = "HCP Terraform Run Task - EventBridge handler" role = aws_iam_role.runtask_eventbridge.arn + architectures = local.lambda_architecture source_code_hash = data.archive_file.runtask_eventbridge.output_base64sha256 filename = data.archive_file.runtask_eventbridge.output_path handler = "handler.lambda_handler" @@ -10,17 +11,19 @@ resource "aws_lambda_function" "runtask_eventbridge" { timeout = local.lambda_default_timeout environment { variables = { - TFC_HMAC_SECRET_ARN = aws_secretsmanager_secret.runtask_hmac.arn - TFC_USE_WAF = var.deploy_waf ? "True" : "False" - TFC_CF_SECRET_ARN = var.deploy_waf ? aws_secretsmanager_secret.runtask_cloudfront[0].arn : null - TFC_CF_SIGNATURE = var.deploy_waf ? local.cloudfront_sig_name : null - EVENT_BUS_NAME = var.event_bus_name + HCP_TF_HMAC_SECRET_ARN = aws_secretsmanager_secret.runtask_hmac.arn + HCP_TF_USE_WAF = var.deploy_waf ? "True" : "False" + HCP_TF_CF_SECRET_ARN = var.deploy_waf ? aws_secretsmanager_secret.runtask_cloudfront[0].arn : null + HCP_TF_CF_SIGNATURE = var.deploy_waf ? local.cloudfront_sig_name : null + EVENT_BUS_NAME = var.event_bus_name + EVENT_RULE_DETAIL_TYPE = local.solution_prefix # ensure uniqueness of event sent to each runtask state machine } } tracing_config { mode = "Active" } reserved_concurrent_executions = local.lambda_reserved_concurrency + tags = local.combined_tags #checkov:skip=CKV_AWS_116:not using DLQ #checkov:skip=CKV_AWS_117:VPC is not required #checkov:skip=CKV_AWS_173:non sensitive environment variables @@ -29,21 +32,31 @@ resource "aws_lambda_function" "runtask_eventbridge" { resource "aws_lambda_function_url" "runtask_eventbridge" { function_name = aws_lambda_function.runtask_eventbridge.function_name - authorization_type = "NONE" - #checkov:skip=CKV_AWS_258:auth set to none, validation hmac inside the lambda code + authorization_type = "AWS_IAM" +} + +resource "aws_lambda_permission" "runtask_eventbridge" { + count = local.waf_deployment + statement_id = "AllowCloudFrontToFunctionUrl" + action = "lambda:InvokeFunctionUrl" + function_name = aws_lambda_function.runtask_eventbridge.function_name + principal = "cloudfront.amazonaws.com" + source_arn = module.runtask_cloudfront[count.index].cloudfront_distribution_arn } resource "aws_cloudwatch_log_group" "runtask_eventbridge" { name = "/aws/lambda/${aws_lambda_function.runtask_eventbridge.function_name}" retention_in_days = var.cloudwatch_log_group_retention kms_key_id = aws_kms_key.runtask_key.arn + tags = local.combined_tags } ################# RunTask Request ################## resource "aws_lambda_function" "runtask_request" { function_name = "${var.name_prefix}-runtask-request" - description = "Terraform Cloud Run Task - Request handler" + description = "HCP Terraform Run Task - Request handler" role = aws_iam_role.runtask_request.arn + architectures = local.lambda_architecture source_code_hash = data.archive_file.runtask_request.output_base64sha256 filename = data.archive_file.runtask_request.output_path handler = "handler.lambda_handler" @@ -55,11 +68,13 @@ resource "aws_lambda_function" "runtask_request" { reserved_concurrent_executions = local.lambda_reserved_concurrency environment { variables = { - TFC_ORG = var.tfc_org - RUNTASK_STAGES = join(",", var.runtask_stages) - WORKSPACE_PREFIX = length(var.workspace_prefix) > 0 ? var.workspace_prefix : null + HCP_TF_ORG = var.tfc_org + RUNTASK_STAGES = join(",", var.runtask_stages) + WORKSPACE_PREFIX = length(var.workspace_prefix) > 0 ? var.workspace_prefix : null + EVENT_RULE_DETAIL_TYPE = local.solution_prefix # ensure uniqueness of event sent to each runtask state machine } } + tags = local.combined_tags #checkov:skip=CKV_AWS_116:not using DLQ #checkov:skip=CKV_AWS_117:VPC is not required #checkov:skip=CKV_AWS_173:no sensitive data in env var @@ -75,8 +90,9 @@ resource "aws_cloudwatch_log_group" "runtask_request" { ################# RunTask Callback ################## resource "aws_lambda_function" "runtask_callback" { function_name = "${var.name_prefix}-runtask-callback" - description = "Terraform Cloud Run Task - Callback handler" + description = "HCP Terraform Run Task - Callback handler" role = aws_iam_role.runtask_callback.arn + architectures = local.lambda_architecture source_code_hash = data.archive_file.runtask_callback.output_base64sha256 filename = data.archive_file.runtask_callback.output_path handler = "handler.lambda_handler" @@ -86,6 +102,7 @@ resource "aws_lambda_function" "runtask_callback" { mode = "Active" } reserved_concurrent_executions = local.lambda_reserved_concurrency + tags = local.combined_tags #checkov:skip=CKV_AWS_116:not using DLQ #checkov:skip=CKV_AWS_117:VPC is not required #checkov:skip=CKV_AWS_272:skip code-signing @@ -95,13 +112,15 @@ resource "aws_cloudwatch_log_group" "runtask_callback" { name = "/aws/lambda/${aws_lambda_function.runtask_callback.function_name}" retention_in_days = var.cloudwatch_log_group_retention kms_key_id = aws_kms_key.runtask_key.arn + tags = local.combined_tags } ################# RunTask Fulfillment ################## resource "aws_lambda_function" "runtask_fulfillment" { function_name = "${var.name_prefix}-runtask-fulfillment" - description = "Terraform Cloud Run Task - Fulfillment handler" + description = "HCP Terraform Run Task - Fulfillment handler" role = aws_iam_role.runtask_fulfillment.arn + architectures = local.lambda_architecture source_code_hash = data.archive_file.runtask_fulfillment.output_base64sha256 filename = data.archive_file.runtask_fulfillment.output_path handler = "handler.lambda_handler" @@ -117,6 +136,7 @@ resource "aws_lambda_function" "runtask_fulfillment" { SUPPORTED_POLICY_DOCUMENT = length(var.supported_policy_document) > 0 ? var.supported_policy_document : null } } + tags = local.combined_tags #checkov:skip=CKV_AWS_116:not using DLQ #checkov:skip=CKV_AWS_117:VPC is not required #checkov:skip=CKV_AWS_173:no sensitive data in env var @@ -127,10 +147,35 @@ resource "aws_cloudwatch_log_group" "runtask_fulfillment" { name = "/aws/lambda/${aws_lambda_function.runtask_fulfillment.function_name}" retention_in_days = var.cloudwatch_log_group_retention kms_key_id = aws_kms_key.runtask_key.arn + tags = local.combined_tags } resource "aws_cloudwatch_log_group" "runtask_fulfillment_output" { name = local.cloudwatch_log_group_name retention_in_days = var.cloudwatch_log_group_retention kms_key_id = aws_kms_key.runtask_key.arn -} \ No newline at end of file + tags = local.combined_tags +} + + +################# Run task Edge ################## +resource "aws_lambda_function" "runtask_edge" { + provider = aws.cloudfront_waf # Lambda@Edge must be in us-east-1 + function_name = "${var.name_prefix}-runtask-edge" + description = "HCP Terraform run task - Lambda@Edge handler" + role = aws_iam_role.runtask_edge.arn + architectures = local.lambda_architecture + source_code_hash = data.archive_file.runtask_edge.output_base64sha256 + filename = data.archive_file.runtask_edge.output_path + handler = "handler.lambda_handler" + runtime = local.lambda_python_runtime + timeout = 5 # Lambda@Edge max timout is 5 + reserved_concurrent_executions = local.lambda_reserved_concurrency + publish = true # Lambda@Edge must be published + tags = local.combined_tags + #checkov:skip=CKV_AWS_116:not using DLQ + #checkov:skip=CKV_AWS_117:VPC is not required + #checkov:skip=CKV_AWS_173:no sensitive data in env var + #checkov:skip=CKV_AWS_272:skip code-signing + #checkov:skip=CKV_AWS_50:no x-ray for lambda@edge +} diff --git a/lambda/runtask_callback/handler.py b/lambda/runtask_callback/handler.py index 22f3eb8..acbcac6 100644 --- a/lambda/runtask_callback/handler.py +++ b/lambda/runtask_callback/handler.py @@ -20,25 +20,20 @@ import re from urllib.request import urlopen, Request from urllib.error import HTTPError, URLError -from urllib.parse import urlencode logger = logging.getLogger() -if 'log_level' in os.environ: - logger.setLevel(os.environ['log_level']) - logger.info("Log level set to %s" % logger.getEffectiveLevel()) -else: - logger.setLevel(logging.INFO) +log_level = os.environ.get("log_level", logging.INFO) -if "TFC_HOST_NAME" in os.environ: - TFC_HOST_NAME = os.environ["TFC_HOST_NAME"] -else: - TFC_HOST_NAME = "app.terraform.io" +logger.setLevel(log_level) +logger.info("Log level set to %s" % logger.getEffectiveLevel()) + +HCP_TF_HOST_NAME = os.environ.get("HCP_TF_HOST_NAME", "app.terraform.io") def lambda_handler(event, context): logger.debug(json.dumps(event)) try: # trim empty url from the payload - if event["payload"]["result"]["fulfillment"]["url"] == False: + if "fulfillment" in event["payload"]["result"] and event["payload"]["result"]["fulfillment"]["url"] == False: event["payload"]["result"]["fulfillment"].pop("url") if event["payload"]["result"]["request"]["status"] == "unverified": # unverified runtask execution @@ -46,7 +41,7 @@ def lambda_handler(event, context): "data": { "attributes": { "status": "failed", - "message": "Verification failed, check TFC org, workspace prefix or Runtasks stage", + "message": "Verification failed, check HCP Terraform org, workspace prefix or Runtasks stage", }, "type": "task-results", } @@ -71,12 +66,12 @@ def lambda_handler(event, context): logger.info("Payload : {}".format(json.dumps(payload))) - # Send runtask callback response to TFC + # Send runtask callback response to HCP Terraform endpoint = event["payload"]["detail"]["task_result_callback_url"] access_token = event["payload"]["detail"]["access_token"] headers = __build_standard_headers(access_token) response = __patch(endpoint, headers, bytes(json.dumps(payload), encoding="utf-8")) - logger.debug("TFC response: {}".format(response)) + logger.debug("HCP Terraform response: {}".format(response)) return "completed" except Exception as e: @@ -96,7 +91,7 @@ def __patch(endpoint, headers, payload): with urlopen(request, timeout=10) as response: #nosec URL validation return response.read(), response else: - raise URLError("Invalid endpoint URL, expected host is: {}".format(TFC_HOST_NAME)) + raise URLError("Invalid endpoint URL, expected host is: {}".format(HCP_TF_HOST_NAME)) except HTTPError as error: logger.error(error.status, error.reason) except URLError as error: @@ -105,6 +100,6 @@ def __patch(endpoint, headers, payload): logger.error("Request timed out") def validate_endpoint(endpoint): # validate that the endpoint hostname is valid - pattern = "^https:\/\/" + str(TFC_HOST_NAME).replace(".", "\.") + "\/"+ ".*" + pattern = "^https:\/\/" + str(HCP_TF_HOST_NAME).replace(".", "\.") + "\/"+ ".*" result = re.match(pattern, endpoint) return result \ No newline at end of file diff --git a/lambda/runtask_edge/Makefile b/lambda/runtask_edge/Makefile new file mode 100644 index 0000000..188e1a6 --- /dev/null +++ b/lambda/runtask_edge/Makefile @@ -0,0 +1,19 @@ +PROJECT = $(CURDIR) + +all: build + +.PHONY: clean build + +clean: + rm -rf build + rm -rf site-packages + +build: + $(info ************ Starting Build: $(PROJECT) ************) + mkdir -p site-packages + cp *.* ./site-packages + mkdir -p build + python3 -m venv build/ + . build/bin/activate; \ + pip3 install -r requirements.txt -t ./site-packages; + rm -rf build \ No newline at end of file diff --git a/lambda/runtask_edge/handler.py b/lambda/runtask_edge/handler.py new file mode 100644 index 0000000..9bcc4b6 --- /dev/null +++ b/lambda/runtask_edge/handler.py @@ -0,0 +1,41 @@ +import base64 +import hashlib +import json +import logging +import os + +logger = logging.getLogger() +log_level = os.environ.get("log_level", logging.INFO) + +logger.setLevel(log_level) +logger.info("Log level set to %s" % logger.getEffectiveLevel()) + + +def lambda_handler(event, _): + logger.info("Incoming event : {}".format(json.dumps(event))) + request = event['Records'][0]['cf']['request'] + headers = request["headers"] + headerName = 'x-amz-content-sha256' + + ''' + CloudFront Origin Access Control will not automatically calculate the payload hash. + this Lambda@Edge will calculate the payload hash and append new header x-amz-content-sha256 + ''' + payload_body = decode_body(request['body']['data']) + logger.debug("Payload : {}".format(payload_body)) + payload_hash = calculate_payload_hash(payload_body) + + # inject new header + headers[headerName] = [{'key': headerName, 'value': payload_hash}] + + logger.info("Returning request: %s" % json.dumps(request)) + return request + + +def decode_body(encoded_body): + return base64.b64decode(encoded_body).decode('utf-8') + + +def calculate_payload_hash(payload): + ## generate sha256 from payload + return hashlib.sha256(payload.encode('utf-8')).hexdigest() \ No newline at end of file diff --git a/main.tf b/lambda/runtask_edge/requirements.txt similarity index 100% rename from main.tf rename to lambda/runtask_edge/requirements.txt diff --git a/lambda/runtask_eventbridge/handler.py b/lambda/runtask_eventbridge/handler.py index 40c5203..e373d89 100644 --- a/lambda/runtask_eventbridge/handler.py +++ b/lambda/runtask_eventbridge/handler.py @@ -1,4 +1,4 @@ -''' +""" Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of @@ -13,118 +13,129 @@ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -''' +""" -"""HashiCorp Terraform Cloud RunTask event handler implementation""" +"""HCP Terraform run task event handler implementation""" import os +import hmac import json -import urllib.parse import base64 -import hmac import hashlib import logging -from cgi import parse_header +import urllib.parse import boto3 -import botocore import botocore.session + +from cgi import parse_header from aws_secretsmanager_caching import SecretCache, SecretCacheConfig -client = botocore.session.get_session().create_client('secretsmanager') +client = botocore.session.get_session().create_client("secretsmanager") cache_config = SecretCacheConfig() cache = SecretCache(config=cache_config, client=client) -logger = logging.getLogger() -if 'log_level' in os.environ: - logger.setLevel(os.environ['log_level']) - logger.info("Log level set to %s" % logger.getEffectiveLevel()) -else: - logger.setLevel(logging.INFO) - -if "TFC_HMAC_SECRET_ARN" in os.environ: - tfc_hmac_secret_arn = os.environ.get('TFC_HMAC_SECRET_ARN') - -if "TFC_USE_WAF" in os.environ: - tfc_use_waf = os.environ.get('TFC_USE_WAF') +hcp_tf_hmac_secret_arn = os.environ.get("HCP_TF_HMAC_SECRET_ARN") +hcp_tf_use_waf = os.environ.get("HCP_TF_USE_WAF") +hcp_tf_cf_secret_arn = os.environ.get("HCP_TF_CF_SECRET_ARN") +hcp_tf_cf_signature = os.environ.get("HCP_TF_CF_SIGNATURE") -if "TFC_CF_SECRET_ARN" in os.environ: - tfc_cf_secret_arn = os.environ.get('TFC_CF_SECRET_ARN') - -if "TFC_CF_SIGNATURE" in os.environ: - tfc_cf_signature = os.environ.get('TFC_CF_SIGNATURE') +logger = logging.getLogger() +log_level = os.environ.get("log_level", logging.INFO) -event_bus_name = os.environ.get('EVENT_BUS_NAME', 'default') +logger.setLevel(log_level) +logger.info("Log level set to %s" % logger.getEffectiveLevel()) -event_bridge_client = boto3.client('events') +event_bus_name = os.environ.get("EVENT_BUS_NAME", "default") +event_rule_detail_type = os.environ.get("EVENT_RULE_DETAIL_TYPE", "aws-ia2") # assume there could be multiple deployment of this module, this will ensure each rule are unique +event_bridge_client = boto3.client("events") +## Add user-agent to event-bridge event def _add_header(request, **kwargs): - userAgentHeader = request.headers['User-Agent'] + ' fURLWebhook/1.0 (HashiCcorp)' - del request.headers['User-Agent'] - request.headers['User-Agent'] = userAgentHeader + userAgentHeader = request.headers["User-Agent"] + " fURLWebhook/1.0 (HashiCorp)" + del request.headers["User-Agent"] + request.headers["User-Agent"] = userAgentHeader +## Add user-agent to event-bridge event event_system = event_bridge_client.meta.events -event_system.register_first('before-sign.events.PutEvents', _add_header) +event_system.register_first("before-sign.events.PutEvents", _add_header) class PutEventError(Exception): """Raised when Put Events Failed""" pass -def lambda_handler(event, _context): - """RunTask function""" +def lambda_handler(event, _): + """Terraform run task function""" logger.debug(json.dumps(event)) - - headers = event.get('headers') + + headers = event.get("headers") # Input validation try: json_payload = get_json_payload(event=event) except ValueError as err: - print_error(f'400 Bad Request - {err}', headers) - return {'statusCode': 400, 'body': str(err)} + print_error(f"400 Bad Request - {err}", headers) + return {"statusCode": 400, "body": str(err)} except BaseException as err: # Unexpected Error - print_error('500 Internal Server Error\n' + - f'Unexpected error: {err}, {type(err)}', headers) - return {'statusCode': 500, 'body': 'Internal Server Error'} + print_error( + "500 Internal Server Error\n" + f"Unexpected error: {err}, {type(err)}", + headers, + ) + return {"statusCode": 500, "body": "Internal Server Error"} - detail_type = 'hashicorp-tfc-runtask' try: - if tfc_use_waf == "True" and not contains_valid_cloudfront_signature(event=event): - print_error('401 Unauthorized - Invalid CloudFront Signature', headers) - return {'statusCode': 401, 'body': 'Invalid CloudFront Signature'} + if hcp_tf_use_waf == "True" and not contains_valid_cloudfront_signature( + event=event + ): + print_error("401 Unauthorized - Invalid CloudFront Signature", headers) + return {"statusCode": 401, "body": "Invalid CloudFront Signature"} if not contains_valid_signature(event=event): - print_error('401 Unauthorized - Invalid Payload Signature', headers) - return {'statusCode': 401, 'body': 'Invalid Payload Signature'} + print_error("401 Unauthorized - Invalid Payload Signature", headers) + return {"statusCode": 401, "body": "Invalid Payload Signature"} - response = forward_event(json_payload, detail_type) + response = forward_event(json_payload, event_rule_detail_type) - if response['FailedEntryCount'] > 0: - print_error('500 FailedEntry Error - The event was not successfully forwarded to Amazon EventBridge\n' + - str(response['Entries'][0]), headers) - return {'statusCode': 500, 'body': 'FailedEntry Error - The entry could not be succesfully forwarded to Amazon EventBridge'} + if response["FailedEntryCount"] > 0: + print_error( + "500 FailedEntry Error - The event was not successfully forwarded to Amazon EventBridge\n" + + str(response["Entries"][0]), + headers, + ) + return { + "statusCode": 500, + "body": "FailedEntry Error - The entry could not be successfully forwarded to Amazon EventBridge", + } - return {'statusCode': 202, 'body': 'Message forwarded to Amazon EventBridge'} + return {"statusCode": 200, "body": "Message forwarded to Amazon EventBridge"} except PutEventError as err: - print_error(f'500 Put Events Error - {err}', headers) - return {'statusCode': 500, 'body': 'Internal Server Error - The request was rejected by Amazon EventBridge API'} + print_error(f"500 Put Events Error - {err}", headers) + return { + "statusCode": 500, + "body": "Internal Server Error - The request was rejected by Amazon EventBridge API", + } except BaseException as err: # Unexpected Error - print_error('500 Internal Server Error\n' + - f'Unexpected error: {err}, {type(err)}', headers) - return {'statusCode': 500, 'body': 'Internal Server Error'} + print_error( + "500 Internal Server Error\n" + f"Unexpected error: {err}, {type(err)}", + headers, + ) + return {"statusCode": 500, "body": "Internal Server Error"} def normalize_payload(raw_payload, is_base64_encoded): """Decode payload if needed""" if raw_payload is None: - raise ValueError('Missing event body') + raise ValueError("Missing event body") if is_base64_encoded: - return base64.b64decode(raw_payload).decode('utf-8') + return base64.b64decode(raw_payload).decode("utf-8") return raw_payload -def contains_valid_cloudfront_signature(event): # Check for the special header value from CloudFront + +def contains_valid_cloudfront_signature( + event, +): # Check for the special header value from CloudFront try: - secret = cache.get_secret_string(tfc_cf_secret_arn) + secret = cache.get_secret_string(hcp_tf_cf_secret_arn) payload_signature = event["headers"]["x-cf-sig"] if secret == payload_signature: return True @@ -134,17 +145,20 @@ def contains_valid_cloudfront_signature(event): # Check for the special header v logger.error("Unable to validate CloudFront custom header signature value") return False + def contains_valid_signature(event): """Check for the payload signature - HashiCorp Terraform Run Task documention: https://developer.hashicorp.com/terraform/cloud-docs/integrations/run-tasks#securing-your-run-task + HashiCorp Terraform run task documentation: https://developer.hashicorp.com/terraform/cloud-docs/integrations/run-tasks#securing-your-run-task """ - secret = cache.get_secret_string(tfc_hmac_secret_arn) + secret = cache.get_secret_string(hcp_tf_hmac_secret_arn) payload_bytes = get_payload_bytes( - raw_payload=event['body'], is_base64_encoded=event['isBase64Encoded']) - computed_signature = compute_signature( - payload_bytes=payload_bytes, secret=secret) + raw_payload=event["body"], is_base64_encoded=event["isBase64Encoded"] + ) + computed_signature = compute_signature(payload_bytes=payload_bytes, secret=secret) - return hmac.compare_digest(event['headers'].get('x-tfc-task-signature', ''), computed_signature) + return hmac.compare_digest( + event["headers"].get("x-tfc-task-signature", ""), computed_signature + ) def get_payload_bytes(raw_payload, is_base64_encoded): @@ -157,57 +171,59 @@ def get_payload_bytes(raw_payload, is_base64_encoded): def compute_signature(payload_bytes, secret): """Compute HMAC-SHA512""" - m = hmac.new(key=secret.encode(), msg=payload_bytes, - digestmod=hashlib.sha512) + m = hmac.new(key=secret.encode(), msg=payload_bytes, digestmod=hashlib.sha512) return m.hexdigest() def get_json_payload(event): """Get JSON string from payload""" - content_type = get_content_type(event.get('headers', {})) - if not (content_type == 'application/json' or - content_type == 'application/x-www-form-urlencoded'): - raise ValueError('Unsupported content-type') + content_type = get_content_type(event.get("headers", {})) + if not ( + content_type == "application/json" + or content_type == "application/x-www-form-urlencoded" + ): + raise ValueError("Unsupported content-type") payload = normalize_payload( - raw_payload=event.get('body'), - is_base64_encoded=event['isBase64Encoded']) + raw_payload=event.get("body"), is_base64_encoded=event["isBase64Encoded"] + ) - if content_type == 'application/x-www-form-urlencoded': + if content_type == "application/x-www-form-urlencoded": parsed_qs = urllib.parse.parse_qs(payload) - if 'payload' not in parsed_qs or len(parsed_qs['payload']) != 1: - raise ValueError('Invalid urlencoded payload') + if "payload" not in parsed_qs or len(parsed_qs["payload"]) != 1: + raise ValueError("Invalid urlencoded payload") - payload = parsed_qs['payload'][0] + payload = parsed_qs["payload"][0] try: json.loads(payload) except ValueError as err: - raise ValueError('Invalid JSON payload') from err + raise ValueError("Invalid JSON payload") from err return payload def forward_event(payload, detail_type): """Forward event to EventBridge""" - try : + try: return event_bridge_client.put_events( Entries=[ { - 'Source': 'app.terraform.io', - 'DetailType': detail_type, - 'Detail': payload, - 'EventBusName': event_bus_name + "Source": "app.terraform.io", + "DetailType": detail_type, + "Detail": payload, + "EventBusName": event_bus_name, }, ] ) except BaseException as err: - raise PutEventError('Put Events Failed') + raise PutEventError("Put Events Failed") + def get_content_type(headers): """Helper function to parse content-type from the header""" - raw_content_type = headers.get('content-type') + raw_content_type = headers.get("content-type") if raw_content_type is None: return None @@ -217,4 +233,4 @@ def get_content_type(headers): def print_error(message, headers): """Helper function to print errors""" - logger.error(f'ERROR: {message}\nHeaders: {str(headers)}') + logger.error(f"ERROR: {message}\nHeaders: {str(headers)}") \ No newline at end of file diff --git a/lambda/runtask_fulfillment/handler.py b/lambda/runtask_fulfillment/handler.py index 5ee2868..2e052b2 100644 --- a/lambda/runtask_fulfillment/handler.py +++ b/lambda/runtask_fulfillment/handler.py @@ -34,21 +34,18 @@ iamConfigMap = {} # map of terraform plan attribute and IAM access analyzer resource type, loaded from default.yaml -if "log_level" in os.environ: - logger.setLevel(os.environ["log_level"]) - logger.info("Log level set to %s" % logger.getEffectiveLevel()) -else: - logger.setLevel(logging.INFO) +log_level = os.environ.get("log_level", logging.INFO) + +logger = logging.getLogger() +logger.setLevel(log_level) if "SUPPORTED_POLICY_DOCUMENT" in os.environ: SUPPORTED_POLICY_DOCUMENT = os.environ["SUPPORTED_POLICY_DOCUMENT"] else: SUPPORTED_POLICY_DOCUMENT = False # default to False and then load it from config file default.yaml -if "TFC_HOST_NAME" in os.environ: - TFC_HOST_NAME = os.environ["TFC_HOST_NAME"] -else: - TFC_HOST_NAME = "app.terraform.io" +HCP_TF_HOST_NAME = os.environ.get("HCP_TF_HOST_NAME", "app.terraform.io") + IAM_ACCESS_ANALYZER_COUNTER = { "ERROR" : 0, @@ -69,7 +66,7 @@ def lambda_handler(event, context): try: if not iamConfigMap: load_config("default.yaml") # load the config file - # Get plan output from Terraform Cloud + # Get plan output from HCP Terraform endpoint = event["payload"]["detail"]["plan_json_api_url"] access_token = event["payload"]["detail"]["access_token"] headers = __build_standard_headers(access_token) @@ -95,7 +92,7 @@ def lambda_handler(event, context): fulfillment_response = fulfillment_response_helper(total_ia2_violation_count, skip_log = False) # generate response else: logger.info("No resource changes detected") - fulfillment_response = fulfillment_response_helper(total_ia2_violation_count = {}, skip_log = True, override_message = "No resource changes detected", overrise_status = "passed") # override response + fulfillment_response = fulfillment_response_helper(total_ia2_violation_count = {}, skip_log = True, override_message = "No resource changes detected", override_status = "passed") # override response return fulfillment_response @@ -310,7 +307,7 @@ def __get(endpoint, headers): # HTTP request helper function with urlopen(request, timeout=10) as response: #nosec URL validation return response.read(), response else: - raise URLError("Invalid endpoint URL, expected host is: {}".format(TFC_HOST_NAME)) + raise URLError("Invalid endpoint URL, expected host is: {}".format(HCP_TF_HOST_NAME)) except HTTPError as error: logger.error(error.status, error.reason) except URLError as error: @@ -319,7 +316,7 @@ def __get(endpoint, headers): # HTTP request helper function logger.error("Request timed out") def validate_endpoint(endpoint): # validate that the endpoint hostname is valid - pattern = "^https:\/\/" + str(TFC_HOST_NAME).replace(".", "\.") + "\/"+ ".*" + pattern = "^https:\/\/" + str(HCP_TF_HOST_NAME).replace(".", "\.") + "\/"+ ".*" result = re.match(pattern, endpoint) return result diff --git a/lambda/runtask_request/handler.py b/lambda/runtask_request/handler.py index 4773e16..70c37a1 100644 --- a/lambda/runtask_request/handler.py +++ b/lambda/runtask_request/handler.py @@ -1,4 +1,4 @@ -''' +""" Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of @@ -13,54 +13,65 @@ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -''' +""" + import json import logging import os -from time import sleep -if "TFC_ORG" in os.environ: - TFC_ORG = os.environ["TFC_ORG"] -else: - TFC_ORG = False +HCP_TF_ORG = os.environ.get("HCP_TF_ORG", False) +WORKSPACE_PREFIX = os.environ.get("WORKSPACE_PREFIX", False) +RUNTASK_STAGES = os.environ.get("RUNTASK_STAGES", False) +EVENT_RULE_DETAIL_TYPE = os.environ.get("EVENT_RULE_DETAIL_TYPE", "aws-ia2") # assume there could be multiple deployment of this module, this will ensure each rule are unique -if "WORKSPACE_PREFIX" in os.environ: - WORKSPACE_PREFIX = os.environ["WORKSPACE_PREFIX"] -else: - WORKSPACE_PREFIX = False +logger = logging.getLogger() +log_level = os.environ.get("log_level", logging.INFO) -if "RUNTASK_STAGES" in os.environ: - RUNTASK_STAGES = os.environ["RUNTASK_STAGES"] -else: - RUNTASK_STAGES = False +logger.setLevel(log_level) +logger.info("Log level set to %s" % logger.getEffectiveLevel()) -logger = logging.getLogger() -if 'log_level' in os.environ: - logger.setLevel(os.environ['log_level']) - logger.info("Log level set to %s" % logger.getEffectiveLevel()) -else: - logger.setLevel(logging.INFO) -def lambda_handler(event, context): +def lambda_handler(event, _): logger.debug(json.dumps(event)) try: VERIFY = True - if event["payload"]["detail-type"] == "hashicorp-tfc-runtask": - if TFC_ORG and event["payload"]["detail"]["organization_name"] != TFC_ORG: - logger.error("TFC Org verification failed : {}".format(event["payload"]["detail"]["organization_name"])) + if event["payload"]["detail-type"] == EVENT_RULE_DETAIL_TYPE: + if ( + HCP_TF_ORG + and event["payload"]["detail"]["organization_name"] != HCP_TF_ORG + ): + logger.error( + "HCP Terraform Org verification failed : {}".format( + event["payload"]["detail"]["organization_name"] + ) + ) VERIFY = False - if WORKSPACE_PREFIX and not (str(event["payload"]["detail"]["workspace_name"]).startswith(WORKSPACE_PREFIX)): - logger.error("TFC workspace prefix verification failed : {}".format(event["payload"]["detail"]["workspace_name"])) + if WORKSPACE_PREFIX and not ( + str(event["payload"]["detail"]["workspace_name"]).startswith( + WORKSPACE_PREFIX + ) + ): + logger.error( + "HCP Terraform workspace prefix verification failed : {}".format( + event["payload"]["detail"]["workspace_name"] + ) + ) VERIFY = False - if RUNTASK_STAGES and not (event["payload"]["detail"]["stage"] in RUNTASK_STAGES): - logger.error("TFC Runtask stage verification failed: {}".format(event["payload"]["detail"]["stage"])) + if RUNTASK_STAGES and not ( + event["payload"]["detail"]["stage"] in RUNTASK_STAGES + ): + logger.error( + "HCP Terraform run task stage verification failed: {}".format( + event["payload"]["detail"]["stage"] + ) + ) VERIFY = False if VERIFY: return "verified" else: return "unverified" - + except Exception as e: logger.exception("Run Task Request error: {}".format(e)) - raise + raise \ No newline at end of file diff --git a/locals.tf b/locals.tf index 4c07beb..7727208 100644 --- a/locals.tf +++ b/locals.tf @@ -1,8 +1,11 @@ locals { + solution_prefix = "${var.name_prefix}-${random_string.solution_prefix.result}" + lambda_managed_policies = [data.aws_iam_policy.aws_lambda_basic_execution_role.arn] lambda_reserved_concurrency = var.lambda_reserved_concurrency lambda_default_timeout = var.lambda_default_timeout - lambda_python_runtime = "python3.9" + lambda_python_runtime = "python3.11" + lambda_architecture = [var.lambda_architecture] cloudwatch_log_group_name = var.cloudwatch_log_group_name @@ -14,4 +17,18 @@ locals { name = local.cloudfront_sig_name value = var.deploy_waf ? aws_secretsmanager_secret_version.runtask_cloudfront[0].secret_string : null } -} \ No newline at end of file + + combined_tags = merge( + var.tags, + { + Solution = local.solution_prefix + } + ) + +} + +resource "random_string" "solution_prefix" { + length = 4 + special = false + upper = false +} diff --git a/outputs.tf b/outputs.tf index 4621b73..30c58a0 100644 --- a/outputs.tf +++ b/outputs.tf @@ -6,10 +6,10 @@ output "runtask_hmac" { output "runtask_url" { value = var.deploy_waf ? "https://${module.runtask_cloudfront[0].cloudfront_distribution_domain_name}" : trim(aws_lambda_function_url.runtask_eventbridge.function_url, "/") - description = "The Run Tasks URL endpoint, you can use this to configure the Run Task setup in Terraform Cloud" + description = "The Run Tasks URL endpoint, you can use this to configure the Run Task setup in HCP Terraform" } output "runtask_id" { value = tfe_organization_run_task.aws_iam_analyzer.id - description = "The Run Tasks id configured in Terraform Cloud" + description = "The Run Tasks id configured in HCP Terraform" } \ No newline at end of file diff --git a/secrets.tf b/secrets.tf index 4190a0c..bec73fc 100644 --- a/secrets.tf +++ b/secrets.tf @@ -5,6 +5,7 @@ resource "aws_secretsmanager_secret" "runtask_hmac" { name = "${var.name_prefix}-runtask-hmac" recovery_window_in_days = var.recovery_window kms_key_id = aws_kms_key.runtask_key.arn + tags = local.combined_tags } resource "aws_secretsmanager_secret_version" "runtask_hmac" { @@ -22,10 +23,11 @@ resource "aws_secretsmanager_secret" "runtask_cloudfront" { name = "${var.name_prefix}-runtask_cloudfront" recovery_window_in_days = var.recovery_window kms_key_id = aws_kms_key.runtask_key.arn + tags = local.combined_tags } resource "aws_secretsmanager_secret_version" "runtask_cloudfront" { count = local.waf_deployment secret_id = aws_secretsmanager_secret.runtask_cloudfront[count.index].id secret_string = random_uuid.runtask_cloudfront[count.index].result -} \ No newline at end of file +} diff --git a/states.tf b/states.tf index 638bcbf..c3ab1ba 100644 --- a/states.tf +++ b/states.tf @@ -16,10 +16,12 @@ resource "aws_sfn_state_machine" "runtask_states" { tracing_configuration { enabled = true } + tags = local.combined_tags } resource "aws_cloudwatch_log_group" "runtask_states" { - name = "/aws/state/${var.name_prefix}-runtask-statemachine" + name = "/aws/vendedlogs/states/${var.name_prefix}-runtask-statemachine" retention_in_days = var.cloudwatch_log_group_retention kms_key_id = aws_kms_key.runtask_key.arn + tags = local.combined_tags } \ No newline at end of file diff --git a/states/runtask_states.asl.json b/states/runtask_states.asl.json index c4c3d1b..7f3207b 100644 --- a/states/runtask_states.asl.json +++ b/states/runtask_states.asl.json @@ -1,5 +1,5 @@ { - "Comment": "Terraform Cloud - Run Task Handler Demo", + "Comment": "HCP Terraform - Run Task Handler Demo", "StartAt": "runtask_request", "States": { "runtask_request": { diff --git a/variables.tf b/variables.tf index ea9cbe5..21e1a8b 100644 --- a/variables.tf +++ b/variables.tf @@ -119,4 +119,20 @@ variable "waf_managed_rule_set" { metric_suffix = "bad_input" } ] +} + +variable "tags" { + description = "Map of tags to apply to resources deployed by this solution." + type = map(any) + default = null +} + +variable "lambda_architecture" { + description = "Lambda architecture (arm64 or x86_64)" + type = string + default = "x86_64" + validation { + condition = contains(["arm64", "x86_64"], var.lambda_architecture) + error_message = "Valid values for var: lambda_architecture are arm64 or x86_64" + } } \ No newline at end of file diff --git a/versions.tf b/versions.tf index 2be4fc0..e2d5eeb 100644 --- a/versions.tf +++ b/versions.tf @@ -3,12 +3,12 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 3.73.0, < 5.0.0" + version = ">=5.72.0" } tfe = { source = "hashicorp/tfe" - version = "~>0.38.0" + version = ">=0.38.0" } random = { @@ -20,5 +20,10 @@ terraform { source = "hashicorp/archive" version = "~>2.2.0" } + + time = { + source = "hashicorp/time" + version = ">=0.12.0" + } } } \ No newline at end of file diff --git a/waf.tf b/waf.tf index 14fc61b..3345f49 100644 --- a/waf.tf +++ b/waf.tf @@ -62,6 +62,7 @@ resource "aws_wafv2_web_acl" "runtask_waf" { metric_name = "${var.name_prefix}-runtask_waf_acl" sampled_requests_enabled = true } + tags = local.combined_tags } resource "aws_cloudwatch_log_group" "runtask_waf" { @@ -70,6 +71,14 @@ resource "aws_cloudwatch_log_group" "runtask_waf" { name = "aws-waf-logs-${var.name_prefix}-runtask_waf_acl" retention_in_days = var.cloudwatch_log_group_retention kms_key_id = aws_kms_key.runtask_waf[count.index].arn + tags = local.combined_tags +} + +resource "aws_cloudwatch_log_resource_policy" "runtask_waf" { + count = local.waf_deployment + provider = aws.cloudfront_waf + policy_document = data.aws_iam_policy_document.runtask_waf_log[count.index].json + policy_name = "aws-waf-logs-${var.name_prefix}-runtask_waf_acl" } resource "aws_wafv2_web_acl_logging_configuration" "runtask_waf" { @@ -82,4 +91,4 @@ resource "aws_wafv2_web_acl_logging_configuration" "runtask_waf" { name = "x-tfc-task-signature" } } -} \ No newline at end of file +}