Using KeepassXC with Terraform

Using KeepassXC with Terraform

A personal Vault instance or paying for AWS secrets manager is overkill for personal projects, especially ones with local or mixed environment setups. A local encrypted file, like a .enc Ansible can use, is annoying to edit. KeepassXC offers a GUI, familiar interface, and pathing system which makes it a viable alternative.

Installing

The entire setup is very quick and easy. Add the keepass provider to your providers.tf or equivalent config like the example below.

terraform {
  required_providers {
    cloudflare = {
      source = "cloudflare/cloudflare"
      version = ">= 4.9.0"
    }

    keepass = {
      source = "iSchluff/keepass"
      required_version = ">= 0.2.1"
    }

    random = {
      source = "hashicorp/random"
    }
  }
  required_version = ">= 0.13"
}

Configuring the Provider

Then instantiate the provider. The variables are:

provider "keepass" {
  database = "passwords.kdbx" #Relative to terraform's root - Environment Variable: KEEPASS_DATABASE
  password = "test123" #Database Password                   - Environment Variable: KEEPASS_PASSWORD
  key = "file.key" #Optional if using a key                 - Environment Variable: KEEPASS_KEY
}

If you want to use an environment variable or a prompt then use:

provider "keepass" {
  database = "k3s.kdbx"
  password = var.keepass_password
}

variable "keepass_password" {
  description = "KeePass Master Password"
  type        = string
  default     = null
}

You can then either input the password at runtime or export KEEPASS_PASSWORD=PASSWORD.

Secret Structure

Secrets are referenced by data blocks in Terraform which reference Keepass entries based on their path.

Keepass Root

These entries are all in the root so the paths would be Root/ENTRYNAME. If we were to organize these entries we would get: Keepass Paths These entries would then become Root/Cloudflare/ENTRYNAME and Root/k3s/k3s_tunnel_token.

The Keepass data structure has the following attributes:

  • title
  • username
  • password
  • url
  • notes
  • attributes

These naturally match the entries in the GUI. Keepass Attributes

They are referenced by resources using Terraform data blocks.

  • data.keepass_entry.cloudflare_token.title = cloudflare_token
  • data.keepass_entry.cloudflare_token.password = token

The attributes attribute:

This is KeePass’ key-value store for custom secret data. If you don’t know what these are(who even uses them?), you’ll probably recognize it below:

Keepass Attributes

Below is the raw output of the extra attributes when calling data.keepass_entry.test.attributes:

tomap({
  "test-attribute" = "this is an attribute"
  "test-attribute2" = "this is another attribute. will this data structure return json? we'll find out"
})

To reference these you just append the attribute name to the data reference

data.keepass_entry.test.attributes.test-attribute

or alternatively read the map in Terraform which allows you to reference all extra attributes in one entry.

data.keepass_entry.test.attributes["test-attribute"]

You can also do this with an output if you’ve created one:

> module.keepass_secrets.test.attibutes["test-attribute"]
"this is an attribute"

Referencing Secrets

If you only need to reference a secret once, it’s easier to reference the data directly.

Create data blocks for each secret you want to reference:

data "keepass_entry" "cloudflare_token" {
  path = "Root/cloudflare_token"
}

Reference it:

provider "cloudflare" {
  api_token    = data.keepass_entry.cloudflare_token.password
}

Using Local Variables

If you reference a secret that’s not too secret multiple times, you can use a local variable. For example, a Cloudflare Zone ID and Account ID aren’t as sensitive as a token so we can put that data into local variables in the same file as the resources:

locals {
  cloudflare_account_id = data.keepass_entry.cloudflare_account_id.password
  cloudflare_zone_id = data.keepass_entry.cloudflare_zone_id.password

}

resource "cloudflare_record" "test-site" {
  zone_id = local.cloudflare_zone_id
  name    = "test-site"
  value   = "test-site"
  type    = "CNAME"
  proxied = true
}

but wait. Why use a separate Keepass entry when we can use attributes? If we create a new Keepass entry named cloudflare_secrets with the API token, Zone ID, and Account ID, we can do this:

Keepass Attritbutes Keepass Attritbutes cloudflare.tf

locals {
  cloudflare_account_id = data.keepass_entry.cloudflare_secrets.attributes.zone_id
  cloudflare_zone_id = data.keepass_entry.cloudflare_secrets.attributes.account_id
}

providers.tf

provider "cloudflare" {
  api_token = data.keepass_entry.cloudflare_secrets.password
}

Now all our Cloudflare information is in one Keepass entry and not split with terraform.tfvars.

What if all the entries are sensitive and you want to over-complicate things?

Using a Module

If you have many secrets and they’re referenced multiple times across files, you can create a module. This is overkill for personal use and who uses Keepass in production? Right?sweat The annoying issue with this method is reiterating the secrets, once in the data and once as an output. If you only use a secret once or only have a couple secrets you use multiple times, this isn’t really worth the effort.


Create a folder called keepass. You’ll be creating main.tf and providers.tf in it. You can optionally create a modules folder or place this in your current one.

The main.tf example looks like the large block below. It’s the data blocks we saw earlier and now with outputs that reference them. The nonsensitive() function is to allow you to read the output via terraform console for testing.

> module.cloudflare_secrets.attribute1
"this is an attribute"

keepass/main.tf

data "keepass_entry" "cloudflare_account_id" {
  path = "Root/Cloudflare/cloudflare_account_id"
}

output "cloudflare_account_id" {
  sensitive = true
  value = data.keepass_entry.cloudflare_secrets.attribute.account_id
}


data "keepass_entry" "cloudflare_zone_id" {
  path = "Root/Cloudflare/cloudflare_zone_id"
}
output "cloudflare_zone_id" {
  sensitive = true
  value = data.keepass_entry.cloudflare_secrets.attribute.zone_id
}


data "keepass_entry" "test_entry" {
  path = "Root/Cloudflare/test_entry"
}

output "attribute-test" {
  sensitive = false
  value = nonsensitive(data.keepass_entry.test_entry.attribute)
}

output "test-attribute" {
  sensitive = false
  value = nonsensitive(data.keepass_entry.test_entry.attributes.test-attribute)
}

and the providers.tf looks like: keepass/providers.tf

terraform {
  required_providers {

    keepass = {
      source = "iSchluff/keepass"
    }

  }
}

variable "keepass_master_password" {
  description = "KeePass Master Password"
  type        = string
  default     = null
}

provider "keepass" {
  database = "k3s.kdbx"
  password = var.keepass_master_password
}

You then reference the secrets like so:

cloudflare.tf or your resource.tf

module "cloudflare_secrets" {
  source = "./keepass/"
}


resource "cloudflare_record" "test-record" {
  zone_id = module.cloudflare_secrets.cloudflare_zone_id
  name    = "test-record"
  value   = "test-record"
  type    = "CNAME"
  proxied = true
}

While this places the secrets all in one module, it’s overkill for personal use.

Done

Integrating KeepassXC into Terraform is a practical method to store secrets for personal projects. Using Keepass enhances security and convenience by eliminating the need to use environment variables for every secret, eliminates complicated enterprise infrastructure, and doesn’t rely on paid services. Remember, the key to successful secrets management lies in striking the right balance between security and convenience. For more in-deph information and resources, check out the links below.

https://registry.terraform.io/providers/iSchluff/keepass/latest/docs

https://support.hashicorp.com/hc/en-us/articles/5175257151891-How-to-output-sensitive-data-with-Terraform

https://developer.hashicorp.com/terraform/language/modules

updatedupdated2023-12-112023-12-11