A module for creating AWS Secrets Manager Secrets without storing their values in Terraform's state. This is done by
pre-encrypting secrets using aws-encryption-cli
, then at apply
stage using a local-exec
provisioner to decrypt
them and set the Secrets Manager secret value. Only a hash of the encrypted value is stored in state.
There are two scenarios where you may want this, with the assumption that you want to manage secret values from Terraform:
- You want to provide state access to developers without granting them the ability to get secret values. (e.g. run a plan on a non-dev environment).
- You want to allow developers to set secrets without granting them the ability to read them.
However, this module has limitations and drawbacks detailed below which you should read before use. When configured
correctly this can be a safer replacement for aws_secretsmanager_secret_version
, but does not solve all functional or
security issues related to managing secrets via infrastucture as code. Use at your own risk.
This module uses local-exec
to call the aws
CLI, base64
, and aws-encryption-cli
via a bash
script. Ensure all are present on your PATH
before using this module.
First deploy this module with no configured secrets. This will create the KMS key you can use to pre-encrypt secrets.
module "stateless_secrets" {
source = "george-richardson/stateless-secrets/aws"
secrets = []
}
output "aws_kms_key_arn" {
value = module.stateless_secrets.kms_key_arn
}
Next pre-encrypt a secret.
# Get KMS key ARN from already deployed Terraform module
KEY_ID=$(terraform output -raw aws_kms_key_arn)
# Encrypt using above key.
# Output must be Base64 encoded (--encode)
# This example is encrypting the file ~/my-secret and outputting to ./my-secret.enc
aws-encryption-cli \
--encrypt \
--encode \
--input ~/my-secret \
--output my-secret.enc \
--wrapping-keys "key=$KEY_ID"
Now add the new secret to the module call, and run another terraform apply
module "stateless_secrets" {
source = "george-richardson/stateless-secrets/aws"
secrets = [
{
name = "my-secret"
encrypted_secret_value_file = "./my-secret.enc"
}
]
}
This module uses local-exec
to populate secret values using the aws
CLI and aws-encryption-cli
, as such it does
not inherit AWS authentication/authorisation configuration from the Terraform AWS provider and must be configured
separately. See the aws_cli_config
, assume_role
, and assume_role_with_web_identity
variables.
As a safety check, the account the CLI is configured to use will be validated against the one configured on the provider before any secrets are created.
You can use the stateless-secret
submodule if you'd like to manage the value of a
single secret. This gives you more control of the KMS keys and secret configuration.
resource "aws_secretsmanager_secret" "my_secret" {
name = "my-secret"
}
module "secret_value" {
source = "george-richardson/stateless-secrets/aws//modules/stateless-secret"
secret_id = aws_secretsmanager_secret.my_secret.arn
encrypted_secret_value = "c29tZX...NlY3JldA=="
}
By default, this module will only deploy two KMS resources, an aws_kms_key
and an aws_kms_alias
pointing to it.
These are to be used for the pre-encryption and subsequent decryption of values. See notes below on how to approach
securing these.
For each secret configured, two more resources will be created:
- An
aws_secretsmanager_secret
, used to track the lifecycle of a secret (but not its value). - A
terraform_data
resource, which uses alocal-exec
provisioner to trigger thesetter.sh
script.
The setter.sh
script is used for decrypting the pre-encrypted secret value inputs with kms:Decrypt
before setting
the value of the Secrets Manager secret with secretsmanager:PutSecretValue
. Only the md5 hash of the encrypted value
is stored in this resource's state. As such, after an initial run to set the secret value, subsequent plan
or apply
runs do not need to access the secret value. Changes to the encrypted value's md5 will cause redeployment of this
resource.
All secrets are expected to have been pre-encrypted using the aws-encryption-cli
with base64 encoding.
aws-encryption-cli
uses envelope encryption, with data keys saved in the resulting file alongside the ciphertext.
It is extremely important to ensure that principals are given least privilege access to secrets and keys. Care should be taken to ensure permissions granted for using KMS keys and Secrets Manager secret values are done so in line with the principle of least privilege, and any legal or compliance requirements that may apply.
The below sections describe the permissions a principal will need to perform various actions related to this module.
Consider using explicit deny rules in IAM, SCP, or resource based policies to prevent principals from accidentally
being granted more access than they require, e.g. with the key_policy
variable.
To run a Terraform plan
using this module (or apply
that does not need to update secret values), the following
permissions are needed:
On the deployed KMS key:
kms:DescribeKey
kms:GetKeyRotationStatus
kms:GetKeyPolicy
On KMS in general:
kms:ListAliases
On the deployed Secrets Manager secrets:
secretsmanager:DescribeSecret
secretsmanager:GetResourcePolicy
To run a Terraform apply
using this module, the following permissions are needed in addition to those needed for
plan
:
To create the KMS key/alias:
kms:CreateKey
kms:CreateAlias
kms:EnableKeyRotation
kms:PutKeyPolicy
To create Secrets Manager secrets and set their values:
kms:Decrypt
secretsmanager:CreateSecret
secretsmanager:PutSecretValue
(Note that the above does not include permissions needed to destroy resources)
To pre-encrypt a secret using this module, the following permissions are needed:
On the deployed KMS key:
kms:GenerateDataKey
Note you can choose to allow principals to pre-encrypt secrets without granting them the apply
permissions. This could
be useful if you have automated terraform apply
, effectively allowing engineers to set values via automation without
granting the ability to decrypt values themselves.
You should understand the following points before deciding to use this module.
- This module is mostly stateless, therefore it cannot detect drift. Changes to secrets passed in to the module will be detected and updated appropriately. Changes to secret values made outside of Terraform (e.g. via the AWS console) will not be detected or reconciled.
- This module uses
local_exec
provisioners and bash scripts (see dependencies above). This reduces the portability of your Terraform configuration. For example, this module would be difficult to run on Windows. - The "setter"
terraform_data
resource which is used to actually run the local script has no destroy behaviour. Destroying this resource without destroying or changing the value of the underlyingaws_secrets_manager_secret
resource doesn't change or wipe the secret value.
You should understand the following points before deciding to use this module.
- This module does not remove the need for storing a second copy of a secret. When using
aws_secretsmanager_secret_version
this second copy would be stored in Terraform's state file, accessible to anyone with access to that state. This module instead relies on passing in a value encrypted by a KMS key, which can have finer grained access controls. However, there is still at least a second copy of every secret, its location has just moved. - This module does not remove a single point of failure for secret compromises. When using
aws_secretsmanager_secret_version
this single point of failure is granting access to the Terraform state file. When using this module, the single point of failure is grantingkms:Decrypt
permission on the KMS key. - When using this module you may be tempted to store your encrypted secrets in version control. If you do this,
understand that granting someone
kms:Decrypt
on the KMS key grants them access to all values ever encrypted with that key. - This module does not support automatic secret rotation by itself. If you can automatically rotate secrets, you should endeavour to do that instead of using this module. If you do use this module, you should still manually rotate your secrets.
- Be careful not to create privilge escalation opportunities when running this module via automation. e.g. granting
an automated pull request triggered
plan
run more priviliges than the user who triggered it, allowing them to retrieve values by adding Terraformoutput
s to a branch. - You shouldn't trust random code you find on the internet. At a minimum you should review the code of this module. Consider forking this repository, or otherwise taking a copy, to avoid supply chain attacks on your secrets.
Name | Version |
---|---|
terraform | >= 1.4.0 |
aws | >= 4.9.0 |
Name | Version |
---|---|
aws | >= 4.9.0 |
Name | Source | Version |
---|---|---|
secrets | ./modules/stateless-secret | n/a |
Name | Type |
---|---|
aws_kms_alias.terraform_secrets | resource |
aws_kms_key.terraform_secrets | resource |
aws_secretsmanager_secret.secret | resource |
aws_caller_identity.current | data source |
aws_iam_policy_document.terraform_secrets_key_policy | data source |
Name | Description | Type | Default | Required |
---|---|---|---|---|
alias_name | Name of the KMS alias to create. | string |
"terraform-secrets" |
no |
assume_role | Details of role to assume before running script. Fields: role_arn: ARN of the role to assume session_name: name of the session external_id: external ID to use duration_seconds: duration of the session |
object({ |
null |
no |
assume_role_with_web_identity | AWS CLI assume role with web identity configuration, see: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-role.html#cli-configure-role-oidc Fields: role_arn: ARN of the role to assume, passed in as AWS_ROLE_ARN session_name: name of the session, passed in as AWS_SESSION_NAME web_identity_token_file: path to the web identity token file, passed in as AWS_WEB_IDENTITY_TOKEN_FILE |
object({ |
null |
no |
aws_cli_config | AWS CLI configuration, see: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html Fields: profile: configuraiton profile to use, passed in as AWS_PROFILE region: region to use, passed in as AWS_REGION config_file: path to the CLI configuration file, passed in as AWS_CONFIG_FILE shared_credentials_file: path to the shared credentials file, passed in as AWS_SHARED_CREDENTIALS_FILE |
object({ |
null |
no |
decrypt_principals | List of additional AWS principals that can decrypt using the deployed KMS key. Ignored if key_policy is set. | list(string) |
[] |
no |
encrypt_principals | List of additional AWS principals that can encrypt using the deployed KMS key. Ignored if key_policy is set. | list(string) |
[] |
no |
key_deletion_window_in_days | Duration in days after which the KMS key is deleted after destruction of the resource. | number |
null |
no |
key_policy | KMS key policy to use. If not set, a default policy is used. | string |
null |
no |
secrets | List of secrets to create in AWS Secrets Manager. One of encrypted_secret_value or encrypted_secret_value_file is required. fields: name: Name of the secret to be created. description: Description of the secret. encrypted_secret_value: Base64 encoded secret value that has been pre-encrypted using aws-encryption-cli. encrypted_secret_value_file: Path to base64 encoded secret file that has been pre-encrypted using aws-encryption-cli. policy: Resource based policy to attach to the secret. recovery_window_in_days: Number of days that AWS Secrets Manager waits before it can delete a secret. secret_kms_key_id: KMS key ID that will be configured for the secret. |
list(object({ |
[] |
no |
Name | Description |
---|---|
kms_alias_arn | The ARN of the KMS alias to be used to encrypt secret value or data keys for secrets to be stored in version control. |
kms_key_arn | The ARN of the KMS key to be used to encrypt secret value or data keys for secrets to be stored in version control. |
secret_arns | Map of created secret's names to ARNs. |