From 9c4d20da9e994530f0442dadd194c8dfb6a9f40b Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Fri, 17 Jul 2026 23:13:24 +1000 Subject: [PATCH] Mount the gpg engine and manage a 'pass' key Mount the gpg secrets engine (the plugin is registered separately via the plugin module + config/plugins, #89) and manage OpenPGP keys with the gpgvaultsecret provider. - gpg_secret_backend module: mounts the registered plugin type; depends_on the plugin module so the mount waits for catalog registration. - gpg_key module (via the gpgvaultsecret provider, registered in root.hcl): create/configure keys; wired through vault_cluster + config discovery (config/gpg_key//.yaml). - config/gpg_secret_backend/gpg.yaml mounts at gpg/; config/gpg_key/gpg/pass.yaml is an rsa-4096 'pass' key for password-store (private key stays in Vault; clients decrypt via gpg/decrypt/pass). Rebased onto master after the access (#88) and plugin-import (#89) PRs merged. --- config/config.hcl | 13 +++++++ config/gpg_key/gpg/pass.yaml | 7 ++++ config/gpg_secret_backend/gpg.yaml | 4 ++ environments/au/syd1/terragrunt.hcl | 2 + environments/root.hcl | 10 +++++ modules/vault_cluster/main.tf | 28 +++++++++++++ modules/vault_cluster/modules/gpg_key/main.tf | 12 ++++++ .../modules/gpg_key/terraform.tf | 9 +++++ .../modules/gpg_key/variables.tf | 39 +++++++++++++++++++ .../modules/gpg_secret_backend/main.tf | 8 ++++ .../modules/gpg_secret_backend/terraform.tf | 9 +++++ .../modules/gpg_secret_backend/variables.tf | 16 ++++++++ modules/vault_cluster/variables.tf | 23 +++++++++++ 13 files changed, 180 insertions(+) create mode 100644 config/gpg_key/gpg/pass.yaml create mode 100644 config/gpg_secret_backend/gpg.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 create mode 100644 modules/vault_cluster/modules/gpg_secret_backend/main.tf create mode 100644 modules/vault_cluster/modules/gpg_secret_backend/terraform.tf create mode 100644 modules/vault_cluster/modules/gpg_secret_backend/variables.tf diff --git a/config/config.hcl b/config/config.hcl index 5ca4f26..e971fe1 100644 --- a/config/config.hcl +++ b/config/config.hcl @@ -205,5 +205,18 @@ locals { }) if startswith(file_path, "plugins/") } + gpg_secret_backend = { + for file_path, content in local.all_configs : + 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/config/gpg_secret_backend/gpg.yaml b/config/gpg_secret_backend/gpg.yaml new file mode 100644 index 0000000..c7ccc11 --- /dev/null +++ b/config/gpg_secret_backend/gpg.yaml @@ -0,0 +1,4 @@ +# config/gpg_secret_backend/gpg.yaml +# Mounts the gpg secrets engine at "gpg". The plugin itself is registered in the +# catalog separately (see config/plugins/vault-plugin-secrets-gpg.yaml). +description: "GPG/OpenPGP secrets engine (sign/verify/encrypt/decrypt)" diff --git a/environments/au/syd1/terragrunt.hcl b/environments/au/syd1/terragrunt.hcl index e481db7..dbc5f18 100644 --- a/environments/au/syd1/terragrunt.hcl +++ b/environments/au/syd1/terragrunt.hcl @@ -71,6 +71,8 @@ inputs = { litellm_secret_backend = local.config.litellm_secret_backend litellm_secret_backend_role = local.config.litellm_secret_backend_role plugins = local.config.plugins + 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 f2bdbf5..d233cd9 100644 --- a/modules/vault_cluster/main.tf +++ b/modules/vault_cluster/main.tf @@ -346,6 +346,34 @@ module "plugin" { plugin_version = each.value.version } +module "gpg_secret_backend" { + source = "./modules/gpg_secret_backend" + + for_each = var.gpg_secret_backend + + path = each.key + plugin = each.value.plugin + description = each.value.description + + depends_on = [module.plugin] +} + +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/modules/gpg_secret_backend/main.tf b/modules/vault_cluster/modules/gpg_secret_backend/main.tf new file mode 100644 index 0000000..39de702 --- /dev/null +++ b/modules/vault_cluster/modules/gpg_secret_backend/main.tf @@ -0,0 +1,8 @@ +# Mounts the gpg secrets engine. The plugin is registered ("imported") in the +# catalog separately via the config/plugins/ discovery and the plugin module; +# this module just enables a mount of the already-registered plugin type. +resource "vault_mount" "this" { + path = var.path + type = var.plugin + description = var.description +} diff --git a/modules/vault_cluster/modules/gpg_secret_backend/terraform.tf b/modules/vault_cluster/modules/gpg_secret_backend/terraform.tf new file mode 100644 index 0000000..ce22437 --- /dev/null +++ b/modules/vault_cluster/modules/gpg_secret_backend/terraform.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.10" + required_providers { + vault = { + source = "hashicorp/vault" + version = "5.6.0" + } + } +} diff --git a/modules/vault_cluster/modules/gpg_secret_backend/variables.tf b/modules/vault_cluster/modules/gpg_secret_backend/variables.tf new file mode 100644 index 0000000..849ee30 --- /dev/null +++ b/modules/vault_cluster/modules/gpg_secret_backend/variables.tf @@ -0,0 +1,16 @@ +variable "path" { + description = "Mount path of the GPG secrets engine (e.g. \"gpg\")" + type = string +} + +variable "plugin" { + description = "Registered plugin name to mount (the catalog name = mount type)" + type = string + default = "vault-plugin-secrets-gpg" +} + +variable "description" { + description = "Human-friendly description of the mount" + type = string + default = null +} diff --git a/modules/vault_cluster/variables.tf b/modules/vault_cluster/variables.tf index ebd03a7..cbafb03 100644 --- a/modules/vault_cluster/variables.tf +++ b/modules/vault_cluster/variables.tf @@ -327,6 +327,29 @@ variable "plugins" { default = {} } +variable "gpg_secret_backend" { + description = "Map of GPG/OpenPGP secret engines to mount (path => registered plugin + description). The plugin is registered separately via config/plugins." + type = map(object({ + plugin = optional(string, "vault-plugin-secrets-gpg") + description = optional(string) + })) + 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))) -- 2.47.3