Skip to content

Terraform module for setting AWS Secrets Manager secret values without storing them in state.

Notifications You must be signed in to change notification settings

george-richardson/terraform-aws-stateless-secrets

Repository files navigation

Terraform AWS Stateless Secrets Module

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:

  1. 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).
  2. 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.

Usage

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.

Basic Usage

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"
    }
  ]
}

AWS Configuration

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.

Setting a single secret

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=="
}

How it works

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:

  1. An aws_secretsmanager_secret, used to track the lifecycle of a secret (but not its value).
  2. A terraform_data resource, which uses a local-exec provisioner to trigger the setter.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.

Choosing suitable permissions for KMS keys and secrets

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.

Terraform plan

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

Terraform apply

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)

Pre-encryption

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.

Drawbacks

You should understand the following points before deciding to use this module.

  1. 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.
  2. 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.
  3. 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 underlying aws_secrets_manager_secret resource doesn't change or wipe the secret value.

Security Considerations

You should understand the following points before deciding to use this module.

  1. 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.
  2. 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 granting kms:Decrypt permission on the KMS key.
  3. 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.
  4. 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.
  5. 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 Terraform outputs to a branch.
  6. 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.

Requirements

Name Version
terraform >= 1.4.0
aws >= 4.9.0

Providers

Name Version
aws >= 4.9.0

Modules

Name Source Version
secrets ./modules/stateless-secret n/a

Resources

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

Inputs

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({
role_arn = string
session_name = optional(string, "terraform-aws-cli-script")
external_id = optional(string, null)
duration_seconds = optional(number, null)
})
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({
role_arn = string
session_name = optional(string, "terraform-aws-cli-script")
web_identity_token_file = string
})
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({
profile = optional(string, null)
region = optional(string, null)
config_file = optional(string, null)
shared_credentials_file = optional(string, null)
})
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({
name = optional(string, null)
description = optional(string, null)
policy = optional(string, null)
recovery_window_in_days = optional(number, null)
secret_kms_key_id = optional(string, null)
encrypted_secret_value = optional(string, null)
encrypted_secret_value_file = optional(string, null)
binary = optional(bool, false)
}))
[] no

Outputs

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.

About

Terraform module for setting AWS Secrets Manager secret values without storing them in state.

Resources

Stars

Watchers

Forks

Packages

No packages published