diff --git a/config/config.hcl b/config/config.hcl index e971fe1..3ad9136 100644 --- a/config/config.hcl +++ b/config/config.hcl @@ -218,5 +218,26 @@ locals { }) if startswith(file_path, "gpg_key/") } + rancher_secret_backend = { + for file_path, content in local.all_configs : + trimsuffix(basename(file_path), ".yaml") => content + if startswith(file_path, "rancher_secret_backend/") + } + rancher_secret_backend_service_account = { + for file_path, content in local.all_configs : + trimsuffix(replace(file_path, "rancher_secret_backend_service_account/", ""), ".yaml") => merge(content, { + name = trimsuffix(basename(file_path), ".yaml") + backend = dirname(replace(file_path, "rancher_secret_backend_service_account/", "")) + }) + if startswith(file_path, "rancher_secret_backend_service_account/") + } + rancher_secret_backend_role = { + for file_path, content in local.all_configs : + trimsuffix(replace(file_path, "rancher_secret_backend_role/", ""), ".yaml") => merge(content, { + name = trimsuffix(basename(file_path), ".yaml") + backend = dirname(replace(file_path, "rancher_secret_backend_role/", "")) + }) + if startswith(file_path, "rancher_secret_backend_role/") + } } } diff --git a/config/rancher_secret_backend/rancher.yaml b/config/rancher_secret_backend/rancher.yaml new file mode 100644 index 0000000..c135a57 --- /dev/null +++ b/config/rancher_secret_backend/rancher.yaml @@ -0,0 +1,8 @@ +# Mounts the rancher token secrets engine at "rancher" and writes its config. +# The plugin is registered in the catalog separately (see +# config/plugins/vault-plugin-secrets-rancher.yaml). Seeded service-account +# tokens live under config/rancher_secret_backend_service_account/rancher/ and +# roles under config/rancher_secret_backend_role/rancher/. +description: "Rancher API token engine (seeded root rotation + dynamic scoped creds)" +rancher_url: "https://rancher.k8s.syd1.au.unkin.net" +request_timeout_seconds: 30 diff --git a/config/rancher_secret_backend_role/rancher/ci.yaml b/config/rancher_secret_backend_role/rancher/ci.yaml new file mode 100644 index 0000000..9585011 --- /dev/null +++ b/config/rancher_secret_backend_role/rancher/ci.yaml @@ -0,0 +1,9 @@ +# A role that mints short-lived Rancher tokens from the "admin" service account. +# Reading rancher/creds/ci returns a lease-bound token deleted from Rancher on +# revoke. Minted tokens inherit the admin service account's RBAC; only cluster +# and TTL are scoped per-token. +--- +service_account: admin +description: "CI/CD ephemeral Rancher token" +ttl: 3600 # seconds (1h) +max_ttl: 28800 # seconds (8h) diff --git a/config/rancher_secret_backend_service_account/rancher/admin.yaml b/config/rancher_secret_backend_service_account/rancher/admin.yaml new file mode 100644 index 0000000..f81284a --- /dev/null +++ b/config/rancher_secret_backend_service_account/rancher/admin.yaml @@ -0,0 +1,8 @@ +# A seeded, auto-rotated Rancher service-account token on the "rancher" engine. +# The seed token itself is sensitive and read from KV (not stored here): +# kv/service/vault/au/syd1/secret_backend/rancher/service_account/admin +# -> keys: token (required), token_name (optional) +# Populate that KV path with a live Rancher admin token BEFORE applying; the +# engine then rotates it (mints a fresh 90d token every 45d) so it never lapses. +token_ttl: 7776000 # 90d in seconds +rotation_period: 3888000 # 45d in seconds diff --git a/environments/au/syd1/terragrunt.hcl b/environments/au/syd1/terragrunt.hcl index dbc5f18..a772255 100644 --- a/environments/au/syd1/terragrunt.hcl +++ b/environments/au/syd1/terragrunt.hcl @@ -74,6 +74,10 @@ inputs = { gpg_secret_backend = local.config.gpg_secret_backend gpg_key = local.config.gpg_key + rancher_secret_backend = local.config.rancher_secret_backend + rancher_secret_backend_service_account = local.config.rancher_secret_backend_service_account + rancher_secret_backend_role = local.config.rancher_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/environments/root.hcl b/environments/root.hcl index 09414e7..a5f146c 100644 --- a/environments/root.hcl +++ b/environments/root.hcl @@ -23,6 +23,12 @@ provider "gpg" { address = local.vault_addr } +# The rancher token secrets engine is managed through its own provider (same +# Vault server; token falls back to VAULT_TOKEN). +provider "rancher" { + address = local.vault_addr +} + terraform { backend "consul" { address = "https://consul.service.consul" @@ -49,6 +55,10 @@ terraform { source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/gpgvaultsecret" version = "0.1.0" } + rancher = { + source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/ranchervaultsecret" + version = "0.1.0" + } } } EOF diff --git a/modules/vault_cluster/main.tf b/modules/vault_cluster/main.tf index d233cd9..0721e81 100644 --- a/modules/vault_cluster/main.tf +++ b/modules/vault_cluster/main.tf @@ -374,6 +374,53 @@ module "gpg_key" { depends_on = [module.gpg_secret_backend] } +module "rancher_secret_backend" { + source = "./modules/rancher_secret_backend" + + for_each = var.rancher_secret_backend + + path = each.key + plugin = each.value.plugin + description = each.value.description + rancher_url = each.value.rancher_url + 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 "rancher_secret_backend_service_account" { + source = "./modules/rancher_secret_backend_service_account" + + for_each = var.rancher_secret_backend_service_account + + backend = each.value.backend + name = each.value.name + country = var.country + region = var.region + token_ttl = each.value.token_ttl + rotation_period = each.value.rotation_period + + depends_on = [module.rancher_secret_backend] +} + +module "rancher_secret_backend_role" { + source = "./modules/rancher_secret_backend_role" + + for_each = var.rancher_secret_backend_role + + backend = each.value.backend + name = each.value.name + service_account = each.value.service_account + cluster_name = each.value.cluster_name + description = each.value.description + ttl = each.value.ttl + max_ttl = each.value.max_ttl + + depends_on = [module.rancher_secret_backend_service_account] +} + module "vault_policy" { source = "./modules/vault_policy" diff --git a/modules/vault_cluster/modules/rancher_secret_backend/main.tf b/modules/vault_cluster/modules/rancher_secret_backend/main.tf new file mode 100644 index 0000000..415508b --- /dev/null +++ b/modules/vault_cluster/modules/rancher_secret_backend/main.tf @@ -0,0 +1,13 @@ +# Mounts the rancher secrets engine and writes its connection config via the +# ranchervaultsecret provider. The plugin is registered ("imported") in the +# catalog separately (config/plugins/vault-plugin-secrets-rancher.yaml). Seeded +# service-account tokens and roles are managed by the sibling modules. +resource "rancher_secret_backend" "this" { + path = var.path + plugin = var.plugin + description = var.description + rancher_url = var.rancher_url + 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/rancher_secret_backend/terraform.tf b/modules/vault_cluster/modules/rancher_secret_backend/terraform.tf new file mode 100644 index 0000000..1672493 --- /dev/null +++ b/modules/vault_cluster/modules/rancher_secret_backend/terraform.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.10" + required_providers { + rancher = { + source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/ranchervaultsecret" + version = "0.1.0" + } + } +} diff --git a/modules/vault_cluster/modules/rancher_secret_backend/variables.tf b/modules/vault_cluster/modules/rancher_secret_backend/variables.tf new file mode 100644 index 0000000..668d06d --- /dev/null +++ b/modules/vault_cluster/modules/rancher_secret_backend/variables.tf @@ -0,0 +1,39 @@ +variable "path" { + description = "Mount path of the rancher secrets engine (e.g. \"rancher\")" + type = string +} + +variable "plugin" { + description = "Registered plugin name to mount (the catalog name = mount type)" + type = string + default = "vault-plugin-secrets-rancher" +} + +variable "description" { + description = "Human-friendly description of the mount" + type = string + default = null +} + +variable "rancher_url" { + description = "Base URL of the Rancher server (e.g. https://rancher.k8s.syd1.au.unkin.net)" + type = string +} + +variable "ca_cert" { + description = "PEM CA certificate that signed the Rancher 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 Rancher server (not recommended)" + type = bool + default = false +} + +variable "request_timeout_seconds" { + description = "HTTP timeout in seconds for calls from the plugin to Rancher" + type = number + default = 30 +} diff --git a/modules/vault_cluster/modules/rancher_secret_backend_role/main.tf b/modules/vault_cluster/modules/rancher_secret_backend_role/main.tf new file mode 100644 index 0000000..a8d4985 --- /dev/null +++ b/modules/vault_cluster/modules/rancher_secret_backend_role/main.tf @@ -0,0 +1,11 @@ +# A role that mints short-lived, optionally cluster-scoped rancher tokens from a +# service account. Reading rancher/creds/ produces a lease-bound token. +resource "rancher_secret_backend_role" "this" { + backend = var.backend + name = var.name + service_account = var.service_account + cluster_name = var.cluster_name + description = var.description + ttl = var.ttl + max_ttl = var.max_ttl +} diff --git a/modules/vault_cluster/modules/rancher_secret_backend_role/terraform.tf b/modules/vault_cluster/modules/rancher_secret_backend_role/terraform.tf new file mode 100644 index 0000000..1672493 --- /dev/null +++ b/modules/vault_cluster/modules/rancher_secret_backend_role/terraform.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.10" + required_providers { + rancher = { + source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/ranchervaultsecret" + version = "0.1.0" + } + } +} diff --git a/modules/vault_cluster/modules/rancher_secret_backend_role/variables.tf b/modules/vault_cluster/modules/rancher_secret_backend_role/variables.tf new file mode 100644 index 0000000..998dd4e --- /dev/null +++ b/modules/vault_cluster/modules/rancher_secret_backend_role/variables.tf @@ -0,0 +1,38 @@ +variable "backend" { + description = "Mount path of the rancher secrets engine this role lives on" + type = string +} + +variable "name" { + description = "Role name" + type = string +} + +variable "service_account" { + description = "Service account (seeded token) used to mint credentials; its user's RBAC is inherited by minted tokens" + type = string +} + +variable "cluster_name" { + description = "Downstream cluster to scope minted tokens to (empty = full Rancher-server scope)" + type = string + default = null +} + +variable "description" { + description = "Description applied to each minted Rancher token" + type = string + default = null +} + +variable "ttl" { + description = "Default lease TTL in seconds for tokens minted from this role" + type = number + default = null +} + +variable "max_ttl" { + description = "Maximum lease TTL in seconds for tokens minted from this role" + type = number + default = null +} diff --git a/modules/vault_cluster/modules/rancher_secret_backend_service_account/main.tf b/modules/vault_cluster/modules/rancher_secret_backend_service_account/main.tf new file mode 100644 index 0000000..6974ae3 --- /dev/null +++ b/modules/vault_cluster/modules/rancher_secret_backend_service_account/main.tf @@ -0,0 +1,18 @@ +# Seeds an auto-rotated rancher service-account token. The seed token is +# sensitive and read from KV, not stored in git: +# kv/service/vault///secret_backend//service_account/ +# Expected keys: token (required), token_name (optional, the ext.cattle.io Token +# metadata.name so the engine can delete the seed after the first rotation). +data "vault_kv_secret_v2" "seed" { + mount = "kv" + name = "service/vault/${var.country}/${var.region}/secret_backend/${var.backend}/service_account/${var.name}" +} + +resource "rancher_secret_backend_service_account" "this" { + backend = var.backend + name = var.name + token = data.vault_kv_secret_v2.seed.data["token"] + token_name = lookup(data.vault_kv_secret_v2.seed.data, "token_name", null) + token_ttl = var.token_ttl + rotation_period = var.rotation_period +} diff --git a/modules/vault_cluster/modules/rancher_secret_backend_service_account/terraform.tf b/modules/vault_cluster/modules/rancher_secret_backend_service_account/terraform.tf new file mode 100644 index 0000000..d04bdf0 --- /dev/null +++ b/modules/vault_cluster/modules/rancher_secret_backend_service_account/terraform.tf @@ -0,0 +1,13 @@ +terraform { + required_version = ">= 1.10" + required_providers { + vault = { + source = "hashicorp/vault" + version = "5.6.0" + } + rancher = { + source = "artifactapi.k8s.syd1.au.unkin.net/terraform-unkin/ranchervaultsecret" + version = "0.1.0" + } + } +} diff --git a/modules/vault_cluster/modules/rancher_secret_backend_service_account/variables.tf b/modules/vault_cluster/modules/rancher_secret_backend_service_account/variables.tf new file mode 100644 index 0000000..d1b6998 --- /dev/null +++ b/modules/vault_cluster/modules/rancher_secret_backend_service_account/variables.tf @@ -0,0 +1,31 @@ +variable "backend" { + description = "Mount path of the rancher secrets engine this service account lives on" + type = string +} + +variable "name" { + description = "Service-account name" + type = string +} + +variable "country" { + description = "Country code, used to locate the seed token in KV" + type = string +} + +variable "region" { + description = "Region code, used to locate the seed token in KV" + type = string +} + +variable "token_ttl" { + description = "Lifetime in seconds requested for each rotated replacement token (default: engine default, 90d)" + type = number + default = null +} + +variable "rotation_period" { + description = "Seconds a token is used before rotation (default: engine default, 45d). Must be < token_ttl" + type = number + default = null +} diff --git a/modules/vault_cluster/variables.tf b/modules/vault_cluster/variables.tf index cbafb03..a013a8a 100644 --- a/modules/vault_cluster/variables.tf +++ b/modules/vault_cluster/variables.tf @@ -350,6 +350,44 @@ variable "gpg_key" { default = {} } +variable "rancher_secret_backend" { + description = "Map of rancher token secret engines to create (mount + config)" + type = map(object({ + plugin = optional(string, "vault-plugin-secrets-rancher") + description = optional(string) + rancher_url = string + ca_cert = optional(string) + tls_skip_verify = optional(bool, false) + request_timeout_seconds = optional(number, 30) + })) + default = {} +} + +variable "rancher_secret_backend_service_account" { + description = "Map of seeded, auto-rotated rancher service-account tokens (seed token read from KV)" + type = map(object({ + name = string + backend = string + token_ttl = optional(number) + rotation_period = optional(number) + })) + default = {} +} + +variable "rancher_secret_backend_role" { + description = "Map of rancher token-minting roles to create" + type = map(object({ + name = string + backend = string + service_account = string + cluster_name = optional(string) + description = 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)))