railiance-platform/workplans/RPF-WP-0046-eso-database-token-renewal.md
codex 1918128f8e
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 4s
RPF-WP-0046-T04 done: renewer adopted, first run renewed all five lanes
Earliest old-token expiry is canned-prompts (~2026-10-10), not 10-25.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 150322@bnt-lap001
Assistant-Session: 16a7b788-374e-4915-a1df-fc87ffd9a5e4
2026-09-24 00:38:59 +02:00

196 lines
8.2 KiB
Markdown

---
id: RPF-WP-0046
type: workplan
title: "Keep the dynamic-database ESO parent tokens alive: periodic tokens and a renewer"
domain: financials
repo: railiance-platform
status: active
owner: railiance-platform
topic_slug: railiance
created: "2026-09-23"
updated: "2026-09-23"
related: [RPF-WP-0045, RPF-WP-0037, RPF-WP-0036]
state_hub_workstream_id: "cdb309e0-adb4-58aa-9c98-5ea270780e8d"
---
## Problem
Five ClusterSecretStores read OpenBao dynamic database credentials
(`path: database`) with a static `tokenSecretRef`. The stores are
`openbao-audit-core-database`, `-canned-prompts-database`,
`-core-hub-database`, `-sbom-nexus-database` and `-tenant-engine-database`.
Each token is a 768h renewable orphan. Nothing renews it, and ESO never renews
a `tokenSecretRef` token. When a token reaches its max TTL, OpenBao revokes
every lease the token created. On 2026-09-23 this took down sbom-nexus
(State Hub `/repos` returned 502), tenant-engine (742 restarts) and
core-hub-api-candidate (43h unready). activity-core restored all three with
fresh 768h tokens (rapp-postgres `3ebd984`, hub messages `200e2aae` and
`42b0365c`). **Those tokens expire again around 2026-10-25T17:00Z.**
## Decision (founder, 2026-09-23)
Auto-renew, rather than switching to Kubernetes auth or static DB roles.
Why Kubernetes auth is not a drop-in fix here: ESO v0.16.1
(`pkg/provider/vault/client.go`, `Close()`) revokes a login-obtained token after
each reconcile unless token caching is enabled. Revoking that token revokes
the dynamic leases it just created, so the delivered DB password would die
within seconds. RPF-WP-0045 was safe only because its stores read KV. Static
database roles would remove the parent-token dependency. That change crosses
rapp-postgres and four consumer repos, and remains a possible later plan.
Design:
- Re-mint each token once as a **periodic** token (`-period=168h`, renewable,
orphan, same policies). A periodic token has no max TTL; each renewal resets
it to the period.
- A daily CronJob `external-secrets/eso-token-renewer` mounts the five token
Secrets read-only and calls `auth/token/renew-self`. The job fails if any
lane fails or ends below 72h. A token that was not re-minted as periodic
trips that threshold near its max TTL, so it cannot expire silently.
- The CronJob ships through ArgoCD (`railiance-platform-addons`), not by
direct apply, per the change gate.
## T01 Renewer worker and manifests
```task
id: RPF-WP-0046-T01
status: done
priority: high
state_hub_task_id: "25592a83-d2e5-5aa2-b23e-5d563a621f19"
```
`argocd/platform-addons/eso-token-renewer/` holds `renew.py`, the
ServiceAccount (no RBAC, no API token), the CronJob and the kustomization. The
kustomization generates the worker ConfigMap. The draft Application is
`argocd/railiance01/drafts/eso-token-renewer.application.yaml`. The
`railiance-platform-addons` AppProject gains `batch/CronJob` in git. That
AppProject is applied by hand, so the git change alone is not live. Tests:
`tests/test_eso_token_renewer.py`. A server-side dry run on railiance01 on
2026-09-23 passed for all three objects.
## T02 Periodic attended mint script
```task
id: RPF-WP-0046-T02
status: done
priority: high
state_hub_task_id: "a318792f-82ba-568b-8633-fba3513aa978"
```
The script is `scripts/openbao-eso-db-token-periodic-attended.sh`. It is
rapp-postgres's reviewed silent contract, changed only to use `-period` and
to cover all five lanes. It verifies `period > 0`, `renewable` and the exact
policy set before it writes the Secret with server-side apply. Lane-to-Secret
parity with the CronJob mounts is tested.
## T03 Re-mint the five tokens as periodic (live, founder)
```task
id: RPF-WP-0046-T03
status: wait
priority: high
state_hub_task_id: "211bc548-34af-5f3a-ab2c-bf29d33f97ad"
```
This step is `ADMINISTER @ realm:kubernetes/railiance01`, `activation=APPROVED`,
and needs an attended OIDC/MFA login:
```sh
python3 /home/worsch/railiance-platform/scripts/openbao-attended-exec.py -- \
/home/worsch/railiance-platform/scripts/openbao-eso-db-token-periodic-attended.sh \
--confirm RPF-WP-0046-PERIODIC-ESO-TOKEN \
--status /home/worsch/railiance-platform/docs/evidence/<date>-eso-db-token-periodic.status \
audit-core canned-prompts core-hub sbom-nexus tenant-engine
```
Then force a refresh and check that all five stores are `Valid` and their
ExternalSecrets are `SecretSynced`.
## T04 Adopt the renewer through ArgoCD (live, founder)
```task
id: RPF-WP-0046-T04
status: done
priority: high
state_hub_task_id: "28726b1b-3777-5945-859b-9e563b12fee6"
```
1. Apply `argocd/railiance01/bootstrap/02-railiance-platform-addons-project.yaml`
by hand, which adds `batch/CronJob`.
2. Pin `targetRevision` in the draft to the reviewed commit, move the draft to
`argocd/railiance01/applications/`, merge, and sync the root by hand.
3. Sync `eso-token-renewer` manually. Then run one Job by hand:
`kubectl -n external-secrets create job --from=cronjob/eso-token-renewer eso-token-renewer-first`.
Every lane must print `ok: true` with a TTL of 604800.
T04 progress, 2026-09-23 (founder go-ahead):
- A `kubectl diff` of the AppProject showed only `+ batch/CronJob`. The
session's permission guard blocked the apply ("Shared Cluster Mutation"), so
the founder runs step 1.
- Step 2 is done in git. The Application was moved to
`argocd/railiance01/applications/` and pinned to `b2ebe10`. The root has no
automated sync, so nothing changes until the manual root sync.
T04 done, 2026-09-24. The founder applied the AppProject and hand-synced the
root (`8d878cc`) and `eso-token-renewer` (`b2ebe10`). The app is
Synced/Healthy. A first manual Job completed in 9 seconds, and all five lanes
returned `ok: true`. The tokens are still pre-T03, so their TTLs cannot be
extended. **The earliest expiry is canned-prompts, about 2026-10-10**, not
2026-10-25. Then audit-core, about 2026-10-17. Evidence:
`docs/evidence/2026-09-24-eso-token-renewer-first-run.json`.
## T05 Move consumers off leases from the old tokens before 2026-10-25
```task
id: RPF-WP-0046-T05
status: wait
priority: high
state_hub_task_id: "acf49bee-e40a-533c-b1f0-4152c989211d"
```
The replaced 768h tokens still expire around 2026-10-25T17:00Z, and they
revoke their leases when they do. ESO refreshes every 5 minutes, so within
minutes of T03 each target Secret holds credentials from the new token. The
mounted files update, but a process that reads its password only at startup
keeps the old one. core-hub runs a credential watcher. Restart the others once
after T03, at least 10 minutes later and well before the earliest old-token expiry (canned-prompts, about 2026-10-10):
`audit-core/audit-core`, `canned-prompts/canned-prompts`,
`sbom-nexus/sbom-nexus`, `tenant-engine/tenant-engine`. Check readiness after
each restart. Then confirm that core-hub-api and core-hub-api-candidate stay
ready.
## T06 Hand-offs and assurance
```task
id: RPF-WP-0046-T06
status: todo
priority: medium
state_hub_task_id: "2f528e56-ebf0-547e-8d4f-3cb34c348d46"
```
- rapp-postgres: the 768h mint scripts (`apply-*-eso-token*.sh`) would undo
T03 if rerun. Point them at this plan's periodic script, or change them to
`-period`.
- audit-core: `scripts/renew-runtime-lease.sh` re-mints a 768h token. It needs
the same alignment.
- railiance-telemetry: alert on a failed `eso-token-renewer` Job, or on no
successful run in 48h.
- RPF-WP-0036: add the renewer's last successful run to `assurance-capture`.
## Risks
- **Renewer outage.** Seven days of slack at the 168h period, and the job
fails loudly. The alert (T06) is what makes it safe.
- **Lease max TTL is unchanged.** Each dynamic lease is still capped by its
role or mount max TTL. A pod that reads its password once and runs longer
than that loses the lease, whatever the parent token does. This is a
consumer reload issue, noted in the T06 hand-offs to the consumer owners.
- **Credential lifetime.** A periodic token lives as long as it is renewed.
The scope stays exact per lane, and the Secrets carry no last-applied
annotation. Revocation is `bao token revoke` on the accessor, or deleting
the Secret and letting the period lapse.
- **Rollback.** Suspend the CronJob, or delete the Application with prune.
Re-minting 768h tokens with rapp-postgres `apply-eso-token-attended.sh`
restores today's state.