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.
These entries are all in the root so the paths would be Root/ENTRYNAME
. If we were to organize these entries we would get:
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.
They are referenced by resources using Terraform data blocks.
data.keepass_entry.cloudflare_token.title
= cloudflare_tokendata.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:
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:
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? 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.
Links
https://registry.terraform.io/providers/iSchluff/keepass/latest/docs