Building Bridges: Safely Upload and Distribute Secrets with Terraform

Have you ever wondered, how you can upload secrets into terraform without exposing them in your codebase? In this blog post, I will show you how to create a secret bridge with Terraform. This will allow you to commit your configuration into your main repository, thus making it easier for other developers to collaborate on the project. It will also make it easier for you to manage your secrets in a secure way.

Background:

Terraform allows you to easily create and manage critical components of your cloud architecture. However, you need to be aware of the security implications when using Terraform. For example, if you store your secrets in plain text in your codebase, they will be exposed to anyone who has access to your repository. This includes not only other developers but also third parties such as contractors or consultants who might have access to your repository. This is why it is important to store your secrets in a secure way.

So you just leave the secrets out of your codebase and add them manually when you run terraform apply.

WHile this might be a way to work around this vulnerability, you might not want to do this for a few reasons:

  1. It is error prone and time consuming. You might forget to add a secret or you might add the wrong secret. This can lead to mistakes and security vulnerabilities.

  2. It is not scalable. If you have multiple secrets, you will need to add them manually every time you run terraform apply. This can become tedious and time consuming.

  3. It is not secure. If you store your secrets in plain text on your machine, they will be exposed to anyone who gains access to it. This will also jeopardize the existance of your Key Vault.

  4. It is not portable. If you want to manage a three tier application, you will need to manage three different sets of secrets. This can become tedious, time consuming and error prone.

There might be other reasons why you don’t want to store your secrets in plain text in your codebase. However, these are the ones that I can think of right now.

So what is the solution?

So according to Yevgeniy Brikman’s post on how to manage secrets in Terraform, there are 3 ways to do this:

  1. Use a secret management service. (AWS, Azure, GCP, HashiCorp Vault, etc.) I will not go into detail on how to use these services as they are out of scope for this blog post. I specifically deal with the scenario where you want to create a new Key Vault or upload hundreds of new secrets into an existing Key Vault.

  2. Use Environment Variables. This is the most common way to do this. You can set environment variables on your machine and then use them in your codebase. However, this is not secure as anyone who has access to your machine will be able to see these environment variables. If you use a CI Server, you will need to set these environment variables on the CI Server as well. This can become tedious, error prone and time consuming.

It will also make it hard in case you want to manage dozens of secrets. You will need to set the equivalent of environment variables on your machine/CI Server and will probably introduce a lot of confusion into your codebase.

  1. Encrypt your secure notes, to check it into your repository. This is the most secure way to deal with uploading/initializing a new Key Vault, allowing to safely upload it. Managing this as a separate file will allow you to easily manage your secrets in a secure way. However, this is not scalable as you will need to manage multiple files (one for each environment). But as already stated in Brikman’s post, this is introduces a lot of complexity as you will need to deal with:
    • Encrypting the secrets (before storing & after editing)
    • Decrypting the secrets (when using it)
    • Managing the encryption keys (share, rotate, revoke)

It has a pretty bad drawback: In case of a leak, one can simply decrypt the secret file from your repository and gets access to the Key/Value pairs…

Solution:

What if there was a way to manage your secrets in a secure way, without having to deal with all the complexity of encrypting/decrypting them? My solution is what I call a secret bridge. This approach is a combination of approach 1 and 3 and allows you to manage initial secrets in a secure way. It allows to:

  1. Define your secrets in a secure way. (encrypted)
  2. Retrieve your secrets in a secure way via Terraform.
  3. Upload your secrets in a secure way.

Managing it via Terraform allows you to check the configuration into your repository and thus sharing it with your team becomes easier. This way, you can bulk upload your secrets into your Key Vault and manage them as per your requirements.

How to create a secret bridge with Terraform:

Provider:

Name Version
maxlaverse/bitwarden 0.6.1
hashicorp/azurerm 3.6.7

Requirements:

  • Terraform 1.5.4
  • Bitwarden CLI 2023.7.0 (used by the bitwarden provider)

  • Azure Account
  • Bitwarden Account

Code Introduction:

In this project, I used my bitwarden account with a secure note of key/value pairs. The Note will be retrieved from Terraform using a community provider. The Note will then be parsed to a map to finally get uploaded as secrets into the Key Vault.

This is the secure note that I used:

dbadmin=supersecret
keyvault=coolkeyvault

Code:

You can find the entire codebase on my GitHub. Here are the most important parts of this project:

I reference the Bitwarden note in my data.tf section. This note will then be parsed into a map:

# Contains the helper functions for this example project

# Parsing the imported secure note into a map
# Source: Bitwarden Secure Note
# Format: key=value
# Example: dbadmin=supersecret

locals {
  bw_secret_string = data.bitwarden_item_secure_note.exampleservice-configuration.notes

  bw_secret_map = {
    for entry in split("\n", local.bw_secret_string) :
    trimspace(element(split("=", entry), 0)) => trimspace(element(split("=", entry), 1))
  }
}

Note that I have to use two locals here. The first one is used to retrieve the secure note from Bitwarden as a raw string. The second one uses the string to parse into a map.

I then use the map to create the secrets in the Key Vault:

# Important: TF handles secrets as sensitive by default;
#            You need to use nonsensitive() in the for_each loop to work

resource "azurerm_key_vault_secret" "example" {
  for_each = nonsensitive(local.bw_secret_map)

  name         = each.key
  value        = each.value
  key_vault_id = azurerm_key_vault.keyvault.id
  expiration_date = timeadd(plantimestamp(), "24h")

  # Prevent the secret value from being updated if rotated in Azure Key Vault
  lifecycle {
    ignore_changes = [
      value,
      expiration_date,
    ]
  }
}

With the help of for_each I am able to dynamically create the secrets as defined in the note and upload them into the Key Vault. I also added a lifecycle block to prevent the secret value from being updated if rotated in Azure Key Vault. Changing the value in bitwarden will also have no effect once the secret is created. This may increase the security of your secrets. Setting the expiration date to 24h will disable the secret, hence enforcing a rotation. This is a good practice to have in place.

Conclusion:

This approach allows you to manage your secrets in a secure way. It allows you to bulk upload your secrets into your Key Vault and manage them as per your requirements. It also allows you to share your secrets with your team in a secure way.

I hope this blog post was helpful to you. If you have any questions, feel free to reach out to me on LinkedIn. I am always happy to help!