One of the most common questions we get about using Terraform to manage infrastructure as code is how to handle secrets such as passwords, API keys, and other sensitive data. For example, here’s a snippet of Terraform code that can be used to deploy MySQL using Amazon RDS:
resource "aws_db_instance" "example" {
engine = "mysql"
engine_version = "5.7"
instance_class = "db.t2.micro"
name = "example"
# How should you manage the credentials for the master user?
username = "???"
password = "???"
}
Notice how Terraform requires you to set two secrets, username
and password
, which are the credentials for the master user of the database. In this blog post, I’ll go over the most common techniques you can use to safely and securely manage such secrets:
The first rule of secrets management is:
Do not store secrets in plain text.
The second rule of secrets management is:
DO NOT STORE SECRETS IN PLAIN TEXT.
Seriously, don’t do it. For example, do NOT hard-code your database credentials directly in your Terraform code and check it into version control:
resource "aws_db_instance" "example" {
engine = "mysql"
engine_version = "5.7"
instance_class = "db.t2.micro"
name = "example"
# DO NOT DO THIS!!!
username = "admin"
password = "password"
# DO NOT DO THIS!!!
}
Storing secrets in plain text in version control is a BAD IDEA. Here are just a few of the reasons why:
In short, if you store secrets in plain text, you are giving malicious actors (e.g., hackers, competitors, disgruntled former employees) countless ways to access your company’s most sensitive data—e.g., by compromising the version control system, or any of the computers you use, or any piece of software on any of those computers, etc—and you’ll have no idea if you were compromised or have any easy way to fix things if you were.
Therefore, I strongly recommend that you always store secrets in an encrypted format—and this applies to all secrets, and not just those used with Terraform! Later in this post, I’ll discuss several different techniques for encrypting and decrypting such secrets.
Hopefully, the previous section has convinced you to not store your secrets in plain text, and the subsequent sections will show you some techniques for encrypting your secrets. However, no matter which technique you use to encrypt the secrets on your end, there is still one place where they will end up in plain text: Terraform state.
Every time you deploy infrastructure with Terraform, it stores lots of data about that infrastructure, including all the parameters you passed in, in a state file. By default, this is a terraform.tfstate
file that is automatically generated in the folder where you ran terraform apply
. So even if you use one of the techniques mentioned later to safely pass in your secrets, such as the credentials for a database:
resource "aws_db_instance" "example" {
engine = "mysql"
engine_version = "5.7"
instance_class = "db.t2.micro"
name = "example"
# Let's assume you found safe way to pass these in
username =
password =
}
These secrets will still end up in terraform.tfstate
in plain text! This has been an open issue for more than 6 years now, with no clear plans for a first-class solution. There are some workarounds out there that can scrub secrets from your state files, but these are brittle and likely to break with each new Terraform release, so I don’t recommend them.
For the time being, no matter which of the techniques discussed below you end up using to manage secrets, you must also:
terraform.tfstate
file, Terraform natively supports a variety of backends, such as S3, GCS, and Azure Blob Storage. Many of these backends support encryption, so that instead of your state files being in plain text, they will always be encrypted, both in transit (e.g., via TLS) and on disk (e.g., via AES-256). Most backends also support collaboration features (e.g., automatically pushing and pulling state; locking), so using a backend is a must-have both from a security and teamwork perspective. See How to Manage Terraform State for more info.And now, without further ado, let’s discuss the various techniques available to you for managing secrets with Terraform.
This first technique keeps plain text secrets out of your code by taking advantage of Terraform’s native support for reading environment variables.
To use this technique, declare variables for the secrets you wish to pass in:
variable "username" {
description = "The username for the DB master user"
type = string
}
variable "password" {
description = "The password for the DB master user"
type = string
}
Update, December 3, 2020: Terraform 0.14 has added the ability to mark variables as sensitive, which helps keep them out of your logs, so you should add sensitive = true
to both variables above!
Next, pass the variables to the Terraform resources that need those secrets:
resource "aws_db_instance" "example" {
engine = "mysql"
engine_version = "5.7"
instance_class = "db.t2.micro"
name = "example"
# Set the secrets from variables
username = var.username
password = var.password
}
You can now pass in a value for each variable foo
by setting an environment variable called TF_VAR_foo
. For example, here’s how you could set username
and password
via environment on Linux, Unix, or Mac, and run terraform apply
to deploy the database:
# Set secrets via environment variables
export TF_VAR_username=(the username)
export TF_VAR_password=(the password)
# When you run Terraform, it'll pick up the secrets automatically
terraform apply
(Pro tip: if you have the HISTCONTROL
environment variable set correctly in a Bash terminal, then any command with a leading space will not be stored in Bash history. Use this when setting environment variables with secrets to avoid having those secrets stored on disk.)
This technique helps you avoid storing secrets in plain text in your code, but it leaves the question of how to actually securely store and manage the secrets unanswered. So in a sense, this technique just kicks the can down the road, whereas the other techniques described later in this blog post are more prescriptive.
That said, so as not to leave you entirely hanging, if you do go with environment variables, the most common solution for storing and managing secrets is to use a password manager such as:
pass
: Open source tool that follows the Unix philosophy: it’s a CLI tool, it does input and output via text streams (i.e., stdin
, stdout
), and under the hood, it stores everything as files, with each secret in its own PGP-encrypted file (you can check these encrypted files into version for team collaboration).These tools solve the “kick the can down the road” problem by relying on human memory: that is, your ability to memorize a password that gives you access to the password manager.
pass
Let’s go through a quick example using pass
. First, you’ll need to store your secrets by using the pass insert
command:
$ pass insert db_username
Enter password for db_username: admin
$ pass insert db_password
Enter password for db_password: password
You can read a secret out to stdout
by running pass <secret>
:
$ pass db_username
admin
You can use this functionality in a subshell to set your secrets as environment variables and then call terraform apply
:
# Read secrets from pass and set as environment variables
export TF_VAR_username=$(pass db_username)
export TF_VAR_password=$(pass db_password)
# When you run Terraform, it'll pick up the secrets automatically
terraform apply
The second technique relies on encrypting the secrets, storing the cipher text in a file, and checking that file into version control.
To encrypt some data, such as some secrets in a file, you need an encryption key. This key is itself a secret! This creates a bit of a conundrum: how do you securely store that key? You can’t check the key into version control as plain text, as then there’s no point of encrypting anything with it. You could encrypt the key with another key, but then you then have to figure out where to store that second key. So you’re back to the “kick the can down the road problem,” as you still have to find a secure way to store your encryption key.
The most common solution to this conundrum is to store the key in a key service provided by your cloud provider, such as:
These key services solve the “kick the can down the road” problem by relying on human memory: in this case, your ability to memorize a password that gives you access to your cloud provider (or perhaps you store that password in a password manager and memorize the password to that instead).
Here’s an example of how you can use a key managed by AWS KMS to encrypt secrets. First, create a file called db-creds.yml
with your secrets:
username: admin
password: password
Note: do NOT check this file into version control!
Next, encrypt this file by using the aws kms encrypt
command and writing the resulting cipher text to db-creds.yml.encrypted
:
aws kms encrypt \
--key-id \
--region \
--plaintext fileb://db-creds.yml \
--output text \
--query CiphertextBlob \
> db-creds.yml.encrypted
You can now safely check db-creds.yml.encrypted
into version control.
To decrypt the secrets from this file in your Terraform code, you can use the aws_kms_secrets
data source (for GCP KMS or Azure Key Vault, you’d instead use the google_kms_secret
or azurerm_key_vault_secret
data sources, respectively):
data "aws_kms_secrets" "creds" {
secret {
name = "db"
payload = file("${path.module}/db-creds.yml.encrypted")
}
}
The code above will read db-creds.yml.encrypted
from disk and, assuming you have permissions to access the corresponding key in KMS, decrypt the contents to get back the original YAML. You can parse the YAML as follows:
locals {
db_creds = yamldecode(data.aws_kms_secrets.creds.plaintext["db"])
}
And now you can read the username
and password
from that YAML and pass them to the aws_db_instance
resource:
resource "aws_db_instance" "example" {
engine = "mysql"
engine_version = "5.7"
instance_class = "db.t2.micro"
name = "example"
# Set the secrets from the encrypted file
username = local.db_creds.username
password = local.db_creds.password
}
One gotcha with this approach is that working with encrypted files is awkward. To make a change, you have to locally decrypt the file with a long aws kms decrypt
command, make some edits, re-encrypt the file with another long aws kms encrypt
command, and the whole time, be extremely careful to not accidentally check the plain text data into version control or leave it sitting behind forever on your computer. This is a tedious and error prone process—unless you use a tool like sops
.
sops and Terragrunt
sops
is an open source tool designed to make it easier to edit and work with files that are encrypted via AWS KMS, GCP KMS, Azure Key Vault, or PGP. sops
can automatically decrypt a file when you open it in your text editor, so you can edit the file in plain text, and when you go to save those files, it automatically encrypts the contents again. This removes the need to run long aws kms
commands to encrypt or decrypt data or worry about accidentally checking plain text secrets into version control. Here’s a .gif
that shows sops
in action:
Terraform does not yet have native support for decrypting files in the format used by sops
. One solution is to install and use the custom provider for sops
, terraform-provider-sops
. Another option, which I’ll demonstrate here, is to use Terragrunt, which has native sops
support built in. Terragrunt is a thin wrapper for Terraform that helps you keep your Terraform code DRY and maintainable (check out the Quick Start guide for an overview).
Let’s say you used sops
to create an encrypted YAML file called db-creds.yml
, as shown in the .gif
above. Now, in your terragrunt.hcl
config, you can use the sops_decrypt_file
function built into Terragrunt to decrypt that file and yamldecode
to parse it as YAML:
locals {
db_creds = yamldecode(sops_decrypt_file(("db-creds.yml")))
}
Next, you can pass username
and password
as inputs to your Terraform code:
inputs = {
username = local.db_creds.username
password = local.db_creds.password
}
Your Terraform code, in turn, can read these inputs via variables:
variable "username" {
description = "The username for the DB master user"
type = string
}
variable "password" {
description = "The password for the DB master user"
type = string
}
Update, December 3, 2020: Terraform 0.14 has added the ability to mark variables as sensitive, which helps keep them out of your logs, so you should add sensitive = true
to both variables above!
And pass those variables through to aws_db_instance
:
resource "aws_db_instance" "example" {
engine = "mysql"
engine_version = "5.7"
instance_class = "db.t2.micro"
name = "example"
# Set the secrets from variables
username = var.username
password = var.password
}
sops
integration does require either a custom provider or wrapper tool like Terragrunt).aws kms encrypt
) or use an external tool such as sops
. There’s a learning curve to using these tools correctly and securely.The third technique relies on storing your secrets in a dedicated secret store: that is, a database that is designed specifically for securely storing sensitive data and tightly controlling access to it.
Here a few of the more popular secret stores you can consider:
These secret stores solve the “kick the can down the road” problem by relying on human memory: in this case, your ability to memorize a password that gives you access to your cloud provider (or multiple passwords in the case of Vault, as it uses Shamir’s Secret Sharing).
First, login to the AWS Secrets Manager UI, click “store a new secret,” and enter the secrets you wish to store:
The default is to use a JSON format, as you can see in the screenshot above. Next, give the secret a unique name:
Click “next” and “store” to save the secret.
Now, in your Terraform code, you can use the aws_secretsmanager_secret_version
data source to read this secret (for HashiCorp Vault, AWS SSM Param Store, or GCP Secret Store, you’d instead use the vault_generic_secret
, aws_ssm_parameter
, or google_secret_manager_secret_version
data source):
data "aws_secretsmanager_secret_version" "creds" {
# Fill in the name you gave to your secret
secret_id = "db-creds"
}
If you stored the secret data as JSON, you can use jsondecode
to parse it:
locals {
db_creds = jsondecode(
data.aws_secretsmanager_secret_version.creds.secret_string
)
}
And now you can use those secrets in the rest of your Terraform code:
resource "aws_db_instance" "example" {
engine = "mysql"
engine_version = "5.7"
instance_class = "db.t2.micro"
name = "example"
# Set the secrets from AWS Secrets Manager
username = local.db_creds.username
password = local.db_creds.password
}
Here are your key takeaways from this blog post:
Your entire infrastructure. Defined as code. In about a day. Gruntwork.io.