From d94e8a8ea2ea7693aecc1fc22df09026c4f132ee Mon Sep 17 00:00:00 2001 From: Giuseppe D'Anna Date: Thu, 15 Oct 2026 08:35:52 +0200 Subject: [PATCH] feat: HashiCorp Vault settings Render a [vault] section in pgdog.toml from a new `vault` block, and the Vault user settings (server_vault_path, vault_refresh_percent, vault_path) in users.toml. vault.approleSecretIdSecret injects the AppRole secret ID from an existing Secret as VAULT_SECRET_ID. Co-Authored-By: Claude Opus 5.5 --- Chart.yaml | 2 +- README.md | 11 +++++---- templates/config.yaml | 27 +++++++++++++++++++++ templates/deployment.yaml | 11 +++++++++ templates/secrets.yaml | 9 +++++++ test/test.sh | 51 +++++++++++++++++++++++++++++++++++++++ test/values-vault.yaml | 27 +++++++++++++++++++++ values.yaml | 27 +++++++++++++++++++++ 8 files changed, 159 insertions(+), 6 deletions(-) create mode 100644 test/values-vault.yaml diff --git a/Chart.yaml b/Chart.yaml index 9f1b63d..01f14f7 100644 --- a/Chart.yaml +++ b/Chart.yaml @@ -1,4 +1,4 @@ apiVersion: v1 name: pgdog -version: v0.90 +version: v0.91 appVersion: "v0.1.60" diff --git a/README.md b/README.md index 0c1476c..9293fa9 100644 --- a/README.md +++ b/README.md @@ -223,11 +223,12 @@ externalSecrets: The chart can inject values from existing Kubernetes Secrets into the PgDog container's environment: -| Setting | Environment variable | Default Secret key | -| -------------------------- | ------------------------ | ------------------ | -| `control.endpointSecret` | `PGDOG_CONTROL_ENDPOINT` | `endpoint` | -| `control.tokenSecret` | `PGDOG_CONTROL_TOKEN` | `token` | -| `otel.datadogApiKeySecret` | `DD_API_KEY` | `dd-api-key` | +| Setting | Environment variable | Default Secret key | +| ----------------------------- | ------------------------ | ------------------ | +| `control.endpointSecret` | `PGDOG_CONTROL_ENDPOINT` | `endpoint` | +| `control.tokenSecret` | `PGDOG_CONTROL_TOKEN` | `token` | +| `otel.datadogApiKeySecret` | `DD_API_KEY` | `dd-api-key` | +| `vault.approleSecretIdSecret` | `VAULT_SECRET_ID` | `secret-id` | #### Example diff --git a/templates/config.yaml b/templates/config.yaml index 22dd355..1f14af4 100644 --- a/templates/config.yaml +++ b/templates/config.yaml @@ -624,6 +624,33 @@ data: column = {{ .Values.multiTenant.column | quote }} {{- end }} + {{- with .Values.vault }} + [vault] + url = {{ required "vault.url is required" .url | quote }} + auth_method = {{ required "vault.authMethod is required" .authMethod | quote }} + {{- if .authMount }} + auth_mount = {{ .authMount | quote }} + {{- end }} + {{- if .namespace }} + namespace = {{ .namespace | quote }} + {{- end }} + {{- if .kubernetesRole }} + kubernetes_role = {{ .kubernetesRole | quote }} + {{- end }} + {{- if .kubernetesJwtPath }} + kubernetes_jwt_path = {{ .kubernetesJwtPath | quote }} + {{- end }} + {{- if .approleRoleId }} + approle_role_id = {{ .approleRoleId | quote }} + {{- end }} + {{- if .approleSecretIdFile }} + approle_secret_id_file = {{ .approleSecretIdFile | quote }} + {{- end }} + {{- if hasKey . "clientTokenTtl" }} + client_token_ttl = {{ include "pgdog.intval" .clientTokenTtl }} + {{- end }} + {{- end }} + {{- if .Values.otel }} [otel] {{- if hasKey .Values.otel "endpoint" }} diff --git a/templates/deployment.yaml b/templates/deployment.yaml index 1131ae3..e0d83a8 100644 --- a/templates/deployment.yaml +++ b/templates/deployment.yaml @@ -139,6 +139,17 @@ spec: {{- end }} {{- end }} {{- end }} + {{- with .Values.vault }} + {{- with .approleSecretIdSecret }} + {{- if .name }} + - name: VAULT_SECRET_ID + valueFrom: + secretKeyRef: + name: {{ .name }} + key: {{ .key | default "secret-id" }} + {{- end }} + {{- end }} + {{- end }} {{- range .Values.env }} {{- if ne .name "NODE_ID" }} - {{- toYaml . | nindent 14 }} diff --git a/templates/secrets.yaml b/templates/secrets.yaml index 33c3859..6d83788 100644 --- a/templates/secrets.yaml +++ b/templates/secrets.yaml @@ -63,6 +63,15 @@ server_iam_region = {{ .serverIamRegion | quote }} {{- if .serverIamAssumeRole }} server_iam_assume_role = {{ .serverIamAssumeRole | quote }} {{- end }} +{{- if .serverVaultPath }} +server_vault_path = {{ .serverVaultPath | quote }} +{{- end }} +{{- if .vaultRefreshPercent }} +vault_refresh_percent = {{ .vaultRefreshPercent }} +{{- end }} +{{- if .vaultPath }} +vault_path = {{ .vaultPath | quote }} +{{- end }} {{- end }} {{- end -}} diff --git a/test/test.sh b/test/test.sh index 5c9d3a4..20cc06e 100755 --- a/test/test.sh +++ b/test/test.sh @@ -37,6 +37,57 @@ else exit 1 fi +# Validate Vault settings render into pgdog.toml, users.toml and the env +echo "" +echo "==> Validating Vault settings..." +vault_manifests=$(helm template test-release "$CHART_DIR" -f "$TEST_DIR/values-vault.yaml") +vault_config=$(echo "$vault_manifests" \ + | yq -r 'select(.kind == "ConfigMap" and .metadata.name == "test-release-pgdog") | .data["pgdog.toml"]') +vault_users=$(echo "$vault_manifests" \ + | yq -r 'select(.kind == "Secret" and .metadata.name == "test-release-pgdog") | .data["users.toml"]' \ + | base64 -d) +vault_env=$(echo "$vault_manifests" \ + | yq -o=json -I=0 'select(.kind == "Deployment") | .spec.template.spec.containers[0].env[] | select(.name == "VAULT_SECRET_ID")') + +for expected in \ + '[vault]' \ + 'url = "https://vault.internal:8200"' \ + 'auth_method = "approle"' \ + 'auth_mount = "pgdog-approle"' \ + 'namespace = "team-a"' \ + 'approle_role_id = "4f8e2c1a-role-id"' \ + 'client_token_ttl = 120'; do + if ! echo "$vault_config" | grep -qF "$expected"; then + echo " FAIL: pgdog.toml is missing: $expected" + echo " Got: $vault_config" + exit 1 + fi +done +echo " [vault] section rendered correctly" + +for expected in \ + 'server_auth = "vault_dynamic"' \ + 'server_vault_path = "database/creds/pgdog-role"' \ + 'vault_refresh_percent = 75' \ + 'server_auth = "vault_static"' \ + 'server_vault_path = "database/static-creds/pgdog-static-role"' \ + 'vault_path = "database/static-creds/pgdog-static-role"'; do + if ! echo "$vault_users" | grep -qF "$expected"; then + echo " FAIL: users.toml is missing: $expected" + echo " Got: $vault_users" + exit 1 + fi +done +echo " Vault user settings rendered correctly" + +if [ "$vault_env" = '{"name":"VAULT_SECRET_ID","valueFrom":{"secretKeyRef":{"name":"pgdog-vault","key":"secret-id"}}}' ]; then + echo " VAULT_SECRET_ID injected from the Secret" +else + echo " FAIL: VAULT_SECRET_ID not injected correctly" + echo " Got: $vault_env" + exit 1 +fi + # Validate zero values survive on the PodDisruptionBudget echo "" echo "==> Validating PodDisruptionBudget zero values..." diff --git a/test/values-vault.yaml b/test/values-vault.yaml new file mode 100644 index 0000000..64f0ab7 --- /dev/null +++ b/test/values-vault.yaml @@ -0,0 +1,27 @@ +# Test HashiCorp Vault settings +databases: + - name: prod + host: 10.0.0.1 + +vault: + url: https://vault.internal:8200 + authMethod: approle + authMount: pgdog-approle + namespace: team-a + approleRoleId: 4f8e2c1a-role-id + approleSecretIdSecret: + name: pgdog-vault + clientTokenTtl: 120 + +users: + - name: pgdog + database: prod + password: hunter2 + serverAuth: vault_dynamic + serverVaultPath: database/creds/pgdog-role + vaultRefreshPercent: 75 + - name: pgdog_static + database: prod + vaultPath: database/static-creds/pgdog-static-role + serverAuth: vault_static + serverVaultPath: database/static-creds/pgdog-static-role diff --git a/values.yaml b/values.yaml index 3425451..ac8c24f 100644 --- a/values.yaml +++ b/values.yaml @@ -257,6 +257,11 @@ databases: [] # when serverAuth is rds_iam. # Set serverIamAssumeRole to an IAM role ARN to assume for RDS IAM authentication # when serverAuth is rds_iam. +# Set serverVaultPath to the Vault credentials path when serverAuth is +# vault_dynamic or vault_static, and vaultRefreshPercent (1-80) to control +# when dynamic credentials are renewed. Set vaultPath to a static role path +# to authenticate clients against its Vault-managed password. These require +# the vault section below. users: [] # mirrors contains a list of databases to replicate traffic from/to. @@ -542,6 +547,28 @@ configSecret: # name: "" # existing Secret name # key: dd-api-key # key within that Secret holding the API key +# vault configures HashiCorp Vault, used by users with serverAuth set to +# vault_dynamic or vault_static, or with vaultPath set. Left unset by default +# so the [vault] section is omitted from pgdog.toml. +# vault: +# url: https://vault.internal:8200 +# authMethod: kubernetes # kubernetes or approle +# authMount: "" # default: kubernetes or approle +# namespace: "" # Vault Enterprise namespace +# # Kubernetes auth +# kubernetesRole: pgdog +# kubernetesJwtPath: "" # default: the pod's service account token +# # AppRole auth +# approleRoleId: "" +# approleSecretIdFile: "" # file holding the secret ID, e.g. via extraVolumes +# # Alternatively, inject the secret ID from an existing Secret you created +# # as the VAULT_SECRET_ID env var, which pgdog reads when +# # approleSecretIdFile is not set. +# approleSecretIdSecret: +# name: "" # existing Secret name +# key: secret-id # key within that Secret holding the secret ID +# clientTokenTtl: 60 # seconds before token expiry to log in again + # ServiceMonitor for Prometheus metrics serviceMonitor: enabled: false