├── .github └── workflows │ ├── terraform-CD.yml │ └── terraform-CI.yml ├── .gitignore ├── Makefile ├── README.md ├── assets └── get_secret.py ├── data_sources.tf ├── hooks └── pre-commit ├── iam.tf ├── locals.tf ├── main.tf ├── outputs.tf ├── requirements.txt ├── terraform.tf ├── test_data ├── probe_role │ ├── data_sources.tf │ ├── main.tf │ ├── outputs.tf │ ├── providers.tf │ ├── terraform.tf │ └── variables.tf └── secret │ ├── datasources.tf │ ├── main.tf │ ├── outputs.tf │ ├── providers.tf │ ├── terraform.tf │ └── variables.tf ├── tests ├── __init__.py ├── conftest.py └── test_module.py └── variables.tf /.github/workflows/terraform-CD.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Terraform CD' 3 | 4 | on: # yamllint disable-line rule:truthy 5 | push: 6 | # Pattern matched against refs/tags 7 | tags: 8 | - "*" # Push events to every tag not containing / 9 | 10 | permissions: 11 | id-token: write # This is required for requesting the JWT 12 | contents: read 13 | 14 | env: 15 | ROLE_ARN: "arn:aws:iam::493370826424:role/ih-tf-terraform-aws-secret-github" 16 | AWS_DEFAULT_REGION: "us-west-1" 17 | 18 | jobs: 19 | publish: 20 | name: 'Publish Module' 21 | runs-on: ubuntu-latest 22 | environment: production 23 | timeout-minutes: 60 24 | # Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latest 25 | defaults: 26 | run: 27 | shell: bash 28 | 29 | steps: 30 | # Checkout the repository to the GitHub Actions runner 31 | - name: Checkout 32 | uses: actions/checkout@v3 33 | 34 | - name: Configure AWS Credentials 35 | uses: aws-actions/configure-aws-credentials@v2 36 | with: 37 | role-to-assume: ${{ env.ROLE_ARN }} 38 | role-session-name: github-actions 39 | aws-region: ${{ env.AWS_DEFAULT_REGION }} 40 | 41 | # Prepare Python environment 42 | - name: Setup Python Environment 43 | run: make bootstrap 44 | 45 | # Publish the module 46 | - name: Publish module 47 | run: | 48 | ih-registry upload 49 | -------------------------------------------------------------------------------- /.github/workflows/terraform-CI.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Terraform CI' 3 | 4 | on: # yamllint disable-line rule:truthy 5 | pull_request: 6 | 7 | permissions: 8 | id-token: write # This is required for requesting the JWT 9 | contents: read 10 | 11 | jobs: 12 | terraform: 13 | name: 'Terraform Test' 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 120 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 18 | ROLE_ARN: "arn:aws:iam::303467602807:role/secret-tester" 19 | 20 | # Use the Bash shell regardless whether the GitHub Actions runner 21 | # is ubuntu-latest, macos-latest, or windows-latest 22 | defaults: 23 | run: 24 | shell: bash 25 | 26 | steps: 27 | # Checkout the repository to the GitHub Actions runner 28 | - name: Checkout 29 | uses: actions/checkout@v3 30 | 31 | - name: Configure AWS Credentials 32 | uses: aws-actions/configure-aws-credentials@v2 33 | with: 34 | role-to-assume: ${{ env.ROLE_ARN }} 35 | role-session-name: "terraform-ci" 36 | aws-region: "us-west-1" 37 | 38 | # Install the latest version of Terraform CLI 39 | - name: Setup Terraform 40 | uses: hashicorp/setup-terraform@v2 41 | with: 42 | terraform_wrapper: false 43 | 44 | # Prepare Python environment 45 | - name: Setup Python Environment 46 | run: make bootstrap 47 | 48 | # Run all required linters 49 | - name: Code Style Check 50 | run: make lint 51 | 52 | # Generates an execution plan for Terraform 53 | - name: Terraform Tests 54 | run: make test 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Local .terraform directories 2 | **/.terraform/* 3 | 4 | # .tfstate files 5 | *.tfstate 6 | *.tfstate.* 7 | 8 | # Crash log files 9 | crash.log 10 | 11 | # Ignore any .tfvars files that are generated automatically for each Terraform run. Most 12 | # .tfvars files are managed as part of configuration and so should be included in 13 | # version control. 14 | # 15 | # example.tfvars 16 | 17 | # Ignore override files as they are usually used to override resources locally and so 18 | # are not checked in 19 | override.tf 20 | override.tf.json 21 | *_override.tf 22 | *_override.tf.json 23 | 24 | # Include override files you do wish to add to version control using negated pattern 25 | # 26 | # !example_override.tf 27 | 28 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 29 | # example: *tfplan* 30 | .terraform.lock.hcl 31 | terraform.tfvars 32 | tf.plan 33 | .idea 34 | /docs/_build/ 35 | /plan.stderr 36 | /plan.stdout 37 | __pycache__ 38 | .pytest_cache 39 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | 3 | define PRINT_HELP_PYSCRIPT 4 | import re, sys 5 | 6 | for line in sys.stdin: 7 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 8 | if match: 9 | target, help = match.groups() 10 | print("%-40s %s" % (target, help)) 11 | endef 12 | export PRINT_HELP_PYSCRIPT 13 | 14 | help: install-hooks 15 | @python -c "$$PRINT_HELP_PYSCRIPT" < Makefile 16 | 17 | .PHONY: install-hooks 18 | install-hooks: ## Install repo hooks 19 | @echo "Checking and installing hooks" 20 | @test -d .git/hooks || (echo "Looks like you are not in a Git repo" ; exit 1) 21 | @test -L .git/hooks/pre-commit || ln -fs ../../hooks/pre-commit .git/hooks/pre-commit 22 | @chmod +x .git/hooks/pre-commit 23 | 24 | 25 | .PHONY: test 26 | test: ## Run tests on the module 27 | rm -f test_data/test_module/.terraform.lock.hcl 28 | pytest -xvvs tests/ 29 | 30 | 31 | .PHONY: bootstrap 32 | bootstrap: ## bootstrap the development environment 33 | pip install -U "pip ~= 23.1" 34 | pip install -U "setuptools ~= 68.0" 35 | pip install -r requirements.txt 36 | 37 | .PHONY: clean 38 | clean: ## clean the repo from cruft 39 | rm -rf .pytest_cache 40 | find . -name '.terraform' -exec rm -fr {} + 41 | 42 | .PHONY: fmt 43 | fmt: format 44 | 45 | .PHONY: format 46 | format: ## Use terraform fmt to format all files in the repo 47 | @echo "Formatting terraform files" 48 | terraform fmt -recursive 49 | black tests 50 | 51 | define BROWSER_PYSCRIPT 52 | import os, webbrowser, sys 53 | 54 | from urllib.request import pathname2url 55 | 56 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 57 | endef 58 | export BROWSER_PYSCRIPT 59 | 60 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 61 | 62 | .PHONY: docs 63 | docs: ## generate Sphinx HTML documentation, including API docs 64 | $(MAKE) -C docs clean 65 | $(MAKE) -C docs html 66 | $(BROWSER) docs/_build/html/index.html 67 | 68 | .PHONY: lint 69 | lint: ## Lint the module 70 | @echo "Check code style" 71 | black --check tests 72 | terraform fmt -check 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # terraform-aws-secret 2 | ## Requirements 3 | 4 | | Name | Version | 5 | |------|---------| 6 | | [terraform](#requirement\_terraform) | ~> 1.5 | 7 | | [aws](#requirement\_aws) | ~> 5.11 | 8 | 9 | ## Providers 10 | 11 | | Name | Version | 12 | |------|---------| 13 | | [aws](#provider\_aws) | ~> 5.11 | 14 | | [external](#provider\_external) | n/a | 15 | 16 | ## Modules 17 | 18 | No modules. 19 | 20 | ## Resources 21 | 22 | | Name | Type | 23 | |------|------| 24 | | [aws_secretsmanager_secret.secret](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret) | resource | 25 | | [aws_secretsmanager_secret_version.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret_version) | resource | 26 | | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | 27 | | [aws_iam_policy_document.permission-policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | 28 | | [aws_iam_role.caller_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_role) | data source | 29 | | [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | 30 | | [external_external.secret_value](https://registry.terraform.io/providers/hashicorp/external/latest/docs/data-sources/external) | data source | 31 | 32 | ## Inputs 33 | 34 | | Name | Description | Type | Default | Required | 35 | |------|-------------|------|---------|:--------:| 36 | | [admins](#input\_admins) | List of role ARNs that will have all permissions of the secret. | `list(string)` | `null` | no | 37 | | [environment](#input\_environment) | Name of environment. | `string` | n/a | yes | 38 | | [owner](#input\_owner) | A tag owner with this value will be placed on a secret. | `string` | `null` | no | 39 | | [readers](#input\_readers) | List of role ARNs that will have read permissions of the secret. | `list(string)` | `null` | no | 40 | | [secret\_description](#input\_secret\_description) | The secret description in AWS Secretsmanager. | `string` | n/a | yes | 41 | | [secret\_name](#input\_secret\_name) | Name of the secret in AWS Secretsmanager. Either secret\_name or secret\_name\_prefix must be set. | `string` | `null` | no | 42 | | [secret\_name\_prefix](#input\_secret\_name\_prefix) | Name prefix of the secret in AWS Secretsmanager. Either secret\_name or secret\_name\_prefix must be set. | `string` | `null` | no | 43 | | [secret\_value](#input\_secret\_value) | Optional value of the secret. | `string` | `null` | no | 44 | | [service\_name](#input\_service\_name) | Descriptive name of a service that will use this secret. | `string` | `"unknown"` | no | 45 | | [tags](#input\_tags) | Tags to apply to secret and other resources the module creates. | `map(string)` | `{}` | no | 46 | | [writers](#input\_writers) | List of role ARNs that will have write permissions of the secret. | `list(string)` | `null` | no | 47 | 48 | ## Outputs 49 | 50 | | Name | Description | 51 | |------|-------------| 52 | | [secret\_arn](#output\_secret\_arn) | ARN of the created secret | 53 | | [secret\_id](#output\_secret\_id) | ID of the created secret | 54 | | [secret\_name](#output\_secret\_name) | Name of the created secret | 55 | | [secret\_value](#output\_secret\_value) | The current secret value. If the value isn't set yet, return `null`. | 56 | -------------------------------------------------------------------------------- /assets/get_secret.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | 4 | import boto3 5 | from botocore.exceptions import ClientError 6 | 7 | 8 | def get_secret(secretsmanager_client, secret_id): 9 | """ 10 | Retrieve a value of a secret by its name. 11 | """ 12 | try: 13 | response = secretsmanager_client.get_secret_value( 14 | SecretId=secret_id, 15 | ) 16 | return response["SecretString"] 17 | except ClientError as e: 18 | if e.response["Error"]["Code"] == "ResourceNotFoundException": 19 | return None 20 | raise 21 | 22 | 23 | def get_client(region, role_arn): 24 | sts = boto3.client("sts") 25 | iam_role = sts.assume_role( 26 | RoleArn=role_arn, RoleSessionName="terraform-aws-secret-data-source" 27 | ) 28 | session = boto3.Session( 29 | region_name=region, 30 | aws_access_key_id=iam_role["Credentials"]["AccessKeyId"], 31 | aws_secret_access_key=iam_role["Credentials"]["SecretAccessKey"], 32 | aws_session_token=iam_role["Credentials"]["SessionToken"], 33 | ) 34 | return session.client("secretsmanager") 35 | 36 | 37 | if __name__ == "__main__": 38 | 39 | print( 40 | json.dumps( 41 | { 42 | "SECRET_VALUE": get_secret( 43 | get_client(region=sys.argv[1], role_arn=sys.argv[3]), 44 | secret_id=sys.argv[2], 45 | ) 46 | } 47 | ) 48 | ) 49 | -------------------------------------------------------------------------------- /data_sources.tf: -------------------------------------------------------------------------------- 1 | # Data sources are defined here 2 | data "aws_caller_identity" "current" {} 3 | data "aws_region" "current" {} 4 | 5 | data "aws_iam_role" "caller_role" { 6 | name = split("/", split(":", data.aws_caller_identity.current.arn)[5])[1] 7 | } 8 | 9 | data "external" "secret_value" { 10 | program = [ 11 | "python", "${path.module}/assets/get_secret.py", data.aws_region.current.name, aws_secretsmanager_secret.secret.id, data.aws_iam_role.caller_role.arn 12 | ] 13 | depends_on = [ 14 | aws_secretsmanager_secret_version.current 15 | ] 16 | } 17 | 18 | data "aws_iam_role" "accessanalyzer" { 19 | name = "AWSServiceRoleForAccessAnalyzer" 20 | } 21 | -------------------------------------------------------------------------------- /hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "Happy coding!" 4 | -------------------------------------------------------------------------------- /iam.tf: -------------------------------------------------------------------------------- 1 | data "aws_iam_policy_document" "permission-policy" { 2 | statement { 3 | principals { 4 | identifiers = ["*"] 5 | type = "AWS" 6 | } 7 | condition { 8 | test = "ArnLike" 9 | values = concat(var.admins == null ? [] : var.admins, [data.aws_iam_role.caller_role.arn]) 10 | variable = "aws:PrincipalArn" 11 | } 12 | actions = local.all_actions 13 | resources = [ 14 | "*" 15 | ] 16 | } 17 | 18 | ## Writers 19 | dynamic "statement" { 20 | for_each = var.writers != null ? [{}] : [] 21 | content { 22 | principals { 23 | identifiers = ["*"] 24 | type = "AWS" 25 | } 26 | condition { 27 | test = "ArnLike" 28 | values = var.writers 29 | variable = "aws:PrincipalArn" 30 | } 31 | actions = concat( 32 | local.list_actions, 33 | local.read_actions, 34 | local.write_actions 35 | ) 36 | resources = [ 37 | "*" 38 | ] 39 | } 40 | } 41 | 42 | dynamic "statement" { 43 | for_each = var.writers != null ? [{}] : [] 44 | content { 45 | effect = "Deny" 46 | principals { 47 | identifiers = ["*"] 48 | type = "AWS" 49 | } 50 | condition { 51 | test = "ArnLike" 52 | values = var.writers 53 | variable = "aws:PrincipalArn" 54 | } 55 | actions = concat( 56 | local.admin_actions, 57 | local.permission_management_actions, 58 | local.tagging_actions 59 | ) 60 | resources = [ 61 | "*" 62 | ] 63 | } 64 | } 65 | 66 | ## Readers 67 | dynamic "statement" { 68 | for_each = var.readers != null ? [{}] : [] 69 | content { 70 | principals { 71 | identifiers = ["*"] 72 | type = "AWS" 73 | } 74 | condition { 75 | test = "ArnLike" 76 | values = local.readers_only 77 | variable = "aws:PrincipalArn" 78 | } 79 | actions = local.read_actions 80 | resources = [ 81 | "*" 82 | ] 83 | } 84 | } 85 | 86 | dynamic "statement" { 87 | for_each = var.readers != null ? [{}] : [] 88 | content { 89 | effect = "Deny" 90 | principals { 91 | identifiers = ["*"] 92 | type = "AWS" 93 | } 94 | condition { 95 | test = "ArnLike" 96 | values = local.readers_only 97 | variable = "aws:PrincipalArn" 98 | } 99 | actions = concat( 100 | local.list_actions, 101 | local.admin_actions, 102 | local.write_actions, 103 | local.permission_management_actions, 104 | local.tagging_actions 105 | ) 106 | resources = [ 107 | "*" 108 | ] 109 | } 110 | } 111 | 112 | # Access Analyzer permissions 113 | statement { 114 | effect = "Allow" 115 | principals { 116 | type = "AWS" 117 | identifiers = [ 118 | data.aws_iam_role.accessanalyzer.arn 119 | ] 120 | } 121 | actions = local.access_analyzer_actions 122 | resources = ["*"] 123 | } 124 | 125 | statement { 126 | effect = "Deny" 127 | principals { 128 | type = "AWS" 129 | identifiers = [ 130 | data.aws_iam_role.accessanalyzer.arn 131 | ] 132 | } 133 | actions = setsubtract( 134 | concat( 135 | local.list_actions, 136 | local.read_actions, 137 | local.write_actions, 138 | local.admin_actions, 139 | local.permission_management_actions, 140 | local.tagging_actions, 141 | ), 142 | local.access_analyzer_actions 143 | ) 144 | resources = ["*"] 145 | } 146 | 147 | ## The rest 148 | statement { 149 | effect = "Deny" 150 | principals { 151 | type = "AWS" 152 | identifiers = ["*"] 153 | } 154 | actions = local.all_actions 155 | resources = [ 156 | "*" 157 | ] 158 | condition { 159 | test = "ArnNotLike" 160 | values = concat( 161 | [ 162 | data.aws_iam_role.caller_role.arn, 163 | data.aws_iam_role.accessanalyzer.arn 164 | ], 165 | var.admins == null ? [] : var.admins, 166 | var.writers == null ? [] : var.writers, 167 | var.readers == null ? [] : var.readers 168 | ) 169 | variable = "aws:PrincipalArn" 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | access_analyzer_actions = [ 3 | "secretsmanager:DescribeSecret", 4 | "secretsmanager:GetResourcePolicy", 5 | "secretsmanager:ListSecrets", 6 | ] 7 | list_actions = [ 8 | "secretsmanager:BatchGetSecretValue", 9 | "secretsmanager:ListSecrets", 10 | ] 11 | admin_actions = [ 12 | "secretsmanager:CreateSecret", 13 | "secretsmanager:DeleteSecret", 14 | "secretsmanager:StopReplicationToReplica", 15 | "secretsmanager:ReplicateSecretToRegions", 16 | "secretsmanager:RemoveRegionsFromReplication", 17 | ] 18 | read_actions = [ 19 | "secretsmanager:DescribeSecret", 20 | "secretsmanager:GetSecretValue", 21 | "secretsmanager:GetRandomPassword", 22 | "secretsmanager:ListSecretVersionIds", 23 | "secretsmanager:GetResourcePolicy", 24 | ] 25 | write_actions = [ 26 | "secretsmanager:PutSecretValue", 27 | "secretsmanager:CancelRotateSecret", 28 | "secretsmanager:UpdateSecret", 29 | "secretsmanager:RestoreSecret", 30 | "secretsmanager:RotateSecret", 31 | "secretsmanager:UpdateSecretVersionStage", 32 | ] 33 | permission_management_actions = [ 34 | "secretsmanager:DeleteResourcePolicy", 35 | "secretsmanager:PutResourcePolicy", 36 | "secretsmanager:ValidateResourcePolicy", 37 | ] 38 | tagging_actions = [ 39 | "secretsmanager:TagResource", 40 | "secretsmanager:UntagResource", 41 | ] 42 | all_actions = ["*"] 43 | 44 | default_module_tags = { 45 | environment : var.environment 46 | service : var.service_name 47 | account : data.aws_caller_identity.current.account_id 48 | created_by_module : "infrahouse/secret/aws" 49 | } 50 | 51 | readers_only = var.readers != null ? ( 52 | var.writers != null ? setsubtract( 53 | toset(var.readers), 54 | toset(var.writers) 55 | ) : var.readers 56 | ) : null 57 | } 58 | -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- 1 | 2 | resource "aws_secretsmanager_secret" "secret" { 3 | description = var.secret_description 4 | name = var.secret_name 5 | name_prefix = var.secret_name_prefix 6 | recovery_window_in_days = 0 7 | policy = data.aws_iam_policy_document.permission-policy.json 8 | tags = merge( 9 | { 10 | owner : var.owner == null ? data.aws_iam_role.caller_role.arn : var.owner 11 | }, 12 | var.tags, 13 | local.default_module_tags, 14 | ) 15 | } 16 | 17 | resource "aws_secretsmanager_secret_version" "current" { 18 | secret_id = aws_secretsmanager_secret.secret.id 19 | secret_string = var.secret_value == null ? "NoValue" : var.secret_value 20 | version_stages = [ 21 | var.secret_value == null ? "INITIAL" : "AWSCURRENT" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /outputs.tf: -------------------------------------------------------------------------------- 1 | output "secret_name" { 2 | description = "Name of the created secret" 3 | value = aws_secretsmanager_secret.secret.name 4 | } 5 | 6 | output "secret_arn" { 7 | description = "ARN of the created secret" 8 | value = aws_secretsmanager_secret.secret.arn 9 | } 10 | 11 | output "secret_id" { 12 | description = "ID of the created secret" 13 | value = aws_secretsmanager_secret.secret.id 14 | } 15 | 16 | output "secret_value" { 17 | description = "The current secret value. If the value isn't set yet, return `null`." 18 | value = data.external.secret_value.result["SECRET_VALUE"] 19 | sensitive = true 20 | } 21 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | black ~= 24.3 2 | boto3 ~= 1.26 3 | botocore ~= 1.26 4 | infrahouse-toolkit ~= 2.0 5 | myst-parser ~= 2.0 6 | pytest-timeout ~= 2.1 7 | pytest-rerunfailures ~= 12.0 8 | requests ~= 2.31 9 | -------------------------------------------------------------------------------- /terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = "~> 1.5" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "~> 5.11" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test_data/probe_role/data_sources.tf: -------------------------------------------------------------------------------- 1 | data "aws_iam_policy_document" "permissions" { 2 | statement { 3 | actions = [ 4 | "secretsmanager:*" 5 | ] 6 | resources = [ 7 | "*" 8 | ] 9 | } 10 | } 11 | 12 | data "aws_iam_policy_document" "trust" { 13 | statement { 14 | actions = ["sts:AssumeRole"] 15 | principals { 16 | type = "AWS" 17 | identifiers = var.trusted_arns 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test_data/probe_role/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_role" "probe" { 2 | assume_role_policy = data.aws_iam_policy_document.trust.json 3 | } 4 | 5 | resource "aws_iam_role_policy" "probe" { 6 | policy = data.aws_iam_policy_document.permissions.json 7 | role = aws_iam_role.probe.id 8 | } 9 | -------------------------------------------------------------------------------- /test_data/probe_role/outputs.tf: -------------------------------------------------------------------------------- 1 | output "role_name" { 2 | value = aws_iam_role.probe.name 3 | } 4 | 5 | output "role_arn" { 6 | value = aws_iam_role.probe.arn 7 | } 8 | -------------------------------------------------------------------------------- /test_data/probe_role/providers.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | assume_role { 3 | role_arn = var.role_arn 4 | } 5 | region = var.region 6 | default_tags { 7 | tags = { 8 | "created_by" : "infrahouse/terraform-aws-secret" # GitHub repository that created a resource 9 | } 10 | 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test_data/probe_role/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 5.11" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test_data/probe_role/variables.tf: -------------------------------------------------------------------------------- 1 | variable "region" {} 2 | variable "role_arn" {} 3 | 4 | variable "trusted_arns" { 5 | description = "List of ARNs allowed to assume the probe role" 6 | type = list(string) 7 | } 8 | -------------------------------------------------------------------------------- /test_data/secret/datasources.tf: -------------------------------------------------------------------------------- 1 | data "aws_caller_identity" "this" {} 2 | -------------------------------------------------------------------------------- /test_data/secret/main.tf: -------------------------------------------------------------------------------- 1 | module "test" { 2 | source = "../../" 3 | secret_description = "Foo description" 4 | secret_name = var.secret_name 5 | secret_name_prefix = var.secret_name_prefix 6 | admins = var.admins 7 | writers = var.writers 8 | readers = var.readers 9 | secret_value = var.secret_value == "generate" ? random_password.value.result : var.secret_value 10 | tags = var.tags 11 | environment = "development" 12 | } 13 | 14 | resource "random_password" "value" { 15 | length = 16 16 | } 17 | -------------------------------------------------------------------------------- /test_data/secret/outputs.tf: -------------------------------------------------------------------------------- 1 | output "secret_value" { 2 | value = module.test.secret_value 3 | sensitive = true 4 | } 5 | 6 | output "secret_arn" { 7 | value = module.test.secret_arn 8 | } 9 | 10 | output "secret_name" { 11 | value = module.test.secret_name 12 | } 13 | -------------------------------------------------------------------------------- /test_data/secret/providers.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = var.region 3 | assume_role { 4 | role_arn = var.role_arn 5 | } 6 | default_tags { 7 | tags = { 8 | "created_by" : "infrahouse/terraform-aws-secret" # GitHub repository that created a resource 9 | } 10 | 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test_data/secret/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | //noinspection HILUnresolvedReference 3 | required_providers { 4 | aws = { 5 | source = "hashicorp/aws" 6 | version = "~> 5.11" 7 | } 8 | cloudinit = { 9 | source = "hashicorp/cloudinit" 10 | version = "~> 2.3" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test_data/secret/variables.tf: -------------------------------------------------------------------------------- 1 | variable "region" {} 2 | variable "role_arn" {} 3 | 4 | variable "admins" { default = null } 5 | variable "writers" { default = null } 6 | variable "readers" { default = null } 7 | variable "secret_name" { 8 | default = "foo" 9 | } 10 | variable "secret_name_prefix" { default = null } 11 | variable "secret_value" { 12 | default = "bar" 13 | } 14 | variable "tags" { default = null } 15 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/infrahouse/terraform-aws-secret/01731570de723f3db0b128046e39ca724d333691/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from textwrap import dedent 2 | 3 | import boto3 4 | import pytest 5 | import logging 6 | from os import path as osp 7 | 8 | from infrahouse_toolkit.logging import setup_logging 9 | from infrahouse_toolkit.terraform import terraform_apply 10 | 11 | # "303467602807" is our test account 12 | TEST_ACCOUNT = "303467602807" 13 | TEST_ROLE_ARN = "arn:aws:iam::303467602807:role/secret-tester" 14 | DEFAULT_PROGRESS_INTERVAL = 10 15 | TRACE_TERRAFORM = False 16 | UBUNTU_CODENAME = "jammy" 17 | 18 | LOG = logging.getLogger(__name__) 19 | REGION = "us-east-2" 20 | TEST_ZONE = "ci-cd.infrahouse.com" 21 | TERRAFORM_ROOT_DIR = "test_data" 22 | 23 | 24 | setup_logging(LOG, debug=True) 25 | 26 | 27 | def pytest_addoption(parser): 28 | parser.addoption( 29 | "--keep-after", 30 | action="store_true", 31 | default=False, 32 | help="If specified, don't destroy resources.", 33 | ) 34 | parser.addoption( 35 | "--test-role-arn", 36 | action="store", 37 | default=TEST_ROLE_ARN, 38 | help=f"AWS IAM role ARN that will create resources. Default, {TEST_ROLE_ARN}", 39 | ) 40 | 41 | 42 | @pytest.fixture(scope="session") 43 | def keep_after(request): 44 | return request.config.getoption("--keep-after") 45 | 46 | 47 | @pytest.fixture(scope="session") 48 | def test_role_arn(request): 49 | return request.config.getoption("--test-role-arn") 50 | 51 | 52 | @pytest.fixture(scope="session") 53 | def aws_iam_role(): 54 | sts = boto3.client("sts") 55 | return sts.assume_role( 56 | RoleArn=TEST_ROLE_ARN, RoleSessionName=TEST_ROLE_ARN.split("/")[1] 57 | ) 58 | 59 | 60 | @pytest.fixture(scope="session") 61 | def boto3_session(aws_iam_role): 62 | return boto3.Session( 63 | aws_access_key_id=aws_iam_role["Credentials"]["AccessKeyId"], 64 | aws_secret_access_key=aws_iam_role["Credentials"]["SecretAccessKey"], 65 | aws_session_token=aws_iam_role["Credentials"]["SessionToken"], 66 | ) 67 | 68 | 69 | @pytest.fixture(scope="session") 70 | def ec2_client(boto3_session): 71 | assert boto3_session.client("sts").get_caller_identity()["Account"] == TEST_ACCOUNT 72 | return boto3_session.client("ec2", region_name=REGION) 73 | 74 | 75 | @pytest.fixture(scope="session") 76 | def ec2_client_map(ec2_client, boto3_session): 77 | regions = [reg["RegionName"] for reg in ec2_client.describe_regions()["Regions"]] 78 | ec2_map = {reg: boto3_session.client("ec2", region_name=reg) for reg in regions} 79 | 80 | return ec2_map 81 | 82 | 83 | @pytest.fixture() 84 | def route53_client(boto3_session): 85 | return boto3_session.client("route53", region_name=REGION) 86 | 87 | 88 | @pytest.fixture() 89 | def elbv2_client(boto3_session): 90 | return boto3_session.client("elbv2", region_name=REGION) 91 | 92 | 93 | @pytest.fixture() 94 | def autoscaling_client(boto3_session): 95 | assert boto3_session.client("sts").get_caller_identity()["Account"] == TEST_ACCOUNT 96 | return boto3_session.client("autoscaling", region_name=REGION) 97 | 98 | 99 | @pytest.fixture() 100 | def secretsmanager_client(boto3_session): 101 | assert boto3_session.client("sts").get_caller_identity()["Account"] == TEST_ACCOUNT 102 | return boto3_session.client("secretsmanager", region_name=REGION) 103 | 104 | 105 | def get_secretsmanager_client_by_role(role_name): 106 | response = boto3.client("sts").assume_role( 107 | RoleArn=role_name, RoleSessionName=TEST_ROLE_ARN.split("/")[1] 108 | ) 109 | # noinspection PyUnresolvedReferences 110 | return boto3.Session( 111 | aws_access_key_id=response["Credentials"]["AccessKeyId"], 112 | aws_secret_access_key=response["Credentials"]["SecretAccessKey"], 113 | aws_session_token=response["Credentials"]["SessionToken"], 114 | ).client("secretsmanager", region_name=REGION) 115 | 116 | 117 | @pytest.fixture() 118 | def probe_role(boto3_session, keep_after): 119 | terraform_module_dir = osp.join(TERRAFORM_ROOT_DIR, "probe_role") 120 | # Create service network 121 | with open(osp.join(terraform_module_dir, "terraform.tfvars"), "w") as fp: 122 | fp.write( 123 | dedent( 124 | f""" 125 | role_arn = "{TEST_ROLE_ARN}" 126 | region = "{REGION}" 127 | trusted_arns = [ 128 | "arn:aws:iam::990466748045:user/aleks", 129 | "{TEST_ROLE_ARN}" 130 | ] 131 | """ 132 | ) 133 | ) 134 | with terraform_apply( 135 | terraform_module_dir, 136 | destroy_after=not keep_after, 137 | json_output=True, 138 | enable_trace=TRACE_TERRAFORM, 139 | ) as tf_output: 140 | yield tf_output 141 | -------------------------------------------------------------------------------- /tests/test_module.py: -------------------------------------------------------------------------------- 1 | import json 2 | from os import path as osp 3 | from textwrap import dedent 4 | 5 | import pytest 6 | from botocore.exceptions import ClientError 7 | from infrahouse_toolkit.terraform import terraform_apply 8 | 9 | from tests.conftest import ( 10 | LOG, 11 | TRACE_TERRAFORM, 12 | REGION, 13 | TERRAFORM_ROOT_DIR, 14 | get_secretsmanager_client_by_role, 15 | ) 16 | 17 | 18 | @pytest.mark.parametrize("probe_role_suffix", ["", "*"]) 19 | def test_module(probe_role, keep_after, test_role_arn, probe_role_suffix): 20 | terraform_module_dir = osp.join(TERRAFORM_ROOT_DIR, "secret") 21 | probe_role_arn = probe_role["role_arn"]["value"] 22 | with open(osp.join(terraform_module_dir, "terraform.tfvars"), "w") as fp: 23 | fp.write( 24 | dedent( 25 | f""" 26 | region = "{REGION}" 27 | role_arn = "{test_role_arn}" 28 | 29 | admins = [ 30 | "arn:aws:iam::303467602807:role/aws-reserved/sso.amazonaws.com/us-west-1/AWSReservedSSO_AWSAdministratorAccess_422821c726d81c14", 31 | "{probe_role_arn}{probe_role_suffix}" 32 | ] 33 | """ 34 | ) 35 | ) 36 | 37 | with terraform_apply( 38 | terraform_module_dir, 39 | destroy_after=not keep_after, 40 | json_output=True, 41 | enable_trace=TRACE_TERRAFORM, 42 | ) as tf_output: 43 | LOG.info("%s", json.dumps(tf_output, indent=4)) 44 | 45 | 46 | def test_module_no_access(probe_role, secretsmanager_client, keep_after, test_role_arn): 47 | terraform_module_dir = osp.join(TERRAFORM_ROOT_DIR, "secret") 48 | probe_role_arn = probe_role["role_arn"]["value"] 49 | with open(osp.join(terraform_module_dir, "terraform.tfvars"), "w") as fp: 50 | fp.write( 51 | dedent( 52 | f""" 53 | region = "{REGION}" 54 | role_arn = "{test_role_arn}" 55 | 56 | admins = [ 57 | "arn:aws:iam::303467602807:role/aws-reserved/sso.amazonaws.com/us-west-1/AWSReservedSSO_AWSAdministratorAccess_422821c726d81c14", 58 | "{test_role_arn}" 59 | ] 60 | """ 61 | ) 62 | ) 63 | 64 | with terraform_apply( 65 | terraform_module_dir, 66 | destroy_after=not keep_after, 67 | json_output=True, 68 | enable_trace=TRACE_TERRAFORM, 69 | ) as tf_output: 70 | LOG.info("%s", json.dumps(tf_output, indent=4)) 71 | sm_client = get_secretsmanager_client_by_role(probe_role_arn) 72 | with pytest.raises(ClientError) as err: 73 | sm_client.get_secret_value( 74 | SecretId="foo", 75 | ) 76 | assert err.type is ClientError 77 | # Example 78 | # e = { 79 | # "Message": ( 80 | # "User: " 81 | # "arn:aws:sts::303467602807:assumed-role/terraform-20240606193517506500000001/secret-tester " 82 | # "is not authorized to perform: secretsmanager:GetSecretValue on resource: foo with " 83 | # "an explicit deny in a resource-based policy" 84 | # ), 85 | # "Code": "AccessDeniedException", 86 | # } 87 | assert err.value.response["Error"]["Code"] == "AccessDeniedException" 88 | 89 | 90 | @pytest.mark.parametrize("probe_role_suffix", ["", "*"]) 91 | def test_module_reads( 92 | probe_role, secretsmanager_client, keep_after, test_role_arn, probe_role_suffix 93 | ): 94 | terraform_module_dir = osp.join(TERRAFORM_ROOT_DIR, "secret") 95 | probe_role_arn = probe_role["role_arn"]["value"] 96 | with open(osp.join(terraform_module_dir, "terraform.tfvars"), "w") as fp: 97 | fp.write( 98 | dedent( 99 | f""" 100 | region = "{REGION}" 101 | role_arn = "{test_role_arn}" 102 | 103 | admins = [ 104 | "arn:aws:iam::303467602807:role/aws-reserved/sso.amazonaws.com/us-west-1/AWSReservedSSO_AWSAdministratorAccess_422821c726d81c14", 105 | "{test_role_arn}" 106 | ] 107 | readers = [ 108 | "{probe_role_arn}{probe_role_suffix}" 109 | ] 110 | """ 111 | ) 112 | ) 113 | 114 | with terraform_apply( 115 | terraform_module_dir, 116 | destroy_after=not keep_after, 117 | json_output=True, 118 | enable_trace=TRACE_TERRAFORM, 119 | ) as tf_output: 120 | LOG.info("%s", json.dumps(tf_output, indent=4)) 121 | sm_client = get_secretsmanager_client_by_role(probe_role["role_arn"]["value"]) 122 | # Can read 123 | assert ( 124 | sm_client.get_secret_value( 125 | SecretId="foo", 126 | )["SecretString"] 127 | == "bar" 128 | ) 129 | 130 | # Can't write 131 | with pytest.raises(ClientError) as err: 132 | sm_client.put_secret_value( 133 | SecretId="foo", 134 | SecretString="barbar", 135 | ) 136 | assert err.type is ClientError 137 | assert err.value.response["Error"]["Code"] == "AccessDeniedException" 138 | 139 | 140 | @pytest.mark.parametrize("probe_role_suffix", ["", "*"]) 141 | def test_module_writes( 142 | probe_role, secretsmanager_client, keep_after, test_role_arn, probe_role_suffix 143 | ): 144 | terraform_module_dir = osp.join(TERRAFORM_ROOT_DIR, "secret") 145 | probe_role_arn = probe_role["role_arn"]["value"] 146 | with open(osp.join(terraform_module_dir, "terraform.tfvars"), "w") as fp: 147 | fp.write( 148 | dedent( 149 | f""" 150 | region = "{REGION}" 151 | role_arn = "{test_role_arn}" 152 | 153 | admins = [ 154 | "arn:aws:iam::303467602807:role/aws-reserved/sso.amazonaws.com/us-west-1/AWSReservedSSO_AWSAdministratorAccess_422821c726d81c14", 155 | "{test_role_arn}" 156 | ] 157 | writers = [ 158 | "{probe_role_arn}{probe_role_suffix}" 159 | ] 160 | """ 161 | ) 162 | ) 163 | 164 | with terraform_apply( 165 | terraform_module_dir, 166 | destroy_after=not keep_after, 167 | json_output=True, 168 | enable_trace=TRACE_TERRAFORM, 169 | ) as tf_output: 170 | LOG.info("%s", json.dumps(tf_output, indent=4)) 171 | sm_client = get_secretsmanager_client_by_role(probe_role["role_arn"]["value"]) 172 | 173 | # Can read 174 | assert ( 175 | sm_client.get_secret_value( 176 | SecretId="foo", 177 | )["SecretString"] 178 | == "bar" 179 | ) 180 | 181 | # Can write 182 | sm_client.put_secret_value( 183 | SecretId="foo", 184 | SecretString="barbar", 185 | ) 186 | assert ( 187 | sm_client.get_secret_value( 188 | SecretId="foo", 189 | )["SecretString"] 190 | == "barbar" 191 | ) 192 | 193 | # Can't delete 194 | with pytest.raises(ClientError) as err: 195 | sm_client.delete_secret(SecretId="foo", ForceDeleteWithoutRecovery=True) 196 | assert err.type is ClientError 197 | assert err.value.response["Error"]["Code"] == "AccessDeniedException" 198 | 199 | 200 | def test_module_secret_value(probe_role, keep_after, test_role_arn): 201 | probe_role_arn = probe_role["role_arn"]["value"] 202 | terraform_module_dir = osp.join(TERRAFORM_ROOT_DIR, "secret") 203 | with open(osp.join(terraform_module_dir, "terraform.tfvars"), "w") as fp: 204 | fp.write( 205 | dedent( 206 | f""" 207 | region = "{REGION}" 208 | role_arn = "{test_role_arn}" 209 | admins = [ 210 | "arn:aws:iam::303467602807:role/aws-reserved/sso.amazonaws.com/us-west-1/AWSReservedSSO_AWSAdministratorAccess_422821c726d81c14", 211 | ] 212 | 213 | writers = [ 214 | "{probe_role_arn}" 215 | ] 216 | secret_value = "generate" 217 | """ 218 | ) 219 | ) 220 | 221 | with terraform_apply( 222 | terraform_module_dir, 223 | destroy_after=not keep_after, 224 | json_output=True, 225 | enable_trace=TRACE_TERRAFORM, 226 | ) as tf_output: 227 | LOG.info("%s", json.dumps(tf_output, indent=4)) 228 | 229 | secret_value_0 = tf_output["secret_value"]["value"] 230 | sm_client = get_secretsmanager_client_by_role(probe_role_arn) 231 | # Can read 232 | assert ( 233 | sm_client.get_secret_value( 234 | SecretId="foo", 235 | )["SecretString"] 236 | == secret_value_0 237 | ) 238 | 239 | # Overwrite the secret and make sure Terraform reverts the secret 240 | sm_client.put_secret_value( 241 | SecretId="foo", 242 | SecretString="barbar", 243 | ) 244 | 245 | with terraform_apply( 246 | terraform_module_dir, 247 | destroy_after=not keep_after, 248 | json_output=True, 249 | enable_trace=TRACE_TERRAFORM, 250 | ): 251 | sm_client = get_secretsmanager_client_by_role(probe_role_arn) 252 | assert ( 253 | sm_client.get_secret_value( 254 | SecretId="foo", 255 | )["SecretString"] 256 | == secret_value_0 257 | ) 258 | 259 | 260 | def test_module_external_value(probe_role, keep_after, test_role_arn): 261 | """ 262 | Create a secret, set the value outside of Terraform 263 | """ 264 | probe_role_arn = probe_role["role_arn"]["value"] 265 | terraform_module_dir = osp.join(TERRAFORM_ROOT_DIR, "secret") 266 | with open(osp.join(terraform_module_dir, "terraform.tfvars"), "w") as fp: 267 | fp.write( 268 | dedent( 269 | f""" 270 | region = "{REGION}" 271 | role_arn = "{test_role_arn}" 272 | admins = [ 273 | "arn:aws:iam::303467602807:role/aws-reserved/sso.amazonaws.com/us-west-1/AWSReservedSSO_AWSAdministratorAccess_422821c726d81c14", 274 | ] 275 | 276 | writers = [ 277 | "{probe_role_arn}" 278 | ] 279 | secret_value = null 280 | """ 281 | ) 282 | ) 283 | # Ensure destroy 284 | with terraform_apply( 285 | terraform_module_dir, 286 | destroy_after=True, 287 | json_output=True, 288 | enable_trace=TRACE_TERRAFORM, 289 | ) as tf_output: 290 | LOG.info("%s", json.dumps(tf_output, indent=4)) 291 | 292 | # The test itself 293 | with terraform_apply( 294 | terraform_module_dir, 295 | destroy_after=not keep_after, 296 | json_output=True, 297 | enable_trace=TRACE_TERRAFORM, 298 | ) as tf_output: 299 | LOG.info("%s", json.dumps(tf_output, indent=4)) 300 | 301 | # secret_value_0 = tf_output["secret_value"]["value"] 302 | sm_client = get_secretsmanager_client_by_role(probe_role_arn) 303 | assert ( 304 | sm_client.get_secret_value( 305 | SecretId="foo", 306 | )["SecretString"] 307 | == "NoValue" 308 | ) 309 | 310 | # Overwrite the secret and make sure Terraform reverts the secret 311 | sm_client.put_secret_value( 312 | SecretId="foo", 313 | SecretString="barbar", 314 | ) 315 | 316 | with terraform_apply( 317 | terraform_module_dir, 318 | destroy_after=not keep_after, 319 | json_output=True, 320 | enable_trace=TRACE_TERRAFORM, 321 | ): 322 | sm_client = get_secretsmanager_client_by_role(probe_role_arn) 323 | assert ( 324 | sm_client.get_secret_value( 325 | SecretId="foo", 326 | )["SecretString"] 327 | == "barbar" 328 | ) 329 | 330 | 331 | def test_module_name_prefix(keep_after, test_role_arn): 332 | terraform_module_dir = osp.join(TERRAFORM_ROOT_DIR, "secret") 333 | with open(osp.join(terraform_module_dir, "terraform.tfvars"), "w") as fp: 334 | fp.write( 335 | dedent( 336 | f""" 337 | region = "{REGION}" 338 | role_arn = "{test_role_arn}" 339 | secret_name = null 340 | secret_name_prefix = "some_secret" 341 | 342 | admins = [ 343 | "arn:aws:iam::303467602807:role/aws-reserved/sso.amazonaws.com/us-west-1/AWSReservedSSO_AWSAdministratorAccess_422821c726d81c14", 344 | ] 345 | """ 346 | ) 347 | ) 348 | 349 | with terraform_apply( 350 | terraform_module_dir, 351 | destroy_after=not keep_after, 352 | json_output=True, 353 | enable_trace=TRACE_TERRAFORM, 354 | ) as tf_output: 355 | LOG.info("%s", json.dumps(tf_output, indent=4)) 356 | assert tf_output["secret_name"]["value"].startswith("some_secret") 357 | 358 | 359 | def test_module_tags(secretsmanager_client, keep_after, test_role_arn): 360 | terraform_module_dir = osp.join(TERRAFORM_ROOT_DIR, "secret") 361 | with open(osp.join(terraform_module_dir, "terraform.tfvars"), "w") as fp: 362 | fp.write( 363 | dedent( 364 | f""" 365 | region = "{REGION}" 366 | role_arn = "{test_role_arn}" 367 | tags = {{ 368 | tag1: "value1" 369 | }} 370 | """ 371 | ) 372 | ) 373 | 374 | with terraform_apply( 375 | terraform_module_dir, 376 | destroy_after=not keep_after, 377 | json_output=True, 378 | enable_trace=TRACE_TERRAFORM, 379 | ) as tf_output: 380 | LOG.info("%s", json.dumps(tf_output, indent=4)) 381 | response = secretsmanager_client.describe_secret( 382 | SecretId=tf_output["secret_arn"]["value"] 383 | ) 384 | assert response["Tags"] == [ 385 | { 386 | "Key": "owner", 387 | "Value": test_role_arn, 388 | }, 389 | { 390 | "Key": "tag1", 391 | "Value": "value1", 392 | }, 393 | { 394 | "Key": "environment", 395 | "Value": "development", 396 | }, 397 | { 398 | "Key": "created_by_module", 399 | "Value": "infrahouse/secret/aws", 400 | }, 401 | { 402 | "Key": "service", 403 | "Value": "unknown", 404 | }, 405 | { 406 | "Key": "created_by", 407 | "Value": "infrahouse/terraform-aws-secret", 408 | }, 409 | { 410 | "Key": "account", 411 | "Value": "303467602807", 412 | }, 413 | ] 414 | 415 | 416 | def test_module_duplicate_role( 417 | probe_role, secretsmanager_client, keep_after, test_role_arn 418 | ): 419 | terraform_module_dir = osp.join(TERRAFORM_ROOT_DIR, "secret") 420 | probe_role_arn = probe_role["role_arn"]["value"] 421 | with open(osp.join(terraform_module_dir, "terraform.tfvars"), "w") as fp: 422 | fp.write( 423 | dedent( 424 | f""" 425 | region = "{REGION}" 426 | role_arn = "{test_role_arn}" 427 | 428 | admins = [ 429 | "arn:aws:iam::303467602807:role/aws-reserved/sso.amazonaws.com/us-west-1/AWSReservedSSO_AWSAdministratorAccess_422821c726d81c14", 430 | "{test_role_arn}" 431 | ] 432 | writers = [ 433 | "{probe_role_arn}" 434 | ] 435 | readers = [ 436 | "{probe_role_arn}" 437 | ] 438 | """ 439 | ) 440 | ) 441 | 442 | with terraform_apply( 443 | terraform_module_dir, 444 | destroy_after=not keep_after, 445 | json_output=True, 446 | enable_trace=TRACE_TERRAFORM, 447 | ) as tf_output: 448 | LOG.info("%s", json.dumps(tf_output, indent=4)) 449 | sm_client = get_secretsmanager_client_by_role(probe_role["role_arn"]["value"]) 450 | 451 | # Can read 452 | assert ( 453 | sm_client.get_secret_value( 454 | SecretId="foo", 455 | )["SecretString"] 456 | == "bar" 457 | ) 458 | 459 | # Can write 460 | sm_client.put_secret_value( 461 | SecretId="foo", 462 | SecretString="barbar", 463 | ) 464 | assert ( 465 | sm_client.get_secret_value( 466 | SecretId="foo", 467 | )["SecretString"] 468 | == "barbar" 469 | ) 470 | 471 | # Can't delete 472 | with pytest.raises(ClientError) as err: 473 | sm_client.delete_secret(SecretId="foo", ForceDeleteWithoutRecovery=True) 474 | assert err.type is ClientError 475 | assert err.value.response["Error"]["Code"] == "AccessDeniedException" 476 | -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | variable "admins" { 2 | description = "List of role ARNs that will have all permissions of the secret." 3 | default = null 4 | type = list(string) 5 | } 6 | 7 | variable "owner" { 8 | description = "A tag owner with this value will be placed on a secret." 9 | default = null 10 | type = string 11 | } 12 | 13 | variable "readers" { 14 | description = "List of role ARNs that will have read permissions of the secret." 15 | default = null 16 | type = list(string) 17 | } 18 | 19 | variable "writers" { 20 | description = "List of role ARNs that will have write permissions of the secret." 21 | default = null 22 | type = list(string) 23 | } 24 | 25 | variable "secret_name" { 26 | description = "Name of the secret in AWS Secretsmanager. Either secret_name or secret_name_prefix must be set." 27 | type = string 28 | default = null 29 | } 30 | 31 | variable "secret_name_prefix" { 32 | description = "Name prefix of the secret in AWS Secretsmanager. Either secret_name or secret_name_prefix must be set." 33 | type = string 34 | default = null 35 | } 36 | 37 | variable "secret_description" { 38 | description = "The secret description in AWS Secretsmanager." 39 | type = string 40 | } 41 | 42 | variable "secret_value" { 43 | description = "Optional value of the secret." 44 | type = string 45 | default = null 46 | } 47 | 48 | variable "environment" { 49 | description = "Name of environment." 50 | type = string 51 | } 52 | 53 | variable "service_name" { 54 | description = "Descriptive name of a service that will use this secret." 55 | type = string 56 | default = "unknown" 57 | } 58 | 59 | variable "tags" { 60 | description = "Tags to apply to secret and other resources the module creates." 61 | type = map(string) 62 | default = {} 63 | } 64 | --------------------------------------------------------------------------------