CCR-2026-0055 (proposed) and request type attended-service-credential-delivery
All checks were successful
CCR intake gate / gate (push) Successful in 9s
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

A value held in platform custody that no workload reads, delivered into a
service by an attended, MFA-gated procedure (privacyIDEA resolver bind for
NetKingdom NK-WP-0056-T04). The contract forbids any policy, auth role, ESO
object or runtime frontdoor. Modelled on attended-host-key-delivery. 4 tests.
The 3 failures in test_credential_change.py also occur on main.

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

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 896183@bnt-lap001
Assistant-Session: 3a18c01a-3205-4d02-94db-9efff25649a2
This commit is contained in:
codex 2026-10-31 11:03:44 +01:00
parent 674a7132d0
commit 2d4a3838bf
5 changed files with 235 additions and 0 deletions

View file

@ -0,0 +1,87 @@
id: CCR-2026-0055
kind: credential-change-request
schema_version: 1
request_type: attended-service-credential-delivery
title: NetKingdom privacyIDEA resolver directory bind (svc-privacyidea) custody
status: proposed
created: '2026-10-31'
updated: '2026-10-31'
requester:
agent: claude-code
reason: 'NK-WP-0056-T04, fourth lane (after CCR-2026-0052/0053/0054): move the svc-privacyidea
LLDAP bind out of the founder''s personal SOPS custody into platform custody.
privacyIDEA stores the resolver password encrypted in its own database and accepts
it only through pi-admin with a fresh MFA code, so no workload reads it: delivery
is an attended write-through.'
review:
required: true
required_approvers:
- platform-owner
- net-kingdom-owner
comments: []
target:
domain: infotech
tenant: platform
workload: net-kingdom-privacyidea
environment: production
purpose: Hold the svc-privacyidea LLDAP bind password in platform custody so that
its rotation needs no decrypted custody copy; privacyIDEA receives it only through
the attended resolver write. No other privacyIDEA, LLDAP or service credential.
openbao:
mount: platform
kv_path: platform/workloads/net-kingdom/directory-binds/svc-privacyidea
fields:
- LDAP_BIND_PASSWORD
auth:
method: attended-oidc
mount: netkingdom
role: platform-admin
runtime_access: false
initial_cas: 0
access_frontdoor:
type: attended-service-config
catalog_id: openbao-platform-admin-login
readiness: pending-review
resolvable: false
lifecycle:
deactivate: Delete the KV entry only after the founder custody copy is confirmed
current; privacyIDEA keeps its stored resolver password.
rotate: Change the svc-privacyidea password in LLDAP, write the next KV version
with CAS on the current version, then run realm_bootstrap.py --rotate-bind --from-openbao
(pi-admin + MFA) and verify with an OTP login. Founder-run.
compromised: Reset svc-privacyidea in LLDAP first (read-only bind, lldap_strict_readonly),
then rotate as above.
delivery:
surface: attended-service-config
service: privacyIDEA (sso), LDAP resolver netkingdom-lldap
target: resolver BINDPW, stored encrypted in the privacyIDEA database
procedure: net-kingdom sso-mfa/k8s/privacyidea/realm_bootstrap.py --rotate-bind
--from-openbao
gate: attended platform-admin (aal2) OpenBao session and pi-admin with a fresh MFA
code
risk:
classification: medium
notes:
- svc-privacyidea is a read-only directory bind (lldap_strict_readonly); a leak
exposes directory attributes, not write access.
- No workload, Kubernetes or ESO reader exists; only the attended platform-admin
session reads the value, in memory.
- Initial KV write is CAS=0 from custody after the value is proven current by an
LLDAP sign-in as svc-privacyidea; no rotation happens in this request.
- Coding agents are fenced from the data and metadata paths in agent-high-risk-boundary
before the first write.
verification:
positive:
- The KV value signs in to LLDAP as svc-privacyidea (current password), compared
without display
- An OTP login resolves the user through the resolver after any write-through
negative:
- Coding-agent boundary denies data and metadata of the path
- No auth role or policy grants the path to a workload
activation_conditions:
- Explicit platform-owner and net-kingdom-owner approval of this exact request (sha256-bound
by the setup tool).
- Attended platform-admin (aal2) session; the founder runs setup with the decrypted
custody copy and the write-through with pi-admin MFA.
- Positive and negative results recorded in value-free receipts.
evidence: []

View file

@ -0,0 +1,21 @@
# Attended service credential delivery
Request type `attended-service-credential-delivery` (`scripts/attended_service_delivery.py`), first used by
CCR-2026-0055 (NetKingdom `svc-privacyidea`, NK-WP-0056-T04).
Some services keep a credential in their own encrypted store and accept it only through an administrator API
that requires a person's MFA. privacyIDEA's LDAP resolver is one: its `BINDPW` lives encrypted in the
privacyIDEA database and only `pi-admin` with a fresh code may change it. ESO cannot deliver such a value, and an
unattended job would need an administrator credential without MFA.
The contract therefore holds the value in OpenBao and nothing else:
- `openbao.auth` is the attended `platform-admin` OIDC session on `netkingdom`; `runtime_access: false`;
`initial_cas: 0`. No policy, auth role, ESO object or other access may be declared.
- The runtime frontdoor stays disabled (`access_frontdoor.resolvable: false`).
- `delivery` names the service, the target inside it, the owner's procedure and the gate (the person's MFA).
- Approval follows `review.required_approvers`; from `applied` onward every listed role must have approved.
The owner's procedure reads the value in memory under the attended session and writes it into the service; the
person's MFA remains the gate. A rotation needs no decrypted custody copy. Modelled on
`attended-host-key-delivery` (CCR-2026-0028), which delivers to a host file instead of a service.

View file

@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""attended-service-credential-delivery CCRs: a value in platform custody that no workload reads.
NetKingdom NK-WP-0056-T04 (svc-privacyidea). Some services keep a credential in
their own encrypted store and accept it only through an administrator API that
requires a person's MFA (privacyIDEA's LDAP resolver bind). The value is held in
OpenBao, written once with CAS=0 from the attended platform-admin session, and
delivered by an attended procedure that reads it in memory and writes it into
the service. No Kubernetes or workload auth role, policy or ESO object exists,
and the runtime frontdoor stays disabled. Modelled on attended-host-key-delivery
(CCR-2026-0028). Nothing here reads, prints or accepts a secret value.
"""
from __future__ import annotations
import re
from typing import Any
REQUEST_TYPE = "attended-service-credential-delivery"
APPROVED = {"applied", "verified", "active"} # approve sets "approved" after the first reviewer
KV_RE = re.compile(r"^platform/workloads/[a-z0-9][a-z0-9-]*(/[a-z0-9][a-z0-9-]*)+$")
FIELD_RE = re.compile(r"^[A-Z][A-Z0-9_]*$")
AUTH = {"method": "attended-oidc", "mount": "netkingdom", "role": "platform-admin"}
DELIVERY_KEYS = ("surface", "service", "target", "procedure", "gate")
DOC = "docs/credential-lane-designs/attended-service-credential-delivery.md"
def validate_contract(ccr: dict[str, Any]) -> list[str]:
errors: list[str] = []
if ccr.get("request_type") != REQUEST_TYPE:
errors.append("unsupported attended service contract")
openbao = ccr.get("openbao")
if not isinstance(openbao, dict):
return errors + ["openbao must be an object"]
if openbao.get("mount") != "platform" or not KV_RE.match(str(openbao.get("kv_path", ""))):
errors.append("openbao.kv_path must be a platform/workloads/... path on mount platform")
fields = openbao.get("fields")
if not isinstance(fields, list) or not fields or not all(isinstance(f, str) and FIELD_RE.match(f) for f in fields):
errors.append("openbao.fields must list upper-case field names")
if openbao.get("auth") != AUTH:
errors.append("openbao.auth must be the attended platform-admin OIDC session only")
if openbao.get("runtime_access") is not False or openbao.get("initial_cas") != 0:
errors.append("runtime_access must be false and initial_cas 0")
extra = set(openbao) - {"mount", "kv_path", "fields", "auth", "runtime_access", "initial_cas"}
if extra:
errors.append("openbao must not declare policies, roles or other access: " + ", ".join(sorted(extra)))
if (ccr.get("access_frontdoor") or {}).get("resolvable") is not False:
errors.append("runtime credential frontdoor must remain disabled")
delivery = ccr.get("delivery")
if not isinstance(delivery, dict) or any(not isinstance(delivery.get(k), str) or not delivery[k] for k in DELIVERY_KEYS):
errors.append("delivery needs surface, service, target, procedure and gate")
elif delivery["surface"] != "attended-service-config":
errors.append("delivery.surface must be attended-service-config")
if ccr.get("status") in APPROVED:
comments = (ccr.get("review") or {}).get("comments") or []
approved = {str(c.get("reviewer", "")).removeprefix("User (").removesuffix(")")
for c in comments if c.get("decision") == "approved"}
for role in (ccr.get("review") or {}).get("required_approvers") or []:
if role not in approved:
errors.append("explicit role approval missing: " + role)
for section, keys in {"verification": ["positive", "negative", "activation_conditions"],
"lifecycle": ["deactivate", "rotate", "compromised"]}.items():
for key in keys:
if not (ccr.get(section) or {}).get(key):
errors.append(f"{section}.{key} required")
return errors
def procedure_note(ccr: dict[str, Any]) -> str:
return (f"Attended delivery into {ccr['delivery']['service']} through {ccr['delivery']['procedure']} "
f"({DOC}). No workload policy, auth role or ESO object is created.")

View file

@ -518,6 +518,8 @@ def validate_ccr(path: Path) -> tuple[dict[str, Any], list[str], list[str]]:
owner = importlib.util.module_from_spec(spec)
spec.loader.exec_module(owner)
errors.extend(owner.validate_contract(ccr))
elif request_type == "attended-service-credential-delivery":
errors.extend(attended_service_module().validate_contract(ccr))
elif request_type in LANE_MODULES:
errors.extend(lane_module(ccr).validate(ccr, REPO_DIR))
else:
@ -724,6 +726,8 @@ def auth_payload(ccr: dict[str, Any]) -> dict[str, Any]:
def render_plan(ccr: dict[str, Any]) -> str:
if ccr.get("request_type") == "attended-host-key-delivery":
return "Use docs/credential-lane-designs/railiance-clock-host-delivery.md through the attended owner procedure. No workload policy or auth role is created."
if ccr.get("request_type") == "attended-service-credential-delivery":
return attended_service_module().procedure_note(ccr)
openbao = ccr["openbao"]
auth = openbao["auth"]
payload = auth_payload(ccr)
@ -755,6 +759,8 @@ def render_plan(ccr: dict[str, Any]) -> str:
def render_operator_commands(ccr: dict[str, Any]) -> str:
if ccr.get("request_type") == "attended-host-key-delivery":
return "Use docs/credential-lane-designs/railiance-clock-host-delivery.md through the attended owner procedure. No workload policy or auth role is created."
if ccr.get("request_type") == "attended-service-credential-delivery":
return attended_service_module().procedure_note(ccr)
openbao = ccr["openbao"]
auth = openbao["auth"]
auth_path = f"auth/{auth['mount']}/role/{auth['role']}"
@ -835,6 +841,15 @@ LANE_MODULES = {
}
def attended_service_module():
"""Owner contract for attended-service-credential-delivery (no workload access)."""
import importlib.util
spec = importlib.util.spec_from_file_location("attended_service_delivery", REPO_DIR / "scripts/attended_service_delivery.py")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def lane_module(ccr: dict[str, Any]):
"""Lazy-load the applier module for a lane request type, or None."""
name = LANE_MODULES.get(ccr.get("request_type"))
@ -1115,6 +1130,8 @@ def render_applier_apply_plan(ccr: dict[str, Any], warnings: list[str]) -> str:
def runbook_readiness_blockers(ccr: dict[str, Any]) -> list[str]:
if ccr.get("request_type") == "attended-host-key-delivery":
return ["use the attended owner procedure in docs/credential-lane-designs/railiance-clock-host-delivery.md"]
if ccr.get("request_type") == "attended-service-credential-delivery":
return ["use the attended procedure: " + attended_service_module().procedure_note(ccr)]
blockers: list[str] = []
status = ccr.get("status")
if status not in RUNBOOK_ALLOWED_STATUSES:

View file

@ -0,0 +1,40 @@
"""attended-service-credential-delivery contract (NetKingdom NK-WP-0056-T04); no values."""
import copy
import importlib.util
from pathlib import Path
import yaml
ROOT = Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location("asd", ROOT / "scripts/attended_service_delivery.py")
m = importlib.util.module_from_spec(spec)
spec.loader.exec_module(m)
CCR = yaml.safe_load((ROOT / "credential-change-requests/CCR-2026-0055-net-kingdom-privacyidea-bind.yaml").read_text())
def test_checked_in_request_is_valid():
assert m.validate_contract(CCR) == []
def test_any_workload_access_is_refused():
for change in ({"policy_name": "x"}, {"runtime_access": True}, {"initial_cas": 1},
{"auth": {"method": "kubernetes", "mount": "kubernetes", "role": "x"}}):
ccr = copy.deepcopy(CCR); ccr["openbao"].update(change)
assert m.validate_contract(ccr), change
ccr = copy.deepcopy(CCR); ccr["access_frontdoor"]["resolvable"] = True
assert "runtime credential frontdoor must remain disabled" in m.validate_contract(ccr)
def test_delivery_and_lifecycle_are_required():
ccr = copy.deepcopy(CCR); del ccr["delivery"]["gate"]
assert "delivery needs surface, service, target, procedure and gate" in m.validate_contract(ccr)
ccr = copy.deepcopy(CCR); ccr["lifecycle"]["rotate"] = ""
assert "lifecycle.rotate required" in m.validate_contract(ccr)
def test_applied_stage_needs_every_required_approval():
ccr = copy.deepcopy(CCR); ccr["status"] = "verified"
ccr["review"]["comments"] = [{"reviewer": "User (platform-owner)", "decision": "approved"}]
assert m.validate_contract(ccr) == ["explicit role approval missing: net-kingdom-owner"]
ccr["review"]["comments"].append({"reviewer": "net-kingdom-owner", "decision": "approved"})
assert m.validate_contract(ccr) == []