├── LICENSE ├── README.md ├── context.tf ├── data.tf ├── functions ├── SecretsManagerRDSMySQLRotationMultiUser.zip └── SecretsManagerRDSMySQLRotationSingleUser.zip ├── main.tf ├── outputs.tf ├── schema.jpg ├── variables.tf └── versions.tf /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Giuseppe Borgese 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This module will create all the resources to store and rotate a MySQL or Aurora password using the AWS Secrets Manager service. 2 | 3 | # Schema 4 | 5 | ![schema](https://raw.githubusercontent.com/giuseppeborgese/terraform-aws-secret-manager-with-rotation/master/schema.jpg) 6 | 7 | # Prerequisites 8 | * A VPC with private subnets and accessibilty to AWS Secrets Manager Endpoint, see below for more details. 9 | * An RDS with MySQL or Aurora already created and reacheable from the private subnets 10 | 11 | 12 | # Usage Example 13 | ``` hcl 14 | module "secret-manager-with-rotation" { 15 | source = "giuseppeborgese/secret-manager-with-rotation/aws" 16 | version = "" 17 | name = "PassRotation" 18 | rotation_days = 10 19 | subnets_lambda = ["subnet-xxxxxx", "subnet-xxxxxx"] 20 | mysql_username = "giuseppe" 21 | mysql_dbname = "my_db_name" 22 | mysql_host = "mysqlEndpointurl.xxxxxx.us-east-1.rds.amazonaws.com" 23 | mysql_password = "dummy_password_will_we_rotated" 24 | } 25 | ``` 26 | 27 | ### Video Tutorial 28 | Take a look to the video to see the module in action 29 | 30 | 31 | [![Rotate automatically a MySQL or Aurora password using AWS Secrets Manager and Terraform](https://img.youtube.com/vi/ljZ6BZJabUk/0.jpg)](https://youtu.be/ljZ6BZJabUk) 32 | 33 | 34 | The subnets specified needs to be private and with internet access to reach the [AWS secrets manager endpoint](https://docs.aws.amazon.com/general/latest/gr/rande.html#asm_region) 35 | You can use a NAT GW or configure your Routes with a VPC Endpoint like is described in [this guide](https://aws.amazon.com/blogs/security/how-to-connect-to-aws-secrets-manager-service-within-a-virtual-private-cloud/) 36 | 37 | # Further details 38 | * Interesting to [force the rotation](https://forums.aws.amazon.com/thread.jspa?threadID=280093&tstart=0) 39 | 40 | # If you like it 41 | Please if you like this module, thumbs up on the youtube video and leave a comment as well for any questions. 42 | -------------------------------------------------------------------------------- /context.tf: -------------------------------------------------------------------------------- 1 | # 2 | # ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label 3 | # All other instances of this file should be a copy of that one 4 | # 5 | # 6 | # Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf 7 | # and then place it in your Terraform module to automatically get 8 | # Cloud Posse's standard configuration inputs suitable for passing 9 | # to Cloud Posse modules. 10 | # 11 | # Modules should access the whole context as `module.this.context` 12 | # to get the input variables with nulls for defaults, 13 | # for example `context = module.this.context`, 14 | # and access individual variables as `module.this.`, 15 | # with final values filled in. 16 | # 17 | # For example, when using defaults, `module.this.context.delimiter` 18 | # will be null, and `module.this.delimiter` will be `-` (hyphen). 19 | # 20 | 21 | module "this" { 22 | source = "cloudposse/label/null" 23 | version = "0.22.1" // requires Terraform >= 0.12.26 24 | 25 | enabled = var.enabled 26 | namespace = var.namespace 27 | environment = var.environment 28 | stage = var.stage 29 | name = var.name 30 | delimiter = var.delimiter 31 | attributes = var.attributes 32 | tags = var.tags 33 | additional_tag_map = var.additional_tag_map 34 | label_order = var.label_order 35 | regex_replace_chars = var.regex_replace_chars 36 | id_length_limit = var.id_length_limit 37 | 38 | context = var.context 39 | } 40 | 41 | # Copy contents of cloudposse/terraform-null-label/variables.tf here 42 | 43 | variable "context" { 44 | type = object({ 45 | enabled = bool 46 | namespace = string 47 | environment = string 48 | stage = string 49 | name = string 50 | delimiter = string 51 | attributes = list(string) 52 | tags = map(string) 53 | additional_tag_map = map(string) 54 | regex_replace_chars = string 55 | label_order = list(string) 56 | id_length_limit = number 57 | }) 58 | default = { 59 | enabled = true 60 | namespace = null 61 | environment = null 62 | stage = null 63 | name = null 64 | delimiter = null 65 | attributes = [] 66 | tags = {} 67 | additional_tag_map = {} 68 | regex_replace_chars = null 69 | label_order = [] 70 | id_length_limit = null 71 | } 72 | description = <<-EOT 73 | Single object for setting entire context at once. 74 | See description of individual variables for details. 75 | Leave string and numeric variables as `null` to use default value. 76 | Individual variable settings (non-null) override settings in context object, 77 | except for attributes, tags, and additional_tag_map, which are merged. 78 | EOT 79 | } 80 | 81 | variable "enabled" { 82 | type = bool 83 | default = null 84 | description = "Set to false to prevent the module from creating any resources" 85 | } 86 | 87 | variable "namespace" { 88 | type = string 89 | default = null 90 | description = "Namespace, which could be your organization name or abbreviation, e.g. 'eg' or 'cp'" 91 | } 92 | 93 | variable "environment" { 94 | type = string 95 | default = null 96 | description = "Environment, e.g. 'uw2', 'us-west-2', OR 'prod', 'staging', 'dev', 'UAT'" 97 | } 98 | 99 | variable "stage" { 100 | type = string 101 | default = null 102 | description = "Stage, e.g. 'prod', 'staging', 'dev', OR 'source', 'build', 'test', 'deploy', 'release'" 103 | } 104 | 105 | variable "name" { 106 | type = string 107 | default = null 108 | description = "Solution name, e.g. 'app' or 'jenkins'" 109 | } 110 | 111 | variable "delimiter" { 112 | type = string 113 | default = null 114 | description = <<-EOT 115 | Delimiter to be used between `namespace`, `environment`, `stage`, `name` and `attributes`. 116 | Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. 117 | EOT 118 | } 119 | 120 | variable "attributes" { 121 | type = list(string) 122 | default = [] 123 | description = "Additional attributes (e.g. `1`)" 124 | } 125 | 126 | variable "tags" { 127 | type = map(string) 128 | default = {} 129 | description = "Additional tags (e.g. `map('BusinessUnit','XYZ')`" 130 | } 131 | 132 | variable "additional_tag_map" { 133 | type = map(string) 134 | default = {} 135 | description = "Additional tags for appending to tags_as_list_of_maps. Not added to `tags`." 136 | } 137 | 138 | variable "label_order" { 139 | type = list(string) 140 | default = null 141 | description = <<-EOT 142 | The naming order of the id output and Name tag. 143 | Defaults to ["namespace", "environment", "stage", "name", "attributes"]. 144 | You can omit any of the 5 elements, but at least one must be present. 145 | EOT 146 | } 147 | 148 | variable "regex_replace_chars" { 149 | type = string 150 | default = null 151 | description = <<-EOT 152 | Regex to replace chars with empty string in `namespace`, `environment`, `stage` and `name`. 153 | If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. 154 | EOT 155 | } 156 | 157 | variable "id_length_limit" { 158 | type = number 159 | default = null 160 | description = <<-EOT 161 | Limit `id` to this many characters. 162 | Set to `0` for unlimited length. 163 | Set to `null` for default, which is `0`. 164 | Does not affect `id_full`. 165 | EOT 166 | } 167 | 168 | #### End of copy of cloudposse/terraform-null-label/variables.tf 169 | -------------------------------------------------------------------------------- /data.tf: -------------------------------------------------------------------------------- 1 | data "aws_partition" "current" {} 2 | data "aws_region" "current" {} 3 | data "aws_caller_identity" "current" {} 4 | data "aws_subnet" "firstsub" { id = var.subnets_lambda[0] } 5 | 6 | data "aws_iam_policy_document" "service" { 7 | statement { 8 | effect = "Allow" 9 | actions = ["sts:AssumeRole"] 10 | principals { 11 | type = "Service" 12 | identifiers = ["lambda.amazonaws.com"] 13 | } 14 | } 15 | } 16 | 17 | data "aws_iam_policy_document" "SecretsManagerRDSMySQLRotationMultiUserRolePolicy0" { 18 | statement { 19 | actions = [ 20 | "ec2:CreateNetworkInterface", 21 | "ec2:DeleteNetworkInterface", 22 | "ec2:DescribeNetworkInterfaces", 23 | "ec2:DetachNetworkInterface", 24 | ] 25 | resources = ["*"] 26 | } 27 | } 28 | 29 | data "aws_iam_policy_document" "SecretsManagerRDSMySQLRotationSingleUserRolePolicy0" { 30 | statement { 31 | actions = [ 32 | "ec2:CreateNetworkInterface", 33 | "ec2:DeleteNetworkInterface", 34 | "ec2:DescribeNetworkInterfaces", 35 | "ec2:DetachNetworkInterface", 36 | ] 37 | resources = ["*"] 38 | } 39 | } 40 | 41 | data "aws_iam_policy_document" "SecretsManagerRDSMySQLRotationMultiUserRolePolicy1" { 42 | statement { 43 | actions = [ 44 | "secretsmanager:DescribeSecret", 45 | "secretsmanager:GetSecretValue", 46 | "secretsmanager:PutSecretValue", 47 | "secretsmanager:UpdateSecretVersionStage", 48 | ] 49 | condition { 50 | test = "StringEquals" 51 | variable = "secretsmanager:resource/AllowRotationLambdaArn" 52 | values = ["arn:${data.aws_partition.current.partition}:lambda:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:function:${aws_lambda_function.default.function_name}"] 53 | } 54 | resources = [ 55 | "arn:${data.aws_partition.current.partition}:secretsmanager:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:secret:*", 56 | ] 57 | } 58 | 59 | statement { 60 | actions = ["secretsmanager:GetRandomPassword"] 61 | resources = ["*"] 62 | } 63 | } 64 | 65 | data "aws_iam_policy_document" "SecretsManagerRDSMySQLRotationSingleUserRolePolicy1" { 66 | statement { 67 | actions = [ 68 | "secretsmanager:DescribeSecret", 69 | "secretsmanager:GetSecretValue", 70 | "secretsmanager:PutSecretValue", 71 | "secretsmanager:UpdateSecretVersionStage", 72 | ] 73 | condition { 74 | test = "StringEquals" 75 | variable = "secretsmanager:resource/AllowRotationLambdaArn" 76 | values = ["arn:${data.aws_partition.current.partition}:lambda:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:function:${aws_lambda_function.default.function_name}"] 77 | } 78 | resources = [ 79 | "arn:${data.aws_partition.current.partition}:secretsmanager:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:secret:*", 80 | ] 81 | } 82 | 83 | statement { 84 | actions = ["secretsmanager:GetRandomPassword"] 85 | resources = ["*"] 86 | } 87 | } 88 | 89 | data "aws_iam_policy_document" "SecretsManagerRDSMySQLRotationMultiUserRolePolicy4" { 90 | statement { 91 | actions = [ 92 | "kms:Decrypt", 93 | "kms:DescribeKey", 94 | "kms:GenerateDataKey", 95 | ] 96 | resources = [aws_kms_key.default.arn] 97 | } 98 | } 99 | 100 | data "aws_iam_policy_document" "SecretsManagerRDSMySQLRotationSingleUserRolePolicy2" { 101 | statement { 102 | actions = [ 103 | "kms:Decrypt", 104 | "kms:DescribeKey", 105 | "kms:GenerateDataKey", 106 | ] 107 | resources = [aws_kms_key.default.arn] 108 | } 109 | } 110 | 111 | data "aws_iam_policy_document" "SecretsManagerRDSMySQLRotationMultiUserRolePolicy2" { 112 | statement { 113 | actions = [ 114 | "secretsmanager:GetSecretValue", 115 | ] 116 | resources = [var.secretsmanager_masterarn] 117 | } 118 | } 119 | 120 | data "aws_iam_policy_document" "kms" { 121 | statement { 122 | sid = "Enable IAM User Permissions" 123 | actions = ["kms:*"] 124 | principals { 125 | type = "AWS" 126 | identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"] 127 | } 128 | resources = ["*"] 129 | } 130 | 131 | statement { 132 | sid = "Allow access for Key Administrators" 133 | actions = [ 134 | "kms:Create*", 135 | "kms:Describe*", 136 | "kms:Enable*", 137 | "kms:List*", 138 | "kms:Put*", 139 | "kms:Update*", 140 | "kms:Revoke*", 141 | "kms:Disable*", 142 | "kms:Get*", 143 | "kms:Delete*", 144 | "kms:TagResource", 145 | "kms:UntagResource", 146 | "kms:ScheduleKeyDeletion", 147 | "kms:CancelKeyDeletion", 148 | ] 149 | principals { 150 | type = "AWS" 151 | identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"] # TODO 152 | } 153 | resources = ["*"] 154 | } 155 | 156 | statement { 157 | sid = "Allow use of the key" 158 | actions = [ 159 | "kms:Encrypt", 160 | "kms:Decrypt", 161 | "kms:ReEncrypt*", 162 | "kms:GenerateDataKey*", 163 | "kms:DescribeKey", 164 | ] 165 | principals { 166 | type = "AWS" 167 | identifiers = [aws_iam_role.default.arn] 168 | } 169 | resources = ["*"] 170 | } 171 | 172 | statement { 173 | sid = "Allow attachment of persistent resources" 174 | actions = [ 175 | "kms:CreateGrant", 176 | "kms:ListGrants", 177 | "kms:RevokeGrant", 178 | ] 179 | principals { 180 | type = "AWS" 181 | identifiers = [aws_iam_role.default.arn] 182 | } 183 | resources = ["*"] 184 | condition { 185 | test = "Bool" 186 | variable = "kms:GrantIsForAWSResource" 187 | values = ["true"] 188 | } 189 | } 190 | } 191 | 192 | -------------------------------------------------------------------------------- /functions/SecretsManagerRDSMySQLRotationMultiUser.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuseppeborgese/terraform-aws-secret-manager-with-rotation/574ed4ce6458b09797a3e6e257a2f59501873205/functions/SecretsManagerRDSMySQLRotationMultiUser.zip -------------------------------------------------------------------------------- /functions/SecretsManagerRDSMySQLRotationSingleUser.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuseppeborgese/terraform-aws-secret-manager-with-rotation/574ed4ce6458b09797a3e6e257a2f59501873205/functions/SecretsManagerRDSMySQLRotationSingleUser.zip -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | filename = var.rotation_type == "single" ? "SecretsManagerRDSMySQLRotationSingleUser.zip" : "SecretsManagerRDSMySQLRotationMultiUser.zip" 3 | lambda_description = var.rotation_type == "single" ? "Conducts an AWS SecretsManager secret rotation for RDS MySQL using single user rotation scheme" : "Conducts an AWS SecretsManager secret rotation for RDS MySQL using multi user rotation scheme" 4 | 5 | secret_string_single_bare = { 6 | username = var.mysql_username 7 | password = var.mysql_password 8 | engine = "mysql" 9 | host = var.mysql_host 10 | port = var.mysql_port 11 | dbname = var.mysql_dbname 12 | } 13 | secret_string_single_replica = { 14 | username = var.mysql_username 15 | password = var.mysql_password 16 | engine = "mysql" 17 | host = var.mysql_host 18 | port = var.mysql_port 19 | dbname = var.mysql_dbname 20 | replicahost = var.mysql_replicahost 21 | } 22 | secret_string_single = var.mysql_replicahost == null ? local.secret_string_single_bare : local.secret_string_single_replica 23 | 24 | secret_string_multi_bare = { 25 | username = var.mysql_username 26 | password = var.mysql_password 27 | engine = "mysql" 28 | host = var.mysql_host 29 | port = var.mysql_port 30 | dbname = var.mysql_dbname 31 | masterarn = var.secretsmanager_masterarn 32 | } 33 | secret_string_multi_replica = { 34 | username = var.mysql_username 35 | password = var.mysql_password 36 | engine = "mysql" 37 | host = var.mysql_host 38 | port = var.mysql_port 39 | dbname = var.mysql_dbname 40 | replicahost = var.mysql_replicahost 41 | masterarn = var.secretsmanager_masterarn 42 | } 43 | secret_string_multi = var.mysql_replicahost == null ? local.secret_string_multi_bare : local.secret_string_multi_replica 44 | } 45 | 46 | resource "aws_iam_role" "default" { 47 | name = "${module.this.id}-password_rotation" 48 | assume_role_policy = data.aws_iam_policy_document.service.json 49 | tags = module.this.tags 50 | } 51 | 52 | resource "aws_iam_role_policy_attachment" "lambda-basic" { 53 | role = aws_iam_role.default.name 54 | policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 55 | } 56 | 57 | resource "aws_iam_role_policy_attachment" "lambda-vpc" { 58 | role = aws_iam_role.default.name 59 | policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole" 60 | } 61 | 62 | resource "aws_iam_role_policy" "SecretsManagerRDSMySQLRotationSingleUserRolePolicy0" { 63 | count = var.rotation_type == "single" ? 1 : 0 64 | name = "SecretsManagerRDSMySQLRotationSingleUserRolePolicy0" 65 | role = aws_iam_role.default.name 66 | policy = data.aws_iam_policy_document.SecretsManagerRDSMySQLRotationSingleUserRolePolicy0.json 67 | } 68 | 69 | resource "aws_iam_role_policy" "SecretsManagerRDSMySQLRotationSingleUserRolePolicy1" { 70 | count = var.rotation_type == "single" ? 1 : 0 71 | name = "SecretsManagerRDSMySQLRotationSingleUserRolePolicy1" 72 | role = aws_iam_role.default.name 73 | policy = data.aws_iam_policy_document.SecretsManagerRDSMySQLRotationSingleUserRolePolicy1.json 74 | } 75 | 76 | resource "aws_iam_role_policy" "SecretsManagerRDSMySQLRotationSingleUserRolePolicy2" { 77 | count = var.rotation_type == "single" ? 1 : 0 78 | name = "SecretsManagerRDSMySQLRotationSingleUserRolePolicy2" 79 | role = aws_iam_role.default.name 80 | policy = data.aws_iam_policy_document.SecretsManagerRDSMySQLRotationSingleUserRolePolicy2.json 81 | } 82 | 83 | resource "aws_iam_role_policy" "SecretsManagerRDSMySQLRotationMultiUserRolePolicy0" { 84 | count = var.rotation_type == "single" ? 0 : 1 85 | name = "SecretsManagerRDSMySQLRotationMultiUserRolePolicy0" 86 | role = aws_iam_role.default.name 87 | policy = data.aws_iam_policy_document.SecretsManagerRDSMySQLRotationMultiUserRolePolicy0.json 88 | } 89 | 90 | resource "aws_iam_role_policy" "SecretsManagerRDSMySQLRotationMultiUserRolePolicy1" { 91 | count = var.rotation_type == "single" ? 0 : 1 92 | name = "SecretsManagerRDSMySQLRotationMultiUserRolePolicy1" 93 | role = aws_iam_role.default.name 94 | policy = data.aws_iam_policy_document.SecretsManagerRDSMySQLRotationMultiUserRolePolicy1.json 95 | } 96 | 97 | resource "aws_iam_role_policy" "SecretsManagerRDSMySQLRotationMultiUserRolePolicy2" { 98 | count = var.rotation_type == "single" ? 0 : 1 99 | name = "SecretsManagerRDSMySQLRotationMultiUserRolePolicy2" 100 | role = aws_iam_role.default.name 101 | policy = data.aws_iam_policy_document.SecretsManagerRDSMySQLRotationMultiUserRolePolicy2.json 102 | } 103 | 104 | resource "aws_iam_role_policy" "SecretsManagerRDSMySQLRotationMultiUserRolePolicy4" { 105 | count = var.rotation_type == "single" ? 0 : 1 106 | name = "SecretsManagerRDSMySQLRotationMultiUserRolePolicy4" 107 | role = aws_iam_role.default.name 108 | policy = data.aws_iam_policy_document.SecretsManagerRDSMySQLRotationMultiUserRolePolicy4.json 109 | } 110 | 111 | #resource "aws_security_group" "default" { 112 | # vpc_id = data.aws_subnet.firstsub.vpc_id 113 | # name = "${module.this.id}-Lambda-SecretManager" 114 | # tags = { 115 | # Name = "${module.this.id}-Lambda-SecretManager" 116 | # } 117 | # egress { 118 | # from_port = 0 119 | # to_port = 0 120 | # protocol = "-1" 121 | # cidr_blocks = ["0.0.0.0/0"] 122 | # } 123 | #} 124 | 125 | resource "aws_lambda_function" "default" { 126 | description = local.lambda_description 127 | filename = "${path.module}/functions/${local.filename}" 128 | source_code_hash = filebase64sha256("${path.module}/functions/${local.filename}") 129 | function_name = "${module.this.id}-password_rotation" 130 | handler = "lambda_function.lambda_handler" 131 | runtime = "python3.7" 132 | timeout = 30 133 | role = aws_iam_role.default.arn 134 | vpc_config { 135 | subnet_ids = var.subnets_lambda 136 | security_group_ids = var.security_group 137 | } 138 | environment { 139 | variables = { #https://docs.aws.amazon.com/general/latest/gr/rande.html#asm_region 140 | SECRETS_MANAGER_ENDPOINT = "https://secretsmanager.${data.aws_region.current.name}.amazonaws.com" 141 | } 142 | } 143 | tags = module.this.tags 144 | } 145 | 146 | resource "aws_lambda_permission" "default" { 147 | function_name = aws_lambda_function.default.function_name 148 | statement_id = "AllowExecutionSecretManager" 149 | action = "lambda:InvokeFunction" 150 | principal = "secretsmanager.amazonaws.com" 151 | } 152 | 153 | resource "aws_kms_key" "default" { 154 | description = "Key for Secrets Manager secret [${module.this.id}]" 155 | enable_key_rotation = true 156 | policy = data.aws_iam_policy_document.kms.json 157 | tags = module.this.tags 158 | } 159 | 160 | resource "aws_kms_alias" "default" { 161 | name = "alias/${module.this.id}" 162 | target_key_id = aws_kms_key.default.key_id 163 | } 164 | 165 | resource "aws_secretsmanager_secret" "default" { 166 | name = module.slash.id 167 | description = "Username and password for RDS user [${var.mysql_username}]." 168 | kms_key_id = aws_kms_key.default.key_id 169 | tags = module.this.tags 170 | #policy = # TODO 171 | } 172 | 173 | resource "aws_secretsmanager_secret_rotation" "default" { 174 | secret_id = aws_secretsmanager_secret.default.id 175 | rotation_lambda_arn = aws_lambda_function.default.arn 176 | rotation_rules { 177 | automatically_after_days = var.rotation_days 178 | } 179 | } 180 | 181 | resource "aws_secretsmanager_secret_version" "default" { 182 | secret_id = aws_secretsmanager_secret.default.id 183 | secret_string = jsonencode(var.rotation_type == "single" ? local.secret_string_single : local.secret_string_multi) 184 | 185 | # Changes to the password in Terraform should not trigger a change in state 186 | # to Secrets Manager as this could cause a loss of access to the target RDS 187 | # instance. 188 | # In other words, once Secrets Manager has managed to rotate the password, 189 | # Terraform should no longer attempt to apply a new password. 190 | lifecycle { 191 | ignore_changes = [ 192 | secret_string 193 | ] 194 | } 195 | } 196 | 197 | module "slash" { 198 | source = "cloudposse/label/null" 199 | version = "0.22.1" 200 | 201 | delimiter = "/" 202 | context = module.this.context 203 | label_order = var.secret_label_order 204 | } 205 | 206 | -------------------------------------------------------------------------------- /outputs.tf: -------------------------------------------------------------------------------- 1 | output "kms_key_arn" { 2 | description = "KMS Key ARN" 3 | value = join("", aws_kms_key.default.*.arn) 4 | } 5 | 6 | output "kms_alias_name" { 7 | description = "KMS Key Alias name" 8 | value = join("", aws_kms_alias.default.*.name) 9 | } 10 | 11 | output "iam_role_arn" { 12 | description = "Lambda IAM Role ARN" 13 | value = join("", aws_iam_role.default.*.arn) 14 | } 15 | 16 | output "iam_role_id" { 17 | description = "Lambda IAM Role ID" 18 | value = join("", aws_iam_role.default.*.id) 19 | } 20 | 21 | output "iam_role_name" { 22 | description = "Lambda IAM Role name" 23 | value = join("", aws_iam_role.default.*.name) 24 | } 25 | 26 | output "lambda_function_arn" { 27 | description = "Lambda Function ARN" 28 | value = join("", aws_lambda_function.default.*.arn) 29 | } 30 | 31 | output "lambda_function_name" { 32 | description = "Lambda Function name" 33 | value = join("", aws_lambda_function.default.*.function_name) 34 | } 35 | 36 | output "secretsmanager_secret_arn" { 37 | description = "Secrets Manager Secret ARN" 38 | value = join("", aws_secretsmanager_secret.default.*.arn) 39 | } 40 | 41 | output "secretsmanager_secret_name" { 42 | description = "Secrets Manager Secret Name" 43 | value = module.slash.id 44 | } 45 | 46 | output "secretsmanager_secret_version_id" { 47 | description = "Secrets Manager Secret version ID" 48 | value = join("", aws_secretsmanager_secret_version.default.*.version_id) 49 | } 50 | 51 | #output "security_group_id" { 52 | # description = "ID of the Security Group" 53 | # value = aws_security_group.default.id 54 | #} 55 | 56 | -------------------------------------------------------------------------------- /schema.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuseppeborgese/terraform-aws-secret-manager-with-rotation/574ed4ce6458b09797a3e6e257a2f59501873205/schema.jpg -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | variable "rotation_type" { 2 | type = string 3 | description = "Is this `single` or `multi` user rotation?" 4 | default = "single" 5 | } 6 | 7 | variable "rotation_days" { 8 | type = number 9 | description = "How often in days the secret will be rotated" 10 | default = 30 11 | } 12 | 13 | variable "subnets_lambda" { 14 | type = list(any) 15 | description = "The subnets where the Lambda Function will be run" 16 | } 17 | 18 | variable "mysql_username" { 19 | type = string 20 | description = "The MySQL/Aurora username you chose during RDS creation or another one that you want to rotate" 21 | } 22 | 23 | variable "mysql_dbname" { 24 | type = string 25 | description = "The Database name inside your RDS" 26 | } 27 | 28 | variable "mysql_host" { 29 | type = string 30 | description = "The RDS endpoint to connect to your database" 31 | } 32 | 33 | variable "mysql_password" { 34 | type = string 35 | description = "The password that you want to rotate, this will be changed after the creation" 36 | } 37 | 38 | variable "mysql_port" { 39 | type = number 40 | description = "In case you don't have your MySQL on default port and you need to change it" 41 | default = 3306 42 | } 43 | 44 | variable "secretsmanager_masterarn" { 45 | type = string 46 | description = "The ARN of the Secrets Manager which rotates the MySQL superuser" 47 | default = "" 48 | } 49 | 50 | #variable "additional_kms_role_arn" { 51 | # type = list 52 | # description = "If you want add another role of another resource to access to the kms key used to encrypt the secret" 53 | # default = [] 54 | #} 55 | 56 | variable "security_group" { 57 | type = list(any) 58 | description = "The security group(s) where the Lambda Function will be run. This must have access to the RDS instance. The best option is to make this the RDS' security group and allow the SG to access itself" 59 | } 60 | 61 | variable "mysql_replicahost" { 62 | type = string 63 | description = "The RDS replica endpoint to connect to your read-only database" 64 | default = null 65 | } 66 | 67 | variable "secret_label_order" { 68 | type = list(any) 69 | default = ["namespace", "environment", "stage", "name", "attributes"] 70 | description = <<-EOT 71 | The naming order of the id output and Name tag. 72 | Defaults to ["namespace", "environment", "stage", "name", "attributes"]. 73 | You can omit any of the 5 elements, but at least one must be present. 74 | EOT 75 | } 76 | 77 | -------------------------------------------------------------------------------- /versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 0.12.26" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = ">= 2.0" 8 | } 9 | } 10 | } 11 | --------------------------------------------------------------------------------