From e90070b9a02e84a5d480ad99caf292e531ea7bd3 Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Thu, 16 Jul 2026 23:45:01 +1000 Subject: [PATCH] Add a gpg_key module and a 'pass' key for password-store Manage OpenPGP keys in the gpg engine from Terraform using the new gpgvaultsecret provider, and create the first key ('pass') for passv. - Add a gpg_key module wrapping the gpg_key resource; wire it through vault_cluster (variable + module, depends_on the mount) and config discovery (config.hcl group keyed /, syd1 terragrunt input), mirroring the *_secret_backend_role modules. - Register the gpg provider in root.hcl (provider block + required_providers, v0.1.0 from the terraform-unkin registry), alongside litellm. - Add config/gpg_key/gpg/pass.yaml: an rsa-4096 key 'pass' in the gpg mount for password-store. Private key stays in Vault; clients import the public key and delegate decryption to gpg/decrypt/pass. --- config/config.hcl | 8 ++++ config/gpg_key/gpg/pass.yaml | 7 ++++ environments/au/syd1/terragrunt.hcl | 1 + environments/root.hcl | 10 +++++ modules/vault_cluster/main.tf | 16 ++++++++ modules/vault_cluster/modules/gpg_key/main.tf | 12 ++++++ .../modules/gpg_key/terraform.tf | 9 +++++ .../modules/gpg_key/variables.tf | 39 +++++++++++++++++++ modules/vault_cluster/variables.tf | 14 +++++++ 9 files changed, 116 insertions(+) create mode 100644 config/gpg_key/gpg/pass.yaml create mode 100644 modules/vault_cluster/modules/gpg_key/main.tf create mode 100644 modules/vault_cluster/modules/gpg_key/terraform.tf create mode 100644 modules/vault_cluster/modules/gpg_key/variables.tf diff --git a/config/config.hcl b/config/config.hcl index 0ecef24..1255ec9 100644 --- a/config/config.hcl +++ b/config/config.hcl @@ -203,5 +203,13 @@ locals { trimsuffix(basename(file_path), ".yaml") => content if startswith(file_path, "gpg_secret_backend/") } + gpg_key = { + for file_path, content in local.all_configs : + trimsuffix(replace(file_path, "gpg_key/", ""), ".yaml") => merge(content, { + name = trimsuffix(basename(file_path), ".yaml") + backend = dirname(replace(file_path, "gpg_key/", "")) + }) + if startswith(file_path, "gpg_key/") + } } } diff --git a/config/gpg_key/gpg/pass.yaml b/config/gpg_key/gpg/pass.yaml new file mode 100644 index 0000000..635a287 --- /dev/null +++ b/config/gpg_key/gpg/pass.yaml @@ -0,0 +1,7 @@ +# config/gpg_key/gpg/pass.yaml +# An OpenPGP key in the gpg engine for password-store (passv). The private key +# stays in Vault; clients import the exported public key to encrypt and delegate +# decryption to gpg/decrypt/pass. Key name = "pass", backend = "gpg". +algorithm: rsa-4096 +identity: "pass " +exportable: false diff --git a/environments/au/syd1/terragrunt.hcl b/environments/au/syd1/terragrunt.hcl index e0e7e7e..261c83f 100644 --- a/environments/au/syd1/terragrunt.hcl +++ b/environments/au/syd1/terragrunt.hcl @@ -71,6 +71,7 @@ inputs = { litellm_secret_backend = local.config.litellm_secret_backend litellm_secret_backend_role = local.config.litellm_secret_backend_role gpg_secret_backend = local.config.gpg_secret_backend + gpg_key = local.config.gpg_key # Pass policy maps to vault_cluster module policy_auth_map = local.policies.policy_auth_map diff --git a/environments/root.hcl b/environments/root.hcl index 7b69eac..09414e7 100644 --- a/environments/root.hcl +++ b/environments/root.hcl @@ -17,6 +17,12 @@ provider "litellm" { address = local.vault_addr } +# The gpg secrets engine's keys are managed through its own provider (same Vault +# server; token falls back to VAULT_TOKEN). +provider "gpg" { + address = local.vault_addr +} + terraform { backend "consul" { address = "https://consul.service.consul" @@ -39,6 +45,10 @@ terraform { source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/litellmvaultsecret" version = "0.1.0" } + gpg = { + source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/gpgvaultsecret" + version = "0.1.0" + } } } EOF diff --git a/modules/vault_cluster/main.tf b/modules/vault_cluster/main.tf index 5db7207..f1766ba 100644 --- a/modules/vault_cluster/main.tf +++ b/modules/vault_cluster/main.tf @@ -346,6 +346,22 @@ module "gpg_secret_backend" { description = each.value.description } +module "gpg_key" { + source = "./modules/gpg_key" + + for_each = var.gpg_key + + backend = each.value.backend + name = each.value.name + algorithm = each.value.algorithm + identity = each.value.identity + exportable = each.value.exportable + deletion_allowed = each.value.deletion_allowed + min_decryption_version = each.value.min_decryption_version + + depends_on = [module.gpg_secret_backend] +} + module "vault_policy" { source = "./modules/vault_policy" diff --git a/modules/vault_cluster/modules/gpg_key/main.tf b/modules/vault_cluster/modules/gpg_key/main.tf new file mode 100644 index 0000000..7a9262f --- /dev/null +++ b/modules/vault_cluster/modules/gpg_key/main.tf @@ -0,0 +1,12 @@ +# Manages an OpenPGP key inside a gpg secrets engine mount, via the +# gpgvaultsecret provider. The private key never leaves Vault; consumers use the +# exported public_key to encrypt and delegate decryption back to the engine. +resource "gpg_key" "this" { + backend = var.backend + name = var.name + algorithm = var.algorithm + identity = var.identity + exportable = var.exportable + deletion_allowed = var.deletion_allowed + min_decryption_version = var.min_decryption_version +} diff --git a/modules/vault_cluster/modules/gpg_key/terraform.tf b/modules/vault_cluster/modules/gpg_key/terraform.tf new file mode 100644 index 0000000..83dfbe2 --- /dev/null +++ b/modules/vault_cluster/modules/gpg_key/terraform.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.10" + required_providers { + gpg = { + source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/gpgvaultsecret" + version = "0.1.0" + } + } +} diff --git a/modules/vault_cluster/modules/gpg_key/variables.tf b/modules/vault_cluster/modules/gpg_key/variables.tf new file mode 100644 index 0000000..57d94a9 --- /dev/null +++ b/modules/vault_cluster/modules/gpg_key/variables.tf @@ -0,0 +1,39 @@ +variable "backend" { + description = "Mount path of the gpg secrets engine (e.g. \"gpg\")" + type = string +} + +variable "name" { + description = "Name of the key" + type = string +} + +variable "algorithm" { + description = "Key algorithm: rsa-2048, rsa-3072, rsa-4096 or ed25519" + type = string + default = "rsa-3072" +} + +variable "identity" { + description = "OpenPGP User ID (defaults to the key name)" + type = string + default = null +} + +variable "exportable" { + description = "Allow exporting the private key (enable-only)" + type = bool + default = false +} + +variable "deletion_allowed" { + description = "Whether the key may be deleted" + type = bool + default = false +} + +variable "min_decryption_version" { + description = "Minimum key version usable for decryption/verification" + type = number + default = null +} diff --git a/modules/vault_cluster/variables.tf b/modules/vault_cluster/variables.tf index 026e419..e47dfe4 100644 --- a/modules/vault_cluster/variables.tf +++ b/modules/vault_cluster/variables.tf @@ -326,6 +326,20 @@ variable "gpg_secret_backend" { default = {} } +variable "gpg_key" { + description = "Map of OpenPGP keys to manage in a gpg engine mount" + type = map(object({ + name = string + backend = string + algorithm = optional(string, "rsa-3072") + identity = optional(string) + exportable = optional(bool, false) + deletion_allowed = optional(bool, false) + min_decryption_version = optional(number) + })) + default = {} +} + variable "policy_auth_map" { description = "Map of auth mounts -> auth roles -> policy names" type = map(map(list(string)))