RPF-WP-0046: periodic ESO database tokens and a daily renewer
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

The five dynamic-database ClusterSecretStores use 768h static tokens that
nothing renews; expiry revoked their DB leases on 2026-09-23 and recurs
around 2026-10-25. Kubernetes auth is not a drop-in fix: ESO v0.16.1 revokes
its login token after each reconcile, which revokes the leases it created.

- eso-token-renewer CronJob (ArgoCD draft, no RBAC, mounted Secrets).
- Attended periodic mint script for all five lanes.
- CronJob added to the platform-addons AppProject in git (not yet applied).

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
This commit is contained in:
codex 2026-09-23 19:53:12 +02:00
parent a66f96d0d4
commit b2ebe10849
8 changed files with 586 additions and 0 deletions

View file

@ -0,0 +1,90 @@
# RPF-WP-0046: daily renew-self for the ESO parent tokens of the five
# dynamic-database ClusterSecretStores. The Secrets are mounted, not read via
# the API, so the ServiceAccount has no RBAC and no API token.
apiVersion: v1
kind: ServiceAccount
metadata:
name: eso-token-renewer
namespace: external-secrets
labels:
app.kubernetes.io/name: eso-token-renewer
app.kubernetes.io/part-of: railiance-platform
automountServiceAccountToken: false
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: eso-token-renewer
namespace: external-secrets
labels:
app.kubernetes.io/name: eso-token-renewer
app.kubernetes.io/part-of: railiance-platform
spec:
schedule: '40 2 * * *'
timeZone: Etc/UTC
suspend: false
concurrencyPolicy: Forbid
startingDeadlineSeconds: 3600
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
jobTemplate:
spec:
activeDeadlineSeconds: 300
backoffLimit: 2
ttlSecondsAfterFinished: 604800
template:
metadata:
labels:
app.kubernetes.io/name: eso-token-renewer
spec:
serviceAccountName: eso-token-renewer
automountServiceAccountToken: false
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 65534
runAsGroup: 65534
fsGroup: 65534
seccompProfile:
type: RuntimeDefault
containers:
- name: renew
image: python:3.12-slim@sha256:d764629ce0ddd8c71fd371e9901efb324a95789d2315a47db7e4d27e78f1b0e9
command: [python3, /worker/renew.py]
env:
- name: PYTHONDONTWRITEBYTECODE
value: '1'
- name: BAO_ADDR
value: http://openbao.openbao.svc:8200
- name: ESO_TOKEN_ROOT
value: /var/run/eso-tokens
- name: MIN_TTL_SECONDS
value: '259200'
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
resources:
requests: {cpu: 10m, memory: 24Mi}
limits: {cpu: 100m, memory: 64Mi}
volumeMounts:
- {name: worker, mountPath: /worker, readOnly: true}
- {name: audit-core, mountPath: /var/run/eso-tokens/audit-core, readOnly: true}
- {name: canned-prompts, mountPath: /var/run/eso-tokens/canned-prompts, readOnly: true}
- {name: core-hub, mountPath: /var/run/eso-tokens/core-hub, readOnly: true}
- {name: sbom-nexus, mountPath: /var/run/eso-tokens/sbom-nexus, readOnly: true}
- {name: tenant-engine, mountPath: /var/run/eso-tokens/tenant-engine, readOnly: true}
volumes:
- name: worker
configMap: {name: eso-token-renewer-worker, defaultMode: 0444}
- name: audit-core
secret: {secretName: openbao-audit-core-eso-token, defaultMode: 0440, items: [{key: token, path: token}]}
- name: canned-prompts
secret: {secretName: openbao-canned-prompts-eso-token, defaultMode: 0440, items: [{key: token, path: token}]}
- name: core-hub
secret: {secretName: openbao-core-hub-eso-token, defaultMode: 0440, items: [{key: token, path: token}]}
- name: sbom-nexus
secret: {secretName: openbao-sbom-nexus-eso-token, defaultMode: 0440, items: [{key: token, path: token}]}
- name: tenant-engine
secret: {secretName: openbao-tenant-engine-eso-token, defaultMode: 0440, items: [{key: token, path: token}]}

View file

@ -0,0 +1,16 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
# RPF-WP-0046. Read only by the railiance01 Application in
# argocd/railiance01/drafts/eso-token-renewer.application.yaml. No coulombcore
# Application points here.
namespace: external-secrets
resources:
- cronjob.yaml
configMapGenerator:
- name: eso-token-renewer-worker
files:
- renew.py

View file

@ -0,0 +1,64 @@
#!/usr/bin/env python3
"""Renew the ESO parent tokens that project OpenBao dynamic database leases.
ESO never renews a tokenSecretRef token, and OpenBao revokes every lease a
token created when that token expires (RPF-WP-0046). Each token Secret is
mounted read-only at <ESO_TOKEN_ROOT>/<lane>/token. For each lane this calls
auth/token/renew-self and prints one JSON line with the lane, the resulting
TTL and an error class. It never prints a token.
Exit 1 if any lane fails or ends below MIN_TTL_SECONDS, so the Job is marked
failed. A non-periodic token cannot be renewed past its max TTL, so it trips
the threshold as it nears expiry instead of failing silently.
"""
import json
import os
from pathlib import Path
import sys
import urllib.error
import urllib.request
ADDR = os.environ.get('BAO_ADDR', 'http://openbao.openbao.svc:8200').rstrip('/')
ROOT = Path(os.environ.get('ESO_TOKEN_ROOT', '/var/run/eso-tokens'))
MIN_TTL = int(os.environ.get('MIN_TTL_SECONDS', str(72 * 3600)))
def renew(token, opener=urllib.request.urlopen):
request = urllib.request.Request(
ADDR + '/v1/auth/token/renew-self', data=b'{}', method='POST',
headers={'X-Vault-Token': token, 'Content-Type': 'application/json'})
with opener(request, timeout=15) as response:
auth = json.load(response).get('auth') or {}
return {'ttl': auth.get('lease_duration'), 'renewable': auth.get('renewable')}
def check_lane(lane_dir, opener=urllib.request.urlopen):
result = {'lane': lane_dir.name, 'ok': False}
try:
token = (lane_dir / 'token').read_text(encoding='utf-8').strip()
if not token:
return dict(result, error='empty_token')
result.update(renew(token, opener))
except urllib.error.HTTPError as error:
return dict(result, error='http_%s' % error.code)
except (urllib.error.URLError, OSError, ValueError) as error:
return dict(result, error=type(error).__name__)
ttl = result.get('ttl')
if not isinstance(ttl, int) or ttl < MIN_TTL:
return dict(result, error='ttl_below_threshold')
return dict(result, ok=True)
def main(opener=urllib.request.urlopen):
lanes = sorted(p for p in ROOT.iterdir() if p.is_dir() and not p.name.startswith('.'))
if not lanes:
print(json.dumps({'ok': False, 'error': 'no_lanes_mounted'}))
return 1
results = [check_lane(lane, opener) for lane in lanes]
for item in results:
print(json.dumps(item, sort_keys=True))
return 0 if all(item['ok'] for item in results) else 1
if __name__ == '__main__':
sys.exit(main())

View file

@ -40,6 +40,8 @@ spec:
kind: ServiceAccount
- group: apps
kind: Deployment
- group: batch
kind: CronJob
- group: rbac.authorization.k8s.io
kind: Role
- group: rbac.authorization.k8s.io

View file

@ -0,0 +1,27 @@
# DRAFT for railiance01 (RPF-WP-0046-T04). Not synced by any root: move to
# ../applications/ only in T04, with the founder's go-ahead, after the
# platform-addons AppProject carries batch/CronJob. No automated sync, no
# finalizer. Set targetRevision to the reviewed commit at adoption.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: eso-token-renewer
namespace: argocd
labels:
app.kubernetes.io/part-of: railiance-gitops
railiance-platform/component: external-secrets
annotations:
argocd.argoproj.io/sync-wave: "2"
spec:
project: railiance-platform-addons
source:
repoURL: https://forgejo.coulomb.social/coulomb/railiance-platform.git
targetRevision: PIN-AT-ADOPTION
path: argocd/platform-addons/eso-token-renewer
destination:
server: https://kubernetes.default.svc
namespace: external-secrets
syncPolicy:
syncOptions:
- ApplyOutOfSyncOnly=true
- PruneLast=true