Secrets Manager Secret with KMS Encryption
Scenario
A common beginner pattern is to generate an RDS password with random_password and store it directly in Terraform state. While the state is encrypted at rest, a better practice is to store credentials in AWS Secrets Manager so that applications can retrieve them at runtime without any credentials in code or environment variables. The security team also requires encryption with a customer-managed KMS key, not the default AWS-managed key.
- Create a customer-managed KMS key with key rotation enabled and a descriptive key policy.
- Create an aws_secretsmanager_secret encrypted with the KMS key and a 7-day recovery window.
- Create an aws_secretsmanager_secret_version with a JSON value containing username, password, and host fields.
- Create an IAM policy that grants secretsmanager:GetSecretValue and kms:Decrypt for use by application roles.
- Output the secret ARN and the IAM policy ARN.
Additional Context
Storing the secret value in Terraform means the value appears in the Terraform state file. For maximum security, only create the secret in Terraform and populate the value via a separate mechanism (CI/CD pipeline, manual CLI command, or a dedicated secrets injection tool like Vault). Mark any Terraform output containing the secret as sensitive = true.
The KMS key policy must explicitly grant the account root principal access, otherwise even account administrators will be locked out of the key. This is a common mistake that results in permanently inaccessible secrets.
versions.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
random = { source = "hashicorp/random", version = "~> 3.5" }
}
}variables.tf
variable "aws_region" { type = string; default = "us-east-1" }
variable "secret_name" { type = string; default = "prod/db/credentials" }
variable "db_host" { type = string; description = "RDS endpoint hostname." }
variable "db_username" { type = string; default = "dbadmin" }
variable "environment" { type = string; default = "prod" }main.tf
# main.tf
provider "aws" {
region = var.aws_region
}
data "aws_caller_identity" "current" {}
resource "random_password" "db" {
length = 24
special = true
override_special = "!#$%-_=+"
}
# KMS Key policy — always grant the account root access.
# Without this, you risk permanently locking yourself out of the key.
data "aws_iam_policy_document" "kms_key_policy" {
statement {
sid = "EnableAccountRootAccess"
effect = "Allow"
principals {
type = "AWS"
identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"]
}
actions = ["kms:*"]
resources = ["*"]
}
statement {
sid = "AllowKeyUsageBySecretsManager"
effect = "Allow"
principals {
type = "Service"
identifiers = ["secretsmanager.amazonaws.com"]
}
actions = ["kms:GenerateDataKey", "kms:Decrypt"]
resources = ["*"]
}
}
# Customer-managed KMS key for encrypting the secret.
resource "aws_kms_key" "secrets" {
description = "CMK for Secrets Manager — ${var.environment}"
deletion_window_in_days = 7
enable_key_rotation = true # automatically rotate the key annually
policy = data.aws_iam_policy_document.kms_key_policy.json
tags = { Environment = var.environment, ManagedBy = "terraform" }
}
resource "aws_kms_alias" "secrets" {
name = "alias/secrets-manager-${var.environment}"
target_key_id = aws_kms_key.secrets.key_id
}
# The secret — metadata only. The actual value is in the version resource below.
resource "aws_secretsmanager_secret" "db_credentials" {
name = var.secret_name
kms_key_id = aws_kms_key.secrets.arn
recovery_window_in_days = 7 # 7-day soft-delete window before permanent deletion
tags = { Environment = var.environment, ManagedBy = "terraform" }
}
# The secret value — stored as a JSON string.
# jsonencode() produces a properly escaped JSON object.
resource "aws_secretsmanager_secret_version" "db_credentials" {
secret_id = aws_secretsmanager_secret.db_credentials.id
secret_string = jsonencode({
username = var.db_username
password = random_password.db.result
host = var.db_host
port = 3306
dbname = "appdb"
})
}
# IAM policy to grant read access to this secret.
# Attach this policy to the IAM role used by your application (see the IAM Role pattern).
data "aws_iam_policy_document" "read_secret" {
statement {
sid = "AllowSecretRead"
effect = "Allow"
actions = [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
]
resources = [aws_secretsmanager_secret.db_credentials.arn]
}
statement {
sid = "AllowKMSDecrypt"
effect = "Allow"
actions = ["kms:Decrypt", "kms:GenerateDataKey"]
resources = [aws_kms_key.secrets.arn]
}
}
resource "aws_iam_policy" "read_secret" {
name = "read-db-credentials-${var.environment}"
policy = data.aws_iam_policy_document.read_secret.json
}outputs.tf
output "secret_arn" { value = aws_secretsmanager_secret.db_credentials.arn }
output "secret_name" { value = aws_secretsmanager_secret.db_credentials.name }
output "kms_key_arn" { value = aws_kms_key.secrets.arn }
output "iam_policy_arn" { value = aws_iam_policy.read_secret.arn }Workflow Commands
terraform init
terraform apply -var="db_host=mydb.xxxx.us-east-1.rds.amazonaws.com"
# Retrieve the secret from CLI (as an application would at runtime)
aws secretsmanager get-secret-value \
--secret-id $(terraform output -raw secret_name) \
--query SecretString --output text | python3 -m json.tool
terraform destroy -var="db_host=..."Apply complete! Resources: 6 added, 0 changed, 0 destroyed. Outputs: iam_policy_arn = "arn:aws:iam::123456789012:policy/read-db-credentials-prod" kms_key_arn = "arn:aws:kms:us-east-1:123456789012:key/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/db/credentials-xxxxxx" secret_name = "prod/db/credentials"

