Establish scoped KeyCape factor custody and verified automatic renewal
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
KeyCape factor custody acceptance / acceptance (push) Successful in 7s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a092fe-13b1-7f12-ac74-7d258af4d79c
This commit is contained in:
codex 2026-09-13 16:25:33 +02:00
parent b75729b799
commit 2e2c31d237
22 changed files with 1169 additions and 1 deletions

View file

@ -0,0 +1,26 @@
name: KeyCape factor custody acceptance
on:
push:
branches: [main]
paths: ["scripts/keycape_factor_*.py", "tests/test_keycape_factor_*.py", "openbao/policies/*keycape-factor*.hcl", "manifests/keycape-factor-custody.yaml", ".forgejo/workflows/keycape-factor-acceptance.yaml"]
workflow_dispatch:
jobs:
acceptance:
runs-on: ubuntu-latest
container:
image: python:3.12-slim@sha256:d764629ce0ddd8c71fd371e9901efb324a95789d2315a47db7e4d27e78f1b0e9
steps:
- name: Test exact source revision
run: |
set -eu
python3 - <<'PYCODE'
import os,tarfile,tempfile,urllib.request
from pathlib import Path
root=Path('/tmp/factor-acceptance');root.mkdir()
url='https://forgejo.coulomb.social/'+os.environ['GITHUB_REPOSITORY']+'/archive/'+os.environ['GITHUB_SHA']+'.tar.gz'
with urllib.request.urlopen(url,timeout=30) as response,tempfile.TemporaryFile() as archive:
archive.write(response.read());archive.seek(0)
with tarfile.open(fileobj=archive,mode='r:gz') as tar:tar.extractall(root,filter='data')
PYCODE
cd /tmp/factor-acceptance/*
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s tests -p 'test_keycape_factor_*.py'

View file

@ -574,3 +574,11 @@ assurance-capture: ## Capture selected status metadata from the pinned railiance
assurance-check: ## Classify freshness/failure; nonzero means incomplete or invalid evidence assurance-check: ## Classify freshness/failure; nonzero means incomplete or invalid evidence
python3 scripts/service_assurance.py evaluate "$(ASSURANCE_OBSERVATION)" python3 scripts/service_assurance.py evaluate "$(ASSURANCE_OBSERVATION)"
.PHONY: keycape-factor-tests
keycape-factor-tests: ## Validate factor custody, scope, renewal and config preservation offline
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s tests -p 'test_keycape_factor_*.py'
.PHONY: keycape-factor-live-acceptance
keycape-factor-live-acceptance: ## Verify native scope and rotation (creates bounded proof/renewal Jobs)
PYTHONDONTWRITEBYTECODE=1 python3 scripts/keycape_factor_acceptance.py

View file

@ -0,0 +1,94 @@
id: CCR-2026-0023
kind: credential-change-request
schema_version: 1
request_type: workload-kv-read
title: KeyCape realm-scoped factor credential delivery
status: active
created: '2026-09-13'
updated: '2026-09-13'
requester:
agent: codex
reason: User authorized establishing provider credential custody and delivery for
platform P05; RPF-WP-0040 and KEY-WP-0035.
review:
required: true
required_approvers:
- platform-operator
comments:
- at: '2026-09-13T00:00:00+00:00'
reviewer: user (platform operator)
decision: approved
comment: User offered administrative authentication and replied "ok, lets do that"
to establishing credential storage and delivery. Exact dedicated path, least-privilege
workload binding and secret-free attended execution implement that authorized
scope. Provider recovery and policy acceptance remain separate gates.
target:
domain: infotech
tenant: platform
workload: key-cape
environment: production
purpose: Deliver only a provider-issued coulomb factor-read JWT to KeyCape, separating
it from issuer credentials and signing keys.
openbao:
mount: platform
kv_path: platform/workloads/net-kingdom/keycape-factor-read
fields:
- TOKEN
- EXPIRES_AT
policy_name: workload-kv-read-keycape-factor-read
policy_file: openbao/policies/workload-kv-read-keycape-factor-read.hcl
metadata_read: true
token_self_lifecycle: true
auth:
method: kubernetes
mount: kubernetes
role: keycape-factor-workload-kv-read
bound_claims:
service_account_names:
- keycape-factor-eso
service_account_namespaces:
- sso
bound_claims_confirmed: true
policies:
- workload-kv-read-keycape-factor-read
ttl: 15m
access_frontdoor:
type: external-secrets
catalog_id: keycape-factor-read
readiness: ready
resolvable: true
delivery:
surface: external-secrets
target: Namespace-restricted ClusterSecretStore openbao-keycape-factor-read -> sso/keycape-factor-read
Secret admin-token. Mount only the JWT in KeyCape; provider password remains in
separate custody.
risk:
classification: high
notes:
- JWT can list factors only in coulomb; enforce provider policy before activation.
- OpenBao TTL does not renew or revoke the privacyIDEA JWT.
- No personal admin credentials delivered to the issuer.
verification:
positive:
- Exact metadata readback and correct-SA Kubernetes login.
- Provider-issued JWT accepted and projected file reread after renewal.
negative:
- Sibling KV paths and writes denied; wrong SA or namespace cannot authenticate.
- Provider mutation permission denied and expired credential fails closed.
activation_conditions:
- Attended metadata apply and exact readback.
- Dedicated provider identity with verified rights/expiry and separate renewable
custody.
- Native delivery and positive/negative factor lookup evidence.
evidence:
- docs/evidence/2026-09-13-keycape-factor-custody.md
lifecycle:
deactivate: Detach reader role, stop renewal and revoke/expire provider token; preserve
custody history.
rotate: Issue replacement before expiry, CAS update exact KV, verify ESO projection
and consumer acceptance; retain no plaintext artifacts.
compromised: Disable the dedicated provider principal and reconcile JWT revocation
or expiry before recovery.
state_hub:
workplan_id: RPF-WP-0040
task_id: RPF-WP-0040-T01

View file

@ -0,0 +1,55 @@
# KeyCape factor credential custody — 2026-09-13
User-authorized attended setup is live under RPF-WP-0040 / CCR-2026-0023.
The operator authenticated through the contained OpenBao login lane. No
credential value was included in source, receipts, chat or ordinary logs.
Dedicated privacyIDEA identity `keycape-factor-reader` has `tokenlist` permission
for `coulomb`. Its password is in a separate OpenBao issuer path; KeyCape
receives only a provider-issued, expiring JWT via External Secrets. Explicit
fallback policy preserves existing administrators' prior rights and excludes
this service identity. The service cannot read provider administration policy.
Live checks passed:
- Exact OpenBao policy/role readback, audience and service-account bindings.
- Provider-issued credential, cross-user factor visibility, and the actual
KeyCape `user` + `realm` + `active=True` lookup shape.
- Wrong service account rejected; issuer password and sibling KV reads denied.
- ExternalSecret `sso/keycape-factor-read`: Ready=True / SecretSynced.
- Initial renewal Job published KV version 2 and revoked its OpenBao session.
A second Job published version 3; the existing mounted file received the
replacement and provider lookup accepted it without a pod restart.
- KeyCape config activation passed server dry-run and UID/resource-version CAS;
readback confirmed all other Secret data and configuration bytes preserved.
Deployment rolled out successfully with the existing image
`sha256:c9eb584d60efecfe00e1745a7e8cd3ebb4ae0f94faa715ef9594cd58a5dcebb6`.
- Fourteen offline tests cover identity/expiry/status rejection, scope boundaries,
failed-provider preservation, CAS failure, session-cleanup failure, retry,
ambiguous config rejection and byte-preserving activation.
Runtime source: `manifests/keycape-factor-custody.yaml`; renewal every ten minutes,
ESO refresh every minute. Renewal runs as non-root with an immutable ConfigMap,
digest-pinned image, dedicated service account and restricted network policy.
The reader has no issuer-password permission; the renewer cannot write that
password. OpenBao sessions last at most fifteen minutes (ESO) or five minutes
(renewer). Successful renewal workers revoke their sessions explicitly.
Repeat: `make keycape-factor-tests` and `make keycape-factor-live-acceptance`.
Exact-commit CI is `.forgejo/workflows/keycape-factor-acceptance.yaml`.
The first proof Job was `keycape-factor-delivery-acceptance`; its short-lived
reader session expires by its fifteen-minute maximum TTL. The final reusable
proof also revokes its session; its result is recorded below after execution.
Limits: this establishes credential custody, renewal and native delivery.
Genuine provider JWT expiry/revocation drills remain RPF-WP-0040-T04; renewing a
JWT does not revoke its predecessor. Optional-after-enrollment policy remains
unchanged. OTP onboarding, lost-factor recovery and their complete platform-admin
browser journeys remain KEY-WP-0035 / USER-WP-0030. Historical resolver incident
NK-WP-0033 is not closed by this new service lane.
Final reusable proof passed: `keycape-factor-proof-0c4a6929`. All eight checks
were true: wrong-SA rejection, issuer-password denial, sibling-secret denial,
mounted-token/custody equality, KeyCape per-user lookup, provider administration
denial, renewed projection acceptance, and explicit reader-session revocation.
KeyCape remained Ready 1/1 at deployment generation 46. CCR validation passed.

View file

@ -1,6 +1,7 @@
# NetKingdom credential custody contract # NetKingdom credential custody contract
Status: **draft / blocked pending OpenBao owner confirmation** Historical resolver lanes: **draft / blocked**.
KeyCape factor service lane: **active**, established and verified 2026-09-13 (below).
Incident: `KEYCAPE-EXPOSURE-20260823-01` Incident: `KEYCAPE-EXPOSURE-20260823-01`
Consumer procedure: NetKingdom `NK-WP-0033`, resolver reconciliation revision Consumer procedure: NetKingdom `NK-WP-0033`, resolver reconciliation revision
`eec7007` / checkout `f2e578c` `eec7007` / checkout `f2e578c`
@ -43,3 +44,75 @@ The routing lane is registered but remains `resolvable: false`. No
`warden access --fetch`, proxy execution, resolver retry, or predecessor `warden access --fetch`, proxy execution, resolver retry, or predecessor
restoration is authorized until the OpenBao owner publishes the missing restoration is authorized until the OpenBao owner publishes the missing
metadata and the attended handoff is approved. metadata and the attended handoff is approved.
## KeyCape factor service lane — authorized setup, 2026-09-13
RPF-WP-0040 / CCR-2026-0023 establish a new dedicated service lane. The user,
acting as platform operator, authorized establishing storage/delivery and performed
attended OpenBao authentication. This is separate from the historical resolver
repair lanes above; their incident receipts are not retroactively completed.
| Purpose | KV v2 CLI path | Fields | Consumer |
| --- | --- | --- | --- |
| Scoped provider JWT | platform/workloads/net-kingdom/keycape-factor-read | TOKEN, EXPIRES_AT, REQUEST | KeyCape via namespace-scoped ESO and adminTokenFile |
| Renewable service login | platform/workloads/net-kingdom/keycape-factor-issuer | USERNAME, PASSWORD, REQUEST | Owner bootstrap, then dedicated renewal worker; never KeyCape |
Provider identity: keycape-factor-reader (local privacyIDEA service admin).
Effective permission: tokenlist for realm coulomb only. Preserve the existing
no-admin-policy baseline for other administrators using an explicit fallback
that excludes this service identity. Only the two reviewed named policies may
be introduced; unexpected existing admin policies abort setup.
Generate the service password in owner-process memory and persist with KV CAS=0
before provider creation, allowing interrupted setup to resume. The attended
worker passes values through stdin/captured pipes only, never argv or receipts.
Only explicit KV absence permits creation. Existing request provenance and
policy definitions must match; retries do not rotate an existing identity.
Reader policy: workload-kv-read-keycape-factor-read, role
keycape-factor-workload-kv-read, bound sso/keycape-factor-eso, audience openbao,
15-minute maximum OpenBao session, no default policy. Metadata apply/readback
passed via the attended owner envelope. Provider issuance, native ESO delivery,
KeyCape mounted-file activation, per-user lookup and rotation without restart
passed. The reader can inspect and revoke only its own OpenBao session in
addition to reading the exact JWT data/metadata paths. See the acceptance record
`docs/evidence/2026-09-13-keycape-factor-custody.md`.
Provider JWTs expire independently of OpenBao sessions. Require 5 minutes to
2 hours remaining lifetime at issuance; renew well before expiry. Disable the
service's tokenlist policy (while retaining fallback exclusion) to withdraw its
factor access; do not rotate privacyIDEA's global signing key as routine renewal.
Verify expiry/permission refusal separately from OpenBao self-revocation.
Renewal: `sso/keycape-factor-renewer` runs every ten minutes under its own
service account, with a five-minute maximum OpenBao session. Its separate policy
reads the issuer password and reads/writes only the delivered JWT path. The
worker verifies provider identity, expiry, factor lookup and denied policy
administration before a CAS write and readback, then revokes its session.
ESO refreshes once per minute. KeyCape mounts only `admin-token`, mode 0440,
through fsGroup 65534; the issuer password never enters its namespace Secret.
Operations: `make keycape-factor-tests` runs offline failure/retry and config
preservation tests. `make keycape-factor-live-acceptance` creates bounded Jobs
that verify native delivery scope and a second renewal reaching the mounted
file. The runner emits booleans and resource identifiers only. CronJob failures
and ExternalSecret Ready=False are the native degraded-state signals. A failed
renewal leaves the last verified token in custody; expiry causes authentication
to fail closed. Restore provider/custody connectivity and launch a renewal Job
from the CronJob; verify SecretSynced and repeat live acceptance.
Attended setup is reproducible through `openbao-attended-exec.py` with
`keycape_factor_metadata.py --apply --receipt <new-file>`,
`keycape_factor_bootstrap.py --receipt <new-file>` and
`keycape_factor_renewal_metadata.py --receipt <new-file>` in that order, followed
by the custody manifest and guarded `keycape_factor_activate.py --apply`.
Use unique receipt paths. Existing mismatched policies/roles/provenance fail
closed; never replace an unknown owner credential. The separate
`keycape_factor_eso_metadata.py` records the migration from initial read-only
metadata to ESO's self-session permissions; fresh setup uses the final policy.
Outstanding acceptance is explicit in RPF-WP-0040-T04: genuine provider JWT
expiry and provider-policy revocation/recovery drills. A newly issued JWT does
not itself revoke its predecessor. Native mounted rotation has passed, but it
is not evidence of predecessor revocation. Optional MFA rollout and lost-factor
recovery remain KEY-WP-0035 / USER-WP-0030, not completed by this service lane.

View file

@ -0,0 +1,267 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: keycape-factor-eso
namespace: sso
labels:
app.kubernetes.io/part-of: net-kingdom-sso-mfa
automountServiceAccountToken: false
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: keycape-factor-renewer
namespace: sso
labels:
app.kubernetes.io/part-of: net-kingdom-sso-mfa
automountServiceAccountToken: false
---
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
name: openbao-keycape-factor-read
spec:
conditions:
- namespaces:
- sso
provider:
vault:
server: http://openbao.openbao.svc.cluster.local:8200
path: platform
version: v2
auth:
kubernetes:
mountPath: kubernetes
role: keycape-factor-workload-kv-read
serviceAccountRef:
name: keycape-factor-eso
namespace: sso
audiences:
- openbao
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: keycape-factor-read
namespace: sso
labels:
app.kubernetes.io/part-of: net-kingdom-sso-mfa
spec:
refreshInterval: 1m
secretStoreRef:
kind: ClusterSecretStore
name: openbao-keycape-factor-read
target:
name: keycape-factor-read
creationPolicy: Owner
data:
- secretKey: admin-token
remoteRef:
key: workloads/net-kingdom/keycape-factor-read
property: TOKEN
- secretKey: expires-at
remoteRef:
key: workloads/net-kingdom/keycape-factor-read
property: EXPIRES_AT
---
apiVersion: v1
kind: ConfigMap
metadata:
name: keycape-factor-renewer-2cf770000e
namespace: sso
labels:
app.kubernetes.io/part-of: net-kingdom-sso-mfa
immutable: true
data:
renew.py: "\"\"\"Dedicated Kubernetes renewal worker. Only sanitized outcome metadata\
\ is emitted.\"\"\"\nimport base64,json,sys,time,urllib.request,urllib.error,urllib.parse\n\
from pathlib import Path\nBAO=\"http://openbao.openbao.svc.cluster.local:8200/v1\"\
\nPI=\"http://privacyidea.mfa.svc.cluster.local:8080\"\nISSUER=\"platform/data/workloads/net-kingdom/keycape-factor-issuer\"\
\nTARGET=\"platform/data/workloads/net-kingdom/keycape-factor-read\"\nUSER=\"\
keycape-factor-reader\"\nPROVENANCE=\"RPF-WP-0040\"\nclass Failure(Exception):pass\n\
class NoRedirect(urllib.request.HTTPRedirectHandler):\n def redirect_request(self,*args,**kwargs):return\
\ None\n\ndef http(url,body=None,headers=None,method=None,form=False):\n data=None\n\
\ headers=dict(headers or {})\n if body is not None:\n data=(urllib.parse.urlencode(body)\
\ if form else json.dumps(body)).encode()\n headers[\"Content-Type\"]=\"application/x-www-form-urlencoded\"\
\ if form else \"application/json\"\n req=urllib.request.Request(url,data=data,headers=headers,method=method)\n\
\ try:\n with urllib.request.build_opener(NoRedirect()).open(req,timeout=15)\
\ as r:\n raw=r.read(1048577)\n if len(raw)>1048576:raise Failure()\n return\
\ r.status,json.loads(raw) if raw else {}\n except urllib.error.HTTPError as e:return\
\ e.code,{}\n\ndef success(code,data):\n if code!=200 or not data.get(\"result\"\
,{}).get(\"status\"):raise Failure()\n return data[\"result\"][\"value\"]\n\n\
def run():\n session=None;receipt={\"success\":False,\"phase\":\"workload_login\"\
}\n try:\n jwt=Path(\"/var/run/keycape-factor/token\").read_text().strip()\n\
\ code,body=http(BAO+\"/auth/kubernetes/login\",{\"role\":\"keycape-factor-renewer\"\
,\"jwt\":jwt})\n if code!=200:raise Failure()\n session=body[\"auth\"][\"client_token\"\
];headers={\"X-Vault-Token\":session}\n receipt[\"phase\"]=\"issuer_custody\"\
\n code,body=http(BAO+\"/\"+ISSUER,headers=headers)\n if code!=200:raise Failure()\n\
\ issuer=body[\"data\"][\"data\"]\n if issuer.get(\"REQUEST\")!=PROVENANCE or\
\ issuer.get(\"USERNAME\")!=USER:raise Failure()\n receipt[\"phase\"]=\"provider_authentication\"\
\n value=success(*http(PI+\"/auth\",{\"username\":USER,\"password\":issuer[\"\
PASSWORD\"]},form=True))\n if value.get(\"role\")!=\"admin\" or value.get(\"\
username\")!=USER:raise Failure()\n token=value[\"token\"];claims=json.loads(base64.urlsafe_b64decode(token.split('.')[1]+'==='));expiry=int(claims[\"\
exp\"])\n if not 300<expiry-time.time()<=7200:raise Failure()\n receipt[\"phase\"\
]=\"provider_scope\"\n provider_headers={\"Authorization\":token}\n listing=success(*http(PI+\"\
/token/?tokenrealm=coulomb&active=True&pagesize=1\",headers=provider_headers))\n\
\ if not isinstance(listing.get(\"tokens\"),list) or not isinstance(listing.get(\"\
count\"),int):raise Failure()\n code,_=http(PI+\"/policy/\",headers=provider_headers)\n\
\ if code not in (401,403):raise Failure()\n receipt[\"phase\"]=\"publish\"\n\
\ code,body=http(BAO+\"/\"+TARGET,headers=headers)\n if code!=200 or body[\"\
data\"][\"data\"].get(\"REQUEST\")!=PROVENANCE:raise Failure()\n version=body[\"\
data\"][\"metadata\"][\"version\"]\n code,_=http(BAO+\"/\"+TARGET,{\"options\"\
:{\"cas\":version},\"data\":{\"TOKEN\":token,\"EXPIRES_AT\":expiry,\"REQUEST\"\
:PROVENANCE}},headers=headers)\n if code!=200:raise Failure()\n code,body=http(BAO+\"\
/\"+TARGET,headers=headers)\n if code!=200 or body[\"data\"][\"data\"][\"TOKEN\"\
]!=token:raise Failure()\n receipt.update(success=True,phase=\"complete\",expires_at=expiry,kv_version=body[\"\
data\"][\"metadata\"][\"version\"],cross_user_factor_visible=listing[\"count\"\
]>0)\n except Exception:pass\n finally:\n if session:\n try:\n code,_=http(BAO+\"\
/auth/token/revoke-self\",headers={\"X-Vault-Token\":session},method=\"POST\"\
)\n receipt[\"session_revoked\"]=code in (200,204)\n except Exception:receipt[\"\
session_revoked\"]=False\n if not receipt[\"session_revoked\"]:receipt[\"success\"\
]=False;receipt[\"phase\"]=\"session_cleanup\"\n return receipt\nif __name__==\"\
__main__\":\n receipt=run();print(json.dumps(receipt));raise SystemExit(0 if receipt[\"\
success\"] else 1)\n"
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: keycape-factor-renewer
namespace: sso
labels:
app.kubernetes.io/part-of: net-kingdom-sso-mfa
spec:
schedule: '*/10 * * * *'
suspend: false
concurrencyPolicy: Forbid
startingDeadlineSeconds: 120
successfulJobsHistoryLimit: 2
failedJobsHistoryLimit: 3
jobTemplate:
spec:
activeDeadlineSeconds: 120
backoffLimit: 1
ttlSecondsAfterFinished: 3600
template:
metadata:
labels:
app.kubernetes.io/name: keycape-factor-renewer
spec:
serviceAccountName: keycape-factor-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'
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: identity
mountPath: /var/run/keycape-factor
readOnly: true
volumes:
- name: worker
configMap:
name: keycape-factor-renewer-2cf770000e
- name: identity
projected:
defaultMode: 288
sources:
- serviceAccountToken:
path: token
audience: openbao
expirationSeconds: 600
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: keycape-factor-renewer
namespace: sso
labels:
app.kubernetes.io/part-of: net-kingdom-sso-mfa
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: keycape-factor-renewer
policyTypes:
- Ingress
- Egress
ingress: []
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: openbao
ports:
- port: 8200
protocol: TCP
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: mfa
podSelector:
matchLabels:
app.kubernetes.io/name: privacyidea
ports:
- port: 8080
protocol: TCP
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- port: 53
protocol: UDP
- port: 53
protocol: TCP
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-keycape-factor-renewal
namespace: mfa
labels:
app.kubernetes.io/part-of: net-kingdom-sso-mfa
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: privacyidea
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: sso
podSelector:
matchLabels:
app.kubernetes.io/name: keycape-factor-renewer
ports:
- port: 8080
protocol: TCP

View file

@ -0,0 +1,11 @@
# Dedicated renewable service password read and JWT-only publish.
path "platform/data/workloads/net-kingdom/keycape-factor-issuer" {
capabilities = ["read"]
}
path "platform/data/workloads/net-kingdom/keycape-factor-read" {
capabilities = ["read", "create", "update"]
}
path "auth/token/revoke-self" {
capabilities = ["update"]
}

View file

@ -0,0 +1,16 @@
# Exact-path JWT delivery; no issuer password or sibling paths.
path "platform/data/workloads/net-kingdom/keycape-factor-read" {
capabilities = ["read"]
}
path "platform/metadata/workloads/net-kingdom/keycape-factor-read" {
capabilities = ["read"]
}
# ESO validates its own short-lived workload session.
path "auth/token/lookup-self" {
capabilities = ["read"]
}
path "auth/token/revoke-self" {
capabilities = ["update"]
}

View file

@ -0,0 +1,39 @@
"""Run native delivery/scope/rotation acceptance; no attended admin or secret output."""
import copy,json,subprocess,time,uuid
from pathlib import Path
from keycape_factor_activate import CLUSTER,kubectl
ROOT=Path(__file__).resolve().parents[1]
def run():
if kubectl("get","ns","kube-system","-o","json")["metadata"]["uid"]!=CLUSTER:raise ValueError()
cron=kubectl("-n","sso","get","cronjob","keycape-factor-renewer","-o","json")
spec=copy.deepcopy(cron["spec"]["jobTemplate"]["spec"])
spec.update(activeDeadlineSeconds=300,backoffLimit=0)
p=spec["template"]["spec"];p["serviceAccountName"]="keycape-factor-eso"
p["containers"][0]["command"]=["python3","-c",(ROOT/"scripts/keycape_factor_delivery_probe.py").read_text()]
p["volumes"].append({"name":"factor","secret":{"secretName":"keycape-factor-read","defaultMode":288}})
p["containers"][0]["volumeMounts"].append({"name":"factor","mountPath":"/factor","readOnly":True})
suffix=uuid.uuid4().hex[:8];name="keycape-factor-proof-"+suffix
kubectl("create","-f","-","-o","json",payload={"apiVersion":"batch/v1","kind":"Job","metadata":{"name":name,"namespace":"sso"},"spec":spec})
deadline=time.monotonic()+310;rotating=False
while time.monotonic()<deadline:
r=subprocess.run(["kubectl","-n","sso","logs","job/"+name],capture_output=True,text=True,timeout=15)
results=[]
if r.returncode==0:
for line in r.stdout.splitlines():
try:results.append(json.loads(line))
except ValueError:pass
if results and not rotating:
if results[0].get("phase")!="awaiting_rotation":raise ValueError()
renewal={"apiVersion":"batch/v1","kind":"Job","metadata":{"name":"keycape-factor-rotate-"+suffix,"namespace":"sso"},"spec":cron["spec"]["jobTemplate"]["spec"]}
kubectl("create","-f","-","-o","json",payload=renewal);rotating=True
print(json.dumps({"phase":"waiting_for_mounted_rotation","job":name}),flush=True)
if results and "phase" not in results[-1]:
result={k:v for k,v in results[-1].items() if isinstance(v,bool)}
result["job"]=name
return result
time.sleep(5)
raise TimeoutError()
if __name__=="__main__":
try:result=run()
except Exception:result={"success":False,"failure":"native acceptance failed or timed out"}
print(json.dumps(result),flush=True);raise SystemExit(0 if result.get("success") else 1)

View file

@ -0,0 +1,57 @@
"""Guarded activation; credentials remain in memory and subprocess stdin."""
import argparse,base64,copy,json,re,subprocess,time
CLUSTER="a553c742-0115-43d4-99a4-a5ca56fe0786"
DEPLOYMENT="99ddd83c-cb3f-4847-bcf8-35f1aa87627f"
IMAGE="forgejo.coulomb.social/coulomb/key-cape@sha256:c9eb584d60efecfe00e1745a7e8cd3ebb4ae0f94faa715ef9594cd58a5dcebb6"
MOUNT={"name":"factor-token","mountPath":"/etc/keycape-factor","readOnly":True}
VOLUME={"name":"factor-token","secret":{"secretName":"keycape-factor-read","defaultMode":288,"items":[{"key":"admin-token","path":"admin-token"}]}}
def kubectl(*args,payload=None):
r=subprocess.run(["kubectl",*args],input=json.dumps(payload).encode() if payload is not None else None,stdout=subprocess.PIPE,stderr=subprocess.PIPE,timeout=40)
if r.returncode:raise RuntimeError("Kubernetes operation failed")
return json.loads(r.stdout) if r.stdout else {}
def rewrite_config(raw):
text=raw.decode();match=re.search(r"(?m)^privacyidea:[^\n]*\n(?:(?:[ \t]+[^\n]*|)[\n]|[ \t]+[^\n]*$)*",text)
if not match:raise ValueError("privacyidea section missing")
section=match.group()
if re.search(r"(?m)^ adminTokenFile:",section):
if not re.search(r"(?m)^ adminTokenFile: /etc/keycape-factor/admin-token$",section) or re.search(r"(?m)^ adminToken:",section):raise ValueError("unexpected credential configuration")
return raw
section,n=re.subn(r"(?m)^ adminToken:[^\n]*$"," adminTokenFile: /etc/keycape-factor/admin-token",section)
if n!=1:raise ValueError("expected exactly one inline token")
return (text[:match.start()]+section+text[match.end():]).encode()
def deployment_patch(obj):
if obj["metadata"]["uid"]!=DEPLOYMENT:raise ValueError("deployment identity mismatch")
spec=obj["spec"]["template"]["spec"];containers=spec["containers"]
i=next(i for i,c in enumerate(containers) if c["name"]=="keycape")
if containers[i]["image"]!=IMAGE:raise ValueError("deployment image mismatch")
mounts=copy.deepcopy(containers[i].get("volumeMounts",[]));volumes=copy.deepcopy(spec.get("volumes",[]))
for entries,expected in ((mounts,MOUNT),(volumes,VOLUME)):
existing=[x for x in entries if x["name"]==expected["name"]]
if existing and existing!=[expected]:raise ValueError("existing factor mount differs")
if not existing:entries.append(expected)
return [{"op":"test","path":"/metadata/uid","value":DEPLOYMENT},{"op":"test","path":"/metadata/resourceVersion","value":obj["metadata"]["resourceVersion"]},{"op":"add","path":f"/spec/template/spec/containers/{i}/volumeMounts","value":mounts},{"op":"add","path":"/spec/template/spec/volumes","value":volumes}]
def run(apply):
if kubectl("get","ns","kube-system","-o","json")["metadata"]["uid"]!=CLUSTER:raise ValueError("cluster identity mismatch")
es=kubectl("-n","sso","get","externalsecret","keycape-factor-read","-o","json")
if not any(x["type"]=="Ready" and x["status"]=="True" for x in es.get("status",{}).get("conditions",[])):raise ValueError("delivery not ready")
delivered=kubectl("-n","sso","get","secret","keycape-factor-read","-o","json")
expiry=int(base64.b64decode(delivered["data"]["expires-at"]))
if expiry-time.time()<600:raise ValueError("delivered token too near expiry")
secret=kubectl("-n","sso","get","secret","keycape-config","-o","json")
old=secret["data"]["config.yaml"];new=base64.b64encode(rewrite_config(base64.b64decode(old))).decode()
patch=[{"op":"test","path":"/metadata/uid","value":secret["metadata"]["uid"]},{"op":"test","path":"/metadata/resourceVersion","value":secret["metadata"]["resourceVersion"]},{"op":"replace","path":"/data/config.yaml","value":new}]
deployment=kubectl("-n","sso","get","deployment","keycape","-o","json");dp=deployment_patch(deployment)
for kind,name,ops in (("secret","keycape-config",patch),("deployment","keycape",dp)):
kubectl("-n","sso","patch",kind,name,"--type=json","--patch-file=/dev/stdin","--dry-run=server","-o","json",payload=ops)
if apply:
kubectl("-n","sso","patch","secret","keycape-config","--type=json","--patch-file=/dev/stdin","-o","json",payload=patch)
kubectl("-n","sso","patch","deployment","keycape","--type=json","--patch-file=/dev/stdin","-o","json",payload=dp)
current=kubectl("-n","sso","get","secret","keycape-config","-o","json")
expected=dict(secret["data"],**{"config.yaml":new})
if current["data"]!=expected:raise ValueError("config readback mismatch")
return {"success":True,"applied":apply,"unrelated_config_preserved":True,"credential_source":"mounted_file","expires_at":expiry}
if __name__=="__main__":
p=argparse.ArgumentParser();p.add_argument("--apply",action="store_true");a=p.parse_args()
try:result=run(a.apply)
except Exception:result={"success":False,"failure":"activation failed; inspect sanitized resource status"}
print(json.dumps(result));raise SystemExit(0 if result["success"] else 1)

View file

@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""Silent attended custody and dedicated provider bootstrap; no personal passwords."""
import argparse,json,os,secrets,subprocess,time,re
from pathlib import Path
from keycape_factor_metadata import call
ROOT=Path(__file__).resolve().parents[1]
ISSUER="platform/data/workloads/net-kingdom/keycape-factor-issuer"
TOKEN="platform/data/workloads/net-kingdom/keycape-factor-read"
REQUEST="RPF-WP-0040"
USER="keycape-factor-reader"
def read_optional(path):
p=subprocess.run(["bao","read","-format=json",path],capture_output=True,text=True,timeout=25)
if p.returncode:
# Only explicit 404 permits creation. Permission or transport failures never mean absent.
if "Code: 404" in p.stderr or "No value found" in p.stderr:return None
raise RuntimeError("custody read failed")
return json.loads(p.stdout)["data"]
def execute():
cluster=json.loads(subprocess.check_output(["kubectl","get","ns","kube-system","-o","json"]))
if cluster["metadata"]["uid"]!="a553c742-0115-43d4-99a4-a5ca56fe0786":raise RuntimeError("wrong cluster")
issuer=read_optional(ISSUER)
if issuer is None:
secret={"USERNAME":USER,"PASSWORD":secrets.token_urlsafe(48),"REQUEST":REQUEST}
call("write","-format=json",ISSUER,"-",payload={"options":{"cas":0},"data":secret})
issuer=read_optional(ISSUER)
secret=issuer["data"]
if set(secret)!={"USERNAME","PASSWORD","REQUEST"} or secret["USERNAME"]!=USER or secret["REQUEST"]!=REQUEST:raise RuntimeError("issuer custody mismatch")
source=(ROOT/"scripts/keycape_factor_provider.py").read_text()
p=subprocess.run(["kubectl","-n","mfa","exec","-i","deployment/privacyidea","-c","privacyidea","--","python3","-c",source],input=json.dumps({"operation":"bootstrap","username":USER,"password":secret["PASSWORD"]}),text=True,capture_output=True,timeout=60)
if p.returncode:
failure=json.loads(p.stdout)
permitted={"admin policy baseline changed","existing managed policy differs","service name already belongs to another setup","service authentication failed","unsupported service token lifetime","factor lookup failed","service has unexpected administration rights","provider response too large"}
reason=failure.get("failure")
raise RuntimeError(reason if reason in permitted or (isinstance(reason,str) and re.fullmatch(r"factor lookup failed [0-9]{3} (True|False) (True|False) (True|False)",reason)) else "provider setup failed without secret-bearing detail")
result=json.loads(p.stdout)
if not result.get("policy_read_denied") or result["expires_at"]<=time.time()+300:raise RuntimeError("provider verification incomplete")
current=read_optional(TOKEN)
if current and current["data"].get("REQUEST")!=REQUEST:raise RuntimeError("token custody belongs to another request")
cas=current["metadata"]["version"] if current else 0
call("write","-format=json",TOKEN,"-",payload={"options":{"cas":cas},"data":{"TOKEN":result["token"],"EXPIRES_AT":result["expires_at"],"REQUEST":REQUEST}})
stored=read_optional(TOKEN)
if stored["data"]["TOKEN"]!=result["token"]:raise RuntimeError("token custody readback failed")
return {"issuer_path":ISSUER,"token_path":TOKEN,"token_version":stored["metadata"]["version"],"expires_at":result["expires_at"],"provider_policy_read_denied":True,"cross_user_factor_visible":result["cross_user_factor_visible"],"values_emitted":False}
def main():
p=argparse.ArgumentParser();p.add_argument("--receipt",type=Path,required=True);args=p.parse_args()
if any(os.environ.get(k) for k in ["OPENBAO_TOKEN","BAO_TOKEN","VAULT_TOKEN"]):return 2
fd=os.open(args.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600);result={"success":False}
try:result.update(execute());result["success"]=True;return 0
except RuntimeError as e:result["failure"]=str(e);return 1
except Exception as e:result["failure_type"]=type(e).__name__;return 1
finally:os.write(fd,json.dumps(result,indent=2).encode());os.close(fd)
if __name__=="__main__":raise SystemExit(main())

View file

@ -0,0 +1,44 @@
"""Native proof job: only boolean scope and projection observations leave the pod."""
import json,time,sys,urllib.parse
from pathlib import Path
sys.path.insert(0,"/worker")
from renew import http,BAO,PI,TARGET,ISSUER,success
result={"success":False};session=None
try:
jwt=Path("/var/run/keycape-factor/token").read_text().strip()
code,body=http(BAO+"/auth/kubernetes/login",{"role":"keycape-factor-renewer","jwt":jwt})
result["wrong_service_account_denied"]=code in (400,403)
code,body=http(BAO+"/auth/kubernetes/login",{"role":"keycape-factor-workload-kv-read","jwt":jwt})
if code!=200:raise ValueError()
session=body["auth"]["client_token"];headers={"X-Vault-Token":session}
code,_=http(BAO+"/"+ISSUER,headers=headers);result["issuer_password_denied"]=code==403
code,_=http(BAO+"/platform/data/workloads/net-kingdom/unrelated",headers=headers);result["sibling_secret_denied"]=code==403
code,data=http(BAO+"/"+TARGET,headers=headers)
if code!=200:raise ValueError()
initial=Path("/factor/admin-token").read_text().strip()
result["mounted_token_matches_custody"]=initial==data["data"]["data"]["TOKEN"]
listing=success(*http(PI+"/token/?tokenrealm=coulomb&active=True&pagesize=1",headers={"Authorization":initial}))
user=listing["tokens"][0]["username"]
query=urllib.parse.urlencode({"user":user,"realm":"coulomb","active":"True"})
listing=success(*http(PI+"/token/?"+query,headers={"Authorization":initial}))
result["keycape_user_lookup_accepted"]=listing["count"]>0 and any(x.get("active") is True for x in listing["tokens"])
code,_=http(PI+"/policy/",headers={"Authorization":initial});result["provider_administration_denied"]=code in (401,403)
print(json.dumps({"phase":"awaiting_rotation",**result}),flush=True)
for _ in range(48):
time.sleep(5)
current=Path("/factor/admin-token").read_text().strip()
if current!=initial:
listing=success(*http(PI+"/token/?"+query,headers={"Authorization":current}))
result["rotated_projection_accepted"]=listing["count"]>0
break
result["success"]=all(v for k,v in result.items() if k!="success") and result.get("rotated_projection_accepted",False)
except Exception:pass
finally:
if session:
try:
code,_=http(BAO+"/auth/token/revoke-self",headers={"X-Vault-Token":session},method="POST")
result["session_revoked"]=code in (200,204)
except Exception:result["session_revoked"]=False
if not result["session_revoked"]:result["success"]=False
print(json.dumps(result),flush=True)
raise SystemExit(0 if result["success"] else 1)

View file

@ -0,0 +1,16 @@
import json,os,sys
from pathlib import Path
from keycape_factor_metadata import call,normalized,policy_text
NAME="workload-kv-read-keycape-factor-read"
OLD='# Exact-path JWT delivery; no issuer password or sibling paths.\npath "platform/data/workloads/net-kingdom/keycape-factor-read" {\n capabilities = ["read"]\n}\npath "platform/metadata/workloads/net-kingdom/keycape-factor-read" {\n capabilities = ["read"]\n}\n\n# ESO validates its own short-lived workload session.\npath "auth/token/lookup-self" {\n capabilities = ["read"]\n}\n'
NEW=Path(__file__).resolve().parents[1].joinpath("openbao/policies/"+NAME+".hcl").read_text()
fd=os.open(sys.argv[1],os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600)
result={"success":False}
try:
current=policy_text(call("policy","read","-format=json",NAME))
if normalized(current) not in (normalized(OLD),normalized(NEW)):raise RuntimeError()
call("write","-format=json","sys/policies/acl/"+NAME,"-",payload={"policy":NEW})
if normalized(policy_text(call("policy","read","-format=json",NAME)))!=normalized(NEW):raise RuntimeError()
result={"success":True,"change":"ESO self-session lookup and revocation only","secret_values_read":False}
finally:
os.write(fd,json.dumps(result).encode());os.close(fd)

View file

@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""Reviewed silent metadata apply inside the attended OpenBao envelope."""
import argparse,json,os,subprocess
from pathlib import Path
ROOT=Path(__file__).resolve().parents[1]
POLICY="workload-kv-read-keycape-factor-read"
ROLE="keycape-factor-workload-kv-read"
ROLE_PATH="auth/kubernetes/role/"+ROLE
POLICY_PATH="sys/policies/acl/"+POLICY
KV="platform/workloads/net-kingdom/keycape-factor-read"
ROLE_CONFIG={"bound_service_account_names":["keycape-factor-eso"],"bound_service_account_namespaces":["sso"],"audience":"openbao","token_policies":[POLICY],"token_ttl":900,"token_max_ttl":900,"token_no_default_policy":True}
def call(*args,payload=None):
p=subprocess.run(["bao",*args],input=json.dumps(payload) if payload is not None else None,capture_output=True,text=True,timeout=25)
if p.returncode:raise RuntimeError("OpenBao metadata command rejected: "+args[0]+" "+args[1])
return json.loads(p.stdout) if p.stdout.strip() else {}
def normalized(value):return "".join(value.split())
def policy_text(value):return value.get("data",value).get("policy","")
def verify_role(data):
return all(data.get(k)==v for k,v in ROLE_CONFIG.items())
def execute(apply=False):
mounts=call("secrets","list","-format=json")
if mounts.get("platform/",{}).get("type")!="kv" or str(mounts["platform/"].get("options",{}).get("version"))!="2":raise RuntimeError("Expected existing KV v2 mount")
auth=call("auth","list","-format=json")
if auth.get("kubernetes/",{}).get("type")!="kubernetes":raise RuntimeError("Expected existing Kubernetes auth")
# Capability checks are individual: the CLI interprets multiple arguments as TOKEN PATH.
capabilities={p:call("token","capabilities","-format=json",p) for p in [POLICY_PATH,ROLE_PATH]}
if any(not {"create","update"}.issubset(set(c)) and "root" not in c for c in capabilities.values()):raise RuntimeError("Missing metadata administration capabilities")
policy=(ROOT/"openbao/policies"/(POLICY+".hcl")).read_text()
if apply:
# New dedicated names only. Existing objects must match; never overwrite foreign metadata.
policies=call("policy","list","-format=json")
if POLICY in policies:
current=call("policy","read","-format=json",POLICY)
if normalized(current.get("data",current).get("policy",""))!=normalized(policy):raise RuntimeError("Existing policy differs")
else:call("write","-format=json",POLICY_PATH,"-",payload={"policy":policy})
roles=call("list","-format=json","auth/kubernetes/role")
role_names=roles if isinstance(roles,list) else roles.get("data",{}).get("keys",[])
if ROLE in role_names:
if not verify_role(call("read","-format=json",ROLE_PATH).get("data",{})):raise RuntimeError("Existing role differs")
else:call("write","-format=json",ROLE_PATH,"-",payload=ROLE_CONFIG)
if not verify_role(call("read","-format=json",ROLE_PATH).get("data",{})):raise RuntimeError("Role readback differs")
if normalized(policy_text(call("policy","read","-format=json",POLICY)))!=normalized(policy):raise RuntimeError("Policy readback differs")
return {"operation":"apply" if apply else "preflight","policy":POLICY,"role":ROLE_PATH,"kv_path":KV,"field":"TOKEN","metadata_verified":apply,"secret_values_read":False,"secret_values_written":False}
def main():
parser=argparse.ArgumentParser();parser.add_argument("--apply",action="store_true");parser.add_argument("--receipt",type=Path,required=True);args=parser.parse_args()
if any(os.environ.get(k) for k in ["OPENBAO_TOKEN","BAO_TOKEN","VAULT_TOKEN"]):return 2
fd=os.open(args.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600)
result={"success":False,"phase":"metadata"}
try:
result.update(execute(args.apply));result["success"]=True;return 0
except RuntimeError as exc:
result["failure"]=str(exc);return 1
except Exception as exc:
result["failure_type"]=type(exc).__name__;return 1
finally:
os.write(fd,json.dumps(result,indent=2).encode());os.close(fd)
if __name__=="__main__":raise SystemExit(main())

View file

@ -0,0 +1,72 @@
"""Run only inside the provider; secret-bearing stdin/stdout stay in owner pipes."""
import contextlib,io,json,logging,sys,time,base64,urllib.request,urllib.error,urllib.parse
USER="keycape-factor-reader"
BASELINE="keycape-preserve-existing-admins"
READER="keycape-factor-reader-coulomb"
REALM="coulomb"
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self,*args,**kwargs):return None
def request(path,payload=None,token=None):
headers={}
if token:headers["Authorization"]=token
data=urllib.parse.urlencode(payload).encode() if payload is not None else None
req=urllib.request.Request("http://127.0.0.1:8080"+path,data=data,headers=headers)
try:
with urllib.request.build_opener(NoRedirect()).open(req,timeout=15) as r:
raw=r.read(1048577)
if len(raw)>1048576:raise RuntimeError("provider response too large")
return r.status,json.loads(raw)
except urllib.error.HTTPError as e:return e.code,{}
def token_result(password):
code,auth=request("/auth",{"username":USER,"password":password})
value=auth.get("result",{}).get("value",{})
if code!=200 or not auth.get("result",{}).get("status") or value.get("role")!="admin" or value.get("username")!=USER:raise RuntimeError("service authentication failed")
token=value.get("token","")
claims=json.loads(base64.urlsafe_b64decode(token.split('.')[1]+'==='))
expiry=int(claims['exp'])
if not 300<expiry-time.time()<=7200:raise RuntimeError("unsupported service token lifetime")
code,listing=request("/token/?tokenrealm=coulomb&active=True&page=1&pagesize=1",token=token)
lv=listing.get("result",{}).get("value",{})
if code!=200 or not listing.get("result",{}).get("status") or not isinstance(lv.get("tokens"),list) or not isinstance(lv.get("count"),int):raise RuntimeError("factor lookup failed "+str(code)+" "+str(bool(listing.get("result",{}).get("status")))+" "+str(isinstance(lv.get("tokens"),list))+" "+str(isinstance(lv.get("count"),int)))
code,_=request("/policy/",token=token)
if code not in (401,403):raise RuntimeError("service has unexpected administration rights")
return {"token":token,"expires_at":expiry,"cross_user_factor_visible":lv["count"]>0,"policy_read_denied":True}
def bootstrap(password):
logging.disable(logging.CRITICAL)
from privacyidea.app import create_app
from privacyidea.lib.policy import PolicyClass,set_policy
from privacyidea.lib.auth import get_db_admins,create_db_admin
app=create_app(config_name="production")
with app.app_context():
policies=PolicyClass().list_policies(scope="admin",active=True)
# Source preflight found no policies. Retry accepts only our exact two policies.
if any(p['name'] not in [BASELINE,READER] for p in policies):raise RuntimeError("admin policy baseline changed")
for p in policies:
expected=({"adminuser":["*","!"+USER],"realm":[],"action":{"*":True}} if p['name']==BASELINE else {"adminuser":[USER],"realm":[REALM],"action":{"tokenlist":True}})
if any(p.get(k)!=v for k,v in expected.items()):raise RuntimeError("existing managed policy differs")
existing={a.username for a in get_db_admins()}
if USER in existing and not policies:raise RuntimeError("service name already belongs to another setup")
if BASELINE not in {p['name'] for p in policies}:
set_policy(name=BASELINE,scope="admin",action="*",adminuser=["*","!"+USER],description="Preserve default rights for existing administrative identities; exclude factor reader")
if READER not in {p['name'] for p in policies}:
set_policy(name=READER,scope="admin",action="tokenlist",adminuser=USER,realm=REALM,description="KeyCape factor listing in coulomb only")
if USER not in existing:create_db_admin(USER,password=password)
return token_result(password)
def main():
data=json.load(sys.stdin)
if data.get('username')!=USER or not isinstance(data.get('password'),str) or len(data['password'])<32:return 2
with contextlib.redirect_stdout(io.StringIO()),contextlib.redirect_stderr(io.StringIO()):
result=bootstrap(data['password']) if data.get('operation')=='bootstrap' else token_result(data['password'])
sys.stdout.write(json.dumps(result));return 0
if __name__=='__main__':
try:code=main()
except RuntimeError as exc:
sys.stdout.write(json.dumps({"failure":str(exc)}));code=1
except Exception as exc:
sys.stdout.write(json.dumps({"failure_type":type(exc).__name__}));code=1
raise SystemExit(code)

View file

@ -0,0 +1,74 @@
"""Dedicated Kubernetes renewal worker. Only sanitized outcome metadata is emitted."""
import base64,json,sys,time,urllib.request,urllib.error,urllib.parse
from pathlib import Path
BAO="http://openbao.openbao.svc.cluster.local:8200/v1"
PI="http://privacyidea.mfa.svc.cluster.local:8080"
ISSUER="platform/data/workloads/net-kingdom/keycape-factor-issuer"
TARGET="platform/data/workloads/net-kingdom/keycape-factor-read"
USER="keycape-factor-reader"
PROVENANCE="RPF-WP-0040"
class Failure(Exception):pass
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self,*args,**kwargs):return None
def http(url,body=None,headers=None,method=None,form=False):
data=None
headers=dict(headers or {})
if body is not None:
data=(urllib.parse.urlencode(body) if form else json.dumps(body)).encode()
headers["Content-Type"]="application/x-www-form-urlencoded" if form else "application/json"
req=urllib.request.Request(url,data=data,headers=headers,method=method)
try:
with urllib.request.build_opener(NoRedirect()).open(req,timeout=15) as r:
raw=r.read(1048577)
if len(raw)>1048576:raise Failure()
return r.status,json.loads(raw) if raw else {}
except urllib.error.HTTPError as e:return e.code,{}
def success(code,data):
if code!=200 or not data.get("result",{}).get("status"):raise Failure()
return data["result"]["value"]
def run():
session=None;receipt={"success":False,"phase":"workload_login"}
try:
jwt=Path("/var/run/keycape-factor/token").read_text().strip()
code,body=http(BAO+"/auth/kubernetes/login",{"role":"keycape-factor-renewer","jwt":jwt})
if code!=200:raise Failure()
session=body["auth"]["client_token"];headers={"X-Vault-Token":session}
receipt["phase"]="issuer_custody"
code,body=http(BAO+"/"+ISSUER,headers=headers)
if code!=200:raise Failure()
issuer=body["data"]["data"]
if issuer.get("REQUEST")!=PROVENANCE or issuer.get("USERNAME")!=USER:raise Failure()
receipt["phase"]="provider_authentication"
value=success(*http(PI+"/auth",{"username":USER,"password":issuer["PASSWORD"]},form=True))
if value.get("role")!="admin" or value.get("username")!=USER:raise Failure()
token=value["token"];claims=json.loads(base64.urlsafe_b64decode(token.split('.')[1]+'==='));expiry=int(claims["exp"])
if not 300<expiry-time.time()<=7200:raise Failure()
receipt["phase"]="provider_scope"
provider_headers={"Authorization":token}
listing=success(*http(PI+"/token/?tokenrealm=coulomb&active=True&pagesize=1",headers=provider_headers))
if not isinstance(listing.get("tokens"),list) or not isinstance(listing.get("count"),int):raise Failure()
code,_=http(PI+"/policy/",headers=provider_headers)
if code not in (401,403):raise Failure()
receipt["phase"]="publish"
code,body=http(BAO+"/"+TARGET,headers=headers)
if code!=200 or body["data"]["data"].get("REQUEST")!=PROVENANCE:raise Failure()
version=body["data"]["metadata"]["version"]
code,_=http(BAO+"/"+TARGET,{"options":{"cas":version},"data":{"TOKEN":token,"EXPIRES_AT":expiry,"REQUEST":PROVENANCE}},headers=headers)
if code!=200:raise Failure()
code,body=http(BAO+"/"+TARGET,headers=headers)
if code!=200 or body["data"]["data"]["TOKEN"]!=token:raise Failure()
receipt.update(success=True,phase="complete",expires_at=expiry,kv_version=body["data"]["metadata"]["version"],cross_user_factor_visible=listing["count"]>0)
except Exception:pass
finally:
if session:
try:
code,_=http(BAO+"/auth/token/revoke-self",headers={"X-Vault-Token":session},method="POST")
receipt["session_revoked"]=code in (200,204)
except Exception:receipt["session_revoked"]=False
if not receipt["session_revoked"]:receipt["success"]=False;receipt["phase"]="session_cleanup"
return receipt
if __name__=="__main__":
receipt=run();print(json.dumps(receipt));raise SystemExit(0 if receipt["success"] else 1)

View file

@ -0,0 +1,34 @@
#!/usr/bin/env python3
"""Exact reviewed renewal metadata; owner-admin operation, not generic KV-read delegation."""
import argparse,json,os
from pathlib import Path
from keycape_factor_metadata import call,normalized,policy_text
ROOT=Path(__file__).resolve().parents[1]
NAME="keycape-factor-renewer"
ROLE={"bound_service_account_names":[NAME],"bound_service_account_namespaces":["sso"],"audience":"openbao","token_policies":[NAME],"token_ttl":300,"token_max_ttl":300,"token_no_default_policy":True}
def execute():
policy=(ROOT/"openbao/policies"/(NAME+".hcl")).read_text()
existing=call("policy","list","-format=json")
if NAME in existing:
if normalized(policy_text(call("policy","read","-format=json",NAME)))!=normalized(policy):raise RuntimeError("existing renewal policy differs")
else:call("write","-format=json","sys/policies/acl/"+NAME,"-",payload={"policy":policy})
roles=call("list","-format=json","auth/kubernetes/role")
names=roles if isinstance(roles,list) else roles.get("data",{}).get("keys",[])
path="auth/kubernetes/role/"+NAME
if NAME in names:
data=call("read","-format=json",path)["data"]
if any(data.get(k)!=v for k,v in ROLE.items()):raise RuntimeError("existing renewal role differs")
else:call("write","-format=json",path,"-",payload=ROLE)
data=call("read","-format=json",path)["data"]
if any(data.get(k)!=v for k,v in ROLE.items()):raise RuntimeError("renewal role readback failed")
if normalized(policy_text(call("policy","read","-format=json",NAME)))!=normalized(policy):raise RuntimeError("renewal policy readback failed")
return {"success":True,"policy":NAME,"role":path,"token_max_ttl":300,"secret_values_read":False,"secret_values_written":False}
def main():
p=argparse.ArgumentParser();p.add_argument("--receipt",type=Path,required=True);a=p.parse_args()
if any(os.environ.get(k) for k in ["VAULT_TOKEN","BAO_TOKEN","OPENBAO_TOKEN"]):return 2
fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600);result={"success":False}
try:result=execute();return 0
except RuntimeError as e:result["failure"]=str(e);return 1
except Exception as e:result["failure_type"]=type(e).__name__;return 1
finally:os.write(fd,json.dumps(result,indent=2).encode());os.close(fd)
if __name__=="__main__":raise SystemExit(main())

View file

@ -0,0 +1,15 @@
import importlib.util,unittest
from pathlib import Path
spec=importlib.util.spec_from_file_location("activation",Path(__file__).resolve().parents[1]/"scripts/keycape_factor_activate.py")
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
class ActivationTests(unittest.TestCase):
def test_preserves_other_bytes_and_is_idempotent(self):
old=b"other: secret\nprivacyidea:\n baseURL: http://provider\n adminToken: expired\n realm: coulomb\n requireForAll: true\nclients:\n - secret: unchanged\n"
new=m.rewrite_config(old)
self.assertEqual(old.replace(b"adminToken: expired",b"adminTokenFile: /etc/keycape-factor/admin-token"),new)
self.assertEqual(new,m.rewrite_config(new))
def test_rejects_ambiguous_source(self):
for text in (b"other: value\n",b"privacyidea:\n adminToken: one\n adminToken: two\n",b"privacyidea:\n adminTokenFile: /unexpected\n"):
with self.assertRaises(ValueError):m.rewrite_config(text)
def test_refuses_unexpected_deployment(self):
with self.assertRaises(ValueError):m.deployment_patch({"metadata":{"uid":"replacement"}})

View file

@ -0,0 +1,31 @@
import importlib.util,unittest
from pathlib import Path
from unittest.mock import patch
spec=importlib.util.spec_from_file_location("factor_metadata",Path(__file__).resolve().parents[1]/"scripts/keycape_factor_metadata.py")
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
class FactorMetadataTests(unittest.TestCase):
def test_exact_role_and_policy_scope(self):
self.assertEqual(["keycape-factor-eso"],m.ROLE_CONFIG["bound_service_account_names"])
self.assertEqual(["sso"],m.ROLE_CONFIG["bound_service_account_namespaces"])
self.assertTrue(m.ROLE_CONFIG["token_no_default_policy"])
self.assertEqual(900,m.ROLE_CONFIG["token_max_ttl"])
policy=(m.ROOT/"openbao/policies"/(m.POLICY+".hcl")).read_text()
self.assertNotIn("*",policy);self.assertNotIn("issuer",policy.splitlines()[1:])
self.assertEqual(3,policy.count('capabilities = ["read"]'))
def test_denied_capabilities_prevent_writes(self):
calls=[]
def fake(*args,**kwargs):
calls.append(args)
if args[:2]==("secrets","list"):return {"platform/":{"type":"kv","options":{"version":"2"}}}
if args[:2]==("auth","list"):return {"kubernetes/":{"type":"kubernetes"}}
return ["deny"]
with patch.object(m,"call",side_effect=fake):
with self.assertRaises(RuntimeError):m.execute(True)
self.assertFalse(any(c[0]=="write" for c in calls))
def test_mismatched_role_fails_readback(self):
self.assertTrue(m.verify_role(dict(m.ROLE_CONFIG)))
self.assertFalse(m.verify_role(dict(m.ROLE_CONFIG,token_policies=["root"])))
def test_policy_readback_accepts_cli_and_api_shapes(self):
self.assertEqual("expected",m.policy_text({"policy":"expected"}))
self.assertEqual("expected",m.policy_text({"data":{"policy":"expected"}}))

View file

@ -0,0 +1,21 @@
import base64,importlib.util,json,time,unittest
from pathlib import Path
from unittest.mock import patch
spec=importlib.util.spec_from_file_location("factor_provider",Path(__file__).resolve().parents[1]/"scripts/keycape_factor_provider.py")
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
class FactorProviderTests(unittest.TestCase):
def check(self,role="admin",auth_status=True,expiry=3600,policy_status=403,listing_status=True):
token="synthetic."+base64.urlsafe_b64encode(json.dumps({"exp":int(time.time())+expiry}).encode()).decode().rstrip("=")+".signature"
def request(path,payload=None,token=None):
if path=="/auth":return 200,{"result":{"status":auth_status,"value":{"role":role,"username":m.USER,"token":self.token}}}
if path=="/policy/":return policy_status,{}
return 200,{"result":{"status":listing_status,"value":{"tokens":[{}],"count":1}}}
self.token=token
with patch.object(m,"request",side_effect=request):return m.token_result("synthetic-password-not-a-real-secret")
def test_success_requires_scoped_admin_and_expiry(self):
r=self.check();self.assertTrue(r["policy_read_denied"]);self.assertTrue(r["cross_user_factor_visible"])
def test_rejects_user_token_expiry_and_privilege_leak(self):
for kwargs in [dict(role="user"),dict(auth_status=False),dict(expiry=-1),dict(expiry=9000),dict(policy_status=200),dict(policy_status=500),dict(listing_status=False)]:
with self.subTest(kwargs=kwargs):
with self.assertRaises(RuntimeError):self.check(**kwargs)
def test_no_redirect_forwarding(self):self.assertIsNone(m.NoRedirect().redirect_request(None,None,None,None,None,None))

View file

@ -0,0 +1,33 @@
import base64,importlib.util,json,time,unittest
from pathlib import Path
from unittest.mock import patch
spec=importlib.util.spec_from_file_location("factor_renew",Path(__file__).resolve().parents[1]/"scripts/keycape_factor_renew.py")
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
class FactorRenewalTests(unittest.TestCase):
def drive(self,provider_failure=False,publish_failure=False,revoke_failure=False):
self.published=False;token="synthetic."+base64.urlsafe_b64encode(json.dumps({"exp":int(time.time())+3600}).encode()).decode()+".sig"
def http(url,body=None,headers=None,method=None,form=False):
if url.endswith('/auth/kubernetes/login'):return 200,{"auth":{"client_token":"synthetic-bao-session"}}
if url.endswith('/auth/token/revoke-self'):return (403 if revoke_failure else 204),{}
if url.endswith(m.ISSUER):return 200,{"data":{"data":{"REQUEST":m.PROVENANCE,"USERNAME":m.USER,"PASSWORD":"synthetic-service-password"}}}
if url.endswith('/auth'):
return (401,{}) if provider_failure else (200,{"result":{"status":True,"value":{"role":"admin","username":m.USER,"token":token}}})
if '/token/?' in url:return 200,{"result":{"status":True,"value":{"tokens":[{}],"count":1}}}
if url.endswith('/policy/'):return 403,{}
if url.endswith(m.TARGET):
if body:
if publish_failure:return 400,{}
self.assertEqual(1,body['options']['cas']);self.assertNotIn('PASSWORD',body['data']);self.published=True;return 200,{}
return 200,{"data":{"data":{"REQUEST":m.PROVENANCE,"TOKEN":token if self.published else "synthetic-old"},"metadata":{"version":2 if self.published else 1}}}
raise AssertionError('Unexpected endpoint')
with patch.object(m,'http',side_effect=http),patch.object(Path,'read_text',return_value='synthetic-kubernetes-jwt'):return m.run()
def test_renewal_publishes_verified_token_and_revokes_session(self):
r=self.drive();self.assertTrue(r['success']);self.assertEqual(2,r['kv_version']);self.assertTrue(r['session_revoked'])
self.assertNotIn('synthetic',json.dumps(r))
def test_rejected_provider_retains_current_token(self):
r=self.drive(provider_failure=True);self.assertFalse(r['success']);self.assertFalse(self.published);self.assertTrue(r['session_revoked'])
self.assertTrue(self.drive()['success'])
def test_cas_failure_is_not_success(self):
r=self.drive(publish_failure=True);self.assertFalse(r['success']);self.assertFalse(self.published);self.assertEqual('publish',r['phase'])
def test_cleanup_failure_is_reported(self):
r=self.drive(revoke_failure=True);self.assertFalse(r['success']);self.assertEqual('session_cleanup',r['phase'])

View file

@ -0,0 +1,64 @@
---
id: RPF-WP-0040
type: workplan
title: "Establish KeyCape factor credential custody and renewal"
domain: financials
repo: railiance-platform
status: active
owner: codex
topic_slug: financials
created: "2026-09-13"
updated: "2026-09-13"
---
User authorized establishing custody/delivery on 2026-09-13 and offered attended administrative authentication. Supports USER-WP-0030-T03 and KEY-WP-0035-T02. Existing incident lanes are not silently repurposed.
## Establish exact-path custody and workload delivery metadata
```task
id: RPF-WP-0040-T01
status: done
priority: high
```
New KV v2 path platform/workloads/net-kingdom/keycape-factor-read, field TOKEN, provider expiry metadata. Reader role binds only sso/keycape-factor-eso, audience openbao, exact-path read policy and fifteen-minute OpenBao tokens. Attended admin metadata apply, exact readback and capability checks. No wildcard secret access or secret value in output.
## Provision a bounded provider identity and renewable credential
```task
id: RPF-WP-0040-T02
status: done
priority: high
```
Create dedicated privacyIDEA keycape-factor-reader with coulomb tokenlist permission. Preserve existing admin rights; baseline has no admin policies. Store renewable issuer credential separately from the delivered JWT. Use protected stdin and in-memory handling, provider-issued JWT with expiry, positive cross-user scope and negative admin policy-read checks. No personal admin password in runtime.
## Activate and verify renewal and consumer delivery
```task
id: RPF-WP-0040-T03
status: done
priority: high
```
Deliver via namespace-scoped ESO, activate KeyCape adminTokenFile with byte-preserving config CAS, schedule renewal before expiry and verify mounted reload and native scope. Predecessor expiry/revocation and recovery drills remain T04. Verify effective scope and real factor acceptance before optional client policy. Preserve existing incident residuals in NK-WP-0033.
Live implementation and evidence: docs/evidence/2026-09-13-keycape-factor-custody.md.
Fourteen offline tests pass; native renewal, exact-path ESO delivery, negative
scope checks, provider per-user lookup and mounted rotation passed. KeyCape
rolled out Ready 1/1 without changing MFA policy or signing/client secrets.
## Verify provider expiry and revocation recovery
```task
id: RPF-WP-0040-T04
status: todo
priority: high
```
Exercise genuine JWT expiry, provider-side permission withdrawal and recovery
with an isolated fixture, preserving real users' factors and admin access.
Verify rejected predecessor handling independently of OpenBao token revocation.
Include provider interruption/retry evidence and maintain visible failed-Job /
ExternalSecret failure signals. This is remaining acceptance, not another owner
handoff. Historical resolver incident NK-WP-0033 remains separately open.