feat(recovery): prepare owner-bound offline continuity preflight
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a1468b-63fa-7710-85dc-8f2e16b3ebfa
This commit is contained in:
parent
535681052c
commit
c59bbf568c
5 changed files with 248 additions and 0 deletions
38
docs/keycape-isolated-recovery-selector.md
Normal file
38
docs/keycape-isolated-recovery-selector.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# KeyCape original service recovery selector
|
||||
|
||||
Source-owner disposition, 2026-10-17, limited to secrets-engine's current consumer
|
||||
contract. Current src/secrets_engine/service_auth.py fixes client/audience
|
||||
secrets-engine-openbao, subject service:secrets-engine, tenant tenant:platform,
|
||||
role secrets-engine and exact openbao:login scope. The NetKingdom producer binds
|
||||
KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET; original KeyCape native evidence
|
||||
uses openbao:admin as the denied-scope case. This selector matches the current
|
||||
consumer source. This disposition is not native assent or a credential grant.
|
||||
|
||||
SECRETS-WP-0012 finished on historical attended custody/probe evidence and stays
|
||||
finished. SECRETS-WP-0008-T06 retains functional steady-state adoption and scoped
|
||||
backend entitlement. SECRETS-WP-0013 retains task-scoped backend execution. The
|
||||
new SECRETS-WP-0015 covers only this original archive/isolated consumer preparation
|
||||
and later accepted continuity proof; those existing gates are not duplicated.
|
||||
|
||||
The current consumer profile forbids a client-secret file and requires its
|
||||
attended in-memory OpenBao source. That source depends on ordinary KeyCape OIDC
|
||||
and OpenBao availability. The isolated issuer's native checker can consume its
|
||||
original archive-delivered issuer binding without a provider login, but that does
|
||||
not prove the secrets-engine consumer path works during an OpenBao/issuer outage.
|
||||
Do not bypass the no-file/source contract, retrieve a live credential, redirect a
|
||||
production consumer or invoke probe-openbao to fill this recovery gap. An exact
|
||||
owner-reviewed outage-compatible proof/custody plan remains required.
|
||||
|
||||
Offline public preflight, after integration of reviewed source:
|
||||
|
||||
```sh
|
||||
python3 tools/check-keycape-recovery-selector.py --selector docs/keycape-recovery-selector.json --source-sha256 sha256:c0ade429edf452ad17f05d690b7ebef091db7f1a42b5ac350f1e40955c8586c0
|
||||
```
|
||||
|
||||
Use the exact hash in the owner packet, not a digest inferred at invocation from
|
||||
an unreviewed file. The tool parses source constants with AST and never imports
|
||||
the credential/backend module or calls the network. It requires all eight public
|
||||
selector fields, exact source hash and unambiguous bounded JSON. Successful output
|
||||
means source correspondence only. Original archived presence/secret binding,
|
||||
actual TLS trust, native claim negatives and selected consumer continuity remain
|
||||
separately evidenced. Missing/changed bindings refuse before any custody request.
|
||||
15
docs/keycape-recovery-selector.json
Normal file
15
docs/keycape-recovery-selector.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"schema": "secrets-engine.public-recovery-selector.v1",
|
||||
"status": "source_correspondence_only_not_native_assent",
|
||||
"issuer": "https://kc.coulomb.social",
|
||||
"consumer": {
|
||||
"client_id": "secrets-engine-openbao",
|
||||
"secret_env": "KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET",
|
||||
"audience": "secrets-engine-openbao",
|
||||
"scope": "openbao:login",
|
||||
"subject": "service:secrets-engine",
|
||||
"tenant": "tenant:platform",
|
||||
"roles": "secrets-engine",
|
||||
"deny_scope": "openbao:admin"
|
||||
}
|
||||
}
|
||||
31
tests/test_keycape_recovery_selector.py
Normal file
31
tests/test_keycape_recovery_selector.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
|
||||
spec=importlib.util.spec_from_file_location('check',Path(__file__).parents[1]/'tools/check-keycape-recovery-selector.py');m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
|
||||
SOURCE=b'CLIENT_ID="secrets-engine-openbao"\nSUBJECT="service:secrets-engine"\nTENANT="tenant:platform"\nROLE="secrets-engine"\nSCOPE="openbao:login"\nraise RuntimeError("source is parsed, never executed")\n'
|
||||
SELECTOR={'client_id':'secrets-engine-openbao','secret_env':'KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET','audience':'secrets-engine-openbao','scope':'openbao:login','subject':'service:secrets-engine','tenant':'tenant:platform','roles':'secrets-engine','deny_scope':'openbao:admin'}
|
||||
DIGEST='sha256:'+hashlib.sha256(SOURCE).hexdigest()
|
||||
def test_source_parsed_not_executed():
|
||||
result=m.verify(SELECTOR,SOURCE,DIGEST)
|
||||
assert result['network_contacted'] is False
|
||||
@pytest.mark.parametrize('field',list(SELECTOR))
|
||||
def test_each_selector_binding_is_exact(field):
|
||||
bad={**SELECTOR,field:'different'}
|
||||
with pytest.raises(ValueError):m.verify(bad,SOURCE,DIGEST)
|
||||
def test_source_drift_and_untyped_digest_refused():
|
||||
for source,digest in ((SOURCE+b'\n',DIGEST),(SOURCE,DIGEST.removeprefix('sha256:'))):
|
||||
with pytest.raises(ValueError):m.verify(SELECTOR,source,digest)
|
||||
def test_duplicate_constants_refused():
|
||||
source=SOURCE+b'CLIENT_ID="other"\n'
|
||||
with pytest.raises(ValueError):m.verify(SELECTOR,source,'sha256:'+hashlib.sha256(source).hexdigest())
|
||||
def test_duplicate_public_metadata_keys_refused():
|
||||
with pytest.raises(ValueError):json.loads('{"consumer":{},"consumer":{}}',object_pairs_hook=m.unique)
|
||||
def test_oversize_public_input_refused(tmp_path):
|
||||
p=tmp_path/'selector.json';p.write_bytes(b'x'*(m.LIMIT+1))
|
||||
with pytest.raises(ValueError):m.read(p)
|
||||
def test_symlink_public_input_refused(tmp_path):
|
||||
p=tmp_path/'selector.json';p.write_text('{}');link=tmp_path/'link';link.symlink_to(p)
|
||||
with pytest.raises(ValueError):m.read(link)
|
||||
64
tools/check-keycape-recovery-selector.py
Normal file
64
tools/check-keycape-recovery-selector.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Offline public selector preflight; no imports of credential/backend clients.
|
||||
|
||||
Checks current consumer source constants and the producer's already-deployed
|
||||
public env/denial selector. This is source metadata, not runtime/owner assent.
|
||||
"""
|
||||
import argparse
|
||||
import ast
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import stat
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
LIMIT=128*1024
|
||||
|
||||
def read(path):
|
||||
path=Path(os.path.abspath(path))
|
||||
if any(p.is_symlink() for p in (path,*path.parents)):raise ValueError()
|
||||
fd=os.open(path,os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK)
|
||||
with os.fdopen(fd,'rb') as source:
|
||||
info=os.fstat(source.fileno())
|
||||
if not stat.S_ISREG(info.st_mode) or not 0<info.st_size<=LIMIT:raise ValueError()
|
||||
raw=source.read(LIMIT+1)
|
||||
if len(raw)>LIMIT:raise ValueError()
|
||||
return raw
|
||||
|
||||
def unique(pairs):
|
||||
result={}
|
||||
for key,value in pairs:
|
||||
if key in result:raise ValueError()
|
||||
result[key]=value
|
||||
return result
|
||||
|
||||
def verify(selector,source,source_sha256):
|
||||
if not re.fullmatch('sha256:[a-f0-9]{64}',source_sha256) or 'sha256:'+hashlib.sha256(source).hexdigest()!=source_sha256:raise ValueError()
|
||||
needed={'CLIENT_ID','SUBJECT','TENANT','ROLE','SCOPE'};constants={}
|
||||
for node in ast.parse(source).body:
|
||||
if isinstance(node,ast.Assign):
|
||||
for target in node.targets:
|
||||
if isinstance(target,ast.Name) and target.id in needed:
|
||||
if target.id in constants or not isinstance(node.value,ast.Constant) or not isinstance(node.value.value,str):raise ValueError()
|
||||
constants[target.id]=node.value.value
|
||||
if set(constants)!=needed:raise ValueError()
|
||||
expected={'client_id':constants['CLIENT_ID'],'secret_env':'KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET',
|
||||
'audience':constants['CLIENT_ID'],'scope':constants['SCOPE'],'subject':constants['SUBJECT'],
|
||||
'tenant':constants['TENANT'],'roles':constants['ROLE'],'deny_scope':'openbao:admin'}
|
||||
if selector!=expected:raise ValueError()
|
||||
return {'schema':'secrets-engine.recovery-selector-preflight.v1','status':'source_selector_matches_not_native_assent',
|
||||
'source_sha256':source_sha256,'consumer':expected,'credential_source_exercised':False,'network_contacted':False}
|
||||
|
||||
def main():
|
||||
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--selector',required=True);p.add_argument('--source-sha256',required=True)
|
||||
args=p.parse_args()
|
||||
try:
|
||||
proposal=json.loads(read(args.selector),object_pairs_hook=unique)
|
||||
if not isinstance(proposal,dict) or not isinstance(proposal.get('consumer'),dict):raise ValueError()
|
||||
result=verify(proposal['consumer'],read(ROOT/'src/secrets_engine/service_auth.py'),args.source_sha256)
|
||||
except Exception:print('{"status":"source_selector_refused","private_diagnostics_suppressed":true}');return 1
|
||||
print(json.dumps(result,indent=2));return 0
|
||||
|
||||
if __name__=='__main__':raise SystemExit(main())
|
||||
100
workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md
Normal file
100
workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
---
|
||||
id: SECRETS-WP-0015
|
||||
type: workplan
|
||||
title: "Prepare original KeyCape service selector and isolated consumer continuity"
|
||||
domain: infotech
|
||||
repo: secrets-engine
|
||||
status: ready
|
||||
owner: codex
|
||||
topic_slug: custodian
|
||||
created: "2026-10-17"
|
||||
updated: "2026-10-17"
|
||||
flavor: implementation
|
||||
depends_on: []
|
||||
related: [CUST-WP-0080, NK-WP-0049, SECRETS-WP-0012, SECRETS-WP-0008, SECRETS-WP-0013]
|
||||
state_hub_workstream_id: "68a0642e-4908-5260-89c9-3adce60e6a4d"
|
||||
---
|
||||
|
||||
Needed source-owner work for NK-WP-0049-T36/T18. Reviewed against current
|
||||
Lifecycle INTENT, explicit consumer constants/custody source and dated historical
|
||||
proof. Ready means work can proceed, not native assent. Finished0012 remains
|
||||
finished; functional adoption/backend entitlement stay0008T06/0013. No new grant,
|
||||
secret fetch, provider login, production redirect or external message occurred.
|
||||
|
||||
## Establish exact current intended source selector
|
||||
|
||||
```task
|
||||
id: SECRETS-WP-0015-T01
|
||||
status: done
|
||||
priority: high
|
||||
assignee: codex
|
||||
state_hub_task_id: "a3e0e243-edac-5ade-a3d5-f58dac41633a"
|
||||
```
|
||||
|
||||
Completed 2026-10-17 as source review. Current consumer constants and config guards
|
||||
agree with secrets-engine-openbao client/audience, service:secrets-engine,
|
||||
tenant:platform, role secrets-engine and only openbao:login. Producer env selector
|
||||
and historical denied openbao:admin case are independently referenced, without
|
||||
accepting the producer's native role. Prior native issuance7Oct and consumer
|
||||
probe10Oct predate reviewed archive16Oct; they neither prove exact archived bytes
|
||||
nor current native assent. Recorded docs/keycape-isolated-recovery-selector.md.
|
||||
Evidence: Custodian recovery-owner-handoffs-r1 and prior17-source review.
|
||||
|
||||
## Deliver bounded offline public source selector preflight
|
||||
|
||||
```task
|
||||
id: SECRETS-WP-0015-T02
|
||||
status: done
|
||||
priority: high
|
||||
assignee: codex
|
||||
state_hub_task_id: "eab3b758-10ce-5464-b5f7-3a152ee1327a"
|
||||
```
|
||||
|
||||
Completed 2026-10-17 as isolated source preparation. Prepared AST-only source-hash
|
||||
and all-eight-selector-field checker with bounded strict JSON, source drift,
|
||||
duplicate, oversize and symlink negatives. It imports no backend/credential client
|
||||
and never exchanges a token. Tests use public/synthetic metadata only. Canonical
|
||||
integration/publication/projection must be recorded separately; successful output
|
||||
is source correspondence, not native acceptance or a new secret source.
|
||||
|
||||
## Prepare outage-compatible original consumer proof without custody bypass
|
||||
|
||||
```task
|
||||
id: SECRETS-WP-0015-T03
|
||||
status: todo
|
||||
priority: high
|
||||
assignee: codex
|
||||
state_hub_task_id: "6262a915-7655-50be-b9f7-481f816d7335"
|
||||
```
|
||||
|
||||
Next: map the exact original archive-delivered registration and producer-native
|
||||
checker result to the intended consumer, then prepare a source-reviewed isolated
|
||||
proof/custody plan. Current consumer source requires attended OpenBao memory read
|
||||
and refuses files; ordinary OpenBao/KeyCape outage is therefore a real dependency.
|
||||
Do not add a fallback, persistent consumer copy or unreviewed injection source.
|
||||
Clarify which continuity claim can be proved offline and which requires an exact
|
||||
accepted owner custody/provider path. Bind current original selectors, image/
|
||||
source/TLS metadata, negative cases, timeout/abort and cleanup; retain protected
|
||||
backend operation entitlement in existing0008/0013. Done when that exact plan and
|
||||
remaining owner dependencies are reviewable, without retrieving any value.
|
||||
|
||||
## Verify the separately admitted actual isolated consumer result
|
||||
|
||||
```task
|
||||
id: SECRETS-WP-0015-T04
|
||||
status: wait
|
||||
priority: high
|
||||
assignee: codex
|
||||
needs_human: true
|
||||
depends_on: [SECRETS-WP-0015-T03, NK-WP-0049-T33]
|
||||
blocking_reason: "Await exact consumer proof/custody plan plus original TLS/private admission readiness; actual attended owner path/authority remains required."
|
||||
state_hub_task_id: "aeec6eb0-39e2-5f7c-8f5d-778133edfc73"
|
||||
```
|
||||
|
||||
Only a separately admitted owner action can establish actual archived registration,
|
||||
issuer trust, native negative checks and consumer continuity. Source selector and
|
||||
historical provider proof do not fill this gate. No production consumer redirect,
|
||||
new grant, live OpenBao read, backend write or retained-factor MFA is authorized.
|
||||
Return exact bounded evidence to NK49T36/T18; MFA remains NK49T19. Human gate is
|
||||
never a worker wakeup. Cross-owner dependency is attached to derived case
|
||||
secrets15-nk49-readiness in the packet; do not strand the wait or invent a driver.
|
||||
Loading…
Add table
Add a link
Reference in a new issue