From bf2c5537168ae51db139b05da4dd11d834a1044e Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Tue, 7 Jul 2026 00:00:43 +1000 Subject: [PATCH] feat: manage litellm secrets engine via terraform-provider-litellmvaultsecret The vault-plugin-secrets-litellm engine mints LiteLLM virtual keys and is now registered in Vault, but nothing declared its mount/config or roles in this repo. Wire in the companion litellm provider so the mount is managed as code alongside the other secret backends. - Add litellm_secret_backend module: mounts the engine and writes its config (base_url, request_timeout_seconds); reads master_key from KV - Add litellm_secret_backend_role module: manages roles (models, max_budget, key_alias_prefix, ttl/max_ttl seconds, metadata) - Register both modules in vault_cluster main.tf and variables.tf - Discover litellm_secret_backend[_role] YAML in config.hcl and pass them through terragrunt inputs - Declare the litellm provider (git.unkin.net/unkin/litellmvaultsecret) and a provider block in the generated root backend.tf - Add example config for the litellm mount and a sample role --- config/config.hcl | 13 ++++++ config/litellm_secret_backend/litellm.yaml | 6 +++ .../litellm/team-a.yaml | 11 +++++ environments/au/syd1/terragrunt.hcl | 2 + environments/root.hcl | 10 +++++ modules/vault_cluster/main.tf | 31 +++++++++++++ .../modules/litellm_secret_backend/main.tf | 14 ++++++ .../litellm_secret_backend/terraform.tf | 13 ++++++ .../litellm_secret_backend/variables.tf | 37 +++++++++++++++ .../litellm_secret_backend_role/main.tf | 10 +++++ .../litellm_secret_backend_role/terraform.tf | 9 ++++ .../litellm_secret_backend_role/variables.tf | 45 +++++++++++++++++++ modules/vault_cluster/variables.tf | 26 +++++++++++ 13 files changed, 227 insertions(+) create mode 100644 config/litellm_secret_backend/litellm.yaml create mode 100644 config/litellm_secret_backend_role/litellm/team-a.yaml create mode 100644 modules/vault_cluster/modules/litellm_secret_backend/main.tf create mode 100644 modules/vault_cluster/modules/litellm_secret_backend/terraform.tf create mode 100644 modules/vault_cluster/modules/litellm_secret_backend/variables.tf create mode 100644 modules/vault_cluster/modules/litellm_secret_backend_role/main.tf create mode 100644 modules/vault_cluster/modules/litellm_secret_backend_role/terraform.tf create mode 100644 modules/vault_cluster/modules/litellm_secret_backend_role/variables.tf diff --git a/config/config.hcl b/config/config.hcl index e165912..03b25d7 100644 --- a/config/config.hcl +++ b/config/config.hcl @@ -185,5 +185,18 @@ locals { trimsuffix(basename(file_path), ".yaml") => content if startswith(file_path, "pki_mount_only/") } + litellm_secret_backend = { + for file_path, content in local.all_configs : + trimsuffix(basename(file_path), ".yaml") => content + if startswith(file_path, "litellm_secret_backend/") + } + litellm_secret_backend_role = { + for file_path, content in local.all_configs : + trimsuffix(replace(file_path, "litellm_secret_backend_role/", ""), ".yaml") => merge(content, { + name = trimsuffix(basename(file_path), ".yaml") + backend = dirname(replace(file_path, "litellm_secret_backend_role/", "")) + }) + if startswith(file_path, "litellm_secret_backend_role/") + } } } diff --git a/config/litellm_secret_backend/litellm.yaml b/config/litellm_secret_backend/litellm.yaml new file mode 100644 index 0000000..5d98662 --- /dev/null +++ b/config/litellm_secret_backend/litellm.yaml @@ -0,0 +1,6 @@ +# Mounts the LiteLLM dynamic secrets engine at "litellm" and writes its config. +# The master key is sensitive and read from KV, not stored here: +# kv/service/vault/au/syd1/secret_backend/litellm -> key "master_key" +description: "LiteLLM dynamic virtual keys" +base_url: "http://litellm.litellm.svc:4000" +request_timeout_seconds: 30 diff --git a/config/litellm_secret_backend_role/litellm/team-a.yaml b/config/litellm_secret_backend_role/litellm/team-a.yaml new file mode 100644 index 0000000..3ddc028 --- /dev/null +++ b/config/litellm_secret_backend_role/litellm/team-a.yaml @@ -0,0 +1,11 @@ +# Role on the "litellm" mount (directory name = backend mount path). +# Constrains the virtual keys generated from creds/team-a. +models: + - gpt-4 + - gpt-3.5-turbo +max_budget: 50 +ttl: 3600 # seconds (1h) +max_ttl: 86400 # seconds (24h) +metadata: + team: a + env: prod diff --git a/environments/au/syd1/terragrunt.hcl b/environments/au/syd1/terragrunt.hcl index 74e7c86..7b59f0e 100644 --- a/environments/au/syd1/terragrunt.hcl +++ b/environments/au/syd1/terragrunt.hcl @@ -68,6 +68,8 @@ inputs = { kubernetes_secret_backend = local.config.kubernetes_secret_backend kubernetes_secret_backend_role = local.config.kubernetes_secret_backend_role pki_mount_only = local.config.pki_mount_only + litellm_secret_backend = local.config.litellm_secret_backend + litellm_secret_backend_role = local.config.litellm_secret_backend_role # Pass policy maps to vault_cluster module policy_auth_map = local.policies.policy_auth_map diff --git a/environments/root.hcl b/environments/root.hcl index 5b995f1..32a12f2 100644 --- a/environments/root.hcl +++ b/environments/root.hcl @@ -11,6 +11,12 @@ provider "vault" { address = local.vault_addr } +# The LiteLLM secrets engine is managed through its own provider, which talks to +# the same Vault server. Token falls back to the VAULT_TOKEN environment variable. +provider "litellm" { + address = local.vault_addr +} + terraform { backend "consul" { address = "https://consul.service.consul" @@ -29,6 +35,10 @@ terraform { source = "hashicorp/consul" version = "2.23.0" } + litellm = { + source = "git.unkin.net/unkin/litellmvaultsecret" + version = "0.1.0" + } } } EOF diff --git a/modules/vault_cluster/main.tf b/modules/vault_cluster/main.tf index 230d977..80c4023 100644 --- a/modules/vault_cluster/main.tf +++ b/modules/vault_cluster/main.tf @@ -303,6 +303,37 @@ module "kubernetes_secret_backend_role" { depends_on = [module.kubernetes_secret_backend] } +module "litellm_secret_backend" { + source = "./modules/litellm_secret_backend" + + for_each = var.litellm_secret_backend + + country = var.country + region = var.region + path = each.key + plugin = each.value.plugin + description = each.value.description + base_url = each.value.base_url + request_timeout_seconds = each.value.request_timeout_seconds +} + +module "litellm_secret_backend_role" { + source = "./modules/litellm_secret_backend_role" + + for_each = var.litellm_secret_backend_role + + name = each.value.name + backend = each.value.backend + models = each.value.models + max_budget = each.value.max_budget + key_alias_prefix = each.value.key_alias_prefix + ttl = each.value.ttl + max_ttl = each.value.max_ttl + metadata = each.value.metadata + + depends_on = [module.litellm_secret_backend] +} + module "vault_policy" { source = "./modules/vault_policy" diff --git a/modules/vault_cluster/modules/litellm_secret_backend/main.tf b/modules/vault_cluster/modules/litellm_secret_backend/main.tf new file mode 100644 index 0000000..2d0acd9 --- /dev/null +++ b/modules/vault_cluster/modules/litellm_secret_backend/main.tf @@ -0,0 +1,14 @@ +# Expected keys in KV secret: master_key +data "vault_kv_secret_v2" "secret_backend_config" { + mount = "kv" + name = "service/vault/${var.country}/${var.region}/secret_backend/${var.path}" +} + +resource "litellm_secret_backend" "this" { + path = var.path + plugin = var.plugin + description = var.description + base_url = var.base_url + master_key = data.vault_kv_secret_v2.secret_backend_config.data["master_key"] + request_timeout_seconds = var.request_timeout_seconds +} diff --git a/modules/vault_cluster/modules/litellm_secret_backend/terraform.tf b/modules/vault_cluster/modules/litellm_secret_backend/terraform.tf new file mode 100644 index 0000000..d5b2279 --- /dev/null +++ b/modules/vault_cluster/modules/litellm_secret_backend/terraform.tf @@ -0,0 +1,13 @@ +terraform { + required_version = ">= 1.10" + required_providers { + vault = { + source = "hashicorp/vault" + version = "5.6.0" + } + litellm = { + source = "git.unkin.net/unkin/litellmvaultsecret" + version = "0.1.0" + } + } +} diff --git a/modules/vault_cluster/modules/litellm_secret_backend/variables.tf b/modules/vault_cluster/modules/litellm_secret_backend/variables.tf new file mode 100644 index 0000000..1bc80ad --- /dev/null +++ b/modules/vault_cluster/modules/litellm_secret_backend/variables.tf @@ -0,0 +1,37 @@ +variable "country" { + description = "Country identifier (used to locate the KV secret holding the master key)" + type = string +} + +variable "region" { + description = "Region identifier (used to locate the KV secret holding the master key)" + type = string +} + +variable "path" { + description = "Mount path of the LiteLLM secrets engine" + type = string +} + +variable "plugin" { + description = "Registered plugin name/type to mount" + type = string + default = "vault-plugin-secrets-litellm" +} + +variable "description" { + description = "Human-friendly description of the mount" + type = string + default = null +} + +variable "base_url" { + description = "Base URL of the LiteLLM proxy (e.g. http://litellm.litellm.svc:4000)" + type = string +} + +variable "request_timeout_seconds" { + description = "HTTP timeout in seconds for calls from the plugin to the LiteLLM proxy" + type = number + default = 30 +} diff --git a/modules/vault_cluster/modules/litellm_secret_backend_role/main.tf b/modules/vault_cluster/modules/litellm_secret_backend_role/main.tf new file mode 100644 index 0000000..a196e1a --- /dev/null +++ b/modules/vault_cluster/modules/litellm_secret_backend_role/main.tf @@ -0,0 +1,10 @@ +resource "litellm_secret_backend_role" "this" { + backend = var.backend + name = var.name + models = var.models + max_budget = var.max_budget + key_alias_prefix = var.key_alias_prefix + ttl = var.ttl + max_ttl = var.max_ttl + metadata = var.metadata +} diff --git a/modules/vault_cluster/modules/litellm_secret_backend_role/terraform.tf b/modules/vault_cluster/modules/litellm_secret_backend_role/terraform.tf new file mode 100644 index 0000000..e3a50f9 --- /dev/null +++ b/modules/vault_cluster/modules/litellm_secret_backend_role/terraform.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.10" + required_providers { + litellm = { + source = "git.unkin.net/unkin/litellmvaultsecret" + version = "0.1.0" + } + } +} diff --git a/modules/vault_cluster/modules/litellm_secret_backend_role/variables.tf b/modules/vault_cluster/modules/litellm_secret_backend_role/variables.tf new file mode 100644 index 0000000..ffba677 --- /dev/null +++ b/modules/vault_cluster/modules/litellm_secret_backend_role/variables.tf @@ -0,0 +1,45 @@ +variable "name" { + description = "Name of the role" + type = string +} + +variable "backend" { + description = "Mount path of the LiteLLM secrets engine this role belongs to" + type = string +} + +variable "models" { + description = "Models a generated key may access. Empty means unrestricted" + type = list(string) + default = null +} + +variable "max_budget" { + description = "Spending limit applied to each generated key. 0 means unlimited" + type = number + default = null +} + +variable "key_alias_prefix" { + description = "Prefix for the auto-generated key alias" + type = string + default = null +} + +variable "ttl" { + description = "Default lease TTL in seconds for keys generated from this role" + type = number + default = null +} + +variable "max_ttl" { + description = "Maximum lease TTL in seconds for keys generated from this role" + type = number + default = null +} + +variable "metadata" { + description = "Metadata attached to each generated key" + type = map(string) + default = null +} diff --git a/modules/vault_cluster/variables.tf b/modules/vault_cluster/variables.tf index 7e06cbe..157bd99 100644 --- a/modules/vault_cluster/variables.tf +++ b/modules/vault_cluster/variables.tf @@ -289,6 +289,32 @@ variable "kubernetes_secret_backend_role" { default = {} } +variable "litellm_secret_backend" { + description = "Map of LiteLLM secret engines to create (mount + config). The master key is read from KV" + type = map(object({ + plugin = optional(string, "vault-plugin-secrets-litellm") + description = optional(string) + base_url = string + request_timeout_seconds = optional(number, 30) + })) + default = {} +} + +variable "litellm_secret_backend_role" { + description = "Map of LiteLLM roles to create" + type = map(object({ + name = string + backend = string + models = optional(list(string)) + max_budget = optional(number) + key_alias_prefix = optional(string) + ttl = optional(number) + max_ttl = optional(number) + metadata = optional(map(string)) + })) + default = {} +} + variable "policy_auth_map" { description = "Map of auth mounts -> auth roles -> policy names" type = map(map(list(string)))