feat(chart): pg password via ExternalSecret/OpenBao (A-Chunk 1)

Per-instance Postgres password sourced from OpenBao via External
Secrets Operator. Dual-mode for the migration window:

  - postgres.passwordVaultPath unset → legacy postgres-secret.yaml
    renders with .Values.postgres.password (helm lookup + random
    fallback, bit-exact existing behaviour for live instances).
  - postgres.passwordVaultPath set → postgres-password-externalsecret.yaml
    renders an ExternalSecret that produces the same <release>-pg
    Secret (POSTGRES_USER/PASSWORD/DB) from OpenBao path
    `tenants/<tenant.id>/instances/<instance.code>/pg`.

Exactly one of the two templates ships per instance (mutually
exclusive `if`s on .Values.postgres.passwordVaultPath). The Postgres
StatefulSet envFroms <release>-pg unchanged.

OpenBao policy already grants the per-cluster ESO read on
`v3/data/tenants/<tenantID>/*` (buildEsoPolicy in tower's
openbao_auth_setup.go) — the new instances/<code>/pg subpath is
covered. No policy change required.

A `required` directive on the ExternalSecret asserts tenant.id is
present when passwordVaultPath is set — fails loud at helm template
time if Tower forgot to populate it.

deletionPolicy: Retain on the ExternalSecret. Postgres PGDATA on
disk hashes to the password in the Secret; an accidental ESO
removal must not cascade into the Secret disappearing.

Chart 0.1.6 → 0.1.7. Verified locally: helm template both modes,
helm lint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
OdooSky v3
2026-05-12 12:56:54 +03:00
parent d7c1b2f895
commit 96071aec8e
4 changed files with 95 additions and 1 deletions

View File

@@ -5,7 +5,7 @@ description: |
Variation between instances is expressed via values.yaml only. Variation between instances is expressed via values.yaml only.
No chart variants. No string-templating in Tower. No chart variants. No string-templating in Tower.
type: application type: application
version: 0.1.6 version: 0.1.7
appVersion: "1.0" appVersion: "1.0"
keywords: keywords:
- odoo - odoo

View File

@@ -0,0 +1,51 @@
{{- if .Values.postgres.passwordVaultPath }}
# postgres-password-externalsecret.yaml — per-instance Postgres password
# sourced from OpenBao via ESO. Produces the same `<release>-pg` Secret
# shape (POSTGRES_USER + POSTGRES_PASSWORD + POSTGRES_DB) that the legacy
# postgres-secret.yaml produces, so postgres-statefulset.yaml is unchanged.
#
# Rendered only when `.Values.postgres.passwordVaultPath` is set. The
# legacy postgres-secret.yaml renders when that field is empty —
# exactly one of the two ships per instance. Tower-managed migration
# in Chunk 3 flips overlays from the legacy path to this one.
#
# OpenBao path convention: `tenants/<tenantID>/instances/<code>/pg`
# with a `password` field. Covered by the per-cluster ESO policy
# `eso-tenant-<cluster>` (buildEsoPolicy in Go) which already grants
# read on `v3/data/tenants/<tenantID>/*`. No policy change required.
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: {{ include "instance.fullname" . }}-pg
labels:
{{- include "instance.labels" . | nindent 4 }}
spec:
refreshInterval: "1h"
secretStoreRef:
name: {{ .Values.postgres.externalSecretsStoreRef | default "openbao-platform" }}
kind: ClusterSecretStore
target:
name: {{ include "instance.fullname" . }}-pg
creationPolicy: Owner
# Retain — never delete the Secret on ExternalSecret deletion. The
# postgres pod's PGDATA on disk is locked to the password hash in
# this Secret; an accidental ESO removal must not cascade into the
# Secret disappearing and forcing a password rotation (which would
# then drift from pg_authid).
deletionPolicy: Retain
template:
type: Opaque
engineVersion: v2
data:
POSTGRES_USER: {{ .Values.postgres.user | quote }}
POSTGRES_PASSWORD: "{{ `{{ .password }}` }}"
POSTGRES_DB: {{ .Values.postgres.database | quote }}
data:
- secretKey: password
remoteRef:
key: tenants/{{ required "postgres.passwordVaultPath requires .Values.tenant.id (set by Tower at create time)" .Values.tenant.id }}/instances/{{ .Values.instance.code }}/pg
property: password
conversionStrategy: Default
decodingStrategy: None
metadataPolicy: None
{{- end }}

View File

@@ -1,3 +1,11 @@
{{- if not .Values.postgres.passwordVaultPath }}
# Legacy postgres-secret.yaml — chart-rendered Secret carrying
# POSTGRES_USER/PASSWORD/DB for the postgres StatefulSet. Used when
# `.Values.postgres.passwordVaultPath` is empty (the pre-ESO path).
# When that field is set, postgres-password-externalsecret.yaml
# renders an ExternalSecret producing the same Secret name + shape
# from OpenBao instead, and this template skips. Exactly one of the
# two ships per instance.
apiVersion: v1 apiVersion: v1
kind: Secret kind: Secret
metadata: metadata:
@@ -9,3 +17,4 @@ stringData:
POSTGRES_USER: {{ .Values.postgres.user | quote }} POSTGRES_USER: {{ .Values.postgres.user | quote }}
POSTGRES_PASSWORD: {{ include "instance.pgPassword" . | quote }} POSTGRES_PASSWORD: {{ include "instance.pgPassword" . | quote }}
POSTGRES_DB: {{ .Values.postgres.database | quote }} POSTGRES_DB: {{ .Values.postgres.database | quote }}
{{- end }}

View File

@@ -17,6 +17,13 @@ instance:
# `instance.size: medium` (etc); they don't have to know the numbers. # `instance.size: medium` (etc); they don't have to know the numbers.
size: small size: small
# tenant — owning tenant identity. Currently only required when
# postgres.passwordVaultPath is set (the ESO path needs to know which
# tenant subtree to read from OpenBao). Tower writes `tenant.id` into
# every new overlay; legacy overlays without ESO leave this empty.
tenant:
id: ""
# The named-size table. Single source of truth for what each instance # The named-size table. Single source of truth for what each instance
# tier actually gets. Adjust here, ALL future instances pick up the # tier actually gets. Adjust here, ALL future instances pick up the
# new defaults on next reconcile. Existing instances keep their # new defaults on next reconcile. Existing instances keep their
@@ -159,7 +166,34 @@ postgres:
database: postgres database: postgres
# If empty, the chart auto-generates on first install and re-reads # If empty, the chart auto-generates on first install and re-reads
# the existing Secret on subsequent upgrades (lookup pattern). # the existing Secret on subsequent upgrades (lookup pattern).
# Ignored when `passwordVaultPath` is set — ESO sources the password
# from OpenBao instead.
password: "" password: ""
# passwordVaultPath — when set, the chart renders an ExternalSecret
# pulling the password from OpenBao at
# `tenants/<tenant.id>/instances/<instance.code>/pg` (field
# `password`). The legacy postgres-secret.yaml template is gated
# off; the ExternalSecret produces the same `<release>-pg` Secret
# shape so postgres-statefulset.yaml is unchanged.
#
# When empty (default), the chart falls back to the legacy
# postgres-secret.yaml path using `.password` above. Tower sets
# this field on every new instance starting v0.77; pre-v0.77
# instances stay on the legacy path until migrated by the one-shot
# tool (Chunk 3 of A-OpenBao).
#
# The actual path resolution is hardcoded in the
# postgres-password-externalsecret.yaml template; this field is
# the on/off toggle. Setting it to any non-empty value is
# equivalent ("use ESO for this instance"); the path string itself
# is currently advisory (it's the OpenBao subtree the operator can
# `bao kv list` to find the password).
passwordVaultPath: ""
# externalSecretsStoreRef — ClusterSecretStore name the
# ExternalSecret references. Provisioned by cluster-platform-v3
# under `openbao-platform` by default. Override only on clusters
# that name the store differently.
externalSecretsStoreRef: openbao-platform
storage: 10Gi storage: 10Gi
backups: backups: