vault: wire up ghp secrets engine (backend + role + policies) (#121)
ci/woodpecker/push/apply Pipeline failed
ci/woodpecker/push/apply Pipeline failed
Why: final step of the ordered ghp plugin add. Mounts + configures the ghp OpenBao secrets engine and its token-minting role (catalog registration is #123; policies #122). How: config/ghp_secret_backend/ghp.yaml (mount at ghp; base_url https://ghp.unkin.net; admin_token read from KV kv/service/vault/au/syd1/secret_backend/ghp/config), config/ghp_secret_backend_role/ghp/agent.yaml (agent role), the two vault_cluster submodules, and the config.hcl ghp_secret_backend/role discovery + terragrunt inputs pulling the ghp provider (vault-secrets-ghp v0.1.0). ORDER — apply in sequence: 1. puppet-prod#520 — install openbao-plugin-secrets-ghp binary (merged). 2. terraform-vault#122 — config-write policy (merged). 3. terraform-vault#123 — register plugin in catalog. 4. THIS PR — mount + config + role. Before apply is clean: set the role real ghp App installation_id (currently sentinel 0), and KV dual-seed the ghpsvc_ token (this engine admin_token + the ghp deployment GHP_AUTH_SERVICE_TOKENS). Reviewed-on: #121 Co-authored-by: unkin-agent <unkin-agent@unkin.net> Co-committed-by: unkin-agent <unkin-agent@unkin.net>
This commit was merged in pull request #121.
This commit is contained in:
@@ -266,5 +266,18 @@ locals {
|
|||||||
})
|
})
|
||||||
if startswith(file_path, "netbox_secret_backend_role/")
|
if startswith(file_path, "netbox_secret_backend_role/")
|
||||||
}
|
}
|
||||||
|
ghp_secret_backend = {
|
||||||
|
for file_path, content in local.all_configs :
|
||||||
|
trimsuffix(basename(file_path), ".yaml") => content
|
||||||
|
if startswith(file_path, "ghp_secret_backend/")
|
||||||
|
}
|
||||||
|
ghp_secret_backend_role = {
|
||||||
|
for file_path, content in local.all_configs :
|
||||||
|
trimsuffix(replace(file_path, "ghp_secret_backend_role/", ""), ".yaml") => merge(content, {
|
||||||
|
name = trimsuffix(basename(file_path), ".yaml")
|
||||||
|
backend = dirname(replace(file_path, "ghp_secret_backend_role/", ""))
|
||||||
|
})
|
||||||
|
if startswith(file_path, "ghp_secret_backend_role/")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Mounts the ghp token secrets engine at "ghp" and writes its config.
|
||||||
|
# The seeded ghp service token is sensitive and read from KV, not stored here:
|
||||||
|
# kv/service/vault/au/syd1/secret_backend/ghp/config
|
||||||
|
# -> key: admin_token (required) the shared ghpsvc_... service token
|
||||||
|
#
|
||||||
|
# admin_token is a static shared secret provisioned into KV by an operator. The
|
||||||
|
# SAME token value must also be present in the running ghp deployment's accepted
|
||||||
|
# service tokens (GHP_AUTH_SERVICE_TOKENS) so ghp ACCEPTS what this engine
|
||||||
|
# PRESENTS. ghp has no rotate endpoint, so the engine never rotates it in place;
|
||||||
|
# the mount uses ignore_changes=[admin_token], making the KV seed create-only
|
||||||
|
# (re-reading a stale KV value never re-pushes it to a live mount).
|
||||||
|
description: "ghp ephemeral scoped agent token engine"
|
||||||
|
base_url: "https://ghp.unkin.net"
|
||||||
|
tls_skip_verify: false
|
||||||
|
request_timeout_seconds: 30
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Role minting ephemeral, scoped ghp agent tokens. Reading ghp/creds/agent mints
|
||||||
|
# a lease-bound token deleted from ghp on revoke/expiry. token_type "agent" binds
|
||||||
|
# the minted token to a ghp App installation, so installation_id is REQUIRED.
|
||||||
|
#
|
||||||
|
# installation_id below is a PLACEHOLDER (0) and MUST be set to the real ghp App
|
||||||
|
# installation id before this role can mint usable tokens. scopes are ghp
|
||||||
|
# permission:level pairs; contents:read is the least-privilege default.
|
||||||
|
---
|
||||||
|
token_type: agent
|
||||||
|
installation_id: 0 # PLACEHOLDER - set to the real ghp App installation id
|
||||||
|
scopes:
|
||||||
|
- contents:read
|
||||||
|
session_prefix: vault
|
||||||
|
ttl: 3600 # 1h
|
||||||
|
max_ttl: 86400 # 24h
|
||||||
@@ -90,6 +90,9 @@ inputs = {
|
|||||||
netbox_secret_backend = local.config.netbox_secret_backend
|
netbox_secret_backend = local.config.netbox_secret_backend
|
||||||
netbox_secret_backend_role = local.config.netbox_secret_backend_role
|
netbox_secret_backend_role = local.config.netbox_secret_backend_role
|
||||||
|
|
||||||
|
ghp_secret_backend = local.config.ghp_secret_backend
|
||||||
|
ghp_secret_backend_role = local.config.ghp_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
|
||||||
|
|||||||
@@ -536,6 +536,43 @@ module "netbox_secret_backend_role" {
|
|||||||
depends_on = [module.netbox_secret_backend, module.netbox_user_management]
|
depends_on = [module.netbox_secret_backend, module.netbox_user_management]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module "ghp_secret_backend" {
|
||||||
|
source = "./modules/ghp_secret_backend"
|
||||||
|
|
||||||
|
for_each = var.ghp_secret_backend
|
||||||
|
|
||||||
|
path = each.key
|
||||||
|
plugin = each.value.plugin
|
||||||
|
description = each.value.description
|
||||||
|
base_url = each.value.base_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 "ghp_secret_backend_role" {
|
||||||
|
source = "./modules/ghp_secret_backend_role"
|
||||||
|
|
||||||
|
for_each = var.ghp_secret_backend_role
|
||||||
|
|
||||||
|
backend = each.value.backend
|
||||||
|
name = each.value.name
|
||||||
|
token_type = each.value.token_type
|
||||||
|
installation_id = each.value.installation_id
|
||||||
|
app_record_id = each.value.app_record_id
|
||||||
|
repositories = each.value.repositories
|
||||||
|
scopes = each.value.scopes
|
||||||
|
session_prefix = each.value.session_prefix
|
||||||
|
ttl = each.value.ttl
|
||||||
|
max_ttl = each.value.max_ttl
|
||||||
|
|
||||||
|
depends_on = [module.ghp_secret_backend]
|
||||||
|
}
|
||||||
|
|
||||||
module "vault_policy" {
|
module "vault_policy" {
|
||||||
source = "./modules/vault_policy"
|
source = "./modules/vault_policy"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Mounts the ghp secrets engine and writes its connection config via the
|
||||||
|
# vault-secrets-ghp provider. The plugin is registered ("imported") in the
|
||||||
|
# catalog separately (config/plugins/vault-plugin-secrets-ghp.yaml). The seeded
|
||||||
|
# ghp service token is sensitive and read from KV, not stored in git:
|
||||||
|
# kv/service/vault/<country>/<region>/secret_backend/<path>/config
|
||||||
|
# Expected key: admin_token (a ghpsvc_... service token that ghp accepts via its
|
||||||
|
# GHP_AUTH_SERVICE_TOKENS list). ghp has no rotate endpoint, so this static
|
||||||
|
# shared secret is the single credential the engine authenticates with.
|
||||||
|
data "vault_kv_secret_v2" "config" {
|
||||||
|
mount = "kv"
|
||||||
|
name = "service/vault/${var.country}/${var.region}/secret_backend/${var.path}/config"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "ghp_secret_backend" "this" {
|
||||||
|
path = var.path
|
||||||
|
plugin = var.plugin
|
||||||
|
description = var.description
|
||||||
|
base_url = var.base_url
|
||||||
|
admin_token = data.vault_kv_secret_v2.config.data["admin_token"]
|
||||||
|
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 consumed only when the engine config
|
||||||
|
# is first created. ghp has no rotate endpoint, so re-reading a (possibly
|
||||||
|
# stale) KV value must never re-push it into the live mount. Ignoring the
|
||||||
|
# token makes this module create-only for it (mirrors gitea/netbox config).
|
||||||
|
ignore_changes = [admin_token]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.10"
|
||||||
|
required_providers {
|
||||||
|
vault = {
|
||||||
|
source = "hashicorp/vault"
|
||||||
|
version = "5.6.0"
|
||||||
|
}
|
||||||
|
ghp = {
|
||||||
|
source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/vault-secrets-ghp"
|
||||||
|
version = "0.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
variable "path" {
|
||||||
|
description = "Mount path of the ghp secrets engine (e.g. \"ghp\")"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "plugin" {
|
||||||
|
description = "Registered plugin name to mount (the catalog name = mount type)"
|
||||||
|
type = string
|
||||||
|
default = "vault-plugin-secrets-ghp"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "description" {
|
||||||
|
description = "Human-friendly description of the mount"
|
||||||
|
type = string
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "base_url" {
|
||||||
|
description = "Base URL of the ghp server (e.g. https://ghp.unkin.net)"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "country" {
|
||||||
|
description = "Country segment of the KV path holding the seeded admin token"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "region" {
|
||||||
|
description = "Region segment of the KV path holding the seeded admin token"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ca_cert" {
|
||||||
|
description = "PEM CA certificate that signed the ghp 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 ghp server (not recommended)"
|
||||||
|
type = bool
|
||||||
|
default = false
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "request_timeout_seconds" {
|
||||||
|
description = "HTTP timeout in seconds for calls from the plugin to ghp"
|
||||||
|
type = number
|
||||||
|
default = 30
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# A role that mints short-lived, scoped ghp tokens. Reading ghp/creds/<name>
|
||||||
|
# produces a lease-bound token that is revoked from ghp when the lease is
|
||||||
|
# revoked or reaches max_ttl.
|
||||||
|
resource "ghp_secret_role" "this" {
|
||||||
|
backend = var.backend
|
||||||
|
name = var.name
|
||||||
|
token_type = var.token_type
|
||||||
|
installation_id = var.installation_id
|
||||||
|
app_record_id = var.app_record_id
|
||||||
|
repositories = var.repositories
|
||||||
|
scopes = var.scopes
|
||||||
|
session_prefix = var.session_prefix
|
||||||
|
ttl = var.ttl
|
||||||
|
max_ttl = var.max_ttl
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.10"
|
||||||
|
required_providers {
|
||||||
|
ghp = {
|
||||||
|
source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/vault-secrets-ghp"
|
||||||
|
version = "0.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
variable "backend" {
|
||||||
|
description = "Mount path of the ghp secrets engine this role belongs to"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "name" {
|
||||||
|
description = "Role name (read ghp/creds/<name> to mint a token)"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "token_type" {
|
||||||
|
description = "ghp token type to mint: \"agent\" (default) or \"proxy\""
|
||||||
|
type = string
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "installation_id" {
|
||||||
|
description = "ghp App installation id the minted agent token is bound to (required when token_type is \"agent\")"
|
||||||
|
type = number
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "app_record_id" {
|
||||||
|
description = "Optional ghp App record id (UUID) to pin agent tokens to; empty selects ghp's default app"
|
||||||
|
type = string
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "repositories" {
|
||||||
|
description = "Optional repositories the minted token is restricted to; empty is open-scoped (all repositories)"
|
||||||
|
type = list(string)
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "scopes" {
|
||||||
|
description = "Optional ghp permission:level scopes (e.g. [\"contents:read\"]); empty is open-scoped"
|
||||||
|
type = list(string)
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "session_prefix" {
|
||||||
|
description = "Prefix for the ghp session id of each minted token (default \"vault\")"
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -468,6 +468,36 @@ variable "netbox_backend_aliases" {
|
|||||||
default = {}
|
default = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "ghp_secret_backend" {
|
||||||
|
description = "Map of ghp token secret engines to create (mount + config; seeded service token read from KV)"
|
||||||
|
type = map(object({
|
||||||
|
plugin = optional(string, "vault-plugin-secrets-ghp")
|
||||||
|
description = optional(string)
|
||||||
|
base_url = string
|
||||||
|
ca_cert = optional(string)
|
||||||
|
tls_skip_verify = optional(bool, false)
|
||||||
|
request_timeout_seconds = optional(number, 30)
|
||||||
|
}))
|
||||||
|
default = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ghp_secret_backend_role" {
|
||||||
|
description = "Map of ghp engine roles; reading ghp/creds/<name> mints a short-lived scoped ghp token"
|
||||||
|
type = map(object({
|
||||||
|
name = string
|
||||||
|
backend = string
|
||||||
|
token_type = optional(string)
|
||||||
|
installation_id = optional(number)
|
||||||
|
app_record_id = optional(string)
|
||||||
|
repositories = optional(list(string))
|
||||||
|
scopes = optional(list(string))
|
||||||
|
session_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)))
|
||||||
|
|||||||
Reference in New Issue
Block a user