2 Commits

Author SHA1 Message Date
unkinben 2c27395613 policies: let terraform-git seed the gitea engine admin credential to KV (#102)
ci/woodpecker/push/apply Pipeline was successful
## Why

terraform-git now provisions the `gitea-vault-admin` site-admin bot and writes its generated password to `kv/service/vault/au/syd1/secret_backend/gitea/config` (as `admin_username` + `admin_password`) so the gitea secrets engine can consume it at creation time. The `woodpecker_terraform_git` / `terraform_git` identity has no write access to that KV path, so its apply would 403 without this grant.

The deployer that *reads* the seed already has read access via `policies/kv/service/vault/secret_backends_read.yaml` (`kv/data/service/vault/+/+/secret_backend/*`), so only the write side is added here.

## Change

- Add `policies/kv/service/vault/au/syd1/secret_backend/gitea/config_write.yaml` granting `create`/`read`/`update` on the gitea config KV path to the `terraform_git` approle and `woodpecker_terraform_git` k8s role.

## Ordering

Merge + apply this before the terraform-git `benvin/gitea-vault-admin` PR applies (which performs the write). Files are disjoint from the other gitea terraform-vault PRs (#100, #101).

https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
Reviewed-on: #102
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-27 20:22:40 +10:00
unkinben d289775e38 policies: grant the vault deployer access to the gitea secrets engine (#100)
ci/woodpecker/push/apply Pipeline was successful
## Why

The forthcoming `gitea_secret_backend` + role configuration (separate PR, `benvin/gitea-secret-engine`) is applied by terraform-vault under the deployment identity (`tf_vault` approle / `woodpecker_terraform_vault` k8s role). That identity has no access to the `gitea/` mount yet, so writing the engine's config and roles would 403. This mirrors `policies/rancher/admin.yaml`.

## Change

- Add `policies/gitea/admin.yaml` granting the deployer:
  - create/read/update/delete on `gitea/config`
  - create/update on `gitea/config/rotate-root` (write-only rotation trigger)
  - full manage + list on `gitea/roles/*` (and list on `gitea/roles`)
- Deliberately excludes `gitea/creds/*` — minting tokens is for consumers, not the deployer.
- No new catalog or mount grant: plugin registration is already covered by the shared, sudo-protected wildcard in `policies/sys/plugins/catalog/admin.yaml`, and mounting uses the deployer's existing `sys/mounts/*` access — same as the rancher engine.

## Order

Merge and apply this **before** the `benvin/gitea-secret-engine` PR, so the deployer can write the engine config/roles on that apply.

https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
Reviewed-on: #100
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-07-27 19:10:21 +10:00
16 changed files with 62 additions and 281 deletions
-13
View File
@@ -239,18 +239,5 @@ locals {
}) })
if startswith(file_path, "rancher_secret_backend_role/") 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/")
}
} }
} }
-11
View File
@@ -1,11 +0,0 @@
# 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
@@ -1,14 +0,0 @@
# 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
@@ -1,14 +0,0 @@
# 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
@@ -1,11 +0,0 @@
# 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"
-3
View File
@@ -78,9 +78,6 @@ inputs = {
rancher_secret_backend_service_account = local.config.rancher_secret_backend_service_account rancher_secret_backend_service_account = local.config.rancher_secret_backend_service_account
rancher_secret_backend_role = local.config.rancher_secret_backend_role 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 # Pass policy maps to vault_cluster module
policy_auth_map = local.policies.policy_auth_map policy_auth_map = local.policies.policy_auth_map
policy_rules_map = local.policies.policy_rules_map policy_rules_map = local.policies.policy_rules_map
-34
View File
@@ -421,40 +421,6 @@ module "rancher_secret_backend_role" {
depends_on = [module.rancher_secret_backend_service_account] 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" { module "vault_policy" {
source = "./modules/vault_policy" source = "./modules/vault_policy"
@@ -1,34 +0,0 @@
# 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/<country>/<region>/secret_backend/<path>/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
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]
}
}
@@ -1,13 +0,0 @@
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"
}
}
}
@@ -1,49 +0,0 @@
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
}
@@ -1,12 +0,0 @@
# A role that mints short-lived, scoped gitea tokens for a target Gitea user.
# Reading gitea/creds/<name> 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
}
@@ -1,9 +0,0 @@
terraform {
required_version = ">= 1.10"
required_providers {
gitea = {
source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/giteavaultsecret"
version = "0.1.0"
}
}
}
@@ -1,37 +0,0 @@
variable "backend" {
description = "Mount path of the gitea secrets engine this role belongs to"
type = string
}
variable "name" {
description = "Role name (read gitea/creds/<name> 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
}
-27
View File
@@ -388,33 +388,6 @@ variable "rancher_secret_backend_role" {
default = {} 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" { 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)))
+42
View File
@@ -0,0 +1,42 @@
# Allow the vault deployer to manage the gitea token secrets engine: its
# connection config (seeded admin credentials), in-place root rotation, and
# token-minting roles.
#
# Scoped to gitea/* only, and deliberately excludes gitea/creds/* — minting
# tokens is for consumers, not the deployer. The plugin-catalog grant needed to
# import the plugin is the shared, sudo-protected wildcard in
# policies/sys/plugins/catalog/admin.yaml (already covers this plugin), and
# mounting the engine uses the deployer's existing sys/mounts/* access, so no
# new catalog/mount grant is added here (mirrors the rancher engine).
---
rules:
# Engine connection config (Gitea URL, TLS, seeded admin username/password).
- path: "gitea/config"
capabilities:
- create
- read
- update
- delete
# In-place rotation of the seeded admin password (write-only trigger).
- path: "gitea/config/rotate-root"
capabilities:
- create
- update
# Token-minting roles.
- path: "gitea/roles/*"
capabilities:
- create
- read
- update
- delete
- list
- path: "gitea/roles"
capabilities:
- read
- list
auth:
approle:
- tf_vault
k8s/au/syd1:
- woodpecker_terraform_vault
@@ -0,0 +1,20 @@
# Allow terraform-git to seed (write once) the gitea secrets engine's admin
# credentials. terraform-git creates the gitea-vault-admin site-admin bot and
# writes its generated password here as admin_username + admin_password; the
# vault gitea engine (managed by the tf_vault deployer) reads it at gitea/config
# creation time. Read is already granted to the deployer via
# policies/kv/service/vault/secret_backends_read.yaml, so this only adds the
# write side for terraform-git's own identity.
---
rules:
- path: "kv/data/service/vault/au/syd1/secret_backend/gitea/config"
capabilities:
- create
- read
- update
auth:
approle:
- terraform_git
k8s/au/syd1:
- woodpecker_terraform_git