RPF-WP-0046: periodic ESO database tokens and a daily renewer
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:
parent
a66f96d0d4
commit
b2ebe10849
8 changed files with 586 additions and 0 deletions
90
argocd/platform-addons/eso-token-renewer/cronjob.yaml
Normal file
90
argocd/platform-addons/eso-token-renewer/cronjob.yaml
Normal 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}]}
|
||||
16
argocd/platform-addons/eso-token-renewer/kustomization.yaml
Normal file
16
argocd/platform-addons/eso-token-renewer/kustomization.yaml
Normal 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
|
||||
64
argocd/platform-addons/eso-token-renewer/renew.py
Normal file
64
argocd/platform-addons/eso-token-renewer/renew.py
Normal 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())
|
||||
Loading…
Add table
Add a link
Reference in a new issue