fix DB password desync + migrate remaining tfvars to Vault

DB desync fix: Stacks with Vault DB engine rotation (24h) now read
the password from vault-database ClusterSecretStore instead of vault-kv.
9 stacks updated with db ExternalSecrets reading from static-creds/*.

Stacks fixed: speedtest, hackmd, health, trading-bot, claude-memory,
woodpecker, linkwarden, nextcloud, url.

terraform.tfvars migration:
- plotting-book: google_client_id/secret → Vault KV + secret_key_ref
- tandoor: email_password var removed (was default="", now optional ESO)
- infra: ssh_private_key, vm_wizard_password, dockerhub_registry_password
  → Vault KV at secret/infra + data source
This commit is contained in:
Viktor Barzin 2026-03-15 21:39:45 +00:00
parent 06a0d0599a
commit 745e43c983
12 changed files with 385 additions and 83 deletions

View file

@ -49,6 +49,42 @@ resource "kubernetes_manifest" "external_secret" {
depends_on = [kubernetes_namespace.claude-memory]
}
# DB credentials from Vault database engine (rotated every 24h)
resource "kubernetes_manifest" "db_external_secret" {
manifest = {
apiVersion = "external-secrets.io/v1beta1"
kind = "ExternalSecret"
metadata = {
name = "claude-memory-db-creds"
namespace = "claude-memory"
}
spec = {
refreshInterval = "15m"
secretStoreRef = {
name = "vault-database"
kind = "ClusterSecretStore"
}
target = {
name = "claude-memory-db-creds"
template = {
data = {
DATABASE_URL = "postgresql://claude_memory:{{ .password }}@${var.postgresql_host}:5432/claude_memory"
DB_PASSWORD = "{{ .password }}"
}
}
}
data = [{
secretKey = "password"
remoteRef = {
key = "static-creds/pg-claude-memory"
property = "password"
}
}]
}
}
depends_on = [kubernetes_namespace.claude-memory]
}
module "tls_secret" {
source = "../../modules/kubernetes/setup_tls_secret"
namespace = kubernetes_namespace.claude-memory.metadata[0].name
@ -143,8 +179,13 @@ resource "kubernetes_deployment" "claude-memory" {
}
env {
name = "DATABASE_URL"
value = "postgresql://claude_memory:${data.vault_kv_secret_v2.secrets.data["db_password"]}@${var.postgresql_host}:5432/claude_memory"
name = "DATABASE_URL"
value_from {
secret_key_ref {
name = "claude-memory-db-creds"
key = "DATABASE_URL"
}
}
}
env {
name = "API_KEY"

View file

@ -193,7 +193,7 @@ resource "kubernetes_manifest" "external_secret" {
spec = {
refreshInterval = "15m"
secretStoreRef = {
name = "vault-kv"
name = "vault-database"
kind = "ClusterSecretStore"
}
target = {
@ -206,7 +206,10 @@ resource "kubernetes_manifest" "external_secret" {
}
data = [{
secretKey = "db_password"
remoteRef = { key = "hackmd", property = "db_password" }
remoteRef = {
key = "static-creds/mysql-codimd"
property = "password"
}
}]
}
}

View file

@ -69,7 +69,7 @@ resource "kubernetes_deployment" "health" {
name = "DATABASE_URL"
value_from {
secret_key_ref {
name = "health-secrets"
name = "health-db-secrets"
key = "DATABASE_URL"
}
}
@ -78,7 +78,7 @@ resource "kubernetes_deployment" "health" {
name = "SECRET_KEY"
value_from {
secret_key_ref {
name = "health-secrets"
name = "health-kv-secrets"
key = "secret_key"
}
}
@ -163,12 +163,46 @@ module "ingress" {
}
}
resource "kubernetes_manifest" "external_secret" {
resource "kubernetes_manifest" "external_secret_db" {
manifest = {
apiVersion = "external-secrets.io/v1beta1"
kind = "ExternalSecret"
metadata = {
name = "health-secrets"
name = "health-db-secrets"
namespace = "health"
}
spec = {
refreshInterval = "15m"
secretStoreRef = {
name = "vault-database"
kind = "ClusterSecretStore"
}
target = {
name = "health-db-secrets"
template = {
data = {
DATABASE_URL = "postgresql+asyncpg://health:{{ .db_password }}@postgresql.dbaas.svc.cluster.local:5432/health"
}
}
}
data = [{
secretKey = "db_password"
remoteRef = {
key = "static-creds/postgresql-health"
property = "password"
}
}]
}
}
depends_on = [kubernetes_namespace.health]
}
resource "kubernetes_manifest" "external_secret_kv" {
manifest = {
apiVersion = "external-secrets.io/v1beta1"
kind = "ExternalSecret"
metadata = {
name = "health-kv-secrets"
namespace = "health"
}
spec = {
@ -178,24 +212,15 @@ resource "kubernetes_manifest" "external_secret" {
kind = "ClusterSecretStore"
}
target = {
name = "health-secrets"
template = {
data = {
DATABASE_URL = "postgresql+asyncpg://health:{{ .db_password }}@postgresql.dbaas.svc.cluster.local:5432/health"
secret_key = "{{ .secret_key }}"
}
}
name = "health-kv-secrets"
}
data = [
{
secretKey = "db_password"
remoteRef = { key = "health", property = "db_password" }
},
{
secretKey = "secret_key"
remoteRef = { key = "health", property = "secret_key" }
data = [{
secretKey = "secret_key"
remoteRef = {
key = "health"
property = "secret_key"
}
]
}]
}
}
depends_on = [kubernetes_namespace.health]

View file

@ -9,25 +9,17 @@
variable "proxmox_host" { type = string }
variable "ssh_private_key" {
type = string
default = ""
sensitive = true
}
variable "ssh_public_key" {
type = string
default = ""
}
variable "vm_wizard_password" {
type = string
sensitive = true
}
variable "k8s_join_command" { type = string }
variable "dockerhub_registry_password" {}
data "vault_kv_secret_v2" "secrets" {
mount = "secret"
name = "infra"
}
# ---------------------------------------------------------------------------
# Locals
@ -54,14 +46,14 @@ module "k8s-node-template" {
proxmox_host = var.proxmox_host
proxmox_user = "root" # SSH user on Proxmox host
ssh_private_key = var.ssh_private_key
ssh_private_key = data.vault_kv_secret_v2.secrets.data["ssh_private_key"]
ssh_public_key = var.ssh_public_key
cloud_image_url = local.cloud_init_image_url
image_path = local.k8s_cloud_init_image_path
template_id = 2000
template_name = local.k8s_vm_template
user_passwd = var.vm_wizard_password
user_passwd = data.vault_kv_secret_v2.secrets.data["vm_wizard_password"]
is_k8s_template = true # provision cloud init file with k8s deps
snippet_name = local.k8s_cloud_init_snippet_name
@ -146,14 +138,14 @@ module "non-k8s-node-template" {
proxmox_host = var.proxmox_host
proxmox_user = "root" # SSH user on Proxmox host
ssh_private_key = var.ssh_private_key
ssh_private_key = data.vault_kv_secret_v2.secrets.data["ssh_private_key"]
ssh_public_key = var.ssh_public_key
cloud_image_url = local.cloud_init_image_url
image_path = local.non_k8s_cloud_init_image_path
template_id = 1000
template_name = local.non_k8s_vm_template
user_passwd = var.vm_wizard_password
user_passwd = data.vault_kv_secret_v2.secrets.data["vm_wizard_password"]
is_k8s_template = false # provision cloud init file without k8s deps
snippet_name = local.non_k8s_cloud_init_snippet_name
@ -169,7 +161,7 @@ module "docker-registry-template" {
proxmox_host = var.proxmox_host
proxmox_user = "root" # SSH user on Proxmox host
ssh_private_key = var.ssh_private_key
ssh_private_key = data.vault_kv_secret_v2.secrets.data["ssh_private_key"]
ssh_public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDHLhYDfyx237eJgOGVoJRECpUS95+7rEBS9vacsIxtx devvm"
cloud_image_url = local.cloud_init_image_url
@ -177,7 +169,7 @@ module "docker-registry-template" {
template_id = 1001
template_name = "docker-registry-template"
user_passwd = var.vm_wizard_password
user_passwd = data.vault_kv_secret_v2.secrets.data["vm_wizard_password"]
is_k8s_template = false # provision cloud init file without k8s deps
snippet_name = "docker-registry.yaml"
@ -212,7 +204,7 @@ module "docker-registry-template" {
format("echo %s | base64 -d > /opt/registry/config-dockerhub.yml",
base64encode(
templatefile("../../modules/docker-registry/config.yaml", {
password = var.dockerhub_registry_password
password = data.vault_kv_secret_v2.secrets.data["dockerhub_registry_password"]
})
)
),

View file

@ -50,6 +50,42 @@ resource "kubernetes_manifest" "external_secret" {
depends_on = [kubernetes_namespace.linkwarden]
}
# DB credentials from Vault database engine (rotated every 24h)
resource "kubernetes_manifest" "db_external_secret" {
manifest = {
apiVersion = "external-secrets.io/v1beta1"
kind = "ExternalSecret"
metadata = {
name = "linkwarden-db-creds"
namespace = "linkwarden"
}
spec = {
refreshInterval = "15m"
secretStoreRef = {
name = "vault-database"
kind = "ClusterSecretStore"
}
target = {
name = "linkwarden-db-creds"
template = {
data = {
DATABASE_URL = "postgresql://linkwarden:{{ .password }}@${var.postgresql_host}:5432/linkwarden"
DB_PASSWORD = "{{ .password }}"
}
}
}
data = [{
secretKey = "password"
remoteRef = {
key = "static-creds/pg-linkwarden"
property = "password"
}
}]
}
}
depends_on = [kubernetes_namespace.linkwarden]
}
module "tls_secret" {
source = "../../modules/kubernetes/setup_tls_secret"
namespace = kubernetes_namespace.linkwarden.metadata[0].name
@ -87,9 +123,9 @@ resource "kubernetes_deployment" "linkwarden" {
app = "linkwarden"
}
annotations = {
"diun.enable" = "false"
"diun.include_tags" = "latest"
"dependency.kyverno.io/wait-for" = "postgresql.dbaas:5432"
"diun.enable" = "false"
"diun.include_tags" = "latest"
"dependency.kyverno.io/wait-for" = "postgresql.dbaas:5432"
}
}
spec {
@ -101,8 +137,13 @@ resource "kubernetes_deployment" "linkwarden" {
container_port = 3000
}
env {
name = "DATABASE_URL"
value = "postgresql://linkwarden:${data.vault_kv_secret_v2.secrets.data["db_password"]}@${var.postgresql_host}:5432/linkwarden"
name = "DATABASE_URL"
value_from {
secret_key_ref {
name = "linkwarden-db-creds"
key = "DATABASE_URL"
}
}
}
env {
name = "NEXT_PUBLIC_AUTHENTIK_ENABLED"

View file

@ -61,6 +61,45 @@ resource "kubernetes_manifest" "external_secret" {
depends_on = [kubernetes_namespace.nextcloud]
}
# DB credentials from Vault database engine (rotated every 24h)
# NOTE: Nextcloud Helm values use plan-time db_password from KV the Helm
# release will use the KV snapshot until the next terragrunt apply. This
# ExternalSecret provides runtime-refreshed credentials for any future
# migration to envFrom-based secret injection.
resource "kubernetes_manifest" "db_external_secret" {
manifest = {
apiVersion = "external-secrets.io/v1beta1"
kind = "ExternalSecret"
metadata = {
name = "nextcloud-db-creds"
namespace = "nextcloud"
}
spec = {
refreshInterval = "15m"
secretStoreRef = {
name = "vault-database"
kind = "ClusterSecretStore"
}
target = {
name = "nextcloud-db-creds"
template = {
data = {
DB_PASSWORD = "{{ .password }}"
}
}
}
data = [{
secretKey = "password"
remoteRef = {
key = "static-creds/mysql-nextcloud"
property = "password"
}
}]
}
}
depends_on = [kubernetes_namespace.nextcloud]
}
resource "kubernetes_resource_quota" "nextcloud" {
metadata {
name = "nextcloud-quota"

View file

@ -2,15 +2,6 @@ variable "tls_secret_name" {
type = string
sensitive = true
}
variable "plotting_book_google_client_id" {
type = string
sensitive = true
}
variable "plotting_book_google_client_secret" {
type = string
sensitive = true
}
resource "kubernetes_namespace" "plotting-book" {
metadata {
name = "plotting-book"
@ -125,12 +116,22 @@ resource "kubernetes_deployment" "plotting-book" {
}
}
env {
name = "GOOGLE_CLIENT_ID"
value = var.plotting_book_google_client_id
name = "GOOGLE_CLIENT_ID"
value_from {
secret_key_ref {
name = "plotting-book-secrets"
key = "google_client_id"
}
}
}
env {
name = "GOOGLE_CLIENT_SECRET"
value = var.plotting_book_google_client_secret
name = "GOOGLE_CLIENT_SECRET"
value_from {
secret_key_ref {
name = "plotting-book-secrets"
key = "google_client_secret"
}
}
}
env {
name = "GOOGLE_CALLBACK_URL"

View file

@ -25,15 +25,17 @@ resource "kubernetes_manifest" "external_secret" {
spec = {
refreshInterval = "15m"
secretStoreRef = {
name = "vault-kv"
name = "vault-database"
kind = "ClusterSecretStore"
}
target = {
name = "speedtest-secrets"
}
dataFrom = [{
extract = {
key = "speedtest"
data = [{
secretKey = "db_password"
remoteRef = {
key = "static-creds/mysql-speedtest"
property = "password"
}
}]
}

View file

@ -2,11 +2,6 @@ variable "tls_secret_name" {
type = string
sensitive = true
}
variable "tandoor_email_password" {
type = string
default = ""
sensitive = true
}
variable "nfs_server" { type = string }
variable "postgresql_host" { type = string }
variable "mail_host" { type = string }
@ -158,8 +153,14 @@ resource "kubernetes_deployment" "tandoor" {
value = "info@viktorbarzin.me"
}
env {
name = "EMAIL_HOST_PASSWORD"
value = var.tandoor_email_password
name = "EMAIL_HOST_PASSWORD"
value_from {
secret_key_ref {
name = "tandoor-secrets"
key = "email_password"
optional = true
}
}
}
env {
name = "EMAIL_USE_TLS"

View file

@ -61,21 +61,18 @@ resource "kubernetes_manifest" "external_secret" {
name = "trading-bot-secrets"
template = {
data = {
TRADING_DATABASE_URL = "postgresql+asyncpg://trading:{{ .db_password }}@${var.postgresql_host}:5432/trading"
TRADING_ALPACA_API_KEY = "{{ .alpaca_api_key }}"
TRADING_ALPACA_SECRET_KEY = "{{ .alpaca_secret_key }}"
TRADING_JWT_SECRET_KEY = "{{ .jwt_secret }}"
TRADING_REDDIT_CLIENT_ID = "{{ .reddit_client_id }}"
TRADING_REDDIT_CLIENT_SECRET = "{{ .reddit_client_secret }}"
TRADING_ALPACA_API_KEY = "{{ .alpaca_api_key }}"
TRADING_ALPACA_SECRET_KEY = "{{ .alpaca_secret_key }}"
TRADING_JWT_SECRET_KEY = "{{ .jwt_secret }}"
TRADING_REDDIT_CLIENT_ID = "{{ .reddit_client_id }}"
TRADING_REDDIT_CLIENT_SECRET = "{{ .reddit_client_secret }}"
TRADING_ALPHA_VANTAGE_API_KEY = "{{ .alpha_vantage_api_key }}"
TRADING_FMP_API_KEY = "{{ .fmp_api_key }}"
DBAAS_ROOT_PASSWORD = "{{ .dbaas_root_password }}"
DB_PASSWORD = "{{ .db_password }}"
TRADING_FMP_API_KEY = "{{ .fmp_api_key }}"
DBAAS_ROOT_PASSWORD = "{{ .dbaas_root_password }}"
}
}
}
data = [
{ secretKey = "db_password", remoteRef = { key = "trading-bot", property = "db_password" } },
{ secretKey = "alpaca_api_key", remoteRef = { key = "trading-bot", property = "alpaca_api_key" } },
{ secretKey = "alpaca_secret_key", remoteRef = { key = "trading-bot", property = "alpaca_secret_key" } },
{ secretKey = "jwt_secret", remoteRef = { key = "trading-bot", property = "jwt_secret" } },
@ -90,6 +87,42 @@ resource "kubernetes_manifest" "external_secret" {
depends_on = [kubernetes_namespace.trading-bot]
}
# DB credentials from Vault database engine (rotated every 24h)
resource "kubernetes_manifest" "db_external_secret" {
manifest = {
apiVersion = "external-secrets.io/v1beta1"
kind = "ExternalSecret"
metadata = {
name = "trading-bot-db-creds"
namespace = "trading-bot"
}
spec = {
refreshInterval = "15m"
secretStoreRef = {
name = "vault-database"
kind = "ClusterSecretStore"
}
target = {
name = "trading-bot-db-creds"
template = {
data = {
TRADING_DATABASE_URL = "postgresql+asyncpg://trading:{{ .password }}@${var.postgresql_host}:5432/trading"
DB_PASSWORD = "{{ .password }}"
}
}
}
data = [{
secretKey = "password"
remoteRef = {
key = "static-creds/pg-trading"
property = "password"
}
}]
}
}
depends_on = [kubernetes_namespace.trading-bot]
}
# Database init job - creates the trading database and user in PostgreSQL
resource "kubernetes_job" "db_init" {
metadata {
@ -125,6 +158,11 @@ resource "kubernetes_job" "db_init" {
name = "trading-bot-secrets"
}
}
env_from {
secret_ref {
name = "trading-bot-db-creds"
}
}
}
restart_policy = "Never"
}
@ -161,6 +199,11 @@ resource "kubernetes_job" "migrations" {
name = "trading-bot-secrets"
}
}
env_from {
secret_ref {
name = "trading-bot-db-creds"
}
}
}
restart_policy = "Never"
}
@ -251,6 +294,11 @@ resource "kubernetes_deployment" "trading-bot-frontend" {
name = "trading-bot-secrets"
}
}
env_from {
secret_ref {
name = "trading-bot-db-creds"
}
}
resources {
requests = {
cpu = "50m"
@ -328,6 +376,11 @@ resource "kubernetes_deployment" "trading-bot-workers" {
name = "trading-bot-secrets"
}
}
env_from {
secret_ref {
name = "trading-bot-db-creds"
}
}
resources {
requests = {
cpu = "10m"
@ -359,6 +412,11 @@ resource "kubernetes_deployment" "trading-bot-workers" {
name = "trading-bot-secrets"
}
}
env_from {
secret_ref {
name = "trading-bot-db-creds"
}
}
resources {
requests = {
cpu = "100m"
@ -390,6 +448,11 @@ resource "kubernetes_deployment" "trading-bot-workers" {
name = "trading-bot-secrets"
}
}
env_from {
secret_ref {
name = "trading-bot-db-creds"
}
}
resources {
requests = {
cpu = "10m"
@ -421,6 +484,11 @@ resource "kubernetes_deployment" "trading-bot-workers" {
name = "trading-bot-secrets"
}
}
env_from {
secret_ref {
name = "trading-bot-db-creds"
}
}
resources {
requests = {
cpu = "10m"
@ -452,6 +520,11 @@ resource "kubernetes_deployment" "trading-bot-workers" {
name = "trading-bot-secrets"
}
}
env_from {
secret_ref {
name = "trading-bot-db-creds"
}
}
resources {
requests = {
cpu = "10m"
@ -483,6 +556,11 @@ resource "kubernetes_deployment" "trading-bot-workers" {
name = "trading-bot-secrets"
}
}
env_from {
secret_ref {
name = "trading-bot-db-creds"
}
}
resources {
requests = {
cpu = "10m"

View file

@ -56,6 +56,46 @@ resource "kubernetes_manifest" "external_secret" {
depends_on = [kubernetes_namespace.shlink]
}
# DB credentials from Vault database engine (rotated every 24h)
# NOTE: The kubernetes_secret "mysql_config" still uses plan-time db_password
# from KV. This ExternalSecret provides runtime-refreshed credentials. Once
# the deployment is migrated to use env_from with this secret, the plan-time
# kubernetes_secret can be removed.
resource "kubernetes_manifest" "db_external_secret" {
manifest = {
apiVersion = "external-secrets.io/v1beta1"
kind = "ExternalSecret"
metadata = {
name = "url-db-creds"
namespace = "url"
}
spec = {
refreshInterval = "15m"
secretStoreRef = {
name = "vault-database"
kind = "ClusterSecretStore"
}
target = {
name = "url-db-creds"
template = {
data = {
DB_USER = "shlink"
DB_PASSWORD = "{{ .password }}"
}
}
}
data = [{
secretKey = "password"
remoteRef = {
key = "static-creds/mysql-shlink"
property = "password"
}
}]
}
}
depends_on = [kubernetes_namespace.shlink]
}
module "tls_secret" {
source = "../../modules/kubernetes/setup_tls_secret"
namespace = kubernetes_namespace.shlink.metadata[0].name
@ -167,7 +207,7 @@ resource "kubernetes_deployment" "shlink" {
# }
env_from {
secret_ref {
name = "mysql-config"
name = "url-db-creds"
}
}
# env {

View file

@ -70,6 +70,45 @@ resource "kubernetes_manifest" "external_secret" {
depends_on = [kubernetes_namespace.woodpecker]
}
# DB credentials from Vault database engine (rotated every 24h)
# NOTE: Woodpecker Helm values use plan-time db_password from KV the Helm
# release will use the KV snapshot until the next terragrunt apply. This
# ExternalSecret provides runtime-refreshed credentials for any future
# migration to envFrom-based secret injection.
resource "kubernetes_manifest" "db_external_secret" {
manifest = {
apiVersion = "external-secrets.io/v1beta1"
kind = "ExternalSecret"
metadata = {
name = "woodpecker-db-creds"
namespace = "woodpecker"
}
spec = {
refreshInterval = "15m"
secretStoreRef = {
name = "vault-database"
kind = "ClusterSecretStore"
}
target = {
name = "woodpecker-db-creds"
template = {
data = {
DB_PASSWORD = "{{ .password }}"
}
}
}
data = [{
secretKey = "password"
remoteRef = {
key = "static-creds/pg-woodpecker"
property = "password"
}
}]
}
}
depends_on = [kubernetes_namespace.woodpecker]
}
resource "kubernetes_config_map" "git_crypt_key" {
metadata {
name = "git-crypt-key"