From a960757ab22b950ea73ee63b2612d31f5e70f249 Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Mon, 27 Jul 2026 19:05:08 +1000 Subject: [PATCH 1/2] gitea: add the gitea token secrets engine (mount, config, teabot roles) Why: teabot's implementer and reviewer bot users should mint ephemeral, scoped Gitea tokens on demand instead of holding standing personal access tokens (Gitea tokens never expire on their own). This registers and mounts the new vault-plugin-secrets-gitea engine and declares its roles, mirroring the rancher engine wiring. Change: - Register the plugin in the catalog (config/plugins/vault-plugin-secrets-gitea.yaml) pinned to the released v0.1.0 binary sha256. - Add gitea_secret_backend + gitea_secret_backend_role modules and wire them through config.hcl, terragrunt.hcl, and vault_cluster variables/main, using the giteavaultsecret provider (terraform-unkin registry, v0.1.0). - Mount the engine at gitea/ against https://git.unkin.net; seeded site-admin credentials are read from KV (service/vault/au/syd1/secret_backend/gitea/config). - Add teabot-implementer (write:repository, write:issue) and teabot-reviewer (read:repository, write:issue) roles, ttl 1h / max_ttl 4h. Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv --- config/config.hcl | 13 +++++ config/gitea_secret_backend/gitea.yaml | 11 +++++ .../gitea/teabot-implementer.yaml | 14 ++++++ .../gitea/teabot-reviewer.yaml | 14 ++++++ .../plugins/vault-plugin-secrets-gitea.yaml | 11 +++++ environments/au/syd1/terragrunt.hcl | 3 ++ modules/vault_cluster/main.tf | 34 +++++++++++++ .../modules/gitea_secret_backend/main.tf | 23 +++++++++ .../modules/gitea_secret_backend/terraform.tf | 13 +++++ .../modules/gitea_secret_backend/variables.tf | 49 +++++++++++++++++++ .../modules/gitea_secret_backend_role/main.tf | 12 +++++ .../gitea_secret_backend_role/terraform.tf | 9 ++++ .../gitea_secret_backend_role/variables.tf | 37 ++++++++++++++ modules/vault_cluster/variables.tf | 27 ++++++++++ 14 files changed, 270 insertions(+) create mode 100644 config/gitea_secret_backend/gitea.yaml create mode 100644 config/gitea_secret_backend_role/gitea/teabot-implementer.yaml create mode 100644 config/gitea_secret_backend_role/gitea/teabot-reviewer.yaml create mode 100644 config/plugins/vault-plugin-secrets-gitea.yaml create mode 100644 modules/vault_cluster/modules/gitea_secret_backend/main.tf create mode 100644 modules/vault_cluster/modules/gitea_secret_backend/terraform.tf create mode 100644 modules/vault_cluster/modules/gitea_secret_backend/variables.tf create mode 100644 modules/vault_cluster/modules/gitea_secret_backend_role/main.tf create mode 100644 modules/vault_cluster/modules/gitea_secret_backend_role/terraform.tf create mode 100644 modules/vault_cluster/modules/gitea_secret_backend_role/variables.tf diff --git a/config/config.hcl b/config/config.hcl index 3ad9136..19b655d 100644 --- a/config/config.hcl +++ b/config/config.hcl @@ -239,5 +239,18 @@ locals { }) if startswith(file_path, "rancher_secret_backend_role/") } + gitea_secret_backend = { + for file_path, content in local.all_configs : + trimsuffix(basename(file_path), ".yaml") => content + if startswith(file_path, "gitea_secret_backend/") + } + gitea_secret_backend_role = { + for file_path, content in local.all_configs : + trimsuffix(replace(file_path, "gitea_secret_backend_role/", ""), ".yaml") => merge(content, { + name = trimsuffix(basename(file_path), ".yaml") + backend = dirname(replace(file_path, "gitea_secret_backend_role/", "")) + }) + if startswith(file_path, "gitea_secret_backend_role/") + } } } diff --git a/config/gitea_secret_backend/gitea.yaml b/config/gitea_secret_backend/gitea.yaml new file mode 100644 index 0000000..363c065 --- /dev/null +++ b/config/gitea_secret_backend/gitea.yaml @@ -0,0 +1,11 @@ +# Mounts the gitea token secrets engine at "gitea" and writes its config. +# The seeded site-admin credentials are sensitive and read from KV, not stored +# here: +# kv/service/vault/au/syd1/secret_backend/gitea/config +# -> keys: admin_username (required), admin_password (required) +# Populate that KV path with a purpose-built Gitea site-admin bot (2FA disabled) +# BEFORE applying, then run `vault write -f gitea/config/rotate-root` after the +# first apply so only Vault holds the admin password. +description: "Gitea ephemeral scoped access token engine" +gitea_url: "https://git.unkin.net" +request_timeout_seconds: 30 diff --git a/config/gitea_secret_backend_role/gitea/teabot-implementer.yaml b/config/gitea_secret_backend_role/gitea/teabot-implementer.yaml new file mode 100644 index 0000000..3f2ce54 --- /dev/null +++ b/config/gitea_secret_backend_role/gitea/teabot-implementer.yaml @@ -0,0 +1,14 @@ +# Role minting ephemeral tokens for the teabot-implementer bot user. +# The implementer clones/pushes code and opens pull requests, so it gets write +# on repositories (clone + push + PR create) and write on issues (PR/issue +# comments). Read is implied by write. No admin/org/user-write scopes. +# Reading gitea/creds/teabot-implementer mints a lease-bound token deleted from +# Gitea on revoke/expiry. +--- +username: teabot-implementer +scopes: + - write:repository + - write:issue +token_name_prefix: vault-teabot-implementer +ttl: 3600 # 1h +max_ttl: 14400 # 4h diff --git a/config/gitea_secret_backend_role/gitea/teabot-reviewer.yaml b/config/gitea_secret_backend_role/gitea/teabot-reviewer.yaml new file mode 100644 index 0000000..b1473f4 --- /dev/null +++ b/config/gitea_secret_backend_role/gitea/teabot-reviewer.yaml @@ -0,0 +1,14 @@ +# Role minting ephemeral tokens for the teabot-reviewer bot user. +# The reviewer reads code and posts pull-request reviews/comments, so it gets +# read on repositories (fetch diffs) and write on issues (PR reviews + issue/PR +# comments). No repository-write, admin, org, or user scopes. +# Reading gitea/creds/teabot-reviewer mints a lease-bound token deleted from +# Gitea on revoke/expiry. +--- +username: teabot-reviewer +scopes: + - read:repository + - write:issue +token_name_prefix: vault-teabot-reviewer +ttl: 3600 # 1h +max_ttl: 14400 # 4h diff --git a/config/plugins/vault-plugin-secrets-gitea.yaml b/config/plugins/vault-plugin-secrets-gitea.yaml new file mode 100644 index 0000000..b101877 --- /dev/null +++ b/config/plugins/vault-plugin-secrets-gitea.yaml @@ -0,0 +1,11 @@ +# config/plugins/vault-plugin-secrets-gitea.yaml +# Imports (registers) the gitea secrets plugin in the catalog. Filename = +# catalog name = mount type. The binary is installed on the OpenBao nodes by +# Puppet (openbao-plugin-secrets-gitea RPM -> +# /opt/openbao-plugins/vault-plugin-secrets-gitea). +# +# 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-gitea +sha256: "8f67fbc216effada5fd7399888a710b62fad83be0b31761a439e7dec3d56509b" diff --git a/environments/au/syd1/terragrunt.hcl b/environments/au/syd1/terragrunt.hcl index a772255..98a9415 100644 --- a/environments/au/syd1/terragrunt.hcl +++ b/environments/au/syd1/terragrunt.hcl @@ -78,6 +78,9 @@ inputs = { rancher_secret_backend_service_account = local.config.rancher_secret_backend_service_account rancher_secret_backend_role = local.config.rancher_secret_backend_role + gitea_secret_backend = local.config.gitea_secret_backend + gitea_secret_backend_role = local.config.gitea_secret_backend_role + # Pass policy maps to vault_cluster module policy_auth_map = local.policies.policy_auth_map policy_rules_map = local.policies.policy_rules_map diff --git a/modules/vault_cluster/main.tf b/modules/vault_cluster/main.tf index 0721e81..3940844 100644 --- a/modules/vault_cluster/main.tf +++ b/modules/vault_cluster/main.tf @@ -421,6 +421,40 @@ module "rancher_secret_backend_role" { depends_on = [module.rancher_secret_backend_service_account] } +module "gitea_secret_backend" { + source = "./modules/gitea_secret_backend" + + for_each = var.gitea_secret_backend + + path = each.key + plugin = each.value.plugin + description = each.value.description + gitea_url = each.value.gitea_url + country = var.country + region = var.region + ca_cert = each.value.ca_cert + tls_skip_verify = each.value.tls_skip_verify + request_timeout_seconds = each.value.request_timeout_seconds + + depends_on = [module.plugin] +} + +module "gitea_secret_backend_role" { + source = "./modules/gitea_secret_backend_role" + + for_each = var.gitea_secret_backend_role + + backend = each.value.backend + name = each.value.name + username = each.value.username + scopes = each.value.scopes + token_name_prefix = each.value.token_name_prefix + ttl = each.value.ttl + max_ttl = each.value.max_ttl + + depends_on = [module.gitea_secret_backend] +} + module "vault_policy" { source = "./modules/vault_policy" diff --git a/modules/vault_cluster/modules/gitea_secret_backend/main.tf b/modules/vault_cluster/modules/gitea_secret_backend/main.tf new file mode 100644 index 0000000..3d37191 --- /dev/null +++ b/modules/vault_cluster/modules/gitea_secret_backend/main.tf @@ -0,0 +1,23 @@ +# Mounts the gitea secrets engine and writes its connection config via the +# giteavaultsecret provider. The plugin is registered ("imported") in the +# catalog separately (config/plugins/vault-plugin-secrets-gitea.yaml). The +# seeded site-admin credentials are sensitive and read from KV, not stored in +# git: +# kv/service/vault///secret_backend//config +# Expected keys: admin_username (required), admin_password (required). +data "vault_kv_secret_v2" "config" { + mount = "kv" + name = "service/vault/${var.country}/${var.region}/secret_backend/${var.path}/config" +} + +resource "gitea_secret_backend" "this" { + path = var.path + plugin = var.plugin + description = var.description + gitea_url = var.gitea_url + admin_username = data.vault_kv_secret_v2.config.data["admin_username"] + admin_password = data.vault_kv_secret_v2.config.data["admin_password"] + ca_cert = var.ca_cert + tls_skip_verify = var.tls_skip_verify + request_timeout_seconds = var.request_timeout_seconds +} diff --git a/modules/vault_cluster/modules/gitea_secret_backend/terraform.tf b/modules/vault_cluster/modules/gitea_secret_backend/terraform.tf new file mode 100644 index 0000000..98f55df --- /dev/null +++ b/modules/vault_cluster/modules/gitea_secret_backend/terraform.tf @@ -0,0 +1,13 @@ +terraform { + required_version = ">= 1.10" + required_providers { + vault = { + source = "hashicorp/vault" + version = "5.6.0" + } + gitea = { + source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/giteavaultsecret" + version = "0.1.0" + } + } +} diff --git a/modules/vault_cluster/modules/gitea_secret_backend/variables.tf b/modules/vault_cluster/modules/gitea_secret_backend/variables.tf new file mode 100644 index 0000000..11467f6 --- /dev/null +++ b/modules/vault_cluster/modules/gitea_secret_backend/variables.tf @@ -0,0 +1,49 @@ +variable "path" { + description = "Mount path of the gitea secrets engine (e.g. \"gitea\")" + type = string +} + +variable "plugin" { + description = "Registered plugin name to mount (the catalog name = mount type)" + type = string + default = "vault-plugin-secrets-gitea" +} + +variable "description" { + description = "Human-friendly description of the mount" + type = string + default = null +} + +variable "gitea_url" { + description = "Base URL of the Gitea server (e.g. https://git.unkin.net)" + type = string +} + +variable "country" { + description = "Country segment of the KV path holding the seeded admin credentials" + type = string +} + +variable "region" { + description = "Region segment of the KV path holding the seeded admin credentials" + type = string +} + +variable "ca_cert" { + description = "PEM CA certificate that signed the Gitea server's TLS cert (optional; omit to use the system trust store)" + type = string + default = null +} + +variable "tls_skip_verify" { + description = "Skip TLS verification of the Gitea server (not recommended)" + type = bool + default = false +} + +variable "request_timeout_seconds" { + description = "HTTP timeout in seconds for calls from the plugin to Gitea" + type = number + default = 30 +} diff --git a/modules/vault_cluster/modules/gitea_secret_backend_role/main.tf b/modules/vault_cluster/modules/gitea_secret_backend_role/main.tf new file mode 100644 index 0000000..be9943b --- /dev/null +++ b/modules/vault_cluster/modules/gitea_secret_backend_role/main.tf @@ -0,0 +1,12 @@ +# A role that mints short-lived, scoped gitea tokens for a target Gitea user. +# Reading gitea/creds/ produces a lease-bound token that is deleted from +# Gitea when the lease is revoked or reaches max_ttl. +resource "gitea_secret_backend_role" "this" { + backend = var.backend + name = var.name + username = var.username + scopes = var.scopes + token_name_prefix = var.token_name_prefix + ttl = var.ttl + max_ttl = var.max_ttl +} diff --git a/modules/vault_cluster/modules/gitea_secret_backend_role/terraform.tf b/modules/vault_cluster/modules/gitea_secret_backend_role/terraform.tf new file mode 100644 index 0000000..6a5585b --- /dev/null +++ b/modules/vault_cluster/modules/gitea_secret_backend_role/terraform.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.10" + required_providers { + gitea = { + source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/giteavaultsecret" + version = "0.1.0" + } + } +} diff --git a/modules/vault_cluster/modules/gitea_secret_backend_role/variables.tf b/modules/vault_cluster/modules/gitea_secret_backend_role/variables.tf new file mode 100644 index 0000000..b8472a5 --- /dev/null +++ b/modules/vault_cluster/modules/gitea_secret_backend_role/variables.tf @@ -0,0 +1,37 @@ +variable "backend" { + description = "Mount path of the gitea secrets engine this role belongs to" + type = string +} + +variable "name" { + description = "Role name (read gitea/creds/ to mint a token)" + type = string +} + +variable "username" { + description = "Target Gitea username the minted tokens belong to" + type = string +} + +variable "scopes" { + description = "Gitea access-token scopes granted to minted tokens (write: implies read:)" + type = list(string) +} + +variable "token_name_prefix" { + description = "Prefix for the generated Gitea token name (optional)" + type = string + default = null +} + +variable "ttl" { + description = "Default lease TTL in seconds for minted tokens" + type = number + default = null +} + +variable "max_ttl" { + description = "Maximum lease TTL in seconds for minted tokens" + type = number + default = null +} diff --git a/modules/vault_cluster/variables.tf b/modules/vault_cluster/variables.tf index a013a8a..2c59ddc 100644 --- a/modules/vault_cluster/variables.tf +++ b/modules/vault_cluster/variables.tf @@ -388,6 +388,33 @@ variable "rancher_secret_backend_role" { default = {} } +variable "gitea_secret_backend" { + description = "Map of gitea token secret engines to create (mount + config; seeded admin creds read from KV)" + type = map(object({ + plugin = optional(string, "vault-plugin-secrets-gitea") + description = optional(string) + gitea_url = string + ca_cert = optional(string) + tls_skip_verify = optional(bool, false) + request_timeout_seconds = optional(number, 30) + })) + default = {} +} + +variable "gitea_secret_backend_role" { + description = "Map of gitea token-minting roles to create" + type = map(object({ + name = string + backend = string + username = string + scopes = list(string) + token_name_prefix = optional(string) + ttl = optional(number) + max_ttl = optional(number) + })) + default = {} +} + variable "policy_auth_map" { description = "Map of auth mounts -> auth roles -> policy names" type = map(map(list(string))) -- 2.47.3 From 13c1f5c1ba47dc56effc004584cd2010de044161 Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Mon, 27 Jul 2026 19:31:12 +1000 Subject: [PATCH 2/2] gitea: consume the KV admin seed only at engine-config creation Why: after the engine is first configured, rotate-root changes the live admin password so it diverges from the KV seed. Re-reading the (possibly stale) seed on a later apply must not push it back and clobber the rotated password. Ben asked that the seed be used only when initially creating the config. Change: - Add lifecycle ignore_changes on admin_username/admin_password to the gitea_secret_backend resource, making the module create-only for the seeded credential. Noted inline that the rancher/litellm seed modules lack this. Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv --- .../modules/gitea_secret_backend/main.tf | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/modules/vault_cluster/modules/gitea_secret_backend/main.tf b/modules/vault_cluster/modules/gitea_secret_backend/main.tf index 3d37191..997e128 100644 --- a/modules/vault_cluster/modules/gitea_secret_backend/main.tf +++ b/modules/vault_cluster/modules/gitea_secret_backend/main.tf @@ -20,4 +20,15 @@ resource "gitea_secret_backend" "this" { ca_cert = var.ca_cert tls_skip_verify = var.tls_skip_verify request_timeout_seconds = var.request_timeout_seconds + + lifecycle { + # The KV seed is a bootstrap credential: it is consumed only when the engine + # config is first created. After creation the live admin password is rotated + # in place (vault write -f gitea/config/rotate-root) and diverges from the + # seed, so re-reading the (possibly stale) KV value must never push it back. + # Ignoring the credential attributes makes this module create-only for them. + # (The sibling rancher/litellm seed modules do not yet do this and would + # re-push their seed on a subsequent apply.) + ignore_changes = [admin_username, admin_password] + } } -- 2.47.3