docs: preserve reviewed NetKingdom session artifacts
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 3s
Reject generated-only commits / generated-commit-guard (push) Has been cancelled

This commit is contained in:
codex 2026-11-01 23:05:08 +01:00
parent 2565caacd9
commit bdf71998e9
44 changed files with 11287 additions and 0 deletions

View file

@ -0,0 +1 @@
100644 ea9ccd0b2bd2cc1ec89723b35c44c53f0d79ba98 0 .custodian-brief.md

View file

@ -0,0 +1,85 @@
<!-- custodian-brief: generated by fix-consistency — do not edit manually -->
# Custodian Brief — net-kingdom
**Domain:** infotech
**Last synced:** 2026-10-16 19:19 UTC
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
## Active Workstreams
### Publish the NetKingdom emission-cadence security profile
Progress: 5/6 done | workplan_id: `04685f94-1991-5e62-80d2-5669913e99fc`
**Open tasks:**
- ► Bind and hand off the published contract `e3fbc8b8`
### Register Playwright human and agent consumers through governed identity delivery
Progress: 1/3 done | workplan_id: `06ba98d5-609e-5aaa-9bf7-4c4f4460e377`
**Open tasks:**
- ! Verify issuer and real consumer acceptance `5a3aaf57`
*(wait: Await owner-scoped registry/custody activation, native cluster authentication and individual transport, then real human and agent acceptance under CUST-WP-0080-T05.)*
- ► Deliver custody integration and guarded registration activation `a9dadd07`
### Remove bootstrap secret output and verify separated recovery custody
Progress: 22/31 done | workplan_id: `17d315f0-829e-57c7-85cc-3a7914152804`
**Open tasks:**
- ! Accept remaining recovery and caller evidence and close the G08 handoff `d68cd4d3`
*(wait: Await the separately tracked native service proofs, current/legacy caller dispositions and residual-scope decisions; archive recovery, backup activation, independent outage retrieval and guide publication are complete.)*
- ! Resolve historical credential and configuration-preserving rotation dispositions `3ea6bf00`
*(wait: Await the caller/consumer ledger, current replacement-lane acceptance and explicit credential-owner dispositions; existing exposure-rotation deferral is unchanged.)*
- ! Verify current caller and provider admission at their actual boundaries `77aa42be`
*(wait: Await the current-caller ledger and exact per-caller owner admission/test inputs; the backup credential grants do not admit unrelated callers.)*
- ! Prove retained privacyIDEA token decryption and existing-token MFA `6f8af1a7`
*(wait: LLDAP T16, token-material T31 and audit T20 passed; await the exact isolated MFA adapter and owner-selected retained-token/factor inputs.)*
- ! Prove restored KeyCape issuer, policy and consumer continuity `141dd773`
*(wait: LLDAP recovery T16 passed; await the exact isolated KeyCape adapter/procedure and retained-token MFA proof for the selected flow.)*
- ! Prove restored Authelia storage and OIDC continuity `b47f9975`
*(wait: LLDAP recovery T16 passed; await the exact isolated Authelia adapter/procedure and private authentication-flow inputs from T15.)*
- ► Map current callers, owners and native acceptance gaps `479d6970`
- … and 2 more open tasks
### Establish SecurityCaseTypes so that known security cases are light
Progress: 7/9 done | workplan_id: `e31f3f9d-0748-5137-b0a3-7b3ca46a66eb`
**Open tasks:**
- ! T05 — Acknowledge the type through informed-decision `d9b26bcf`
*(wait: Type1.2 architecture has a confirmed source-only founder acknowledgment; canonical1.1 remains unchanged/default and the1.2 reference case stays proposed. Needs explicit version selection/reference-source integration without normal admission bypass, closed owner adapters plus global/native receipt verifier (WARDEN-WP-0044-T03), independently verified six-object structural materialization and CCR42 claims/created-only provenance, exact broker/case/source reconciliation, then separately authorized native mint/delivery and attended registry/consumer verification. Source acknowledgment, a bundle or local tests do not prove implemented/materialized/live status.)*
- ! T06 — First KnownSecurityCase: Forgejo `c3c9c3ae`
*(wait: The next types follow once the first one is proven end to end.)*
### Define an execution-attribution receipt for Railiance runs
Progress: 1/2 done | workplan_id: `e2533f3a-aa43-59b3-bff3-8e64b6149487`
**Open tasks:**
- ! Agree the evidence holder and schema with audit-core and Railiance `963120c1`
*(wait: Audit-core accepted evidence custody and published field/admission requirements in AUDIT-WP-0014-T01. Still needs Railiance owner confirmation of exact emitter and wire binding before canonical schema/validator implementation, followed by actual emission/readback and attribution/clock/unknown-case acceptance.)*
### Enterprise Federation & SAML — Expanded-Mode Keycloak Identity Broker
Progress: 1/9 done | workplan_id: `1075448f-d533-5f9e-94b7-c3adfe151a07`
**Open tasks:**
- ! Admit the database consumer `2fe6f100`
*(wait: ADR-0009 adoption trigger absent; then railiance-platform/rapp-postgres must admit the database consumer and return backup/restore evidence.)*
- ! Package the broker deployment `32d1411b`
*(wait: ADR-0009 adoption trigger and named package/runtime owner absent; requires T02 database admission and governed OpenBao/ESO role plus immutable issuer assignment.)*
- ! Integrate an upstream identity provider `6697a994`
*(wait: Named tenant/upstream IdP owner/federation need absent; requires admitted broker and owner-reviewed tenant-scoped claim mappings plus actual login evidence.)*
- ! Define federated assurance `d845380d`
*(wait: Adoption trigger absent; requires per-IdP assurance trust mapping and reviewed privileged step-up delivery from issuer/package and access-engine owners.)*
- ! Verify IAM conformance and coexistence `9cb7fd93`
*(wait: Requires admitted live broker, canonical tenant issuer selection and dedicated test-client conformance/coexistence evidence; adoption trigger absent.)*
- ! Enforce tenant and platform boundaries `38998c68`
*(wait: Requires tenant-engine tier/issuer ownership and access-engine boundary/freshness enforcement evidence after adoption trigger and broker admission.)*
- ! Prove recovery and audit delivery `e5f44ddc`
*(wait: Requires admitted database/broker, named recovery and audit owners, off-host restore and observed audit/break-glass receipts; adoption trigger absent.)*
- … and 1 more open tasks
---
## MCP Orientation (when available)
If the state-hub MCP server is reachable, call:
`get_domain_summary("infotech")`
This provides richer cross-domain context.
If the MCP call fails, use this file as your orientation source.

View file

@ -0,0 +1,5 @@
{
"exit_code": 1,
"stdout": "",
"stderr": "{\"blocked_blob_ids\": [\"feb4df39d0e3e0c3bffd976c31c3b30196651863\"], \"scanned_blobs\": 9, \"scanner_sha256\": \"88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\", \"scanner_version\": \"8.30.1\", \"scope\": \"changed-staged-blobs\", \"status\": \"blocked\"}\n"
}

View file

@ -0,0 +1,5 @@
{
"path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md",
"before_sha256": "8b6e12a53b0d5e88d71bb1d3e11bedd0ee3a66b30a9ea2b86e6ac1ff38539baf",
"after_sha256": "29c10e410449ebf1f4b0fcc804177895cb91b47c35adb33fbe9246265ad57cbc"
}

View file

@ -0,0 +1,956 @@
---
id: NK-WP-0049
type: workplan
title: "Remove bootstrap secret output and verify separated recovery custody"
domain: infotech
repo: net-kingdom
status: active
owner: codex
topic_slug: netkingdom
created: "2026-10-12"
updated: "2026-10-16"
flavor: implementation
depends_on: []
state_hub_workstream_id: "17d315f0-829e-57c7-85cc-3a7914152804"
---
Native owner work for CUST-WP-0080 G08. This workplan remains active until the
remaining caller, service-recovery and custody acceptance gates are satisfied.
NK-WP-0045 covers the non-secret root-grant record. Founder-deferred exposure
rotation remains in CUST-WP-0073-T05; this plan does not silently reopen it.
**Replanned 2026-10-16 at the operator's request:** substantial completed work is
now represented by completed tasks, and remaining work has separate deliverables,
dependencies and acceptance criteria. T01–T03 retain their historical IDs and Hub
UUIDs. Completed parts of the old T02/T03 umbrellas are extracted into T04–T14;
T02 now covers the current-caller ledger and T03 retains final owner acceptance.
These are retrospective task records of evidenced work, not additional delivery
or newly granted authority. The earlier dated narrative remains in Git at
`cc2dd01`; its evidence references are carried into the tasks below.
**Current position:** 22 tasks done; 2 in progress; 1 ready to start (`todo`);
6 waiting on explicit prerequisites. These are task counts, not an effort-based
completion percentage. Assignees are also written in task descriptions because
the current Forge projector does not populate the dedicated Hub assignee field. The outage-retrieval tool and restore-packet draft are now prepared;
independent outage retrieval and Storage Share publication are accepted;
service admission remains an explicit gate.
**Next actions:** finish the read-only caller ledger (T02); admit the drafted
isolated restore packet (T15) and record residual-scope handoffs (T24).
Independent outage retrieval (T23) and guide publication (T26) are done. After owner admission, execute the individual native service and
caller checks. A task's owner is accountable for the result; another repository's
approval or credential grant still requires its own owner path.
**Closure:** T03 assembles owner acceptance after its named dependencies pass.
Passing archive recovery does not prove existing-user authentication, retained
MFA-token decryption, historical audit signatures or current caller admission.
The verified backup job stays active while those separate tasks are completed.
## Remove secret-bearing output and ambient root discovery
```task
id: NK-WP-0049-T01
status: done
priority: high
assignee: codex
state_hub_task_id: "d7ca8d5f-c5ff-5cba-bd25-038cb53ffe4e"
```
**Accountable worker:** codex. Service/credential-owner acceptance remains with
the named owner boundary.
2026-10-12: regression reproduced emergency/reprint root export, generation
prefix disclosure, permissive file modes, dry-run side effects and unattended
entry. Replaced emergency export with a non-secret guide (legacy secret-input
arguments refused), suppressed generation prefixes/tracing and set private
permissions. Bootstrap requires the registered public recipient and an attended
terminal; never generates/discovers a permanent private key. Encrypted resume
requires an explicit matching temporary custody key before cluster activity.
Dry-run is a non-mutating plan. The live cluster/apply path was not executed.
A fresh uniquely named encrypted ops bundle follows apply; old delivery flags
cannot skip current acknowledgment. New acknowledgment state binds its SHA256
and timestamp. Declining stops before cleanup/completion. This is an operator
statement, not an independent restore receipt. Historical fields remain unchanged.
Make/help/status/console and current credential guidance reflect that distinction.
Evidence and exact source manifest: Custodian
`docs/evidence/2026-10-12-netkingdom-security/bootstrap-handoff.json`.
## Harden custody, SOPS and rotation source entry points
```task
id: NK-WP-0049-T04
status: done
priority: high
assignee: codex
depends_on: [NK-WP-0049-T01]
state_hub_task_id: "04c4e8be-5bd8-5d5c-99b1-79d8ae758462"
```
**Accountable worker:** codex. Service/credential-owner acceptance remains with
the named owner boundary.
Completed 2026-10-12–13. Public-recipient-only encryption, explicit temporary
custody, private no-overwrite output, sanitized diagnostics, stdin-only secret
transport and failed-operation cleanup replace ambient-key and output paths.
SOPS read/edit/data-key rotation preserve recipient/MAC/source-version guards;
component rotations preserve unrelated fields and retain recovery candidates.
This records source and disposable-native proof, not live rotation or revocation.
Evidence: Custodian `docs/evidence/2026-10-12-netkingdom-security/` receipts
`custody-entrypoints.json`, `sops-make-entrypoints.json`, `rotation-output.json`,
and `admin-bootstrap.json`; October13 `sops-write-callers.json`.
## Repair component, diagnostic, LDAP and resolver callers
```task
id: NK-WP-0049-T05
status: done
priority: high
assignee: codex
depends_on: [NK-WP-0049-T04]
state_hub_task_id: "e8e38f42-609e-5c07-a8e6-e73c8eb4ae95"
```
**Accountable worker:** codex. Service/credential-owner acceptance remains with
the named owner boundary.
Completed 2026-10-13–14. Repaired KeyCape configuration and component Secret
publication, original privacyIDEA key export/restore transport, scoped MFA
counts, LDAP group maintenance and configuration-preserving resolver cutover.
Retired unsafe personal-token mint/refresh and direct LDAP lifecycle shortcuts.
Disposable native PI/LLDAP tests and source regressions passed; estate provider
admission, historical credential validity and service authentication remain T21/T22.
Evidence: Custodian October13 `keycape-caller-reconciliation.json`,
`service-secret-callers.json`, `privacyidea-key-custody.json`,
`mfa-state-diagnostic.json`, `realm-bootstrap-repair.json`; October14
`legacy-lldap-retirement.json`, `platform-group-boundary.json`,
`lldap-group-maintenance.json`, `resolver-cutover.json` under the corresponding
`docs/evidence/YYYY-MM-DD-netkingdom-security/` directories.
## Inventory source callers and retire remaining unsafe delivery paths
```task
id: NK-WP-0049-T06
status: done
priority: high
assignee: codex
depends_on: [NK-WP-0049-T04, NK-WP-0049-T05]
state_hub_task_id: "a5f50891-c5b2-513b-8670-e1915ee52015"
```
**Accountable worker:** codex. Service/credential-owner acceptance remains with
the named owner boundary.
Completed 2026-10-15. Enumerated all 87 current non-test shell/Python callers
in the declared bootstrap/k8s scope plus Make/console/delivery adapters. Repaired
streamed encrypted bundle publication and fail-closed bootstrap/lab paths;
retired unsafe delivery copiers, root prompts, registry patchers and stale
rollback recipes. The attended inventory records source dispositions and their
native-proof limits. T02 separately maps these to current live users/owners.
Evidence: [caller inventory](../docs/evidence/2026-10-15-bootstrap-recovery/caller-source-inventory.json),
[attended procedures](../docs/attended-procedure-inventory.md) and the October15
recovery evidence directory. No live production bootstrap or lab-init acceptance
is claimed.
## Discover off-host coverage and recover the existing PI database
```task
id: NK-WP-0049-T07
status: done
priority: high
assignee: codex
state_hub_task_id: "2d7302a2-319a-5633-bf58-d51ccd25ebf6"
```
**Accountable worker:** codex. Service/credential-owner acceptance remains with
the named owner boundary.
Completed 2026-10-15–16. Bounded Storage Share/Scaleway inventories identified
existing database coverage and the missing full identity bundle. The operator
independently matched the backup recipient, then recovered the exact existing
PI dump: correct-key decryption, wrong-key refusal, isolated native PostgreSQL
restore/readback and cleanup passed (5 tokens, 2 resolvers, 2 realms).
Evidence: October15 recovery `offsite-discovery-summary.json`,
`privacyidea-transfer-r1.json`, `privacyidea-database-drill-r1.json`,
`database-drill-review.json`. This database-only proof led to T08–T14.
## Implement the full identity capture and archive recovery toolchain
```task
id: NK-WP-0049-T08
status: done
priority: high
assignee: codex
depends_on: [NK-WP-0049-T06, NK-WP-0049-T07]
state_hub_task_id: "317ca173-c784-5329-9810-5e3d300604b8"
```
**Accountable worker:** codex. Service/credential-owner acceptance remains with
the named owner boundary.
Completed 2026-10-16. Defined exact LLDAP/Authelia/KeyCape/privacyIDEA assets,
Deployment/PVC/reference bindings, original keys and snapshot consistency limits.
Implemented native SQLite/PG capture, streamed age sealing, strict data-bearing
manifest checks, provider readback, independent retrieval, bounded retention,
root runtime and attended archive verifier. All 56 focused checks passed,
including actual synthetic encrypted/native PostgreSQL recovery.
Evidence: [profile](../tools/bootstrap-recovery/identity-backup-profile.json),
[implementation review](../docs/evidence/2026-10-16-identity-backup-implementation/review.json),
and the live evidence's tested ESO-default/host-binding corrections. Production
archive recovery is separately evidenced in T13.
## Admit and verify the two scoped backup credential deliveries
```task
id: NK-WP-0049-T09
status: done
priority: high
assignee: codex
depends_on: [NK-WP-0049-T08]
state_hub_task_id: "4a47eb2a-7e42-5ae7-9df1-f5ede746eca1"
```
**Accountable worker:** codex. Service/credential-owner acceptance remains with
the named owner boundary.
Completed 2026-10-16. Platform CCR-2026-0048/0049 are verified. The operator
approved folder-only provider-normalized publisher21/retention25 rights, including
re-sharing, in addition to the original rollout. Coding-agent denials precede KV
publication. Exact 900-second single-policy readers, wrong-SA/namespace/audience
rejection, metadata/sibling/old-escrow/token-create denials, ESO equality and
reader/session revocation passed. Operator passwords and age private keys are
not job credentials.
Evidence: live `publisher-setup-verified.json`, `retention-setup-verified.json`,
`publisher-native-delivery-verified.json`, `retention-native-delivery-verified.json`;
approval decisions `40bfae40-6108-4e51-8613-073339f5345f` and
`5edd5203-fae7-461b-8775-5d22b7873971`; Platform CCR files.
## Apply capture isolation and install the guarded node runtime
```task
id: NK-WP-0049-T10
status: done
priority: high
assignee: codex
depends_on: [NK-WP-0049-T08, NK-WP-0049-T09]
state_hub_task_id: "0519444c-d559-5455-aa27-8c5dc8b832b0"
```
**Accountable worker:** codex. Service/credential-owner acceptance remains with
the named owner boundary.
Completed 2026-10-16. Applied exact UID/resource-version/spec-guarded DNS
selector exclusions, preserving ordinary service matches. Capture refuses any
remaining helper allow policy. Installed immutable runtime sources bound to the
actual hostname, machine identity and cluster UID. Disabled-unit replacement
checked previous contents and retained rollback bytes. Health unit validation
passed. Activation remained gated until T13.
Evidence: live `isolation-applied.json`, `capture-ready-plan.json`,
`installed-runtime-packet-r2.json`, `upgrade-disabled-runtime.py`. Actual complete
hardened-job execution is recorded in T14.
## Connect recovery to Railiance bootstrap and reuse discovery
```task
id: NK-WP-0049-T11
status: done
priority: high
assignee: codex
depends_on: [NK-WP-0049-T08]
state_hub_task_id: "d69af493-4026-56a8-a526-c267d90dce18"
```
**Accountable worker:** codex. Service/credential-owner acceptance remains with
the named owner boundary.
Completed 2026-10-16. Added the Railiance bootstrap `identity-recovery`
dispatcher and independent-custody → isolated verification → layer/service-owner
handoff. Updated `capability.security.bootstrap-recovery`, linked bootstrap
sequencing and composed reuse-surface federation. Keyword discovery for identity
backup, restore, privacyIDEA and disaster recovery was verified. The operator
actually used this handoff in T13. No unattended production importer is claimed.
Evidence: [ceremony](../docs/bootstrap-recovery-ceremony.md), owning capability
entry, `railiance-bootstrap/docs/identity-recovery.md`, bootstrap commit `d1dcfde`
and reuse-surface federation commit `662782e`.
## Create and verify the first full off-host identity bundle
```task
id: NK-WP-0049-T12
status: done
priority: high
assignee: codex
depends_on: [NK-WP-0049-T09, NK-WP-0049-T10]
state_hub_task_id: "decb415a-8012-521c-9dbe-d73c6ff0bbfb"
```
**Accountable worker:** codex. Service/credential-owner acceptance remains with
the named owner boundary.
Completed 2026-10-16. Captured original service keys/current data plus the
linked native PI database dump, sealed and published a unique Storage Share
artifact, then downloaded it and verified size/digest before publishing its index.
Helper and plaintext cleanup passed. Artifact:
`net-kingdom-identity-20261016T063159Z-5882771aa2ce.tar.age` (109738344 bytes).
Evidence: live `actual-capture.json`, `actual-offsite.json`; ciphertext SHA256
`7117b3fe1e4840c86b301c94c85e6b16709e5626bb0884bd0580bde630e30e30`.
No age private key was needed by the capture job.
## Independently retrieve and recover the complete actual archive
```task
id: NK-WP-0049-T13
status: done
priority: high
assignee: codex
depends_on: [NK-WP-0049-T11, NK-WP-0049-T12]
state_hub_task_id: "602d4921-cfac-5e5b-9993-4fbf67195c7d"
```
**Accountable worker:** codex. Service/credential-owner acceptance remains with
the named owner boundary.
Completed 2026-10-16. Independently downloaded the exact first generation
through attended Backup-account custody, rehashed it locally and revoked the
session. The operator used the independently held age key through the bootstrap
handoff. Correct/wrong-key cases, all 36 manifest files/hashes, original RSA pairs,
SQLite integrity and isolated native PG16.14 restore/readback passed; plaintext
and containers were removed. No key or restored secret values reached the agent.
Evidence: live `independent-owner-transfer.json`, `actual-full-archive-recovery.json`,
`actual-full-recovery-review.json`. Existing-user/MFA authentication and old audit
signatures are deliberately separate T16–T20 outcomes.
## Activate and prove the complete recurring backup and retention job
```task
id: NK-WP-0049-T14
status: done
priority: high
assignee: codex
depends_on: [NK-WP-0049-T10, NK-WP-0049-T13]
state_hub_task_id: "eaf0708f-b3b7-5531-8172-95edf30c3a19"
```
**Accountable worker:** codex. Service/credential-owner acceptance remains with
the named owner boundary.
Completed 2026-10-16 after T13. Both daily backup/retention and hourly health
timers are enabled/active. The actual hardened systemd service passed retention
before/after a second capture/upload/download (Result=success, ExecMainStatus=0).
Both verified generations are retained, the independently recovered point is
pinned, and nothing was deleted. Helpers/plaintext were cleaned; failure markers
and reporting outbox are empty. Health check succeeded.
Evidence: live `activation-result.json`, `activation-review.json`,
`recurring-job-review.json`. Cadence: daily 03:45 UTC plus up to 5 minutes jitter,
hourly health, 25-hour freshness bound. Second-provider/host-loss gaps go to T24;
this completed task is not a full-estate recovery claim.
## Map current callers, owners and native acceptance gaps
```task
id: NK-WP-0049-T02
status: progress
priority: high
assignee: codex
depends_on: [NK-WP-0049-T06]
state_hub_task_id: "479d6970-6516-50f5-9436-bb5b56d58aad"
```
**Accountable worker:** codex. Service/credential-owner acceptance remains with
the named owner boundary.
The source inventory and known legacy objects exist; a complete current-use
ledger is not yet accepted. Next action is read-only reconciliation of the 93
entries in the latest October15 source inventory (the earlier snapshot had 87) against active Make/bootstrap/operator routes and owner evidence.
**Done when:** each entry is identified as current, retired or owner-deferred,
with its owner, credential route, current consumer(s), native evidence or exact
missing proof, and next responsible task. Explicitly cover the historical
`sso/keycape-pi-token` and `mfa/privacyidea-trigger-admin` objects, lab init,
factor diagnostic delivery, LDAP group/provider boundary, resolver cutover and
root-output/isolation boundaries. Secret presence is not credential validity.
Send current native checks to T21 and historical revocation/rotation decisions to
T22. Do not reopen completed source repairs without a new finding.
## Prepare the isolated full-service restore and acceptance packet
```task
id: NK-WP-0049-T15
status: progress
priority: high
assignee: codex
depends_on: [NK-WP-0049-T08, NK-WP-0049-T13, NK-WP-0049-T27, NK-WP-0049-T28, NK-WP-0049-T29, NK-WP-0049-T30]
state_hub_task_id: "1d1eb903-a4b0-5bc4-a421-0b842d7f68ae"
```
**Accountable worker:** codex. Service/credential-owner acceptance remains with
the named owner boundary.
**Prepared draft scope moved to T27.** See
`docs/identity-service-restore-packet.md`: restore order, containment, safe evidence
and per-service positive/negative checks are drafted. Read-only current runtime
image digests were observed for all four services; correspondence to the selected
archive and independent image availability are not yet proved.
**Next action:** admit a named disposable target/private route, verify and stage
matching images independently, obtain private existing-user/MFA test inputs and
review version-specific native commands with service owners. Incorporate T02's
ledger before submitting current-caller acceptance.
2026-10-16 checkpoint: T28 completed the exact local image save/load/inspect
and T29 prepared and natively tested the bounded LLDAP stage. Its actual owner
run is T16 and can proceed independently of the remaining multi-service packet.
The other service adapters, matching archive/runtime lineage and private
existing-user/MFA inputs are still required; no blanket T15 completion is claimed.
**Done when:** service owners accept the exact targets/procedure, existing-user
and MFA test identities/input custody are available without posting secrets,
outbound notification/provider writes are disabled, and every required new
credential or production effect has its own owner decision. The verifier deletes
its plaintext; it is not a production importer. This task prepares authority and
inputs for T16–T20, not an automatic production restore.
## Prove restored LLDAP directory and existing-user authentication
```task
id: NK-WP-0049-T16
status: done
priority: high
assignee: codex
depends_on: [NK-WP-0049-T13, NK-WP-0049-T29]
state_hub_task_id: "b64d4d6a-e851-58d1-9929-f03343147de0"
```
**Accountable worker:** codex. Service/credential-owner acceptance remains with
the named owner boundary.
Owner boundary: NetKingdom directory/User Engine owners. Restore the captured
original directory/configuration in the admitted isolated environment.
**Done when:** user, group and membership identities are preserved; an independently
supplied existing-user credential authenticates; a wrong credential is refused;
provider writes/outbound effects stay confined; cleanup is verified. Native
fixtures from T05 and SQLite integrity from T13 do not satisfy this current-data
acceptance. Record only safe counts, identities and outcomes.
Completed 2026-10-16 at 18:37 UTC. The operator privately supplied the
independent age key and an existing backup-era account credential to the exact
local executor. All 36 archive hashes and the known ciphertext/source/target
bindings matched. Native existing-user LDAP bind passed; explicit invalid
credentials were rejected. The original seven users, 17 groups, 13 memberships
and the available attribute/schema rows were preserved across service startup,
authentication and graceful shutdown. The container had only loopback/no
external routes; pre-import isolation and container/tmpfs cleanup passed.
No production endpoint was contacted or secret exported to the agent.
Evidence: `actual-isolated-lldap-recovery.json` and
`actual-isolated-lldap-review.json` in
`docs/evidence/2026-10-16-identity-outage-recovery/`. This closes the actual
LLDAP stage independently of the remaining T15 packet. It does not prove
Authelia/KeyCape authentication, retained-token MFA or audit continuity. T17,
T18 and T19 retain their remaining adapter/input prerequisites.
## Prove restored Authelia storage and OIDC continuity
```task
id: NK-WP-0049-T17
status: wait
priority: high
assignee: codex
depends_on: [NK-WP-0049-T15, NK-WP-0049-T16]
blocking_reason: "LLDAP recovery T16 passed; await the exact isolated Authelia adapter/procedure and private authentication-flow inputs from T15."
state_hub_task_id: "b47f9975-d6d7-5bb8-93ce-886deb1dfa70"
```
**Accountable worker:** codex. Service/credential-owner acceptance remains with
the named owner boundary.
Owner boundary: SSO/Authelia and directory owners. Restore the original storage,
encryption/session/signing material and configuration in isolation.
**Done when:** native storage decryption succeeds, issuer/signing identity and
client configuration are preserved, and an isolated authentication flow has
positive/negative results using the restored directory. Disable outbound
notifications and verify cleanup. Archive integrity/RSA parsing alone are not
native Authelia acceptance.
## Prove retained privacyIDEA token decryption and existing-token MFA
```task
id: NK-WP-0049-T19
status: wait
priority: high
assignee: codex
depends_on: [NK-WP-0049-T15, NK-WP-0049-T16, NK-WP-0049-T31]
blocking_reason: "LLDAP T16, token-material T31 and audit T20 passed; await the exact isolated MFA adapter and owner-selected retained-token/factor inputs."
state_hub_task_id: "6f8af1a7-2a3f-5fa8-8202-83103ebd55b2"
```
**Accountable worker:** codex. Service/credential-owner acceptance remains with
the named owner boundary.
Owner boundary: MFA/privacyIDEA and directory owners. Use the exact linked dump
and original enckey from the independently recovered generation.
**Done when:** retained token material decrypts inside the isolated process,
a selected existing token completes a native MFA check, a wrong key/code is
refused, and cleanup is verified. Obtain any token/user input through attended
private custody; never emit token plaintext, seeds or credentials. Native database
row counts and disposable synthetic-token fixtures do not prove this outcome.
2026-10-16 refinement: native token-material readback is separately tracked in
T31 using the prepared T30 executor. T19 retains actual existing-token MFA and
wrong-code/wrong-key authentication refusal. Internal decryption or padding
success does not authenticate a user. No MFA code is needed for T20/T31; the
selected user's private factor input and exact isolated MFA adapter remain T19.
## Prove historical and new privacyIDEA audit-signature continuity
```task
id: NK-WP-0049-T20
status: done
priority: high
assignee: codex
depends_on: [NK-WP-0049-T13, NK-WP-0049-T30]
state_hub_task_id: "375fa8f9-b8a5-5048-97a7-ec141aa7c45d"
```
**Accountable worker:** codex. Service/credential-owner acceptance remains with
the named owner boundary.
Owner boundary: MFA/privacyIDEA audit owner. Use the original recovered audit
pair and linked database in the admitted isolated target.
**Done when:** existing pre-backup audit signatures are verified natively, a new
isolated audit entry is signed and verified with the preserved identity, a wrong
key fails, and cleanup is recorded. Report any historical verification failure
for explicit owner disposition; do not regenerate keys to hide it. Matching RSA
pairs from T13 are a prerequisite, not audit-history verification.
Completed 2026-10-16 at 19:10 UTC using the actual independently retrieved
bundle and original audit identity on the fixed isolated local engine. All
279,209 stored rows were signed and verified; zero unsigned, invalid or legacy
signatures. A new isolated entry verified, and tampering/unrelated-public-key
checks were refused. Container/plaintext cleanup passed. Receipt and reviewed
source/ciphertext/target bindings: `actual-isolated-privacyidea-recovery.json`
and `actual-isolated-privacyidea-review.json` in
`docs/evidence/2026-10-16-identity-outage-recovery/`.
Stored-row continuity is accepted; event completeness and existing-token MFA
are separate and are not claimed.
## Prove restored KeyCape issuer, policy and consumer continuity
```task
id: NK-WP-0049-T18
status: wait
priority: high
assignee: codex
depends_on: [NK-WP-0049-T15, NK-WP-0049-T16, NK-WP-0049-T19]
blocking_reason: "LLDAP recovery T16 passed; await the exact isolated KeyCape adapter/procedure and retained-token MFA proof for the selected flow."
state_hub_task_id: "141dd773-c1fa-56c1-a852-bbe2372e5125"
```
**Accountable worker:** codex. Service/credential-owner acceptance remains with
the named owner boundary.
Owner boundary: KeyCape and the selected current consumer owners. Restore the
original signing/configuration, registry, authentication policies and receipts.
**Done when:** issuer/JWKS signing identity, registry/client configuration and
policy/receipt continuity are preserved; selected current consumers pass native
positive/negative authentication using the restored dependencies; and isolation
and cleanup hold. Confirm the actual dependency graph in T15. Do not route a
production consumer to the isolated issuer or mint new client grants implicitly.
## Verify current caller and provider admission at their actual boundaries
```task
id: NK-WP-0049-T21
status: wait
priority: high
assignee: codex
depends_on: [NK-WP-0049-T02]
blocking_reason: "Await the current-caller ledger and exact per-caller owner admission/test inputs; the backup credential grants do not admit unrelated callers."
state_hub_task_id: "77aa42be-7a39-5d93-8042-03d78b4094b4"
```
**Accountable worker:** codex. Service/credential-owner acceptance remains with
the named owner boundary.
Owner boundary: each current caller's service/provider and credential owner.
Use T02's ledger to select the exact current procedures and required owner lanes.
**Done when:** each current caller has scoped delivery and native positive/negative
acceptance or an explicit accepted retirement/disposition. Include dedicated
factor-diagnostic admission (do not borrow KeyCape's reader), deployed reserved
LDAP group denial, resolver/cutover input provenance and functional proof, and
bootstrap/root-output/isolation callers. Fixtures, an existing tty and historical
bootstrap receipts are not current authority. Propose exact write/cutover actions
before execution; record deferred actions separately rather than faking a pass.
## Resolve historical credential and configuration-preserving rotation dispositions
```task
id: NK-WP-0049-T22
status: wait
priority: high
assignee: codex
depends_on: [NK-WP-0049-T02, NK-WP-0049-T21]
blocking_reason: "Await the caller/consumer ledger, current replacement-lane acceptance and explicit credential-owner dispositions; existing exposure-rotation deferral is unchanged."
state_hub_task_id: "3ea6bf00-a87f-58ea-bdce-328c13a03a55"
```
**Accountable worker:** codex. Service/credential-owner acceptance remains with
the named owner boundary.
Owner boundary: Platform/custody and affected consumer owners. Review historical
personal-admin/trigger credential copies and unsafe predecessor paths against
known current consumers and rollback custody.
**Done when:** every affected credential has an owner-accepted disposition:
verified revocation/replacement, continued bounded use with acceptance, or an
explicit linked deferral. Signing/client-secret changes require preservation and
native multi-consumer proof if actually approved. CUST-WP-0073-T05's founder
rotation deferral and decision `1591daa1-6e85-4dfd-aadd-5a9fe1ef47fa` remain in force;
record their exact applicability rather than automatically rotating or treating
source retirement as provider revocation.
## Provide retrieval that works when OpenBao and SSO are unavailable
```task
id: NK-WP-0049-T23
status: done
priority: high
assignee: codex
depends_on: [NK-WP-0049-T13, NK-WP-0049-T25]
state_hub_task_id: "1ec537ee-dfee-5576-be36-a81bdba417d1"
```
**Accountable worker:** codex. Service/credential-owner acceptance remains with
the named owner boundary.
Completed 2026-10-16 at 18:02 UTC. The operator used independently held Backup
account access in the private prompt. The actual protected 109,738,344-byte
ciphertext was downloaded with unchanged ETag/size/SHA256, with no OpenBao/SSO
contact or provider writes by the retrieval path. The human-held age key matched
the registered recipient; wrong-key refusal, all 36 manifest hashes, SQLite
integrity, original key pairs and native isolated PostgreSQL 16.14 restore passed.
The database retained five token, two resolver and two realm rows. Disposable
container and temporary plaintext cleanup passed. The agent received no secret.
Evidence: `actual-offline-transfer.json`, `actual-offline-recovery.json` and
`actual-offline-review.json` under
`docs/evidence/2026-10-16-identity-outage-recovery/`. The review rechecked the
local ciphertext digest and verifier source. This proves the independent path
without requiring a deliberate production outage. Service authentication,
retained-token decryption and historical audit verification remain T16–T20.
T26 separately records accepted guide publication.
## Record owner disposition for second-provider and external monitoring gaps
```task
id: NK-WP-0049-T24
status: todo
priority: medium
assignee: codex
depends_on: [NK-WP-0049-T14]
state_hub_task_id: "474f5a78-a643-59c6-bcf9-fc4a908dfcb0"
```
**Accountable worker:** codex. Service/credential-owner acceptance remains with
the named owner boundary.
**Next action:** record the current single-provider and host-local-health limits,
then obtain explicit owner scope decisions for a second encrypted provider copy,
external host-loss/freshness detection and recurring recovery-drill cadence.
**Done when:** each gap has either an accepted bounded residual risk or a linked
live intake/workplan with owner and trigger. Existing daily RPO/25-hour health
settings remain the approved baseline. Scaleway full-identity publication and
an external watchdog are not implemented; do not add their grants/jobs implicitly.
This task triages the new insights instead of silently expanding NK49's required
implementation to an entire disaster-recovery programme.
## Deliver and verify the offline retrieval adapter and recovery tools kit
```task
id: NK-WP-0049-T25
status: done
priority: high
assignee: codex
depends_on: [NK-WP-0049-T13]
state_hub_task_id: "1c5ea7a1-edf7-5072-b36b-8be7f827957a"
```
**Accountable worker:** codex.
Completed 2026-10-16. `retrieve-identity-offline.py` uses privately entered
independent Backup-account access or an exact read-capable identity-folder share.
It performs only fixed-provider HEAD/GET, binds the reviewed generation by strong
ETag/length/SHA256, refuses redirects/proxies/unreviewed scope and removes failed
downloads. No OpenBao/SSO, age key or standing agent credential is required.
`recovery-kit.py` builds a reproducible allowlisted ZIP containing the tools,
public profile, known-good ciphertext receipt and recovery guides, with file
checksums. Its publication path creates only three versioned guide/ZIP/checksum
objects and verifies readback without overwriting existing conflicting objects.
`run-offline-recovery.sh` provides the owner's two-hidden-prompt handoff.
Evidence: `tests/test_identity_offline_recovery.py` and the bounded test/build
receipt in `docs/evidence/2026-10-16-identity-outage-recovery/`. The focused run
passed 65 tests including real disposable age/PostgreSQL fixtures. ZIP-contained
entry points import without sibling repositories. This is source/fixture proof,
not actual independent-provider retrieval (T23) or publication (T26).
2026-10-16 correction: the first owner handoff stopped before its credential
prompt because Python text `r+` requires a seekable stream. Reproduced against a
real controlling PTY; replaced both prompts with the shared hidden-input helper,
refusing redirected input and echo fallback. Added real-terminal and both-CLI
regressions plus closed stage/HTTP-code diagnostics (no raw responses). The
focused rerun passed 36 tests. Evidence: `terminal-prompt-fix.json` in the outage
evidence directory. This source correction does not complete T23/T26.
## Publish and verify the disaster recovery guide on Storage Share
```task
id: NK-WP-0049-T26
status: done
priority: high
assignee: codex
depends_on: [NK-WP-0049-T25, NK-WP-0049-T27]
state_hub_task_id: "94d054fe-e5d8-5ddf-8e48-80714123d184"
```
**Accountable worker:** codex; operator holds provider access privately.
Requested by the operator on 2026-10-16; decision
`fc3ef8db-0a33-41d5-a0ba-d13169c5f884` records the bounded scope. Guide source:
`docs/disaster-recovery-storage-share.md`. Document scope, independently held
provider/key requirements, exact known-good fallback, copy/download verification,
archive-check commands, service restore order, recurring schedule and remaining
coverage limits. Include the offline tools so failed Forgejo does not prevent
access to the recovery scripts; retain an independent copy and trusted digest.
Completed 2026-10-16 at 18:01 UTC through the private independent-provider
owner handoff. All three objects were conditionally created and read back with
matching strong ETag, length and content; the recorded digests match a fresh
reproducible local build. No existing object or provider permission was changed.
The failed first attempt and terminal correction remain documented in T25.
**Published location:** Storage Share account Backup,
`railiance-backups/net-kingdom-identity/`:
- `net-kingdom-recovery-57d1940947aa25ff-START-HERE.md`
- `net-kingdom-recovery-57d1940947aa25ff.zip`
- `net-kingdom-recovery-57d1940947aa25ff.zip.sha256`
**Trusted ZIP SHA256:**
`57d1940947aa25ff83d0f25ddf30090eb40595cccaab64225de882a2fd59ea2f`.
Evidence: `actual-offline-publication.json` and `actual-offline-review.json` in
`docs/evidence/2026-10-16-identity-outage-recovery/`. The published guide is the
exact pre-drill revision; this acceptance record records the later successful
drill without implying an unperformed republication. Keep an independent copy
and trusted digest outside the failed service/provider dependency.
## Draft the isolated service restore packet and observe candidate images
```task
id: NK-WP-0049-T27
status: done
priority: high
assignee: codex
depends_on: [NK-WP-0049-T08, NK-WP-0049-T13]
state_hub_task_id: "e273e803-bee7-5d90-bc51-ecc08c0c8eff"
```
**Accountable worker:** codex.
Completed 2026-10-16. Draft in `docs/identity-service-restore-packet.md` names the
known recovered input, restore order, original key/data ownership, default-deny
containment, blocked production/provider/notification effects, cleanup and five
native acceptance lanes with positive/negative checks. Read-only metadata from
the pinned primary cluster resolved all four current service image digests,
including the separately observed privacyIDEA main container and LLDAP's mutable
source tag. Evidence: `docs/evidence/2026-10-16-identity-outage-recovery/image-observation.json`.
This task extracts completed preparation from T15. Named target admission,
archive/runtime image-lineage matching and independent image availability,
private existing-user/MFA inputs and owner-reviewed native commands remain T15;
no service restore, production change or owner acceptance is claimed.
## Stage and verify the five exact service images for local offline recovery
```task
id: NK-WP-0049-T28
status: done
priority: high
assignee: codex
depends_on: [NK-WP-0049-T27]
state_hub_task_id: "4829b091-76d2-5b10-bdfc-032800190c0a"
```
**Accountable worker:** codex.
Completed 2026-10-16. All five pinned references (PostgreSQL, LLDAP, Authelia,
KeyCape, privacyIDEA) were inspected on the fixed local Linux Docker engine
`bnt-lap001`, ID `74f93783-6cef-43a8-a3c4-f62586cd3bc1`. Missing LLDAP and
privacyIDEA images were pulled only by their observed immutable digests. Saved a
private 347,521,536-byte image archive, loaded it and re-inspected every exact
reference. Evidence and archive SHA256/location:
`docs/evidence/2026-10-16-identity-outage-recovery/offline-service-images.json`.
The local drill no longer requires the production registry. This is not off-host
image escrow or proof of historical mutable-tag/backup lineage; those limits
remain in T15. No service custody or age key was supplied to image caching.
## Prepare and verify the bounded native LLDAP recovery executor
```task
id: NK-WP-0049-T29
status: done
priority: high
assignee: codex
depends_on: [NK-WP-0049-T13, NK-WP-0049-T27, NK-WP-0049-T28]
state_hub_task_id: "2f8c73b0-1cdd-5b22-99b6-017ccde4454b"
```
**Accountable worker:** codex; operator retains actual private recovery inputs.
Completed 2026-10-16. `isolated-lldap.py` and `run-isolated-lldap.sh` provide the
exact owner-run stage in `docs/identity-service-restore-packet.md`: fixed local
engine/images, no remote Docker endpoint, original key/seed/JWT/directory data,
Linux-tmpfs decryption, memory/stdin-only private inputs, network-none/read-only
containers, isolation inspection before import, actual existing-user bind,
explicit invalid-credential denial, stable directory rows and cleanup checks.
Missing original server keys are refused; reset/replacement/notification paths
are disabled. No live Kubernetes/OpenBao/SSO path or provider grant is used.
Evidence: `tests/test_isolated_lldap_recovery.py` exercises the actual pinned
image, disposable retained-account directory and a full synthetic encrypted
bundle through the same native executor. The private real-bundle run and its
acceptance remain T16; fixture success is not actual user/MFA/audit recovery.
See `isolated-lldap-preparation.json` in the October16 outage evidence directory
for exact test/source/target provenance and limitations.
## Prepare the bounded native privacyIDEA token-material and audit executor
```task
id: NK-WP-0049-T30
status: done
priority: high
assignee: codex
depends_on: [NK-WP-0049-T13, NK-WP-0049-T28]
state_hub_task_id: "6e4a43db-9be2-5662-bdbf-1595fce03052"
```
**Accountable worker:** codex; operator retains private recovery inputs.
Completed 2026-10-16. `isolated-privacyidea.py` and its owner launcher use the
fixed local Docker target and exact cached PostgreSQL/privacyIDEA3.12.2 images.
The linked dump, original enckey/audit pair and required secret fields are
restored only into a loopback-only, nonroot, read-only, capability-free,
no-host-mount/port, tmpfs-backed environment with inspected pre-import isolation.
Decrypted host staging is tmpfs; normal cleanup removes all private data and
owned containers, with a bounded holding lifetime if the parent is interrupted.
The native provider decrypts token-key material internally, checks every stored
audit row, adds one isolated signed entry and checks wrong-public-key/tampering
negatives. Original policy for legacy signature verification is retained only
from literal archived configuration; archived Python is never executed. Invalid
or unsigned history remains reported, not repaired or sampled away. Native
material readback is not existing-token MFA or proof of event completeness.
Evidence: `tests/test_isolated_privacyidea_recovery.py` and
`isolated-privacyidea-preparation.json` in the October16 outage evidence folder.
Synthetic pinned-image tests exercise valid/tampered/unsigned history, malformed
keys, encrypted archive to native restore, isolation and cleanup. Real original
material/audit acceptance remains T31/T20; actual MFA remains T19.
## Verify retained privacyIDEA token material inside the isolated provider
```task
id: NK-WP-0049-T31
status: done
priority: high
assignee: codex
depends_on: [NK-WP-0049-T13, NK-WP-0049-T30]
state_hub_task_id: "26383841-8076-5c53-bbd1-9db64e32544a"
```
**Accountable worker:** codex; operator supplies only the independently held age
key in the hidden helper. This task extracts actual material-readback scope from
T19, preserving T19's existing-token MFA acceptance gate and historical identity.
Completed 2026-10-16 at 19:10 UTC in the same actual run as T20. All five
retained token materials were decrypted internally with the original enckey;
an unrelated key could not reproduce any of the five. All 36 manifest hashes,
exact executor/ciphertext/target bindings, isolation and cleanup passed. Safe
receipts are the `actual-isolated-privacyidea-{recovery,review}.json` files
referenced under T20. No retained-factor authentication is claimed.
**Done when:** exact ciphertext/source/target/manifest bindings pass; the native
provider reads every nonempty encrypted token-key field using the original
enckey without exporting material; an unrelated key cannot reproduce that
material; aggregate counts and container/plaintext cleanup are recorded.
Only bounded internal readback is accepted here. Padding/readback success does
not prove the recovered secret authenticates a retained factor; actual positive
MFA and wrong-key/wrong-code authentication refusal remain T19. No token seed,
plaintext, serial or user record belongs in the receipt, Git, State Hub or chat.
## Accept remaining recovery and caller evidence and close the G08 handoff
```task
id: NK-WP-0049-T03
status: wait
priority: high
assignee: codex
depends_on: [NK-WP-0049-T02, NK-WP-0049-T16, NK-WP-0049-T17, NK-WP-0049-T18, NK-WP-0049-T19, NK-WP-0049-T20, NK-WP-0049-T21, NK-WP-0049-T22, NK-WP-0049-T23, NK-WP-0049-T24, NK-WP-0049-T26]
blocking_reason: "Await the separately tracked native service proofs, current/legacy caller dispositions and residual-scope decisions; archive recovery, backup activation, independent outage retrieval and guide publication are complete."
state_hub_task_id: "d68cd4d3-1ba3-530e-88f9-26ebb8dd6eac"
```
**Accountable worker:** codex. Service/credential-owner acceptance remains with
the named owner boundary.
Owner boundary: NetKingdom, Platform and affected service owners, with Custodian
G08 receiving the final evidence. The completed archive/job milestones remain
accepted independently of this final gate.
**Done when:** T02 and T15–T31 have their stated completed or explicitly accepted
handoff outcomes; service authentication/token/audit proof and current/legacy
caller dispositions are accepted; the source workplan and projected tasks agree;
and G08 has an exact evidence/handoff record. Record actionable residuals as live
work records before workplan closure. Preserve historical IDs and receipts; do
not call a deferred rotation or an untested full-estate restore complete.
## Evidence locations and refinement history
“Live” above means `docs/evidence/2026-10-16-identity-backup-live/` in NetKingdom.
Implementation preparation is in the October16 implementation directory; earlier
backup discovery/database recovery is in `docs/evidence/2026-10-15-bootstrap-recovery/`.
The source-repair receipts explicitly labelled Custodian belong to that owning
repository. Use the [recovery ceremony](../docs/bootstrap-recovery-ceremony.md)
for exact procedure and approval history. No secret values belong in task evidence.
Refinement rule: substantial work and newly understood remaining scope must be
visible in task blocks at a checkpoint, not only in progress prose. This replan
preserves T01–T03 identity and original evidence while making the completed
milestones and each remaining acceptance gate separately manageable.
Managed identifier assignment also consumed a preexisting staged generated brief;
the follow-up commit restores its prior published baseline while preserving the
operator's working brief and original staged version. This is a projection-tool
side effect, not new recovery evidence.

View file

@ -0,0 +1,79 @@
--- a/workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md
+++ b/workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md
@@ -559,8 +559,8 @@
status: wait
priority: high
assignee: codex
-depends_on: [NK-WP-0049-T15, NK-WP-0049-T16, NK-WP-0049-T19]
-blocking_reason: "LLDAP recovery T16 passed; await the exact isolated KeyCape adapter/procedure and retained-token MFA proof for the selected flow."
+depends_on: [NK-WP-0049-T15, NK-WP-0049-T16, NK-WP-0049-T19, NK-WP-0049-T32, NK-WP-0049-T33]
+blocking_reason: "Source and synthetic KeyCape lifecycle proof are delivered in T32; await exact owner-held recovery attempt preparation T33, selected consumer acceptance and retained-token MFA for the selected flow."
state_hub_task_id: "141dd773-c1fa-56c1-a852-bbe2372e5125"
```
@@ -910,6 +910,65 @@
MFA and wrong-key/wrong-code authentication refusal remain T19. No token seed,
plaintext, serial or user record belongs in the receipt, Git, State Hub or chat.
+## Deliver isolated KeyCape restore source and synthetic lifecycle proof
+
+```task
+id: NK-WP-0049-T32
+status: done
+priority: high
+assignee: codex
+```
+
+Completed 2026-10-16. This extracts bounded source/fixture delivery from T18;
+T18 retains actual original archive, issuer and selected consumer acceptance.
+The adapter reuses the existing transfer/archive/custody contracts, binds the
+fixed local daemon and cached images, restores private state only to temporary
+memory, verifies original issuer/TLS/signing/configuration/policy bytes and runs
+the original native service-client checker with wrong-secret/excess-scope denial.
+Durable pre-import obligations and exact parent pidfd/session quiescence precede
+loss/deadline cleanup. Daemon response uncertainty remains uncertainty after
+resource absence; it never authorizes recovery success or replay.
+
+Frozen R6 fixes a demonstrated image-declared anonymous-volume incompatibility
+using bounded helper-only tmpfs, preserving anonymous disk-volume refusal. Safe
+phase markers carry no private diagnostics. Independently passed 72 focused
+checks, then actual pinned-image synthetic positive, hard-parent-loss, deadline
+and nonzero-after-accept cases. Independent fixed-daemon inventories and host
+mount-namespace checks prove all exact resources and plaintext absent. Reaper
+outcomes are retained; initial sandbox receipt lookup failure is preserved and
+superseded by device-bound host verification. R1–R5 failures remain retained.
+
+Evidence: Custodian docs/changes/netkingdom-target-2026-10-16/
+identity-restore-adapter-next/revisions/r6/, actual-fixture-r6-all.json and
+coordinator-r6-all-review.json. Exact source/dependency hashes and candidate
+manifest bind the proof. This uses generated test keys only; it proves neither
+actual owner-held recovery, human MFA, downstream reconnect nor native authority.
+
+## Prepare the exact owner-held KeyCape recovery attempt
+
+```task
+id: NK-WP-0049-T33
+status: todo
+priority: high
+assignee: codex
+depends_on: [NK-WP-0049-T32]
+```
+
+**Accountable worker:** NetKingdom codex with KeyCape and selected consumer owners.
+Prepare the exact archive/manifest, original issuer/TLS/trust and executable
+bindings, selected current registration and dependency graph using public metadata.
+Use the existing independently held archive-key and owner-controlled TLS custody
+paths; never put secret values in the packet. Validate the attended launcher and
+its full dependency closure, execution deadline, recovery obligations and safe
+receipt path before asking for any owner-held input. Bind approved source revision
+and rollback/abort limits through the existing owner process.
+
+**Done when:** reviewed executable command and metadata-only admission packet
+are ready for the existing attended custody path; prerequisites and independent
+receipt verification are explicit. T18 still owns actual execution, original
+consumer continuity and acceptance. T19 retains retained-factor MFA. This task
+neither authorizes a production consumer redirect nor creates a new client grant.
+
## Accept remaining recovery and caller evidence and close the G08 handoff
```task

View file

@ -0,0 +1,5 @@
{
"exit_code": 0,
"stdout": "[main 878abc9] docs(recovery): retain isolated source publication and task bindings\n 1 file changed, 13 insertions(+)\n",
"stderr": "{\"blocked_blob_ids\": [], \"scanned_blobs\": 1, \"scanner_sha256\": \"88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\", \"scanner_version\": \"8.30.1\", \"scope\": \"changed-staged-blobs\", \"status\": \"clean\"}\n"
}

View file

@ -0,0 +1,100 @@
<!-- custodian-brief: generated by fix-consistency — do not edit manually -->
# Custodian Brief — net-kingdom
**Domain:** infotech
**Last synced:** 2026-10-15 17:24 UTC
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
## Active Workstreams
### Implement deterministic posture and evidence feedback
Progress: 4/5 done | workplan_id: `9d7b04f9-3803-5613-b7a5-8bd606c77f5a`
**Open tasks:**
- ! Obtain audit-core freshness adoption `7d2029d2`
*(wait: audit-core tenancy.yaml must publish structured authoritative owner, observation/expiry and replacement-action metadata for WH-ENG-20260822-AUDIT-E2-03. Re-evaluate with explicit current as-of; later recovery prose does not renew E2.)*
### Publish the NetKingdom emission-cadence security profile
Progress: 4/5 done | workplan_id: `04685f94-1991-5e62-80d2-5669913e99fc`
**Open tasks:**
- ! Bind and hand off the published contract `e3fbc8b8`
*(wait: Approval Engine and Qonto owner declarations now pass the published schema and supplied-inventory profile; local-identity incompatibility is retained. Requires actual sender cadence mappings, lifecycle delivery/readback/reconciliation and Kings Guard operational feed evidence.)*
### Reconcile reef placement and security-zone canon dependencies
Progress: 3/6 done | workplan_id: `965ad365-6b81-50a1-a2a3-2d0c1fcce0b4`
**Open tasks:**
- ! T02 — Extend provider declarations to reefs `5b35b646`
*(wait: Requires repo-manager acceptance of the reef provider carrier/schema, then railiance-infra authored ceilings, conditions and evidence in the authoritative reef declaration.)*
- ! T03 — Reconcile reef ceilings mechanically `e3f0bb0f`
*(wait: Mechanical join of reef ceilings to consumer V declarations waits on T02.)*
- ! T06 — Resolve the `DataClassification` mismatch `bf8b3e1d`
*(wait: Requires InfoTechCanon ruling on public disclosure classification versus synthetic provenance, then ops-warden authoritative dataclass_floor mapping and tests; no public-to-synthetic alias.)*
### Remove bootstrap secret output and verify separated recovery custody
Progress: 1/3 done | workplan_id: `17d315f0-829e-57c7-85cc-3a7914152804`
**Open tasks:**
- ! Verify owner-approved recovery and current native callers `d68cd4d3`
*(wait: Source caller reconciliation and exact owner-scoped isolated recovery ceremony are required; no live bootstrap or root retrieval is authorized by this source receipt.)*
- ► Reconcile other legacy custody callers and root-output boundaries `479d6970`
### Enterprise Federation & SAML — Expanded-Mode Keycloak Identity Broker
Progress: 1/9 done | workplan_id: `1075448f-d533-5f9e-94b7-c3adfe151a07`
**Open tasks:**
- ! Admit the database consumer `2fe6f100`
*(wait: ADR-0009 adoption trigger absent; then railiance-platform/rapp-postgres must admit the database consumer and return backup/restore evidence.)*
- ! Package the broker deployment `32d1411b`
*(wait: ADR-0009 adoption trigger and named package/runtime owner absent; requires T02 database admission and governed OpenBao/ESO role plus immutable issuer assignment.)*
- ! Integrate an upstream identity provider `6697a994`
*(wait: Named tenant/upstream IdP owner/federation need absent; requires admitted broker and owner-reviewed tenant-scoped claim mappings plus actual login evidence.)*
- ! Define federated assurance `d845380d`
*(wait: Adoption trigger absent; requires per-IdP assurance trust mapping and reviewed privileged step-up delivery from issuer/package and access-engine owners.)*
- ! Verify IAM conformance and coexistence `9cb7fd93`
*(wait: Requires admitted live broker, canonical tenant issuer selection and dedicated test-client conformance/coexistence evidence; adoption trigger absent.)*
- ! Enforce tenant and platform boundaries `38998c68`
*(wait: Requires tenant-engine tier/issuer ownership and access-engine boundary/freshness enforcement evidence after adoption trigger and broker admission.)*
- ! Prove recovery and audit delivery `e5f44ddc`
*(wait: Requires admitted database/broker, named recovery and audit owners, off-host restore and observed audit/break-glass receipts; adoption trigger absent.)*
- … and 1 more open tasks
### Establish SecurityCaseTypes so that known security cases are light
Progress: 7/9 done | workplan_id: `e31f3f9d-0748-5137-b0a3-7b3ca46a66eb`
**Open tasks:**
- ! T05 — Acknowledge the type through informed-decision `d9b26bcf`
*(wait: Type1.2 architecture has a confirmed source-only founder acknowledgment; canonical1.1 remains unchanged/default and the1.2 reference case stays proposed. Needs explicit version selection/reference-source integration without normal admission bypass, closed owner adapters plus global/native receipt verifier (WARDEN-WP-0044-T03), independently verified six-object structural materialization and CCR42 claims/created-only provenance, exact broker/case/source reconciliation, then separately authorized native mint/delivery and attended registry/consumer verification. Source acknowledgment, a bundle or local tests do not prove implemented/materialized/live status.)*
- ! T06 — First KnownSecurityCase: Forgejo `c3c9c3ae`
*(wait: The next types follow once the first one is proven end to end.)*
### Register Playwright human and agent consumers through governed identity delivery
Progress: 1/3 done | workplan_id: `06ba98d5-609e-5aaa-9bf7-4c4f4460e377`
**Open tasks:**
- ! Verify issuer and real consumer acceptance `5a3aaf57`
*(wait: Await owner-scoped registry/custody activation, native cluster authentication and individual transport, then real human and agent acceptance under CUST-WP-0080-T05.)*
- ► Deliver custody integration and guarded registration activation `a9dadd07`
### Define an execution-attribution receipt for Railiance runs
Progress: 1/2 done | workplan_id: `e2533f3a-aa43-59b3-bff3-8e64b6149487`
**Open tasks:**
- ! Agree the evidence holder and schema with audit-core and Railiance `963120c1`
*(wait: Audit-core accepted evidence custody and published field/admission requirements in AUDIT-WP-0014-T01. Still needs Railiance owner confirmation of exact emitter and wire binding before canonical schema/validator implementation, followed by actual emission/readback and attribution/clock/unknown-case acceptance.)*
### Cut over NetKingdom identity to railiance01 and retire CoulombCore
Progress: 7/8 done | workplan_id: `d76ddccc-00c8-548a-b141-2cd660fa38da`
**Open tasks:**
- ! T08 - Final deletion and closure `42a3b4c0`
*(wait: Founder 2026-10-11: coulombcore is retired and the machine reused (D4 batch approved). Order: host-side inventory (containers, on-disk manifests; no kube API, certs expired 2026-08-31), then stop k3s for good and wipe for reuse. Executes after Sol's passing railiance01 restore/restart drill receipt. Never rotate certs or restart k3s there first.)*
---
## MCP Orientation (when available)
If the state-hub MCP server is reachable, call:
`get_domain_summary("infotech")`
This provides richer cross-domain context.
If the MCP call fails, use this file as your orientation source.

View file

@ -0,0 +1 @@
100644 ea9ccd0b2bd2cc1ec89723b35c44c53f0d79ba98 0 .custodian-brief.md

View file

@ -0,0 +1,85 @@
<!-- custodian-brief: generated by fix-consistency — do not edit manually -->
# Custodian Brief — net-kingdom
**Domain:** infotech
**Last synced:** 2026-10-16 19:19 UTC
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
## Active Workstreams
### Publish the NetKingdom emission-cadence security profile
Progress: 5/6 done | workplan_id: `04685f94-1991-5e62-80d2-5669913e99fc`
**Open tasks:**
- ► Bind and hand off the published contract `e3fbc8b8`
### Register Playwright human and agent consumers through governed identity delivery
Progress: 1/3 done | workplan_id: `06ba98d5-609e-5aaa-9bf7-4c4f4460e377`
**Open tasks:**
- ! Verify issuer and real consumer acceptance `5a3aaf57`
*(wait: Await owner-scoped registry/custody activation, native cluster authentication and individual transport, then real human and agent acceptance under CUST-WP-0080-T05.)*
- ► Deliver custody integration and guarded registration activation `a9dadd07`
### Remove bootstrap secret output and verify separated recovery custody
Progress: 22/31 done | workplan_id: `17d315f0-829e-57c7-85cc-3a7914152804`
**Open tasks:**
- ! Accept remaining recovery and caller evidence and close the G08 handoff `d68cd4d3`
*(wait: Await the separately tracked native service proofs, current/legacy caller dispositions and residual-scope decisions; archive recovery, backup activation, independent outage retrieval and guide publication are complete.)*
- ! Resolve historical credential and configuration-preserving rotation dispositions `3ea6bf00`
*(wait: Await the caller/consumer ledger, current replacement-lane acceptance and explicit credential-owner dispositions; existing exposure-rotation deferral is unchanged.)*
- ! Verify current caller and provider admission at their actual boundaries `77aa42be`
*(wait: Await the current-caller ledger and exact per-caller owner admission/test inputs; the backup credential grants do not admit unrelated callers.)*
- ! Prove retained privacyIDEA token decryption and existing-token MFA `6f8af1a7`
*(wait: LLDAP T16, token-material T31 and audit T20 passed; await the exact isolated MFA adapter and owner-selected retained-token/factor inputs.)*
- ! Prove restored KeyCape issuer, policy and consumer continuity `141dd773`
*(wait: LLDAP recovery T16 passed; await the exact isolated KeyCape adapter/procedure and retained-token MFA proof for the selected flow.)*
- ! Prove restored Authelia storage and OIDC continuity `b47f9975`
*(wait: LLDAP recovery T16 passed; await the exact isolated Authelia adapter/procedure and private authentication-flow inputs from T15.)*
- ► Map current callers, owners and native acceptance gaps `479d6970`
- … and 2 more open tasks
### Establish SecurityCaseTypes so that known security cases are light
Progress: 7/9 done | workplan_id: `e31f3f9d-0748-5137-b0a3-7b3ca46a66eb`
**Open tasks:**
- ! T05 — Acknowledge the type through informed-decision `d9b26bcf`
*(wait: Type1.2 architecture has a confirmed source-only founder acknowledgment; canonical1.1 remains unchanged/default and the1.2 reference case stays proposed. Needs explicit version selection/reference-source integration without normal admission bypass, closed owner adapters plus global/native receipt verifier (WARDEN-WP-0044-T03), independently verified six-object structural materialization and CCR42 claims/created-only provenance, exact broker/case/source reconciliation, then separately authorized native mint/delivery and attended registry/consumer verification. Source acknowledgment, a bundle or local tests do not prove implemented/materialized/live status.)*
- ! T06 — First KnownSecurityCase: Forgejo `c3c9c3ae`
*(wait: The next types follow once the first one is proven end to end.)*
### Define an execution-attribution receipt for Railiance runs
Progress: 1/2 done | workplan_id: `e2533f3a-aa43-59b3-bff3-8e64b6149487`
**Open tasks:**
- ! Agree the evidence holder and schema with audit-core and Railiance `963120c1`
*(wait: Audit-core accepted evidence custody and published field/admission requirements in AUDIT-WP-0014-T01. Still needs Railiance owner confirmation of exact emitter and wire binding before canonical schema/validator implementation, followed by actual emission/readback and attribution/clock/unknown-case acceptance.)*
### Enterprise Federation & SAML — Expanded-Mode Keycloak Identity Broker
Progress: 1/9 done | workplan_id: `1075448f-d533-5f9e-94b7-c3adfe151a07`
**Open tasks:**
- ! Admit the database consumer `2fe6f100`
*(wait: ADR-0009 adoption trigger absent; then railiance-platform/rapp-postgres must admit the database consumer and return backup/restore evidence.)*
- ! Package the broker deployment `32d1411b`
*(wait: ADR-0009 adoption trigger and named package/runtime owner absent; requires T02 database admission and governed OpenBao/ESO role plus immutable issuer assignment.)*
- ! Integrate an upstream identity provider `6697a994`
*(wait: Named tenant/upstream IdP owner/federation need absent; requires admitted broker and owner-reviewed tenant-scoped claim mappings plus actual login evidence.)*
- ! Define federated assurance `d845380d`
*(wait: Adoption trigger absent; requires per-IdP assurance trust mapping and reviewed privileged step-up delivery from issuer/package and access-engine owners.)*
- ! Verify IAM conformance and coexistence `9cb7fd93`
*(wait: Requires admitted live broker, canonical tenant issuer selection and dedicated test-client conformance/coexistence evidence; adoption trigger absent.)*
- ! Enforce tenant and platform boundaries `38998c68`
*(wait: Requires tenant-engine tier/issuer ownership and access-engine boundary/freshness enforcement evidence after adoption trigger and broker admission.)*
- ! Prove recovery and audit delivery `e5f44ddc`
*(wait: Requires admitted database/broker, named recovery and audit owners, off-host restore and observed audit/break-glass receipts; adoption trigger absent.)*
- … and 1 more open tasks
---
## MCP Orientation (when available)
If the state-hub MCP server is reachable, call:
`get_domain_summary("infotech")`
This provides richer cross-domain context.
If the MCP call fails, use this file as your orientation source.

View file

@ -0,0 +1,5 @@
{
"exit_code": 0,
"stdout": "[main 5fcd595] feat(recovery): add isolated KeyCape restore and loss cleanup proof\n 9 files changed, 1985 insertions(+), 2 deletions(-)\n create mode 100644 tests/isolated-keycape-native-fixture-manifest.json\n create mode 100644 tests/run_isolated_keycape_native_fixture.py\n create mode 100644 tests/test_isolated_keycape_fixture_plan.py\n create mode 100644 tests/test_isolated_keycape_recovery.py\n create mode 100644 tests/test_isolated_keycape_session.py\n create mode 100644 tools/bootstrap-recovery/isolated-keycape-session.py\n create mode 100644 tools/bootstrap-recovery/isolated-keycape.py\n create mode 100644 tools/bootstrap-recovery/run-isolated-keycape.sh\n",
"stderr": "{\"blocked_blob_ids\": [], \"scanned_blobs\": 9, \"scanner_sha256\": \"88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509\", \"scanner_version\": \"8.30.1\", \"scope\": \"changed-staged-blobs\", \"status\": \"clean\"}\n"
}

View file

@ -0,0 +1 @@
WARN: git commit failed for writeback: Generated-only commits rejected: staged

View file

@ -0,0 +1,375 @@
{
"repo_slug": "net-kingdom",
"repo_path": "/home/worsch/net-kingdom",
"source_revision": "5fcd5952add1c0167116094298a1d5feaa60cf30",
"source_revision_reason": null,
"issues": [
{
"severity": "WARN",
"check_id": "C-24",
"message": "Repo classification advisory: capability_tag 'security' is not a recommended family tag (allowed, check for synonym)",
"file_path": "",
"db_id": "",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-35",
"message": "Repo Manager warning: workplans/: repository uses more than one workplan prefix: ['NET-WP', 'NK-WP']",
"file_path": "workplans/",
"db_id": "",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-33",
"message": "WORK-RECORDS.md is missing or stale \u2014 regenerate via fix-consistency (CUST-WP-0061-T04 generated work-record index)",
"file_path": "WORK-RECORDS.md",
"db_id": "",
"file_value": "",
"db_value": "",
"fixable": true
},
{
"severity": "WARN",
"check_id": "C-37",
"message": "Residual workplan 'nk-wp-0011' has no origin/origin_ref provenance (not invented)",
"file_path": "workplans/NK-WP-0011-enterprise-federation-saml.md",
"db_id": "1075448f-d533-5f9e-94b7-c3adfe151a07",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-20",
"message": "Workplan dependency target 'USER-WP-0020' is not linked to State Hub",
"file_path": "workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md",
"db_id": "",
"file_value": "USER-WP-0020",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-20",
"message": "Workplan dependency target 'KEY-WP-0004' is not linked to State Hub",
"file_path": "workplans/NK-WP-0022-railiance01-identity-cutover-and-coulombcore-retirement.md",
"db_id": "",
"file_value": "KEY-WP-0004",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-20",
"message": "Workplan dependency target 'USER-WP-0020' is not linked to State Hub",
"file_path": "workplans/NK-WP-0023-user-engine-portal-platform-integration.md",
"db_id": "",
"file_value": "USER-WP-0020",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-20",
"message": "Workplan dependency target 'KEY-WP-0004' is not linked to State Hub",
"file_path": "workplans/NK-WP-0023-user-engine-portal-platform-integration.md",
"db_id": "",
"file_value": "KEY-WP-0004",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-20",
"message": "Workplan dependency target 'USER-WP-0021' is not linked to State Hub",
"file_path": "workplans/NK-WP-0024-user-engine-portal-integration-expansion.md",
"db_id": "",
"file_value": "USER-WP-0021",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-20",
"message": "Workplan dependency target 'FLEX-WP-0009' is not linked to State Hub",
"file_path": "workplans/NK-WP-0024-user-engine-portal-integration-expansion.md",
"db_id": "",
"file_value": "FLEX-WP-0009",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-20",
"message": "Workplan dependency target 'AUDIT-WP-0003' is not linked to State Hub",
"file_path": "workplans/NK-WP-0024-user-engine-portal-integration-expansion.md",
"db_id": "",
"file_value": "AUDIT-WP-0003",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-20",
"message": "Workplan dependency target 'EMAIL-WP-0004' is not linked to State Hub",
"file_path": "workplans/NK-WP-0024-user-engine-portal-integration-expansion.md",
"db_id": "",
"file_value": "EMAIL-WP-0004",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-20",
"message": "Workplan dependency target 'USER-WP-0022' is not linked to State Hub",
"file_path": "workplans/NK-WP-0025-public-self-registration-and-application-jit.md",
"db_id": "",
"file_value": "USER-WP-0022",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-20",
"message": "Workplan dependency target 'KEY-WP-0008' is not linked to State Hub",
"file_path": "workplans/NK-WP-0025-public-self-registration-and-application-jit.md",
"db_id": "",
"file_value": "KEY-WP-0008",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-20",
"message": "Workplan dependency target 'CSOC-WP-0003' is not linked to State Hub",
"file_path": "workplans/NK-WP-0025-public-self-registration-and-application-jit.md",
"db_id": "",
"file_value": "CSOC-WP-0003",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-37",
"message": "Residual workplan 'nk-wp-0027' has no origin/origin_ref provenance (not invented)",
"file_path": "workplans/NK-WP-0027-reef-placement-reconciliation.md",
"db_id": "965ad365-6b81-50a1-a2a3-2d0c1fcce0b4",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-37",
"message": "Residual workplan 'nk-wp-0031' has no origin/origin_ref provenance (not invented)",
"file_path": "workplans/NK-WP-0031-deterministic-posture-feedback.md",
"db_id": "9d7b04f9-3803-5613-b7a5-8bd606c77f5a",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-37",
"message": "Residual workplan 'nk-wp-0040' has no origin/origin_ref provenance (not invented)",
"file_path": "workplans/NK-WP-0040-execution-attribution-receipt.md",
"db_id": "e2533f3a-aa43-59b3-bff3-8e64b6149487",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-20",
"message": "Task dependency target 'NK-WP-0049-T32' of task 'NK-WP-0049-T18' is not linked to State Hub",
"file_path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md#NK-WP-0049-T18",
"db_id": "",
"file_value": "NK-WP-0049-T32",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-20",
"message": "Task dependency target 'NK-WP-0049-T33' of task 'NK-WP-0049-T18' is not linked to State Hub",
"file_path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md#NK-WP-0049-T18",
"db_id": "",
"file_value": "NK-WP-0049-T33",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-41",
"message": "Task wait-qualifier drift 'NK-WP-0049-T18': file={'needs_human': False, 'blocking_reason': 'Source and synthetic KeyCape lifecycle proof are delivered in T32; await exact owner-held recovery attempt preparation T33, selected consumer acceptance and retained-token MFA for the selected flow.', 'decision_id': None, 'blocked_on': None, 'wait_until': None} db={'needs_human': False, 'blocking_reason': 'LLDAP recovery T16 passed; await the exact isolated KeyCape adapter/procedure and retained-token MFA proof for the selected flow.', 'decision_id': None, 'blocked_on': None, 'wait_until': None} (file wins)",
"file_path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md#NK-WP-0049-T18",
"db_id": "141dd773-c1fa-56c1-a852-bbe2372e5125",
"file_value": "{\"blocked_on\": null, \"blocking_reason\": \"Source and synthetic KeyCape lifecycle proof are delivered in T32; await exact owner-held recovery attempt preparation T33, selected consumer acceptance and retained-token MFA for the selected flow.\", \"decision_id\": null, \"needs_human\": false, \"wait_until\": null}",
"db_value": "{\"blocked_on\": null, \"blocking_reason\": \"LLDAP recovery T16 passed; await the exact isolated KeyCape adapter/procedure and retained-token MFA proof for the selected flow.\", \"decision_id\": null, \"needs_human\": false, \"wait_until\": null}",
"fixable": true
},
{
"severity": "WARN",
"check_id": "C-11",
"message": "Task 'NK-WP-0049-T32' has no state_hub_task_id",
"file_path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md#NK-WP-0049-T32",
"db_id": "",
"file_value": "",
"db_value": "",
"fixable": true
},
{
"severity": "WARN",
"check_id": "C-11",
"message": "Task 'NK-WP-0049-T33' has no state_hub_task_id",
"file_path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md#NK-WP-0049-T33",
"db_id": "",
"file_value": "",
"db_value": "",
"fixable": true
},
{
"severity": "WARN",
"check_id": "C-20",
"message": "Workplan dependency target 'RAIL-PL-WP-0002' is not linked to State Hub",
"file_path": "workplans/archived/260702-NET-WP-0017-it-security-readiness-for-user-onboarding.md",
"db_id": "",
"file_value": "RAIL-PL-WP-0002",
"db_value": "",
"fixable": false
},
{
"severity": "INFO",
"check_id": "C-08",
"message": "Retired DB workstream 'nk-wp-0035@retired-20260908' (id=064c5e8b\u2026, status=archived) has no backing workplan file \u2014 projection tombstone, not live work",
"file_path": "",
"db_id": "064c5e8b-7852-4d7f-a5fa-e26e406ab8e0",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "INFO",
"check_id": "C-08",
"message": "Retired DB workstream 'adhoc-2026-08-14@retired-20260827' (id=a5097121\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work",
"file_path": "",
"db_id": "a5097121-1461-59f8-81fa-e6ecef527337",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "INFO",
"check_id": "C-08",
"message": "Retired DB workstream 'adhoc-2026-08-23@retired-20260827' (id=e25ba5fa\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work",
"file_path": "",
"db_id": "e25ba5fa-eaae-5039-8d93-a69c6d837fc4",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "INFO",
"check_id": "C-08",
"message": "Retired DB workstream 'net-kingdom-adhoc-2026-06-14@retired-20260827' (id=c6f3d6bf\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work",
"file_path": "",
"db_id": "c6f3d6bf-4916-490d-96ce-092d2776d7e7",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "INFO",
"check_id": "C-08",
"message": "Retired DB workstream 'net-kingdom-adhoc-2026-07-02@retired-20260827' (id=67c1c7ac\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work",
"file_path": "",
"db_id": "67c1c7ac-d8b1-41dc-a81a-4e43f1afd068",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-26",
"message": "Workplan id 'NET-WP-0020' uses prefix 'NET-WP' but repo canonical prefix is 'NK-WP' \u2014 new plans must conform",
"file_path": "workplans/NET-WP-0020-openbao-unseal-custody-and-ssh-automation.md",
"db_id": "",
"file_value": "NET-WP-0020",
"db_value": "NK-WP",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-26",
"message": "Workplan id 'NET-WP-0015' uses prefix 'NET-WP' but repo canonical prefix is 'NK-WP' \u2014 new plans must conform",
"file_path": "workplans/archived/260603-NET-WP-0015-platform-root-custody-and-openbao-identity-bootstrap.md",
"db_id": "",
"file_value": "NET-WP-0015",
"db_value": "NK-WP",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-26",
"message": "Workplan id 'NET-WP-0016' uses prefix 'NET-WP' but repo canonical prefix is 'NK-WP' \u2014 new plans must conform",
"file_path": "workplans/archived/260603-NET-WP-0016-guided-security-bootstrap-experience.md",
"db_id": "",
"file_value": "NET-WP-0016",
"db_value": "NK-WP",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-26",
"message": "Workplan id 'NET-WP-0018' uses prefix 'NET-WP' but repo canonical prefix is 'NK-WP' \u2014 new plans must conform",
"file_path": "workplans/archived/260604-NET-WP-0018-bootstrap-automation-and-rebuild-readiness.md",
"db_id": "",
"file_value": "NET-WP-0018",
"db_value": "NK-WP",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-26",
"message": "Workplan id 'NET-WP-0017' uses prefix 'NET-WP' but repo canonical prefix is 'NK-WP' \u2014 new plans must conform",
"file_path": "workplans/archived/260702-NET-WP-0017-it-security-readiness-for-user-onboarding.md",
"db_id": "",
"file_value": "NET-WP-0017",
"db_value": "NK-WP",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-26",
"message": "Workplan id 'NET-WP-0019' uses prefix 'NET-WP' but repo canonical prefix is 'NK-WP' \u2014 new plans must conform",
"file_path": "workplans/archived/260702-NET-WP-0019-t06-adjacent-user-lifecycle-dry-run-polish.md",
"db_id": "",
"file_value": "NET-WP-0019",
"db_value": "NK-WP",
"fixable": false
}
],
"fixes_applied": [
"bindings: synced 64/64",
"C-17 cleared: pushed 1 backlogged commit(s)",
"C-33 fixed: regenerated WORK-RECORDS.md",
"C-41 fixed: task 141dd773\u2026 wait qualifiers synced",
"C-11 fixed: task 'NK-WP-0049-T32' \u2192 cd78dbb7\u2026",
"C-11 fixed: task 'NK-WP-0049-T33' \u2192 56c8d469\u2026",
"brief: .custodian-brief.md updated",
"push: pushed"
],
"summary": {
"fail": 0,
"automation_error": 0,
"warn": 30,
"info": 5
},
"result": "warn"
}

View file

@ -0,0 +1,337 @@
#!/usr/bin/env python3
"""T18 native adapter for an owner-admitted, already restored isolated session.
Called after identity-bundle.restore, never with an unvalidated directory. This
module does not provision TLS, grant credentials, or claim downstream acceptance.
The existing decrypt/tmpfs wrapper remains responsible for archive cleanup.
"""
import base64
import hashlib
import importlib.util
import json
import io
import secrets
import tarfile
import re
import subprocess
from pathlib import Path
HERE = Path(__file__).resolve().parent
IMAGE = 'forgejo.coulomb.social/coulomb/key-cape@sha256:cdec9dd7c515f7e0a6169fc3fa560ee2cc47bf40d899755dec808c362f5ce010'
LABEL = 'net-kingdom.recovery-run'
class Refused(Exception):
pass
def load_bundle():
spec = importlib.util.spec_from_file_location('identity_bundle', HERE / 'identity-bundle.py')
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def private_payload(root, manifest, expected_manifest_sha256):
"""Bind the complete validated archive before selecting KeyCape material.
Private hashes never enter the public receipt. The digest is supplied by the
existing private archive admission, not inferred from an untrusted manifest.
"""
encoded = json.dumps(manifest, sort_keys=True, separators=(',', ':')).encode()
if hashlib.sha256(encoded).hexdigest() != expected_manifest_sha256:
raise Refused()
entries = manifest['files']
if len(entries) != len({e['path'] for e in entries}):
raise Refused()
values = {}
bundle = load_bundle()
for entry in entries:
if not bundle.allowed(entry['path'], manifest['profile']):
raise Refused()
path = root / entry['path']
bundle.private(path)
value = path.read_bytes()
if len(value) != entry['bytes'] or hashlib.sha256(value).hexdigest() != entry['sha256']:
raise Refused()
values[entry['path']] = value
deployment = json.loads(values['resources/sso/deployment/keycape.json'])
main = next(c for c in deployment['spec']['template']['spec']['containers'] if c['name'] == 'keycape')
if main['image'] != IMAGE:
# Mutable-tag/historical mismatches need a separate explicit owner admission.
raise Refused()
secret = json.loads(values['resources/sso/secret/keycape-config.json'])['data']
files = {'/etc/keycape/' + name: base64.b64decode(secret[name], validate=True)
for name in ('config.yaml', 'key.pem')}
for path, value in values.items():
if path.startswith('data/keycape/'):
relative = path.removeprefix('data/keycape/')
files['/var/lib/keycape-policy/' + relative] = value
if '/var/lib/keycape-policy/policy.json' not in files:
raise Refused()
# Resolve original delivered environment, including all referenced secrets.
env = {}
for item in main.get('env', []):
if item['name'] in env:
raise Refused()
if 'value' in item:
env[item['name']] = item['value']
else:
ref = item['valueFrom']['secretKeyRef']
source = json.loads(values['resources/sso/secret/' + ref['name'] + '.json'])['data']
env[item['name']] = base64.b64decode(source[ref['key']], validate=True).decode()
if env.get('KEYCAPE_CONFIG') != '/etc/keycape/config.yaml' or env.get('KEYCAPE_POLICY_PATH') != '/var/lib/keycape-policy/policy.json':
raise Refused()
return {'files': {p: base64.b64encode(v).decode() for p, v in files.items()}, 'env': env}
def command(argv, **kwargs):
maximum = kwargs.pop('max_output', 32768)
result = subprocess.run(argv, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
timeout=kwargs.pop('timeout', 180), **kwargs)
if result.returncode or len(result.stdout) > maximum:
raise Refused()
return result.stdout
def inspect_session(session):
"""No credential environment is read by Docker inspect or returned to caller."""
if not re.fullmatch(r'[a-f0-9]{32}', session['run_id']):
raise Refused()
names = session['containers']
if not names or len(names) > 12 or len(set(names)) != len(names):
raise Refused()
for name in names:
if not re.fullmatch(r'nk49-kc-' + session['run_id'] + r'-[a-z0-9-]{1,24}', name):
raise Refused()
if session['worker'] not in names or session['issuer'] not in names:
raise Refused()
import os
contexts=json.loads(command(['docker','context','inspect']))
endpoints=[contexts[0]['Endpoints']['docker']['Host'],os.environ.get('DOCKER_HOST','unix:///var/run/docker.sock')]
if not all(x.startswith('unix://') for x in endpoints):raise Refused()
info = json.loads(command(['docker', 'info', '--format', '{"ID":{{json .ID}},"Name":{{json .Name}},"OSType":{{json .OSType}}}']))
if info != {'ID': '74f93783-6cef-43a8-a3c4-f62586cd3bc1', 'Name': 'bnt-lap001', 'OSType': 'linux'}:
raise Refused()
# The worker is the anchor; all dependencies/TLS/issuer share its network-none namespace.
anchor = command(['docker', 'container', 'inspect', session['worker'], '--format', '{{.Id}}']).decode().strip()
for name in names:
obj = json.loads(command(['docker', 'container', 'inspect', name, '--format',
'{"host":{{json .HostConfig}},"mounts":{{json .Mounts}},"labels":{{json .Config.Labels}},"image":{{json .Config.Image}},"user":{{json .Config.User}}}']))
host = obj['host']
if (obj['labels'].get(LABEL) != session['run_id'] or not obj['user'] or obj['user'].split(':')[0] in ('0', 'root')
or not host['ReadonlyRootfs'] or host['Privileged'] or host.get('Binds') or host.get('PortBindings')
or host.get('PidMode') or host.get('IpcMode') == 'host'
or host.get('UTSMode') or host.get('UsernsMode') == 'host' or host.get('CgroupnsMode') == 'host'
or host.get('CapAdd') or host.get('Devices') or host.get('DeviceRequests') or host.get('VolumesFrom')
or host.get('CapDrop') != ['ALL']
or host.get('SecurityOpt') not in (['no-new-privileges'], ['no-new-privileges:true'])
or any(m['Type'] != 'tmpfs' for m in obj['mounts'])
or host['NetworkMode'] != ('none' if name == session['worker'] else 'container:' + anchor)
or obj['image'] != session['images'][name] or '@sha256:' not in obj['image']):
raise Refused()
if session['images'][session['issuer']] != IMAGE:
raise Refused()
def container_file(container, path):
# Docker streams one tar member into memory; never materialize private files.
raw = command(['docker', 'cp', container + ':' + path, '-'], max_output=16*1024*1024)
with tarfile.open(fileobj=io.BytesIO(raw), mode='r:') as archive:
members = archive.getmembers()
if len(members) != 1 or not members[0].isfile() or members[0].size > 8*1024*1024:
raise Refused()
return archive.extractfile(members[0]).read()
JWKS_PROBE = r"""
import base64,ipaddress,json,socket,ssl,sys,urllib.request
from urllib.parse import urlsplit
p=json.load(sys.stdin);issuer=p['issuer'];origin=urlsplit(issuer)
assert origin.scheme=='https'
assert all(ipaddress.ip_address(x[4][0]).is_loopback for x in socket.getaddrinfo(origin.hostname,origin.port or 443,type=socket.SOCK_STREAM))
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self,*args,**kwargs):return None
opener=urllib.request.build_opener(urllib.request.ProxyHandler({}),NoRedirect(),
urllib.request.HTTPSHandler(context=ssl.create_default_context(cadata=p['trust'])))
def get(url):
u=urlsplit(url)
assert (u.scheme,u.hostname,u.port or 443)==(origin.scheme,origin.hostname,origin.port or 443)
assert not u.username and not u.password and not u.fragment
with opener.open(url,timeout=10) as response:
raw=response.read(1024*1024+1);assert len(raw)<=1024*1024
return json.loads(raw)
d=get(issuer.rstrip('/')+'/.well-known/openid-configuration');assert d['issuer']==issuer
keys=get(d['jwks_uri'])['keys']
def integer(value):return int.from_bytes(base64.urlsafe_b64decode(value+'='*(-len(value)%4)),'big')
assert any(k.get('kty')=='RSA' and integer(k['n'])==int(p['n']) and integer(k['e'])==int(p['e']) for k in keys)
print('{"original_public_signing_key_in_live_jwks":true}')
"""
def check_jwks(session, admission, public_numbers):
trust=container_file(session['issuer'],admission['trust_path']).decode()
request={'issuer':admission['issuer'],'trust':trust,'n':str(public_numbers.n),'e':str(public_numbers.e)}
result=json.loads(command(['docker','exec','-i',session['worker'],'python3','-c',JWKS_PROBE],input=json.dumps(request).encode()))
if result != {'original_public_signing_key_in_live_jwks': True}:raise Refused()
def run_verifier(session, admission, payload):
issuer = session['issuer']
from urllib.parse import urlsplit
import ipaddress
address = urlsplit(admission['issuer'])
if (address.scheme != 'https' or not address.hostname or address.username or address.password
or address.query or address.fragment):
raise Refused()
hosts = container_file(issuer, '/etc/hosts').decode().splitlines()
matched = [row.split('#')[0].split() for row in hosts]
matched = [row for row in matched if len(row)>1 and address.hostname in row[1:]]
if not matched or any(not ipaddress.ip_address(row[0]).is_loopback for row in matched):
raise Refused()
if hashlib.sha256(container_file(issuer, '/keycape')).hexdigest() != admission['verifier_sha256']:
raise Refused()
if hashlib.sha256(container_file(issuer, admission['trust_path'])).hexdigest() != admission['trust_sha256']:
raise Refused()
# Capture private environment only in owner process memory, never receipt.
environment = json.loads(command(['docker','container','inspect',issuer,'--format','{{json .Config.Env}}']))
env = dict(item.split('=',1) for item in environment)
if env.get('SSL_CERT_FILE') != admission['trust_path'] or any(k.lower().endswith('_proxy') and v for k,v in env.items()):
raise Refused()
for name,value in payload['env'].items():
if env.get(name) != value:
raise Refused()
files = {k:base64.b64decode(v,validate=True) for k,v in payload['files'].items()}
def equal():
for path,value in files.items():
if container_file(issuer,path) != value:
raise Refused()
# Full durable state tree comparison also excludes extra runtime receipts.
raw = command(['docker','cp',issuer+':/var/lib/keycape-policy/.','-'],max_output=32*1024*1024)
found = {}
with tarfile.open(fileobj=io.BytesIO(raw),mode='r:') as archive:
for member in archive:
if member.isdir():continue
name = member.name.removeprefix('./')
if not member.isfile() or name in found or member.size>8*1024*1024:
raise Refused()
found[name]=archive.extractfile(member).read()
if found != {k.removeprefix('/var/lib/keycape-policy/'):v for k,v in files.items() if k.startswith('/var/lib/keycape-policy/')}:
raise Refused()
equal()
# Reading YAML is data parsing only; no archived executable is evaluated.
import yaml
cfg = yaml.safe_load(files['/etc/keycape/config.yaml'])
if cfg['issuer'] != admission['issuer']:
raise Refused()
consumer = admission['consumer']
client = next(x for x in cfg['clients'] if x['clientId']==consumer['client_id'])
if (client['secretRef']!='env:'+consumer['secret_env'] or client['clientType']!='confidential'
or 'client_credentials' not in client['grantTypes'] or not env.get(consumer['secret_env'])):
raise Refused()
wrong = secrets.token_urlsafe(48)
if wrong == env[consumer['secret_env']]:raise Refused()
args = ['docker','exec','--env','NK49_UNRELATED_NEGATIVE_SECRET='+wrong,issuer,'/keycape','verify-client',
'-issuer',admission['issuer'],'-previous-secret-env','NK49_UNRELATED_NEGATIVE_SECRET']
flags = {'client_id':'client-id','secret_env':'secret-env','audience':'audience','scope':'scope',
'subject':'expect-subject','tenant':'expect-tenant','roles':'expect-roles','deny_scope':'deny-scope'}
for name,flag in flags.items():args+=['-'+flag,consumer[name]]
# Native command reports on stderr. Do not return or print its raw response.
result = subprocess.run(args,stdout=subprocess.PIPE,stderr=subprocess.PIPE,timeout=150)
raw = (result.stdout+result.stderr).decode()
required = ['discovery and endpoint origin','exchange and JWKS signature for granted scopes','access-token claim bindings',
'principal_type is service','sub matches the registration','tenant matches the registration','roles present',
'granted scope carries no excess grant','predecessor secret refused']
required += ['excess scope refused: '+scope for scope in consumer['deny_scope'].split()]
if result.returncode or any('PASS '+name+'\n' not in raw for name in required) or 'FAIL ' in raw:
raise Refused()
equal()
from cryptography.hazmat.primitives import serialization
key=serialization.load_pem_private_key(files['/etc/keycape/key.pem'],password=None)
check_jwks(session,admission,key.public_key().public_numbers())
public=key.public_key().public_bytes(serialization.Encoding.DER,serialization.PublicFormat.SubjectPublicKeyInfo)
return {'native_service_registration_verified':True,'wrong_unrelated_secret_explicitly_refused':True,
'denied_scopes_refused':True,'original_config_registry_policy_receipts_equal':True,
'public_signing_spki_sha256':hashlib.sha256(public).hexdigest(),
'original_https_issuer_and_trust_preserved':True,'original_public_signing_key_in_live_jwks':True}
def cleanup(session, receipt=None):
"""Attempt every owned cleanup even when one obligation fails.
Preserve exact safe container names and outcomes so the owner can recover an
uncertain/partial cleanup. An inspect/daemon failure never means absence.
"""
obligations = []
failed = False
for name in reversed(session['containers']):
row = {'container': name, 'status': 'unverified'}
obligations.append(row)
try:
label = command(['docker', 'container', 'inspect', name, '--format',
'{{index .Config.Labels "' + LABEL + '"}}']).decode().strip()
if label != session['run_id']:
row['status'] = 'ownership_mismatch_not_removed'
failed = True
continue
# A failed remove is followed by a successful explicit inventory;
# interrupted remove may actually have completed.
try:
command(['docker', 'rm', '--force', '--volumes', name])
except Exception:
pass
remaining = command(['docker','container','ls','--all','--filter',
'name=^/' + name + '$','--format','{{.Names}}']).decode().splitlines()
if remaining:
row['status'] = 'still_present'
failed = True
else:
row['status'] = 'absence_verified'
except Exception:
failed = True
if receipt is not None:
receipt['container_cleanup_obligations'] = obligations
if failed:
raise Refused()
def native_checks(root, manifest, admission, receipt):
"""Owner wrapper provides a reviewed session after existing restore pipeline.
Admission must bind ciphertext, canonical private manifest digest, executable
and trust bytes, exact image/target list, original issuer and selected consumer.
None of these admission fields grant authority or carry a client secret.
"""
session = admission['session']
# Validate ownership/containment before permitting either exec or cleanup.
inspect_session(session)
receipt.update(status='failed', source_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
ciphertext_sha256=admission['ciphertext_sha256'], production_contacted=False,
human_mfa_proven=False, downstream_consumer_reconnect_proven=False,
container_cleanup_verified=False)
try:
for field in ('ciphertext_sha256', 'manifest_sha256', 'verifier_sha256', 'trust_sha256'):
if not re.fullmatch('[a-f0-9]{64}', admission[field]):
raise Refused()
for field in ('client_id','secret_env','audience','scope','subject','tenant','roles','deny_scope'):
if not isinstance(admission['consumer'].get(field), str) or not admission['consumer'][field].strip():
raise Refused()
payload = private_payload(root, manifest, admission['manifest_sha256'])
result = run_verifier(session, admission, payload)
required = {'native_service_registration_verified','wrong_unrelated_secret_explicitly_refused',
'denied_scopes_refused','original_config_registry_policy_receipts_equal',
'original_https_issuer_and_trust_preserved','original_public_signing_key_in_live_jwks'}
if any(result.get(k) is not True for k in required) or not re.fullmatch('[a-f0-9]{64}', result.get('public_signing_spki_sha256','')):
raise Refused()
# Closed receipt allowlist: never copy arbitrary worker output.
receipt.update({k: True for k in required})
receipt['public_signing_spki_sha256'] = result['public_signing_spki_sha256']
receipt['status'] = 'isolated_keycape_native_registration_checked'
finally:
try:
cleanup(session, receipt)
receipt['container_cleanup_verified'] = True
except Exception:
receipt['status'] = 'failed'
raise

View file

@ -0,0 +1,144 @@
# T18 KeyCape recovery adapter candidate, revision 2
**Pending root review and native fixture proof. T18 remains open.** Source and
39 passing synthetic tests are prepared in `/tmp/nk49-keycape-adapter-next` and
copied here. No canonical owner file, credential, actual recovery, Docker run,
production route or secret-bearing archive was touched. `candidate.patch` adds
five files to NetKingdom; exact digests/check provenance are in `preparation.json`.
Files:
- `isolated-keycape.py`: archive/source/containment/native acceptance checks.
- `isolated-keycape-session.py`: existing-pipeline decryption, private import,
isolated original-image session, HTTPS transport, checks and cleanup.
- `run-isolated-keycape.sh`: attended owner launcher through the existing hidden
custody helper, with target preflight before the backup-key prompt.
- Two focused test files: check-stage negatives and session/cleanup ordering.
The wrapper uses existing `isolated-lldap`, offline transfer validation,
`verify-identity.custody` and `identity-bundle.restore` APIs. It checks the exact
independently retrieved ciphertext length/digest, custody recipient, complete
archive manifest and original immutable archived KeyCape image. Decryption and
archive restore remain on `/dev/shm`. The private manifest digest comes from
owner admission; it is never emitted publicly. Archive commands/args/envFrom and
non-KeyCape environment overrides are refused. Runtime environment permits only
original KeyCape deliveries, PATH/HOME/HOSTNAME and the exact admitted
SSL_CERT_FILE; duplicate or additional security environment (including
SSL_CERT_DIR/GODEBUG/PYTHONOPTIMIZE/proxy overrides) is refused. Original config/complete static
registry, signing key, policy/receipt tree, factor delivery and original delivered
environment are preserved. Secrets reach Docker by environment **names** only,
with values in the owner subprocess environment, never argv or printed output.
The original KeyCape image is distroless. Docker's documented
[cp corner cases](https://docs.docker.com/reference/cli/docker/container/cp/#corner-cases)
exclude tmpfs and user-created mounts. Consequently the candidate shares exactly
four Docker **local-driver tmpfs** volumes between the existing pinned Python
helper and original KeyCape issuer. Driver, scope, exact options, owner label,
source, target and read/write flags are inspected before private import. There
are no host directory mounts, disk volumes, new image, JWT verifier or execution
service. The helper's private import and comparison occur inside those exact
mounts; daemon `cp` is used only for public `/keycape` executable readback from
the original image root filesystem. This volume extension still needs native
pinned-image fixture verification before approval.
Both containers use the fixed local Docker engine, no network interfaces beyond
loopback, readonly root, nonroot UID, no capabilities/devices/privilege/host
namespaces or unadmitted security options. Peers share the exact network-none
anchor. Only namespace-local unprivileged-port sysctl `0` is admitted so the
existing nonroot Python helper can present original HTTPS on loopback443 and
forward to the original KeyCape process on loopback8080. The issuer uses its exact
original image default `/keycape` launch: Entrypoint/Cmd and actual Path/Args are
inspected; the original file/environment bytes and executable digest are bound.
Production dependency addresses remain unreachable. No readiness, LDAP,
Authelia, MFA, tenant-engine or notification endpoint is imitated. A selected
service registration can be checked without claiming `/readyz` passed.
The owner supplies an existing trusted issuer certificate/private key and trust
bundle through private regular files. No certificate, CA, signing key, client
registration or credential is generated/reissued. Original HTTPS hostname, trust
and same-origin discovery/JWKS are retained, without redirect/proxy/insecure
bypass. The public JWKS probe requires exactly the original owner's single RSA
signing key with `use:sig`, `alg:RS256`, `kid:key-1`, matching the original key's
public numbers. It checks before and after the native exchange. Every security
condition uses explicit refusal, including when Python optimization is enabled.
This probe only compares public discovery/JWKS data; JWT/exchange/claim/scope and
wrong-secret verification exclusively use shipped `/keycape verify-client`.
The negative check supplies a generated unrelated random secret through the
existing `-previous-secret-env` refusal mechanism. The bounded receipt says
**wrong unrelated secret refused**, never rotation. An absent exact PASS,
nonzero exit, TLS/discovery error, malformed output or any FAIL refuses evidence.
All durable private files and the full receipt tree are compared before/after;
extra receipt files refuse continuity. Public receipts expose only booleans,
public signing SPKI fingerprint, ciphertext/source hashes and exact safe cleanup
obligations. No config, env value, private digest, credential, JWT, response body,
subject or client ID is copied into receipts.
Normal/failure cleanup attempts every safely owned container and tmpfs volume,
records exact verified/uncertain obligations and refuses success if any removal
is uncertain. The existing outer tmpfs staging is checked absent. A scoped
1800-second cleanup child contains only public exact run identifiers, no custody
FD or credential environment, and survives loss of the parent process. Normal
verified cleanup cancels it. This is a bounded owner-session recovery mechanism,
not a standing daemon; interruption behavior and Docker tmpfs lifecycle still
need native fixture proof. A refused preflight/import records uncertainty rather
than inventing absence.
## Exact owner inputs and invocation, after approval/native fixture proof
The launcher takes six paths/public arguments:
```
bash tools/bootstrap-recovery/run-isolated-keycape.sh \
VERIFIED_TRANSFER_RECEIPT PRIVATE_ADMISSION ADMISSION_SHA256 \
PRIVATE_EXISTING_TLS_CERT PRIVATE_EXISTING_TLS_KEY PRIVATE_ORIGINAL_TRUST
```
Do not run this candidate until root review and synthetic pinned-image runtime
proof admit the exact source. It prompts only for the independently held age key
using the existing custody helper. Current client credentials come only from the
original encrypted archive's already delivered registration/environment; no key,
password, factor or TLS private input is passed to the agent.
The hash-bound owner admission JSON has `ciphertext_sha256`, `manifest_sha256`,
`verifier_sha256` (original image `/keycape` binary), `trust_sha256`,
`tls_cert_sha256`, original `issuer`, and `consumer` containing existing exact
`client_id`, `secret_env`, `audience`, `scope`, `subject`, `tenant`, `roles`,
`deny_scope`. Private `manifest_sha256` is SHA256 of
`json.dumps(manifest, sort_keys=True, separators=(',', ':')).encode()` from the
admitted original archive. The wrapper checks it against the validated manifest,
not an arbitrary directory. Trust path/session names are derived internally;
operator input cannot select a remote Docker engine or arbitrary container set.
The selected original issuer configuration must use HTTPS443 and local service
port8080, as the admitted owner release does. Other configurations fail closed
and need explicit source review, rather than silent rewrite.
## Remaining proof and retained review findings
The 39 tests use synthetic data and mocked Docker/native outcomes. They exercise
archive integrity/symlink/lineage drift, optimizer-safe JWKS wrong-key/extra-key/
wrong-kid/cross-origin/DNS negatives, default launch binding, capabilities/devices/
namespace/security overrides, transport-vs-wrong-secret distinction, private
import ordering, secret-free argv, shared-tmpfs planning, and partial/uncertain
cleanup. They do **not** prove the actual pinned image supports the complete
session or that the selected current credential/factor works.
Next: root review; native disposable pinned-image fixture with original hostname
and a synthetic trusted TLS pair; positive/negative service behavior and hard
interruption cleanup; then an admitted owner-run actual recovery. Actual T18
still needs selected downstream-consumer reconnect and owner acceptance. Human/
browser flow uses the existing `keycape login`; retained-factor MFA stays T19.
Both remain explicitly unproven in the receipt.
R1 source/receipt are retained under `revisions/r1/`. Root's findings were added
as regression tests: added caps/devices/security exceptions; cleanup failure
incorrectly preserving success or abandoning later obligations; Python assert
optimization; extra JWKS signer ambiguity; same-image alternate launch; and
unsupported private tmpfs daemon-copy assumptions. Revision2 addresses these in
source. No root approval or native acceptance is fabricated.
Knowledge orientation: emergent-security0.2.6, digest
`8d1d6a9d4d07fec7b619618de7dfe2254c1790f85f4198c7fe19db9751c02`,
withheld/changed-source status; current owner AGENTS/companion/recovery contract
and verifier source were reviewed. Supervised assignment; no autopilot or
privileged execution claimed.

View file

@ -0,0 +1,218 @@
import base64
import hashlib
import importlib.util
import io
import json
import subprocess
import tarfile
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
SOURCE=Path(__file__).parents[1]/'tools/bootstrap-recovery/isolated-keycape.py'
spec=importlib.util.spec_from_file_location('adapter',SOURCE)
a=importlib.util.module_from_spec(spec);spec.loader.exec_module(a)
class ArchiveTests(unittest.TestCase):
def setUp(self):
self.tmp=tempfile.TemporaryDirectory();self.addCleanup(self.tmp.cleanup)
self.root=Path(self.tmp.name)
config={'data':{k:base64.b64encode(v).decode() for k,v in {'config.yaml':b'issuer: https://issuer.example','key.pem':b'fixture-key'}.items()}}
self.values={'resources/sso/secret/keycape-config.json':json.dumps(config).encode(),
'resources/sso/deployment/keycape.json':json.dumps({'spec':{'template':{'spec':{'containers':[{'name':'keycape','image':a.IMAGE,'env':[
{'name':'KEYCAPE_CONFIG','value':'/etc/keycape/config.yaml'},
{'name':'KEYCAPE_POLICY_PATH','value':'/var/lib/keycape-policy/policy.json'},
{'name':'KEYCAPE_ORIGINAL_SECRET','valueFrom':{'secretKeyRef':{'name':'client','key':'secret'}}}]}]}}}}).encode(),
'resources/sso/secret/client.json':json.dumps({'data':{'secret':base64.b64encode(b'fixture-private').decode()}}).encode(),
'data/keycape/policy.json':b'{}','data/keycape/receipts/one.json':b'{"original":true}'}
for name,value in self.values.items():
p=self.root/name;p.parent.mkdir(parents=True,exist_ok=True);p.write_bytes(value);p.chmod(0o600)
self.manifest={'profile':{'components':[{'namespace':'sso','deployment':'keycape','secret_refs':['keycape-config','client'],'configmap_refs':[]}]},
'files':[{'path':k,'bytes':len(v),'sha256':hashlib.sha256(v).hexdigest()} for k,v in self.values.items()]}
def payload(self):
digest=hashlib.sha256(json.dumps(self.manifest,sort_keys=True,separators=(',',':')).encode()).hexdigest()
return a.private_payload(self.root,self.manifest,digest)
def test_original_state_and_secret_delivery_retained(self):
p=self.payload();self.assertEqual(p['env']['KEYCAPE_ORIGINAL_SECRET'],'fixture-private')
self.assertEqual(base64.b64decode(p['files']['/var/lib/keycape-policy/receipts/one.json']),b'{"original":true}')
def test_archive_digest_refused(self):
with self.assertRaises(a.Refused):a.private_payload(self.root,self.manifest,'0'*64)
def test_changed_file_refused(self):
(self.root/'data/keycape/policy.json').write_text('{"changed":true}')
with self.assertRaises(a.Refused):self.payload()
def test_symlink_refused(self):
p=self.root/'data/keycape/policy.json';p.unlink();p.symlink_to('/etc/hostname')
with self.assertRaises(Exception):self.payload()
def test_duplicate_manifest_refused(self):
self.manifest['files'].append(self.manifest['files'][0])
with self.assertRaises(a.Refused):self.payload()
def test_image_mismatch_refused(self):
d=json.loads(self.values['resources/sso/deployment/keycape.json']);d['spec']['template']['spec']['containers'][0]['image']='keycape:latest'
raw=json.dumps(d).encode();p=self.root/'resources/sso/deployment/keycape.json';p.write_bytes(raw)
for row in self.manifest['files']:
if row['path'].endswith('/deployment/keycape.json'):row.update(bytes=len(raw),sha256=hashlib.sha256(raw).hexdigest())
with self.assertRaises(a.Refused):self.payload()
class NativeTests(unittest.TestCase):
def setUp(self):
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
self.key=rsa.generate_private_key(public_exponent=65537,key_size=2048).private_bytes(serialization.Encoding.PEM,serialization.PrivateFormat.PKCS8,serialization.NoEncryption())
self.cfg=b'issuer: https://issuer.example\nclients:\n- clientId: existing\n clientType: confidential\n secretRef: env:KEYCAPE_ORIGINAL_SECRET\n grantTypes: [client_credentials]\n'
self.files={'/etc/keycape/config.yaml':self.cfg,'/etc/keycape/key.pem':self.key,'/var/lib/keycape-policy/policy.json':b'{}'}
self.payload={'env':{'KEYCAPE_ORIGINAL_SECRET':'fixture-private'},'files':{k:base64.b64encode(v).decode() for k,v in self.files.items()}}
self.admission={'issuer':'https://issuer.example','verifier_sha256':hashlib.sha256(b'binary').hexdigest(),
'trust_path':'/trust.pem','trust_sha256':hashlib.sha256(b'original-trust').hexdigest(),
'consumer':{'client_id':'existing','secret_env':'KEYCAPE_ORIGINAL_SECRET','scope':'read','audience':'consumer',
'subject':'service:existing','tenant':'tenant:platform','roles':'reader','deny_scope':'write'}}
self.session={'issuer':'isolated','volumes':{x:{'target':x} for x in ('/etc/keycape','/var/lib/keycape-policy','/etc/keycape-factor','/recovery-tls')}}
self.output=''.join('PASS '+x+'\n' for x in ['discovery and endpoint origin','exchange and JWKS signature for granted scopes','access-token claim bindings','principal_type is service','sub matches the registration','tenant matches the registration','roles present','granted scope carries no excess grant','predecessor secret refused','excess scope refused: write'])
def file(self,container,path):
return {**self.files,'/etc/hosts':b'127.0.0.1 issuer.example\n','/keycape':b'binary','/trust.pem':b'original-trust'}[path]
def command(self,argv,**kwargs):
if argv[1:3]==['container','inspect']:return json.dumps(['KEYCAPE_ORIGINAL_SECRET=fixture-private','SSL_CERT_FILE=/trust.pem']).encode()
output=io.BytesIO()
with tarfile.open(fileobj=output,mode='w') as t:
m=tarfile.TarInfo('policy.json');m.size=2;t.addfile(m,io.BytesIO(b'{}'))
return output.getvalue()
def verify(self,code=0,output=None):
with patch.object(a,'check_jwks'),patch.object(a,'runtime_file',return_value=b'original-trust'),patch.object(a,'runtime_files',side_effect=lambda session,paths=(),tree=False:({k:v for k,v in self.files.items() if k.startswith('/var/lib/keycape-policy/')} if tree else dict(self.files))),patch.object(a,'container_file',side_effect=self.file),patch.object(a,'command',side_effect=self.command),patch.object(a.subprocess,'run',return_value=subprocess.CompletedProcess([],code,b'',(self.output if output is None else output).encode())) as run:
result=a.run_verifier(self.session,self.admission,self.payload)
argv=run.call_args.args[0]
self.assertNotIn('fixture-private',' '.join(argv))
self.assertIn('-previous-secret-env',argv)
return result
def test_original_registration_and_unrelated_secret(self):
r=self.verify();self.assertTrue(r['wrong_unrelated_secret_explicitly_refused']);self.assertNotIn('fixture-private',json.dumps(r))
def test_transport_failure_never_counts_as_wrong_secret(self):
with self.assertRaises(a.Refused):self.verify(1,'FAIL discovery and endpoint origin: TLS failure\n')
def test_missing_explicit_rejection_never_counts(self):
with self.assertRaises(a.Refused):self.verify(0,self.output.replace('PASS predecessor secret refused\n',''))
def test_fail_line_refused_even_zero_exit(self):
with self.assertRaises(a.Refused):self.verify(0,self.output+'FAIL transport\n')
def test_security_environment_override_refused(self):
original=self.command
def altered(argv,**kwargs):
if argv[1:3]==['container','inspect']:return json.dumps(['KEYCAPE_ORIGINAL_SECRET=fixture-private','SSL_CERT_FILE=/trust.pem','SSL_CERT_DIR=/unreviewed']).encode()
return original(argv,**kwargs)
with patch.object(self,'command',side_effect=altered):
with self.assertRaises(a.Refused):self.verify()
def test_http_issuer_refused(self):
self.admission['issuer']='http://issuer.example'
with self.assertRaises(a.Refused):self.verify()
def test_changed_trust_refused(self):
self.admission['trust_sha256']='0'*64
with self.assertRaises(a.Refused):self.verify()
def test_config_drift_refused(self):
self.payload['files']['/etc/keycape/config.yaml']=base64.b64encode(b'changed').decode()
with self.assertRaises(a.Refused):self.verify()
class BoundaryTests(unittest.TestCase):
def test_remote_docker_refused_before_info(self):
session={'run_id':'a'*32,'containers':['nk49-kc-'+('a'*32)+'-worker'],'worker':'nk49-kc-'+('a'*32)+'-worker','issuer':'nk49-kc-'+('a'*32)+'-worker'}
with patch.object(a,'command',return_value=b'[{"Endpoints":{"docker":{"Host":"tcp://remote:2375"}}}]') as command:
with self.assertRaises(a.Refused):a.inspect_session(session)
self.assertEqual(command.call_count,1)
def test_failed_verifier_cleanup_is_attempted(self):
receipt={};admission={'session':{},'ciphertext_sha256':'a'*64,'manifest_sha256':'b'*64,'verifier_sha256':'c'*64,'trust_sha256':'d'*64,'consumer':{x:'yes' for x in ('client_id','secret_env','audience','scope','subject','tenant','roles','deny_scope')}}
with patch.object(a,'inspect_session'),patch.object(a,'private_payload',return_value={}),patch.object(a,'run_verifier',side_effect=a.Refused),patch.object(a,'cleanup') as cleanup:
with self.assertRaises(a.Refused):a.native_checks(Path('/unused'),{},admission,receipt)
cleanup.assert_called_once();self.assertTrue(receipt['container_cleanup_verified']);self.assertEqual(receipt['status'],'failed')
def test_cleanup_failure_overrides_success(self):
admission={'session':{},'ciphertext_sha256':'a'*64,'manifest_sha256':'b'*64,'verifier_sha256':'c'*64,'trust_sha256':'d'*64,'consumer':{x:'yes' for x in ('client_id','secret_env','audience','scope','subject','tenant','roles','deny_scope')}}
result={x:True for x in ('native_service_registration_verified','wrong_unrelated_secret_explicitly_refused','denied_scopes_refused','original_config_registry_policy_receipts_equal','original_https_issuer_and_trust_preserved','original_public_signing_key_in_live_jwks')}
result['public_signing_spki_sha256']='e'*64;receipt={}
with patch.object(a,'inspect_session'),patch.object(a,'private_payload',return_value={}),patch.object(a,'run_verifier',return_value=result),patch.object(a,'cleanup',side_effect=a.Refused):
with self.assertRaises(a.Refused):a.native_checks(Path('/unused'),{},admission,receipt)
self.assertEqual(receipt['status'],'failed');self.assertFalse(receipt['container_cleanup_verified'])
class ContainmentTests(unittest.TestCase):
def inspect(self,overrides=None,launch=None):
name='nk49-kc-'+('a'*32)+'-issuer'
session={'run_id':'a'*32,'containers':[name],'issuer':name,'worker':name,'images':{name:a.IMAGE}}
host={'NetworkMode':'none','ReadonlyRootfs':True,'Privileged':False,'CapDrop':['ALL'],'SecurityOpt':['no-new-privileges']}
host.update(overrides or {})
def command(argv,**kwargs):
if argv[1:3]==['context','inspect']:return b'[{"Endpoints":{"docker":{"Host":"unix:///var/run/docker.sock"}}}]'
if argv[1]=='info':return b'{"ID":"74f93783-6cef-43a8-a3c4-f62586cd3bc1","Name":"bnt-lap001","OSType":"linux"}'
if argv[-1]=='{{.Id}}':return b'anchor-id'
if '"path"' in argv[-1]:return json.dumps(launch or {'entrypoint':['/keycape'],'cmd':None,'path':'/keycape','args':[]}).encode()
return json.dumps({'host':host,'mounts':[],'labels':{a.LABEL:'a'*32},'image':a.IMAGE,'user':'1000:1000'}).encode()
with patch.object(a,'command',side_effect=command),patch.dict('os.environ',{'DOCKER_HOST':'unix:///var/run/docker.sock'}):a.inspect_session(session)
def test_minimal_network_none_admitted(self):self.inspect()
def test_alternate_same_image_command_refused(self):
with self.assertRaises(a.Refused):self.inspect(launch={'entrypoint':['/keycape'],'cmd':['check-config'],'path':'/keycape','args':['check-config']})
def test_cap_add_refused(self):
with self.assertRaises(a.Refused):self.inspect({'CapAdd':['NET_ADMIN']})
def test_devices_refused(self):
with self.assertRaises(a.Refused):self.inspect({'Devices':[{'PathOnHost':'/dev/mem'}]})
def test_device_requests_refused(self):
with self.assertRaises(a.Refused):self.inspect({'DeviceRequests':[{'Driver':'nvidia'}]})
def test_host_namespace_refused(self):
for field in ('PidMode','IpcMode','UTSMode','UsernsMode','CgroupnsMode'):
with self.subTest(field=field),self.assertRaises(a.Refused):self.inspect({field:'host'})
def test_security_override_refused(self):
with self.assertRaises(a.Refused):self.inspect({'SecurityOpt':['no-new-privileges','seccomp=unconfined']})
class CleanupTests(unittest.TestCase):
def test_partial_cleanup_attempts_every_owned_container(self):
session={'run_id':'a'*32,'containers':['first','second','third']};receipt={}
removed=[]
def command(argv,**kwargs):
if argv[1:3]==['container','inspect']:return ('a'*32).encode()
if argv[1]=='rm':
removed.append(argv[-1])
if argv[-1]=='third':raise a.Refused()
return b''
if 'name=^/third$' in argv:return b'third\n'
return b''
with patch.object(a,'command',side_effect=command):
with self.assertRaises(a.Refused):a.cleanup(session,receipt)
self.assertEqual(removed,['third','second','first'])
self.assertEqual([r['status'] for r in receipt['container_cleanup_obligations']],['still_present','absence_verified','absence_verified'])
def test_ownership_mismatch_does_not_remove_and_continues(self):
session={'run_id':'a'*32,'containers':['first','second']};receipt={};removed=[]
def command(argv,**kwargs):
if argv[1:3]==['container','inspect']:return (('b' if argv[3]=='second' else 'a')*32).encode()
if argv[1]=='rm':removed.append(argv[-1])
return b''
with patch.object(a,'command',side_effect=command):
with self.assertRaises(a.Refused):a.cleanup(session,receipt)
self.assertEqual(removed,['first'])
self.assertEqual(receipt['container_cleanup_obligations'][0]['status'],'ownership_mismatch_not_removed')
def test_inspect_failure_retains_uncertain_obligation(self):
receipt={}
with patch.object(a,'command',side_effect=a.Refused):
with self.assertRaises(a.Refused):a.cleanup({'run_id':'a'*32,'containers':['exact-owned-name']},receipt)
self.assertEqual(receipt['container_cleanup_obligations'],[{'container':'exact-owned-name','status':'unverified'}])
class JWKSTests(unittest.TestCase):
def execute(self,n='123',endpoint='https://issuer.example/jwks',resolved='127.0.0.1',extra=False,kid='key-1'):
request={'issuer':'https://issuer.example','trust':'public-certificate','n':'123','e':'65537'}
def enc(i):return base64.urlsafe_b64encode(int(i).to_bytes((int(i).bit_length()+7)//8,'big')).decode().rstrip('=')
responses=[{'issuer':'https://issuer.example','jwks_uri':endpoint},{'keys':[{'kty':'RSA','kid':kid,'use':'sig','alg':'RS256','n':enc(n),'e':enc(65537)}]}]
if extra:responses[1]['keys'].append(dict(responses[1]['keys'][0],kid='key-2'))
class Response:
def __init__(self,value):self.value=value
def __enter__(self):return self
def __exit__(self,*args):pass
def read(self,*args):return json.dumps(self.value).encode()
class Opener:
def open(self,*args,**kwargs):return Response(responses.pop(0))
with patch('sys.stdin',io.StringIO(json.dumps(request))),patch('sys.stdout',io.StringIO()),patch('socket.getaddrinfo',return_value=[(2,1,6,'',(resolved,443))]),patch('ssl.create_default_context'),patch('urllib.request.build_opener',return_value=Opener()):
exec(compile(a.JWKS_PROBE,'<jwks-probe>','exec',optimize=2),{})
def test_exact_original_public_key(self):self.execute()
def test_extra_signing_key_refused_under_optimization(self):
with self.assertRaises(ValueError):self.execute(extra=True)
def test_wrong_kid_refused_under_optimization(self):
with self.assertRaises(ValueError):self.execute(kid='replacement')
def test_wrong_live_signing_key_refused(self):
with self.assertRaises(ValueError):self.execute(n='124')
def test_cross_origin_jwks_refused(self):
with self.assertRaises(ValueError):self.execute(endpoint='https://production.example/jwks')
def test_production_dns_refused_before_fetch(self):
with self.assertRaises(ValueError):self.execute(resolved='192.0.2.1')
if __name__=='__main__':unittest.main()

View file

@ -0,0 +1,62 @@
import base64
import importlib.util
import json
from pathlib import Path
import unittest
from unittest.mock import patch
SOURCE=Path(__file__).parents[1]/'tools/bootstrap-recovery/isolated-keycape-session.py'
spec=importlib.util.spec_from_file_location('session',SOURCE)
s=importlib.util.module_from_spec(spec);spec.loader.exec_module(s)
class SessionTests(unittest.TestCase):
def test_tmpfs_plan_has_four_exact_original_mounts(self):
session=s.plan_session()
self.assertEqual({v['target'] for v in session['volumes'].values()},set(s.RUNROOTS))
self.assertTrue(all(v['options']==s.VOLUME_OPTIONS for v in session['volumes'].values()))
self.assertEqual(session['images'][session['issuer']],s.adapter.IMAGE)
def test_non_tmpfs_driver_options_refused(self):
session=s.plan_session();name,spec=next(iter(session['volumes'].items()))
wrong=[{'Name':name,'Driver':'local','Scope':'local','Options':{'type':'none','device':'/host/data'},'Labels':{s.adapter.LABEL:session['run_id']}}]
with patch.object(s,'run',return_value=json.dumps(wrong).encode()):
with self.assertRaises(s.adapter.Refused):s.verify_volume(name,spec,session['run_id'])
def test_owner_secret_is_not_docker_argv_and_import_is_after_inspection(self):
session=s.plan_session();events=[]
payload={'files':{'/etc/keycape/config.yaml':base64.b64encode(b'original').decode()},'env':{'KEYCAPE_TEST_SECRET':'fixture-sensitive'}}
def run(argv,**kwargs):
events.append(('run',argv,kwargs))
if argv[-1]=='{{.Id}}':return b'anchor'
return b''
with patch.object(s,'run',side_effect=run),patch.object(s,'verify_volume'),patch.object(s.adapter,'inspect_session',side_effect=lambda *args,**kwargs:events.append(('inspect',args,kwargs))):
s.provision(session,payload,{'/recovery-tls/trust.pem':b'original-trust'},'https://issuer.example')
joined=' '.join(' '.join(e[1]) for e in events if e[0]=='run')
self.assertNotIn('fixture-sensitive',joined)
private_index=next(i for i,e in enumerate(events) if e[0]=='run' and s.IMPORT in e[1])
inspect_index=next(i for i,e in enumerate(events) if e[0]=='inspect')
self.assertLess(inspect_index,private_index)
issuer_call=next(e for e in events if e[0]=='run' and e[1][-1]==s.adapter.IMAGE)
self.assertEqual(issuer_call[2]['env']['KEYCAPE_TEST_SECRET'],'fixture-sensitive')
self.assertIn('KEYCAPE_TEST_SECRET',issuer_call[1])
self.assertNotIn('--entrypoint',issuer_call[1])
def test_non_original_https_port_refused_before_any_mutation(self):
with patch.object(s,'run') as run:
with self.assertRaises(s.adapter.Refused):s.provision(s.plan_session(),{'files':{},'env':{}},{},'http://issuer.example')
run.assert_not_called()
def test_container_failure_still_attempts_each_volume_cleanup(self):
session=s.plan_session();receipt={};removed=[]
def run(argv,**kwargs):
if argv[1:3]==['volume','rm']:removed.append(argv[-1])
return b''
with patch.object(s.adapter,'cleanup',side_effect=s.adapter.Refused),patch.object(s,'verify_volume'),patch.object(s,'run',side_effect=run):
with self.assertRaises(s.adapter.Refused):s.cleanup_all(session,receipt)
self.assertEqual(set(removed),set(session['volumes']))
self.assertFalse(receipt['container_cleanup_verified'])
self.assertEqual(len(receipt['tmpfs_volume_cleanup_obligations']),4)
def test_reaper_inherits_no_custody_or_service_environment(self):
with patch.object(s.subprocess,'Popen') as spawn:
s.start_reaper(s.plan_session())
kwargs=spawn.call_args.kwargs
self.assertEqual(set(kwargs['env']),{'PATH','DOCKER_HOST'})
self.assertTrue(kwargs['close_fds']);self.assertTrue(kwargs['start_new_session'])
if __name__=='__main__':unittest.main()

View file

@ -0,0 +1,278 @@
#!/usr/bin/env python3
"""Owner-run T18 session assembly using the existing archive/containment pipeline.
Candidate only: native pinned-image tests and review are required before use.
Shared Docker local-driver tmpfs is required because Docker cp does not support
private tmpfs/mount paths, and the original KeyCape image is distroless.
"""
import argparse
import base64
from datetime import datetime, timezone
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import signal
import subprocess
import sys
import tempfile
import time
import uuid
HERE=Path(__file__).resolve().parent
def load(name):
spec=importlib.util.spec_from_file_location(name,HERE/(name+'.py'))
module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module);return module
adapter=load('isolated-keycape')
local=load('isolated-lldap')
bundle=local.bundle
WORKER=local.CLIENT
RUNROOTS=('/etc/keycape','/var/lib/keycape-policy','/etc/keycape-factor','/recovery-tls')
VOLUME_OPTIONS={'type':'tmpfs','device':'tmpfs','o':'size=33554432,uid=1000,gid=1000,mode=0700,nosuid,nodev,noexec'}
IMPORT=r'''
import base64,json,os,sys
from pathlib import Path
os.umask(0o077)
p=json.load(sys.stdin)
allowed=('/etc/keycape/','/var/lib/keycape-policy/','/etc/keycape-factor/','/recovery-tls/')
for name,encoded in p.items():
f=Path(name)
if not name.startswith(allowed) or '..' in f.parts or any(x.is_symlink() for x in (f,*f.parents)):raise ValueError()
f.parent.mkdir(mode=0o700,parents=True,exist_ok=True)
fd=os.open(f,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
with os.fdopen(fd,'wb') as out:out.write(base64.b64decode(encoded,validate=True))
'''
# Small transport inside the already pinned helper; it has only loopback routes.
# No token handling, auth bypass, dependency imitation, reset or notifier path.
TLS=r'''
import select,socket,socketserver,ssl,threading
context=ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.minimum_version=ssl.TLSVersion.TLSv1_2
context.load_cert_chain('/recovery-tls/server.pem','/recovery-tls/server-key.pem')
class Handler(socketserver.BaseRequestHandler):
def handle(self):
try:
with context.wrap_socket(self.request,server_side=True) as incoming:
with socket.create_connection(('127.0.0.1',8080),timeout=10) as outgoing:
incoming.settimeout(15);outgoing.settimeout(15)
while True:
ready,_,_=select.select([incoming,outgoing],[],[],15)
if not ready:return
for source in ready:
data=source.recv(65536)
if not data:return
(outgoing if source is incoming else incoming).sendall(data)
except Exception:pass
class Server(socketserver.ThreadingTCPServer):
allow_reuse_address=True
daemon_threads=True
with Server(('127.0.0.1',443),Handler) as server:server.serve_forever()
'''
def run(argv,**kwargs):return adapter.command(argv,**kwargs)
def plan_session():
run_id=uuid.uuid4().hex
stem='nk49-kc-'+run_id
worker=stem+'-transport';issuer=stem+'-issuer'
volumes={stem+'-v'+str(i):{'target':root,'options':dict(VOLUME_OPTIONS)} for i,root in enumerate(RUNROOTS)}
return {'run_id':run_id,'containers':[worker,issuer], 'worker':worker,'issuer':issuer,
'images':{worker:WORKER,issuer:adapter.IMAGE},'volumes':volumes}
def verify_volume(name,spec,run_id):
obj=json.loads(run(['docker','volume','inspect',name]))
if (len(obj)!=1 or obj[0]['Name']!=name or obj[0]['Driver']!='local' or obj[0].get('Scope')!='local'
or obj[0].get('Options')!=VOLUME_OPTIONS or spec['options']!=VOLUME_OPTIONS
or obj[0].get('Labels',{}).get(adapter.LABEL)!=run_id):raise adapter.Refused()
def base_args(session,name,network):
return ['docker','run','--detach','--pull=never','--name',name,'--network',network,
'--label',adapter.LABEL+'='+session['run_id'],'--read-only','--cap-drop','ALL',
'--security-opt','no-new-privileges','--user','1000:1000','--memory','512m','--cpus','1',
'--pids-limit','64','--log-driver','none','--restart','no',
'--tmpfs','/tmp:rw,nosuid,nodev,noexec,size=16777216,uid=1000,gid=1000,mode=0700']
def mounts(session,readonly=False):
args=[]
for name,spec in session['volumes'].items():
args+=['--mount','type=volume,source='+name+',target='+spec['target']+(',readonly' if readonly else '')]
return args
def provision(session,payload,tls,issuer):
"""Inspect containment and driver options before the first private import."""
from urllib.parse import urlsplit
address=urlsplit(issuer)
if address.scheme!='https' or not address.hostname or address.port not in (None,443) or address.path not in ('','/'):
raise adapter.Refused()
for name,spec in session['volumes'].items():
run(['docker','volume','create','--driver','local','--label',adapter.LABEL+'='+session['run_id'],
'--opt','type=tmpfs','--opt','device=tmpfs','--opt','o='+VOLUME_OPTIONS['o'],name])
verify_volume(name,spec,session['run_id'])
args=base_args(session,session['worker'],'none')+mounts(session)
args+=['--sysctl','net.ipv4.ip_unprivileged_port_start=0','--add-host',address.hostname+':127.0.0.1',
'--entrypoint','python3',WORKER,'-c','import time;time.sleep(1800)']
run(args)
# Check helper and every private filesystem before touching the ciphertext's
# private contents. Issuer is created only after originals are present.
adapter.inspect_session({**session,'containers':[session['worker']],'issuer':session['worker']},worker_only=True)
files={**payload['files'],**{k:base64.b64encode(v).decode() for k,v in tls.items()}}
run(['docker','exec','-i',session['worker'],'python3','-c',IMPORT],input=json.dumps(files).encode())
anchor=run(['docker','container','inspect',session['worker'],'--format','{{.Id}}']).decode().strip()
args=base_args(session,session['issuer'],'container:'+anchor)+mounts(session,readonly=True)
# CLI argv carries environment names only. Original credentials stay in the
# owner subprocess environment and Docker's private container configuration.
env={'PATH':os.environ.get('PATH',os.defpath),'DOCKER_HOST':'unix:///var/run/docker.sock',**payload['env'],
'SSL_CERT_FILE':'/recovery-tls/trust.pem'}
for name in (*payload['env'],'SSL_CERT_FILE'):args+=['--env',name]
args+=[adapter.IMAGE]
run(args,env=env)
adapter.inspect_session(session)
run(['docker','exec','--detach',session['worker'],'python3','-c',TLS])
def cleanup_all(session,receipt):
failed=False
try:adapter.cleanup(session,receipt)
except Exception:failed=True
rows=[]
for name,spec in session['volumes'].items():
row={'volume':name,'status':'unverified'};rows.append(row)
try:
verify_volume(name,spec,session['run_id'])
run(['docker','volume','rm',name])
remaining=run(['docker','volume','ls','--filter','name=^'+name+'$','--format','{{.Name}}']).decode().splitlines()
if remaining:row['status']='still_present';failed=True
else:row['status']='absence_verified'
except Exception:failed=True
receipt['tmpfs_volume_cleanup_obligations']=rows
receipt['container_cleanup_verified']=not failed
if failed:raise adapter.Refused()
def start_reaper(session):
# Scoped cleanup child contains only public exact run identifiers, no custody
# descriptors or secret environment. It survives loss of the parent owner
# process; normal successful cleanup cancels it. This is not a standing job.
code="""import importlib.util,json,sys,time
spec=importlib.util.spec_from_file_location('session',sys.argv[1]);m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
time.sleep(1800)
try:m.cleanup_all(json.loads(sys.argv[2]),{})
except Exception:pass
"""
return subprocess.Popen([sys.executable,'-c',code,str(Path(__file__).resolve()),json.dumps(session)],
stdin=subprocess.DEVNULL,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,
env={'PATH':os.environ.get('PATH',os.defpath),'DOCKER_HOST':'unix:///var/run/docker.sock'},
close_fds=True,start_new_session=True)
def execute(transfer,admission,tls,receipt):
"""Existing private custody/transfer/archive APIs; no second archive format."""
target=local.preflight()
for image in (adapter.IMAGE,):run(['docker','image','inspect',image])
local.offline.validate(transfer)
if transfer.get('independent_owner_retrieval_verified') is not True:raise adapter.Refused()
path=Path(transfer['fetched_path']);bundle.private(path,max_bytes=bundle.LIMIT)
if (path.stat().st_size!=transfer['ciphertext_bytes'] or local.offline.offsite.digest(path)!=transfer['ciphertext_sha256']
or admission['ciphertext_sha256']!=transfer['ciphertext_sha256']):raise adapter.Refused()
if hashlib.sha256(tls['/recovery-tls/trust.pem']).hexdigest()!=admission['trust_sha256']:raise adapter.Refused()
if hashlib.sha256(tls['/recovery-tls/server.pem']).hexdigest()!=admission['tls_cert_sha256']:raise adapter.Refused()
# Validate exact private certificate/key pair and original issuer name before
# opening service containers; do not generate replacement trust or keys.
from cryptography import x509
from cryptography.hazmat.primitives import serialization
cert=x509.load_pem_x509_certificate(tls['/recovery-tls/server.pem'])
key=serialization.load_pem_private_key(tls['/recovery-tls/server-key.pem'],password=None)
if cert.public_key().public_numbers()!=key.public_key().public_numbers():raise adapter.Refused()
keyfd=local.verify.custody.private_key_fd();work=None;session=None;reaper=None
receipt.update(status='failed',target=target,session_source_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
source_files_sha256={name:hashlib.sha256((HERE/name).read_bytes()).hexdigest() for name in ('isolated-keycape.py','isolated-keycape-session.py','run-isolated-keycape.sh','isolated-lldap.py','identity-bundle.py','verify-identity.py','retrieve-identity-offline.py','identity-offsite.py')},
temporary_plaintext_cleanup_verified=False,container_cleanup_verified=False,
downstream_consumer_reconnect_proven=False,human_mfa_proven=False)
try:
if run(['age-keygen','-y',f'/proc/self/fd/{keyfd}'],pass_fds=(keyfd,)).strip()!=bundle.profile()['public_recipient'].encode():raise adapter.Refused()
with tempfile.TemporaryDirectory(prefix='nk49-keycape-',dir='/dev/shm') as temporary:
work=Path(temporary);archive=work/'archive.tar'
with archive.open('xb') as output:
p=subprocess.run(['age','-d','-i',f'/proc/self/fd/{keyfd}',str(path)],stdout=output,stderr=subprocess.DEVNULL,pass_fds=(keyfd,),timeout=180)
if p.returncode or archive.stat().st_size>bundle.LIMIT+4*1024*1024:raise adapter.Refused()
root=work/'restored';root.mkdir(mode=0o700)
manifest,_=bundle.restore(archive,root)
payload=adapter.private_payload(root,manifest,admission['manifest_sha256'])
import yaml
cfg=yaml.safe_load(base64.b64decode(payload['files']['/etc/keycape/config.yaml']))
if cfg.get('port')!=8080 or cfg.get('issuer')!=admission['issuer']:raise adapter.Refused()
# Public executable readback uses the daemon archive API only on the
# original image rootfs, never a tmpfs or private user-created mount.
session=plan_session()
reaper=start_reaper(session)
receipt['bounded_cleanup_reaper_started']=True
probe='nk49-kc-'+session['run_id']+'-image-probe'
try:
run(['docker','create','--name',probe,'--network','none','--read-only','--label',adapter.LABEL+'='+session['run_id'],adapter.IMAGE])
binary=adapter.container_file(probe,'/keycape')
if hashlib.sha256(binary).hexdigest()!=admission['verifier_sha256']:raise adapter.Refused()
finally:
receipt['public_image_probe_cleanup_obligations']=[{'container':probe,'status':'unverified'}]
run(['docker','rm',probe])
if run(['docker','container','ls','--all','--filter','name=^/'+probe+'$','--format','{{.Names}}']).strip():raise adapter.Refused()
receipt['public_image_probe_cleanup_obligations'][0]['status']='absence_verified'
provision(session,payload,tls,admission['issuer'])
receipt.update(ciphertext_sha256=transfer['ciphertext_sha256'],all_manifest_hashes_verified=True,manifest_file_count=len(manifest['files']))
checked_admission={**admission,'session':session,'trust_path':'/recovery-tls/trust.pem'}
# Native checker consumes no service secret argument or new grant.
# Retry only bounded public readiness; no account/factor attempt.
for attempt in range(40):
try:
numbers=serialization.load_pem_private_key(base64.b64decode(payload['files']['/etc/keycape/key.pem']),password=None).public_key().public_numbers()
adapter.check_jwks(session,checked_admission,numbers);break
except Exception:
if attempt==39:raise
time.sleep(.25)
adapter.native_checks(root,manifest,checked_admission,receipt,cleanup_session=False)
finally:
os.close(keyfd)
try:
if session is not None:cleanup_all(session,receipt)
if reaper is not None:
reaper.terminate();reaper.wait(timeout=10)
except Exception:
receipt['status']='failed'
raise
finally:
if work is not None and not work.exists():receipt['temporary_plaintext_cleanup_verified']=True
def main():
p=argparse.ArgumentParser(description=__doc__)
for name in ('transfer-receipt','admission','admission-sha256','tls-cert','tls-key','trust','receipt'):p.add_argument('--'+name,required=True)
args=p.parse_args();os.umask(0o077)
fd=os.open(args.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
receipt={'schema':'netkingdom.isolated-keycape-recovery.v1','status':'failed','production_contacted':False,'secret_values_exported':False}
try:
raw=Path(args.admission).read_bytes()
if hashlib.sha256(raw).hexdigest()!=args.admission_sha256:raise adapter.Refused()
admission=json.loads(raw)
tls={}
for name,path in (('server.pem',args.tls_cert),('server-key.pem',args.tls_key),('trust.pem',args.trust)):
source=Path(path);bundle.private(source,max_bytes=1024*1024);tls['/recovery-tls/'+name]=source.read_bytes()
execute(json.loads(Path(args.transfer_receipt).read_text()),admission,tls,receipt)
except (Exception,KeyboardInterrupt):receipt.update(status='failed',error='isolated_keycape_failed_private_diagnostics_suppressed')
finally:
receipt['finished_at']=datetime.now(timezone.utc).isoformat()
with os.fdopen(fd,'w') as out:json.dump(receipt,out,indent=2);out.write('\n')
print('OK: isolated KeyCape native checks and cleanup passed.' if receipt['status']=='isolated_keycape_native_registration_checked' else 'ERROR: isolated KeyCape stopped; review safe receipt.')
return int(receipt['status']!='isolated_keycape_native_registration_checked')
if __name__=='__main__':
for sig in (signal.SIGHUP,signal.SIGTERM,signal.SIGINT):signal.signal(sig,local.offline.interrupted)
raise SystemExit(main())

View file

@ -0,0 +1,391 @@
#!/usr/bin/env python3
"""T18 native adapter for an owner-admitted, already restored isolated session.
Called after identity-bundle.restore, never with an unvalidated directory. This
module does not provision TLS, grant credentials, or claim downstream acceptance.
The existing decrypt/tmpfs wrapper remains responsible for archive cleanup.
"""
import base64
import hashlib
import importlib.util
import json
import io
import secrets
import tarfile
import re
import subprocess
from pathlib import Path
HERE = Path(__file__).resolve().parent
IMAGE = 'forgejo.coulomb.social/coulomb/key-cape@sha256:cdec9dd7c515f7e0a6169fc3fa560ee2cc47bf40d899755dec808c362f5ce010'
LABEL = 'net-kingdom.recovery-run'
class Refused(Exception):
pass
def load_bundle():
spec = importlib.util.spec_from_file_location('identity_bundle', HERE / 'identity-bundle.py')
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def private_payload(root, manifest, expected_manifest_sha256):
"""Bind the complete validated archive before selecting KeyCape material.
Private hashes never enter the public receipt. The digest is supplied by the
existing private archive admission, not inferred from an untrusted manifest.
"""
encoded = json.dumps(manifest, sort_keys=True, separators=(',', ':')).encode()
if hashlib.sha256(encoded).hexdigest() != expected_manifest_sha256:
raise Refused()
entries = manifest['files']
if len(entries) != len({e['path'] for e in entries}):
raise Refused()
values = {}
bundle = load_bundle()
for entry in entries:
if not bundle.allowed(entry['path'], manifest['profile']):
raise Refused()
path = root / entry['path']
bundle.private(path)
value = path.read_bytes()
if len(value) != entry['bytes'] or hashlib.sha256(value).hexdigest() != entry['sha256']:
raise Refused()
values[entry['path']] = value
deployment = json.loads(values['resources/sso/deployment/keycape.json'])
main = next(c for c in deployment['spec']['template']['spec']['containers'] if c['name'] == 'keycape')
if main.get('command') or main.get('args') or main.get('envFrom'):
raise Refused()
if main['image'] != IMAGE:
# Mutable-tag/historical mismatches need a separate explicit owner admission.
raise Refused()
secret = json.loads(values['resources/sso/secret/keycape-config.json'])['data']
files = {'/etc/keycape/' + name: base64.b64decode(secret[name], validate=True)
for name in ('config.yaml', 'key.pem')}
for path, value in values.items():
if path.startswith('data/keycape/'):
relative = path.removeprefix('data/keycape/')
files['/var/lib/keycape-policy/' + relative] = value
if '/var/lib/keycape-policy/policy.json' not in files:
raise Refused()
if 'resources/sso/secret/keycape-factor-read.json' in values:
factor=json.loads(values['resources/sso/secret/keycape-factor-read.json'])['data']
files['/etc/keycape-factor/admin-token']=base64.b64decode(factor['admin-token'],validate=True)
# Resolve original delivered environment, including all referenced secrets.
env = {}
for item in main.get('env', []):
if not re.fullmatch(r'KEYCAPE_[A-Z0-9_]+',item['name']):raise Refused()
if item['name'] in env:
raise Refused()
if 'value' in item:
env[item['name']] = item['value']
else:
ref = item['valueFrom']['secretKeyRef']
source = json.loads(values['resources/sso/secret/' + ref['name'] + '.json'])['data']
env[item['name']] = base64.b64decode(source[ref['key']], validate=True).decode()
if env.get('KEYCAPE_CONFIG') != '/etc/keycape/config.yaml' or env.get('KEYCAPE_POLICY_PATH') != '/var/lib/keycape-policy/policy.json':
raise Refused()
return {'files': {p: base64.b64encode(v).decode() for p, v in files.items()}, 'env': env}
def command(argv, **kwargs):
maximum = kwargs.pop('max_output', 32768)
result = subprocess.run(argv, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
timeout=kwargs.pop('timeout', 180), **kwargs)
if result.returncode or len(result.stdout) > maximum:
raise Refused()
return result.stdout
def inspect_session(session, worker_only=False):
"""No credential environment is read by Docker inspect or returned to caller."""
if not re.fullmatch(r'[a-f0-9]{32}', session['run_id']):
raise Refused()
names = session['containers']
if not names or len(names) > 12 or len(set(names)) != len(names):
raise Refused()
for name in names:
if not re.fullmatch(r'nk49-kc-' + session['run_id'] + r'-[a-z0-9-]{1,24}', name):
raise Refused()
if session['worker'] not in names or session['issuer'] not in names:
raise Refused()
import os
contexts=json.loads(command(['docker','context','inspect']))
endpoints=[contexts[0]['Endpoints']['docker']['Host'],os.environ.get('DOCKER_HOST','unix:///var/run/docker.sock')]
if not all(x.startswith('unix://') for x in endpoints):raise Refused()
info = json.loads(command(['docker', 'info', '--format', '{"ID":{{json .ID}},"Name":{{json .Name}},"OSType":{{json .OSType}}}']))
if info != {'ID': '74f93783-6cef-43a8-a3c4-f62586cd3bc1', 'Name': 'bnt-lap001', 'OSType': 'linux'}:
raise Refused()
# The worker is the anchor; all dependencies/TLS/issuer share its network-none namespace.
anchor = command(['docker', 'container', 'inspect', session['worker'], '--format', '{{.Id}}']).decode().strip()
for name,spec in session.get('volumes',{}).items():
volume=json.loads(command(['docker','volume','inspect',name]))
expected={'type':'tmpfs','device':'tmpfs','o':'size=33554432,uid=1000,gid=1000,mode=0700,nosuid,nodev,noexec'}
if len(volume)!=1 or volume[0]['Driver']!='local' or volume[0].get('Scope')!='local' or volume[0].get('Options')!=expected or spec['options']!=expected or volume[0].get('Labels',{}).get(LABEL)!=session['run_id']:raise Refused()
for name in names:
obj = json.loads(command(['docker', 'container', 'inspect', name, '--format',
'{"host":{{json .HostConfig}},"mounts":{{json .Mounts}},"labels":{{json .Config.Labels}},"image":{{json .Config.Image}},"user":{{json .Config.User}},"entrypoint":{{json .Config.Entrypoint}},"cmd":{{json .Config.Cmd}}}']))
host = obj['host']
mounts=obj['mounts']
destinations=[m['Destination'] for m in mounts]
if len(destinations)!=len(set(destinations)):raise Refused()
for volume,expected_spec in session.get('volumes',{}).items():
matches=[m for m in mounts if m['Destination']==expected_spec['target']]
if len(matches)!=1 or matches[0]['Type']!='volume' or matches[0]['Name']!=volume or matches[0]['RW']!=(name==session['worker']):raise Refused()
if (obj['labels'].get(LABEL) != session['run_id'] or not obj['user'] or obj['user'].split(':')[0] in ('0', 'root')
or not host['ReadonlyRootfs'] or host['Privileged'] or host.get('Binds') or host.get('PortBindings')
or host.get('PidMode') or host.get('IpcMode') == 'host'
or host.get('UTSMode') or host.get('UsernsMode') == 'host' or host.get('CgroupnsMode') == 'host'
or host.get('Sysctls',{}) not in ({},{'net.ipv4.ip_unprivileged_port_start':'0'} if name==session['worker'] else {})
or host.get('CapAdd') or host.get('Devices') or host.get('DeviceRequests') or host.get('VolumesFrom')
or host.get('CapDrop') != ['ALL']
or host.get('SecurityOpt') not in (['no-new-privileges'], ['no-new-privileges:true'])
or any(m['Type']!='tmpfs' and not (m['Type']=='volume' and m['Name'] in session.get('volumes',{}) and m['Destination']==session['volumes'][m['Name']]['target']) for m in obj['mounts'])
or host['NetworkMode'] != ('none' if name == session['worker'] else 'container:' + anchor)
or obj['image'] != session['images'][name] or '@sha256:' not in obj['image']):
raise Refused()
if worker_only:return
if session['images'][session['issuer']] != IMAGE:
raise Refused()
# Known admitted archive has no command/args override; image default /keycape
# must be the actual issuer executable, not an alternate same-image command.
launch=json.loads(command(['docker','container','inspect',session['issuer'],'--format',
'{"entrypoint":{{json .Config.Entrypoint}},"cmd":{{json .Config.Cmd}},"path":{{json .Path}},"args":{{json .Args}}}']))
if launch != {'entrypoint':['/keycape'],'cmd':None,'path':'/keycape','args':[]}:
raise Refused()
def container_file(container, path):
# Docker streams one tar member into memory; never materialize private files.
raw = command(['docker', 'cp', container + ':' + path, '-'], max_output=128*1024*1024)
with tarfile.open(fileobj=io.BytesIO(raw), mode='r:') as archive:
members = archive.getmembers()
if len(members) != 1 or not members[0].isfile() or members[0].size > 96*1024*1024:
raise Refused()
return archive.extractfile(members[0]).read()
JWKS_PROBE = r"""
import base64,ipaddress,json,socket,ssl,sys,urllib.request
from urllib.parse import urlsplit
p=json.load(sys.stdin);issuer=p['issuer'];origin=urlsplit(issuer)
def require(condition):
if not condition:raise ValueError('public_jwks_binding_refused')
require(origin.scheme=='https')
resolved=socket.getaddrinfo(origin.hostname,origin.port or 443,type=socket.SOCK_STREAM)
require(bool(resolved) and all(ipaddress.ip_address(x[4][0]).is_loopback for x in resolved))
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self,*args,**kwargs):return None
opener=urllib.request.build_opener(urllib.request.ProxyHandler({}),NoRedirect(),
urllib.request.HTTPSHandler(context=ssl.create_default_context(cadata=p['trust'])))
def get(url):
u=urlsplit(url)
require((u.scheme,u.hostname,u.port or 443)==(origin.scheme,origin.hostname,origin.port or 443))
require(not u.username and not u.password and not u.fragment)
with opener.open(url,timeout=10) as response:
raw=response.read(1024*1024+1);require(len(raw)<=1024*1024)
return json.loads(raw)
d=get(issuer.rstrip('/')+'/.well-known/openid-configuration');require(d['issuer']==issuer)
keys=get(d['jwks_uri'])['keys']
def integer(value):return int.from_bytes(base64.urlsafe_b64decode(value+'='*(-len(value)%4)),'big')
# The exact admitted owner release constructs one key, kid key-1. Reject every
# extra key, including irrelevant keys: a native successful exchange can then
# only have used the original signer through the existing verifier.
require(isinstance(keys,list) and len(keys)==1)
k=keys[0]
require(k.get('kty')=='RSA' and k.get('kid')=='key-1' and k.get('use')=='sig' and k.get('alg')=='RS256')
require(integer(k['n'])==int(p['n']) and integer(k['e'])==int(p['e']))
print('{"original_public_signing_key_in_live_jwks":true}')
"""
def check_jwks(session, admission, public_numbers):
trust=runtime_file(session,admission['trust_path']).decode()
request={'issuer':admission['issuer'],'trust':trust,'n':str(public_numbers.n),'e':str(public_numbers.e)}
result=json.loads(command(['docker','exec','-i',session['worker'],'python3','-c',JWKS_PROBE],input=json.dumps(request).encode()))
if result != {'original_public_signing_key_in_live_jwks': True}:raise Refused()
RUNTIME_FILES = r"""
import base64,json,sys
from pathlib import Path
p=json.load(sys.stdin)
roots=('/etc/keycape/','/var/lib/keycape-policy/','/etc/keycape-factor/','/recovery-tls/')
result={}
paths=[]
if p.get('tree'):
paths=list(Path('/var/lib/keycape-policy').rglob('*'))
else:paths=[Path(x) for x in p['paths']]
for f in paths:
if f.is_dir():continue
if not str(f).startswith(roots) or not f.is_file() or any(x.is_symlink() for x in (f,*f.parents)) or f.stat().st_size>8*1024*1024:raise ValueError()
result[str(f)]=base64.b64encode(f.read_bytes()).decode()
if len(result)>4096:raise ValueError()
print(json.dumps(result))
"""
def runtime_files(session, paths=(), tree=False):
raw=command(['docker','exec','-i',session['worker'],'python3','-c',RUNTIME_FILES],
input=json.dumps({'paths':list(paths),'tree':tree}).encode(),max_output=48*1024*1024)
return {k:base64.b64decode(v,validate=True) for k,v in json.loads(raw).items()}
def runtime_file(session,path):
return runtime_files(session,[path])[path]
def run_verifier(session, admission, payload):
issuer = session['issuer']
if {v['target'] for v in session.get('volumes',{}).values()}!={'/etc/keycape','/var/lib/keycape-policy','/etc/keycape-factor','/recovery-tls'}:raise Refused()
from urllib.parse import urlsplit
import ipaddress
address = urlsplit(admission['issuer'])
if (address.scheme != 'https' or not address.hostname or address.username or address.password
or address.query or address.fragment):
raise Refused()
if hashlib.sha256(container_file(issuer, '/keycape')).hexdigest() != admission['verifier_sha256']:
raise Refused()
if hashlib.sha256(runtime_file(session, admission['trust_path'])).hexdigest() != admission['trust_sha256']:
raise Refused()
# Capture private environment only in owner process memory, never receipt.
environment = json.loads(command(['docker','container','inspect',issuer,'--format','{{json .Config.Env}}']))
env = dict(item.split('=',1) for item in environment)
if len(env)!=len(environment):raise Refused()
if set(env)-set(payload['env'])-{'PATH','HOME','HOSTNAME','SSL_CERT_FILE'}:raise Refused()
if env.get('SSL_CERT_FILE') != admission['trust_path'] or any(k.lower().endswith('_proxy') and v for k,v in env.items()):
raise Refused()
if {k:v for k,v in env.items() if k.startswith('KEYCAPE_')}!={k:v for k,v in payload['env'].items() if k.startswith('KEYCAPE_')}:raise Refused()
for name,value in payload['env'].items():
if env.get(name) != value:
raise Refused()
files = {k:base64.b64decode(v,validate=True) for k,v in payload['files'].items()}
def equal():
if runtime_files(session,files)!=files:raise Refused()
durable=runtime_files(session,tree=True)
if durable!={k:v for k,v in files.items() if k.startswith('/var/lib/keycape-policy/')}:
raise Refused()
equal()
# Reading YAML is data parsing only; no archived executable is evaluated.
import yaml
cfg = yaml.safe_load(files['/etc/keycape/config.yaml'])
if cfg['issuer'] != admission['issuer']:
raise Refused()
consumer = admission['consumer']
client = next(x for x in cfg['clients'] if x['clientId']==consumer['client_id'])
if (client['secretRef']!='env:'+consumer['secret_env'] or client['clientType']!='confidential'
or 'client_credentials' not in client['grantTypes'] or not env.get(consumer['secret_env'])):
raise Refused()
from cryptography.hazmat.primitives import serialization
key=serialization.load_pem_private_key(files['/etc/keycape/key.pem'],password=None)
check_jwks(session,admission,key.public_key().public_numbers())
wrong = secrets.token_urlsafe(48)
if wrong == env[consumer['secret_env']]:raise Refused()
args = ['docker','exec','--env','NK49_UNRELATED_NEGATIVE_SECRET='+wrong,issuer,'/keycape','verify-client',
'-issuer',admission['issuer'],'-previous-secret-env','NK49_UNRELATED_NEGATIVE_SECRET']
flags = {'client_id':'client-id','secret_env':'secret-env','audience':'audience','scope':'scope',
'subject':'expect-subject','tenant':'expect-tenant','roles':'expect-roles','deny_scope':'deny-scope'}
for name,flag in flags.items():args+=['-'+flag,consumer[name]]
# Native command reports on stderr. Do not return or print its raw response.
result = subprocess.run(args,stdout=subprocess.PIPE,stderr=subprocess.PIPE,timeout=150)
raw = (result.stdout+result.stderr).decode()
required = ['discovery and endpoint origin','exchange and JWKS signature for granted scopes','access-token claim bindings',
'principal_type is service','sub matches the registration','tenant matches the registration','roles present',
'granted scope carries no excess grant','predecessor secret refused']
required += ['excess scope refused: '+scope for scope in consumer['deny_scope'].split()]
if result.returncode or any('PASS '+name+'\n' not in raw for name in required) or 'FAIL ' in raw:
raise Refused()
equal()
from cryptography.hazmat.primitives import serialization
key=serialization.load_pem_private_key(files['/etc/keycape/key.pem'],password=None)
check_jwks(session,admission,key.public_key().public_numbers())
public=key.public_key().public_bytes(serialization.Encoding.DER,serialization.PublicFormat.SubjectPublicKeyInfo)
return {'native_service_registration_verified':True,'wrong_unrelated_secret_explicitly_refused':True,
'denied_scopes_refused':True,'original_config_registry_policy_receipts_equal':True,
'public_signing_spki_sha256':hashlib.sha256(public).hexdigest(),
'original_https_issuer_and_trust_preserved':True,'original_public_signing_key_in_live_jwks':True}
def cleanup(session, receipt=None):
"""Attempt every owned cleanup even when one obligation fails.
Preserve exact safe container names and outcomes so the owner can recover an
uncertain/partial cleanup. An inspect/daemon failure never means absence.
"""
obligations = []
failed = False
for name in reversed(session['containers']):
row = {'container': name, 'status': 'unverified'}
obligations.append(row)
try:
label = command(['docker', 'container', 'inspect', name, '--format',
'{{index .Config.Labels "' + LABEL + '"}}']).decode().strip()
if label != session['run_id']:
row['status'] = 'ownership_mismatch_not_removed'
failed = True
continue
# A failed remove is followed by a successful explicit inventory;
# interrupted remove may actually have completed.
try:
command(['docker', 'rm', '--force', '--volumes', name])
except Exception:
pass
remaining = command(['docker','container','ls','--all','--filter',
'name=^/' + name + '$','--format','{{.Names}}']).decode().splitlines()
if remaining:
row['status'] = 'still_present'
failed = True
else:
row['status'] = 'absence_verified'
except Exception:
failed = True
if receipt is not None:
receipt['container_cleanup_obligations'] = obligations
if failed:
raise Refused()
def native_checks(root, manifest, admission, receipt, cleanup_session=True):
"""Owner wrapper provides a reviewed session after existing restore pipeline.
Admission must bind ciphertext, canonical private manifest digest, executable
and trust bytes, exact image/target list, original issuer and selected consumer.
None of these admission fields grant authority or carry a client secret.
"""
session = admission['session']
# Validate ownership/containment before permitting either exec or cleanup.
inspect_session(session)
receipt.update(status='failed', source_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
ciphertext_sha256=admission['ciphertext_sha256'], production_contacted=False,
human_mfa_proven=False, downstream_consumer_reconnect_proven=False,
container_cleanup_verified=False)
try:
for field in ('ciphertext_sha256', 'manifest_sha256', 'verifier_sha256', 'trust_sha256'):
if not re.fullmatch('[a-f0-9]{64}', admission[field]):
raise Refused()
for field in ('client_id','secret_env','audience','scope','subject','tenant','roles','deny_scope'):
if not isinstance(admission['consumer'].get(field), str) or not admission['consumer'][field].strip():
raise Refused()
payload = private_payload(root, manifest, admission['manifest_sha256'])
result = run_verifier(session, admission, payload)
required = {'native_service_registration_verified','wrong_unrelated_secret_explicitly_refused',
'denied_scopes_refused','original_config_registry_policy_receipts_equal',
'original_https_issuer_and_trust_preserved','original_public_signing_key_in_live_jwks'}
if any(result.get(k) is not True for k in required) or not re.fullmatch('[a-f0-9]{64}', result.get('public_signing_spki_sha256','')):
raise Refused()
# Closed receipt allowlist: never copy arbitrary worker output.
receipt.update({k: True for k in required})
receipt['public_signing_spki_sha256'] = result['public_signing_spki_sha256']
receipt['status'] = 'isolated_keycape_native_registration_checked'
finally:
try:
if cleanup_session:
cleanup(session, receipt)
receipt['container_cleanup_verified'] = True
except Exception:
receipt['status'] = 'failed'
raise

View file

@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Candidate owner launcher. Review and native fixture proof required before use.
set +x
set -euo pipefail
ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
cd -- "$ROOT"
umask 077
[[ $# == 6 ]] || { printf '%s\n' 'Usage: run-isolated-keycape.sh VERIFIED_TRANSFER_RECEIPT ADMISSION ADMISSION_SHA256 TLS_CERT TLS_KEY TRUST'; exit 1; }
[[ "$3" =~ ^[a-f0-9]{64}$ ]] || { printf '%s\n' 'ERROR: exact reviewed admission digest required.'; exit 1; }
for index in 1 2 4 5 6; do
[[ -f "${!index}" && ! -L "${!index}" ]] || { printf '%s\n' 'ERROR: provide regular owner-controlled input files.'; exit 1; }
done
[[ -t 0 && -t 1 && -t 2 ]] || { printf '%s\n' 'ERROR: run in your own interactive terminal.'; exit 1; }
for tool in python3 age age-keygen docker; do
command -v "$tool" >/dev/null || { printf 'ERROR: required tool unavailable: %s\n' "$tool"; exit 1; }
done
python3 - <<'PY'
import importlib.util
from pathlib import Path
p=Path('tools/bootstrap-recovery/isolated-keycape-session.py')
spec=importlib.util.spec_from_file_location('session',p)
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
try:
m.local.preflight()
m.run(['docker','image','inspect',m.adapter.IMAGE])
except Exception:
raise SystemExit('ERROR: exact local Docker/images/tmpfs preflight failed; no key requested.')
print('OK: fixed local target and cached pinned images available.')
PY
NK49_KC_RUN="$(mktemp -d /tmp/nk49-isolated-keycape-XXXXXXXX)"
printf 'Safe evidence directory: %s\n' "$NK49_KC_RUN"
printf '%s\n' 'Owner-reviewed candidate: isolated original issuer/trust and current registered service-client checks only.'
printf '%s\n' 'No browser, downstream consumer or retained-factor MFA acceptance is claimed.'
printf '%s\n' 'Enter only the independently held backup age key at the hidden prompt.'
bash sso-mfa/bootstrap/sops-custody-unlock.sh \
--expected-recipient age1zvryunvjhvpkmasskauga2heeg0ztnte9ymgppvjge36ekumk50syr3tsz \
--prompt -- python3 tools/bootstrap-recovery/isolated-keycape-session.py \
--transfer-receipt "$1" --admission "$2" --admission-sha256 "$3" \
--tls-cert "$4" --tls-key "$5" --trust "$6" --receipt "$NK49_KC_RUN/recovery.json"
printf 'Safe receipt: %s/recovery.json\n' "$NK49_KC_RUN"

View file

@ -0,0 +1,19 @@
# Frozen revision3 — known deadline race, not approved
This exact snapshot passed52 synthetic tests. No Docker mutations, credentials,
actual recovery or canonical changes occurred. It preserves the target/plaintext
registration fixes while retaining the known deadline race: a live parent or
in-flight daemon request can outlast fixed-timer cleanup. Do not execute its
owner launcher. A successor is being prepared.
Reproduce without racing edits:
```sh
python3 -m unittest discover -s /home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/identity-restore-adapter-next/revisions/r3/test-root/tests -q
```
`dependency-closure-sha256.json` binds every source/test/profile in the frozen
test root, including unchanged existing pipeline dependencies. These are source
and synthetic proof only, with Docker/native outcomes mocked. Original issuer
hostname resolution in container-network sharing still requires native fixture
proof after review.

View file

@ -0,0 +1,210 @@
#!/usr/bin/env python3
"""Age-only SOPS recovery to a new private file; no plaintext process output."""
from __future__ import annotations
import json
import os
from pathlib import Path
import re
import shutil
import signal
import stat
import subprocess
import sys
import tempfile
import uuid
ROOT = Path(__file__).resolve().parents[2]
RECIPIENT = re.compile(r'age1[023456789acdefghjklmnpqrstuvwxyz]{58}\Z')
class Refused(Exception):
pass
def public_recipient():
lines = (ROOT / 'keys/age.pub').read_text().splitlines()
values = [line.strip() for line in lines if line.strip() and not line.startswith('#')]
if len(values) != 1 or not RECIPIENT.fullmatch(values[0]):
raise Refused('registered public recipient is invalid')
result = subprocess.run(['age', '-r', values[0]], input=b'', stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, timeout=10)
if result.returncode:
raise Refused('registered public recipient is invalid')
return values[0]
def unique_object(pairs):
result = {}
for key, value in pairs:
if key in result:
raise Refused('duplicate encrypted document key')
result[key] = value
return result
def parse_document(raw, suffix):
if suffix == '.json':
data = json.loads(raw, object_pairs_hook=unique_object)
kind = 'json'
elif suffix in {'.yaml', '.yml'}:
import yaml
class UniqueLoader(yaml.SafeLoader):
def construct_mapping(self, node, deep=False):
self.flatten_mapping(node)
return unique_object((self.construct_object(key, deep=deep),
self.construct_object(value, deep=deep))
for key, value in node.value)
try:
data = yaml.load(raw, Loader=UniqueLoader)
except yaml.YAMLError:
raise Refused('invalid encrypted YAML input') from None
kind = 'yaml'
else:
raise Refused('age recovery supports SOPS JSON/YAML input')
return data, kind
def age_document(source, recipient, *, raw=None):
# Inspect ciphertext metadata before giving a credential to any subprocess.
# This repository's admitted SOPS profile is age-only, not arbitrary cloud
# KMS, key services, SSH identities or executable age plugins.
if raw is None:
with source.open("rb") as encrypted:
raw = encrypted.read(8 * 1024 * 1024 + 1)
if len(raw) > 8 * 1024 * 1024:
raise Refused('encrypted input is too large')
data, kind = parse_document(raw, source.suffix)
metadata = data.get('sops') if isinstance(data, dict) else None
if not isinstance(metadata, dict):
raise Refused('SOPS metadata is missing')
known = {'age', 'kms', 'gcp_kms', 'azure_kv', 'hc_vault', 'hckms', 'pgp',
'key_groups', 'shamir_threshold', 'lastmodified', 'mac', 'version',
'unencrypted_suffix', 'encrypted_suffix', 'unencrypted_regex',
'encrypted_regex', 'unencrypted_comment_regex', 'encrypted_comment_regex',
'mac_only_encrypted'}
if set(metadata) - known or metadata.get('key_groups'):
raise Refused('unsupported SOPS custody profile')
if any(metadata.get(key) for key in ('kms', 'gcp_kms', 'azure_kv', 'hc_vault', 'hckms', 'pgp')):
raise Refused('recovery requires age-only metadata')
recipients = metadata.get('age')
if not isinstance(recipients, list) or not recipients:
raise Refused('age recipient metadata is missing')
if any(not isinstance(row, dict) or not RECIPIENT.fullmatch(str(row.get('recipient', '')))
for row in recipients):
raise Refused('unsupported age recipient')
if recipient not in [row['recipient'] for row in recipients]:
raise Refused('document does not include the registered recipient')
return raw, kind
def private_key_fd():
name = os.environ.get('NETKINGDOM_CUSTODY_AGE_KEY_FILE', '')
if not name:
raise Refused('explicit temporary custody key required')
path = Path(os.path.abspath(name))
if any(p.is_symlink() for p in (path, *path.parents)):
raise Refused('custody key path must not contain symlinks')
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
info = os.fstat(fd)
if not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() or info.st_mode & 0o077 or info.st_nlink != 1 or info.st_size > 16384:
os.close(fd)
raise Refused('custody key must be an owned private regular file')
return fd
def decrypt(recipient):
source_name = os.environ.get('NK_SOPS_FILE', '')
output_name = os.environ.get('NK_SOPS_OUTPUT', '')
if not source_name or not output_name:
raise Refused('supply FILE and OUTPUT; plaintext stdout is disabled')
source = Path(source_name).resolve(strict=True)
output = Path(output_name).absolute()
parent = output.parent.resolve(strict=True)
if any((p / '.git').is_file() or (p / '.git/HEAD').is_file()
or ((p / 'HEAD').is_file() and (p / 'objects').is_dir() and (p / 'refs').is_dir())
for p in [parent, *parent.parents]):
raise Refused('plaintext output must be outside Git worktrees')
raw, kind = age_document(source, recipient)
binary = shutil.which('sops')
if binary is None:
raise Refused('sops executable is unavailable')
directory = os.open(parent, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
temporary = '.netkingdom-sops-' + uuid.uuid4().hex
keyfd = None
try:
info = os.fstat(directory)
if info.st_uid != os.getuid() or info.st_mode & 0o077:
raise Refused('OUTPUT requires an owned private parent directory')
try:
os.stat(output.name, dir_fd=directory, follow_symlinks=False)
except FileNotFoundError:
pass
else:
raise Refused('OUTPUT already exists')
keyfd = private_key_fd()
derived = subprocess.run(['age-keygen', '-y', f'/proc/self/fd/{keyfd}'],
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
pass_fds=(keyfd,), timeout=10)
if derived.returncode or derived.stdout.strip() != recipient.encode():
raise Refused('custody key does not match the registered recipient')
fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
0o600, dir_fd=directory)
with os.fdopen(fd, 'wb') as plain, tempfile.TemporaryDirectory(prefix='netkingdom-sops-home-') as isolated:
# HOME/XDG isolation matters: SOPS loads ambient keys in addition to
# an explicit key file. No inline keys, SSH keys, key commands,
# proxies or remote keyservice configuration are inherited.
env = {'PATH': os.environ.get('PATH', os.defpath), 'HOME': isolated,
'XDG_CONFIG_HOME': isolated, 'GNUPGHOME': isolated,
'SOPS_AGE_KEY_FILE': f'/proc/self/fd/{keyfd}',
'AWS_EC2_METADATA_DISABLED': 'true'}
result = subprocess.run([binary, '--decrypt', '--input-type', kind,
'--output-type', kind, '/dev/stdin'],
input=raw, stdout=plain, stderr=subprocess.DEVNULL,
env=env, cwd=isolated, pass_fds=(keyfd,), timeout=30)
if result.returncode:
raise Refused('SOPS decryption failed; no plaintext published')
plain.flush()
os.fsync(plain.fileno())
# Atomic no-overwrite publication within the private directory.
os.link(temporary, output.name, src_dir_fd=directory, dst_dir_fd=directory,
follow_symlinks=False)
os.unlink(temporary, dir_fd=directory)
os.fsync(directory)
finally:
if keyfd is not None:
os.close(keyfd)
try:
os.unlink(temporary, dir_fd=directory)
except FileNotFoundError:
pass
os.close(directory)
print('Decrypted to the requested private file. Remove it after the approved operation.')
def interrupted(_signum, _frame):
raise Refused('operation interrupted')
def main():
for sig in (signal.SIGTERM, signal.SIGHUP, signal.SIGINT):
signal.signal(sig, interrupted)
try:
if len(sys.argv) != 2 or sys.argv[1] not in {'setup', 'decrypt'}:
raise Refused('unsupported custody operation')
recipient = public_recipient()
if sys.argv[1] == 'setup':
print('Registered public recipient validated. No private key was read, copied or generated.')
else:
decrypt(recipient)
return 0
except Refused as exc:
print('ERROR: ' + str(exc), file=sys.stderr)
except (OSError, ValueError, TypeError, ImportError, subprocess.SubprocessError):
# Parser, filesystem and tool errors can include user data. Never echo.
print('ERROR: custody operation failed; input and tool diagnostics suppressed.', file=sys.stderr)
return 1
if __name__ == '__main__':
raise SystemExit(main())

View file

@ -0,0 +1,223 @@
import base64
import hashlib
import importlib.util
import io
import json
import subprocess
import tarfile
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
SOURCE=Path(__file__).parents[1]/'tools/bootstrap-recovery/isolated-keycape.py'
spec=importlib.util.spec_from_file_location('adapter',SOURCE)
a=importlib.util.module_from_spec(spec);spec.loader.exec_module(a)
class ArchiveTests(unittest.TestCase):
def setUp(self):
self.tmp=tempfile.TemporaryDirectory();self.addCleanup(self.tmp.cleanup)
self.root=Path(self.tmp.name)
config={'data':{k:base64.b64encode(v).decode() for k,v in {'config.yaml':b'issuer: https://issuer.example','key.pem':b'fixture-key'}.items()}}
self.values={'resources/sso/secret/keycape-config.json':json.dumps(config).encode(),
'resources/sso/deployment/keycape.json':json.dumps({'spec':{'template':{'spec':{'containers':[{'name':'keycape','image':a.IMAGE,'env':[
{'name':'KEYCAPE_CONFIG','value':'/etc/keycape/config.yaml'},
{'name':'KEYCAPE_POLICY_PATH','value':'/var/lib/keycape-policy/policy.json'},
{'name':'KEYCAPE_ORIGINAL_SECRET','valueFrom':{'secretKeyRef':{'name':'client','key':'secret'}}}]}]}}}}).encode(),
'resources/sso/secret/client.json':json.dumps({'data':{'secret':base64.b64encode(b'fixture-private').decode()}}).encode(),
'data/keycape/policy.json':b'{}','data/keycape/receipts/one.json':b'{"original":true}'}
for name,value in self.values.items():
p=self.root/name;p.parent.mkdir(parents=True,exist_ok=True);p.write_bytes(value);p.chmod(0o600)
self.manifest={'profile':{'components':[{'namespace':'sso','deployment':'keycape','secret_refs':['keycape-config','client'],'configmap_refs':[]}]},
'files':[{'path':k,'bytes':len(v),'sha256':hashlib.sha256(v).hexdigest()} for k,v in self.values.items()]}
def payload(self):
digest=hashlib.sha256(json.dumps(self.manifest,sort_keys=True,separators=(',',':')).encode()).hexdigest()
return a.private_payload(self.root,self.manifest,digest)
def test_original_state_and_secret_delivery_retained(self):
p=self.payload();self.assertEqual(p['env']['KEYCAPE_ORIGINAL_SECRET'],'fixture-private')
self.assertEqual(base64.b64decode(p['files']['/var/lib/keycape-policy/receipts/one.json']),b'{"original":true}')
def test_archive_digest_refused(self):
with self.assertRaises(a.Refused):a.private_payload(self.root,self.manifest,'0'*64)
def test_changed_file_refused(self):
(self.root/'data/keycape/policy.json').write_text('{"changed":true}')
with self.assertRaises(a.Refused):self.payload()
def test_symlink_refused(self):
p=self.root/'data/keycape/policy.json';p.unlink();p.symlink_to('/etc/hostname')
with self.assertRaises(Exception):self.payload()
def test_duplicate_manifest_refused(self):
self.manifest['files'].append(self.manifest['files'][0])
with self.assertRaises(a.Refused):self.payload()
def test_image_mismatch_refused(self):
d=json.loads(self.values['resources/sso/deployment/keycape.json']);d['spec']['template']['spec']['containers'][0]['image']='keycape:latest'
raw=json.dumps(d).encode();p=self.root/'resources/sso/deployment/keycape.json';p.write_bytes(raw)
for row in self.manifest['files']:
if row['path'].endswith('/deployment/keycape.json'):row.update(bytes=len(raw),sha256=hashlib.sha256(raw).hexdigest())
with self.assertRaises(a.Refused):self.payload()
class NativeTests(unittest.TestCase):
def setUp(self):
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
self.key=rsa.generate_private_key(public_exponent=65537,key_size=2048).private_bytes(serialization.Encoding.PEM,serialization.PrivateFormat.PKCS8,serialization.NoEncryption())
self.cfg=b'issuer: https://issuer.example\nclients:\n- clientId: existing\n clientType: confidential\n secretRef: env:KEYCAPE_ORIGINAL_SECRET\n grantTypes: [client_credentials]\n'
self.files={'/etc/keycape/config.yaml':self.cfg,'/etc/keycape/key.pem':self.key,'/var/lib/keycape-policy/policy.json':b'{}'}
self.payload={'env':{'KEYCAPE_ORIGINAL_SECRET':'fixture-private'},'files':{k:base64.b64encode(v).decode() for k,v in self.files.items()}}
self.admission={'issuer':'https://issuer.example','verifier_sha256':hashlib.sha256(b'binary').hexdigest(),
'trust_path':'/trust.pem','trust_sha256':hashlib.sha256(b'original-trust').hexdigest(),
'consumer':{'client_id':'existing','secret_env':'KEYCAPE_ORIGINAL_SECRET','scope':'read','audience':'consumer',
'subject':'service:existing','tenant':'tenant:platform','roles':'reader','deny_scope':'write'}}
self.session={'issuer':'isolated','volumes':{x:{'target':x} for x in ('/etc/keycape','/var/lib/keycape-policy','/etc/keycape-factor','/recovery-tls')}}
self.output=''.join('PASS '+x+'\n' for x in ['discovery and endpoint origin','exchange and JWKS signature for granted scopes','access-token claim bindings','principal_type is service','sub matches the registration','tenant matches the registration','roles present','granted scope carries no excess grant','predecessor secret refused','excess scope refused: write'])
def file(self,container,path):
return {**self.files,'/etc/hosts':b'127.0.0.1 issuer.example\n','/keycape':b'binary','/trust.pem':b'original-trust'}[path]
def command(self,argv,**kwargs):
if argv[1:3]==['container','inspect']:return json.dumps(['KEYCAPE_ORIGINAL_SECRET=fixture-private','SSL_CERT_FILE=/trust.pem']).encode()
output=io.BytesIO()
with tarfile.open(fileobj=output,mode='w') as t:
m=tarfile.TarInfo('policy.json');m.size=2;t.addfile(m,io.BytesIO(b'{}'))
return output.getvalue()
def verify(self,code=0,output=None):
with patch.object(a,'check_jwks'),patch.object(a,'runtime_file',return_value=b'original-trust'),patch.object(a,'runtime_files',side_effect=lambda session,paths=(),tree=False:({k:v for k,v in self.files.items() if k.startswith('/var/lib/keycape-policy/')} if tree else dict(self.files))),patch.object(a,'container_file',side_effect=self.file),patch.object(a,'command',side_effect=self.command),patch.object(a.subprocess,'run',return_value=subprocess.CompletedProcess([],code,b'',(self.output if output is None else output).encode())) as run:
result=a.run_verifier(self.session,self.admission,self.payload)
argv=run.call_args.args[0]
self.assertNotIn('fixture-private',' '.join(argv))
self.assertIn('-previous-secret-env',argv)
return result
def test_original_registration_and_unrelated_secret(self):
r=self.verify();self.assertTrue(r['wrong_unrelated_secret_explicitly_refused']);self.assertNotIn('fixture-private',json.dumps(r))
def test_transport_failure_never_counts_as_wrong_secret(self):
with self.assertRaises(a.Refused):self.verify(1,'FAIL discovery and endpoint origin: TLS failure\n')
def test_missing_explicit_rejection_never_counts(self):
with self.assertRaises(a.Refused):self.verify(0,self.output.replace('PASS predecessor secret refused\n',''))
def test_fail_line_refused_even_zero_exit(self):
with self.assertRaises(a.Refused):self.verify(0,self.output+'FAIL transport\n')
def test_security_environment_override_refused(self):
original=self.command
def altered(argv,**kwargs):
if argv[1:3]==['container','inspect']:return json.dumps(['KEYCAPE_ORIGINAL_SECRET=fixture-private','SSL_CERT_FILE=/trust.pem','SSL_CERT_DIR=/unreviewed']).encode()
return original(argv,**kwargs)
with patch.object(self,'command',side_effect=altered):
with self.assertRaises(a.Refused):self.verify()
def test_http_issuer_refused(self):
self.admission['issuer']='http://issuer.example'
with self.assertRaises(a.Refused):self.verify()
def test_changed_trust_refused(self):
self.admission['trust_sha256']='0'*64
with self.assertRaises(a.Refused):self.verify()
def test_config_drift_refused(self):
self.payload['files']['/etc/keycape/config.yaml']=base64.b64encode(b'changed').decode()
with self.assertRaises(a.Refused):self.verify()
class BoundaryTests(unittest.TestCase):
def test_remote_docker_refused_before_info(self):
session={'run_id':'a'*32,'containers':['nk49-kc-'+('a'*32)+'-worker'],'worker':'nk49-kc-'+('a'*32)+'-worker','issuer':'nk49-kc-'+('a'*32)+'-worker'}
with patch.object(a,'command',return_value=b'[{"Endpoints":{"docker":{"Host":"tcp://remote:2375"}}}]') as command:
with self.assertRaises(a.Refused):a.inspect_session(session)
self.assertEqual(command.call_count,1)
def test_failed_verifier_cleanup_is_attempted(self):
receipt={};admission={'session':{},'ciphertext_sha256':'a'*64,'manifest_sha256':'b'*64,'verifier_sha256':'c'*64,'trust_sha256':'d'*64,'consumer':{x:'yes' for x in ('client_id','secret_env','audience','scope','subject','tenant','roles','deny_scope')}}
with patch.object(a,'inspect_session'),patch.object(a,'private_payload',return_value={}),patch.object(a,'run_verifier',side_effect=a.Refused),patch.object(a,'cleanup') as cleanup:
with self.assertRaises(a.Refused):a.native_checks(Path('/unused'),{},admission,receipt)
cleanup.assert_called_once();self.assertTrue(receipt['container_cleanup_verified']);self.assertEqual(receipt['status'],'failed')
def test_cleanup_failure_overrides_success(self):
admission={'session':{},'ciphertext_sha256':'a'*64,'manifest_sha256':'b'*64,'verifier_sha256':'c'*64,'trust_sha256':'d'*64,'consumer':{x:'yes' for x in ('client_id','secret_env','audience','scope','subject','tenant','roles','deny_scope')}}
result={x:True for x in ('native_service_registration_verified','wrong_unrelated_secret_explicitly_refused','denied_scopes_refused','original_config_registry_policy_receipts_equal','original_https_issuer_and_trust_preserved','original_public_signing_key_in_live_jwks')}
result['public_signing_spki_sha256']='e'*64;receipt={}
with patch.object(a,'inspect_session'),patch.object(a,'private_payload',return_value={}),patch.object(a,'run_verifier',return_value=result),patch.object(a,'cleanup',side_effect=a.Refused):
with self.assertRaises(a.Refused):a.native_checks(Path('/unused'),{},admission,receipt)
self.assertEqual(receipt['status'],'failed');self.assertFalse(receipt['container_cleanup_verified'])
class ContainmentTests(unittest.TestCase):
def inspect(self,overrides=None,launch=None):
name='nk49-kc-'+('a'*32)+'-issuer'
session={'run_id':'a'*32,'containers':[name],'issuer':name,'worker':name,'images':{name:a.IMAGE}}
host={'NetworkMode':'none','ReadonlyRootfs':True,'Privileged':False,'CapDrop':['ALL'],'SecurityOpt':['no-new-privileges']}
host.update(overrides or {})
def command(argv,**kwargs):
if argv[1:3]==['context','inspect']:return b'[{"Endpoints":{"docker":{"Host":"unix:///var/run/docker.sock"}}}]'
if argv[1]=='info':return b'{"ID":"74f93783-6cef-43a8-a3c4-f62586cd3bc1","Name":"bnt-lap001","OSType":"linux"}'
if argv[-1]=='{{.Id}}':return b'anchor-id'
if '"path"' in argv[-1]:return json.dumps(launch or {'entrypoint':['/keycape'],'cmd':None,'path':'/keycape','args':[]}).encode()
return json.dumps({'host':host,'mounts':[],'labels':{a.LABEL:'a'*32},'image':a.IMAGE,'user':'1000:1000'}).encode()
with patch.object(a,'command',side_effect=command),patch.dict('os.environ',{'DOCKER_HOST':'unix:///var/run/docker.sock'}):a.inspect_session(session)
def test_minimal_network_none_admitted(self):self.inspect()
def test_alternate_same_image_command_refused(self):
with self.assertRaises(a.Refused):self.inspect(launch={'entrypoint':['/keycape'],'cmd':['check-config'],'path':'/keycape','args':['check-config']})
def test_cap_add_refused(self):
with self.assertRaises(a.Refused):self.inspect({'CapAdd':['NET_ADMIN']})
def test_devices_refused(self):
with self.assertRaises(a.Refused):self.inspect({'Devices':[{'PathOnHost':'/dev/mem'}]})
def test_device_requests_refused(self):
with self.assertRaises(a.Refused):self.inspect({'DeviceRequests':[{'Driver':'nvidia'}]})
def test_host_namespace_refused(self):
for field in ('PidMode','IpcMode','UTSMode','UsernsMode','CgroupnsMode'):
with self.subTest(field=field),self.assertRaises(a.Refused):self.inspect({field:'host'})
def test_security_override_refused(self):
with self.assertRaises(a.Refused):self.inspect({'SecurityOpt':['no-new-privileges','seccomp=unconfined']})
class CleanupTests(unittest.TestCase):
def test_partial_cleanup_attempts_every_owned_container(self):
session={'run_id':'a'*32,'containers':['first','second','third']};receipt={}
removed=[]
def command(argv,**kwargs):
if argv[1:3]==['container','inspect']:return ('a'*32).encode()
if argv[1]=='rm':
removed.append(argv[-1])
if argv[-1]=='third':raise a.Refused()
return b''
if argv[1:3]==['container','ls']:
name=next(x for x in argv if x.startswith('name=^/')).removeprefix('name=^/').removesuffix('$')
return (name+'\n').encode() if name=='third' or name not in removed else b''
return b''
with patch.object(a,'command',side_effect=command):
with self.assertRaises(a.Refused):a.cleanup(session,receipt)
self.assertEqual(removed,['third','second','first'])
self.assertEqual([r['status'] for r in receipt['container_cleanup_obligations']],['still_present','absence_verified','absence_verified'])
def test_ownership_mismatch_does_not_remove_and_continues(self):
session={'run_id':'a'*32,'containers':['first','second']};receipt={};removed=[]
def command(argv,**kwargs):
if argv[1:3]==['container','inspect']:return (('b' if argv[3]=='second' else 'a')*32).encode()
if argv[1]=='rm':removed.append(argv[-1])
if argv[1:3]==['container','ls']:
name=next(x for x in argv if x.startswith('name=^/')).removeprefix('name=^/').removesuffix('$')
return (name+'\n').encode() if name not in removed else b''
return b''
with patch.object(a,'command',side_effect=command):
with self.assertRaises(a.Refused):a.cleanup(session,receipt)
self.assertEqual(removed,['first'])
self.assertEqual(receipt['container_cleanup_obligations'][0]['status'],'ownership_mismatch_not_removed')
def test_inspect_failure_retains_uncertain_obligation(self):
receipt={}
with patch.object(a,'command',side_effect=a.Refused):
with self.assertRaises(a.Refused):a.cleanup({'run_id':'a'*32,'containers':['exact-owned-name']},receipt)
self.assertEqual(receipt['container_cleanup_obligations'],[{'container':'exact-owned-name','status':'unverified'}])
class JWKSTests(unittest.TestCase):
def execute(self,n='123',endpoint='https://issuer.example/jwks',resolved='127.0.0.1',extra=False,kid='key-1'):
request={'issuer':'https://issuer.example','trust':'public-certificate','n':'123','e':'65537'}
def enc(i):return base64.urlsafe_b64encode(int(i).to_bytes((int(i).bit_length()+7)//8,'big')).decode().rstrip('=')
responses=[{'issuer':'https://issuer.example','jwks_uri':endpoint},{'keys':[{'kty':'RSA','kid':kid,'use':'sig','alg':'RS256','n':enc(n),'e':enc(65537)}]}]
if extra:responses[1]['keys'].append(dict(responses[1]['keys'][0],kid='key-2'))
class Response:
def __init__(self,value):self.value=value
def __enter__(self):return self
def __exit__(self,*args):pass
def read(self,*args):return json.dumps(self.value).encode()
class Opener:
def open(self,*args,**kwargs):return Response(responses.pop(0))
with patch('sys.stdin',io.StringIO(json.dumps(request))),patch('sys.stdout',io.StringIO()),patch('socket.getaddrinfo',return_value=[(2,1,6,'',(resolved,443))]),patch('ssl.create_default_context'),patch('urllib.request.build_opener',return_value=Opener()):
exec(compile(a.JWKS_PROBE,'<jwks-probe>','exec',optimize=2),{})
def test_exact_original_public_key(self):self.execute()
def test_extra_signing_key_refused_under_optimization(self):
with self.assertRaises(ValueError):self.execute(extra=True)
def test_wrong_kid_refused_under_optimization(self):
with self.assertRaises(ValueError):self.execute(kid='replacement')
def test_wrong_live_signing_key_refused(self):
with self.assertRaises(ValueError):self.execute(n='124')
def test_cross_origin_jwks_refused(self):
with self.assertRaises(ValueError):self.execute(endpoint='https://production.example/jwks')
def test_production_dns_refused_before_fetch(self):
with self.assertRaises(ValueError):self.execute(resolved='192.0.2.1')
if __name__=='__main__':unittest.main()

View file

@ -0,0 +1,138 @@
import base64
import importlib.util
import json
import os
import tempfile
from pathlib import Path
import unittest
from unittest.mock import patch
SOURCE=Path(__file__).parents[1]/'tools/bootstrap-recovery/isolated-keycape-session.py'
spec=importlib.util.spec_from_file_location('session',SOURCE)
s=importlib.util.module_from_spec(spec);spec.loader.exec_module(s)
class SessionTests(unittest.TestCase):
def test_tmpfs_plan_has_four_exact_original_mounts(self):
session=s.plan_session()
self.assertEqual({v['target'] for v in session['volumes'].values()},set(s.RUNROOTS))
self.assertTrue(all(v['options']==s.VOLUME_OPTIONS for v in session['volumes'].values()))
self.assertEqual(session['images'][session['issuer']],s.adapter.IMAGE)
def test_non_tmpfs_driver_options_refused(self):
session=s.plan_session();name,spec=next(iter(session['volumes'].items()))
wrong=[{'Name':name,'Driver':'local','Scope':'local','Options':{'type':'none','device':'/host/data'},'Labels':{s.adapter.LABEL:session['run_id']}}]
with patch.object(s,'run',return_value=json.dumps(wrong).encode()):
with self.assertRaises(s.adapter.Refused):s.verify_volume(name,spec,session['run_id'])
def test_owner_secret_is_not_docker_argv_and_import_is_after_inspection(self):
session=s.plan_session();events=[]
payload={'files':{'/etc/keycape/config.yaml':base64.b64encode(b'original').decode()},'env':{'KEYCAPE_TEST_SECRET':'fixture-sensitive'}}
def run(argv,**kwargs):
events.append(('run',argv,kwargs))
if argv[-1]=='{{.Id}}':return b'anchor'
return b''
with patch.object(s,'run',side_effect=run),patch.object(s,'verify_volume'),patch.object(s.adapter,'inspect_session',side_effect=lambda *args,**kwargs:events.append(('inspect',args,kwargs))):
s.provision(session,payload,{'/recovery-tls/trust.pem':b'original-trust'},'https://issuer.example')
joined=' '.join(' '.join(e[1]) for e in events if e[0]=='run')
self.assertNotIn('fixture-sensitive',joined)
private_index=next(i for i,e in enumerate(events) if e[0]=='run' and s.IMPORT in e[1])
inspect_index=next(i for i,e in enumerate(events) if e[0]=='inspect')
self.assertLess(inspect_index,private_index)
issuer_call=next(e for e in events if e[0]=='run' and e[1][-1]==s.adapter.IMAGE)
self.assertEqual(issuer_call[2]['env']['KEYCAPE_TEST_SECRET'],'fixture-sensitive')
self.assertIn('KEYCAPE_TEST_SECRET',issuer_call[1])
self.assertNotIn('--entrypoint',issuer_call[1])
def test_non_original_https_port_refused_before_any_mutation(self):
with patch.object(s,'run') as run:
with self.assertRaises(s.adapter.Refused):s.provision(s.plan_session(),{'files':{},'env':{}},{},'http://issuer.example')
run.assert_not_called()
def test_container_failure_still_attempts_each_volume_cleanup(self):
session=s.plan_session();receipt={};removed=[]
def run(argv,**kwargs):
if argv[1:3]==['volume','rm']:removed.append(argv[-1])
if argv[1:3]==['volume','ls']:
name=next(x for x in argv if x.startswith('name=^')).removeprefix('name=^').removesuffix('$')
return (name+'\n').encode() if name not in removed else b''
return b''
with patch.object(s.adapter,'cleanup',side_effect=s.adapter.Refused),patch.object(s,'verify_volume'),patch.object(s,'run',side_effect=run):
with self.assertRaises(s.adapter.Refused):s.cleanup_all(session,receipt)
self.assertEqual(set(removed),set(session['volumes']))
self.assertFalse(receipt['container_cleanup_verified'])
self.assertEqual(len(receipt['tmpfs_volume_cleanup_obligations']),4)
def test_reaper_inherits_no_custody_or_service_environment(self):
session=s.plan_session();staging={'path':'/dev/shm/registered','run_id':session['run_id']}
with patch.object(s.subprocess,'Popen') as spawn,patch.object(s,'register_workdir',return_value=staging),patch.object(s.select,'select',side_effect=lambda *args:([spawn.return_value.stdout],[],[])):
spawn.return_value.stdout.readline.return_value=b'registered\n'
s.start_reaper(session,staging)
kwargs=spawn.call_args.kwargs
self.assertEqual(set(kwargs['env']),{'PATH','DOCKER_HOST'})
self.assertTrue(kwargs['close_fds']);self.assertTrue(kwargs['start_new_session'])
class TargetBindingTests(unittest.TestCase):
def test_every_direct_docker_command_is_explicitly_bound(self):
with patch.dict('os.environ',{},clear=True),patch.object(s.adapter.subprocess,'run',return_value=__import__('subprocess').CompletedProcess([],0,b'ok')) as run:
self.assertEqual(s.run(['docker','info']),b'ok')
argv=run.call_args.args[0]
self.assertEqual(argv[:3],['docker','--host',s.adapter.ENDPOINT])
self.assertEqual(run.call_args.kwargs['env']['DOCKER_HOST'],s.adapter.ENDPOINT)
def test_nondefault_unix_socket_is_refused_before_any_docker_call(self):
with patch.dict('os.environ',{'DOCKER_HOST':'unix:///tmp/other-daemon.sock'},clear=True),patch.object(s.adapter.subprocess,'run') as run:
with self.assertRaises(s.adapter.Refused):s.run(['docker','info'])
run.assert_not_called()
def test_conflicting_context_is_refused(self):
with patch.dict('os.environ',{'DOCKER_CONTEXT':'production'},clear=True),patch.object(s.adapter.subprocess,'run') as run:
with self.assertRaises(s.adapter.Refused):s.run(['docker','info'])
run.assert_not_called()
def test_conflicting_per_call_host_is_refused(self):
with patch.dict('os.environ',{},clear=True),patch.object(s.adapter.subprocess,'run') as run:
with self.assertRaises(s.adapter.Refused):s.run(['docker','info'],env={'DOCKER_HOST':'unix:///tmp/other.sock'})
run.assert_not_called()
def test_reused_preflight_uses_same_bound_command(self):
self.assertIs(s.local.command,s.adapter.command)
def test_probe_is_registered_before_session_creation(self):
session=s.plan_session()
self.assertEqual(session['public_image_probe'],'nk49-kc-'+session['run_id']+'-image-probe')
self.assertEqual(session['docker_endpoint'],s.adapter.ENDPOINT)
class PlaintextLifecycleTests(unittest.TestCase):
def setUp(self):
self.tmp=tempfile.TemporaryDirectory();self.addCleanup(self.tmp.cleanup)
self.root=Path(self.tmp.name)
self.override=patch.object(s,'STAGING_ROOT',self.root);self.override.start();self.addCleanup(self.override.stop)
self.run_id='a'*32
self.work=self.root/('nk49-keycape-'+self.run_id+'-abcdefgh');self.work.mkdir(mode=0o700)
self.staging=s.register_workdir(self.work,self.run_id)
def test_exact_plaintext_directory_removed_and_recorded(self):
(self.work/'archive.tar').write_bytes(b'synthetic-private-archive')
restored=self.work/'restored';restored.mkdir(mode=0o700);(restored/'fixture').write_bytes(b'synthetic')
receipt={};s.cleanup_workdir(self.staging,receipt)
self.assertFalse(self.work.exists());self.assertTrue(receipt['temporary_plaintext_cleanup_verified'])
self.assertEqual(receipt['plaintext_cleanup_obligation']['status'],'absence_verified')
def test_docker_failure_does_not_skip_plaintext_cleanup(self):
(self.work/'archive.tar').write_bytes(b'synthetic')
receipt={}
with patch.object(s,'cleanup_all',side_effect=s.adapter.Refused):
with self.assertRaises(s.adapter.Refused):s.recover_obligations({},self.staging,receipt)
self.assertFalse(self.work.exists());self.assertTrue(receipt['temporary_plaintext_cleanup_verified'])
def test_replacement_inode_is_not_removed(self):
# Preserve original inode while replacing its registered pathname.
old=self.root/'retained-original';self.work.rename(old);self.work.mkdir(mode=0o700)
(self.work/'replacement').write_bytes(b'not-owned-by-this-obligation');receipt={}
with self.assertRaises(s.adapter.Refused):s.cleanup_workdir(self.staging,receipt)
self.assertTrue((self.work/'replacement').exists());self.assertTrue(old.exists())
self.assertEqual(receipt['plaintext_cleanup_obligation']['status'],'identity_mismatch_not_removed')
def test_symlink_root_is_not_followed(self):
self.work.rmdir();outside=self.root/'outside';outside.mkdir();(outside/'kept').write_bytes(b'keep')
self.work.symlink_to(outside,target_is_directory=True)
with self.assertRaises(s.adapter.Refused):s.cleanup_workdir(self.staging,{})
self.assertTrue((outside/'kept').exists())
def test_descendant_symlink_is_not_followed(self):
outside=self.root/'kept';outside.write_bytes(b'keep');(self.work/'link').symlink_to(outside)
with self.assertRaises(s.adapter.Refused):s.cleanup_workdir(self.staging,{})
self.assertEqual(outside.read_bytes(),b'keep')
def test_wrong_owner_identity_is_not_removed(self):
changed={**self.staging,'owner_uid':os.getuid()+1}
with self.assertRaises(s.adapter.Refused):s.cleanup_workdir(changed,{})
self.assertTrue(self.work.exists())
def test_unregistered_path_refused(self):
with self.assertRaises(s.adapter.Refused):s.cleanup_workdir({**self.staging,'path':str(self.root)}, {})
if __name__=='__main__':unittest.main()

View file

@ -0,0 +1,156 @@
#!/usr/bin/env python3
"""Restore the fetched PI database in an isolated, disposable local PostgreSQL.
Run only through the attended custody helper. No cluster, production database,
network, ports, host data mounts, IAM login or token decryption is involved.
"""
import argparse
from datetime import datetime, timezone
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import signal
import subprocess
import sys
import tempfile
import time
import uuid
ROOT=Path(__file__).resolve().parents[2]
spec=importlib.util.spec_from_file_location('custody',ROOT/'sso-mfa/bootstrap/sops-private.py')
custody=importlib.util.module_from_spec(spec);spec.loader.exec_module(custody)
RECIPIENT='age1zvryunvjhvpkmasskauga2heeg0ztnte9ymgppvjge36ekumk50syr3tsz'
IMAGE='postgres@sha256:ff23cdce56cac62ada6f66013e1a50864c0abbe79d132d40b6e05bd80f378a70'
OBJECT='r01-net-kingdom-pg/net-kingdom-pg-privacyidea_db-20261015T023000Z.dump.age'
SIZE=89129309
class Refused(Exception):pass
def run(args,**kwargs):
result=subprocess.run(args,stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,timeout=600,**kwargs)
if result.returncode:raise Refused()
return result.stdout
def transfer_input(receipt):
r=json.loads(receipt.read_text())
if (r.get('schema')!='netkingdom.privacyidea-offhost-transfer.v1' or
r.get('status')!='ciphertext_fetched' or r.get('object')!=OBJECT or
r.get('ciphertext_bytes')!=SIZE or r.get('object_unchanged_during_transfer') is not True):raise Refused()
path=Path(r['ciphertext_path'])
if not path.is_absolute() or path.parent.parent!=Path('/tmp') or not path.parent.name.startswith('nk49-offhost-'):raise Refused()
if any(p.is_symlink() for p in (path,*path.parents)):raise Refused()
st=path.stat()
if st.st_uid!=os.getuid() or st.st_mode&0o077 or st.st_nlink!=1 or not path.is_file() or st.st_size!=SIZE:raise Refused()
blob=path.read_bytes()
if len(blob)!=SIZE or hashlib.sha256(blob).hexdigest()!=r['ciphertext_sha256']:raise Refused()
return blob,r['ciphertext_sha256']
def restore(dump,container):
# Require the exact image already present; do not pull through a mutable tag.
image_id=run(['docker','image','inspect','--format','{{.Id}}',IMAGE]).decode().strip()
run(['docker','run','--detach','--pull=never','--name',container,'--network','none',
'--security-opt','no-new-privileges','--memory','2g','--cpus','2',
'--tmpfs','/var/lib/postgresql/data:rw,nosuid,nodev,size=1073741824',
'--env','POSTGRES_HOST_AUTH_METHOD=trust','--env','POSTGRES_DB=recovery',
IMAGE,'postgres','-c','listen_addresses='])
for _ in range(60):
# The image starts a temporary server during initdb. Wait until the
# entrypoint has exec'd the final server before attempting the restore.
pid1=subprocess.run(['docker','exec',container,'cat','/proc/1/comm'],
stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,timeout=10)
if pid1.returncode or pid1.stdout.strip()!=b'postgres':
time.sleep(1);continue
ready=subprocess.run(['docker','exec',container,'pg_isready','-U','postgres','-d','recovery'],
stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,timeout=10)
if ready.returncode==0:break
time.sleep(1)
else:raise Refused()
version=run(['docker','exec',container,'pg_restore','--version']).decode().strip()
with dump.open('rb') as source:
run(['docker','exec','-i',container,'pg_restore','-U','postgres','-d','recovery',
'--no-owner','--no-privileges','--exit-on-error'],stdin=source)
# Fixed aggregate queries only. No identity, resolver config or token values.
counts={}
for table in ('token','resolver','realm'):
raw=run(['docker','exec',container,'psql','-X','-U','postgres','-d','recovery','-At',
'-v','ON_ERROR_STOP=1','-c',f'SELECT count(*) FROM public.{table};']).strip()
if not raw.isdigit():raise Refused()
counts[table]=int(raw)
return {'image':IMAGE,'image_id':image_id,'pg_restore_version':version,'aggregate_row_counts':counts}
def main():
p=argparse.ArgumentParser(description=__doc__)
p.add_argument('--transfer-receipt',required=True);p.add_argument('--receipt',required=True)
a=p.parse_args();os.umask(0o077)
# Reserve the receipt before any recovery action; never overwrite evidence.
fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
r={'schema':'netkingdom.privacyidea-database-drill.v1','status':'failed',
'started_at':datetime.now(timezone.utc).isoformat(),'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
'production_contacted':False,'secret_values_exported':False,'native_identity_recovery_proven':False,
'token_decryption_proven':False,'audit_signature_continuity_proven':False,
'full_identity_file_coverage_proven':False,'container_cleanup_verified':False,
'temporary_plaintext_cleanup_verified':False}
container='nk49-pi-recovery-'+uuid.uuid4().hex
keyfd=None;work=None
try:
blob,digest=transfer_input(Path(a.transfer_receipt));r['ciphertext_sha256']=digest
keyfd=custody.private_key_fd()
derived=run(['age-keygen','-y',f'/proc/self/fd/{keyfd}'],pass_fds=(keyfd,)).strip()
if derived!=RECIPIENT.encode():raise Refused()
r['registered_recipient_match']=True
with tempfile.TemporaryDirectory(prefix='nk49-pi-drill-',dir='/tmp') as temporary:
work=Path(temporary)
env={'PATH':os.environ.get('PATH',os.defpath),'HOME':temporary,'XDG_CONFIG_HOME':temporary}
wrong=work/'wrong.key'
run(['age-keygen','-o',str(wrong)],env=env)
negative=subprocess.run(['age','-d','-i',str(wrong)],input=blob,stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,timeout=60,env=env)
if negative.returncode==0:raise Refused()
r['wrong_key_refused']=True
dump=work/'privacyidea.dump'
with dump.open('xb') as output:
result=subprocess.run(['age','-d','-i',f'/proc/self/fd/{keyfd}'],input=blob,
stdout=output,stderr=subprocess.DEVNULL,timeout=120,pass_fds=(keyfd,),env=env)
if result.returncode or not 5<dump.stat().st_size<SIZE:raise Refused()
with dump.open('rb') as source:
if source.read(5)!=b'PGDMP':raise Refused()
r['offhost_ciphertext_decryption_verified']=True
try:
r.update(restore(dump,container));r['database_restore_and_readback_passed']=True
finally:
removed=subprocess.run(['docker','rm','--force','--volumes',container],stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,timeout=60)
# A removal error is a failed ceremony even if the restore passed.
if removed.returncode:raise Refused()
absent=subprocess.run(['docker','container','inspect',container],stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,timeout=15)
# Confirm the daemon remains reachable, so inspect failure means absent.
run(['docker','info','--format','{{.ServerVersion}}'])
if absent.returncode==0:raise Refused()
r['container_cleanup_verified']=True
if work.exists():raise Refused()
r['temporary_plaintext_cleanup_verified']=True;r['status']='isolated_database_drill_passed'
except Exception:
r['error']='database_drill_failed_diagnostics_suppressed'
finally:
if keyfd is not None:os.close(keyfd)
if work is not None and not work.exists():r['temporary_plaintext_cleanup_verified']=True
r['finished_at']=datetime.now(timezone.utc).isoformat()
with os.fdopen(fd,'w') as out:json.dump(r,out,indent=2);out.write('\n')
if r['status']=='isolated_database_drill_passed':
print('OK: off-host database decrypted, isolated restore/readback passed, disposable data removed. Full identity recovery remains open.')
return 0
print('ERROR: database drill failed; private diagnostics suppressed. Retain the safe receipt for review.',file=sys.stderr)
return 1
if __name__=='__main__':
for sig in (signal.SIGHUP,signal.SIGTERM,signal.SIGINT):signal.signal(sig,custody.interrupted)
raise SystemExit(main())

View file

@ -0,0 +1,158 @@
{
"schema": "netkingdom.identity-backup-profile.v1",
"status": "implemented_pending_owner_admission",
"owner": "net-kingdom with railiance-platform/rapp-postgres runtime and storage admission",
"public_recipient": "age1zvryunvjhvpkmasskauga2heeg0ztnte9ymgppvjge36ekumk50syr3tsz",
"archive_profile": "netkingdom-identity-v1; new profile, not the bootstrap secrets-only tar profile",
"components": [
{
"namespace": "sso",
"deployment": "lldap",
"deployment_uid": "743a86bd-5cb0-4ca0-9c56-a3438f4746a8",
"pvc_refs": [
{
"name": "lldap-data",
"uid": "0a06b65a-e7b3-42d3-916e-e74dbe5fc7da"
}
],
"secret_refs": [
"lldap-secrets"
],
"configmap_refs": [],
"required_assets": [
"SQLite online-backup snapshot of /data/users.db with integrity check",
"remaining non-backup durable /data files, including any private identity material",
"current deployment configuration and exact lldap-secrets fields"
],
"native_acceptance": [
"directory user/group/membership preservation",
"existing-user password authentication in isolated environment"
]
},
{
"namespace": "sso",
"deployment": "authelia",
"deployment_uid": "dc40a925-2306-4a3b-89c0-e714187503c3",
"pvc_refs": [
{
"name": "authelia-data",
"uid": "86826554-b014-4df4-8b28-866aa64f6413"
}
],
"secret_refs": [
"authelia-secrets",
"authelia-smtp"
],
"configmap_refs": [
"authelia-config"
],
"required_assets": [
"SQLite online-backup snapshot of /var/authelia/data/db.sqlite3 with integrity check",
"current configuration.yml and all Secret-mounted encryption/OIDC/session fields",
"explicit disposition for notifier credentials and remaining durable files"
],
"native_acceptance": [
"storage decryption",
"same OIDC signing identity and preserved client configuration",
"isolated authentication flow with outbound notification disabled"
]
},
{
"namespace": "sso",
"deployment": "keycape",
"deployment_uid": "99ddd83c-cb3f-4847-bcf8-35f1aa87627f",
"pvc_refs": [
{
"name": "keycape-authentication-policy",
"uid": "423f558f-8c11-4edf-956b-243559df4a8e"
}
],
"secret_refs": [
"keycape-approval-engine-operator-client",
"keycape-config",
"keycape-factor-read",
"keycape-informed-decision-sitting-requester-client",
"keycape-rapp-qonto-client",
"keycape-secrets-engine-approval-client",
"keycape-secrets-engine-openbao-client",
"keycape-secrets-engine-requester-client"
],
"configmap_refs": [],
"required_assets": [
"exact current keycape-config config.yaml and key.pem",
"complete stable policy/receipt PVC snapshot",
"current deployment and owner disposition for every referenced credential: encrypted recovery or independently recoverable provider reissue path"
],
"native_acceptance": [
"same issuer/JWKS signing identity",
"registry, authentication policy and receipt continuity",
"current consumer positive/negative authentication under isolated test endpoints"
]
},
{
"namespace": "mfa",
"deployment": "privacyidea",
"deployment_uid": "58c7f96d-61cb-4dd4-bca2-54661c0ac375",
"pvc_refs": [
{
"name": "privacyidea-data",
"uid": "65650fc1-02d8-4c0f-84fc-e0015c8f3ed9"
},
{
"name": "privacyidea-logs",
"uid": "39fa90d9-bc54-4a03-a0d6-ae85165e0cb9"
}
],
"secret_refs": [
"privacyidea-config"
],
"configmap_refs": [
"factor-recovery-01119187d90d7b76",
"keycape-onboarding-9465e5e0b98f7986",
"privacyidea-cfg"
],
"required_assets": [
"original /etc/privacyidea/enckey, private.pem and public.pem with matching RSA pair validation",
"current privacyidea-config and mounted configuration/scripts",
"bounded native PostgreSQL custom dump with exact database identity and key-generation linkage",
"explicit disposition for service logs; audit database/signatures must be preserved"
],
"native_acceptance": [
"decrypt existing retained token material without exporting it",
"verify pre-backup audit signatures and create/verify a new isolated audit entry",
"existing-token MFA and negative-key refusal"
]
}
],
"capture_contract": {
"plaintext": "private task staging outside Git; never process output, State Hub or logs; verified cleanup after encryption",
"consistency": "native SQLite online-backup and PostgreSQL logical snapshot; explicit capture interval per asset; no global atomic snapshot claim; before/after source identities/config versions and directory inventories must match",
"key_policy": "preserve original service keys; no age private key in the payload, no replacement generation, no root/unseal collection",
"encryption": "public-recipient-only streaming archive to age; private fresh ciphertext, no overwrite; manifest inside ciphertext with source identities, times, complete required paths, digests and omissions",
"limits": "measure first; proposed 512 MiB encrypted admission ceiling, no silent truncation/exclusions",
"historical_database": "existing verified October15 dump remains protected; new bundle must link its own exact database/key capture generation and recovery receipt"
},
"proposed_destinations": {
"initial": "Storage Share Backup/railiance-backups/net-kingdom-identity/",
"future_secondary": "s3://railiance-platform-pg-backup/platform-pg/application-archives/net-kingdom/; not implemented/admitted in this rollout",
"admitted": false,
"publisher": "CCR-2026-0048: new folder-only read/create public DAV share, delivered by exact ESO reader; no operator password in runtime; explicitly owner-approved provider-added re-sharing (permissions21)",
"retention": "CCR-2026-0049: separate folder-only read/delete share and process; newest plus seven daily/two weekly and independently recovered pinned generation; unknown/unindexed points never deleted; explicitly owner-approved provider-added re-sharing (permissions25)",
"schedule": "source prepared: daily 03:45 UTC plus up to five minutes jitter, hourly 25-hour freshness check; installation leaves timers disabled until independent archive recovery",
"quota": "512 MiB ceiling, two GiB fresh provider headroom; initial 10 GiB account quota checked; unsupported public-share quota fails closed"
},
"admission_gates": [
"Approve exact CCR-2026-0048/0049 grants and node runtime packet; three required owner reviews, confirmed native SA/namespace/audience boundaries before acceptance",
"Exclude helper capture label from existing broad DNS allow policies; validate read-only helper manifests and cluster/PVC/source bindings before private capture",
"Preserve every referenced credential encrypted with original keys; fresh native SQLite/PG snapshots and exact manifest, with documented omissions and no global atomicity claim",
"Create fresh ciphertext and exact Storage Share object, read back ciphertext and receipt by strong ETag/hash, independently retrieve using operator custody",
"Attended independent age-key proof: wrong-key denial, complete archive hashes, SQLite/RSA checks, disposable native PostgreSQL restore and verified cleanup before timer activation",
"Full NK49 closure separately requires native directory/issuer/MFA authentication, retained token decryption, audit signature continuity, current callers and legacy credential dispositions; archive acceptance does not close those gates",
"Host-local health cannot report a dead host; external outage monitoring and second-provider copy remain separate coverage gaps"
],
"runtime_host": {
"ssh_alias": "railiance01",
"hostname": "239.62.205.92.host.secureserver.net",
"machine_id_sha256": "fe7367a60951922122af7ba18ed2cde65c7e0a232bbedfb045cb575de2094c2f"
}
}

View file

@ -0,0 +1,211 @@
#!/usr/bin/env python3
"""Strict data-bearing identity archive format, native checks and private sealing.
No custody key is used to create a bundle. Its manifest and plaintext digests
stay inside encryption. Public receipts expose only ciphertext provenance.
"""
import base64
from datetime import datetime,timezone
import hashlib
import importlib.util
import io
import json
import os
from pathlib import Path,PurePosixPath
import re
import sqlite3
import stat
import subprocess
import tarfile
import tempfile
ROOT=Path(__file__).resolve().parents[2]
PROFILE=ROOT/'tools/bootstrap-recovery/identity-backup-profile.json'
LIMIT=512*1024*1024
FILE_LIMIT=256*1024*1024
NAME=re.compile(r'[A-Za-z0-9_.-]{1,160}\Z')
class Refused(Exception):pass
def profile():return json.loads(PROFILE.read_text())
def private(path,directory=False,max_bytes=FILE_LIMIT):
if any(p.is_symlink() for p in (path,*path.parents)):raise Refused()
s=path.lstat()
if s.st_uid!=os.getuid() or s.st_mode&0o077:raise Refused()
if directory:
if not stat.S_ISDIR(s.st_mode):raise Refused()
elif not stat.S_ISREG(s.st_mode) or s.st_nlink!=1 or s.st_size>max_bytes:raise Refused()
def allowed(name,profile_data=None):
p=PurePosixPath(name)
if p.is_absolute() or any(not NAME.fullmatch(x) or x in {'.','..'} for x in p.parts):return False
if len(p.parts)>=3 and p.parts[:2] in [('data','lldap'),('data','authelia'),('data','keycape'),('data','privacyidea')]:return True
if name=='databases/privacyidea.dump':return True
for c in (profile_data or profile())['components']:
prefix=f"resources/{c['namespace']}/"
if name==prefix+'deployment/'+c['deployment']+'.json':return True
if any(name==prefix+'secret/'+n+'.json' for n in c['secret_refs']):return True
if any(name==prefix+'configmap/'+n+'.json' for n in c['configmap_refs']):return True
return False
def save(root,name,value,profile_data=None):
if not allowed(name,profile_data) or len(value)>FILE_LIMIT or b'AGE-SECRET-KEY-' in value:raise Refused()
target=root/name;target.parent.mkdir(mode=0o700,parents=True,exist_ok=True)
fd=os.open(target,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
with os.fdopen(fd,'wb') as out:out.write(value)
def import_files(raw,root,mode):
names=set();total=0
with tarfile.open(fileobj=io.BytesIO(raw),mode='r:') as archive:
for member in archive:
expected=('data/privacyidea/',) if mode=='mfa' else ('data/lldap/','data/authelia/','data/keycape/')
if (not member.isfile() or not member.name.startswith(expected) or member.name in names
or member.size<0 or member.size>FILE_LIMIT or len(names)>=2048):raise Refused()
names.add(member.name);total+=member.size
if total>LIMIT:raise Refused()
data=archive.extractfile(member).read()
if len(data)!=member.size:raise Refused()
save(root,member.name,data)
def resource(root,ns,kind,name):
p=root/f'resources/{ns}/{kind}/{name}.json'
data=json.loads(p.read_bytes())
if data['metadata']['name']!=name or data['metadata']['namespace']!=ns:raise Refused()
return data
def checks(root,profile_data=None):
# Checks describe stored formats/integrity, not current login validity.
for c in (profile_data or profile())['components']:
deployment=resource(root,c['namespace'],'deployment',c['deployment'])
if deployment.get('kind')!='Deployment' or deployment['metadata']['uid']!=c['deployment_uid']:raise Refused()
for name in c['secret_refs']:
data=resource(root,c['namespace'],'secret',name)
if data.get('kind')!='Secret' or not isinstance(data.get('data'),dict) or not data['data']:raise Refused()
for value in data['data'].values():
if b'AGE-SECRET-KEY-' in base64.b64decode(value,validate=True):raise Refused()
for name in c['configmap_refs']:
if resource(root,c['namespace'],'configmap',name).get('kind')!='ConfigMap':raise Refused()
for name,file in [('lldap','users.db'),('authelia','db.sqlite3')]:
db=root/f'data/{name}/{file}'
with sqlite3.connect(db.as_uri()+'?mode=ro',uri=True) as con:
if con.execute('PRAGMA integrity_check').fetchall()!=[('ok',)]:raise Refused()
with (root/'databases/privacyidea.dump').open('rb') as source:
if source.read(5)!=b'PGDMP':raise Refused()
spec=importlib.util.spec_from_file_location('pi_keys',ROOT/'sso-mfa/k8s/privacyidea/key-custody-remote.py')
keys=importlib.util.module_from_spec(spec);spec.loader.exec_module(keys)
keys.validate({name:(root/'data/privacyidea'/name).read_bytes() for name in ('enckey','private.pem','public.pem')})
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
config=resource(root,'sso','secret','keycape-config')['data']
if not base64.b64decode(config['config.yaml'],validate=True):raise Refused()
signing=serialization.load_pem_private_key(base64.b64decode(config['key.pem'],validate=True),password=None)
if not isinstance(signing,rsa.RSAPrivateKey) or signing.key_size<2048:raise Refused()
# Policy must exist, even for an empty set of receipts; do not silently
# restore an issuer without its current authentication policy.
policy=json.loads((root/'data/keycape/policy.json').read_bytes())
if not isinstance(policy,dict):raise Refused()
return {'sqlite_integrity_passed':True,'privacyidea_key_format_and_pair_passed':True,
'keycape_signing_key_format_passed':True,'policy_json_present':True,
'database_custom_format_present':True,'native_identity_recovery_proven':False}
def inventory(root):
private(root,True);entries=[];total=0
for path in sorted(root.rglob('*')):
if path.is_dir():private(path,True);continue
private(path);name=path.relative_to(root).as_posix()
if not allowed(name):raise Refused()
with path.open('rb') as source:value=source.read(FILE_LIMIT+1)
if len(value)>FILE_LIMIT or b'AGE-SECRET-KEY-' in value:raise Refused()
total+=len(value)
if total>LIMIT or len(entries)>=4096:raise Refused()
entries.append({'path':name,'bytes':len(value),'sha256':hashlib.sha256(value).hexdigest()})
if not entries:raise Refused()
return entries
def seal(root,output,capture):
output=output.absolute();private(output.parent,True)
if output.exists() or output.is_symlink():raise Refused()
checked=checks(root);entries=inventory(root)
manifest={'schema':'netkingdom.identity-archive.v1','profile_sha256':hashlib.sha256(PROFILE.read_bytes()).hexdigest(),
'profile':profile(),
'captured_at':datetime.now(timezone.utc).isoformat(),'capture':capture,'files':entries,'checks':checked}
with tempfile.TemporaryDirectory(prefix='.identity-seal-',dir=output.parent) as temporary:
ciphertext=Path(temporary)/'ciphertext'
fd=os.open(ciphertext,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600)
proc=subprocess.Popen(['age','-r',profile()['public_recipient']],stdin=subprocess.PIPE,stdout=fd,stderr=subprocess.DEVNULL)
os.close(fd)
try:
with tarfile.open(fileobj=proc.stdin,mode='w|') as archive:
raw=json.dumps(manifest,sort_keys=True).encode();m=tarfile.TarInfo('manifest.json');m.size=len(raw);m.mode=0o600
archive.addfile(m,io.BytesIO(raw))
for entry in entries:
with (root/entry['path']).open('rb') as source:
value=source.read(FILE_LIMIT+1)
if hashlib.sha256(value).hexdigest()!=entry['sha256']:raise Refused()
m=tarfile.TarInfo(entry['path']);m.size=len(value);m.mode=0o600
archive.addfile(m,io.BytesIO(value))
proc.stdin.close()
if proc.wait(timeout=120) or not 0<ciphertext.stat().st_size<=LIMIT:raise Refused()
# Check source completeness after encryption too; changed/new files
# refuse publication. This is not same-UID adversary isolation.
if inventory(root)!=entries:raise Refused()
with ciphertext.open('rb') as source:
os.fsync(source.fileno());digest=hashlib.file_digest(source,'sha256').hexdigest()
os.link(ciphertext,output)
finally:
if proc.poll() is None:proc.kill();proc.wait()
return {'schema':'netkingdom.identity-capture.v1','status':'encrypted_bundle_created',
'ciphertext_sha256':digest,'ciphertext_bytes':output.stat().st_size,'ciphertext_path':str(output),
'profile_sha256':manifest['profile_sha256'],'file_count':len(entries),
'checks':checked,'offsite_recovery_proven':False,'secret_values_exported':False}
def restore(archive_path,root):
private(root,True)
with tarfile.open(archive_path,mode='r:') as archive:
first=archive.next()
if first is None or first.name!='manifest.json' or not first.isfile() or not 0<first.size<2*1024*1024:raise Refused()
manifest=json.load(archive.extractfile(first))
if manifest.get('schema')!='netkingdom.identity-archive.v1':raise Refused()
captured=manifest['profile']
if captured.get('schema')!='netkingdom.identity-backup-profile.v1':raise Refused()
if len(captured['components'])!=4:raise Refused()
if {(c['namespace'],c['deployment']) for c in captured['components']}!={('sso','lldap'),('sso','authelia'),('sso','keycape'),('mfa','privacyidea')}:raise Refused()
for c in captured['components']:
for key in ('secret_refs','configmap_refs'):
if len(set(c[key]))!=len(c[key]) or any(not NAME.fullmatch(n) or n in {'.','..'} for n in c[key]):raise Refused()
entries=manifest['files'];expected={e['path']:e for e in entries}
if len(expected)!=len(entries) or len(entries)>4096:raise Refused()
seen=set();total=0
# Iteration starts with the already-read first member; explicitly skip
# only that member. A later duplicate manifest is never accepted.
for index,member in enumerate(archive):
if index==0 and member is first:continue
name=member.name
if (not member.isfile() or name in seen or name not in expected or not allowed(name,captured)
or not 0<=member.size<=FILE_LIMIT or member.size!=expected[name]['bytes']):raise Refused()
seen.add(name);total+=member.size
if total>LIMIT:raise Refused()
raw=archive.extractfile(member).read()
if len(raw)!=member.size or hashlib.sha256(raw).hexdigest()!=expected[name]['sha256']:raise Refused()
save(root,name,raw,captured)
if seen!=set(expected):raise Refused()
return manifest,checks(root,captured)
def require_runtime_host():
import socket
host=profile()['runtime_host']
if (os.geteuid()!=0 or socket.gethostname()!=host['hostname']
or hashlib.sha256(Path('/etc/machine-id').read_bytes()).hexdigest()!=host['machine_id_sha256']):raise Refused()

View file

@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""Folder-scoped Storage Share ciphertext publication and verified readback.
Runtime gets only a dedicated read/create share token. Account administration,
deletion and age private keys have no route in this publisher.
"""
import base64
from datetime import datetime,timezone
import hashlib
import json
import os
from pathlib import Path
import re
import tempfile
import urllib.error
import urllib.request
import xml.etree.ElementTree as ET
HOST='https://nx4069.your-storageshare.de'
BASE=HOST+'/public.php/webdav/'
NAME=re.compile(r'net-kingdom-identity-\d{8}T\d{6}Z-[a-f0-9]{12}\.tar\.age\Z')
LIMIT=512*1024*1024
HEADROOM=2*1024**3
class Refused(Exception):pass
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self,*args,**kwargs):raise Refused()
def opener():return urllib.request.build_opener(urllib.request.ProxyHandler({}),NoRedirect())
def headers(token):
if not isinstance(token,str) or not re.fullmatch('[A-Za-z0-9]{10,128}',token):raise Refused()
return {'Authorization':'Basic '+base64.b64encode((token+':').encode()).decode(),
'X-Requested-With':'XMLHttpRequest','Accept-Encoding':'identity'}
def digest(path):
with path.open('rb') as source:return hashlib.file_digest(source,'sha256').hexdigest()
def quota(client,token):
body=b'<d:propfind xmlns:d="DAV:"><d:prop><d:quota-available-bytes/></d:prop></d:propfind>'
h=headers(token);h.update(Depth='0');h['Content-Type']='application/xml'
with client.open(urllib.request.Request(BASE,data=body,method='PROPFIND',headers=h),timeout=30) as response:
if response.status!=207 or response.url!=BASE:raise Refused()
raw=response.read(1024*1024+1)
if len(raw)>1024*1024:raise Refused()
amount=ET.fromstring(raw).findtext('.//{DAV:}quota-available-bytes') or ''
if not amount.isdigit():raise Refused()
return int(amount)
def etag(response,url,size):
value=response.headers.get('ETag','')
if (response.status!=200 or response.url!=url or response.headers.get('Content-Length')!=str(size)
or not re.fullmatch(r'"[^"\x00-\x20]{1,180}"',value)):raise Refused()
return value
def readback(client,token,name,size,expected,target):
valid=NAME.fullmatch(name) or (name.endswith('.receipt.json') and NAME.fullmatch(name.removesuffix('.receipt.json')) and size<=65536)
if not valid or not 0<size<=LIMIT:raise Refused()
url=BASE+name;h=headers(token)
with client.open(urllib.request.Request(url,method='HEAD',headers=h),timeout=30) as response:version=etag(response,url,size)
h['If-Match']=version;count=0;sha=hashlib.sha256()
with client.open(urllib.request.Request(url,headers=h),timeout=180) as response:
if etag(response,url,size)!=version:raise Refused()
fd=os.open(target,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
with os.fdopen(fd,'wb') as out:
while block:=response.read(1024*1024):
count+=len(block)
if count>size:raise Refused()
sha.update(block);out.write(block)
out.flush();os.fsync(out.fileno())
if count!=size or sha.hexdigest()!=expected:raise Refused()
with client.open(urllib.request.Request(url,method='HEAD',headers=h),timeout=30) as response:
if etag(response,url,size)!=version:raise Refused()
return version
def publish(client,token,source,capture,target):
if (not NAME.fullmatch(source.name) or source.is_symlink() or source.stat().st_mode&0o077
or source.stat().st_uid!=os.getuid() or source.stat().st_nlink!=1 or not source.is_file()
or source.stat().st_size>LIMIT or capture.get('status')!='encrypted_bundle_created'
or capture.get('helper_cleanup_verified') is not True or capture.get('temporary_plaintext_cleanup_verified') is not True
or digest(source)!=capture.get('ciphertext_sha256') or source.stat().st_size!=capture.get('ciphertext_bytes')):raise Refused()
with source.open('rb') as stream:
if stream.read(22)!=b'age-encryption.org/v1\n':raise Refused()
available=quota(client,token)
if available<source.stat().st_size+HEADROOM:raise Refused()
url=BASE+source.name;h=headers(token)
try:
with client.open(urllib.request.Request(url,method='HEAD',headers=h),timeout=30):raise Refused()
except urllib.error.HTTPError as error:
if error.code!=404:raise Refused() from None
h.update({'If-None-Match':'*','Content-Type':'application/octet-stream','Content-Length':str(source.stat().st_size)})
# An unknown upload outcome is never retried under this name automatically.
with source.open('rb') as stream:
with client.open(urllib.request.Request(url,data=stream,method='PUT',headers=h),timeout=600) as response:
if response.status!=201 or response.url!=url:raise Refused()
version=readback(client,token,source.name,source.stat().st_size,capture['ciphertext_sha256'],target)
index={'schema':'netkingdom.identity-offsite-index.v1','status':'readback_verified',
'artifact':source.name,'ciphertext_sha256':capture['ciphertext_sha256'],
'ciphertext_bytes':source.stat().st_size,'captured_at':capture['finished_at']}
raw=json.dumps(index,sort_keys=True).encode();index_name=source.name+'.receipt.json';index_url=BASE+index_name
h=headers(token);h.update({'If-None-Match':'*','Content-Type':'application/json','Content-Length':str(len(raw))})
with client.open(urllib.request.Request(index_url,data=raw,method='PUT',headers=h),timeout=30) as response:
if response.status!=201 or response.url!=index_url:raise Refused()
with tempfile.TemporaryDirectory(prefix='.identity-index-',dir=target.parent) as temporary:
readback(client,token,index_name,len(raw),hashlib.sha256(raw).hexdigest(),Path(temporary)/'index.json')
return {'schema':'netkingdom.identity-offsite.v1','status':'readback_verified',
'observed_at':datetime.now(timezone.utc).isoformat(),'provider':'Storage Share',
'folder':'Backup/railiance-backups/net-kingdom-identity','artifact':source.name,
'ciphertext_sha256':capture['ciphertext_sha256'],'ciphertext_bytes':source.stat().st_size,
'fetched_path':str(target),'etag':version,'quota_available_before':available,
'new_object_created':True,'downloaded_digest_verified':True,
'verified_index_published':True,'index_name':index_name,
'age_private_key_used':False,'secret_values_exported':False,'retained_objects_deleted':False,
'native_identity_recovery_proven':False}

View file

@ -0,0 +1,381 @@
#!/usr/bin/env python3
"""Owner-run T18 session assembly using the existing archive/containment pipeline.
Candidate only: native pinned-image tests and review are required before use.
Shared Docker local-driver tmpfs is required because Docker cp does not support
private tmpfs/mount paths, and the original KeyCape image is distroless.
"""
import argparse
import base64
from datetime import datetime, timezone
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import signal
import re
import stat
import select
import subprocess
import sys
import tempfile
import time
import uuid
HERE=Path(__file__).resolve().parent
def load(name):
spec=importlib.util.spec_from_file_location(name,HERE/(name+'.py'))
module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module);return module
adapter=load('isolated-keycape')
local=load('isolated-lldap')
bundle=local.bundle
# Bind the reused preflight itself, not only this module's direct Docker calls.
local.command=adapter.command
_original_preflight=local.preflight
def preflight():
contexts=json.loads(adapter.command(['docker','context','inspect']))
if len(contexts)!=1 or contexts[0]['Endpoints']['docker']['Host']!=adapter.ENDPOINT:raise adapter.Refused()
result=_original_preflight();result['docker_endpoint']=adapter.ENDPOINT;return result
local.preflight=preflight
WORKER=local.CLIENT
STAGING_ROOT=Path('/dev/shm')
RUNROOTS=('/etc/keycape','/var/lib/keycape-policy','/etc/keycape-factor','/recovery-tls')
VOLUME_OPTIONS={'type':'tmpfs','device':'tmpfs','o':'size=33554432,uid=1000,gid=1000,mode=0700,nosuid,nodev,noexec'}
IMPORT=r'''
import base64,json,os,sys
from pathlib import Path
os.umask(0o077)
p=json.load(sys.stdin)
allowed=('/etc/keycape/','/var/lib/keycape-policy/','/etc/keycape-factor/','/recovery-tls/')
for name,encoded in p.items():
f=Path(name)
if not name.startswith(allowed) or '..' in f.parts or any(x.is_symlink() for x in (f,*f.parents)):raise ValueError()
f.parent.mkdir(mode=0o700,parents=True,exist_ok=True)
fd=os.open(f,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
with os.fdopen(fd,'wb') as out:out.write(base64.b64decode(encoded,validate=True))
'''
# Small transport inside the already pinned helper; it has only loopback routes.
# No token handling, auth bypass, dependency imitation, reset or notifier path.
TLS=r'''
import select,socket,socketserver,ssl,threading
context=ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.minimum_version=ssl.TLSVersion.TLSv1_2
context.load_cert_chain('/recovery-tls/server.pem','/recovery-tls/server-key.pem')
class Handler(socketserver.BaseRequestHandler):
def handle(self):
try:
with context.wrap_socket(self.request,server_side=True) as incoming:
with socket.create_connection(('127.0.0.1',8080),timeout=10) as outgoing:
incoming.settimeout(15);outgoing.settimeout(15)
while True:
ready,_,_=select.select([incoming,outgoing],[],[],15)
if not ready:return
for source in ready:
data=source.recv(65536)
if not data:return
(outgoing if source is incoming else incoming).sendall(data)
except Exception:pass
class Server(socketserver.ThreadingTCPServer):
allow_reuse_address=True
daemon_threads=True
with Server(('127.0.0.1',443),Handler) as server:server.serve_forever()
'''
def run(argv,**kwargs):return adapter.command(argv,**kwargs)
def plan_session():
run_id=uuid.uuid4().hex
stem='nk49-kc-'+run_id
worker=stem+'-transport';issuer=stem+'-issuer'
volumes={stem+'-v'+str(i):{'target':root,'options':dict(VOLUME_OPTIONS)} for i,root in enumerate(RUNROOTS)}
return {'run_id':run_id,'containers':[worker,issuer], 'worker':worker,'issuer':issuer,
'images':{worker:WORKER,issuer:adapter.IMAGE},'volumes':volumes,
'public_image_probe':stem+'-image-probe','docker_endpoint':adapter.ENDPOINT}
def verify_volume(name,spec,run_id):
obj=json.loads(run(['docker','volume','inspect',name]))
if (len(obj)!=1 or obj[0]['Name']!=name or obj[0]['Driver']!='local' or obj[0].get('Scope')!='local'
or obj[0].get('Options')!=VOLUME_OPTIONS or spec['options']!=VOLUME_OPTIONS
or obj[0].get('Labels',{}).get(adapter.LABEL)!=run_id):raise adapter.Refused()
def base_args(session,name,network):
return ['docker','run','--detach','--pull=never','--name',name,'--network',network,
'--label',adapter.LABEL+'='+session['run_id'],'--read-only','--cap-drop','ALL',
'--security-opt','no-new-privileges','--user','1000:1000','--memory','512m','--cpus','1',
'--pids-limit','64','--log-driver','none','--restart','no',
'--tmpfs','/tmp:rw,nosuid,nodev,noexec,size=16777216,uid=1000,gid=1000,mode=0700']
def mounts(session,readonly=False):
args=[]
for name,spec in session['volumes'].items():
args+=['--mount','type=volume,source='+name+',target='+spec['target']+(',readonly' if readonly else '')]
return args
def provision(session,payload,tls,issuer):
"""Inspect containment and driver options before the first private import."""
from urllib.parse import urlsplit
address=urlsplit(issuer)
if address.scheme!='https' or not address.hostname or address.port not in (None,443) or address.path not in ('','/'):
raise adapter.Refused()
for name,spec in session['volumes'].items():
run(['docker','volume','create','--driver','local','--label',adapter.LABEL+'='+session['run_id'],
'--opt','type=tmpfs','--opt','device=tmpfs','--opt','o='+VOLUME_OPTIONS['o'],name])
verify_volume(name,spec,session['run_id'])
args=base_args(session,session['worker'],'none')+mounts(session)
args+=['--sysctl','net.ipv4.ip_unprivileged_port_start=0','--add-host',address.hostname+':127.0.0.1',
'--entrypoint','python3',WORKER,'-c','import time;time.sleep(1800)']
run(args)
# Check helper and every private filesystem before touching the ciphertext's
# private contents. Issuer is created only after originals are present.
adapter.inspect_session({**session,'containers':[session['worker']],'issuer':session['worker']},worker_only=True)
files={**payload['files'],**{k:base64.b64encode(v).decode() for k,v in tls.items()}}
run(['docker','exec','-i',session['worker'],'python3','-c',IMPORT],input=json.dumps(files).encode())
anchor=run(['docker','container','inspect',session['worker'],'--format','{{.Id}}']).decode().strip()
args=base_args(session,session['issuer'],'container:'+anchor)+mounts(session,readonly=True)
# CLI argv carries environment names only. Original credentials stay in the
# owner subprocess environment and Docker's private container configuration.
env={'PATH':os.environ.get('PATH',os.defpath),'DOCKER_HOST':'unix:///var/run/docker.sock',**payload['env'],
'SSL_CERT_FILE':'/recovery-tls/trust.pem'}
for name in (*payload['env'],'SSL_CERT_FILE'):args+=['--env',name]
args+=[adapter.IMAGE]
run(args,env=env)
adapter.inspect_session(session)
run(['docker','exec','--detach',session['worker'],'python3','-c',TLS])
def cleanup_all(session,receipt):
failed=False
try:adapter.cleanup({**session,'containers':session['containers']+[session['public_image_probe']]},receipt)
except Exception:failed=True
rows=[]
for name,spec in session['volumes'].items():
row={'volume':name,'status':'unverified'};rows.append(row)
try:
exists=run(['docker','volume','ls','--filter','name=^'+name+'$','--format','{{.Name}}']).decode().splitlines()
if not exists:
row['status']='absence_verified'
continue
verify_volume(name,spec,session['run_id'])
run(['docker','volume','rm',name])
remaining=run(['docker','volume','ls','--filter','name=^'+name+'$','--format','{{.Name}}']).decode().splitlines()
if remaining:row['status']='still_present';failed=True
else:row['status']='absence_verified'
except Exception:failed=True
receipt['tmpfs_volume_cleanup_obligations']=rows
receipt['container_cleanup_verified']=not failed
if failed:raise adapter.Refused()
def register_workdir(work, run_id):
if work.parent!=STAGING_ROOT or not re.fullmatch('nk49-keycape-'+run_id+'-[a-z0-9_]{8}',work.name):raise adapter.Refused()
if any(p.is_symlink() for p in (work,*work.parents)):raise adapter.Refused()
info=work.lstat()
if not stat.S_ISDIR(info.st_mode) or info.st_uid!=os.getuid() or stat.S_IMODE(info.st_mode)!=0o700:raise adapter.Refused()
return {'path':str(work),'run_id':run_id,'device':info.st_dev,'inode':info.st_ino,'owner_uid':info.st_uid}
def cleanup_workdir(staging, receipt):
"""Delete only the registered inode below actual /dev/shm, never a replacement."""
row={'path':staging['path'],'device':staging['device'],'inode':staging['inode'],'status':'unverified'}
receipt['plaintext_cleanup_obligation']=row
work=Path(staging['path'])
if work.parent!=STAGING_ROOT or not re.fullmatch('nk49-keycape-'+staging['run_id']+'-[a-z0-9_]{8}',work.name):raise adapter.Refused()
if not any(line.split()[1:3]==['/dev/shm','tmpfs'] for line in Path('/proc/mounts').read_text().splitlines()):raise adapter.Refused()
if any(p.is_symlink() for p in work.parents):raise adapter.Refused()
parentfd=os.open(work.parent,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW)
try:
try:info=os.stat(work.name,dir_fd=parentfd,follow_symlinks=False)
except FileNotFoundError:
row['status']='absence_verified';receipt['temporary_plaintext_cleanup_verified']=True;return
if (not stat.S_ISDIR(info.st_mode) or info.st_uid!=staging['owner_uid'] or info.st_uid!=os.getuid()
or info.st_dev!=staging['device'] or info.st_ino!=staging['inode'] or stat.S_IMODE(info.st_mode)!=0o700):
row['status']='identity_mismatch_not_removed';raise adapter.Refused()
def remove(parent,name,expected):
fd=os.open(name,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=parent)
try:
current=os.fstat(fd)
if (current.st_dev,current.st_ino)!=(expected.st_dev,expected.st_ino):raise adapter.Refused()
for child in os.listdir(fd):
before=os.stat(child,dir_fd=fd,follow_symlinks=False)
if before.st_uid!=staging['owner_uid'] or before.st_dev!=staging['device']:raise adapter.Refused()
if stat.S_ISDIR(before.st_mode):remove(fd,child,before)
elif stat.S_ISREG(before.st_mode) and before.st_nlink==1:os.unlink(child,dir_fd=fd)
else:raise adapter.Refused() # symlinks/special files/hardlinks are uncertain, never followed
finally:os.close(fd)
after=os.stat(name,dir_fd=parent,follow_symlinks=False)
if (after.st_dev,after.st_ino)!=(expected.st_dev,expected.st_ino):raise adapter.Refused()
os.rmdir(name,dir_fd=parent)
remove(parentfd,work.name,info)
try:os.stat(work.name,dir_fd=parentfd,follow_symlinks=False)
except FileNotFoundError:
row['status']='absence_verified';receipt['temporary_plaintext_cleanup_verified']=True;return
raise adapter.Refused()
finally:os.close(parentfd)
def recover_obligations(session, staging, receipt):
failed=False
try:cleanup_all(session,receipt)
except Exception:failed=True
# The plaintext obligation is attempted even when Docker is unreachable.
try:cleanup_workdir(staging,receipt)
except Exception:failed=True
if failed:raise adapter.Refused()
def start_reaper(session, staging):
# Starts and ACKs exact obligations before any plaintext archive write.
# The child gets no custody descriptors or credential environment and uses
# the same explicit endpoint for every Docker operation after parent loss.
adapter.docker_environment()
if session.get('docker_endpoint')!=adapter.ENDPOINT or staging.get('run_id')!=session['run_id']:raise adapter.Refused()
if register_workdir(Path(staging['path']),session['run_id'])!=staging:raise adapter.Refused()
code="""import importlib.util,json,os,sys,time
spec=importlib.util.spec_from_file_location('session',sys.argv[1]);m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
session=json.loads(sys.argv[2]);staging=json.loads(sys.argv[3]);receipt={'status':'failed'}
print('registered',flush=True)
time.sleep(1800)
try:
m.recover_obligations(session,staging,receipt);receipt['status']='bounded_cleanup_verified'
except Exception:receipt['status']='bounded_cleanup_uncertain'
# Retain bounded outcome beside the exact registered staging directory; this
# child owns only this exclusive receipt path, never an arbitrary output path.
try:
fd=os.open(staging['path']+'-reaper.json',os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
with os.fdopen(fd,'w') as out:json.dump(receipt,out)
except Exception:pass
"""
child=subprocess.Popen([sys.executable,'-c',code,str(Path(__file__).resolve()),json.dumps(session),json.dumps(staging)],
stdin=subprocess.DEVNULL,stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,
env={'PATH':os.environ.get('PATH',os.defpath),'DOCKER_HOST':adapter.ENDPOINT},
close_fds=True,start_new_session=True)
ready,_,_=select.select([child.stdout],[],[],10)
if not ready or child.stdout.readline()!=b'registered\n':
child.terminate();child.wait(timeout=5);raise adapter.Refused()
child.stdout.close()
return child
def execute(transfer,admission,tls,receipt,checkpoint=None):
"""Existing private custody/transfer/archive APIs; no second archive format."""
target=local.preflight()
for image in (adapter.IMAGE,):run(['docker','image','inspect',image])
local.offline.validate(transfer)
if transfer.get('independent_owner_retrieval_verified') is not True:raise adapter.Refused()
path=Path(transfer['fetched_path']);bundle.private(path,max_bytes=bundle.LIMIT)
if (path.stat().st_size!=transfer['ciphertext_bytes'] or local.offline.offsite.digest(path)!=transfer['ciphertext_sha256']
or admission['ciphertext_sha256']!=transfer['ciphertext_sha256']):raise adapter.Refused()
if hashlib.sha256(tls['/recovery-tls/trust.pem']).hexdigest()!=admission['trust_sha256']:raise adapter.Refused()
if hashlib.sha256(tls['/recovery-tls/server.pem']).hexdigest()!=admission['tls_cert_sha256']:raise adapter.Refused()
# Validate exact private certificate/key pair and original issuer name before
# opening service containers; do not generate replacement trust or keys.
from cryptography import x509
from cryptography.hazmat.primitives import serialization
cert=x509.load_pem_x509_certificate(tls['/recovery-tls/server.pem'])
key=serialization.load_pem_private_key(tls['/recovery-tls/server-key.pem'],password=None)
if cert.public_key().public_numbers()!=key.public_key().public_numbers():raise adapter.Refused()
keyfd=local.verify.custody.private_key_fd();work=None;session=None;reaper=None;staging=None
receipt.update(status='failed',target=target,session_source_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
source_files_sha256={name:hashlib.sha256((HERE/name).read_bytes()).hexdigest() for name in ('isolated-keycape.py','isolated-keycape-session.py','run-isolated-keycape.sh','isolated-lldap.py','identity-bundle.py','verify-identity.py','retrieve-identity-offline.py','identity-offsite.py')},
temporary_plaintext_cleanup_verified=False,container_cleanup_verified=False,
downstream_consumer_reconnect_proven=False,human_mfa_proven=False)
try:
if run(['age-keygen','-y',f'/proc/self/fd/{keyfd}'],pass_fds=(keyfd,)).strip()!=bundle.profile()['public_recipient'].encode():raise adapter.Refused()
session=plan_session()
work=Path(tempfile.mkdtemp(prefix='nk49-keycape-'+session['run_id']+'-',dir='/dev/shm'))
staging=register_workdir(work,session['run_id'])
reaper=start_reaper(session,staging)
receipt.update(bounded_cleanup_reaper_started=True,plaintext_cleanup_obligation={**staging,'status':'registered_before_plaintext'},docker_endpoint=adapter.ENDPOINT,
reaper_outcome_path=staging['path']+'-reaper.json',
registered_cleanup={'containers':session['containers']+[session['public_image_probe']],'tmpfs_volumes':list(session['volumes'])})
if checkpoint is not None:checkpoint(receipt)
archive=work/'archive.tar'
with archive.open('xb') as output:
p=subprocess.run(['age','-d','-i',f'/proc/self/fd/{keyfd}',str(path)],stdout=output,stderr=subprocess.DEVNULL,pass_fds=(keyfd,),timeout=180)
if p.returncode or archive.stat().st_size>bundle.LIMIT+4*1024*1024:raise adapter.Refused()
root=work/'restored';root.mkdir(mode=0o700)
manifest,_=bundle.restore(archive,root)
payload=adapter.private_payload(root,manifest,admission['manifest_sha256'])
import yaml
cfg=yaml.safe_load(base64.b64decode(payload['files']['/etc/keycape/config.yaml']))
if cfg.get('port')!=8080 or cfg.get('issuer')!=admission['issuer']:raise adapter.Refused()
# Public executable readback uses the daemon archive API only on the
# original image rootfs, never a tmpfs or private user-created mount.
probe=session['public_image_probe']
try:
run(['docker','create','--name',probe,'--network','none','--read-only','--label',adapter.LABEL+'='+session['run_id'],adapter.IMAGE])
binary=adapter.container_file(probe,'/keycape')
if hashlib.sha256(binary).hexdigest()!=admission['verifier_sha256']:raise adapter.Refused()
finally:
receipt['public_image_probe_cleanup_obligations']=[{'container':probe,'status':'unverified'}]
run(['docker','rm',probe])
if run(['docker','container','ls','--all','--filter','name=^/'+probe+'$','--format','{{.Names}}']).strip():raise adapter.Refused()
receipt['public_image_probe_cleanup_obligations'][0]['status']='absence_verified'
provision(session,payload,tls,admission['issuer'])
receipt.update(ciphertext_sha256=transfer['ciphertext_sha256'],all_manifest_hashes_verified=True,manifest_file_count=len(manifest['files']))
checked_admission={**admission,'session':session,'trust_path':'/recovery-tls/trust.pem'}
# Native checker consumes no service secret argument or new grant.
# Retry only bounded public readiness; no account/factor attempt.
for attempt in range(40):
try:
numbers=serialization.load_pem_private_key(base64.b64decode(payload['files']['/etc/keycape/key.pem']),password=None).public_key().public_numbers()
adapter.check_jwks(session,checked_admission,numbers);break
except Exception:
if attempt==39:raise
time.sleep(.25)
adapter.native_checks(root,manifest,checked_admission,receipt,cleanup_session=False)
finally:
os.close(keyfd)
try:
if session is not None and staging is not None:recover_obligations(session,staging,receipt)
if reaper is not None:
reaper.terminate();reaper.wait(timeout=10)
except Exception:
receipt['status']='failed'
raise
finally:
if work is not None and not work.exists():receipt['temporary_plaintext_cleanup_verified']=True
def persist_receipt(fd,receipt):
raw=(json.dumps(receipt,indent=2)+'\n').encode()
os.lseek(fd,0,os.SEEK_SET);os.ftruncate(fd,0)
with os.fdopen(os.dup(fd),'wb') as output:
output.write(raw);output.flush();os.fsync(output.fileno())
def main():
p=argparse.ArgumentParser(description=__doc__)
for name in ('transfer-receipt','admission','admission-sha256','tls-cert','tls-key','trust','receipt'):p.add_argument('--'+name,required=True)
args=p.parse_args();os.umask(0o077)
fd=os.open(args.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
receipt={'schema':'netkingdom.isolated-keycape-recovery.v1','status':'failed','production_contacted':False,'secret_values_exported':False}
try:
raw=Path(args.admission).read_bytes()
if hashlib.sha256(raw).hexdigest()!=args.admission_sha256:raise adapter.Refused()
admission=json.loads(raw)
tls={}
for name,path in (('server.pem',args.tls_cert),('server-key.pem',args.tls_key),('trust.pem',args.trust)):
source=Path(path);bundle.private(source,max_bytes=1024*1024);tls['/recovery-tls/'+name]=source.read_bytes()
execute(json.loads(Path(args.transfer_receipt).read_text()),admission,tls,receipt,checkpoint=lambda r:persist_receipt(fd,r))
except (Exception,KeyboardInterrupt):receipt.update(status='failed',error='isolated_keycape_failed_private_diagnostics_suppressed')
finally:
receipt['finished_at']=datetime.now(timezone.utc).isoformat()
try:persist_receipt(fd,receipt)
finally:os.close(fd)
print('OK: isolated KeyCape native checks and cleanup passed.' if receipt['status']=='isolated_keycape_native_registration_checked' else 'ERROR: isolated KeyCape stopped; review safe receipt.')
return int(receipt['status']!='isolated_keycape_native_registration_checked')
if __name__=='__main__':
for sig in (signal.SIGHUP,signal.SIGTERM,signal.SIGINT):signal.signal(sig,local.offline.interrupted)
raise SystemExit(main())

View file

@ -0,0 +1,419 @@
#!/usr/bin/env python3
"""T18 native adapter for an owner-admitted, already restored isolated session.
Called after identity-bundle.restore, never with an unvalidated directory. This
module does not provision TLS, grant credentials, or claim downstream acceptance.
The existing decrypt/tmpfs wrapper remains responsible for archive cleanup.
"""
import base64
import hashlib
import importlib.util
import json
import io
import os
import secrets
import tarfile
import re
import subprocess
from pathlib import Path
HERE = Path(__file__).resolve().parent
IMAGE = 'forgejo.coulomb.social/coulomb/key-cape@sha256:cdec9dd7c515f7e0a6169fc3fa560ee2cc47bf40d899755dec808c362f5ce010'
LABEL = 'net-kingdom.recovery-run'
class Refused(Exception):
pass
def load_bundle():
spec = importlib.util.spec_from_file_location('identity_bundle', HERE / 'identity-bundle.py')
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def private_payload(root, manifest, expected_manifest_sha256):
"""Bind the complete validated archive before selecting KeyCape material.
Private hashes never enter the public receipt. The digest is supplied by the
existing private archive admission, not inferred from an untrusted manifest.
"""
encoded = json.dumps(manifest, sort_keys=True, separators=(',', ':')).encode()
if hashlib.sha256(encoded).hexdigest() != expected_manifest_sha256:
raise Refused()
entries = manifest['files']
if len(entries) != len({e['path'] for e in entries}):
raise Refused()
values = {}
bundle = load_bundle()
for entry in entries:
if not bundle.allowed(entry['path'], manifest['profile']):
raise Refused()
path = root / entry['path']
bundle.private(path)
value = path.read_bytes()
if len(value) != entry['bytes'] or hashlib.sha256(value).hexdigest() != entry['sha256']:
raise Refused()
values[entry['path']] = value
deployment = json.loads(values['resources/sso/deployment/keycape.json'])
main = next(c for c in deployment['spec']['template']['spec']['containers'] if c['name'] == 'keycape')
if main.get('command') or main.get('args') or main.get('envFrom'):
raise Refused()
if main['image'] != IMAGE:
# Mutable-tag/historical mismatches need a separate explicit owner admission.
raise Refused()
secret = json.loads(values['resources/sso/secret/keycape-config.json'])['data']
files = {'/etc/keycape/' + name: base64.b64decode(secret[name], validate=True)
for name in ('config.yaml', 'key.pem')}
for path, value in values.items():
if path.startswith('data/keycape/'):
relative = path.removeprefix('data/keycape/')
files['/var/lib/keycape-policy/' + relative] = value
if '/var/lib/keycape-policy/policy.json' not in files:
raise Refused()
if 'resources/sso/secret/keycape-factor-read.json' in values:
factor=json.loads(values['resources/sso/secret/keycape-factor-read.json'])['data']
files['/etc/keycape-factor/admin-token']=base64.b64decode(factor['admin-token'],validate=True)
# Resolve original delivered environment, including all referenced secrets.
env = {}
for item in main.get('env', []):
if not re.fullmatch(r'KEYCAPE_[A-Z0-9_]+',item['name']):raise Refused()
if item['name'] in env:
raise Refused()
if 'value' in item:
env[item['name']] = item['value']
else:
ref = item['valueFrom']['secretKeyRef']
source = json.loads(values['resources/sso/secret/' + ref['name'] + '.json'])['data']
env[item['name']] = base64.b64decode(source[ref['key']], validate=True).decode()
if env.get('KEYCAPE_CONFIG') != '/etc/keycape/config.yaml' or env.get('KEYCAPE_POLICY_PATH') != '/var/lib/keycape-policy/policy.json':
raise Refused()
return {'files': {p: base64.b64encode(v).decode() for p, v in files.items()}, 'env': env}
ENDPOINT = 'unix:///var/run/docker.sock'
def docker_environment(environment=None):
# Even an admitted daemon exposed by another Unix socket is outside this
# exact session binding. Conflicting ambient routing is refused, not ignored.
for source in (os.environ, environment or {}):
if source.get('DOCKER_HOST') not in (None,'',ENDPOINT):raise Refused()
if source.get('DOCKER_CONTEXT') not in (None,'','default'):raise Refused()
if any(source.get(k) for k in ('DOCKER_TLS_VERIFY','DOCKER_CERT_PATH')):raise Refused()
env=dict(os.environ if environment is None else environment)
for key in ('DOCKER_CONTEXT','DOCKER_TLS_VERIFY','DOCKER_CERT_PATH'):env.pop(key,None)
env['DOCKER_HOST']=ENDPOINT
return env
def process(argv, **kwargs):
if argv[0]=='docker':
argv=['docker','--host',ENDPOINT,*argv[1:]]
kwargs['env']=docker_environment(kwargs.get('env'))
return subprocess.run(argv,**kwargs)
def command(argv, **kwargs):
maximum = kwargs.pop('max_output', 32768)
result = process(argv, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
timeout=kwargs.pop('timeout', 180), **kwargs)
if result.returncode or len(result.stdout) > maximum:
raise Refused()
return result.stdout
def inspect_session(session, worker_only=False):
"""No credential environment is read by Docker inspect or returned to caller."""
if not re.fullmatch(r'[a-f0-9]{32}', session['run_id']):
raise Refused()
names = session['containers']
if not names or len(names) > 12 or len(set(names)) != len(names):
raise Refused()
for name in names:
if not re.fullmatch(r'nk49-kc-' + session['run_id'] + r'-[a-z0-9-]{1,24}', name):
raise Refused()
if session['worker'] not in names or session['issuer'] not in names:
raise Refused()
import os
contexts=json.loads(command(['docker','context','inspect']))
endpoints=[contexts[0]['Endpoints']['docker']['Host'],os.environ.get('DOCKER_HOST') or ENDPOINT]
if not all(x==ENDPOINT for x in endpoints):raise Refused()
info = json.loads(command(['docker', 'info', '--format', '{"ID":{{json .ID}},"Name":{{json .Name}},"OSType":{{json .OSType}}}']))
if info != {'ID': '74f93783-6cef-43a8-a3c4-f62586cd3bc1', 'Name': 'bnt-lap001', 'OSType': 'linux'}:
raise Refused()
# The worker is the anchor; all dependencies/TLS/issuer share its network-none namespace.
anchor = command(['docker', 'container', 'inspect', session['worker'], '--format', '{{.Id}}']).decode().strip()
for name,spec in session.get('volumes',{}).items():
volume=json.loads(command(['docker','volume','inspect',name]))
expected={'type':'tmpfs','device':'tmpfs','o':'size=33554432,uid=1000,gid=1000,mode=0700,nosuid,nodev,noexec'}
if len(volume)!=1 or volume[0]['Driver']!='local' or volume[0].get('Scope')!='local' or volume[0].get('Options')!=expected or spec['options']!=expected or volume[0].get('Labels',{}).get(LABEL)!=session['run_id']:raise Refused()
for name in names:
obj = json.loads(command(['docker', 'container', 'inspect', name, '--format',
'{"host":{{json .HostConfig}},"mounts":{{json .Mounts}},"labels":{{json .Config.Labels}},"image":{{json .Config.Image}},"user":{{json .Config.User}},"entrypoint":{{json .Config.Entrypoint}},"cmd":{{json .Config.Cmd}}}']))
host = obj['host']
mounts=obj['mounts']
destinations=[m['Destination'] for m in mounts]
if len(destinations)!=len(set(destinations)):raise Refused()
for volume,expected_spec in session.get('volumes',{}).items():
matches=[m for m in mounts if m['Destination']==expected_spec['target']]
if len(matches)!=1 or matches[0]['Type']!='volume' or matches[0]['Name']!=volume or matches[0]['RW']!=(name==session['worker']):raise Refused()
if (obj['labels'].get(LABEL) != session['run_id'] or not obj['user'] or obj['user'].split(':')[0] in ('0', 'root')
or not host['ReadonlyRootfs'] or host['Privileged'] or host.get('Binds') or host.get('PortBindings')
or host.get('PidMode') or host.get('IpcMode') == 'host'
or host.get('UTSMode') or host.get('UsernsMode') == 'host' or host.get('CgroupnsMode') == 'host'
or host.get('Sysctls',{}) not in ({},{'net.ipv4.ip_unprivileged_port_start':'0'} if name==session['worker'] else {})
or host.get('CapAdd') or host.get('Devices') or host.get('DeviceRequests') or host.get('VolumesFrom')
or host.get('CapDrop') != ['ALL']
or host.get('SecurityOpt') not in (['no-new-privileges'], ['no-new-privileges:true'])
or any(m['Type']!='tmpfs' and not (m['Type']=='volume' and m['Name'] in session.get('volumes',{}) and m['Destination']==session['volumes'][m['Name']]['target']) for m in obj['mounts'])
or host['NetworkMode'] != ('none' if name == session['worker'] else 'container:' + anchor)
or obj['image'] != session['images'][name] or '@sha256:' not in obj['image']):
raise Refused()
if worker_only:return
if session['images'][session['issuer']] != IMAGE:
raise Refused()
# Known admitted archive has no command/args override; image default /keycape
# must be the actual issuer executable, not an alternate same-image command.
launch=json.loads(command(['docker','container','inspect',session['issuer'],'--format',
'{"entrypoint":{{json .Config.Entrypoint}},"cmd":{{json .Config.Cmd}},"path":{{json .Path}},"args":{{json .Args}}}']))
if launch != {'entrypoint':['/keycape'],'cmd':None,'path':'/keycape','args':[]}:
raise Refused()
def container_file(container, path):
# Docker streams one tar member into memory; never materialize private files.
raw = command(['docker', 'cp', container + ':' + path, '-'], max_output=128*1024*1024)
with tarfile.open(fileobj=io.BytesIO(raw), mode='r:') as archive:
members = archive.getmembers()
if len(members) != 1 or not members[0].isfile() or members[0].size > 96*1024*1024:
raise Refused()
return archive.extractfile(members[0]).read()
JWKS_PROBE = r"""
import base64,ipaddress,json,socket,ssl,sys,urllib.request
from urllib.parse import urlsplit
p=json.load(sys.stdin);issuer=p['issuer'];origin=urlsplit(issuer)
def require(condition):
if not condition:raise ValueError('public_jwks_binding_refused')
require(origin.scheme=='https')
resolved=socket.getaddrinfo(origin.hostname,origin.port or 443,type=socket.SOCK_STREAM)
require(bool(resolved) and all(ipaddress.ip_address(x[4][0]).is_loopback for x in resolved))
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self,*args,**kwargs):return None
opener=urllib.request.build_opener(urllib.request.ProxyHandler({}),NoRedirect(),
urllib.request.HTTPSHandler(context=ssl.create_default_context(cadata=p['trust'])))
def get(url):
u=urlsplit(url)
require((u.scheme,u.hostname,u.port or 443)==(origin.scheme,origin.hostname,origin.port or 443))
require(not u.username and not u.password and not u.fragment)
with opener.open(url,timeout=10) as response:
raw=response.read(1024*1024+1);require(len(raw)<=1024*1024)
return json.loads(raw)
d=get(issuer.rstrip('/')+'/.well-known/openid-configuration');require(d['issuer']==issuer)
keys=get(d['jwks_uri'])['keys']
def integer(value):return int.from_bytes(base64.urlsafe_b64decode(value+'='*(-len(value)%4)),'big')
# The exact admitted owner release constructs one key, kid key-1. Reject every
# extra key, including irrelevant keys: a native successful exchange can then
# only have used the original signer through the existing verifier.
require(isinstance(keys,list) and len(keys)==1)
k=keys[0]
require(k.get('kty')=='RSA' and k.get('kid')=='key-1' and k.get('use')=='sig' and k.get('alg')=='RS256')
require(integer(k['n'])==int(p['n']) and integer(k['e'])==int(p['e']))
print('{"original_public_signing_key_in_live_jwks":true}')
"""
def check_jwks(session, admission, public_numbers):
trust=runtime_file(session,admission['trust_path']).decode()
request={'issuer':admission['issuer'],'trust':trust,'n':str(public_numbers.n),'e':str(public_numbers.e)}
result=json.loads(command(['docker','exec','-i',session['worker'],'python3','-c',JWKS_PROBE],input=json.dumps(request).encode()))
if result != {'original_public_signing_key_in_live_jwks': True}:raise Refused()
RUNTIME_FILES = r"""
import base64,json,sys
from pathlib import Path
p=json.load(sys.stdin)
roots=('/etc/keycape/','/var/lib/keycape-policy/','/etc/keycape-factor/','/recovery-tls/')
result={}
paths=[]
if p.get('tree'):
paths=list(Path('/var/lib/keycape-policy').rglob('*'))
else:paths=[Path(x) for x in p['paths']]
for f in paths:
if f.is_dir():continue
if not str(f).startswith(roots) or not f.is_file() or any(x.is_symlink() for x in (f,*f.parents)) or f.stat().st_size>8*1024*1024:raise ValueError()
result[str(f)]=base64.b64encode(f.read_bytes()).decode()
if len(result)>4096:raise ValueError()
print(json.dumps(result))
"""
def runtime_files(session, paths=(), tree=False):
raw=command(['docker','exec','-i',session['worker'],'python3','-c',RUNTIME_FILES],
input=json.dumps({'paths':list(paths),'tree':tree}).encode(),max_output=48*1024*1024)
return {k:base64.b64decode(v,validate=True) for k,v in json.loads(raw).items()}
def runtime_file(session,path):
return runtime_files(session,[path])[path]
def run_verifier(session, admission, payload):
issuer = session['issuer']
if {v['target'] for v in session.get('volumes',{}).values()}!={'/etc/keycape','/var/lib/keycape-policy','/etc/keycape-factor','/recovery-tls'}:raise Refused()
from urllib.parse import urlsplit
import ipaddress
address = urlsplit(admission['issuer'])
if (address.scheme != 'https' or not address.hostname or address.username or address.password
or address.query or address.fragment):
raise Refused()
if hashlib.sha256(container_file(issuer, '/keycape')).hexdigest() != admission['verifier_sha256']:
raise Refused()
if hashlib.sha256(runtime_file(session, admission['trust_path'])).hexdigest() != admission['trust_sha256']:
raise Refused()
# Capture private environment only in owner process memory, never receipt.
environment = json.loads(command(['docker','container','inspect',issuer,'--format','{{json .Config.Env}}']))
env = dict(item.split('=',1) for item in environment)
if len(env)!=len(environment):raise Refused()
if set(env)-set(payload['env'])-{'PATH','HOME','HOSTNAME','SSL_CERT_FILE'}:raise Refused()
if env.get('SSL_CERT_FILE') != admission['trust_path'] or any(k.lower().endswith('_proxy') and v for k,v in env.items()):
raise Refused()
if {k:v for k,v in env.items() if k.startswith('KEYCAPE_')}!={k:v for k,v in payload['env'].items() if k.startswith('KEYCAPE_')}:raise Refused()
for name,value in payload['env'].items():
if env.get(name) != value:
raise Refused()
files = {k:base64.b64decode(v,validate=True) for k,v in payload['files'].items()}
def equal():
if runtime_files(session,files)!=files:raise Refused()
durable=runtime_files(session,tree=True)
if durable!={k:v for k,v in files.items() if k.startswith('/var/lib/keycape-policy/')}:
raise Refused()
equal()
# Reading YAML is data parsing only; no archived executable is evaluated.
import yaml
cfg = yaml.safe_load(files['/etc/keycape/config.yaml'])
if cfg['issuer'] != admission['issuer']:
raise Refused()
consumer = admission['consumer']
client = next(x for x in cfg['clients'] if x['clientId']==consumer['client_id'])
if (client['secretRef']!='env:'+consumer['secret_env'] or client['clientType']!='confidential'
or 'client_credentials' not in client['grantTypes'] or not env.get(consumer['secret_env'])):
raise Refused()
from cryptography.hazmat.primitives import serialization
key=serialization.load_pem_private_key(files['/etc/keycape/key.pem'],password=None)
check_jwks(session,admission,key.public_key().public_numbers())
wrong = secrets.token_urlsafe(48)
if wrong == env[consumer['secret_env']]:raise Refused()
args = ['docker','exec','--env','NK49_UNRELATED_NEGATIVE_SECRET='+wrong,issuer,'/keycape','verify-client',
'-issuer',admission['issuer'],'-previous-secret-env','NK49_UNRELATED_NEGATIVE_SECRET']
flags = {'client_id':'client-id','secret_env':'secret-env','audience':'audience','scope':'scope',
'subject':'expect-subject','tenant':'expect-tenant','roles':'expect-roles','deny_scope':'deny-scope'}
for name,flag in flags.items():args+=['-'+flag,consumer[name]]
# Native command reports on stderr. Do not return or print its raw response.
result = process(args,stdout=subprocess.PIPE,stderr=subprocess.PIPE,timeout=150)
raw = (result.stdout+result.stderr).decode()
required = ['discovery and endpoint origin','exchange and JWKS signature for granted scopes','access-token claim bindings',
'principal_type is service','sub matches the registration','tenant matches the registration','roles present',
'granted scope carries no excess grant','predecessor secret refused']
required += ['excess scope refused: '+scope for scope in consumer['deny_scope'].split()]
if result.returncode or any('PASS '+name+'\n' not in raw for name in required) or 'FAIL ' in raw:
raise Refused()
equal()
from cryptography.hazmat.primitives import serialization
key=serialization.load_pem_private_key(files['/etc/keycape/key.pem'],password=None)
check_jwks(session,admission,key.public_key().public_numbers())
public=key.public_key().public_bytes(serialization.Encoding.DER,serialization.PublicFormat.SubjectPublicKeyInfo)
return {'native_service_registration_verified':True,'wrong_unrelated_secret_explicitly_refused':True,
'denied_scopes_refused':True,'original_config_registry_policy_receipts_equal':True,
'public_signing_spki_sha256':hashlib.sha256(public).hexdigest(),
'original_https_issuer_and_trust_preserved':True,'original_public_signing_key_in_live_jwks':True}
def cleanup(session, receipt=None):
"""Attempt every owned cleanup even when one obligation fails.
Preserve exact safe container names and outcomes so the owner can recover an
uncertain/partial cleanup. An inspect/daemon failure never means absence.
"""
obligations = []
failed = False
for name in reversed(session['containers']):
row = {'container': name, 'status': 'unverified'}
obligations.append(row)
try:
present=command(['docker','container','ls','--all','--filter','name=^/'+name+'$','--format','{{.Names}}']).decode().splitlines()
if not present:
row['status']='absence_verified'
continue
label = command(['docker', 'container', 'inspect', name, '--format',
'{{index .Config.Labels "' + LABEL + '"}}']).decode().strip()
if label != session['run_id']:
row['status'] = 'ownership_mismatch_not_removed'
failed = True
continue
# A failed remove is followed by a successful explicit inventory;
# interrupted remove may actually have completed.
try:
command(['docker', 'rm', '--force', '--volumes', name])
except Exception:
pass
remaining = command(['docker','container','ls','--all','--filter',
'name=^/' + name + '$','--format','{{.Names}}']).decode().splitlines()
if remaining:
row['status'] = 'still_present'
failed = True
else:
row['status'] = 'absence_verified'
except Exception:
failed = True
if receipt is not None:
receipt['container_cleanup_obligations'] = obligations
if failed:
raise Refused()
def native_checks(root, manifest, admission, receipt, cleanup_session=True):
"""Owner wrapper provides a reviewed session after existing restore pipeline.
Admission must bind ciphertext, canonical private manifest digest, executable
and trust bytes, exact image/target list, original issuer and selected consumer.
None of these admission fields grant authority or carry a client secret.
"""
session = admission['session']
# Validate ownership/containment before permitting either exec or cleanup.
inspect_session(session)
receipt.update(status='failed', source_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
ciphertext_sha256=admission['ciphertext_sha256'], production_contacted=False,
human_mfa_proven=False, downstream_consumer_reconnect_proven=False,
container_cleanup_verified=False)
try:
for field in ('ciphertext_sha256', 'manifest_sha256', 'verifier_sha256', 'trust_sha256'):
if not re.fullmatch('[a-f0-9]{64}', admission[field]):
raise Refused()
for field in ('client_id','secret_env','audience','scope','subject','tenant','roles','deny_scope'):
if not isinstance(admission['consumer'].get(field), str) or not admission['consumer'][field].strip():
raise Refused()
payload = private_payload(root, manifest, admission['manifest_sha256'])
result = run_verifier(session, admission, payload)
required = {'native_service_registration_verified','wrong_unrelated_secret_explicitly_refused',
'denied_scopes_refused','original_config_registry_policy_receipts_equal',
'original_https_issuer_and_trust_preserved','original_public_signing_key_in_live_jwks'}
if any(result.get(k) is not True for k in required) or not re.fullmatch('[a-f0-9]{64}', result.get('public_signing_spki_sha256','')):
raise Refused()
# Closed receipt allowlist: never copy arbitrary worker output.
receipt.update({k: True for k in required})
receipt['public_signing_spki_sha256'] = result['public_signing_spki_sha256']
receipt['status'] = 'isolated_keycape_native_registration_checked'
finally:
try:
if cleanup_session:
cleanup(session, receipt)
receipt['container_cleanup_verified'] = True
except Exception:
receipt['status'] = 'failed'
raise

View file

@ -0,0 +1,343 @@
#!/usr/bin/env python3
"""Owner-run native LLDAP restore from the verified identity bundle.
Private inputs remain in the owner process/pipe. Decrypted staging uses Linux
tmpfs. Containers have no network, ports, host mounts or production API access.
"""
import argparse
import base64
from datetime import datetime, timezone
import hashlib
import importlib.util
import io
import json
import os
from pathlib import Path, PurePosixPath
import re
import signal
import sqlite3
import subprocess
import tarfile
import tempfile
import time
import tomllib
import uuid
HERE = Path(__file__).resolve().parent
def load(name):
spec = importlib.util.spec_from_file_location(name, HERE / (name + '.py'))
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
verify = load('verify-identity')
offline = load('retrieve-identity-offline')
bundle = verify.bundle
IMAGE = 'docker.io/lldap/lldap@sha256:9e605a66c02514bfcffd1b67cafb1e98d50992216bb2871d7ae44622047dd09d'
CLIENT = 'ghcr.io/gpappsoft/privacyidea-docker@sha256:af7841adad262f129e0c1d4f553af13f21cb2f4dc713533f316cfe43ed0b4473'
ENGINE_ID = '74f93783-6cef-43a8-a3c4-f62586cd3bc1'
ENGINE_NAME = 'bnt-lap001'
def command(args, **kwargs):
result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
timeout=kwargs.pop('timeout', 90), **kwargs)
if result.returncode:
raise bundle.Refused()
return result.stdout
def preflight():
contexts = json.loads(command(['docker', 'context', 'inspect']))
endpoints = [contexts[0]['Endpoints']['docker']['Host']]
if os.environ.get('DOCKER_HOST'):
endpoints.append(os.environ['DOCKER_HOST'])
if not all(url.startswith('unix://') for url in endpoints):
raise bundle.Refused()
facts = json.loads(command(['docker', 'info', '--format',
'{"ID":{{json .ID}},"Name":{{json .Name}},"OSType":{{json .OSType}}}']))
if facts['ID'] != ENGINE_ID or facts['Name'] != ENGINE_NAME or facts['OSType'] != 'linux':
raise bundle.Refused()
# /dev/shm must actually be tmpfs, not a similarly named disk directory.
mounts = Path('/proc/mounts').read_text().splitlines()
if not any(line.split()[1:3] == ['/dev/shm', 'tmpfs'] for line in mounts):
raise bundle.Refused()
images = []
for image in (IMAGE, CLIENT):
image_id = command(['docker', 'image', 'inspect', '--format', '{{.Id}}', image]).decode().strip()
images.append({'reference': image, 'image_id': image_id})
return {'engine_id': facts['ID'], 'engine_name': facts['Name'], 'images': images,
'production_contacted': False, 'network_mode': 'none', 'host_ports_published': False}
def snapshot(path):
# Include all fields of durable identity tables, including stored password
# hashes. Only counts and the equality boolean leave this process.
result = {}
with sqlite3.connect(path.as_uri() + '?mode=ro', uri=True) as db:
if db.execute('PRAGMA integrity_check').fetchall() != [('ok',)]:
raise bundle.Refused()
tables = {row[0] for row in db.execute("SELECT name FROM sqlite_master WHERE type='table'")}
for name in ('users', 'groups', 'memberships', 'user_attributes', 'group_attributes',
'user_attribute_values', 'group_attribute_values',
'user_attribute_schema', 'group_attribute_schema'):
if name in tables:
rows = db.execute('SELECT * FROM "' + name + '"').fetchall()
encoded = sorted(repr(row) for row in rows)
result[name] = {'count': len(rows), 'digest': hashlib.sha256('\n'.join(encoded).encode()).digest()}
if not {'users', 'groups', 'memberships'} <= result.keys():
raise bundle.Refused()
return result
def configuration(root):
data = root / 'data/lldap'
existing = data / 'lldap_config.toml'
config = tomllib.loads(existing.read_text()) if existing.is_file() else {}
deployment = bundle.resource(root, 'sso', 'deployment', 'lldap')
main = next(c for c in deployment['spec']['template']['spec']['containers'] if c['name'] == 'lldap')
env = {e['name']: e['value'] for e in main.get('env', []) if 'value' in e}
secret = bundle.resource(root, 'sso', 'secret', 'lldap-secrets')['data']
jwt = base64.b64decode(secret['LLDAP_JWT_SECRET'], validate=True)
password = base64.b64decode(secret['LLDAP_LDAP_USER_PASS'], validate=True)
if not jwt or not password or b'\n' in jwt or b'\n' in password:
raise bundle.Refused()
base = env.get('LLDAP_LDAP_BASE_DN', config.get('ldap_base_dn'))
user = env.get('LLDAP_LDAP_USER_DN', config.get('ldap_user_dn', 'admin'))
if not isinstance(base, str) or not base or not isinstance(user, str):
raise bundle.Refused()
chosen = {'database_url': 'sqlite:///data/users.db?mode=rwc',
'ldap_base_dn': base, 'ldap_user_dn': user,
'ldap_host': '127.0.0.1', 'http_host': '127.0.0.1',
'ldap_port': 3890, 'http_port': 17170,
'force_ldap_user_pass_reset': False, 'force_update_private_key': False}
seed = env.get('LLDAP_KEY_SEED', config.get('key_seed'))
if seed:
if not isinstance(seed, str):
raise bundle.Refused()
chosen['key_seed'] = seed
else:
value = env.get('LLDAP_KEY_FILE', config.get('key_file', '/data/private_key'))
key = PurePosixPath(value)
if not key.is_absolute():
key = PurePosixPath('/data') / key
if key.parent != PurePosixPath('/data') or not (data / key.name).is_file():
raise bundle.Refused()
chosen['key_file'] = str(key)
# JSON strings are valid TOML basic strings for these service inputs.
raw = '\n'.join(k + ' = ' + json.dumps(v) for k, v in chosen.items())
raw += '\n[smtp_options]\nenable_password_reset = false\n[ldaps_options]\nenabled = false\n'
return raw.encode(), jwt, password, base
def archive_files(files):
out = io.BytesIO()
with tarfile.open(fileobj=out, mode='w') as archive:
for name, value in files.items():
if PurePosixPath(name).is_absolute() or '..' in PurePosixPath(name).parts:
raise bundle.Refused()
entry = tarfile.TarInfo(name)
entry.size = len(value)
entry.mode = 0o600
entry.uid = entry.gid = 1000
archive.addfile(entry, io.BytesIO(value))
return out.getvalue()
PROBE = r'''
import contextlib,io,json,logging,socket,sys,time
logging.disable(logging.CRITICAL)
result={'status':'failed'}
stage='network_isolation'
try:
with contextlib.redirect_stdout(io.StringIO()),contextlib.redirect_stderr(io.StringIO()):
from pathlib import Path
assert {p.name for p in Path('/sys/class/net').iterdir()}=={'lo'}
assert len(Path('/proc/net/route').read_text().splitlines())==1
stage='native_client_import'
from ldap3 import Server,Connection,NONE
from ldap3.utils.dn import escape_rdn
values=json.load(sys.stdin)
stage='directory_startup'
for _ in range(60):
try:
with socket.create_connection(('127.0.0.1',3890),timeout=1):pass
break
except OSError:time.sleep(.25)
else:raise RuntimeError()
dn='uid='+escape_rdn(values['user'])+',ou=people,'+values['base']
server=Server('127.0.0.1',port=3890,get_info=NONE,connect_timeout=3)
stage='existing_user_bind'
connection=Connection(server,user=dn,password=values['password'],auto_bind=True,raise_exceptions=True)
assert connection.bound
connection.unbind()
stage='wrong_password_bind'
wrong=Connection(server,user=dn,password=values['wrong_password'],auto_bind=False,raise_exceptions=False)
refused=not wrong.bind() and wrong.result.get('result')==49
wrong.unbind()
assert refused
result={'status':'verified','existing_user_native_ldap_bind':True,'wrong_password_refused':True,
'only_loopback_interface_verified':True,'external_routes_absent':True}
except Exception as error:result={'status':'failed','failed_stage':stage,'failure_type':type(error).__name__}
print(json.dumps(result))
'''
def restore_lldap(root, user, password, receipt):
before = snapshot(root / 'data/lldap/users.db')
config, jwt, admin_password, base = configuration(root)
name = 'nk49-lldap-' + uuid.uuid4().hex
client_name = name + '-client'
try:
command(['docker', 'run', '--detach', '--pull=never', '--name', name,
'--network', 'none', '--read-only', '--cap-drop', 'ALL',
'--security-opt', 'no-new-privileges', '--user', '1000:1000',
'--memory', '256m', '--cpus', '1', '--pids-limit', '128',
'--tmpfs', '/data:rw,nosuid,nodev,noexec,size=268435456,uid=1000,gid=1000,mode=0700',
'--tmpfs', '/run/recovery:rw,nosuid,nodev,noexec,size=1048576,uid=1000,gid=1000,mode=0700',
'--tmpfs', '/tmp:rw,nosuid,nodev,noexec,size=16777216,uid=1000,gid=1000',
'--env', 'LLDAP_JWT_SECRET_FILE=/run/recovery/jwt',
'--env', 'LLDAP_LDAP_USER_PASS_FILE=/run/recovery/admin-password',
'--entrypoint', '/bin/sh', IMAGE, '-c', 'sleep 1800'])
guard = json.loads(command(['docker', 'container', 'inspect', name, '--format',
'{"host":{{json .HostConfig}},"mounts":{{json .Mounts}},"user":{{json .Config.User}}}']))
host = guard['host']
if (host['NetworkMode'] != 'none' or not host['ReadonlyRootfs'] or host['Privileged']
or host.get('Binds') or host.get('PortBindings') or 'ALL' not in host.get('CapDrop', [])
or 'no-new-privileges' not in host.get('SecurityOpt', [])
or guard['user'] != '1000:1000'
or any(mount['Type'] != 'tmpfs' for mount in guard['mounts'])):
raise bundle.Refused()
receipt['container_isolation_verified_before_private_import'] = True
payload = {p.relative_to(root / 'data/lldap').as_posix(): p.read_bytes()
for p in (root / 'data/lldap').rglob('*') if p.is_file()}
command(['docker', 'exec', '-i', name, 'tar', '-xf', '-', '-C', '/data'], input=archive_files(payload))
command(['docker', 'exec', '-i', name, 'tar', '-xf', '-', '-C', '/run/recovery'],
input=archive_files({'lldap.toml': config, 'jwt': jwt, 'admin-password': admin_password}))
# No credential values in Docker configuration, environment or argv.
command(['docker', 'exec', '--detach', name, '/bin/sh', '-c',
'echo $$ > /run/recovery/server.pid; exec /app/lldap run --config-file /run/recovery/lldap.toml > /dev/null 2>&1'])
raw = command(['docker', 'run', '--interactive', '--rm', '--name', client_name, '--pull=never', '--network', 'container:' + name,
'--read-only', '--cap-drop', 'ALL', '--security-opt', 'no-new-privileges',
'--memory', '256m', '--cpus', '1', '--pids-limit', '64',
'--entrypoint', 'python3', CLIENT, '-c', PROBE],
input=json.dumps({'user': user, 'password': password, 'base': base,
'wrong_password': 'nk49-wrong-' + uuid.uuid4().hex}).encode())
result = json.loads(raw)
receipt.update(result)
if result.get('status') != 'verified':
raise bundle.Refused()
command(['docker', 'exec', name, '/bin/sh', '-c', 'kill -TERM "$(cat /run/recovery/server.pid)"'])
for _ in range(60):
running = subprocess.run(['docker', 'exec', name, '/bin/sh', '-c',
'kill -0 "$(cat /run/recovery/server.pid)"'],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=10)
if running.returncode:
break
time.sleep(.1)
else:
raise bundle.Refused()
raw = command(['docker', 'exec', name, '/bin/sh', '-c',
'test ! -s /data/users.db-wal && cat /data/users.db'])
after_path = root / 'post-restore-users.db'
after_path.write_bytes(raw)
if snapshot(after_path) != before:
raise bundle.Refused()
receipt.update(directory_identity_rows_preserved=True,
directory_comparison_scope=sorted(before),
directory_table_counts={k: v['count'] for k, v in before.items()})
finally:
# A failed create can still have created a container (e.g. lost reply).
for owned in (client_name, name):
found = subprocess.run(['docker', 'container', 'inspect', owned], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, timeout=15)
if found.returncode == 0:
command(['docker', 'rm', '--force', '--volumes', owned])
absent = subprocess.run(['docker', 'container', 'inspect', owned], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, timeout=15)
command(['docker', 'info', '--format', '{{.ID}}'])
if absent.returncode == 0:
raise bundle.Refused()
receipt['container_cleanup_verified'] = True
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--transfer-receipt', required=True)
parser.add_argument('--receipt', required=True)
parser.add_argument('--user', required=True)
args = parser.parse_args()
os.umask(0o077)
fd = os.open(args.receipt, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
result = {'schema': 'netkingdom.isolated-lldap-recovery.v1', 'status': 'failed',
'production_contacted': False, 'secret_values_exported': False,
'container_cleanup_verified': False, 'temporary_plaintext_cleanup_verified': False,
'source_sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest()}
keyfd = None
work = None
stage = 'target_preflight'
try:
result['target'] = preflight()
stage = 'input_validation'
if not re.fullmatch(r'[A-Za-z0-9_.-]{1,128}', args.user):
raise bundle.Refused()
transfer = json.loads(Path(args.transfer_receipt).read_text())
offline.validate(transfer)
if transfer.get('independent_owner_retrieval_verified') is not True:
raise bundle.Refused()
path = Path(transfer['fetched_path'])
bundle.private(path, max_bytes=bundle.LIMIT)
if path.stat().st_size != transfer['ciphertext_bytes'] or offline.offsite.digest(path) != transfer['ciphertext_sha256']:
raise bundle.Refused()
keyfd = verify.custody.private_key_fd()
derived = command(['age-keygen', '-y', f'/proc/self/fd/{keyfd}'], pass_fds=(keyfd,)).strip()
if derived != bundle.profile()['public_recipient'].encode():
raise bundle.Refused()
stage = 'private_existing_password_prompt'
password = offline.private_prompt('Existing account password for the isolated LLDAP check: ')
with tempfile.TemporaryDirectory(prefix='nk49-service-recovery-', dir='/dev/shm') as temporary:
work = Path(temporary)
archive = work / 'archive.tar'
stage = 'archive_decryption'
with archive.open('xb') as output:
proc = subprocess.run(['age', '-d', '-i', f'/proc/self/fd/{keyfd}', str(path)],
stdout=output, stderr=subprocess.DEVNULL, pass_fds=(keyfd,), timeout=180)
if proc.returncode or archive.stat().st_size > bundle.LIMIT + 4 * 1024 * 1024:
raise bundle.Refused()
restored = work / 'restored'
restored.mkdir(mode=0o700)
stage = 'archive_validation'
manifest, _ = bundle.restore(archive, restored)
result.update(ciphertext_sha256=transfer['ciphertext_sha256'],
manifest_file_count=len(manifest['files']), all_manifest_hashes_verified=True)
stage = 'native_directory_check'
restore_lldap(restored, args.user, password, result)
password = None
if work.exists():
raise bundle.Refused()
result.update(status='isolated_lldap_verified', temporary_plaintext_cleanup_verified=True)
except (Exception, KeyboardInterrupt):
result.update(status='failed', error='isolated_lldap_check_failed_private_diagnostics_suppressed')
result.setdefault('failed_stage', stage)
finally:
if keyfd is not None:
os.close(keyfd)
if work is not None and not work.exists():
result['temporary_plaintext_cleanup_verified'] = True
result['finished_at'] = datetime.now(timezone.utc).isoformat()
with os.fdopen(fd, 'w') as output:
json.dump(result, output, indent=2)
output.write('\n')
success = result['status'] == 'isolated_lldap_verified'
print('OK: restored LLDAP existing-user bind, wrong-password refusal, directory preservation and cleanup passed.'
if success else 'ERROR: isolated LLDAP recovery refused; review the safe receipt. No production change.')
return 0 if success else 1
if __name__ == '__main__':
for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP):
signal.signal(sig, offline.interrupted)
raise SystemExit(main())

View file

@ -0,0 +1,164 @@
#!/usr/bin/env python3
"""Download one reviewed ciphertext using privately entered provider access.
No OpenBao, SSO, Kubernetes, credential discovery, provider writes or age key.
Run by the operator in their own terminal, never through a recorded agent PTY.
"""
import argparse
import base64
from datetime import datetime, timezone
import getpass
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import re
import shutil
import signal
import sys
import tempfile
import urllib.request
import urllib.error
import warnings
HERE = Path(__file__).resolve().parent
spec = importlib.util.spec_from_file_location('offsite', HERE / 'identity-offsite.py')
offsite = importlib.util.module_from_spec(spec)
spec.loader.exec_module(offsite)
ACCOUNT_BASE = offsite.HOST + '/remote.php/dav/files/Backup/railiance-backups/net-kingdom-identity/'
def private_prompt(label):
# getpass opens the terminal with os.open, which does not require seeking.
# Reject redirected input and turn its echoing fallback warning into an
# exception before it can read a credential without echo suppression.
if not sys.stdin.isatty() or not sys.stderr.isatty():
raise offsite.Refused()
with warnings.catch_warnings():
warnings.simplefilter('error', getpass.GetPassWarning)
return getpass.getpass(label)
def failure_metadata(error):
# No exception messages, response bodies, URLs or headers enter receipts.
if isinstance(error, urllib.error.HTTPError):
return {'failure_kind': 'provider_http_error', 'http_status': int(error.code)}
if isinstance(error, urllib.error.URLError):
return {'failure_kind': 'provider_transport_error'}
if isinstance(error, getpass.GetPassWarning):
return {'failure_kind': 'terminal_echo_suppression_unavailable'}
if isinstance(error, offsite.Refused):
return {'failure_kind': 'scope_or_integrity_refusal'}
return {'failure_kind': 'local_operation_failed'}
def validate(record):
if (record.get('schema') != 'netkingdom.identity-offsite.v1'
or record.get('status') != 'readback_verified'
or record.get('provider') != 'Storage Share'
or record.get('folder') != 'Backup/railiance-backups/net-kingdom-identity'
or not offsite.NAME.fullmatch(record.get('artifact', ''))
or not re.fullmatch('[a-f0-9]{64}', record.get('ciphertext_sha256', ''))
or type(record.get('ciphertext_bytes')) is not int
or not 0 < record['ciphertext_bytes'] <= offsite.LIMIT
or not re.fullmatch(r'"[^"\x00-\x20]{1,180}"', record.get('etag', ''))):
raise offsite.Refused()
def retrieve(record, secret, mode, client=None):
validate(record)
if mode not in {'account', 'folder-share'} or not secret or any(c in secret for c in '\r\n\x00'):
raise offsite.Refused()
auth = offsite.headers(secret) if mode == 'folder-share' else {
'Authorization': 'Basic ' + base64.b64encode(('Backup:' + secret).encode()).decode(),
'Accept-Encoding': 'identity'}
url = (offsite.BASE if mode == 'folder-share' else ACCOUNT_BASE) + record['artifact']
client = client or offsite.opener()
directory = None
complete = False
try:
with client.open(urllib.request.Request(url, method='HEAD', headers=auth), timeout=30) as response:
if offsite.etag(response, url, record['ciphertext_bytes']) != record['etag']:
raise offsite.Refused()
directory = Path(tempfile.mkdtemp(prefix='nk49-identity-fetched-', dir='/tmp'))
target = directory / record['artifact']
auth['If-Match'] = record['etag']
count = 0
digest = hashlib.sha256()
with client.open(urllib.request.Request(url, headers=auth), timeout=180) as response:
if offsite.etag(response, url, record['ciphertext_bytes']) != record['etag']:
raise offsite.Refused()
fd = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
with os.fdopen(fd, 'wb') as output:
while block := response.read(1024 * 1024):
count += len(block)
if count > record['ciphertext_bytes']:
raise offsite.Refused()
digest.update(block)
output.write(block)
output.flush()
os.fsync(output.fileno())
if count != record['ciphertext_bytes'] or digest.hexdigest() != record['ciphertext_sha256']:
raise offsite.Refused()
with client.open(urllib.request.Request(url, method='HEAD', headers=auth), timeout=30) as response:
if offsite.etag(response, url, record['ciphertext_bytes']) != record['etag']:
raise offsite.Refused()
complete = True
# Closed allowlist: never copy arbitrary source-receipt fields to output.
result = {k: record[k] for k in ('schema', 'provider', 'folder', 'artifact',
'ciphertext_bytes', 'ciphertext_sha256', 'etag')}
result.update(status='readback_verified', fetched_path=str(target),
independent_owner_retrieval_verified=True,
credential_source='independently_held_provider_access',
provider_auth_mode=mode, openbao_contacted=False, sso_contacted=False,
provider_writes_performed=False, age_private_key_used=False,
secret_values_exported=False, object_unchanged_during_transfer=True,
downloaded_at=datetime.now(timezone.utc).isoformat())
return result
finally:
if directory is not None and not complete:
shutil.rmtree(directory)
def interrupted(signum, frame):
raise InterruptedError()
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--source-receipt', required=True)
parser.add_argument('--receipt', required=True)
parser.add_argument('--auth', choices=('account', 'folder-share'), default='account')
args = parser.parse_args()
os.umask(0o077)
fd = os.open(args.receipt, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
result = {'status': 'failed', 'error': 'offline_retrieval_failed_private_diagnostics_suppressed'}
stage = 'source_receipt_validation'
try:
record = json.loads(Path(args.source_receipt).read_text())
validate(record)
# getpass must never fall back to echoed stdin (e.g. a captured agent tool).
stage = 'private_provider_prompt'
secret = private_prompt('Storage Share Backup app password or exact folder-share token: ')
try:
stage = 'provider_download'
result = retrieve(record, secret, args.auth)
finally:
secret = None
except (Exception, KeyboardInterrupt) as error:
result.update(failed_stage=stage, **failure_metadata(error))
finally:
with os.fdopen(fd, 'w') as output:
json.dump(result, output, indent=2)
output.write('\n')
ok = result['status'] == 'readback_verified'
print('OK: ciphertext retrieved without OpenBao or SSO; ETag, size and SHA256 verified.' if ok
else 'ERROR: retrieval refused; private diagnostics suppressed. Use a new receipt path to retry.')
return 0 if ok else 1
if __name__ == '__main__':
for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP):
signal.signal(sig, interrupted)
sys.exit(main())

View file

@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""Silent attended owner retrieval of an existing identity ciphertext.
Uses Backup-account recovery custody only in memory; no private age key, upload,
share, grant, deletion or production restore operation.
"""
import argparse
import base64
from datetime import datetime,timezone
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import re
import tempfile
import urllib.request
HERE=Path(__file__).resolve().parent
def load(name):
spec=importlib.util.spec_from_file_location(name,HERE/(name+'.py'))
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m);return m
inventory=load('inventory-storage-share');offsite=load('identity-offsite')
def retrieve(record,out):
if (record.get('schema')!='netkingdom.identity-offsite.v1' or record.get('status')!='readback_verified'
or not offsite.NAME.fullmatch(record.get('artifact','')) or not re.fullmatch('[a-f0-9]{64}',record.get('ciphertext_sha256',''))
or not 0<record.get('ciphertext_bytes',0)<=offsite.LIMIT):raise offsite.Refused()
identity,credentials=inventory.caller_credentials()
auth=base64.b64encode((credentials['BACKUP_USERNAME']+':'+credentials['BACKUP_PASSWORD']).encode()).decode()
url=offsite.HOST+'/remote.php/dav/files/Backup/railiance-backups/net-kingdom-identity/'+record['artifact']
h={'Authorization':'Basic '+auth,'Accept-Encoding':'identity'}
client=offsite.opener()
with client.open(urllib.request.Request(url,method='HEAD',headers=h),timeout=30) as response:
version=offsite.etag(response,url,record['ciphertext_bytes'])
if version!=record['etag']:raise offsite.Refused()
h['If-Match']=version
directory=Path(tempfile.mkdtemp(prefix='nk49-identity-fetched-',dir='/tmp'))
target=directory/record['artifact'];count=0;digest=hashlib.sha256()
with client.open(urllib.request.Request(url,headers=h),timeout=180) as response:
if offsite.etag(response,url,record['ciphertext_bytes'])!=version:raise offsite.Refused()
with target.open('xb') as output:
while block:=response.read(1024*1024):
count+=len(block)
if count>record['ciphertext_bytes']:raise offsite.Refused()
digest.update(block);output.write(block)
if count!=record['ciphertext_bytes'] or digest.hexdigest()!=record['ciphertext_sha256']:raise offsite.Refused()
with client.open(urllib.request.Request(url,method='HEAD',headers=h),timeout=30) as response:
if offsite.etag(response,url,record['ciphertext_bytes'])!=version:raise offsite.Refused()
out.update(record);out.update(status='readback_verified',fetched_path=str(target),
independent_owner_retrieval_verified=True,caller_entity_id=identity.get('entity_id'),
age_private_key_used=False,secret_values_exported=False,downloaded_at=datetime.now(timezone.utc).isoformat())
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--source-receipt',required=True);p.add_argument('--receipt',required=True)
a=p.parse_args();os.umask(0o077);fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
r={'status':'failed','source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest()}
try:retrieve(json.loads(Path(a.source_receipt).read_text()),r)
except Exception:r.update(status='failed',error='contained_owner_retrieval_failed')
finally:
with os.fdopen(fd,'w') as out:json.dump(r,out,indent=2);out.write('\n')
return int(r['status']=='failed')
if __name__=='__main__':raise SystemExit(main())

View file

@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Candidate owner launcher. Review and native fixture proof required before use.
set +x
set -euo pipefail
ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
cd -- "$ROOT"
umask 077
[[ $# == 6 ]] || { printf '%s\n' 'Usage: run-isolated-keycape.sh VERIFIED_TRANSFER_RECEIPT ADMISSION ADMISSION_SHA256 TLS_CERT TLS_KEY TRUST'; exit 1; }
[[ "$3" =~ ^[a-f0-9]{64}$ ]] || { printf '%s\n' 'ERROR: exact reviewed admission digest required.'; exit 1; }
for index in 1 2 4 5 6; do
[[ -f "${!index}" && ! -L "${!index}" ]] || { printf '%s\n' 'ERROR: provide regular owner-controlled input files.'; exit 1; }
done
[[ -t 0 && -t 1 && -t 2 ]] || { printf '%s\n' 'ERROR: run in your own interactive terminal.'; exit 1; }
for tool in python3 age age-keygen docker; do
command -v "$tool" >/dev/null || { printf 'ERROR: required tool unavailable: %s\n' "$tool"; exit 1; }
done
python3 - <<'PY'
import importlib.util
from pathlib import Path
p=Path('tools/bootstrap-recovery/isolated-keycape-session.py')
spec=importlib.util.spec_from_file_location('session',p)
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
try:
m.local.preflight()
m.run(['docker','image','inspect',m.adapter.IMAGE])
except Exception:
raise SystemExit('ERROR: exact local Docker/images/tmpfs preflight failed; no key requested.')
print('OK: fixed local target and cached pinned images available.')
PY
NK49_KC_RUN="$(mktemp -d /tmp/nk49-isolated-keycape-XXXXXXXX)"
printf 'Safe evidence directory: %s\n' "$NK49_KC_RUN"
printf '%s\n' 'Owner-reviewed candidate: isolated original issuer/trust and current registered service-client checks only.'
printf '%s\n' 'No browser, downstream consumer or retained-factor MFA acceptance is claimed.'
printf '%s\n' 'Enter only the independently held backup age key at the hidden prompt.'
bash sso-mfa/bootstrap/sops-custody-unlock.sh \
--expected-recipient age1zvryunvjhvpkmasskauga2heeg0ztnte9ymgppvjge36ekumk50syr3tsz \
--prompt -- python3 tools/bootstrap-recovery/isolated-keycape-session.py \
--transfer-receipt "$1" --admission "$2" --admission-sha256 "$3" \
--tls-cert "$4" --tls-key "$5" --trust "$6" --receipt "$NK49_KC_RUN/recovery.json"
printf 'Safe receipt: %s/recovery.json\n' "$NK49_KC_RUN"

View file

@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""Attended independent-key recovery of the new data-bearing off-host bundle."""
import argparse
from datetime import datetime,timezone
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import signal
import subprocess
import tempfile
import uuid
HERE=Path(__file__).resolve().parent
def load(name,path=None):
spec=importlib.util.spec_from_file_location(name,path or HERE/(name+'.py'))
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m);return m
bundle=load('identity-bundle');pg=load('check-privacyidea-database')
custody=load('sops_private',HERE.parents[1]/'sso-mfa/bootstrap/sops-private.py')
def verify(transfer,keyfd,receipt):
if (transfer.get('schema')!='netkingdom.identity-offsite.v1' or transfer.get('status')!='readback_verified'
or transfer.get('independent_owner_retrieval_verified') is not True
or transfer.get('ciphertext_bytes',0)>bundle.LIMIT):raise bundle.Refused()
path=Path(transfer['fetched_path']);bundle.private(path,max_bytes=bundle.LIMIT)
with path.open('rb') as source:digest=hashlib.file_digest(source,'sha256').hexdigest()
if path.stat().st_size!=transfer['ciphertext_bytes'] or digest!=transfer['ciphertext_sha256']:raise bundle.Refused()
derived=pg.run(['age-keygen','-y',f'/proc/self/fd/{keyfd}'],pass_fds=(keyfd,)).strip()
if derived!=bundle.profile()['public_recipient'].encode():raise bundle.Refused()
receipt.update(ciphertext_sha256=digest,artifact=transfer['artifact'],registered_recipient_match=True)
with tempfile.TemporaryDirectory(prefix='nk49-identity-restore-',dir='/tmp') as temporary:
work=Path(temporary);env={'PATH':os.environ.get('PATH',os.defpath),'HOME':temporary,'XDG_CONFIG_HOME':temporary}
wrong=work/'wrong.key';pg.run(['age-keygen','-o',str(wrong)],env=env)
negative=subprocess.run(['age','-d','-i',str(wrong),str(path)],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,env=env,timeout=120)
if negative.returncode==0:raise bundle.Refused()
receipt['wrong_key_refused']=True
plain=work/'archive.tar'
with plain.open('xb') as output:
result=subprocess.run(['age','-d','-i',f'/proc/self/fd/{keyfd}',str(path)],stdout=output,
stderr=subprocess.DEVNULL,env=env,pass_fds=(keyfd,),timeout=180)
if result.returncode or plain.stat().st_size>bundle.LIMIT+4*1024*1024:raise bundle.Refused()
restored=work/'restored';restored.mkdir(mode=0o700)
manifest,checks=bundle.restore(plain,restored)
receipt.update(checks=checks,manifest_file_count=len(manifest['files']),all_manifest_hashes_verified=True,
profile_sha256=manifest['profile_sha256'])
container='nk49-identity-db-'+uuid.uuid4().hex
try:
receipt['database_restore']=pg.restore(restored/'databases/privacyidea.dump',container)
finally:
pg.run(['docker','rm','--force','--volumes',container])
absent=subprocess.run(['docker','container','inspect',container],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,timeout=15)
pg.run(['docker','info','--format','{{.ServerVersion}}'])
if absent.returncode==0:raise bundle.Refused()
receipt['container_cleanup_verified']=True
if work.exists():raise bundle.Refused()
receipt.update(temporary_plaintext_cleanup_verified=True,status='identity_archive_verified',
full_service_authentication_proven=False,existing_token_decryption_proven=False,audit_signature_continuity_proven=False)
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--transfer-receipt',required=True);p.add_argument('--receipt',required=True)
a=p.parse_args();os.umask(0o077)
fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
r={'schema':'netkingdom.identity-recovery.v1','status':'failed','started_at':datetime.now(timezone.utc).isoformat(),
'production_contacted':False,'secret_values_exported':False,'container_cleanup_verified':False,
'temporary_plaintext_cleanup_verified':False,'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest()}
keyfd=None
try:
transfer=json.loads(Path(a.transfer_receipt).read_text());keyfd=custody.private_key_fd()
verify(transfer,keyfd,r)
except Exception:r.update(status='failed',error='identity_recovery_failed_private_diagnostics_suppressed')
finally:
if keyfd is not None:os.close(keyfd)
r['finished_at']=datetime.now(timezone.utc).isoformat()
with os.fdopen(fd,'w') as out:json.dump(r,out,indent=2);out.write('\n')
if r['status']=='identity_archive_verified':
print('OK: off-host identity bundle decrypted; complete manifest, key pairs, SQLite integrity and PostgreSQL restore passed; disposable data removed. Service authentication remains separate acceptance.')
return 0
print('ERROR: identity recovery failed; review safe receipt. Private diagnostics suppressed.');return 1
if __name__=='__main__':
for sig in (signal.SIGHUP,signal.SIGTERM,signal.SIGINT):signal.signal(sig,custody.interrupted)
raise SystemExit(main())

View file

@ -0,0 +1,81 @@
# Revision4 lifecycle rationale and independent review boundary
Revision3's timer could delete data while a living parent or its CLI children
created more resources. Revision4 makes the source invariant explicit: **cleanup
cannot start until the exclusive owner execution has no live creator**.
The main process creates a new SID/PGID before private input, binds its own PID,
UID and kernel start ticks, and refuses inheriting a shell/custody/user group.
The independent reaper has a pidfd for that exact parent. On parent loss or the
fixed 1800-second deadline, it stops that parent and only its verified fresh
session members, using per-process pidfds after SID/PGID/UID/start-time checks.
A changed leader identity or unknown member refuses signaling and cleanup; a
survivor that cannot exit refuses cleanup. Normal host subprocesses inherit this
exclusive group and cannot request a session/group escape. The reaper itself is
in another session and carries no credential environment or custody FD.
The plaintext directory and all future Docker object names, including the public
image probe, are registered and acknowledged before the first decrypted byte is
written. A safe fsynced checkpoint preserves those exact obligations. Cleanup
checks the registered directory inode/device/owner/mode and uses directory FDs;
it never follows symlinks or expands deletion to a replacement path. Docker
failure still allows an independent attempt at the exact plaintext obligation.
All Docker commands, reused preflight, native verifier and reaper are bound to
one explicit `unix:///var/run/docker.sock` endpoint. Conflicting ambient or
per-call hosts/contexts/TLS routing and a differently configured context endpoint
are refused. Actual engine ID/name/Linux checks remain required.
Quiescing a CLI does not cancel an already submitted daemon request. Exact
non-secret sibling markers retain this uncertainty outside removable plaintext.
The outcome remains `bounded_cleanup_uncertain` even if subsequent readback
finds all named objects absent. No T18 acceptance follows such a result.
## Frozen verification
Run the frozen suite without racing further candidate edits:
```sh
python3 -m unittest discover -s /home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-16/identity-restore-adapter-next/revisions/r4/test-root/tests -q
```
61 tests pass. Two spawn actual **disposable synthetic processes**, creating a
new owner session and a child that would later write a marker. Deadline shutdown
and SIGKILL parent-loss both quiesce those processes and prevent late creation.
Other tests check deadline refusal before Docker starts, foreign/reused identity
refusal, cleanup ordering, daemon uncertainty surviving plaintext removal,
checkpoint rewriting, exact staging identity safeguards and fixed daemon routing.
Docker calls/native responses remain mocked; no Docker mutation, real recovery,
custody input or production route was executed. These tests are not native
compatibility or actual owner acceptance.
## Native fixture oracle, after independent review only
Use only a complete synthetic identity archive and synthetic independently
trusted TLS pair with the existing pinned cached images and fixed local engine.
The fixture's original HTTPS issuer/registry/key/policy/receipts must survive
unchanged; never substitute a production archive or actual credentials.
1. Verify local-driver tmpfs options, source/target/RW sharing, pre-import
containment, UID/modes, original image default launch and private cleanup.
2. Demonstrate the **issuer container's** native `verify-client` resolves and
authenticates the original HTTPS hostname with the admitted trust. A helper's
successful DNS/JWKS check alone is insufficient. Missing/wrong issuer hosts
mapping must fail the native command even if the helper can fetch JWKS.
Docker's container-network hosts-file behavior is not assumed proven. If the
fixture fails this check, refuse the candidate and review the supported
hostname-delivery translation; do not change issuer or bypass TLS.
3. Exercise correct existing synthetic client delivery, explicit wrong-secret
denial, denied scope, singleton original `key-1` JWKS, and config/state drift
negatives through the shipped verifier. Compare every durable file/tree.
4. Kill the exact synthetic owner at the recorded decrypt/restore/session stages.
Verify its children are quiescent before exact plaintext/container/volume/
public-probe cleanup. Use the checkpoint's reaper outcome path.
5. In a synthetic harness, tighten the execution deadline and arrange a pending
marker-creation/daemon request. Prove no live creator remains before cleanup;
if a daemon call was in flight, retain uncertainty even after absent readback.
Never declare that uncertain run accepted or silently retry it.
No native fixture is authorized/executed by this document. Root review remains
pending. Actual owner recovery, downstream reconnect, browser continuity and T19
retained-factor MFA remain separate gates.

View file

@ -0,0 +1,168 @@
# T18 KeyCape recovery adapter candidate, revision 4
**Pending root review and native fixture proof. T18 remains open.** Source and
61 passing synthetic tests are prepared in `/tmp/nk49-keycape-adapter-next` and
copied here. No canonical owner file, credential, actual recovery, Docker run,
production route or secret-bearing archive was touched. `candidate.patch` adds
five files to NetKingdom; exact digests/check provenance are in `preparation.json`.
Files:
- `isolated-keycape.py`: archive/source/containment/native acceptance checks.
- `isolated-keycape-session.py`: existing-pipeline decryption, private import,
isolated original-image session, HTTPS transport, checks and cleanup.
- `run-isolated-keycape.sh`: attended owner launcher through the existing hidden
custody helper, with target preflight before the backup-key prompt.
- Two focused test files: check-stage negatives and session/cleanup ordering.
The wrapper uses existing `isolated-lldap`, offline transfer validation,
`verify-identity.custody` and `identity-bundle.restore` APIs. It checks the exact
independently retrieved ciphertext length/digest, custody recipient, complete
archive manifest and original immutable archived KeyCape image. Decryption and
archive restore remain on `/dev/shm`. The private manifest digest comes from
owner admission; it is never emitted publicly. Archive commands/args/envFrom and
non-KeyCape environment overrides are refused. Runtime environment permits only
original KeyCape deliveries, PATH/HOME/HOSTNAME and the exact admitted
SSL_CERT_FILE; duplicate or additional security environment (including
SSL_CERT_DIR/GODEBUG/PYTHONOPTIMIZE/proxy overrides) is refused. Original config/complete static
registry, signing key, policy/receipt tree, factor delivery and original delivered
environment are preserved. Secrets reach Docker by environment **names** only,
with values in the owner subprocess environment, never argv or printed output.
The original KeyCape image is distroless. Docker's documented
[cp corner cases](https://docs.docker.com/reference/cli/docker/container/cp/#corner-cases)
exclude tmpfs and user-created mounts. Consequently the candidate shares exactly
four Docker **local-driver tmpfs** volumes between the existing pinned Python
helper and original KeyCape issuer. Driver, scope, exact options, owner label,
source, target and read/write flags are inspected before private import. There
are no host directory mounts, disk volumes, new image, JWT verifier or execution
service. The helper's private import and comparison occur inside those exact
mounts; daemon `cp` is used only for public `/keycape` executable readback from
the original image root filesystem. This volume extension still needs native
pinned-image fixture verification before approval.
Both containers use the fixed local Docker engine, no network interfaces beyond
loopback, readonly root, nonroot UID, no capabilities/devices/privilege/host
namespaces or unadmitted security options. Peers share the exact network-none
anchor. Only namespace-local unprivileged-port sysctl `0` is admitted so the
existing nonroot Python helper can present original HTTPS on loopback443 and
forward to the original KeyCape process on loopback8080. The issuer uses its exact
original image default `/keycape` launch: Entrypoint/Cmd and actual Path/Args are
inspected; the original file/environment bytes and executable digest are bound.
Production dependency addresses remain unreachable. No readiness, LDAP,
Authelia, MFA, tenant-engine or notification endpoint is imitated. A selected
service registration can be checked without claiming `/readyz` passed.
The owner supplies an existing trusted issuer certificate/private key and trust
bundle through private regular files. No certificate, CA, signing key, client
registration or credential is generated/reissued. Original HTTPS hostname, trust
and same-origin discovery/JWKS are retained, without redirect/proxy/insecure
bypass. The public JWKS probe requires exactly the original owner's single RSA
signing key with `use:sig`, `alg:RS256`, `kid:key-1`, matching the original key's
public numbers. It checks before and after the native exchange. Every security
condition uses explicit refusal, including when Python optimization is enabled.
This probe only compares public discovery/JWKS data; JWT/exchange/claim/scope and
wrong-secret verification exclusively use shipped `/keycape verify-client`.
The negative check supplies a generated unrelated random secret through the
existing `-previous-secret-env` refusal mechanism. The bounded receipt says
**wrong unrelated secret refused**, never rotation. An absent exact PASS,
nonzero exit, TLS/discovery error, malformed output or any FAIL refuses evidence.
All durable private files and the full receipt tree are compared before/after;
extra receipt files refuse continuity. Public receipts expose only booleans,
public signing SPKI fingerprint, ciphertext/source hashes and exact safe cleanup
obligations. No config, env value, private digest, credential, JWT, response body,
subject or client ID is copied into receipts.
Normal/failure cleanup attempts every safely owned container and tmpfs volume,
records exact verified/uncertain obligations and refuses success if any removal
is uncertain. Before the first plaintext write, the wrapper registers the exact
private `/dev/shm` directory inode/device/owner/mode and every future container,
volume and public-image-probe obligation; it requires a reaper ACK and fsyncs a
safe receipt checkpoint. Plaintext removal uses directory FDs and refuses
replacement inode, owner mismatch, symlink, hardlink or special-file traversal.
Docker failure does not skip this separate plaintext obligation.
Revision4 creates a **fresh exclusive execution session/process group** before
reading TLS or custody inputs. It never reuses a shell/custody/user group. The
independent reaper receives only the exact parent pidfd, public identifiers and
fixed endpoint, with no custody descriptor or service environment. Parent loss
or the 1800-second deadline first quiesces the owner invocation and its children:
parent identity is bound by pidfd; each remaining same-SID/PGID/UID member is
compared by stable process start time and signaled through its own pidfd. Unknown
membership, PID reuse or uninterruptible survivors refuses cleanup. No broad
user/inherited-group kill is used. Only after no live creator remains may the
reaper remove resources. Every Docker call also refuses an expired or mismatched
execution binding; host subprocesses cannot request a new session/group escape.
A CLI can die after submitting a daemon request. Public exact sibling
`-docker-inflight`/`-docker-uncertain` markers survive plaintext removal, so even
clear later resource readback cannot turn an in-flight request into proven
cleanup. The reaper writes an exclusive bounded `-reaper.json` outcome next to
the registered staging directory; its exact path is in the pre-decrypt receipt.
Normal verified cleanup cancels the reaper. These are scoped invocation processes,
not standing jobs. This revision's real disposable process tests prove the
local shutdown/quiescence behavior only; Docker tmpfs/import/issuer behavior
still requires independent native fixture proof.
## Exact owner inputs and invocation, after approval/native fixture proof
The launcher takes six paths/public arguments:
```
bash tools/bootstrap-recovery/run-isolated-keycape.sh \
VERIFIED_TRANSFER_RECEIPT PRIVATE_ADMISSION ADMISSION_SHA256 \
PRIVATE_EXISTING_TLS_CERT PRIVATE_EXISTING_TLS_KEY PRIVATE_ORIGINAL_TRUST
```
Do not run this candidate until root review and synthetic pinned-image runtime
proof admit the exact source. It prompts only for the independently held age key
using the existing custody helper. Current client credentials come only from the
original encrypted archive's already delivered registration/environment; no key,
password, factor or TLS private input is passed to the agent.
The hash-bound owner admission JSON has `ciphertext_sha256`, `manifest_sha256`,
`verifier_sha256` (original image `/keycape` binary), `trust_sha256`,
`tls_cert_sha256`, original `issuer`, and `consumer` containing existing exact
`client_id`, `secret_env`, `audience`, `scope`, `subject`, `tenant`, `roles`,
`deny_scope`. Private `manifest_sha256` is SHA256 of
`json.dumps(manifest, sort_keys=True, separators=(',', ':')).encode()` from the
admitted original archive. The wrapper checks it against the validated manifest,
not an arbitrary directory. Trust path/session names are derived internally;
operator input cannot select a remote Docker engine or arbitrary container set.
The selected original issuer configuration must use HTTPS443 and local service
port8080, as the admitted owner release does. Other configurations fail closed
and need explicit source review, rather than silent rewrite.
## Remaining proof and retained review findings
The 61 tests use synthetic data and mocked Docker/native outcomes. They exercise
archive integrity/symlink/lineage drift, optimizer-safe JWKS wrong-key/extra-key/
wrong-kid/cross-origin/DNS negatives, default launch binding, capabilities/devices/
namespace/security overrides, transport-vs-wrong-secret distinction, private
import ordering, secret-free argv, shared-tmpfs planning, and partial/uncertain
cleanup. Two additional real disposable process tests exercise only synthetic owner/
grandchild shutdown and hard parent loss, without Docker. The suite does
**not** prove the actual pinned image supports the complete
session or that the selected current credential/factor works.
Next: root review; native disposable pinned-image fixture with original hostname
and a synthetic trusted TLS pair; positive/negative service behavior and hard
interruption cleanup; then an admitted owner-run actual recovery. Actual T18
still needs selected downstream-consumer reconnect and owner acceptance. Human/
browser flow uses the existing `keycape login`; retained-factor MFA stays T19.
Both remain explicitly unproven in the receipt.
Prior exact source snapshots are retained under `revisions/r1/`, `revisions/r2/`
and `revisions/r3/`; frozen test roots/closure manifests accompany R3 and R4. Root's findings were added
as regression tests: added caps/devices/security exceptions; cleanup failure
incorrectly preserving success or abandoning later obligations; Python assert
optimization; extra JWKS signer ambiguity; same-image alternate launch; and
unsupported private tmpfs daemon-copy assumptions. Revision4 addresses these in source, plus the later lifecycle and target
findings. See `LIFECYCLE-R4.md` for the remaining native fixture oracle. No root approval or native acceptance is fabricated.
Knowledge orientation: emergent-security0.2.6, digest
`8d1d6a9d4d07fec7b619618de7dfe2254c1790f85f4198c7fe19db9751c02`,
withheld/changed-source status; current owner AGENTS/companion/recovery contract
and verifier source were reviewed. Supervised assignment; no autopilot or
privileged execution claimed.

View file

@ -0,0 +1,210 @@
#!/usr/bin/env python3
"""Age-only SOPS recovery to a new private file; no plaintext process output."""
from __future__ import annotations
import json
import os
from pathlib import Path
import re
import shutil
import signal
import stat
import subprocess
import sys
import tempfile
import uuid
ROOT = Path(__file__).resolve().parents[2]
RECIPIENT = re.compile(r'age1[023456789acdefghjklmnpqrstuvwxyz]{58}\Z')
class Refused(Exception):
pass
def public_recipient():
lines = (ROOT / 'keys/age.pub').read_text().splitlines()
values = [line.strip() for line in lines if line.strip() and not line.startswith('#')]
if len(values) != 1 or not RECIPIENT.fullmatch(values[0]):
raise Refused('registered public recipient is invalid')
result = subprocess.run(['age', '-r', values[0]], input=b'', stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, timeout=10)
if result.returncode:
raise Refused('registered public recipient is invalid')
return values[0]
def unique_object(pairs):
result = {}
for key, value in pairs:
if key in result:
raise Refused('duplicate encrypted document key')
result[key] = value
return result
def parse_document(raw, suffix):
if suffix == '.json':
data = json.loads(raw, object_pairs_hook=unique_object)
kind = 'json'
elif suffix in {'.yaml', '.yml'}:
import yaml
class UniqueLoader(yaml.SafeLoader):
def construct_mapping(self, node, deep=False):
self.flatten_mapping(node)
return unique_object((self.construct_object(key, deep=deep),
self.construct_object(value, deep=deep))
for key, value in node.value)
try:
data = yaml.load(raw, Loader=UniqueLoader)
except yaml.YAMLError:
raise Refused('invalid encrypted YAML input') from None
kind = 'yaml'
else:
raise Refused('age recovery supports SOPS JSON/YAML input')
return data, kind
def age_document(source, recipient, *, raw=None):
# Inspect ciphertext metadata before giving a credential to any subprocess.
# This repository's admitted SOPS profile is age-only, not arbitrary cloud
# KMS, key services, SSH identities or executable age plugins.
if raw is None:
with source.open("rb") as encrypted:
raw = encrypted.read(8 * 1024 * 1024 + 1)
if len(raw) > 8 * 1024 * 1024:
raise Refused('encrypted input is too large')
data, kind = parse_document(raw, source.suffix)
metadata = data.get('sops') if isinstance(data, dict) else None
if not isinstance(metadata, dict):
raise Refused('SOPS metadata is missing')
known = {'age', 'kms', 'gcp_kms', 'azure_kv', 'hc_vault', 'hckms', 'pgp',
'key_groups', 'shamir_threshold', 'lastmodified', 'mac', 'version',
'unencrypted_suffix', 'encrypted_suffix', 'unencrypted_regex',
'encrypted_regex', 'unencrypted_comment_regex', 'encrypted_comment_regex',
'mac_only_encrypted'}
if set(metadata) - known or metadata.get('key_groups'):
raise Refused('unsupported SOPS custody profile')
if any(metadata.get(key) for key in ('kms', 'gcp_kms', 'azure_kv', 'hc_vault', 'hckms', 'pgp')):
raise Refused('recovery requires age-only metadata')
recipients = metadata.get('age')
if not isinstance(recipients, list) or not recipients:
raise Refused('age recipient metadata is missing')
if any(not isinstance(row, dict) or not RECIPIENT.fullmatch(str(row.get('recipient', '')))
for row in recipients):
raise Refused('unsupported age recipient')
if recipient not in [row['recipient'] for row in recipients]:
raise Refused('document does not include the registered recipient')
return raw, kind
def private_key_fd():
name = os.environ.get('NETKINGDOM_CUSTODY_AGE_KEY_FILE', '')
if not name:
raise Refused('explicit temporary custody key required')
path = Path(os.path.abspath(name))
if any(p.is_symlink() for p in (path, *path.parents)):
raise Refused('custody key path must not contain symlinks')
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
info = os.fstat(fd)
if not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() or info.st_mode & 0o077 or info.st_nlink != 1 or info.st_size > 16384:
os.close(fd)
raise Refused('custody key must be an owned private regular file')
return fd
def decrypt(recipient):
source_name = os.environ.get('NK_SOPS_FILE', '')
output_name = os.environ.get('NK_SOPS_OUTPUT', '')
if not source_name or not output_name:
raise Refused('supply FILE and OUTPUT; plaintext stdout is disabled')
source = Path(source_name).resolve(strict=True)
output = Path(output_name).absolute()
parent = output.parent.resolve(strict=True)
if any((p / '.git').is_file() or (p / '.git/HEAD').is_file()
or ((p / 'HEAD').is_file() and (p / 'objects').is_dir() and (p / 'refs').is_dir())
for p in [parent, *parent.parents]):
raise Refused('plaintext output must be outside Git worktrees')
raw, kind = age_document(source, recipient)
binary = shutil.which('sops')
if binary is None:
raise Refused('sops executable is unavailable')
directory = os.open(parent, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
temporary = '.netkingdom-sops-' + uuid.uuid4().hex
keyfd = None
try:
info = os.fstat(directory)
if info.st_uid != os.getuid() or info.st_mode & 0o077:
raise Refused('OUTPUT requires an owned private parent directory')
try:
os.stat(output.name, dir_fd=directory, follow_symlinks=False)
except FileNotFoundError:
pass
else:
raise Refused('OUTPUT already exists')
keyfd = private_key_fd()
derived = subprocess.run(['age-keygen', '-y', f'/proc/self/fd/{keyfd}'],
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
pass_fds=(keyfd,), timeout=10)
if derived.returncode or derived.stdout.strip() != recipient.encode():
raise Refused('custody key does not match the registered recipient')
fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
0o600, dir_fd=directory)
with os.fdopen(fd, 'wb') as plain, tempfile.TemporaryDirectory(prefix='netkingdom-sops-home-') as isolated:
# HOME/XDG isolation matters: SOPS loads ambient keys in addition to
# an explicit key file. No inline keys, SSH keys, key commands,
# proxies or remote keyservice configuration are inherited.
env = {'PATH': os.environ.get('PATH', os.defpath), 'HOME': isolated,
'XDG_CONFIG_HOME': isolated, 'GNUPGHOME': isolated,
'SOPS_AGE_KEY_FILE': f'/proc/self/fd/{keyfd}',
'AWS_EC2_METADATA_DISABLED': 'true'}
result = subprocess.run([binary, '--decrypt', '--input-type', kind,
'--output-type', kind, '/dev/stdin'],
input=raw, stdout=plain, stderr=subprocess.DEVNULL,
env=env, cwd=isolated, pass_fds=(keyfd,), timeout=30)
if result.returncode:
raise Refused('SOPS decryption failed; no plaintext published')
plain.flush()
os.fsync(plain.fileno())
# Atomic no-overwrite publication within the private directory.
os.link(temporary, output.name, src_dir_fd=directory, dst_dir_fd=directory,
follow_symlinks=False)
os.unlink(temporary, dir_fd=directory)
os.fsync(directory)
finally:
if keyfd is not None:
os.close(keyfd)
try:
os.unlink(temporary, dir_fd=directory)
except FileNotFoundError:
pass
os.close(directory)
print('Decrypted to the requested private file. Remove it after the approved operation.')
def interrupted(_signum, _frame):
raise Refused('operation interrupted')
def main():
for sig in (signal.SIGTERM, signal.SIGHUP, signal.SIGINT):
signal.signal(sig, interrupted)
try:
if len(sys.argv) != 2 or sys.argv[1] not in {'setup', 'decrypt'}:
raise Refused('unsupported custody operation')
recipient = public_recipient()
if sys.argv[1] == 'setup':
print('Registered public recipient validated. No private key was read, copied or generated.')
else:
decrypt(recipient)
return 0
except Refused as exc:
print('ERROR: ' + str(exc), file=sys.stderr)
except (OSError, ValueError, TypeError, ImportError, subprocess.SubprocessError):
# Parser, filesystem and tool errors can include user data. Never echo.
print('ERROR: custody operation failed; input and tool diagnostics suppressed.', file=sys.stderr)
return 1
if __name__ == '__main__':
raise SystemExit(main())

View file

@ -0,0 +1,223 @@
import base64
import hashlib
import importlib.util
import io
import json
import subprocess
import tarfile
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
SOURCE=Path(__file__).parents[1]/'tools/bootstrap-recovery/isolated-keycape.py'
spec=importlib.util.spec_from_file_location('adapter',SOURCE)
a=importlib.util.module_from_spec(spec);spec.loader.exec_module(a)
class ArchiveTests(unittest.TestCase):
def setUp(self):
self.tmp=tempfile.TemporaryDirectory();self.addCleanup(self.tmp.cleanup)
self.root=Path(self.tmp.name)
config={'data':{k:base64.b64encode(v).decode() for k,v in {'config.yaml':b'issuer: https://issuer.example','key.pem':b'fixture-key'}.items()}}
self.values={'resources/sso/secret/keycape-config.json':json.dumps(config).encode(),
'resources/sso/deployment/keycape.json':json.dumps({'spec':{'template':{'spec':{'containers':[{'name':'keycape','image':a.IMAGE,'env':[
{'name':'KEYCAPE_CONFIG','value':'/etc/keycape/config.yaml'},
{'name':'KEYCAPE_POLICY_PATH','value':'/var/lib/keycape-policy/policy.json'},
{'name':'KEYCAPE_ORIGINAL_SECRET','valueFrom':{'secretKeyRef':{'name':'client','key':'secret'}}}]}]}}}}).encode(),
'resources/sso/secret/client.json':json.dumps({'data':{'secret':base64.b64encode(b'fixture-private').decode()}}).encode(),
'data/keycape/policy.json':b'{}','data/keycape/receipts/one.json':b'{"original":true}'}
for name,value in self.values.items():
p=self.root/name;p.parent.mkdir(parents=True,exist_ok=True);p.write_bytes(value);p.chmod(0o600)
self.manifest={'profile':{'components':[{'namespace':'sso','deployment':'keycape','secret_refs':['keycape-config','client'],'configmap_refs':[]}]},
'files':[{'path':k,'bytes':len(v),'sha256':hashlib.sha256(v).hexdigest()} for k,v in self.values.items()]}
def payload(self):
digest=hashlib.sha256(json.dumps(self.manifest,sort_keys=True,separators=(',',':')).encode()).hexdigest()
return a.private_payload(self.root,self.manifest,digest)
def test_original_state_and_secret_delivery_retained(self):
p=self.payload();self.assertEqual(p['env']['KEYCAPE_ORIGINAL_SECRET'],'fixture-private')
self.assertEqual(base64.b64decode(p['files']['/var/lib/keycape-policy/receipts/one.json']),b'{"original":true}')
def test_archive_digest_refused(self):
with self.assertRaises(a.Refused):a.private_payload(self.root,self.manifest,'0'*64)
def test_changed_file_refused(self):
(self.root/'data/keycape/policy.json').write_text('{"changed":true}')
with self.assertRaises(a.Refused):self.payload()
def test_symlink_refused(self):
p=self.root/'data/keycape/policy.json';p.unlink();p.symlink_to('/etc/hostname')
with self.assertRaises(Exception):self.payload()
def test_duplicate_manifest_refused(self):
self.manifest['files'].append(self.manifest['files'][0])
with self.assertRaises(a.Refused):self.payload()
def test_image_mismatch_refused(self):
d=json.loads(self.values['resources/sso/deployment/keycape.json']);d['spec']['template']['spec']['containers'][0]['image']='keycape:latest'
raw=json.dumps(d).encode();p=self.root/'resources/sso/deployment/keycape.json';p.write_bytes(raw)
for row in self.manifest['files']:
if row['path'].endswith('/deployment/keycape.json'):row.update(bytes=len(raw),sha256=hashlib.sha256(raw).hexdigest())
with self.assertRaises(a.Refused):self.payload()
class NativeTests(unittest.TestCase):
def setUp(self):
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
self.key=rsa.generate_private_key(public_exponent=65537,key_size=2048).private_bytes(serialization.Encoding.PEM,serialization.PrivateFormat.PKCS8,serialization.NoEncryption())
self.cfg=b'issuer: https://issuer.example\nclients:\n- clientId: existing\n clientType: confidential\n secretRef: env:KEYCAPE_ORIGINAL_SECRET\n grantTypes: [client_credentials]\n'
self.files={'/etc/keycape/config.yaml':self.cfg,'/etc/keycape/key.pem':self.key,'/var/lib/keycape-policy/policy.json':b'{}'}
self.payload={'env':{'KEYCAPE_ORIGINAL_SECRET':'fixture-private'},'files':{k:base64.b64encode(v).decode() for k,v in self.files.items()}}
self.admission={'issuer':'https://issuer.example','verifier_sha256':hashlib.sha256(b'binary').hexdigest(),
'trust_path':'/trust.pem','trust_sha256':hashlib.sha256(b'original-trust').hexdigest(),
'consumer':{'client_id':'existing','secret_env':'KEYCAPE_ORIGINAL_SECRET','scope':'read','audience':'consumer',
'subject':'service:existing','tenant':'tenant:platform','roles':'reader','deny_scope':'write'}}
self.session={'issuer':'isolated','volumes':{x:{'target':x} for x in ('/etc/keycape','/var/lib/keycape-policy','/etc/keycape-factor','/recovery-tls')}}
self.output=''.join('PASS '+x+'\n' for x in ['discovery and endpoint origin','exchange and JWKS signature for granted scopes','access-token claim bindings','principal_type is service','sub matches the registration','tenant matches the registration','roles present','granted scope carries no excess grant','predecessor secret refused','excess scope refused: write'])
def file(self,container,path):
return {**self.files,'/etc/hosts':b'127.0.0.1 issuer.example\n','/keycape':b'binary','/trust.pem':b'original-trust'}[path]
def command(self,argv,**kwargs):
if argv[1:3]==['container','inspect']:return json.dumps(['KEYCAPE_ORIGINAL_SECRET=fixture-private','SSL_CERT_FILE=/trust.pem']).encode()
output=io.BytesIO()
with tarfile.open(fileobj=output,mode='w') as t:
m=tarfile.TarInfo('policy.json');m.size=2;t.addfile(m,io.BytesIO(b'{}'))
return output.getvalue()
def verify(self,code=0,output=None):
with patch.object(a,'check_jwks'),patch.object(a,'runtime_file',return_value=b'original-trust'),patch.object(a,'runtime_files',side_effect=lambda session,paths=(),tree=False:({k:v for k,v in self.files.items() if k.startswith('/var/lib/keycape-policy/')} if tree else dict(self.files))),patch.object(a,'container_file',side_effect=self.file),patch.object(a,'command',side_effect=self.command),patch.object(a.subprocess,'run',return_value=subprocess.CompletedProcess([],code,b'',(self.output if output is None else output).encode())) as run:
result=a.run_verifier(self.session,self.admission,self.payload)
argv=run.call_args.args[0]
self.assertNotIn('fixture-private',' '.join(argv))
self.assertIn('-previous-secret-env',argv)
return result
def test_original_registration_and_unrelated_secret(self):
r=self.verify();self.assertTrue(r['wrong_unrelated_secret_explicitly_refused']);self.assertNotIn('fixture-private',json.dumps(r))
def test_transport_failure_never_counts_as_wrong_secret(self):
with self.assertRaises(a.Refused):self.verify(1,'FAIL discovery and endpoint origin: TLS failure\n')
def test_missing_explicit_rejection_never_counts(self):
with self.assertRaises(a.Refused):self.verify(0,self.output.replace('PASS predecessor secret refused\n',''))
def test_fail_line_refused_even_zero_exit(self):
with self.assertRaises(a.Refused):self.verify(0,self.output+'FAIL transport\n')
def test_security_environment_override_refused(self):
original=self.command
def altered(argv,**kwargs):
if argv[1:3]==['container','inspect']:return json.dumps(['KEYCAPE_ORIGINAL_SECRET=fixture-private','SSL_CERT_FILE=/trust.pem','SSL_CERT_DIR=/unreviewed']).encode()
return original(argv,**kwargs)
with patch.object(self,'command',side_effect=altered):
with self.assertRaises(a.Refused):self.verify()
def test_http_issuer_refused(self):
self.admission['issuer']='http://issuer.example'
with self.assertRaises(a.Refused):self.verify()
def test_changed_trust_refused(self):
self.admission['trust_sha256']='0'*64
with self.assertRaises(a.Refused):self.verify()
def test_config_drift_refused(self):
self.payload['files']['/etc/keycape/config.yaml']=base64.b64encode(b'changed').decode()
with self.assertRaises(a.Refused):self.verify()
class BoundaryTests(unittest.TestCase):
def test_remote_docker_refused_before_info(self):
session={'run_id':'a'*32,'containers':['nk49-kc-'+('a'*32)+'-worker'],'worker':'nk49-kc-'+('a'*32)+'-worker','issuer':'nk49-kc-'+('a'*32)+'-worker'}
with patch.object(a,'command',return_value=b'[{"Endpoints":{"docker":{"Host":"tcp://remote:2375"}}}]') as command:
with self.assertRaises(a.Refused):a.inspect_session(session)
self.assertEqual(command.call_count,1)
def test_failed_verifier_cleanup_is_attempted(self):
receipt={};admission={'session':{},'ciphertext_sha256':'a'*64,'manifest_sha256':'b'*64,'verifier_sha256':'c'*64,'trust_sha256':'d'*64,'consumer':{x:'yes' for x in ('client_id','secret_env','audience','scope','subject','tenant','roles','deny_scope')}}
with patch.object(a,'inspect_session'),patch.object(a,'private_payload',return_value={}),patch.object(a,'run_verifier',side_effect=a.Refused),patch.object(a,'cleanup') as cleanup:
with self.assertRaises(a.Refused):a.native_checks(Path('/unused'),{},admission,receipt)
cleanup.assert_called_once();self.assertTrue(receipt['container_cleanup_verified']);self.assertEqual(receipt['status'],'failed')
def test_cleanup_failure_overrides_success(self):
admission={'session':{},'ciphertext_sha256':'a'*64,'manifest_sha256':'b'*64,'verifier_sha256':'c'*64,'trust_sha256':'d'*64,'consumer':{x:'yes' for x in ('client_id','secret_env','audience','scope','subject','tenant','roles','deny_scope')}}
result={x:True for x in ('native_service_registration_verified','wrong_unrelated_secret_explicitly_refused','denied_scopes_refused','original_config_registry_policy_receipts_equal','original_https_issuer_and_trust_preserved','original_public_signing_key_in_live_jwks')}
result['public_signing_spki_sha256']='e'*64;receipt={}
with patch.object(a,'inspect_session'),patch.object(a,'private_payload',return_value={}),patch.object(a,'run_verifier',return_value=result),patch.object(a,'cleanup',side_effect=a.Refused):
with self.assertRaises(a.Refused):a.native_checks(Path('/unused'),{},admission,receipt)
self.assertEqual(receipt['status'],'failed');self.assertFalse(receipt['container_cleanup_verified'])
class ContainmentTests(unittest.TestCase):
def inspect(self,overrides=None,launch=None):
name='nk49-kc-'+('a'*32)+'-issuer'
session={'run_id':'a'*32,'containers':[name],'issuer':name,'worker':name,'images':{name:a.IMAGE}}
host={'NetworkMode':'none','ReadonlyRootfs':True,'Privileged':False,'CapDrop':['ALL'],'SecurityOpt':['no-new-privileges']}
host.update(overrides or {})
def command(argv,**kwargs):
if argv[1:3]==['context','inspect']:return b'[{"Endpoints":{"docker":{"Host":"unix:///var/run/docker.sock"}}}]'
if argv[1]=='info':return b'{"ID":"74f93783-6cef-43a8-a3c4-f62586cd3bc1","Name":"bnt-lap001","OSType":"linux"}'
if argv[-1]=='{{.Id}}':return b'anchor-id'
if '"path"' in argv[-1]:return json.dumps(launch or {'entrypoint':['/keycape'],'cmd':None,'path':'/keycape','args':[]}).encode()
return json.dumps({'host':host,'mounts':[],'labels':{a.LABEL:'a'*32},'image':a.IMAGE,'user':'1000:1000'}).encode()
with patch.object(a,'command',side_effect=command),patch.dict('os.environ',{'DOCKER_HOST':'unix:///var/run/docker.sock'}):a.inspect_session(session)
def test_minimal_network_none_admitted(self):self.inspect()
def test_alternate_same_image_command_refused(self):
with self.assertRaises(a.Refused):self.inspect(launch={'entrypoint':['/keycape'],'cmd':['check-config'],'path':'/keycape','args':['check-config']})
def test_cap_add_refused(self):
with self.assertRaises(a.Refused):self.inspect({'CapAdd':['NET_ADMIN']})
def test_devices_refused(self):
with self.assertRaises(a.Refused):self.inspect({'Devices':[{'PathOnHost':'/dev/mem'}]})
def test_device_requests_refused(self):
with self.assertRaises(a.Refused):self.inspect({'DeviceRequests':[{'Driver':'nvidia'}]})
def test_host_namespace_refused(self):
for field in ('PidMode','IpcMode','UTSMode','UsernsMode','CgroupnsMode'):
with self.subTest(field=field),self.assertRaises(a.Refused):self.inspect({field:'host'})
def test_security_override_refused(self):
with self.assertRaises(a.Refused):self.inspect({'SecurityOpt':['no-new-privileges','seccomp=unconfined']})
class CleanupTests(unittest.TestCase):
def test_partial_cleanup_attempts_every_owned_container(self):
session={'run_id':'a'*32,'containers':['first','second','third']};receipt={}
removed=[]
def command(argv,**kwargs):
if argv[1:3]==['container','inspect']:return ('a'*32).encode()
if argv[1]=='rm':
removed.append(argv[-1])
if argv[-1]=='third':raise a.Refused()
return b''
if argv[1:3]==['container','ls']:
name=next(x for x in argv if x.startswith('name=^/')).removeprefix('name=^/').removesuffix('$')
return (name+'\n').encode() if name=='third' or name not in removed else b''
return b''
with patch.object(a,'command',side_effect=command):
with self.assertRaises(a.Refused):a.cleanup(session,receipt)
self.assertEqual(removed,['third','second','first'])
self.assertEqual([r['status'] for r in receipt['container_cleanup_obligations']],['still_present','absence_verified','absence_verified'])
def test_ownership_mismatch_does_not_remove_and_continues(self):
session={'run_id':'a'*32,'containers':['first','second']};receipt={};removed=[]
def command(argv,**kwargs):
if argv[1:3]==['container','inspect']:return (('b' if argv[3]=='second' else 'a')*32).encode()
if argv[1]=='rm':removed.append(argv[-1])
if argv[1:3]==['container','ls']:
name=next(x for x in argv if x.startswith('name=^/')).removeprefix('name=^/').removesuffix('$')
return (name+'\n').encode() if name not in removed else b''
return b''
with patch.object(a,'command',side_effect=command):
with self.assertRaises(a.Refused):a.cleanup(session,receipt)
self.assertEqual(removed,['first'])
self.assertEqual(receipt['container_cleanup_obligations'][0]['status'],'ownership_mismatch_not_removed')
def test_inspect_failure_retains_uncertain_obligation(self):
receipt={}
with patch.object(a,'command',side_effect=a.Refused):
with self.assertRaises(a.Refused):a.cleanup({'run_id':'a'*32,'containers':['exact-owned-name']},receipt)
self.assertEqual(receipt['container_cleanup_obligations'],[{'container':'exact-owned-name','status':'unverified'}])
class JWKSTests(unittest.TestCase):
def execute(self,n='123',endpoint='https://issuer.example/jwks',resolved='127.0.0.1',extra=False,kid='key-1'):
request={'issuer':'https://issuer.example','trust':'public-certificate','n':'123','e':'65537'}
def enc(i):return base64.urlsafe_b64encode(int(i).to_bytes((int(i).bit_length()+7)//8,'big')).decode().rstrip('=')
responses=[{'issuer':'https://issuer.example','jwks_uri':endpoint},{'keys':[{'kty':'RSA','kid':kid,'use':'sig','alg':'RS256','n':enc(n),'e':enc(65537)}]}]
if extra:responses[1]['keys'].append(dict(responses[1]['keys'][0],kid='key-2'))
class Response:
def __init__(self,value):self.value=value
def __enter__(self):return self
def __exit__(self,*args):pass
def read(self,*args):return json.dumps(self.value).encode()
class Opener:
def open(self,*args,**kwargs):return Response(responses.pop(0))
with patch('sys.stdin',io.StringIO(json.dumps(request))),patch('sys.stdout',io.StringIO()),patch('socket.getaddrinfo',return_value=[(2,1,6,'',(resolved,443))]),patch('ssl.create_default_context'),patch('urllib.request.build_opener',return_value=Opener()):
exec(compile(a.JWKS_PROBE,'<jwks-probe>','exec',optimize=2),{})
def test_exact_original_public_key(self):self.execute()
def test_extra_signing_key_refused_under_optimization(self):
with self.assertRaises(ValueError):self.execute(extra=True)
def test_wrong_kid_refused_under_optimization(self):
with self.assertRaises(ValueError):self.execute(kid='replacement')
def test_wrong_live_signing_key_refused(self):
with self.assertRaises(ValueError):self.execute(n='124')
def test_cross_origin_jwks_refused(self):
with self.assertRaises(ValueError):self.execute(endpoint='https://production.example/jwks')
def test_production_dns_refused_before_fetch(self):
with self.assertRaises(ValueError):self.execute(resolved='192.0.2.1')
if __name__=='__main__':unittest.main()