2 Commits

Author SHA1 Message Date
Ben Vincent 3fe5806ff5 Import (register) the rancher plugin in the catalog
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/plan Pipeline was successful
Registers the released v0.1.0 rancher secrets plugin in the OpenBao plugin
catalog so it can be mounted. The binary is laid down on the vault nodes by
Puppet (openbao-plugin-secrets-rancher RPM).

- Add config/plugins/vault-plugin-secrets-rancher.yaml (type=secret,
  command=vault-plugin-secrets-rancher, sha256 pinned to the released binary).
2026-07-17 23:38:13 +10:00
unkinben 933de177fa Register + mount the GPG secrets engine at gpg/ (#87)
ci/woodpecker/push/apply Pipeline was successful
Complete the deploy of the [vault-plugin-secrets-gpg](https://git.unkin.net/unkin/vault-plugin-secrets-gpg) engine. Puppet ([puppet-prod #480](unkin/puppet-prod#480)) installs the `openbao-plugin-secrets-gpg` RPM onto the OpenBao nodes; this registers that binary in the plugin catalog and enables the secrets engine so `gpg/` is actually usable.

- Add a `gpg_secret_backend` module using the standard `hashicorp/vault` provider (already required at 5.6.0): `vault_plugin` (catalog register with a pinned sha256) + `vault_mount` (enable at the mount path).
- Wire it through `vault_cluster` (new `gpg_secret_backend` variable + module block) and the config discovery (`config.hcl` group + syd1 terragrunt input), mirroring `litellm_secret_backend`.
- Add `config/gpg_secret_backend/gpg.yaml` mounting at `gpg/` and pinning the released v0.1.0 binary sha256 (`0e92d740…a7b20`, extracted from the published RPM). Puppet installs the RPM floating, so this sha must be bumped in lockstep on any plugin upgrade or OpenBao rejects the binary.

Validated locally with `tofu validate` + `tofu fmt`. Granting non-root access to `gpg/*` (auth roles + policies) is a follow-up scoped to whoever consumes the engine (e.g. passv from CI).

Reviewed-on: #87
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-17 23:19:38 +10:00
15 changed files with 191 additions and 13 deletions
+13
View File
@@ -205,5 +205,18 @@ locals {
}) })
if startswith(file_path, "plugins/") 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/")
}
} }
} }
+7
View File
@@ -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 <pass@unkin.net>"
exportable: false
+4
View File
@@ -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)"
@@ -1,13 +0,0 @@
# config/plugins/vault-plugin-secrets-litellm.yaml
# Imports (registers) the litellm secrets plugin in the catalog. This plugin was
# registered manually before terraform managed the catalog, so its state must be
# imported before the first apply (see the PR description) — otherwise apply
# tries to create an entry that already exists.
#
# sha256 is the released v0.1.1 openbao binary
# (openbao-plugin-secrets-litellm RPM -> /opt/openbao-plugins/vault-plugin-secrets-litellm),
# which Puppet installs floating. Verify against the live catalog during import
# (`bao read sys/plugins/catalog/secret/vault-plugin-secrets-litellm`).
type: secret
command: vault-plugin-secrets-litellm
sha256: "2263ebcb3498877a87ddcf31a9cbc6efca6b81702a5faecf7fe7e40200ca7a1f"
@@ -0,0 +1,11 @@
# config/plugins/vault-plugin-secrets-rancher.yaml
# Imports (registers) the rancher secrets plugin in the catalog. Filename =
# catalog name = mount type. The binary is installed on the OpenBao nodes by
# Puppet (openbao-plugin-secrets-rancher RPM ->
# /opt/openbao-plugins/vault-plugin-secrets-rancher).
#
# sha256 pins the released v0.1.0 binary; bump it in lockstep with any RPM
# upgrade or OpenBao will refuse to launch the plugin.
type: secret
command: vault-plugin-secrets-rancher
sha256: "d2b17f45b5fa9e6549443fec3ba22f17ef575d2d811c14324187dc9904ec574a"
+2
View File
@@ -71,6 +71,8 @@ inputs = {
litellm_secret_backend = local.config.litellm_secret_backend litellm_secret_backend = local.config.litellm_secret_backend
litellm_secret_backend_role = local.config.litellm_secret_backend_role litellm_secret_backend_role = local.config.litellm_secret_backend_role
plugins = local.config.plugins 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 # Pass policy maps to vault_cluster module
policy_auth_map = local.policies.policy_auth_map policy_auth_map = local.policies.policy_auth_map
+10
View File
@@ -17,6 +17,12 @@ provider "litellm" {
address = local.vault_addr 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 { terraform {
backend "consul" { backend "consul" {
address = "https://consul.service.consul" address = "https://consul.service.consul"
@@ -39,6 +45,10 @@ terraform {
source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/litellmvaultsecret" source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/litellmvaultsecret"
version = "0.1.0" version = "0.1.0"
} }
gpg = {
source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/gpgvaultsecret"
version = "0.1.0"
}
} }
} }
EOF EOF
+28
View File
@@ -346,6 +346,34 @@ module "plugin" {
plugin_version = each.value.version 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" { module "vault_policy" {
source = "./modules/vault_policy" source = "./modules/vault_policy"
@@ -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
}
@@ -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"
}
}
}
@@ -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
}
@@ -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
}
@@ -0,0 +1,9 @@
terraform {
required_version = ">= 1.10"
required_providers {
vault = {
source = "hashicorp/vault"
version = "5.6.0"
}
}
}
@@ -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
}
+23
View File
@@ -327,6 +327,29 @@ variable "plugins" {
default = {} 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" { variable "policy_auth_map" {
description = "Map of auth mounts -> auth roles -> policy names" description = "Map of auth mounts -> auth roles -> policy names"
type = map(map(list(string))) type = map(map(list(string)))