AWS KMS and Secrets Manager with Terraform: Encrypt and Store Secrets
Keys and secrets as code #
AWS KMS manages encryption keys, and AWS Secrets Manager stores secrets (passwords, API keys) and can rotate them. Together they are the base of a secure application. Read first secrets management in Terraform to understand what ends up in the state.
A customer managed KMS key #
data "aws_caller_identity" "current" {}
data "aws_iam_policy_document" "key" {
# The account administrators manage the key through IAM
statement {
sid = "EnableIAMPolicies"
actions = ["kms:*"]
resources = ["*"]
principals {
type = "AWS"
identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"]
}
}
}
resource "aws_kms_key" "app" {
description = "Application data key"
enable_key_rotation = true
deletion_window_in_days = 30
policy = data.aws_iam_policy_document.key.json
}
resource "aws_kms_alias" "app" {
name = "alias/ditwl-pro-app"
target_key_id = aws_kms_key.app.key_id
}Notes:
- The statement for the account root does not give access to everyone: it enables IAM policies to grant it. Without it, only the key policy decides, and you can lock yourself out.
enable_key_rotation = truerotates the key material every year.deletion_window_in_daysis the wait before real deletion. Data encrypted with a deleted key is lost.- Use the alias in code so applications do not depend on the key ID.
To let a role use the key, grant kms:Decrypt, kms:Encrypt and kms:GenerateDataKey on its ARN in the role's IAM policy.
A secret in Secrets Manager #
Create the secret as a container and keep the real value out of Terraform when you can:
resource "aws_secretsmanager_secret" "api" {
name = "pro/app/api-key"
description = "Third-party API key"
kms_key_id = aws_kms_key.app.arn
recovery_window_in_days = 7
}Set the value with the CLI or from the application that owns it:
$ aws secretsmanager put-secret-value \
--secret-id pro/app/api-key --secret-string "$API_KEY"
When you must set it with Terraform (for example a generated password), remember that it will be in the state:
resource "random_password" "db" {
length = 32
special = false
}
resource "aws_secretsmanager_secret_version" "db" {
secret_id = aws_secretsmanager_secret.db.id
secret_string = jsonencode({ username = "app", password = random_password.db.result })
}Encrypt the state with a KMS-enabled backend or OpenTofu state encryption. For RDS prefer manage_master_user_password = true, where AWS creates and rotates the secret and Terraform never sees the password (RDS).
Reading a secret in Terraform #
data "aws_secretsmanager_secret_version" "api" {
secret_id = aws_secretsmanager_secret.api.id
}
locals {
api_key = data.aws_secretsmanager_secret_version.api.secret_string
}Any value that you read this way is stored in the state. Prefer that the application reads the secret at runtime with its own IAM role, and pass only the secret name or ARN as an environment variable.
Rotation #
Secrets Manager can rotate secrets with a Lambda function. For RDS, AWS provides ready-made rotation functions:
resource "aws_secretsmanager_secret_rotation" "db" {
secret_id = aws_secretsmanager_secret.db.id
rotation_lambda_arn = aws_lambda_function.rotate.arn
rotation_rules {
automatically_after_days = 30
}
}SSM Parameter Store as a cheaper alternative #
For configuration and simple secrets, Systems Manager Parameter Store has no per-secret fee for standard parameters:
resource "aws_ssm_parameter" "app_config" {
name = "/pro/app/log_level"
type = "String"
value = "info"
}
resource "aws_ssm_parameter" "api_key" {
name = "/pro/app/api_key"
type = "SecureString"
key_id = aws_kms_key.app.arn
value = var.api_key # sensitive variable, ends up in the state
}| Secrets Manager | Parameter Store | |
|---|---|---|
| Automatic rotation | Yes | No |
| Price | Per secret per month plus API calls | Free for standard parameters |
| Cross-account sharing | Yes, with resource policies | Limited |
| Best for | Database credentials, rotating secrets | Configuration, static values |
Cost and cleanup #
Each customer managed KMS key and each secret has a monthly fee. Secrets scheduled for deletion keep the name reserved during the recovery window, which can break a quick destroy followed by apply with the same name: set recovery_window_in_days = 0 only in test environments.