Encrypt secrets in Terraform

3 minute read

In case you wanted to encrypt a value which should not be seen in source control Terraform allows for encryption in .tf and .tvars using RSA + Base64 Encryption.

It so seems that Terraform currently does not have a mechanism to encrypt values itself so this has to involve some external script.

Generate key pair

Personally I like having Terraform for generating these keys as I have this template ready but you are free to use openssl for this.

NOTE: if you don’t yet have openssl installed choco install openssl on other OS than Windows use your favorite package manager

The boring way

PS> openssl genrsa -out id_rsa 2048

The Terraform Way

resource "tls_private_key" "ppk" {
  algorithm = "RSA"
  rsa_bits  = 2048
}

resource "local_file" "priv_key" {
  filename = "${path.module}/id_rsa"
  content  = tls_private_key.ppk.private_key_pem
}

resource "local_file" "pub_key_pem" {
  filename = "${path.module}/id_rsa.pem"
  content = tls_private_key.ppk.public_key_pem
}

Encrypt secret values

Now that we have private and public keys we can encrypt secrets using openssl. As rsautil doesn’t seem to work with in memory variables we pass in a secretfile

PS> New-Item -Name cleartext.txt
PS> Set-Content .\cleartext.txt -Value "mysecretvalue"
PS> openssl rsautl -encrypt -inkey .\id_rsa -in .\cleartext.txt -out encrypted.txt

Now we should have an encrypted secret file within the same directory, now we can encode it.

PS> openssl enc -a -in .\encrypted.txt -out .\encrypted64.txt -none

Now we should have a base64 encoded string which we can only decrypt using the private key that we’ve created previously. I pipe clip function here to get the value to my clipboard

PS> Get-Content .\encrypted64.txt | clip

Don’t forget to cleanup

PS> rm cleartext.txt
PS> rm encrypted.txt

Use encrypted secrets

Now we can use the secret value in Terraform files using the rsadecrypt() function. To test this I wrote a small terraform code snippet.

locals {
    encrypted_string = <<EOT
pezs7ThtbvR5S9oMsUo4UmRh3yMMkYzMyrTugpXOLXASccu5MVOJTZviD14PvZ02
HBC8C2gA6hF/LSoXT6A4ba9CAbA9ZtDQHq9Gldf6BB20KArbzNDAFrHdgUhJQC/n
TEuSHrIc5FNsm9JOYGCVEe/n/uhc/W9vkwgE5iAN1ACH3BLAn+KZ/5fV9r7IhtLs
Ar3sjGBJWGp9hQN8FcD/s9EoEtrhy4Gp3cuXmlseVJx5LuWeSSS6HT7glr7FCg38
ow0jSp/Ucue/ak19TYdmBD2c4m+2lz95jkRl3R1Ip+m9cn+1R5TVeihgYUAhbY7/
bXsPe6BvjcaP98h3J5Tbqw==
EOT
    private_key_path = "${path.module}/id_rsa"
}

First I’ve pasted my encoded secret in heredoc format to preserve any new lines that were created for this encoded document, however after testing I realized that this didn’t matter much and we can trim new lines and get the same results, so the above can also be written in single line like this:

locals {
    encrypted_string = "pezs7ThtbvR5S9oMsUo4UmRh3yMMkYzMyrTugpXOLXASccu5MVOJTZviD14PvZ02HBC8C2gA6hF/LSoXT6A4ba9CAbA9ZtDQHq9Gldf6BB20KArbzNDAFrHdgUhJQC/nTEuSHrIc5FNsm9JOYGCVEe/n/uhc/W9vkwgE5iAN1ACH3BLAn+KZ/5fV9r7IhtLsAr3sjGBJWGp9hQN8FcD/s9EoEtrhy4Gp3cuXmlseVJx5LuWeSSS6HT7glr7FCg38ow0jSp/Ucue/ak19TYdmBD2c4m+2lz95jkRl3R1Ip+m9cn+1R5TVeihgYUAhbY7/bXsPe6BvjcaP98h3J5Tbqw=="
    private_key_path = "${path.module}/id_rsa"
}

EDIT My colleague suggested that we can get encoded output in single line format by adding a -A flag.

PS> openssl enc -a -A -in .\encrypted.txt -out .\encrypted64.txt -none

Of course we could define this as a variable and not a local and have it in .tfvars file or we could use file() function to read the value from encrypted file but for my testing I did this just by copying straight into the file.

Now for the main code body I’ll output a new file with the decrypted value, also going to use the decrypted value as a prefix in random_pet resource and have it output to console.

resource "local_file" "secret" {
    filename = "./secretout.txt"
    content = rsadecrypt(local.encrypted_string,file(local.private_key_path))
}

resource "random_pet" "mypet" {
    prefix = trimspace(rsadecrypt(local.encrypted_string,file(local.private_key_path)))
}

output "rsadecrypt_output" {
    value = trimspace(rsadecrypt(local.encrypted_string,file(local.private_key_path)))
}

output "random_pet_name" {
    value = trimspace(random_pet.mypet.id)
}

Let’s do a terraform init followed by terraform plan

Terraform will perform the following actions:

  # local_file.secret will be created
  + resource "local_file" "secret" {
      + content              = <<-EOT
            mysecretvalue
        EOT
      + directory_permission = "0777"
      + file_permission      = "0777"
      + filename             = "./secretout.txt"
      + id                   = (known after apply)
    }

  # random_pet.mypet will be created
  + resource "random_pet" "mypet" {
      + id        = (known after apply)
      + length    = 2
      + prefix    = "mysecretvalue"
      + separator = "-"
    }

Plan: 2 to add, 0 to change, 0 to destroy.

Lastly terraform apply -auto-approve

local_file.secret: Creating...
random_pet.mypet: Creating...
random_pet.mypet: Creation complete after 0s [id=mysecretvalue-master-airedale]
local_file.secret: Creation complete after 0s [id=aaf05d4870a3af4a183c0885592a85035f8bbf0f]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

random_pet_name = "mysecretvalue-master-airedale"
rsadecrypt_output = "mysecretvalue"

Happy coding :)