diff --git a/stacks/claude-memory/main.tf b/stacks/claude-memory/main.tf index 2825c049..cacddc51 100644 --- a/stacks/claude-memory/main.tf +++ b/stacks/claude-memory/main.tf @@ -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" diff --git a/stacks/hackmd/main.tf b/stacks/hackmd/main.tf index 281b29c6..26ca2d17 100644 --- a/stacks/hackmd/main.tf +++ b/stacks/hackmd/main.tf @@ -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" + } }] } } diff --git a/stacks/health/main.tf b/stacks/health/main.tf index 24ce403a..f4b4f4ba 100644 --- a/stacks/health/main.tf +++ b/stacks/health/main.tf @@ -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] diff --git a/stacks/infra/main.tf b/stacks/infra/main.tf index 5dd2a48f..8fd1d899 100644 --- a/stacks/infra/main.tf +++ b/stacks/infra/main.tf @@ -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"] }) ) ), diff --git a/stacks/linkwarden/main.tf b/stacks/linkwarden/main.tf index 6a406412..a35e79e7 100644 --- a/stacks/linkwarden/main.tf +++ b/stacks/linkwarden/main.tf @@ -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" diff --git a/stacks/nextcloud/main.tf b/stacks/nextcloud/main.tf index 5c958272..b2855039 100644 --- a/stacks/nextcloud/main.tf +++ b/stacks/nextcloud/main.tf @@ -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" diff --git a/stacks/plotting-book/main.tf b/stacks/plotting-book/main.tf index e9417b8e..5ee47bc0 100644 --- a/stacks/plotting-book/main.tf +++ b/stacks/plotting-book/main.tf @@ -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" diff --git a/stacks/speedtest/main.tf b/stacks/speedtest/main.tf index a45853b6..205f6cc1 100644 --- a/stacks/speedtest/main.tf +++ b/stacks/speedtest/main.tf @@ -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" } }] } diff --git a/stacks/tandoor/main.tf b/stacks/tandoor/main.tf index a5c1b5f3..308cb111 100644 --- a/stacks/tandoor/main.tf +++ b/stacks/tandoor/main.tf @@ -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" diff --git a/stacks/trading-bot/main.tf b/stacks/trading-bot/main.tf index 8559d0f8..da09c2c3 100644 --- a/stacks/trading-bot/main.tf +++ b/stacks/trading-bot/main.tf @@ -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" diff --git a/stacks/url/main.tf b/stacks/url/main.tf index ae1ac0e4..d39d07a8 100644 --- a/stacks/url/main.tf +++ b/stacks/url/main.tf @@ -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 { diff --git a/stacks/woodpecker/main.tf b/stacks/woodpecker/main.tf index 9011da1d..f94a1e1d 100644 --- a/stacks/woodpecker/main.tf +++ b/stacks/woodpecker/main.tf @@ -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"