From e70ef2f32aae3289f4766f0477bacbc455d1b2e8 Mon Sep 17 00:00:00 2001 From: codex Date: Tue, 15 Sep 2026 23:00:20 +0200 Subject: [PATCH] Implement attended Railiance Clock host key delivery Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a09cbb-87c6-7900-a145-4ce53ba9f1a6 --- ...028-railiance-clock-authority-signing.yaml | 99 +++++++------ .../railiance-clock-host-delivery.md | 36 +++++ ...read-railiance-clock-authority-signing.hcl | 3 - schemas/credential-change-request.schema.yaml | 5 + scripts/credential-change.py | 14 +- scripts/railiance_clock_custody.py | 132 ++++++++++++++++++ tests/test_railiance_clock_custody.py | 44 ++++++ 7 files changed, 284 insertions(+), 49 deletions(-) create mode 100644 docs/credential-lane-designs/railiance-clock-host-delivery.md delete mode 100644 openbao/policies/workload-kv-read-railiance-clock-authority-signing.hcl create mode 100644 scripts/railiance_clock_custody.py create mode 100644 tests/test_railiance_clock_custody.py diff --git a/credential-change-requests/CCR-2026-0028-railiance-clock-authority-signing.yaml b/credential-change-requests/CCR-2026-0028-railiance-clock-authority-signing.yaml index a24c846..bfe98ab 100644 --- a/credential-change-requests/CCR-2026-0028-railiance-clock-authority-signing.yaml +++ b/credential-change-requests/CCR-2026-0028-railiance-clock-authority-signing.yaml @@ -1,14 +1,15 @@ id: CCR-2026-0028 kind: credential-change-request schema_version: 1 -request_type: workload-kv-read +request_type: attended-host-key-delivery title: Railiance Clock authority signing key custody and host delivery status: approved created: '2026-09-15' updated: '2026-09-15' requester: agent: codex - reason: User requested production admission of the Railiance Clock authority after the host time baseline and disposable native acceptance passed. + reason: User requested production admission of the Railiance Clock authority after + the host time baseline and disposable native acceptance passed. review: required: true required_approvers: @@ -22,71 +23,83 @@ review: - at: '2026-09-15' reviewer: User (railiance-clock-owner) decision: approved - comment: User explicitly approved CCR-2026-0028 for the railiance-clock-owner role. + comment: User explicitly approved CCR-2026-0028 for the railiance-clock-owner + role. + - at: '2026-09-15' + reviewer: User (platform-operator and railiance-clock-owner) + decision: approved_host_delivery + comment: User accepted the corrected systemd host-file design and instructed then + lets go; existing two-role approval retained. target: domain: infotech tenant: platform workload: railiance-clock environment: production - purpose: Hold one ES256 authority private signing key in OpenBao and deliver it only to the private railiance01 Railiance Clock service; no public listener or consumer-wide distribution. + purpose: Hold one ES256 authority private signing key in OpenBao and deliver it + only to the private railiance01 Railiance Clock service; no public listener or + consumer-wide distribution. openbao: mount: platform kv_path: platform/workloads/railiance-clock/authority-signing fields: - PRIVATE_KEY_PEM - KEY_ID - metadata_read: false - token_self_lifecycle: true - policy_name: workload-kv-read-railiance-clock-authority-signing - policy_file: openbao/policies/workload-kv-read-railiance-clock-authority-signing.hcl auth: - method: kubernetes - mount: kubernetes - role: external-secrets-railiance-clock-authority - bound_claims: - service_account_names: - - railiance-clock - service_account_namespaces: - - railiance-clock - bound_claims_confirmed: false - policies: - - workload-kv-read-railiance-clock-authority-signing - ttl: 15m - audience: openbao - token_max_ttl: 15m - token_explicit_max_ttl: 15m - token_no_default_policy: true + method: attended-oidc + mount: netkingdom + role: platform-admin + runtime_access: false + initial_cas: 0 access_frontdoor: - type: host-file-delivery - catalog_id: railiance-clock-authority-signing - selector: Private Railiance Clock authority key on railiance01 - readiness: pending-review + type: attended-host-file + catalog_id: openbao-platform-admin-login + readiness: approved-pending-apply resolvable: false delivery: surface: attended-host-file - target: railiance01:/etc/railiance-clock/authority-key.pem, owner railiance-clock, mode 0600; key material must be generated and delivered through the governed OpenBao platform-admin lane and never appear in Git, argv, logs, or chat. - bootstrap_command: See the attended platform custody runbook to be added with this CCR; configure the service only after key admission, public-key metadata readback, and exact artifact hash verification. + host: 92.205.62.239 + ssh_user: tegwick + target: /var/lib/railiance-clock/signing.pem + owner: railiance-clock + mode: '0600' + directory_mode: '0700' + procedure: scripts/railiance_clock_custody.py risk: classification: high notes: - - Compromise permits forging Railiance Clock samples until the key is revoked and every consumer trust file is replaced. - - The authority remains loopback-only on railiance01; no workstation-wide or public endpoint is admitted. - - Key generation, OpenBao write, host delivery, and rotation require an attended platform operator and railiance-clock owner review. + - Compromise permits forging Railiance Clock samples until the key is revoked and + every consumer trust file is replaced. + - The authority remains loopback-only on railiance01; no workstation-wide or public + endpoint is admitted. + - Key generation, OpenBao write, host delivery, and rotation require an attended + platform operator and railiance-clock owner review. verification: positive: - - Exact service identity reads only PRIVATE_KEY_PEM and KEY_ID; authority readiness and signed sample verification pass. - - Host file ownership and mode are exact; private key value is absent from all command output and logs. + - Attended platform-admin identity and OpenBao audit enabled; initial key stored + with CAS zero. + - Host account, private directory, exact key ownership and permissions verified; + public key matches custody. + - Authority readiness and independent signed sample verification pass. negative: - - Other service accounts, namespaces, sibling paths, metadata/listing, and public listeners are denied. - - Wrong key id, wrong authority/environment/epoch, expired trust, rollback, and stale health fail closed. + - Existing key is never overwritten during initial admission; interrupted delivery + resumes only with the admitted custody version. + - Wrong key id, authority, environment, epoch, stale health and expired trust are + refused. + - No new workload role, policy or token; no public authority listener. activation_conditions: - - CCR approved by platform-operator and railiance-clock-owner. - - Dedicated OpenBao policy, Kubernetes role, and host delivery path are applied and read back without secret disclosure. - - Fresh public-key metadata, artifact SHA-256, /readyz, and colocated sample verification are recorded. + - Existing two-role approval covers attended host delivery. + - Ansible prepares the service account and private directory before custody. + - Reviewed helper runs through warden access openbao-platform-admin-login --exec; + host key verification is required. + - Fresh public key, artifact SHA256, readiness and signed sample acceptance are + recorded. evidence: [] lifecycle: - deactivate: Stop the authority, remove the host key, revoke the workload policy/role, and replace consumer trust files. - rotate: Stop authority and consumers, generate a new key under attended custody, deliver mode 0600, then replace trust files and verify old signatures fail. - compromised: Stop the authority immediately, revoke the policy and key version, replace all trust files, and restart only after fresh verification. + deactivate: Stop authority and withdraw consumer trust. Retain custody and host + key until owner retention decision. + rotate: Stop authority, perform separately versioned attended CAS rotation and atomic + host replacement, then admit new public key and epoch for consumers. + compromised: Stop authority immediately and withdraw all consumer trust; replace + key forward under attended custody before restarting. state_hub: workplan_id: RCLK-WP-0005 diff --git a/docs/credential-lane-designs/railiance-clock-host-delivery.md b/docs/credential-lane-designs/railiance-clock-host-delivery.md new file mode 100644 index 0000000..ef3cdf9 --- /dev/null +++ b/docs/credential-lane-designs/railiance-clock-host-delivery.md @@ -0,0 +1,36 @@ +# Railiance Clock attended host delivery — CCR-2026-0028 + +The operator approved CCR-2026-0028 for both platform and Clock ownership, then +accepted the correction to systemd host delivery. Kubernetes is present on +Railiance01, but this authority runs as a host service and needs no Kubernetes +identity or standing OpenBao read grant. + +Ansible's `railiance_clock_prepare_only=true` prepares the nologin system account, +root-owned installation directory, and service-owned mode-0700 state directory. +It does not require a key or activate the authority. + +Run the committed `scripts/railiance_clock_custody.py --receipt ` +inside `warden access openbao-platform-admin-login --exec -- python3 ...`. +The helper requires the exact approved CCR, a non-root platform-admin session +from auth/netkingdom, and enabled audit. It generates an ES256 key in memory, +creates the exact KV path with CAS zero, and verifies version 1 before delivery. +No workload policy or role is created. SSH uses strict known-host verification; +the receiver writes a complete mode-0600 file atomically to +`/var/lib/railiance-clock/signing.pem`, owned by `railiance-clock`, and refuses to +overwrite any different existing key. Only public-key metadata enters the receipt. + +If custody succeeded and host delivery failed, inspect the receipt stage/version. +Use `--resume-version 1` through a fresh attended envelope to deliver that same +initial version. This never creates or rotates a key. A changed custody version, +existing different host key, or permission drift requires reconciliation. + +After successful delivery, Ansible uses `railiance_clock_key_preinstalled=true`, +checks private key metadata, installs the pinned wheel and health probe, and +starts the localhost-only authority. `railiance_clock_private_key_source` is not +needed in this mode. Readiness and independent signature verification are required +before marking the CCR verified. No client trust is inferred from readiness. + +Rotation is a separate explicit operation: stop the authority, CAS against the +known current custody version, replace the host key atomically, and distribute +new public key/epoch trust before resuming clients. This initial-admission helper +deliberately does not implement rotation or overwrite keys. diff --git a/openbao/policies/workload-kv-read-railiance-clock-authority-signing.hcl b/openbao/policies/workload-kv-read-railiance-clock-authority-signing.hcl deleted file mode 100644 index ed4a20b..0000000 --- a/openbao/policies/workload-kv-read-railiance-clock-authority-signing.hcl +++ /dev/null @@ -1,3 +0,0 @@ -path "platform/data/workloads/railiance-clock/authority-signing" { - capabilities = ["read"] -} diff --git a/schemas/credential-change-request.schema.yaml b/schemas/credential-change-request.schema.yaml index d4f3469..f772ce3 100644 --- a/schemas/credential-change-request.schema.yaml +++ b/schemas/credential-change-request.schema.yaml @@ -38,6 +38,7 @@ allowed_statuses: allowed_request_types: - workload-kv-read + - attended-host-key-delivery secret_markers_rejected: - AGE-SECRET-KEY-1 @@ -157,3 +158,7 @@ guardrails: disallowed_path_fragments: - "*" - ".." + +# Attended host delivery is currently restricted to CCR-2026-0028. +# Exact contract validation: scripts/railiance_clock_custody.py. +# Delegated workload appliers must refuse this request type. diff --git a/scripts/credential-change.py b/scripts/credential-change.py index 60356cb..b3fc654 100755 --- a/scripts/credential-change.py +++ b/scripts/credential-change.py @@ -464,10 +464,16 @@ def validate_ccr(path: Path) -> tuple[dict[str, Any], list[str], list[str]]: if isinstance(status, str) and status not in ALLOWED_STATUSES: errors.append(f"status must be one of {sorted(ALLOWED_STATUSES)}") request_type = ccr.get("request_type") - if request_type != "workload-kv-read": - errors.append("request_type must be workload-kv-read") - else: + if request_type == "workload-kv-read": validate_workload_kv_read(ccr, errors, warnings) + elif request_type == "attended-host-key-delivery": + import importlib.util + spec = importlib.util.spec_from_file_location("railiance_clock_custody", REPO_DIR / "scripts/railiance_clock_custody.py") + owner = importlib.util.module_from_spec(spec) + spec.loader.exec_module(owner) + errors.extend(owner.validate_contract(ccr)) + else: + errors.append("unsupported request_type") return ccr, errors, warnings @@ -554,6 +560,8 @@ def render_summary(ccr: dict[str, Any], warnings: list[str]) -> str: def generated_policy_hcl(ccr: dict[str, Any]) -> str: + if ccr.get("request_type") != "workload-kv-read": + fail("attended host delivery uses its owner procedure; no workload policy") openbao = ccr["openbao"] mount = openbao["mount"] suffix = openbao["kv_path"][len(mount) + 1 :] diff --git a/scripts/railiance_clock_custody.py b/scripts/railiance_clock_custody.py new file mode 100644 index 0000000..929914a --- /dev/null +++ b/scripts/railiance_clock_custody.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +"""Attended CCR-2026-0028 initial custody and host delivery. Silent, resumable.""" +from pathlib import Path +import argparse +import hashlib +import json +import os +import shlex +import subprocess +import sys + +ROOT = Path(__file__).resolve().parents[1] +CCR = ROOT / 'credential-change-requests/CCR-2026-0028-railiance-clock-authority-signing.yaml' +KV = 'platform/data/workloads/railiance-clock/authority-signing' +KID = 'railiance01-clock-20260915-v1' +TARGET = '/var/lib/railiance-clock/signing.pem' + + +def validate_contract(ccr): + errors=[] + expected={'mount':'platform','kv_path':'platform/workloads/railiance-clock/authority-signing', + 'fields':['PRIVATE_KEY_PEM','KEY_ID'],'auth':{'method':'attended-oidc','mount':'netkingdom','role':'platform-admin'}, + 'runtime_access':False,'initial_cas':0} + delivery={'surface':'attended-host-file','host':'92.205.62.239','ssh_user':'tegwick', + 'target':TARGET,'owner':'railiance-clock','mode':'0600','directory_mode':'0700', + 'procedure':'scripts/railiance_clock_custody.py'} + if ccr.get('id')!='CCR-2026-0028' or ccr.get('request_type')!='attended-host-key-delivery': + errors.append('unsupported attended host contract') + if ccr.get('openbao')!=expected or ccr.get('delivery')!=delivery: + errors.append('exact Clock custody and host binding required') + if ccr.get('access_frontdoor',{}).get('resolvable') is not False: + errors.append('runtime credential frontdoor must remain disabled') + if ccr.get('status') in {'approved','applied','verified','active'}: + comments=ccr.get('review',{}).get('comments',[]) + for role in ['platform-operator','railiance-clock-owner']: + if not any(c.get('reviewer')==f'User ({role})' and c.get('decision')=='approved' for c in comments): + errors.append('explicit role approval missing: '+role) + for section,fields in {'verification':['positive','negative','activation_conditions'], + 'lifecycle':['deactivate','rotate','compromised']}.items(): + for field in fields: + if not ccr.get(section,{}).get(field):errors.append(section+'.'+field+' required') + return errors + +class CustodyError(Exception): pass + +def run_cmd(argv,payload=None): + r=subprocess.run(argv,input=payload,capture_output=True,timeout=90) + if r.returncode:raise CustodyError('contained_command_failed') + return r.stdout + +def bao(args,payload=None): + return json.loads(run_cmd(['bao',*args],None if payload is None else json.dumps(payload).encode())) + +REMOTE = '''import os,sys,stat,pwd,subprocess,json,hashlib +p='/var/lib/railiance-clock/signing.pem' +u=pwd.getpwnam('railiance-clock') +d=os.lstat(os.path.dirname(p)) +assert stat.S_ISDIR(d.st_mode) and d.st_uid==u.pw_uid and stat.S_IMODE(d.st_mode)==0o700 +key=sys.stdin.buffer.read(8193) +assert 0