diff --git a/docs/keycape-tls-recovery-boundary.md b/docs/keycape-tls-recovery-boundary.md new file mode 100644 index 0000000..4095397 --- /dev/null +++ b/docs/keycape-tls-recovery-boundary.md @@ -0,0 +1,51 @@ +# Original KeyCape TLS recovery boundary + +Source disposition, 2026-10-17, scoped to railiance-cluster INTENT: cluster runtime, +ingress/certificate plumbing and backup posture. This is source preparation only. +It is not native acceptance, an issuer/key custody assent, a reissue decision or +permission to mutate the cluster. + +NetKingdom's ingress source names `kc.coulomb.social`, `sso/kc-tls`, +`ClusterIssuer/letsencrypt-prod` and Traefik termination before KeyCape8080. +NetKingdom owns that namespace intent; railiance-cluster owns cert-manager, +ingress controller and cluster backup posture. No inspected source establishes +who independently holds the original private TLS recovery material when the +ordinary issuer/cluster is unavailable. A missing exact routing-catalog entry +cannot prove absence of an owning subsystem. + +The identity archive excludes `kc-tls`. Current S2 backup source encrypts SQLite +state, Helm values and kubeconfig to the same host's `/opt/backup/railiance/cluster` +and retains seven versions. No inspected source proves an actually fresh, +independent offsite object or original certificate/key/trust recovery. Archived +RAIL-BS-WP-0004 is historical local-backup delivery; backlog RCLUSTER-WP-0007-T05 +is gated HA issuance/renewal; active RCLUSTER-WP-0017 is signer runtime/storage. +None is an existing live task for this original TLS recovery demand. + +`railiance-backup-s2` also stages raw state/Helm output in named `/tmp` paths +without early hard-loss cleanup; `railiance-restore-s2` prints whole-state restore +instructions. Do not use either as an isolated TLS extraction path. RCLUSTER-WP-0018 +tracks exact inventory, safe private staging/extraction preparation where required, +independent outage proof and future archive-boundary disposition. No original +ciphertext or historical outcome is rewritten by that work. + +`tools/public-tls-metadata.py` is an offline public-only step. After the source +owner identifies independent original **public certificate/chain and trust files**: + +```sh +python3 tools/public-tls-metadata.py --public-cert ORIGINAL_PUBLIC_CERT_CHAIN.pem --public-trust ORIGINAL_PUBLIC_TRUST.pem +``` + +It accepts no private-key argument, bounds regular-file reads, refuses symlinks and +non-certificate PEM material, and reports public hashes/SANs/validity/counts only. +The file names are owner-local placeholders, not a key request. Output establishes +public metadata, not originality, hostname/chain acceptance, reissue permission or +custody. Do not supply a Secret object or combined certificate/key PEM. Fail closed +when original source, trust or exact bindings are missing; no fresh self-signed +trust workaround is admitted. + +For FUTURE NetKingdom archives, the source owners must explicitly disposition +original TLS coverage or a reviewed reissue contract: producer/custody owner, +private artifact boundary, offline recovery independence, original public hashes, +scoped capture/read permissions, retention and native verification. Merely adding +`kc-tls` to secret_refs would change capture/permission/custody scope and is not +authorized by this source review. It cannot repair the current archive retroactively. diff --git a/tests/test_public_tls_metadata.py b/tests/test_public_tls_metadata.py new file mode 100644 index 0000000..53b323e --- /dev/null +++ b/tests/test_public_tls_metadata.py @@ -0,0 +1,34 @@ +import importlib.util +from pathlib import Path +from datetime import datetime,timedelta,timezone +from unittest.mock import patch +import pytest +from cryptography import x509 +from cryptography.hazmat.primitives import hashes,serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.x509.oid import NameOID + +spec=importlib.util.spec_from_file_location('metadata',Path(__file__).parents[1]/'tools/public-tls-metadata.py');m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m) +@pytest.fixture +def cert(tmp_path): + key=rsa.generate_private_key(public_exponent=65537,key_size=2048);name=x509.Name([x509.NameAttribute(NameOID.COMMON_NAME,'synthetic.invalid')]);now=datetime.now(timezone.utc) + cert=(x509.CertificateBuilder().subject_name(name).issuer_name(name).public_key(key.public_key()).serial_number(1).not_valid_before(now-timedelta(minutes=1)).not_valid_after(now+timedelta(hours=1)).add_extension(x509.SubjectAlternativeName([x509.DNSName('synthetic.invalid')]),critical=False).sign(key,hashes.SHA256())) + p=tmp_path/'public.pem';p.write_bytes(cert.public_bytes(serialization.Encoding.PEM));return p + +def test_public_only_metadata_and_hashes(cert): + result=m.describe(cert,cert) + assert result['leaf_dns_sans']==['synthetic.invalid'];assert result['network_contacted'] is False + assert '-----BEGIN' not in str(result) + +def test_mixed_or_private_pem_is_refused(cert): + cert.write_bytes(cert.read_bytes()+b'\n-----BEGIN PRIVATE KEY-----\nsynthetic\n-----END PRIVATE KEY-----') + with pytest.raises(ValueError):m.public_bundle(cert) + +def test_oversize_is_refused_before_read(cert): + with patch.object(m,'LIMIT',1),patch.object(m.os,'open') as opened: + with pytest.raises(ValueError):m.public_bundle(cert) + opened.assert_not_called() + +def test_symlink_refused(cert): + link=cert.with_name('link.pem');link.symlink_to(cert) + with pytest.raises(ValueError):m.public_bundle(link) diff --git a/tools/public-tls-metadata.py b/tools/public-tls-metadata.py new file mode 100644 index 0000000..b3d429f --- /dev/null +++ b/tools/public-tls-metadata.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +"""Owner-run public certificate/trust metadata; no private-key argument or network. + +The owner supplies public-only PEM files from the independently established +original recovery source. This does not identify that source or accept reissue. +""" +import argparse +from datetime import timezone +import hashlib +import json +import os +from pathlib import Path +import re +import stat +from cryptography import x509 +from cryptography.hazmat.primitives import serialization + +LIMIT=1024*1024 +CERT=re.compile(br'-----BEGIN CERTIFICATE-----\r?\n[A-Za-z0-9+/=\r\n]+-----END CERTIFICATE-----') + +def public_bundle(path): + # Bound the read before allocation; refuse every non-certificate PEM block. + path=Path(os.path.abspath(path)) + if any(p.is_symlink() for p in (path,*path.parents)) or not stat.S_ISREG(path.stat().st_mode) or not 0LIMIT:raise ValueError() + blocks=CERT.findall(raw) + if not blocks or len(blocks)>512 or CERT.sub(b'',raw).strip():raise ValueError() + return raw,[x509.load_pem_x509_certificate(block) for block in blocks] + +def describe(cert_path,trust_path): + certificate,chain=public_bundle(cert_path);trust,roots=public_bundle(trust_path);leaf=chain[0] + try:names=leaf.extensions.get_extension_for_class(x509.SubjectAlternativeName).value.get_values_for_type(x509.DNSName) + except x509.ExtensionNotFound:names=[] + spki=leaf.public_key().public_bytes(serialization.Encoding.DER,serialization.PublicFormat.SubjectPublicKeyInfo) + def utc_date(name): + value=getattr(leaf,name+'_utc',None) + if value is None:value=getattr(leaf,name).replace(tzinfo=timezone.utc) + return value.isoformat() + return {'schema':'nk49.public-tls-metadata.v1','status':'public_metadata_only_not_originality_or_owner_assent', + 'tls_cert_sha256':hashlib.sha256(certificate).hexdigest(),'trust_sha256':hashlib.sha256(trust).hexdigest(), + 'leaf_public_spki_sha256':'sha256:'+hashlib.sha256(spki).hexdigest(),'leaf_dns_sans':names, + 'leaf_not_before':utc_date('not_valid_before'),'leaf_not_after':utc_date('not_valid_after'), + 'server_chain_certificates':len(chain),'trust_certificates':len(roots),'network_contacted':False,'private_key_argument_supported':False} + +def main(): + p=argparse.ArgumentParser(description=__doc__);p.add_argument('--public-cert',required=True);p.add_argument('--public-trust',required=True);a=p.parse_args() + try:result=describe(a.public_cert,a.public_trust) + except Exception:print('{"status":"public_metadata_refused","private_diagnostics_suppressed":true}');return 1 + print(json.dumps(result,indent=2));return 0 + +if __name__=='__main__':raise SystemExit(main()) diff --git a/workplans/ADHOC-2026-07-27.md b/workplans/ADHOC-2026-07-27.md index 4c54d62..30a71bc 100644 --- a/workplans/ADHOC-2026-07-27.md +++ b/workplans/ADHOC-2026-07-27.md @@ -20,6 +20,7 @@ state_hub_workstream_id: "05ef985a-656e-5d28-970a-db1300a7b648" id: RCLUSTER-WP-ADHOC-2026-07-27-T01 status: done priority: high +state_hub_task_id: "3e3f39d3-5070-5ba3-97db-19f5f361f264" ``` Enable only `kubernetes.podspec-init-containers` in Knative diff --git a/workplans/RCLUSTER-WP-0018-keycape-original-tls-recovery.md b/workplans/RCLUSTER-WP-0018-keycape-original-tls-recovery.md new file mode 100644 index 0000000..48652ed --- /dev/null +++ b/workplans/RCLUSTER-WP-0018-keycape-original-tls-recovery.md @@ -0,0 +1,132 @@ +--- +id: RCLUSTER-WP-0018 +type: workplan +title: "Establish original KeyCape TLS recovery and future archive boundary" +domain: financials +repo: railiance-cluster +status: ready +owner: codex +topic_slug: railiance +created: "2026-10-17" +updated: "2026-10-17" +flavor: implementation +depends_on: [] +related: [CUST-WP-0080, NK-WP-0049, RCLUSTER-WP-0007, RCLUSTER-WP-0017] +state_hub_workstream_id: "e0a7813d-c916-5e49-84b2-5e5b30441891" +--- + +Required owner source work for NK-WP-0049-T35 and CUST-WP-0080 G08. Reviewed +against current INTENT/SCOPE, backup sources and existing work records; ready +means work can proceed, not privileged execute authority. This does not promote +HA/backlog work, reopen archived local backup delivery or duplicate signer0017. +Source-owner scope is certificate plumbing/backup posture; NetKingdom owns +namespace ingress, while exact original independent TLS custody remains to be +established. No secret read, reissue, live change or native assent occurred. + +## Record source boundary and public-only metadata preparation + +```task +id: RCLUSTER-WP-0018-T01 +status: done +priority: high +assignee: codex +state_hub_task_id: "7280193d-0d59-5b78-a2d3-96ebd127127f" +``` + +Completed 2026-10-17 as isolated source preparation. Independently reviewed the +17 public source references against current owner bytes, checked current owner +instructions/environment/supervision and existing plans, and recorded the exact +boundary in docs/keycape-tls-recovery-boundary.md. Prepared bounded public-only +certificate/trust metadata projection with generated-certificate tests; no actual +certificate/key read. Source integration/publication and task projection remain +separate receipts, not claimed by this completed preparation task. + +Evidence: the-custodian docs/changes/netkingdom-target-2026-10-16/ +recovery-owner-handoffs-r1/ and prior recovery-attempt-r1/t35-t36-source-next/. + +## Establish exact independent original TLS artifact and provider custody metadata + +```task +id: RCLUSTER-WP-0018-T02 +status: todo +priority: high +assignee: codex +state_hub_task_id: "90836435-917f-504c-a711-3f29ece348e3" +``` + +Next: identify the actual original sso/kc-tls custody owner and independent public +provenance through existing owner processes. Record artifact/provider, timestamp, +public certificate/trust hashes, private custody entry point and outage dependencies +without values. Source review cannot infer a fresh backup, original key coverage +or independent decryption custody from a local backup script. Use credential +routing before any later credential need; missing route is not proof of absence. +Done when exact independent recovery or an explicitly owned unavailable capability +is evidenced as metadata and a reviewed next action, with no guessed assent. + +## Prepare safe isolated original extraction or explicit reissue alternative + +```task +id: RCLUSTER-WP-0018-T03 +status: wait +priority: high +assignee: codex +depends_on: [RCLUSTER-WP-0018-T02] +blocking_reason: "Exact original artifact/provider/custody disposition is required before selecting extraction or reissue source." +state_hub_task_id: "5dfa9a3a-2639-5cf6-b0a4-13eef417d47a" +``` + +From T02, reuse the actual owner path to prepare an exact attended private-only +recovery command and full dependency/cleanup proof. Do not run whole-state restore +or expose raw state/Secret output. If existing backup staging is required, correct +only the needed path with bounded memory-only staging and early exact cleanup, +including error/parent-loss tests before use. Original recovery and accepted +reissue are distinct claims; no self-signed trust substitute, new broker or +implicit production action. Done when the exact source proposal, public metadata +preflight, negatives, timeout, abort and custody obligations are reviewed. + +## Verify admitted original TLS recovery through the actual owner boundary + +```task +id: RCLUSTER-WP-0018-T04 +status: wait +priority: high +assignee: codex +needs_human: true +depends_on: [RCLUSTER-WP-0018-T02, RCLUSTER-WP-0018-T03] +blocking_reason: "Await exact reviewed attended owner custody path and its existing scoped authority; no private input or native attempt requested yet." +state_hub_task_id: "021b07bb-65bc-5743-bac5-4294b4be74f6" +``` + +Only the actual admitted owner action can prove recovered certificate/key pairing, +original hostname/trust and independent outage operation, or the expressly accepted +reissue result. Retain exact source/artifact/target/identity/negative/cleanup receipts. +Human custody/authority gates are not worker wakeups. Source and historical local +backup proof never fill native assent. Return bounded public proof to NK49T35; +no production mutation, credential retrieval or approval is granted by this task. + +## Disposition original TLS coverage in the future identity archive contract + +```task +id: RCLUSTER-WP-0018-T05 +status: todo +priority: high +assignee: codex +state_hub_task_id: "5bc67a22-0991-5625-86a6-f749560d8a0e" +``` + +Next: deliver the source-owned requirement in docs/keycape-tls-recovery-boundary.md +to NetKingdom's existing contract process: original TLS custody or accepted reissue, +independent provider/key recovery, public identity hashes, scoped capture/read +permissions and verification/retention. Record named owner dependencies as live +work before closure. Do not append kc-tls to secret_refs, expand capture credentials +or rewrite a historical archive without those owner reviews/authority. Done when +the future contract requirement has an exact tracked disposition/handoff; it is +not proof of current archive coverage or actual future capture. + +## Coordination and completion + +Beneficiary NK-WP-0049-T35/CUST-WP-0080 remains linked to this obligor record by +derived cross_owner_wait case nk49-t35-rcluster18 in the packet. No new project +driver, live worker injection or message was created. Own T03 wait has an explicit +local prerequisite; T04 is a human gate. Close only after source/native obligations +are proved or an explicit bounded scope disposition hands off remaining live work.