Deliver database credentials as a rotatable mounted directory
AUDIT-WP-0005-T02 (progress). rapp-postgres has landed platform-pg with the audit_core database, roles, and dynamic credential provisioning, so audit-core's side is now built against it. In-cluster delivery is a mounted directory rather than environment variables. A dynamic lease rotates while the pod runs and an env var is fixed at process start, so env delivery would force a restart on every rotation - and every restart is a delivery gap, which is what this task forbids. CredentialDirectory is re-read on every connection attempt via psycopg_pool's callable kwargs, so a rotated lease takes effect with no restart. Rotation is logged by password fingerprint, never by value. deploy/externalsecrets.yaml follows the ClusterSecretStore -> ExternalSecret -> Secret pattern already used by activity-core and rapp-qonto, at a 15m refresh rather than the default 1h since the interval bounds how long a revoked lease can stay mounted. All manifests validated --dry-run=server --validate=strict. The rotation test was initially vacuous: it passed against a deliberately naive implementation that read credentials once at startup, because pooled sessions stay authenticated after a password change and nothing forced a reconnect. It now terminates the role's sessions first, and is verified to fail against the naive implementation and pass against the real one. Tests 82 -> 84. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
fc48378a3f
commit
7636e83dcc
7 changed files with 364 additions and 20 deletions
|
|
@ -101,13 +101,16 @@ spec:
|
|||
value: "30"
|
||||
- name: AUDIT_CORE_DB_STATEMENT_TIMEOUT_MS
|
||||
value: "30000"
|
||||
# Both secrets are delivered through the OpenBao lane
|
||||
# (AUDIT-WP-0005-T02) — never committed, never set by hand.
|
||||
- name: AUDIT_CORE_DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: audit-core-database
|
||||
key: url
|
||||
# The database credential is a mounted directory, not a variable.
|
||||
# A dynamic lease rotates while the pod runs; an env var is fixed at
|
||||
# process start, so env delivery would force a restart — and a
|
||||
# delivery gap — on every rotation. audit-core re-reads this
|
||||
# directory on each connection attempt (AUDIT-WP-0005-T02).
|
||||
- name: AUDIT_CORE_CREDENTIAL_DIR
|
||||
value: /etc/audit-core/db
|
||||
# The sender registry is read once at startup, so a variable is
|
||||
# adequate here. Token rotation is overlap-first inside the
|
||||
# registry itself and needs no restart either.
|
||||
- name: AUDIT_CORE_SENDERS
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
|
|
@ -132,6 +135,9 @@ spec:
|
|||
volumeMounts:
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
- name: database-credential
|
||||
mountPath: /etc/audit-core/db
|
||||
readOnly: true
|
||||
startupProbe:
|
||||
httpGet: {path: /healthz, port: http}
|
||||
periodSeconds: 3
|
||||
|
|
@ -155,3 +161,10 @@ spec:
|
|||
volumes:
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
- name: database-credential
|
||||
secret:
|
||||
# Kubernetes updates the projected files in place when the
|
||||
# ExternalSecret refreshes, which is what makes restart-free
|
||||
# rotation possible.
|
||||
secretName: audit-core-database
|
||||
defaultMode: 0400
|
||||
|
|
|
|||
75
deploy/externalsecrets.yaml
Normal file
75
deploy/externalsecrets.yaml
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# Credential delivery for audit-core (AUDIT-WP-0005-T02).
|
||||
#
|
||||
# Follows the ClusterSecretStore -> ExternalSecret -> Secret pattern already in
|
||||
# use by activity-core and rapp-qonto. audit-core never holds a credential in
|
||||
# its own configuration; it reads whatever is currently mounted.
|
||||
#
|
||||
# PREREQUISITE (rapp-postgres / railiance-platform, not this repo):
|
||||
# - a ClusterSecretStore named openbao-audit-core, scoped to this namespace
|
||||
# - the OpenBao database role rapp-postgres/audit-core-runtime issuing leases
|
||||
# against the audit_core_app group role
|
||||
# Apply order: ClusterSecretStore, then this, then the Deployment.
|
||||
---
|
||||
apiVersion: external-secrets.io/v1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: audit-core-database
|
||||
namespace: audit-core
|
||||
spec:
|
||||
# Shorter than the platform default of 1h: these are dynamic leases, and the
|
||||
# refresh interval bounds how long a revoked lease can remain mounted.
|
||||
refreshInterval: 15m
|
||||
secretStoreRef:
|
||||
kind: ClusterSecretStore
|
||||
name: openbao-audit-core
|
||||
target:
|
||||
name: audit-core-database
|
||||
creationPolicy: Owner
|
||||
deletionPolicy: Retain
|
||||
template:
|
||||
engineVersion: v2
|
||||
# One value per file. The pod mounts this Secret as a directory and
|
||||
# audit-core re-reads it on every connection attempt, so a rotated lease
|
||||
# takes effect without a restart and without a delivery gap.
|
||||
data:
|
||||
username: "{{ .username }}"
|
||||
password: "{{ .password }}"
|
||||
host: platform-pg-rw.databases.svc.cluster.local
|
||||
port: "5432"
|
||||
dbname: audit_core
|
||||
data:
|
||||
- secretKey: username
|
||||
remoteRef:
|
||||
key: platform/workloads/audit-core/database/audit-core-runtime
|
||||
property: username
|
||||
- secretKey: password
|
||||
remoteRef:
|
||||
key: platform/workloads/audit-core/database/audit-core-runtime
|
||||
property: password
|
||||
---
|
||||
apiVersion: external-secrets.io/v1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: audit-core-senders
|
||||
namespace: audit-core
|
||||
spec:
|
||||
refreshInterval: 1h
|
||||
secretStoreRef:
|
||||
kind: ClusterSecretStore
|
||||
name: openbao-audit-core
|
||||
target:
|
||||
name: audit-core-senders
|
||||
creationPolicy: Owner
|
||||
deletionPolicy: Retain
|
||||
data:
|
||||
# The sender registry: which credential may write for which tenant and
|
||||
# source, and each sender's secret_policy. Held in OpenBao rather than the
|
||||
# manifest because it contains bearer tokens.
|
||||
#
|
||||
# Rotation is overlap-first: add the replacement to a sender's `tokens`
|
||||
# list, move the sender, then drop the predecessor. Both are valid in
|
||||
# between, so there is no delivery gap.
|
||||
- secretKey: senders.json
|
||||
remoteRef:
|
||||
key: platform/workloads/audit-core/senders
|
||||
property: senders.json
|
||||
Loading…
Add table
Add a link
Reference in a new issue