docs: preserve netkingdom-target-2026-10-16 session evidence (batch 6)
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Reject generated-only commits / generated-commit-guard (push) Successful in 11s

This commit is contained in:
codex 2026-11-01 22:16:29 +01:00
parent a870092d09
commit 7a3632ffcc
187 changed files with 22808 additions and 0 deletions

View file

@ -0,0 +1,5 @@
#!/usr/bin/env bash
# Managed verification/rollback belongs to rapp-user-engine, not this migration tree.
set +x
printf '%s\n' 'Retired: use rapp-user-engine make verify-live and its reviewed release/rollback contract. This legacy helper cannot reapply old manifests, exercise rollback or certify historical backup jobs.' >&2
exit 2

View file

@ -0,0 +1,137 @@
#!/usr/bin/env bash
# Verify that the active identity stack has no runtime dependency on the
# retired/source host. Secret values are decoded only in process memory and
# never printed; failures report resource and key locators only.
set +x
set -euo pipefail
umask 077
KUBECTL="${KUBECTL:-kubectl}"
SOURCE_HOST_MARKER="${SOURCE_HOST_MARKER:-coulombcore}"
SOURCE_IP="${SOURCE_IP:-92.205.130.254}"
NAMESPACES="${NAMESPACES:-sso mfa user-engine databases}"
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
for namespace in $NAMESPACES; do
for kind in configmap secret deployment statefulset; do
"$KUBECTL" -n "$namespace" get "$kind" -o json \
>"$tmp/${namespace}-${kind}.json"
done
done
python3 - "$tmp" "$SOURCE_HOST_MARKER" "$SOURCE_IP" <<'PY'
import base64
import json
import pathlib
import sys
root = pathlib.Path(sys.argv[1])
patterns = {
"source-host": sys.argv[2].lower(),
"source-ip": sys.argv[3].lower(),
"public-lldap": "lldap.coulomb.social",
"public-privacyidea": "pink.coulomb.social",
"public-privacyidea-account": "pink-account.coulomb.social",
}
failures = []
for path in sorted(root.glob("*.json")):
namespace, kind = path.stem.rsplit("-", 1)
for item in json.loads(path.read_text()).get("items", []):
name = item["metadata"]["name"]
if kind in {"configmap", "secret"}:
for key, raw in (item.get("data") or {}).items():
try:
value = (
base64.b64decode(raw).decode("utf-8", "replace")
if kind == "secret"
else raw
)
except Exception:
continue
lowered = value.lower()
hits = [label for label, needle in patterns.items() if needle in lowered]
if hits:
failures.append(
f"{kind}/{namespace}/{name}:{key} matches={','.join(hits)}"
)
else:
lowered = json.dumps(item.get("spec", {}), sort_keys=True).lower()
hits = [label for label, needle in patterns.items() if needle in lowered]
if hits:
failures.append(
f"{kind}/{namespace}/{name} matches={','.join(hits)}"
)
if failures:
print("stale_dependencies=found")
for failure in failures:
print(failure)
raise SystemExit(1)
print("stale_dependencies=absent")
PY
keycape_config="$(
"$KUBECTL" -n sso get secret keycape-config \
-o jsonpath='{.data.config\.yaml}' | base64 -d
)"
for expected in \
lldap.sso.svc.cluster.local \
authelia.sso.svc.cluster.local \
privacyidea.mfa.svc.cluster.local \
user-engine \
rapp-qonto
do
if [[ "$keycape_config" != *"$expected"* ]]; then
echo "keycape_expected_dependency_missing=$expected" >&2
exit 1
fi
done
unset keycape_config
echo "keycape_dependencies=cluster-local-and-complete"
pg_pod="$(
"$KUBECTL" -n databases get pod \
-l 'cnpg.io/cluster=net-kingdom-pg,role=primary' \
-o jsonpath='{.items[0].metadata.name}'
)"
resolver_class="$(
"$KUBECTL" -n databases exec "$pg_pod" -- \
psql -X -A -t -U postgres -d privacyidea_db -c \
"select case
when lower(\"Value\") like '%lldap.sso.svc.cluster.local%'
then 'cluster-local'
else 'other'
end
from resolverconfig
where \"Key\"='LDAPURI';" 2>/dev/null
)"
if [[ "$resolver_class" != "cluster-local" ]]; then
echo "privacyidea_lldap_resolver=$resolver_class" >&2
exit 1
fi
echo "privacyidea_lldap_resolver=cluster-local"
for target in \
sso/lldap \
sso/authelia \
sso/keycape \
mfa/privacyidea \
user-engine/user-engine
do
namespace="${target%/*}"
name="${target#*/}"
ready="$(
"$KUBECTL" -n "$namespace" get deployment "$name" \
-o jsonpath='{.status.readyReplicas}'
)"
if [[ "${ready:-0}" -lt 1 ]]; then
echo "deployment_not_ready=$target" >&2
exit 1
fi
done
echo "identity_deployments=ready"
echo "identity_cutover_dependencies=PASS"

View file

@ -0,0 +1,114 @@
#!/usr/bin/env bash
# verify-t02.sh — verify T02 (K8s foundations) completion criteria
#
# Run after applying all T02 manifests. Exits 0 if all checks pass.
#
# Usage: ./verify-t02.sh [--kubeconfig PATH]
set -euo pipefail
KUBECTL="kubectl"
if [[ "${1:-}" == "--kubeconfig" ]]; then
KUBECTL="kubectl --kubeconfig $2"
fi
PASS=0
FAIL=0
check() {
local desc="$1"
shift
if "$@" &>/dev/null; then
echo " PASS $desc"
PASS=$((PASS + 1))
else
echo " FAIL $desc"
FAIL=$((FAIL + 1))
fi
}
check_output() {
local desc="$1"
local expected="$2"
shift 2
local actual
actual="$("$@" 2>/dev/null || true)"
if echo "$actual" | grep -q "$expected"; then
echo " PASS $desc"
PASS=$((PASS + 1))
else
echo " FAIL $desc (got: $actual)"
FAIL=$((FAIL + 1))
fi
}
echo "=== T02 Verification — K8s Foundations ==="
echo ""
# ── Namespaces ────────────────────────────────────────────────────────────────
echo "[ Namespaces ]"
check "namespace sso exists" $KUBECTL get namespace sso
check "namespace mfa exists" $KUBECTL get namespace mfa
check "namespace databases exists" $KUBECTL get namespace databases
check "sso has net-kingdom/component label" \
$KUBECTL get namespace sso -o jsonpath='{.metadata.labels.net-kingdom/component}'
echo ""
# ── NetworkPolicies ───────────────────────────────────────────────────────────
echo "[ NetworkPolicies ]"
for ns in sso mfa databases; do
check "default-deny-all in $ns" \
$KUBECTL get networkpolicy default-deny-all -n "$ns"
check "allow-egress-dns in $ns" \
$KUBECTL get networkpolicy allow-egress-dns -n "$ns"
done
check "allow-traefik-to-keycape in sso" $KUBECTL get networkpolicy allow-traefik-to-keycape -n sso
check "allow-user-engine-to-keycape in sso" $KUBECTL get networkpolicy allow-user-engine-to-keycape -n sso
check "allow-keycape-egress-to-privacyidea in sso" $KUBECTL get networkpolicy allow-keycape-egress-to-privacyidea -n sso
check "allow-ingress-from-traefik in mfa" $KUBECTL get networkpolicy allow-ingress-from-traefik -n mfa
check "allow-ingress-from-keycape in mfa" $KUBECTL get networkpolicy allow-ingress-from-keycape -n mfa
check "allow-egress-to-postgres in mfa" $KUBECTL get networkpolicy allow-egress-to-postgres -n mfa
check "allow-ingress-from-keycloak in databases" $KUBECTL get networkpolicy allow-ingress-from-keycloak -n databases
check "allow-ingress-from-privacyidea in databases" $KUBECTL get networkpolicy allow-ingress-from-privacyidea -n databases
echo ""
# ── cert-manager ─────────────────────────────────────────────────────────────
echo "[ cert-manager ]"
check "cert-manager namespace exists" $KUBECTL get namespace cert-manager
check "selfsigned-issuer exists" $KUBECTL get clusterissuer selfsigned-issuer
check "letsencrypt-prod issuer exists" $KUBECTL get clusterissuer letsencrypt-prod
check_output "selfsigned-issuer is Ready" "True" \
$KUBECTL get clusterissuer selfsigned-issuer -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}'
# Test certificate (applied separately — only check if present)
if $KUBECTL get namespace cert-manager-test &>/dev/null; then
check_output "test certificate is Ready" "True" \
$KUBECTL get certificate selfsigned-test -n cert-manager-test \
-o jsonpath='{.status.conditions[?(@.type=="Ready")].status}'
else
echo " SKIP test certificate (namespace cert-manager-test not found — apply test-certificate.yaml)"
fi
echo ""
# ── StorageClass ──────────────────────────────────────────────────────────────
echo "[ StorageClass ]"
check "a default StorageClass exists" \
$KUBECTL get storageclass -o jsonpath='{.items[?(@.metadata.annotations.storageclass\.kubernetes\.io/is-default-class=="true")].metadata.name}'
if $KUBECTL get namespace storage-test &>/dev/null; then
check_output "storage-test pod Completed" "Completed\|Succeeded" \
$KUBECTL get pod storage-test -n storage-test -o jsonpath='{.status.phase}'
else
echo " SKIP StorageClass PVC test (apply storage/verify-pvc.yaml first)"
fi
echo ""
# ── Summary ───────────────────────────────────────────────────────────────────
TOTAL=$((PASS + FAIL))
echo "=== Results: $PASS/$TOTAL passed ==="
if [[ $FAIL -gt 0 ]]; then
echo " $FAIL check(s) failed."
exit 1
else
echo " T02 COMPLETE — all checks passed."
fi

View file

@ -0,0 +1,170 @@
#!/usr/bin/env bash
# verify-t03.sh — verify NK-WP-0001-T03 done-criteria
#
# Checks:
# 1. CloudNativePG operator is installed and running
# 2. Cluster net-kingdom-pg is Ready
# 3. privacyidea_db database exists
# 4. privacyidea role exists
# 5. K8s Secrets are present in the databases namespace
# 6. (Optional) Scheduled backup CR is present when backup is configured
#
# Note: keycloak_db removed — Keycloak replaced by Authelia+LLDAP+KeyCape (T05).
#
# Usage:
# chmod +x verify-t03.sh
# ./verify-t03.sh
set -euo pipefail
PASS=0
FAIL=0
WARN=0
pass() { echo " [PASS] $1"; PASS=$((PASS + 1)); }
fail() { echo " [FAIL] $1"; FAIL=$((FAIL + 1)); }
warn() { echo " [WARN] $1"; WARN=$((WARN + 1)); }
section() { echo ""; echo "── $1 ──────────────────────────────────────"; }
# ── 1. CloudNativePG operator ─────────────────────────────────────────────────
section "1. CloudNativePG operator"
if kubectl get ns cnpg-system &>/dev/null; then
pass "cnpg-system namespace exists"
else
fail "cnpg-system namespace not found — install operator first (see postgresql/README.md)"
fi
if kubectl get crd clusters.postgresql.cnpg.io &>/dev/null; then
pass "clusters.postgresql.cnpg.io CRD registered"
else
fail "CloudNativePG CRD not found — operator not installed"
fi
CNPG_READY=$(kubectl get pods -n cnpg-system -l app.kubernetes.io/name=cloudnative-pg \
--field-selector=status.phase=Running --no-headers 2>/dev/null | wc -l || echo 0)
if [[ "$CNPG_READY" -ge 1 ]]; then
pass "CloudNativePG operator pod running ($CNPG_READY pod(s))"
else
fail "No running CloudNativePG operator pods in cnpg-system"
fi
# ── 2. Cluster readiness ──────────────────────────────────────────────────────
section "2. Cluster net-kingdom-pg"
CLUSTER_READY=$(kubectl get cluster net-kingdom-pg -n databases \
-o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || echo "")
if [[ "$CLUSTER_READY" == "True" ]]; then
pass "Cluster net-kingdom-pg status: Ready"
else
CLUSTER_PHASE=$(kubectl get cluster net-kingdom-pg -n databases \
-o jsonpath='{.status.phase}' 2>/dev/null || echo "not found")
fail "Cluster net-kingdom-pg not Ready (phase: $CLUSTER_PHASE)"
fi
PRIMARY_POD=$(kubectl get pod -n databases \
-l "cnpg.io/cluster=net-kingdom-pg,role=primary" \
--field-selector=status.phase=Running \
-o name 2>/dev/null | head -1 || echo "")
if [[ -n "$PRIMARY_POD" ]]; then
pass "Primary pod running: $PRIMARY_POD"
else
fail "No running primary pod found for net-kingdom-pg"
fi
# ── 3. Databases ──────────────────────────────────────────────────────────────
section "3. Databases"
if [[ -n "$PRIMARY_POD" ]]; then
DB_LIST=$(kubectl exec -n databases "$PRIMARY_POD" -- \
psql -U postgres -tAc "SELECT datname FROM pg_database WHERE datname = 'privacyidea_db';" \
2>/dev/null || echo "")
if echo "$DB_LIST" | grep -q "privacyidea_db"; then
pass "privacyidea_db exists"
else
fail "privacyidea_db not found"
fi
else
warn "Skipping database checks — no primary pod available"
fi
# ── 4. Roles ──────────────────────────────────────────────────────────────────
section "4. Database roles"
if [[ -n "$PRIMARY_POD" ]]; then
ROLE_LIST=$(kubectl exec -n databases "$PRIMARY_POD" -- \
psql -U postgres -tAc "SELECT rolname FROM pg_roles WHERE rolname = 'privacyidea';" \
2>/dev/null || echo "")
if echo "$ROLE_LIST" | grep -q "privacyidea"; then
pass "role privacyidea exists"
else
fail "role privacyidea not found"
fi
else
warn "Skipping role checks — no primary pod available"
fi
# ── 5. K8s Secrets ────────────────────────────────────────────────────────────
section "5. K8s Secrets (databases namespace)"
for secret in net-kingdom-pg-privacyidea-app; do
if kubectl get secret "$secret" -n databases &>/dev/null; then
pass "Secret $secret exists"
else
fail "Secret $secret not found — run create-secrets.sh"
fi
done
# CNPG auto-creates these
for secret in net-kingdom-pg-superuser; do
if kubectl get secret "$secret" -n databases &>/dev/null; then
pass "Secret $secret exists (CNPG-managed)"
else
warn "Secret $secret not found (CNPG creates this; may appear after cluster init)"
fi
done
# ── 6. Backup configuration ───────────────────────────────────────────────────
section "6. Backup (optional until object storage is provisioned)"
if kubectl get scheduledbackup net-kingdom-pg-daily -n databases &>/dev/null; then
BACKUP_SUSPENDED=$(kubectl get scheduledbackup net-kingdom-pg-daily -n databases \
-o jsonpath='{.spec.suspend}' 2>/dev/null || echo "false")
if [[ "$BACKUP_SUSPENDED" == "true" ]]; then
warn "ScheduledBackup net-kingdom-pg-daily is suspended"
else
pass "ScheduledBackup net-kingdom-pg-daily present and active"
fi
LAST_BACKUP=$(kubectl get backup -n databases \
-l "cnpg.io/cluster=net-kingdom-pg" \
--sort-by='.metadata.creationTimestamp' \
-o name 2>/dev/null | tail -1 || echo "")
if [[ -n "$LAST_BACKUP" ]]; then
pass "At least one backup found: $LAST_BACKUP"
else
warn "No backups yet — trigger a manual backup or wait for schedule"
fi
else
warn "ScheduledBackup not deployed — configure object storage, then apply scheduled-backup.yaml"
fi
# ── Summary ───────────────────────────────────────────────────────────────────
echo ""
echo "════════════════════════════════════════════════"
echo " T03 verification: PASS=$PASS WARN=$WARN FAIL=$FAIL"
echo "════════════════════════════════════════════════"
if [[ "$FAIL" -gt 0 ]]; then
echo " Result: INCOMPLETE — resolve FAIL items before proceeding to T04"
exit 1
elif [[ "$WARN" -gt 0 ]]; then
echo " Result: PARTIAL — T03 core done; WARN items should be addressed before production"
exit 0
else
echo " Result: COMPLETE — T03 done-criteria met"
exit 0
fi

View file

@ -0,0 +1,213 @@
#!/usr/bin/env bash
# verify-t04.sh — verify NK-WP-0001-T04 done-criteria
#
# Checks:
# 1. privacyIDEA pod is Running in the mfa namespace
# 2. privacyidea Service exists
# 3. Traefik Middlewares exist
# 4. Ingress resources exist with correct hostnames
# 5. TLS certificates issued by cert-manager
# 6. Required K8s Secrets are present
# 7. PVCs are Bound
# 8. Enckey and auditkey Secrets present (from enckey-bootstrap.sh)
#
# Usage:
# chmod +x verify-t04.sh
# ./verify-t04.sh
set -euo pipefail
NAMESPACE="mfa"
PASS=0
FAIL=0
WARN=0
pass() { echo " [PASS] $1"; ((PASS++)); }
fail() { echo " [FAIL] $1"; ((FAIL++)); }
warn() { echo " [WARN] $1"; ((WARN++)); }
section() { echo ""; echo "── $1 ──────────────────────────────────────"; }
# ── 1. privacyIDEA pod ────────────────────────────────────────────────────────
section "1. privacyIDEA pod (namespace: $NAMESPACE)"
PI_POD=$(kubectl get pod -n "$NAMESPACE" \
-l app.kubernetes.io/name=privacyidea \
--field-selector=status.phase=Running \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "")
if [[ -n "$PI_POD" ]]; then
pass "Pod Running: $PI_POD"
READY=$(kubectl get pod -n "$NAMESPACE" "$PI_POD" \
-o jsonpath='{.status.containerStatuses[0].ready}' 2>/dev/null || echo "false")
if [[ "$READY" == "true" ]]; then
pass "Pod readiness probe passing"
else
fail "Pod is Running but not Ready (probe failing — check logs)"
fi
else
PENDING=$(kubectl get pod -n "$NAMESPACE" \
-l app.kubernetes.io/name=privacyidea \
-o name 2>/dev/null | wc -l || echo 0)
if [[ "$PENDING" -gt 0 ]]; then
fail "privacyIDEA pod(s) exist but none are Running (check kubectl describe pod -n $NAMESPACE)"
else
fail "No privacyIDEA pods found in namespace $NAMESPACE — apply deployment.yaml"
fi
fi
# ── 2. Service ────────────────────────────────────────────────────────────────
section "2. Service"
if kubectl get service privacyidea -n "$NAMESPACE" &>/dev/null; then
pass "Service privacyidea exists"
PORT=$(kubectl get service privacyidea -n "$NAMESPACE" \
-o jsonpath='{.spec.ports[0].port}' 2>/dev/null || echo "")
if [[ "$PORT" == "8080" ]]; then
pass "Service port: 8080"
else
warn "Service port is $PORT (expected 8080 — check container port and netpol)"
fi
else
fail "Service privacyidea not found in namespace $NAMESPACE"
fi
# ── 3. Traefik Middlewares ────────────────────────────────────────────────────
section "3. Traefik Middlewares"
for mw in privacyidea-rate-limit privacyidea-admin-allowlist; do
if kubectl get middleware "$mw" -n "$NAMESPACE" &>/dev/null; then
pass "Middleware $mw exists"
else
fail "Middleware $mw not found — apply middleware.yaml"
fi
done
# ── 4. Ingress resources ──────────────────────────────────────────────────────
section "4. Ingress resources"
for ing in privacyidea privacyidea-admin privacyidea-account; do
if kubectl get ingress "$ing" -n "$NAMESPACE" &>/dev/null; then
pass "Ingress $ing exists"
else
fail "Ingress $ing not found — apply ingress.yaml"
fi
done
# Verify hostnames in the main Ingress
PI_HOST=$(kubectl get ingress privacyidea -n "$NAMESPACE" \
-o jsonpath='{.spec.rules[0].host}' 2>/dev/null || echo "")
if [[ "$PI_HOST" == "pink.coulomb.social" ]]; then
pass "Ingress host: pink.coulomb.social"
else
fail "Ingress host is '$PI_HOST' (expected pink.coulomb.social)"
fi
ACCOUNT_HOST=$(kubectl get ingress privacyidea-account -n "$NAMESPACE" \
-o jsonpath='{.spec.rules[0].host}' 2>/dev/null || echo "")
if [[ "$ACCOUNT_HOST" == "pink-account.coulomb.social" ]]; then
pass "Self-service host: pink-account.coulomb.social"
else
fail "Self-service host is '$ACCOUNT_HOST' (expected pink-account.coulomb.social)"
fi
# ── 5. TLS certificates ───────────────────────────────────────────────────────
section "5. TLS certificates"
for cert_secret in pink-tls pink-account-tls; do
if kubectl get secret "$cert_secret" -n "$NAMESPACE" &>/dev/null; then
# Check cert-manager Ready condition
CERT_NAME="${cert_secret%-tls}"
CERT_READY=$(kubectl get certificate "$CERT_NAME" -n "$NAMESPACE" \
-o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || echo "")
if [[ "$CERT_READY" == "True" ]]; then
pass "Certificate $cert_name is Ready (TLS secret $cert_secret exists)"
else
warn "TLS secret $cert_secret exists but certificate status is not Ready (DNS propagation pending?)"
fi
else
warn "TLS secret $cert_secret not yet issued (cert-manager pending — check DNS and ACME)"
fi
done
# ── 6. K8s Secrets ────────────────────────────────────────────────────────────
section "6. K8s Secrets (namespace: $NAMESPACE)"
for secret in privacyidea-config; do
if kubectl get secret "$secret" -n "$NAMESPACE" &>/dev/null; then
pass "Secret $secret exists"
else
fail "Secret $secret not found — run create-secrets.sh"
fi
done
# DR secrets — created by enckey-bootstrap.sh (may not exist yet on first run)
for secret in privacyidea-enckey privacyidea-auditkeys; do
if kubectl get secret "$secret" -n "$NAMESPACE" &>/dev/null; then
pass "Secret $secret exists"
else
warn "Secret $secret not found — use privacyidea/KEY-RECOVERY.md for private export"
fi
done
# ── 7. PVCs ───────────────────────────────────────────────────────────────────
section "7. PersistentVolumeClaims"
for pvc in privacyidea-data privacyidea-logs; do
STATUS=$(kubectl get pvc "$pvc" -n "$NAMESPACE" \
-o jsonpath='{.status.phase}' 2>/dev/null || echo "not found")
if [[ "$STATUS" == "Bound" ]]; then
pass "PVC $pvc: Bound"
elif [[ "$STATUS" == "not found" ]]; then
fail "PVC $pvc not found — apply pvc.yaml"
else
fail "PVC $pvc status: $STATUS (expected Bound)"
fi
done
# ── 8. Key material check ─────────────────────────────────────────────────────
section "8. Key material (inside pod)"
if [[ -n "$PI_POD" ]]; then
if kubectl exec -n "$NAMESPACE" "$PI_POD" -- test -f /etc/privacyidea/enckey 2>/dev/null; then
pass "enckey present in pod"
else
warn "enckey not found in pod — stop and follow privacyidea/KEY-RECOVERY.md; do not generate a replacement recovery key"
fi
if kubectl exec -n "$NAMESPACE" "$PI_POD" -- test -f /etc/privacyidea/private.pem 2>/dev/null; then
pass "audit private.pem present in pod"
else
warn "audit private.pem not found — use privacyidea/KEY-RECOVERY.md for private export"
fi
if kubectl exec -n "$NAMESPACE" "$PI_POD" -- \
pi-manage admin list 2>/dev/null | grep -q "pi-admin"; then
pass "Admin user pi-admin exists"
else
warn "pi-admin not found — run bootstrap-admin.sh"
fi
# Legacy trigger-account presence is not a current KeyCape readiness gate.
else
warn "Skipping key material checks — no running pod"
fi
# ── Summary ───────────────────────────────────────────────────────────────────
echo ""
echo "════════════════════════════════════════════════════════════"
echo " T04 verification: PASS=$PASS WARN=$WARN FAIL=$FAIL"
echo "════════════════════════════════════════════════════════════"
if [[ "$FAIL" -gt 0 ]]; then
echo " Result: INCOMPLETE — resolve FAIL items before proceeding to T05"
exit 1
elif [[ "$WARN" -gt 0 ]]; then
echo " Result: PARTIAL — T04 core is up; WARN items should be resolved before T05"
exit 0
else
echo " Result: COMPLETE — T04 done-criteria met; proceed to T05 (SSO core: LLDAP+Authelia+KeyCape)"
exit 0
fi

View file

@ -0,0 +1,313 @@
#!/usr/bin/env bash
# verify-t05.sh — verify NK-WP-0001-T05 done-criteria
#
# Checks all three components of the new SSO stack (LLDAP, Authelia, KeyCape).
#
# Sections:
# 1. LLDAP pod Running+Ready
# 2. LLDAP services (ldap :3890, web-ui :17170)
# 3. LLDAP Middleware (lldap-admin-allowlist)
# 4. LLDAP Ingress + hostname (lldap.coulomb.social)
# 5. LLDAP TLS certificate
# 6. LLDAP Secret (lldap-secrets)
# 7. LLDAP PVC Bound
# 8. LLDAP health endpoint
# 9. Authelia pod Running+Ready
# 10. Authelia service
# 11. Authelia Ingress + hostname (auth.coulomb.social)
# 12. Authelia TLS certificate
# 13. Authelia Secrets (authelia-secrets)
# 14. Authelia PVC Bound
# 15. Authelia health endpoint
# 16. KeyCape pod Running+Ready
# 17. KeyCape service
# 18. KeyCape Middlewares (keycape-rate-limit, keycape-hsts)
# 19. KeyCape Ingress + hostname (kc.coulomb.social)
# 20. KeyCape TLS certificate
# 21. KeyCape Secret (keycape-config)
# 22. KeyCape health endpoint (/healthz)
# 23. KeyCape OIDC discovery (/.well-known/openid-configuration)
#
# Usage:
# chmod +x verify-t05.sh
# ./verify-t05.sh
set -euo pipefail
NAMESPACE="sso"
LLDAP_HOST="lldap.coulomb.social"
AUTH_HOST="auth.coulomb.social"
KC_HOST="kc.coulomb.social"
PASS=0
FAIL=0
WARN=0
pass() { echo " [PASS] $1"; ((PASS++)); }
fail() { echo " [FAIL] $1"; ((FAIL++)); }
warn() { echo " [WARN] $1"; ((WARN++)); }
section() { echo ""; echo "── $1 ──────────────────────────────────────"; }
check_pod() {
local name="$1"
local label="$2"
local pod=""
pod=$(kubectl get pod -n "$NAMESPACE" \
-l "app.kubernetes.io/name=${label}" \
--field-selector=status.phase=Running \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "")
if [[ -n "$pod" ]]; then
pass "Pod Running: $pod"
local ready
ready=$(kubectl get pod -n "$NAMESPACE" "$pod" \
-o jsonpath='{.status.containerStatuses[0].ready}' 2>/dev/null || echo "false")
if [[ "$ready" == "true" ]]; then
pass "Pod readiness probe passing"
else
fail "Pod is Running but not Ready (probe failing — check logs)"
fi
echo "$pod"
else
local count
count=$(kubectl get pod -n "$NAMESPACE" \
-l "app.kubernetes.io/name=${label}" -o name 2>/dev/null | wc -l || echo 0)
if [[ "$count" -gt 0 ]]; then
fail "${name} pod(s) exist but none Running (kubectl describe pod -n $NAMESPACE -l app.kubernetes.io/name=${label})"
else
fail "No ${name} pods found — apply deployment.yaml"
fi
echo ""
fi
}
check_service() {
local svc="$1"; shift
local ports=("$@")
if kubectl get service "$svc" -n "$NAMESPACE" &>/dev/null; then
pass "Service $svc exists"
for port in "${ports[@]}"; do
if kubectl get service "$svc" -n "$NAMESPACE" \
-o jsonpath="{.spec.ports[*].port}" 2>/dev/null | grep -qw "$port"; then
pass "Service $svc has port $port"
else
fail "Service $svc missing port $port"
fi
done
else
fail "Service $svc not found"
fi
}
check_ingress() {
local ing="$1"; local expected_host="$2"
if kubectl get ingress "$ing" -n "$NAMESPACE" &>/dev/null; then
pass "Ingress $ing exists"
local host
host=$(kubectl get ingress "$ing" -n "$NAMESPACE" \
-o jsonpath='{.spec.rules[0].host}' 2>/dev/null || echo "")
if [[ "$host" == "$expected_host" ]]; then
pass "Ingress $ing host: $expected_host"
else
fail "Ingress $ing host is '$host' (expected $expected_host)"
fi
else
fail "Ingress $ing not found — apply ingress.yaml"
fi
}
check_tls() {
local secret="$1"; local host="$2"
if kubectl get secret "$secret" -n "$NAMESPACE" &>/dev/null; then
local cert_name="${secret%-tls}"
local cert_ready
cert_ready=$(kubectl get certificate "$cert_name" -n "$NAMESPACE" \
-o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || echo "")
if [[ "$cert_ready" == "True" ]]; then
pass "Certificate $cert_name Ready (TLS secret $secret issued)"
else
warn "TLS secret $secret exists but cert status not Ready (DNS/ACME pending?)"
fi
else
warn "TLS secret $secret not yet issued (cert-manager pending — check DNS and ACME)"
fi
}
check_secret() {
local secret="$1"
if kubectl get secret "$secret" -n "$NAMESPACE" &>/dev/null; then
pass "Secret $secret exists"
else
fail "Secret $secret not found — run create-secrets.sh"
fi
}
check_pvc() {
local pvc="$1"
local status
status=$(kubectl get pvc "$pvc" -n "$NAMESPACE" \
-o jsonpath='{.status.phase}' 2>/dev/null || echo "not found")
if [[ "$status" == "Bound" ]]; then
pass "PVC $pvc: Bound"
elif [[ "$status" == "not found" ]]; then
fail "PVC $pvc not found — apply pvc.yaml"
else
fail "PVC $pvc status: $status (expected Bound)"
fi
}
# ═══════════════════════════════════════════════════════════════════
# LLDAP
# ═══════════════════════════════════════════════════════════════════
section "1. LLDAP pod (namespace: $NAMESPACE)"
LLDAP_POD=$(check_pod "LLDAP" "lldap")
section "2. LLDAP services"
check_service "lldap" 3890 17170
section "3. LLDAP Middleware"
if kubectl get middleware lldap-admin-allowlist -n "$NAMESPACE" &>/dev/null; then
pass "Middleware lldap-admin-allowlist exists"
else
fail "Middleware lldap-admin-allowlist not found — apply middleware.yaml"
fi
section "4. LLDAP Ingress"
check_ingress "lldap" "$LLDAP_HOST"
section "5. LLDAP TLS certificate"
check_tls "lldap-tls" "$LLDAP_HOST"
section "6. LLDAP Secret"
check_secret "lldap-secrets"
section "7. LLDAP PVC"
check_pvc "lldap-data"
section "8. LLDAP health endpoint"
if [[ -n "$LLDAP_POD" ]]; then
HTTP=$(kubectl exec -n "$NAMESPACE" "$LLDAP_POD" -- \
wget -qO- http://localhost:17170/health 2>/dev/null || echo "")
if echo "$HTTP" | grep -qi "ok\|healthy\|true"; then
pass "LLDAP health endpoint responds OK"
else
warn "LLDAP health endpoint response unclear: '$HTTP'"
fi
else
warn "Skipping LLDAP health check — no running pod"
fi
# ═══════════════════════════════════════════════════════════════════
# Authelia
# ═══════════════════════════════════════════════════════════════════
section "9. Authelia pod (namespace: $NAMESPACE)"
AUTHELIA_POD=$(check_pod "Authelia" "authelia")
section "10. Authelia service"
check_service "authelia" 9091
section "11. Authelia Ingress"
check_ingress "authelia" "$AUTH_HOST"
section "12. Authelia TLS certificate"
check_tls "auth-tls" "$AUTH_HOST"
section "13. Authelia Secrets"
check_secret "authelia-secrets"
section "14. Authelia PVC"
check_pvc "authelia-data"
section "15. Authelia health endpoint"
if [[ -n "$AUTHELIA_POD" ]]; then
STATUS=$(kubectl exec -n "$NAMESPACE" "$AUTHELIA_POD" -- \
wget -qO- http://localhost:9091/api/health 2>/dev/null || echo "")
if echo "$STATUS" | grep -qi "ok\|healthy"; then
pass "Authelia /api/health responds OK"
else
warn "Authelia /api/health response unclear: '$STATUS'"
fi
else
warn "Skipping Authelia health check — no running pod"
fi
# ═══════════════════════════════════════════════════════════════════
# KeyCape
# ═══════════════════════════════════════════════════════════════════
section "16. KeyCape pod (namespace: $NAMESPACE)"
KC_POD=$(check_pod "KeyCape" "keycape")
section "17. KeyCape service"
check_service "keycape" 8080
section "18. KeyCape Middlewares"
for mw in keycape-rate-limit keycape-hsts; do
if kubectl get middleware "$mw" -n "$NAMESPACE" &>/dev/null; then
pass "Middleware $mw exists"
else
fail "Middleware $mw not found — apply middleware.yaml"
fi
done
section "19. KeyCape Ingress"
check_ingress "keycape" "$KC_HOST"
section "20. KeyCape TLS certificate"
check_tls "kc-tls" "$KC_HOST"
section "21. KeyCape Secrets"
check_secret "keycape-config"
section "22. KeyCape health endpoint"
if [[ -n "$KC_POD" ]]; then
STATUS=$(kubectl exec -n "$NAMESPACE" "$KC_POD" -- \
wget -qO- http://localhost:8080/healthz 2>/dev/null || echo "")
if echo "$STATUS" | grep -qi "ok\|healthy"; then
pass "KeyCape /healthz responds OK"
else
warn "KeyCape /healthz response unclear: '$STATUS'"
fi
else
warn "Skipping KeyCape health check — no running pod"
fi
section "23. KeyCape OIDC discovery"
if [[ -n "$KC_POD" ]]; then
DISCOVERY=$(kubectl exec -n "$NAMESPACE" "$KC_POD" -- \
wget -qO- "http://localhost:8080/.well-known/openid-configuration" 2>/dev/null || echo "")
if echo "$DISCOVERY" | grep -q '"issuer"'; then
pass "OIDC discovery endpoint returns issuer"
ISSUER=$(echo "$DISCOVERY" | python3 -c \
"import sys,json; d=json.load(sys.stdin); print(d.get('issuer',''))" 2>/dev/null || echo "")
if [[ "$ISSUER" == "https://$KC_HOST" ]]; then
pass "OIDC issuer matches CP-NK-004: $ISSUER"
else
warn "OIDC issuer is '$ISSUER' (expected https://$KC_HOST)"
fi
else
fail "OIDC discovery endpoint did not return expected JSON"
fi
else
warn "Skipping OIDC discovery check — no running pod"
fi
# ═══════════════════════════════════════════════════════════════════
# Summary
# ═══════════════════════════════════════════════════════════════════
echo ""
echo "════════════════════════════════════════════════════════════"
echo " T05 verification: PASS=$PASS WARN=$WARN FAIL=$FAIL"
echo "════════════════════════════════════════════════════════════"
if [[ "$FAIL" -gt 0 ]]; then
echo " Result: INCOMPLETE — resolve FAIL items before proceeding to T06"
exit 1
elif [[ "$WARN" -gt 0 ]]; then
echo " Result: PARTIAL — T05 core is up; WARN items should be resolved before T06"
exit 0
else
echo " Result: COMPLETE — T05 done-criteria met; proceed to T06 (MFA flow integration)"
exit 0
fi

View file

@ -0,0 +1,7 @@
#!/usr/bin/env bash
# Functional realm/resolver and token-backed MFA proof, NK-WP-0034.
# Usage: ./verify-t06.sh --user platform-root
# Runbook: docs/verify-t06.md (credentials are prompted, not read from old bundles).
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
exec python3 "$SCRIPT_DIR/privacyidea/verify_mfa.py" "$@"

View file

@ -0,0 +1,5 @@
#!/usr/bin/env bash
# Explicit private input and preview/readback adapter; see lldap/GROUP-MAINTENANCE.md.
set +x
set -euo pipefail
exec python3 "$(dirname -- "$0")/lldap/lldap_admin.py" verify "$@"

View file

@ -0,0 +1,175 @@
#!/usr/bin/env bash
# verify-t08.sh — verify NK-WP-0001-T08 done-criteria
#
# Checks backups, DR readiness, and break-glass account.
#
# Sections:
# 1. Backup CronJobs exist (lldap-backup, authelia-backup, privacyidea-backup)
# 2. backup-sa ServiceAccount and RBAC exist
# 3. lldap-backup has run successfully at least once
# 4. authelia-backup has run successfully at least once
# 5. privacyidea-backup has run successfully at least once
# 6. privacyIDEA enckey backup exists on PVC
# 7. LLDAP SQLite backup exists on PVC
# 8. DR-RUNBOOK.md present in repo
# 9. KeePassXC ops bundle (pack-bundle.sh) — manual confirmation required
#
# Usage:
# chmod +x verify-t08.sh
# ./verify-t08.sh
set -euo pipefail
SSO_NAMESPACE="sso"
MFA_NAMESPACE="mfa"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PASS=0
FAIL=0
WARN=0
pass() { echo " [PASS] $1"; PASS=$((PASS + 1)); }
fail() { echo " [FAIL] $1"; FAIL=$((FAIL + 1)); }
warn() { echo " [WARN] $1"; WARN=$((WARN + 1)); }
section() { echo ""; echo "── $1 ──────────────────────────────────────"; }
check_cronjob() {
local name="$1"; local ns="$2"
if kubectl get cronjob "$name" -n "$ns" &>/dev/null; then
pass "CronJob $name exists (namespace: $ns)"
local schedule
schedule=$(kubectl get cronjob "$name" -n "$ns" \
-o jsonpath='{.spec.schedule}' 2>/dev/null || echo "?")
pass " Schedule: $schedule"
else
fail "CronJob $name not found in namespace $ns — apply backup/cronjob-sqlite-backups.yaml"
fi
}
check_last_job() {
local cronjob="$1"; local ns="$2"
# Find the most recent Job spawned by this CronJob
LAST_JOB=$(kubectl get job -n "$ns" \
-l "batch.kubernetes.io/controller-uid" \
--sort-by=.metadata.creationTimestamp \
-o jsonpath='{.items[-1].metadata.name}' 2>/dev/null || echo "")
# Simpler: look for any completed job with the cronjob name prefix
SUCCEEDED=$(kubectl get job -n "$ns" \
-o jsonpath="{.items[?(@.metadata.ownerReferences[0].name==\"$cronjob\")].status.succeeded}" \
2>/dev/null || echo "")
if [[ "$SUCCEEDED" == *"1"* ]]; then
pass "CronJob $cronjob has at least one successful run"
else
warn "CronJob $cronjob has no successful runs yet — trigger manually to test:"
warn " kubectl create job -n $ns --from=cronjob/$cronjob ${cronjob}-manual-test"
fi
}
# ── 1. Backup CronJobs ────────────────────────────────────────────────────────
section "1. Backup CronJobs"
check_cronjob "lldap-backup" "$SSO_NAMESPACE"
check_cronjob "authelia-backup" "$SSO_NAMESPACE"
check_cronjob "privacyidea-backup" "$MFA_NAMESPACE"
# ── 2. RBAC ───────────────────────────────────────────────────────────────────
section "2. Backup ServiceAccount and RBAC (namespace: $SSO_NAMESPACE)"
if kubectl get serviceaccount backup-sa -n "$SSO_NAMESPACE" &>/dev/null; then
pass "ServiceAccount backup-sa exists"
else
fail "ServiceAccount backup-sa not found — apply backup/cronjob-sqlite-backups.yaml"
fi
if kubectl get role backup-scaler -n "$SSO_NAMESPACE" &>/dev/null; then
pass "Role backup-scaler exists"
else
fail "Role backup-scaler not found"
fi
if kubectl get rolebinding backup-sa-scaler -n "$SSO_NAMESPACE" &>/dev/null; then
pass "RoleBinding backup-sa-scaler exists"
else
fail "RoleBinding backup-sa-scaler not found"
fi
# ── 3–5. CronJob run history ──────────────────────────────────────────────────
section "3. lldap-backup run history"
check_last_job "lldap-backup" "$SSO_NAMESPACE"
section "4. authelia-backup run history"
check_last_job "authelia-backup" "$SSO_NAMESPACE"
section "5. privacyidea-backup run history"
check_last_job "privacyidea-backup" "$MFA_NAMESPACE"
# ── 6. privacyIDEA enckey backup on PVC ──────────────────────────────────────
section "6. privacyIDEA enckey backup on PVC"
PI_POD=$(kubectl get pod -n "$MFA_NAMESPACE" \
-l app.kubernetes.io/name=privacyidea \
--field-selector=status.phase=Running \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "")
if [[ -n "$PI_POD" ]]; then
# PI PVC is mounted at /etc/privacyidea (not /data) in the privacyIDEA container
BACKUP_COUNT=$(kubectl exec -n "$MFA_NAMESPACE" "$PI_POD" -- \
sh -c 'ls /etc/privacyidea/backups/enckey.backup.* 2>/dev/null | wc -l' 2>/dev/null || echo "0")
BACKUP_COUNT="${BACKUP_COUNT// /}"
if [[ "$BACKUP_COUNT" -gt 0 ]]; then
pass "privacyIDEA enckey backups found on PVC ($BACKUP_COUNT file(s))"
else
warn "No enckey backup files on PVC yet — trigger privacyidea-backup CronJob to create one"
warn " kubectl create job -n $MFA_NAMESPACE --from=cronjob/privacyidea-backup pi-backup-test"
fi
else
warn "Skipping enckey backup check — no running privacyIDEA pod"
fi
# ── 7. LLDAP SQLite backup on PVC ────────────────────────────────────────────
section "7. LLDAP SQLite backup on PVC"
LLDAP_POD=$(kubectl get pod -n "$SSO_NAMESPACE" \
-l app.kubernetes.io/name=lldap \
--field-selector=status.phase=Running \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "")
if [[ -n "$LLDAP_POD" ]]; then
BACKUP_COUNT=$(kubectl exec -n "$SSO_NAMESPACE" "$LLDAP_POD" -- \
sh -c 'ls /data/backups/users.backup.* 2>/dev/null | wc -l' 2>/dev/null || echo "0")
BACKUP_COUNT="${BACKUP_COUNT// /}"
if [[ "$BACKUP_COUNT" -gt 0 ]]; then
pass "LLDAP SQLite backups found on PVC ($BACKUP_COUNT file(s))"
else
warn "No LLDAP backup files on PVC yet — trigger lldap-backup CronJob to create one"
warn " kubectl create job -n $SSO_NAMESPACE --from=cronjob/lldap-backup lldap-backup-test"
fi
else
warn "Skipping LLDAP backup check — no running LLDAP pod"
fi
# ── 8. DR runbook present ─────────────────────────────────────────────────────
section "8. DR runbook"
RUNBOOK="$SCRIPT_DIR/backup/DR-RUNBOOK.md"
if [[ -f "$RUNBOOK" ]]; then
pass "DR-RUNBOOK.md present at $RUNBOOK"
else
fail "DR-RUNBOOK.md not found — it should be at sso-mfa/k8s/backup/DR-RUNBOOK.md"
fi
# ── 9. Offsite backup (manual confirmation) ───────────────────────────────────
section "9. Offsite backup (manual)"
warn "Cannot verify offsite backup automatically — confirm manually:"
warn " - pack-bundle.sh has been run with current secrets"
warn " - ops-bundle.tar.age stored in a separate physical location"
warn " - age decryption key stored separately (NOT in the same location as the bundle)"
# ── Summary ───────────────────────────────────────────────────────────────────
echo ""
echo "════════════════════════════════════════════════════════════"
echo " T08 verification: PASS=$PASS WARN=$WARN FAIL=$FAIL"
echo "════════════════════════════════════════════════════════════"
if [[ "$FAIL" -gt 0 ]]; then
echo " Result: INCOMPLETE — resolve FAIL items before marking T08 done"
exit 1
elif [[ "$WARN" -gt 0 ]]; then
echo " Result: PARTIAL — structure is in place; resolve WARN items (trigger CronJobs)"
exit 0
else
echo " Result: COMPLETE — T08 done-criteria met; SSO/MFA platform workplan complete!"
exit 0
fi

View file

@ -0,0 +1,124 @@
{
"schema": "nk49.keycape-native-fixture-proposal.v1",
"status": "prepared_not_executed_not_approved",
"source_sha256": {
"sso-mfa/bootstrap/sops-private.py": "sha256:acac68e2936ed38b7d7c9e35eb4619647c99c117a3928a1c5ce704b570906780",
"sso-mfa/k8s/privacyidea/key-custody-remote.py": "sha256:27b2b885c81923d85054e3999c5613e104fe627779b2b50398ed9bed5088f47e",
"tests/run_isolated_keycape_native_fixture.py": "sha256:ecbadecc475622eaaf20a726110aebef5f4fe7af141110a98aca1640179b4a03",
"tests/test_identity_bundle.py": "sha256:be5ce74097594cc7f7e77dbccd6cb53aa72f263cd1bb442ce5464038aed1116c",
"tests/test_isolated_keycape_fixture_plan.py": "sha256:5945619f4c5957187763276cc900870a9f08f9475b63c240694688e0c2eca7dd",
"tests/test_isolated_keycape_recovery.py": "sha256:247e9a87f5be2ee9f20f7edbb4a9f612ab7cb0cd940392c3caeca8b046605f55",
"tests/test_isolated_keycape_session.py": "sha256:cbdf51efde26eff337e9303a2c431f80b333b8846d763eb62853f81eb3f13e2c",
"tools/bootstrap-recovery/capture-identity.py": "sha256:47dca7c4dc96fa7eb9fb02c81fca9ca8dc5281aac918979eb7367148168ba705",
"tools/bootstrap-recovery/check-privacyidea-database.py": "sha256:90f454c87998d7f5d625e3c6656c3418f8f56275dc1879a9d19ed605431b94a4",
"tools/bootstrap-recovery/fetch-privacyidea-backup.py": "sha256:233160e1206643a5cf7ad05141d733f0a75aff651190e27f77515a124ff0a59f",
"tools/bootstrap-recovery/identity-backup-profile.json": "sha256:40f9e5b5bb957e5b23e03b31701702b850502d16a92a0fa0ebf92618cc3f18a1",
"tools/bootstrap-recovery/identity-bundle.py": "sha256:524a03c065db4307fcde83d33af39503f3de19cb6ef3a8ca72c991d4a8c08ee6",
"tools/bootstrap-recovery/identity-credential-setup.py": "sha256:ddd6127fa331c0185523ec3bb22d743a791edd56a2e6af7ad8d7949b7c051141",
"tools/bootstrap-recovery/identity-custody.json": "sha256:f8aee8487ed01d7940b82676bb4a5c8d61f51b6c598521b8b8a73588aaea4d4f",
"tools/bootstrap-recovery/identity-files.py": "sha256:d0063a9eedd3610e3d45bd2d834aae83cff74e4b3504d138b3548f427e89331d",
"tools/bootstrap-recovery/identity-isolation.py": "sha256:88d1e13112a44c79571825af89d3fe5b01fa490cff2653badef4c62a45c844f1",
"tools/bootstrap-recovery/identity-offsite.py": "sha256:e7c31fa9ee081961af873348ee3b4829a645d40c7dc7f1e22c564aa9ed01bc49",
"tools/bootstrap-recovery/identity-retention.py": "sha256:a41ea4759f69c35548149fc8d0d5ace547f16cf9fe4c9dac10b119a4d0becdec",
"tools/bootstrap-recovery/identity-runtime.py": "sha256:a2c0acf567c204705b90be64d561d0fda2285e0fc71a962d6e1b149f571e1940",
"tools/bootstrap-recovery/install-identity-backup.py": "sha256:5c612a2a51c9b59aad8cc01d418c2f563cbd92c62dc1fa8be994e5fb59df90f1",
"tools/bootstrap-recovery/inventory-scaleway.py": "sha256:a219df205708fa4ed44b763a418585d63c820c25434bc0fd14e805eb5c2f79d3",
"tools/bootstrap-recovery/inventory-storage-share.py": "sha256:673f9b17fb2f3fef6fcdb5c50a2bcca69e3df3157a9e5582a2d995c6b87640b5",
"tools/bootstrap-recovery/isolated-keycape-session.py": "sha256:8fa38efe28167b59a6307f342b2ac8fcda78ecda60e6228f22017424d2343c22",
"tools/bootstrap-recovery/isolated-keycape.py": "sha256:f67627b7d1647d75187dd899874efe22383bc91de5d97dabb9801076668ab3c2",
"tools/bootstrap-recovery/isolated-lldap.py": "sha256:35bfa8411186ad936a199bd450c7e18c2ad51059301a6db9650e6bd055ab1b81",
"tools/bootstrap-recovery/isolated-privacyidea.py": "sha256:4df7196678aac1ad594f32abf45c68dc21a9d2760d9e8a536a215a4a3c5d277f",
"tools/bootstrap-recovery/prepare-identity-coverage.py": "sha256:cd5f3067d28b4c7cc93ed08b8cf3869218bdecf7c54b5dffed22d697de71dbb1",
"tools/bootstrap-recovery/recovery-kit.py": "sha256:6fa0f1494dccaf003b6c6396c5de8b36faa2b276eeb16badd04383f5efb71e66",
"tools/bootstrap-recovery/retrieve-identity-offline.py": "sha256:3421dbb3f3280a001d33ea2deb12ab609404a0b4254cd43c21518d77d166f972",
"tools/bootstrap-recovery/retrieve-identity.py": "sha256:ec123d47a07305253570dacab7e103f8c193a23f20c9fc87466ea4a95ae0ec13",
"tools/bootstrap-recovery/run-isolated-keycape.sh": "sha256:0d994c9c14ebdffdbf7879bd7562042531083d0e8955424d0ae099898a141668",
"tools/bootstrap-recovery/verify-bundle.py": "sha256:885b05a2011e711228e42e316bda70bbe0d97d481383d61aa8469d0f10452370",
"tools/bootstrap-recovery/verify-identity-delivery.py": "sha256:63658d1976806f8d0ce80ba8f750bbcfdca59da936b6eb6a78a250f0e53710f1",
"tools/bootstrap-recovery/verify-identity.py": "sha256:039864d0141b552887752bf17cfdcf1bef4bdc9126f946a9c0533e4fd54ebf4f",
"sso-mfa/bootstrap/sops-custody-unlock.sh": "sha256:86c527a8ae0d0cca66a2345c566b25fbff5bc52c1e22c725548c0fbb2f86a58e",
"sso-mfa/bootstrap/sops-custody-fd.py": "sha256:df29eaba83ec6b5fc311664dcf4b22d8f3c8ed7274ed8e6c419ada0dcd169c0f",
"tools/bootstrap-recovery/prepare-isolated-keycape.py": "sha256:5098bdfa4e0ad30627c1716e5e355a2c78dca6ccc607bd2148a2e06126632288",
"tests/test_prepare_isolated_keycape.py": "sha256:4b0871f9a11a3940983464f1b40eca1613110b1cb3cb9694f4891d58e14c700a",
"tests/test_sealed_custody.py": "sha256:f0fa07c89e2419464dbee0489462864a4533770417a554b1ab1d1ed5d7b6c49f",
"tests/test_private_manifest_binding.py": "sha256:2e7ee16af65390343435b7210ef191aa2a10e9dcbb8759ed95fa8bf6fb81893b"
},
"fixed_endpoint": "unix:///var/run/docker.sock",
"engine_id": "74f93783-6cef-43a8-a3c4-f62586cd3bc1",
"engine_name": "bnt-lap001",
"images": [
"forgejo.coulomb.social/coulomb/key-cape@sha256:cdec9dd7c515f7e0a6169fc3fa560ee2cc47bf40d899755dec808c362f5ce010",
"ghcr.io/gpappsoft/privacyidea-docker@sha256:af7841adad262f129e0c1d4f553af13f21cb2f4dc713533f316cfe43ed0b4473"
],
"pull_policy": "never",
"synthetic_issuer": "https://nk49-fixture.invalid",
"cases": [
"positive",
"hard-parent-loss",
"deadline",
"nonzero-after-accept"
],
"limits": {
"sequential_cases": true,
"max_live_service_containers": 2,
"max_tmpfs_volumes": 4,
"max_public_probe_containers": 1,
"service_memory_bytes_each": 536870912,
"service_cpus_each": 1,
"tmpfs_volume_bytes_each": 33554432,
"host_tmpfs_fixture_limit_expected_bytes": 8388608,
"execution_deadline_seconds": {
"deadline_case": 20,
"other_cases": 120
},
"driver_wait_seconds_per_case": 150
},
"resources": {
"container_names": "nk49-kc-<fresh-run-id>-{transport,issuer,image-probe}",
"volume_names": "nk49-kc-<fresh-run-id>-v{0,1,2,3}",
"label": "net-kingdom.recovery-run=<fresh-run-id>",
"host_plaintext": "exact private /dev/shm/nk49-keycape-<run-id>-<suffix> registered before writes",
"synthetic_source_root": "private /dev/shm/nk49-keycape-fixture-<suffix>",
"safe_result": "private /tmp/nk49-keycape-native-fixture-review-<suffix>/result.json"
},
"scope": "candidate itself on complete synthetic format/integrity archive fixture; no native PI/LLDAP/Authelia proof, real custody, real factor or real client credential; successor binds sealed-descriptor custody preparation sources; synthetic fixture itself overrides custody with generated fixture key as before",
"mutations": [
"conditional creation of exact fresh local containers and local-driver tmpfs volumes",
"private synthetic file import to those volumes",
"original-image KeyCape service/client exchange within same loopback-only namespace",
"exact owned quiescent removal and bounded safe receipts"
],
"forbidden": [
"Docker execution before independent approval",
"image pulls/builds/pushes",
"production routes/ports/host networking/host directory mounts",
"OpenBao/Kubernetes/provider APIs",
"actual identity archive, age key, TLS key, user password, factor or client secret"
],
"cleanup": "candidate exact ownership/volume-driver/inode checks; quiesce fresh owner SID/PGID via pidfds first; retained inflight uncertainty never accepted even on absent readback",
"oracles": [
"positive issuer-native verify-client must establish original-hostname HTTPS trust; helper alone never suffices",
"same original single RS256 key-1 JWKS and exact config/registry/policy/history/lock/deliveries",
"correct service exchange and explicit wrong-secret/excess-scope denial through existing native verifier",
"hard-parent-loss and tightened deadline both quiesce before exact resource/plaintext cleanup",
"after real Docker run accepts a synthetic container, inject returncode1; require failed recovery and sticky daemon uncertainty after cleanup"
],
"proposed_command": [
"python3",
"tests/run_isolated_keycape_native_fixture.py",
"--execute-reviewed-fixture",
"--expected-manifest-sha256",
"<SHA256-of-this-file>",
"--case",
"positive"
],
"extended_command": [
"python3",
"tests/run_isolated_keycape_native_fixture.py",
"--execute-reviewed-fixture",
"--expected-manifest-sha256",
"<SHA256-of-this-file>",
"--case",
"all"
]
}

View file

@ -0,0 +1,221 @@
#!/usr/bin/env python3
"""Prepare/run the exact reviewed T18 candidate on disposable synthetic material.
Default --plan performs no Docker calls and generates no key/material. Execution
requires the reviewed fixture manifest hash and source closure. This runner calls
the candidate itself; hooks only pause or inject a post-response transport fault.
"""
import argparse
import base64
from datetime import datetime,timedelta,timezone
import hashlib
import importlib.util
import io
import json
import os
import re
from pathlib import Path
import signal
import subprocess
import sys
import tarfile
import tempfile
import time
import uuid
ROOT=Path(__file__).resolve().parents[1]
MANIFEST=ROOT/'tests/isolated-keycape-native-fixture-manifest.json'
ISSUER='https://nk49-fixture.invalid'
CASES=('positive','hard-parent-loss','deadline','nonzero-after-accept')
def load(name,path):
spec=importlib.util.spec_from_file_location(name,path)
module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module);return module
s=load('session',ROOT/'tools/bootstrap-recovery/isolated-keycape-session.py')
def require(condition):
if not condition:raise RuntimeError('synthetic_fixture_refused')
def reviewed_manifest(expected):
raw=MANIFEST.read_bytes();require(hashlib.sha256(raw).hexdigest()==expected)
manifest=json.loads(raw)
for name,digest in manifest['source_sha256'].items():
require(isinstance(digest,str) and re.fullmatch('sha256:[a-f0-9]{64}',digest))
require('sha256:'+hashlib.sha256((ROOT/name).read_bytes()).hexdigest()==digest)
return manifest
def private_write(path,value):
path.parent.mkdir(mode=0o700,parents=True,exist_ok=True)
fd=os.open(path,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
with os.fdopen(fd,'wb') as out:out.write(value)
def materials(root):
"""Reuse the existing full archive fixture factory, no real archive accepted."""
from cryptography import x509
from cryptography.hazmat.primitives import hashes,serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
import yaml
existing=load('existing_bundle_fixture',ROOT/'tests/test_identity_bundle.py')
payload=existing.stage.__wrapped__(root)
config={'issuer':ISSUER,'port':8080,'privateKeyPem':'/etc/keycape/key.pem','environment':'dev','tokenLifetime':'5m',
'lldap':{'url':'ldap://127.0.0.1:1','bindDN':'cn=fixture','bindPW':'synthetic-only','baseDN':'dc=fixture'},
'authelia':{'baseURL':'http://127.0.0.1:1','clientId':'synthetic','clientSecret':'synthetic','redirectURI':ISSUER+'/authorize/callback'},
'privacyidea':{'baseURL':'http://127.0.0.1:1'},
'clients':[{'clientId':'nk49-fixture-consumer','clientType':'confidential','audience':'nk49-fixture',
'grantTypes':['client_credentials'],'allowedScopes':['fixture:read'],'secretRef':'env:KEYCAPE_FIXTURE_CLIENT_SECRET',
'serviceSubject':'service:nk49-fixture','tenant':'tenant:fixture','roles':['fixture-reader']}]+
[{'clientId':name,'displayName':name,'clientType':'public','grantTypes':['authorization_code'],
'redirectUris':['https://nk49-fixture.invalid/callback'],'allowedScopes':['openid'],'mfaRequired':True}
for name in ('vergabe-demo-company','user-engine-portal')]}
secret_path=payload/'resources/sso/secret/keycape-config.json';secret=json.loads(secret_path.read_text())
secret['data']['config.yaml']=base64.b64encode(yaml.safe_dump(config).encode()).decode();secret_path.write_text(json.dumps(secret))
client_path=payload/'resources/sso/secret/keycape-rapp-qonto-client.json';client=json.loads(client_path.read_text())
client['data']={'client-secret':base64.b64encode(b'synthetic-fixture-client-secret').decode()};client_path.write_text(json.dumps(client))
factor_path=payload/'resources/sso/secret/keycape-factor-read.json';factor=json.loads(factor_path.read_text())
factor['data']={'admin-token':base64.b64encode(b'synthetic-unused-factor-reader').decode()};factor_path.write_text(json.dumps(factor))
dep_path=payload/'resources/sso/deployment/keycape.json';dep=json.loads(dep_path.read_text())
dep['spec']={'template':{'spec':{'containers':[{'name':'keycape','image':s.adapter.IMAGE,'env':[
{'name':'KEYCAPE_CONFIG','value':'/etc/keycape/config.yaml'},
{'name':'KEYCAPE_POLICY_PATH','value':'/var/lib/keycape-policy/policy.json'},
{'name':'KEYCAPE_FIXTURE_CLIENT_SECRET','valueFrom':{'secretKeyRef':{'name':'keycape-rapp-qonto-client','key':'client-secret'}}}]}]}}};dep_path.write_text(json.dumps(dep))
policy={'Revision':1,'Modes':{k:'mandatory' for k in ('vergabe-demo-company','user-engine-portal')},'History':[
{'reference':'synthetic-retained-receipt','actor':'service:fixture','client':'user-engine-portal','before':'mandatory','after':'mandatory','revision':1,'at':1}]}
# JSON tags in the owner store are lowercase; retain an existing lock inode
# representation as part of the archived tree, never synthesize it after import.
policy={k.lower():v for k,v in policy.items()};(payload/'data/keycape/policy.json').write_text(json.dumps(policy))
private_write(payload/'data/keycape/policy.json.lock',b'')
key=rsa.generate_private_key(public_exponent=65537,key_size=2048)
subject=x509.Name([x509.NameAttribute(NameOID.COMMON_NAME,'nk49-fixture.invalid')]);now=datetime.now(timezone.utc)
cert=(x509.CertificateBuilder().subject_name(subject).issuer_name(subject).public_key(key.public_key()).serial_number(x509.random_serial_number())
.not_valid_before(now-timedelta(minutes=5)).not_valid_after(now+timedelta(days=1))
.add_extension(x509.SubjectAlternativeName([x509.DNSName('nk49-fixture.invalid')]),critical=False)
.add_extension(x509.BasicConstraints(ca=True,path_length=None),critical=True).sign(key,hashes.SHA256()))
tls={'/recovery-tls/server.pem':cert.public_bytes(serialization.Encoding.PEM),
'/recovery-tls/server-key.pem':key.private_bytes(serialization.Encoding.PEM,serialization.PrivateFormat.PKCS8,serialization.NoEncryption()),
'/recovery-tls/trust.pem':cert.public_bytes(serialization.Encoding.PEM)}
for path,value in tls.items():private_write(root/Path(path).name,value)
age=root/'synthetic-age.key';s.run(['age-keygen','-o',str(age)])
recipient=s.run(['age-keygen','-y',str(age)]).decode().strip();profile=s.bundle.profile();profile['public_recipient']=recipient
s.bundle.profile=lambda:profile
artifact=root/'net-kingdom-identity-20261016T063159Z-0123456789ab.tar.age';s.bundle.seal(payload,artifact,{'synthetic_native_keycape_fixture':True})
raw=s.run(['age','-d','-i',str(age),str(artifact)],max_output=s.bundle.LIMIT)
with tarfile.open(fileobj=io.BytesIO(raw)) as archive:manifest=json.load(archive.extractfile('manifest.json'))
transfer={'schema':'netkingdom.identity-offsite.v1','status':'readback_verified','provider':'Storage Share',
'folder':'Backup/railiance-backups/net-kingdom-identity','artifact':artifact.name,'ciphertext_bytes':artifact.stat().st_size,
'ciphertext_sha256':hashlib.sha256(artifact.read_bytes()).hexdigest(),'etag':'"synthetic"','fetched_path':str(artifact),
'independent_owner_retrieval_verified':True}
# Derive public executable identity only from the exact cached admitted image.
probe=s.plan_session();name=probe['public_image_probe']
try:
s.run(['docker','create','--pull=never','--name',name,'--network','none','--read-only','--label',s.adapter.LABEL+'='+probe['run_id'],s.adapter.IMAGE])
binary=s.adapter.container_file(name,'/keycape')
finally:s.adapter.cleanup({**probe,'containers':[name]}, {})
admission={'issuer':ISSUER,'ciphertext_sha256':transfer['ciphertext_sha256'],
'manifest_sha256':hashlib.sha256(json.dumps(manifest,sort_keys=True,separators=(',',':')).encode()).hexdigest(),
'verifier_sha256':hashlib.sha256(binary).hexdigest(),'trust_sha256':hashlib.sha256(tls['/recovery-tls/trust.pem']).hexdigest(),
'tls_cert_sha256':hashlib.sha256(tls['/recovery-tls/server.pem']).hexdigest(),
'consumer':{'client_id':'nk49-fixture-consumer','secret_env':'KEYCAPE_FIXTURE_CLIENT_SECRET','audience':'nk49-fixture','scope':'fixture:read',
'subject':'service:nk49-fixture','tenant':'tenant:fixture','roles':'fixture-reader','deny_scope':'fixture:write'}}
private_write(root/'fixture.json',json.dumps({'schema':'nk49.generated-keycape-fixture.v1','transfer':transfer,'admission':admission,'recipient':recipient}).encode())
def worker(root,case):
execution=s.isolate_execution();execution['deadline']=time.monotonic()+(20 if case=='deadline' else 120)
data=json.loads((root/'fixture.json').read_text());require(data['schema']=='nk49.generated-keycape-fixture.v1')
profile=s.bundle.profile();profile['public_recipient']=data['recipient'];s.bundle.profile=lambda:profile
s.local.verify.custody.private_key_fd=lambda:os.open(root/'synthetic-age.key',os.O_RDONLY|os.O_NOFOLLOW)
tls={'/recovery-tls/'+name:(root/name).read_bytes() for name in ('server.pem','server-key.pem','trust.pem')}
receipt={};output=root/(case+'.json');checkpoint=lambda r:output.write_text(json.dumps(r))
provision=s.provision
def paused(*args):
provision(*args)
if case in ('hard-parent-loss','deadline'):
private_write(root/(case+'.barrier'),b'private-import-and-default-launch-complete')
time.sleep(150)
s.provision=paused
if case=='nonzero-after-accept':
original=s.adapter.subprocess.run;injected=False
def fault(argv,**kwargs):
nonlocal injected
result=original(argv,**kwargs)
if argv[:3]==['docker','--host',s.adapter.ENDPOINT] and 'run' in argv and not injected and result.returncode==0:
injected=True
return subprocess.CompletedProcess(argv,1,b'',b'')
return result
s.adapter.subprocess.run=fault
try:s.execute(data['transfer'],data['admission'],tls,receipt,execution,checkpoint=checkpoint)
except BaseException:receipt['status']='failed'
finally:output.write_text(json.dumps(receipt))
return int(receipt.get('status')!='isolated_keycape_native_registration_checked')
def run_cases(root,summary,cases):
for case in cases:
child=subprocess.Popen([sys.executable,str(Path(__file__).resolve()),'--worker-root',str(root),'--worker-case',case,
'--execute-reviewed-fixture','--expected-manifest-sha256',summary['manifest_sha256']],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
try:
end=time.monotonic()+150
if case=='hard-parent-loss':
while not (root/(case+'.barrier')).exists() and child.poll() is None and time.monotonic()<end:time.sleep(.05)
require((root/(case+'.barrier')).exists())
execution=json.loads((root/(case+'.json')).read_text())['execution_binding'];require(execution['pid']==child.pid)
fd=os.pidfd_open(child.pid)
try:signal.pidfd_send_signal(fd,signal.SIGKILL)
finally:os.close(fd)
child.wait(timeout=max(1,end-time.monotonic()))
receipt=json.loads((root/(case+'.json')).read_text())
summary['receipts'][case]=receipt
if case=='deadline':require((root/(case+'.barrier')).exists())
if case=='positive':
require(receipt.get('status')=='isolated_keycape_native_registration_checked' and receipt.get('container_cleanup_verified') and receipt.get('temporary_plaintext_cleanup_verified'))
require(receipt.get('native_service_registration_verified') and receipt.get('original_public_signing_key_in_live_jwks'))
summary['actual_issuer_original_hostname_tls_verified']=True
else:
require(receipt.get('status')!='isolated_keycape_native_registration_checked')
outcome=Path(receipt['reaper_outcome_path'])
while not outcome.exists() and time.monotonic()<end:time.sleep(.05)
require(outcome.exists());cleanup=json.loads(outcome.read_text())
require(cleanup.get('execution_quiescence_verified') and cleanup.get('temporary_plaintext_cleanup_verified'))
if case=='nonzero-after-accept':require(cleanup.get('status')=='bounded_cleanup_uncertain' and cleanup.get('in_flight_daemon_request_uncertain'))
else:require(cleanup.get('status')=='bounded_cleanup_verified')
summary['cases'][case]='passed'
finally:
if child.poll() is None:
fd=os.pidfd_open(child.pid)
try:signal.pidfd_send_signal(fd,signal.SIGKILL)
finally:os.close(fd)
child.wait(timeout=5)
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--plan',action='store_true')
p.add_argument('--execute-reviewed-fixture',action='store_true');p.add_argument('--expected-manifest-sha256')
p.add_argument('--worker-root');p.add_argument('--worker-case',choices=CASES);p.add_argument('--case',choices=(*CASES,'all'),default='positive');args=p.parse_args()
if not args.execute_reviewed_fixture:
print(MANIFEST.read_text());return 0
require(not args.plan);manifest=reviewed_manifest(args.expected_manifest_sha256);os.umask(0o077)
if args.worker_root:
root=Path(args.worker_root);require(root.parent==Path('/dev/shm') and root.name.startswith('nk49-keycape-fixture-'))
s.bundle.private(root,True);return worker(root,args.worker_case)
require(not args.worker_case)
s.local.preflight();s.run(['docker','image','inspect',s.adapter.IMAGE,'--format','{{.Id}}'])
safe=Path(tempfile.mkdtemp(prefix='nk49-keycape-native-fixture-review-',dir='/tmp'))
summary={'schema':'nk49.synthetic-keycape-native-fixture.v1','status':'failed','manifest_sha256':args.expected_manifest_sha256,
'cases':{},'receipts':{},'actual_credentials_used':False,'actual_recovery_proven':False,'docker_executed':True}
print('Safe synthetic fixture result: '+str(safe/'result.json'),flush=True)
try:
with tempfile.TemporaryDirectory(prefix='nk49-keycape-fixture-',dir='/dev/shm') as temporary:
root=Path(temporary);materials(root);run_cases(root,summary,CASES if args.case=='all' else (args.case,))
summary['status']='synthetic_native_fixture_passed'
finally:(safe/'result.json').write_text(json.dumps(summary,indent=2)+'\n')
print('Safe synthetic fixture result: '+str(safe/'result.json'))
return int(summary['status']!='synthetic_native_fixture_passed')
if __name__=='__main__':raise SystemExit(main())

View file

@ -0,0 +1,178 @@
"""Disposable age keys only; subprocess logs are never asserted with raw values."""
import os
from pathlib import Path
import subprocess
import pytest
ROOT = Path(__file__).resolve().parents[1]
BOOT = ROOT / 'sso-mfa/bootstrap'
@pytest.fixture
def custody(tmp_path):
home = tmp_path / 'home'; home.mkdir()
key = tmp_path / 'disposable.key'
subprocess.run(['age-keygen', '-o', str(key)], capture_output=True, check=True)
public = subprocess.check_output(['age-keygen', '-y', str(key)], text=True).strip()
env = {'PATH': os.environ['PATH'], 'HOME': str(home), 'TMPDIR': str(tmp_path)}
return key, public, env
def unlock(custody, *args):
_, public, env = custody
return subprocess.run(['bash', str(BOOT / 'sops-custody-unlock.sh'), '--expected-recipient', public, *args],
env=env, capture_output=True, text=True, timeout=10)
def test_custody_unknown_arguments_are_not_echoed(custody):
result = unlock(custody, 'synthetic-secret-argument')
assert result.returncode != 0
assert 'synthetic-secret-argument' not in result.stdout + result.stderr
def test_signal_preserves_failure_status_and_removes_temporary_key(custody):
key, _, env = custody
result = unlock(custody, '--from-file', str(key), '--', 'sh', '-c', 'kill -TERM "$PPID"; exit 0')
assert result.returncode == 143
assert not list(Path(env['TMPDIR']).glob('netkingdom-sops-age.*'))
def test_ambiguous_custody_sources_are_refused_before_child(custody, tmp_path):
key, _, _ = custody
marker = tmp_path / 'child-called'
result = unlock(custody, '--from-file', str(key), '--from-file', str(key), '--', 'touch', str(marker))
assert result.returncode != 0
assert not marker.exists()
def test_explicit_custody_check_cleans_key(custody):
key, _, env = custody
result = unlock(custody, '--from-file', str(key), '--check-only')
assert result.returncode == 0
assert not list(Path(env['TMPDIR']).glob('netkingdom-sops-age.*'))
def test_prompt_does_not_leak_when_shell_tracing_is_requested(custody):
import pty, select, time
key, public, env = custody
private_line = next(line for line in key.read_bytes().splitlines() if line.startswith(b'AGE-SECRET-KEY-'))
master, slave = pty.openpty()
child = subprocess.Popen(['bash', '-x', str(BOOT / 'sops-custody-unlock.sh'),
'--expected-recipient', public, '--prompt', '--check-only'],
stdin=slave, stdout=slave, stderr=slave, env=env)
os.close(slave)
captured = bytearray(); sent = False
deadline = time.monotonic() + 10
try:
while time.monotonic() < deadline:
if select.select([master], [], [], 0.05)[0]:
try: chunk = os.read(master, 65536)
except OSError: break
if not chunk: break
captured.extend(chunk)
if b'input hidden): ' in captured and not sent:
os.write(master, private_line + b'\n'); sent = True
assert child.wait(timeout=2) == 0
assert bool(private_line in captured) is False
assert not list(Path(env['TMPDIR']).glob('netkingdom-sops-age.*'))
finally:
if child.poll() is None: child.kill(); child.wait()
os.close(master)
def encrypt_fixture(tmp_path, public):
secrets = tmp_path / 'secrets'; (secrets / 'service').mkdir(parents=True)
(secrets / 'service/one.env').write_text('TEST_VALUE=synthetic-private-payload\n')
out = tmp_path / 'secrets.enc/service'; out.mkdir(parents=True)
subprocess.run(['age', '-r', public, '-o', str(out / 'one.env.age'), str(secrets / 'service/one.env')],
capture_output=True, check=True)
return secrets, out
def test_encryption_refuses_private_key_as_recipient(custody, tmp_path):
key, public, env = custody
secrets, _ = encrypt_fixture(tmp_path, public)
result = subprocess.run(['bash', str(BOOT / 'encrypt-secrets.sh'), str(secrets), str(key), '--no-shred'],
env=env, capture_output=True, text=True, timeout=10)
assert result.returncode != 0
assert 'public recipient' in result.stderr
assert (secrets / 'service/one.env').exists()
def test_decryption_does_not_fall_back_to_home_key(custody, tmp_path):
key, public, env = custody
encrypt_fixture(tmp_path, public)
home_key = Path(env['HOME']) / '.config/net-kingdom/age.key'
home_key.parent.mkdir(parents=True); home_key.write_bytes(key.read_bytes())
output = tmp_path / 'restored'
result = subprocess.run(['bash', str(BOOT / 'decrypt-secrets.sh'), str(output)],
env=env, capture_output=True, text=True, timeout=10)
assert result.returncode != 0
assert not output.exists()
def test_decryption_failure_removes_partial_plaintext(custody, tmp_path):
key, public, env = custody
_, encrypted = encrypt_fixture(tmp_path, public)
(encrypted / 'two.env.age').write_bytes(b'not-a-valid-ciphertext')
output = tmp_path / 'restored'
result = subprocess.run(['bash', str(BOOT / 'decrypt-secrets.sh'), str(output), str(key)],
env=env, capture_output=True, text=True, timeout=10)
assert result.returncode != 0
assert not output.exists()
assert not list(tmp_path.glob('.netkingdom-decrypt.*'))
def test_explicit_decryption_has_private_modes_and_no_output(custody, tmp_path):
key, public, env = custody
original, _ = encrypt_fixture(tmp_path, public)
output = tmp_path / 'restored'
result = subprocess.run(['bash', str(BOOT / 'decrypt-secrets.sh'), str(output), str(key)],
env=env, capture_output=True, text=True, timeout=10)
assert result.returncode == 0
restored = output / 'service/one.env'
assert restored.read_bytes() == (original / 'service/one.env').read_bytes()
assert output.stat().st_mode & 0o077 == 0
assert restored.stat().st_mode & 0o077 == 0
assert 'synthetic-private-payload' not in result.stdout + result.stderr
def test_default_encryption_uses_registered_public_file(custody, tmp_path):
import shutil
_, public, env = custody
checkout = tmp_path / 'fixture-repo'
script_dir = checkout / 'sso-mfa/bootstrap'; script_dir.mkdir(parents=True)
shutil.copyfile(BOOT / 'encrypt-secrets.sh', script_dir / 'encrypt-secrets.sh')
(checkout / 'keys').mkdir(); (checkout / 'keys/age.pub').write_text(public + '\n')
secrets = tmp_path / 'plaintext'; (secrets / 'service').mkdir(parents=True)
(secrets / 'service/test.env').write_text('TEST=synthetic-private-payload\n')
result = subprocess.run(['bash', str(script_dir / 'encrypt-secrets.sh'), str(secrets)],
env=env, capture_output=True, text=True, timeout=10)
assert result.returncode == 0
assert not secrets.exists()
assert (tmp_path / 'secrets.enc/service/test.env.age').is_file()
assert not (Path(env['HOME']) / '.config').exists()
def test_custody_child_does_not_inherit_competing_inline_key(custody):
key, public, env = custody
env = {**env, 'SOPS_AGE_KEY': 'synthetic-competing-key', 'SOPS_AGE_KEY_CMD': 'false'}
result = unlock((key, public, env), '--from-file', str(key), '--', 'sh', '-c',
'test -z "${SOPS_AGE_KEY+x}" && test -z "${SOPS_AGE_KEY_CMD+x}" && test -f "$SOPS_AGE_KEY_FILE"')
assert result.returncode == 0
@pytest.mark.parametrize('kind', ['file', 'directory', 'dangling-symlink'])
def test_decryption_never_overwrites_existing_output(custody, tmp_path, kind):
key, public, env = custody
encrypt_fixture(tmp_path, public)
output = tmp_path / 'restored'
if kind == 'file': output.write_bytes(b'existing-output')
elif kind == 'directory': output.mkdir(); (output / 'existing').write_bytes(b'existing-output')
else: output.symlink_to(tmp_path / 'uncreated')
result = subprocess.run(['bash', str(BOOT / 'decrypt-secrets.sh'), str(output), str(key)],
env=env, capture_output=True, text=True, timeout=10)
assert result.returncode != 0
if kind == 'file': assert output.read_bytes() == b'existing-output'
elif kind == 'directory': assert (output / 'existing').read_bytes() == b'existing-output'
else: assert output.is_symlink(); assert not (tmp_path / 'uncreated').exists()

View file

@ -0,0 +1,225 @@
"""Actual SQLite/age/RSA fixtures and hostile archive/custody boundaries."""
import base64
import importlib.util
import io
import json
import os
from pathlib import Path
import sqlite3
import subprocess
import tarfile
import time
import uuid
from copy import deepcopy
import pytest
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
ROOT=Path(__file__).resolve().parents[1]
def load(name):
spec=importlib.util.spec_from_file_location(name,ROOT/f'tools/bootstrap-recovery/{name}.py')
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m);return m
b=load('identity-bundle');files=load('identity-files');capture=load('capture-identity')
@pytest.fixture
def stage(tmp_path):
os.umask(0o077);root=tmp_path/'payload';root.mkdir(mode=0o700)
key=rsa.generate_private_key(public_exponent=65537,key_size=2048)
private=key.private_bytes(serialization.Encoding.PEM,serialization.PrivateFormat.TraditionalOpenSSL,serialization.NoEncryption())
public=key.public_key().public_bytes(serialization.Encoding.PEM,serialization.PublicFormat.SubjectPublicKeyInfo)
for c in b.profile()['components']:
ns=c['namespace'];name=c['deployment']
d={'kind':'Deployment','metadata':{'name':name,'namespace':ns,'uid':c['deployment_uid']},'spec':{}}
b.save(root,f'resources/{ns}/deployment/{name}.json',json.dumps(d).encode())
for name in c['secret_refs']:
data={'fixture':base64.b64encode(b'synthetic-private-value-canary').decode()}
if name=='keycape-config':data={'key.pem':base64.b64encode(private).decode(),'config.yaml':base64.b64encode(b'issuer: https://isolated.invalid').decode()}
b.save(root,f'resources/{ns}/secret/{name}.json',json.dumps({'kind':'Secret','metadata':{'name':name,'namespace':ns},'data':data}).encode())
for name in c['configmap_refs']:
b.save(root,f'resources/{ns}/configmap/{name}.json',json.dumps({'kind':'ConfigMap','metadata':{'name':name,'namespace':ns},'data':{'fixture':'synthetic'}}).encode())
for component,name in [('lldap','users.db'),('authelia','db.sqlite3')]:
path=root/f'data/{component}/{name}';path.parent.mkdir(mode=0o700,parents=True)
with sqlite3.connect(path) as db:db.execute('CREATE TABLE retained(value TEXT)');db.execute("INSERT INTO retained VALUES ('fixture-preserved')")
path.chmod(0o600)
b.save(root,'data/keycape/policy.json',b'{"version":1}')
for name,value in [('enckey',os.urandom(96)),('private.pem',private),('public.pem',public)]:
b.save(root,'data/privacyidea/'+name,value)
b.save(root,'databases/privacyidea.dump',b'PGDMP-synthetic-format-only')
return root
def test_complete_bundle_has_native_integrity_checks(stage):
r=b.checks(stage)
assert r['sqlite_integrity_passed'] and r['privacyidea_key_format_and_pair_passed']
assert r['native_identity_recovery_proven'] is False
def test_real_age_seal_manifest_is_encrypted_and_plaintext_never_prints(stage,tmp_path,monkeypatch,capsys):
key=tmp_path/'fixture-age.key'
subprocess.run(['age-keygen','-o',str(key)],check=True,capture_output=True)
recipient=subprocess.run(['age-keygen','-y',str(key)],check=True,capture_output=True).stdout.decode().strip()
p=b.profile();p['public_recipient']=recipient;monkeypatch.setattr(b,'profile',lambda:p)
artifact=tmp_path/'identity.tar.age';r=b.seal(stage,artifact,{'test_only':True})
assert artifact.stat().st_mode&0o077==0
assert b'synthetic-private-value-canary' not in artifact.read_bytes()
result=subprocess.run(['age','-d','-i',str(key),str(artifact)],check=True,capture_output=True)
with tarfile.open(fileobj=io.BytesIO(result.stdout)) as archive:
manifest=json.load(archive.extractfile('manifest.json'))
assert len(manifest['files'])==r['file_count']
assert 'data/privacyidea/enckey' in archive.getnames()
assert 'data/lldap/users.db' in archive.getnames()
plain=tmp_path/'decrypted.tar';plain.write_bytes(result.stdout)
recovered=tmp_path/'recovered';recovered.mkdir(mode=0o700)
restored,checks=b.restore(plain,recovered)
assert checks['privacyidea_key_format_and_pair_passed']
assert (recovered/'data/privacyidea/enckey').read_bytes()==(stage/'data/privacyidea/enckey').read_bytes()
assert 'synthetic-private-value-canary' not in json.dumps(r)+capsys.readouterr().out
with pytest.raises(b.Refused):b.seal(stage,artifact,{})
@pytest.mark.parametrize('defect',['missing-key','bad-sqlite','bad-audit-pair','wrong-deployment','private-age-key','symlink','hardlink','unknown-payload'])
def test_incomplete_unsafe_and_mismatched_inputs_refuse(stage,tmp_path,defect):
if defect=='missing-key':(stage/'data/privacyidea/enckey').unlink()
if defect=='bad-sqlite':(stage/'data/lldap/users.db').write_bytes(b'broken')
if defect=='bad-audit-pair':
key=rsa.generate_private_key(public_exponent=65537,key_size=2048)
(stage/'data/privacyidea/public.pem').write_bytes(key.public_key().public_bytes(serialization.Encoding.PEM,serialization.PublicFormat.SubjectPublicKeyInfo))
if defect=='wrong-deployment':
p=stage/'resources/sso/deployment/keycape.json';r=json.loads(p.read_text());r['metadata']['uid']='replacement';p.write_text(json.dumps(r))
if defect=='private-age-key':(stage/'data/keycape/poison').write_bytes(b'AGE-SECRET-KEY-' + b'fixture')
if defect=='symlink':(stage/'data/keycape/linked').symlink_to(stage/'data/privacyidea/enckey')
if defect=='hardlink':os.link(stage/'data/privacyidea/enckey',stage/'data/keycape/linked')
if defect=='unknown-payload':(stage/'unreviewed').write_bytes(b'not-allowed')
with pytest.raises(Exception):
b.checks(stage);b.inventory(stage)
@pytest.mark.parametrize('name,kind',[('../escape','file'),('/absolute','file'),('data/keycape/valid','symlink')])
def test_tar_import_refuses_traversal_and_links(tmp_path,name,kind):
raw=io.BytesIO()
with tarfile.open(fileobj=raw,mode='w') as archive:
member=tarfile.TarInfo(name);member.size=1
if kind=='symlink':member.type=tarfile.SYMTYPE;member.linkname='/etc/passwd'
archive.addfile(member,io.BytesIO(b'x'))
with pytest.raises(b.Refused):b.import_files(raw.getvalue(),tmp_path,'sso')
def test_native_sqlite_snapshot_keeps_committed_wal_rows_and_source_unchanged(tmp_path):
root=tmp_path/'sources';root.mkdir();scratch=tmp_path/'scratch';scratch.mkdir()
for component in ['lldap','authelia','keycape']:(root/component).mkdir()
for component,name in [('lldap','users.db'),('authelia','db.sqlite3')]:
with sqlite3.connect(root/component/name) as con:
con.execute('CREATE TABLE retained(value TEXT)');con.execute("INSERT INTO retained VALUES ('native-snapshot')")
(root/'keycape/policy.json').write_text('{}')
live=sqlite3.connect(root/'lldap/users.db');live.execute('PRAGMA journal_mode=WAL')
live.execute("INSERT INTO retained VALUES ('committed-wal')");live.commit()
try:
result=files.capture('sso',root,scratch)
restored=tmp_path/'restored.sqlite';restored.write_bytes(result['data/lldap/users.db'])
with sqlite3.connect(restored) as con:assert con.execute('SELECT count(*) FROM retained').fetchone()==(2,)
assert list(scratch.iterdir())==[]
assert not any(name.endswith(('-wal','-shm')) for name in result)
finally:live.close()
def test_helper_manifest_has_no_writable_service_mount_or_credentials():
objects=capture.manifests('fixture')
for obj in objects:
if obj['kind']=='NetworkPolicy':
assert obj['spec']['egress']==[] and obj['spec']['ingress']==[]
else:
s=obj['spec'];assert s['automountServiceAccountToken'] is False
assert 'fsGroup' not in s['securityContext']
assert s['activeDeadlineSeconds']==900
assert all(v['persistentVolumeClaim']['readOnly'] for v in s['volumes'] if 'persistentVolumeClaim' in v)
assert not any('secret' in v for v in s['volumes'])
def test_cleanup_delete_carries_exact_uid_precondition(monkeypatch):
calls=[];monkeypatch.setattr(capture,'command',lambda args,payload=None,**kw:calls.append((args,payload)) or b'')
capture.delete_created({'kind':'Pod','metadata':{'namespace':'sso','name':'nk49-capture-fixture','uid':'exact-created-uid'}})
assert json.loads(calls[0][1])['preconditions']=={'uid':'exact-created-uid'}
@pytest.mark.parametrize('defect',['writable-mount','extra-container','service-token','different-image','host-network'])
def test_mutated_helper_is_refused_before_private_exec(defect):
wanted=next(o for o in capture.manifests('fixture') if o['kind']=='Pod')
actual=deepcopy(wanted);spec=actual['spec']
if defect=='writable-mount':spec['containers'][0]['volumeMounts'][0]['readOnly']=False
if defect=='extra-container':spec['containers'].append(deepcopy(spec['containers'][0]))
if defect=='service-token':spec['automountServiceAccountToken']=True
if defect=='different-image':spec['containers'][0]['image']='unreviewed:latest'
if defect=='host-network':spec['hostNetwork']=True
with pytest.raises(capture.Refused):capture.validate_helper(actual,wanted)
def test_namespace_dns_allow_is_excluded_only_for_capture_helpers():
selector={'matchExpressions':[{'key':'net-kingdom/capture','operator':'DoesNotExist'}]}
assert capture.selects(selector,{'app.kubernetes.io/name':'authelia'})
assert not capture.selects(selector,{'net-kingdom/capture':'fixture'})
def test_unknown_create_outcome_is_checkpointed_and_never_claimed_clean(tmp_path,monkeypatch):
monkeypatch.setattr(capture.bundle,'profile',lambda:{'components':[]})
monkeypatch.setattr(capture,'command',lambda *args,**kwargs:(_ for _ in ()).throw(TimeoutError()))
r={'started_at':'fixture'};checkpoints=[]
p={'run_id':'fixture','create_resources':[capture.manifests('fixture')[0]]}
with pytest.raises(capture.Refused):capture.capture(p,tmp_path/'unused.age',r,'fixture',lambda:checkpoints.append(deepcopy(r)))
assert r['helper_cleanup_verified'] is False and r['unverified_create_outcomes']
assert any(c.get('unverified_create_outcomes') for c in checkpoints)
@pytest.mark.skipif(os.environ.get('NK49_RUN_DISPOSABLE_POSTGRES_TEST')!='1',reason='explicit local Docker fixture opt-in')
def test_full_new_archive_decrypt_extract_native_database_restore_and_cleanup(stage,tmp_path,monkeypatch):
verify=load('verify-identity');fixture='nk49-bundle-fixture-'+uuid.uuid4().hex
def command(args,**kwargs):
return subprocess.run(args,check=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE,timeout=120,**kwargs).stdout
try:
command(['docker','run','--detach','--pull=never','--name',fixture,'--network','none',
'--tmpfs','/var/lib/postgresql/data:rw,size=268435456','--env','POSTGRES_HOST_AUTH_METHOD=trust',
verify.pg.IMAGE,'postgres','-c','listen_addresses='])
for _ in range(60):
probe=subprocess.run(['docker','exec',fixture,'pg_isready','-U','postgres'],capture_output=True)
if probe.returncode==0:break
time.sleep(1)
command(['docker','exec','-i',fixture,'psql','-X','-U','postgres','-v','ON_ERROR_STOP=1'],
input=b'CREATE TABLE token(id integer); INSERT INTO token VALUES (1),(2); CREATE TABLE resolver(id integer); CREATE TABLE realm(id integer);')
(stage/'databases/privacyidea.dump').write_bytes(command(['docker','exec',fixture,'pg_dump','-U','postgres','-Fc','postgres']))
key=tmp_path/'disposable-age.key';command(['age-keygen','-o',str(key)])
recipient=command(['age-keygen','-y',str(key)]).decode().strip()
p=b.profile();p['public_recipient']=recipient
monkeypatch.setattr(b,'profile',lambda:p);monkeypatch.setattr(verify.bundle,'profile',lambda:p)
artifact=tmp_path/'net-kingdom-identity-20261016T000000Z-0123456789ab.tar.age'
c=b.seal(stage,artifact,{'test_only':True})
transfer={'schema':'netkingdom.identity-offsite.v1','status':'readback_verified',
'independent_owner_retrieval_verified':True,'fetched_path':str(artifact),'artifact':artifact.name,
'ciphertext_sha256':c['ciphertext_sha256'],'ciphertext_bytes':c['ciphertext_bytes']}
r={};fd=os.open(key,os.O_RDONLY)
try:verify.verify(transfer,fd,r)
finally:os.close(fd)
assert r['status']=='identity_archive_verified'
assert r['database_restore']['aggregate_row_counts']['token']==2
assert r['all_manifest_hashes_verified'] and r['wrong_key_refused']
assert r['container_cleanup_verified'] and r['temporary_plaintext_cleanup_verified']
assert not r['full_service_authentication_proven']
finally:subprocess.run(['docker','rm','--force','--volumes',fixture],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,timeout=30)
def test_runtime_host_requires_owner_and_exact_machine_binding(monkeypatch):
import hashlib,socket
module=load('identity-bundle')
current=module.profile();current['runtime_host']={'hostname':'verified-node','machine_id_sha256':hashlib.sha256(b'fixture-machine').hexdigest()}
monkeypatch.setattr(module,'profile',lambda:current)
monkeypatch.setattr(module.os,'geteuid',lambda:0);monkeypatch.setattr(socket,'gethostname',lambda:'verified-node')
original=Path.read_bytes
monkeypatch.setattr(Path,'read_bytes',lambda p:b'fixture-machine' if str(p)=='/etc/machine-id' else original(p))
module.require_runtime_host()
monkeypatch.setattr(socket,'gethostname',lambda:'foreign-node')
with pytest.raises(module.Refused):module.require_runtime_host()
monkeypatch.setattr(socket,'gethostname',lambda:'verified-node');monkeypatch.setattr(module.os,'geteuid',lambda:1000)
with pytest.raises(module.Refused):module.require_runtime_host()

View file

@ -0,0 +1,49 @@
import contextlib
import hashlib
import importlib.util
import io
import json
from pathlib import Path
import sys
import tempfile
import unittest
from unittest.mock import patch
ROOT=Path(__file__).resolve().parents[1]
spec=importlib.util.spec_from_file_location('fixture_runner',ROOT/'tests/run_isolated_keycape_native_fixture.py')
f=importlib.util.module_from_spec(spec);spec.loader.exec_module(f)
class FixturePreparationTests(unittest.TestCase):
def test_default_plan_neither_runs_docker_nor_generates_materials(self):
output=io.StringIO()
with patch.object(sys,'argv',['fixture','--plan']),patch.object(f.s,'run') as run,patch.object(f,'materials') as materials,patch.object(f.s.local,'preflight') as preflight,contextlib.redirect_stdout(output):
self.assertEqual(f.main(),0)
run.assert_not_called();materials.assert_not_called();preflight.assert_not_called()
manifest=json.loads(output.getvalue())
self.assertEqual(manifest['status'],'prepared_not_executed_not_approved')
self.assertEqual(manifest['fixed_endpoint'],'unix:///var/run/docker.sock')
def test_wrong_reviewed_manifest_hash_stops_before_preflight_or_generation(self):
with patch.object(sys,'argv',['fixture','--execute-reviewed-fixture','--expected-manifest-sha256','0'*64]),patch.object(f.s,'run') as run,patch.object(f,'materials') as materials,patch.object(f.s.local,'preflight') as preflight:
with self.assertRaises(RuntimeError):f.main()
run.assert_not_called();materials.assert_not_called();preflight.assert_not_called()
def test_untyped_or_malformed_public_digest_refused(self):
for invalid in ('0'*64, 'sha256:'+'A'*64, 'sha256:'+'0'*63, None):
with self.subTest(invalid_type=type(invalid).__name__), tempfile.TemporaryDirectory() as temporary:
manifest=json.loads(f.MANIFEST.read_text())
first=next(iter(manifest['source_sha256']))
manifest['source_sha256'][first]=invalid
path=Path(temporary)/'manifest.json'
path.write_text(json.dumps(manifest))
with patch.object(f,'MANIFEST',path), patch.object(f.s,'run') as run:
with self.assertRaises(RuntimeError):
f.reviewed_manifest(hashlib.sha256(path.read_bytes()).hexdigest())
run.assert_not_called()
def test_manifest_binds_current_candidate_and_existing_fixture_factory(self):
digest=hashlib.sha256(f.MANIFEST.read_bytes()).hexdigest()
manifest=f.reviewed_manifest(digest)
self.assertIn('tests/test_identity_bundle.py',manifest['source_sha256'])
self.assertIn('tools/bootstrap-recovery/isolated-keycape-session.py',manifest['source_sha256'])
self.assertIn('sso-mfa/k8s/privacyidea/key-custody-remote.py',manifest['source_sha256'])
if __name__=='__main__':unittest.main()

View file

@ -0,0 +1,234 @@
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 PublicReadinessTransportTests(unittest.TestCase):
def test_provider_probe_failure_is_closed_false_data_not_failed_docker_exec(self):
output=io.StringIO()
with patch('sys.stdin',io.StringIO('{}')),patch('sys.stdout',output):
exec(compile(a.JWKS_TRANSPORT,'<public-readiness>','exec',optimize=2),{})
self.assertEqual(json.loads(output.getvalue()),{'original_public_signing_key_in_live_jwks':False})
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,mounts=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':mounts or [],'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_image_declared_anonymous_disk_volume_remains_refused(self):
with self.assertRaises(a.Refused):
self.inspect(mounts=[{'Type':'volume','Name':'anonymous-fixture','Destination':'/privacyidea/etc/persistent','RW':True}])
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,292 @@
import base64
import importlib.util
import json
import io
import os
import tempfile
import signal
import subprocess
import sys
import time
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_pinned_helper_image_volume_is_explicitly_masked_with_tmpfs(self):
calls=[]
def run(argv,**kwargs):calls.append(argv);return b'anchor' if argv[-1]=='{{.Id}}' else b''
with patch.object(s,'run',side_effect=run),patch.object(s,'verify_volume'),patch.object(s.adapter,'inspect_session'):
s.provision(s.plan_session(),{'files':{},'env':{}},{},'https://issuer.example')
worker=next(argv for argv in calls if s.WORKER in argv)
mask='/privacyidea/etc/persistent:rw,nosuid,nodev,noexec,size=16777216,uid=1000,gid=1000,mode=0700'
self.assertIn(mask,worker)
self.assertEqual(worker[worker.index(mask)-1],'--tmpfs')
issuer=next(argv for argv in calls if argv[-1]==s.adapter.IMAGE)
self.assertNotIn(mask,issuer)
def test_public_phase_sequence_reports_containment_before_private_import(self):
session=s.plan_session();phases=[]
def run(argv,**kwargs):
return b'anchor' if argv[-1]=='{{.Id}}' else b''
with patch.object(s,'run',side_effect=run),patch.object(s,'verify_volume'),patch.object(s.adapter,'inspect_session'):
s.provision(session,{'files':{},'env':{}},{},'https://issuer.example',phases.append)
self.assertEqual(phases,list(s.PROVISION_PHASES))
def test_worker_containment_refusal_keeps_exact_phase_and_skips_import(self):
phases=[];calls=[]
def run(argv,**kwargs):calls.append(argv);return b''
with patch.object(s,'run',side_effect=run),patch.object(s,'verify_volume'),patch.object(s.adapter,'inspect_session',side_effect=s.adapter.Refused):
with self.assertRaises(s.adapter.Refused):
s.provision(s.plan_session(),{'files':{},'env':{}},{},'https://issuer.example',phases.append)
self.assertEqual(phases[-1],'worker_containment')
self.assertFalse(any(s.IMPORT in argv for argv in calls))
def test_issuer_containment_refusal_does_not_claim_provision_complete(self):
phases=[]
def run(argv,**kwargs):return b'anchor' if argv[-1]=='{{.Id}}' else b''
with patch.object(s,'run',side_effect=run),patch.object(s,'verify_volume'),patch.object(s.adapter,'inspect_session',side_effect=[None,s.adapter.Refused()]):
with self.assertRaises(s.adapter.Refused):
s.provision(s.plan_session(),{'files':{},'env':{}},{},'https://issuer.example',phases.append)
self.assertEqual(phases[-1],'issuer_containment')
self.assertNotIn('complete',phases)
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,'verify_execution'),patch.object(s.os,'pidfd_open',return_value=99),patch.object(s.os,'close'),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,{'pid':123})
kwargs=spawn.call_args.kwargs
self.assertEqual(set(kwargs['env']),{'PATH','DOCKER_HOST'})
self.assertTrue(kwargs['close_fds']);self.assertTrue(kwargs['start_new_session']);self.assertEqual(kwargs['pass_fds'],(99,))
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)}, {})
@unittest.skipUnless(hasattr(os,'pidfd_open') and hasattr(signal,'pidfd_send_signal'),'Linux pidfd required')
class ExecutionDeadlineTests(unittest.TestCase):
def start_synthetic_owner(self,root):
late=str(root/'late-creation')
code="""import importlib.util,json,subprocess,sys,time
spec=importlib.util.spec_from_file_location('session',sys.argv[1]);s=importlib.util.module_from_spec(spec);spec.loader.exec_module(s)
execution=s.isolate_execution()
child=subprocess.Popen([sys.executable,'-c',"import pathlib,sys,time;time.sleep(2);pathlib.Path(sys.argv[1]).write_text('late')",sys.argv[2]])
print(json.dumps({'execution':execution,'child_pid':child.pid}),flush=True)
time.sleep(20)
"""
parent=subprocess.Popen([sys.executable,'-c',code,str(SOURCE),late],stdout=subprocess.PIPE,stderr=subprocess.DEVNULL)
message=json.loads(parent.stdout.readline());parent.stdout.close()
def finish():
if parent.poll() is None:parent.kill()
parent.wait(timeout=5)
try:
before=s.process_facts(message['child_pid'])
if before is None or before['sid']!=message['execution']['sid'] or before['pgid']!=message['execution']['pgid']:return
fd=os.pidfd_open(message['child_pid'])
try:
after=s.process_facts(message['child_pid'])
if after is not None and all(after[k]==before[k] for k in ('pid','sid','pgid','start_ticks','uid')):
signal.pidfd_send_signal(fd,signal.SIGKILL)
except ProcessLookupError:pass
finally:os.close(fd)
except ProcessLookupError:pass
self.addCleanup(finish)
return parent,message['execution'],Path(late)
def test_deadline_quiesces_only_fresh_owner_group_before_cleanup(self):
with tempfile.TemporaryDirectory() as directory:
parent,execution,late=self.start_synthetic_owner(Path(directory));fd=os.pidfd_open(parent.pid)
try:
receipt={};s.quiesce_execution(execution,fd,receipt)
self.assertTrue(receipt['execution_quiescence_verified'])
self.assertFalse(s.group_members(execution));self.assertFalse(late.exists())
finally:os.close(fd)
def test_hard_parent_loss_stops_surviving_marker_creator(self):
with tempfile.TemporaryDirectory() as directory:
parent,execution,late=self.start_synthetic_owner(Path(directory));fd=os.pidfd_open(parent.pid)
try:
signal.pidfd_send_signal(fd,signal.SIGKILL)
poll=__import__('select').poll();poll.register(fd,__import__('select').POLLIN);self.assertTrue(poll.poll(2000))
receipt={};s.quiesce_execution(execution,fd,receipt)
self.assertTrue(receipt['execution_quiescence_verified']);self.assertFalse(late.exists())
self.assertFalse(s.group_members(execution))
finally:os.close(fd)
def test_expired_execution_cannot_start_any_docker_command(self):
binding={'pid':os.getpid(),'sid':os.getsid(0),'pgid':os.getpgrp(),'deadline':time.monotonic()-1,'staging':'/tmp/not-created'}
with patch.object(s.adapter,'EXECUTION',binding),patch.object(s.adapter.subprocess,'run') as run:
with self.assertRaises(s.adapter.Refused):s.run(['docker','create','fixed-image'])
run.assert_not_called()
def test_reused_or_foreign_session_refuses_signals(self):
binding={'pid':123,'sid':123,'pgid':123,'start_ticks':10,'uid':os.getuid()}
wrong={**binding,'sid':456,'state':'S'}
with patch.object(s,'process_facts',return_value=wrong),patch.object(s.signal,'pidfd_send_signal') as send:
with self.assertRaises(s.adapter.Refused):s.quiesce_execution(binding,99,{})
send.assert_not_called()
def test_inflight_uncertainty_survives_plaintext_directory_removal(self):
with tempfile.TemporaryDirectory() as directory:
path=Path(directory)/'owned-stage';path.mkdir();marker=Path(str(path)+'-docker-inflight');marker.touch()
staging={'path':str(path)};self.assertTrue(s.daemon_uncertain(staging));path.rmdir()
self.assertTrue(s.daemon_uncertain(staging))
class ReaperOrderingTests(unittest.TestCase):
def exercise(self,quiescence_fails=False):
events=[];records=[]
def quiet(*args):
events.append('quiesce')
if quiescence_fails:raise s.adapter.Refused()
args[-1]['execution_quiescence_verified']=True
def recover(*args):events.append('cleanup')
with patch.object(s,'verify_execution'),patch.object(s,'pidfd_dead',return_value=False),patch.object(s.select,'poll') as poll,patch.object(s,'quiesce_execution',side_effect=quiet),patch.object(s,'daemon_uncertain',return_value=True),patch.object(s,'recover_obligations',side_effect=recover),patch.object(s.os,'close'),patch.object(s.os,'open',return_value=99),patch.object(s.os,'fdopen'),patch.object(s.json,'dump',side_effect=lambda r,*args:records.append(dict(r))),patch('sys.stdout',io.StringIO()):
poll.return_value.poll.return_value=[]
s.reaper_main({}, {'path':'/dev/shm/exact-staging'}, {'deadline':time.monotonic()-1},77)
return events,records[0]
def test_daemon_uncertainty_survives_verified_resource_readback(self):
events,receipt=self.exercise();self.assertEqual(events,['quiesce','cleanup'])
self.assertTrue(receipt['execution_quiescence_verified']);self.assertTrue(receipt['in_flight_daemon_request_uncertain'])
self.assertEqual(receipt['status'],'bounded_cleanup_uncertain')
def test_unverified_quiescence_prevents_any_cleanup(self):
events,receipt=self.exercise(True);self.assertEqual(events,['quiesce'])
self.assertFalse(receipt['execution_quiescence_verified']);self.assertEqual(receipt['status'],'bounded_cleanup_uncertain')
def test_checkpoint_rewrites_one_valid_json_document(self):
with tempfile.TemporaryFile() as stream:
s.persist_receipt(stream.fileno(),{'status':'registered','long':'x'*100})
s.persist_receipt(stream.fileno(),{'status':'failed'})
stream.seek(0);self.assertEqual(json.load(stream),{'status':'failed'})
def test_nonzero_docker_result_retains_uncertainty_before_inflight_clear(self):
with tempfile.TemporaryDirectory() as directory:
staging=str(Path(directory)/'stage');Path(staging).mkdir()
binding={'pid':os.getpid(),'sid':os.getsid(0),'pgid':os.getpgrp(),'deadline':time.monotonic()+10,'staging':staging}
def returned(*args,**kwargs):
self.assertTrue(Path(staging+'-docker-inflight').exists())
return subprocess.CompletedProcess(args[0],1,b'')
with patch.object(s.adapter,'EXECUTION',binding),patch.object(s.adapter.subprocess,'run',side_effect=returned):
with self.assertRaises(s.adapter.Refused):s.run(['docker','create','synthetic-already-accepted'])
self.assertFalse(Path(staging+'-docker-inflight').exists());self.assertTrue(s.daemon_uncertain({'path':staging}))
def test_only_policy_mount_is_writable_in_issuer(self):
plan=s.plan_session();args=s.mounts(plan,readonly=True)
mounts=args[1::2]
for value in mounts:
self.assertEqual(not value.endswith(',readonly'),'target=/var/lib/keycape-policy' in value)
def test_docker_timeout_retains_independent_uncertainty_marker(self):
with tempfile.TemporaryDirectory() as directory:
staging=str(Path(directory)/'stage');Path(staging).mkdir()
binding={'pid':os.getpid(),'sid':os.getsid(0),'pgid':os.getpgrp(),'deadline':time.monotonic()+10,'staging':staging}
def timeout(*args,**kwargs):
self.assertTrue(Path(staging+'-docker-inflight').exists())
raise subprocess.TimeoutExpired('synthetic Docker',1)
with patch.object(s.adapter,'EXECUTION',binding),patch.object(s.adapter.subprocess,'run',side_effect=timeout):
with self.assertRaises(subprocess.TimeoutExpired):s.run(['docker','create','fixed-image'])
self.assertFalse(Path(staging+'-docker-inflight').exists())
self.assertTrue(Path(staging+'-docker-uncertain').exists());self.assertTrue(s.daemon_uncertain({'path':staging}))
if __name__=='__main__':unittest.main()

View file

@ -0,0 +1,108 @@
import copy
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import tempfile
import pty
import subprocess
import unittest
from unittest.mock import patch
ROOT=Path(__file__).parents[1]
spec=importlib.util.spec_from_file_location('prepare',ROOT/'tools/bootstrap-recovery/prepare-isolated-keycape.py')
p=importlib.util.module_from_spec(spec);spec.loader.exec_module(p)
class PreparationTests(unittest.TestCase):
def setUp(self):
self.temp=tempfile.TemporaryDirectory();self.addCleanup(self.temp.cleanup)
self.path=Path(self.temp.name)/'ciphertext';self.path.write_bytes(b'synthetic ciphertext');self.path.chmod(0o600)
self.transfer={'schema':'netkingdom.identity-offsite.v1','status':'readback_verified','provider':'Storage Share',
'folder':'Backup/railiance-backups/net-kingdom-identity','artifact':'net-kingdom-identity-20261016T063159Z-5882771aa2ce.tar.age',
'ciphertext_sha256':'a'*64,'ciphertext_bytes':len(b'synthetic ciphertext'),'etag':'"synthetic"','fetched_path':str(self.path),
'independent_owner_retrieval_verified':True}
self.admission={name:'a'*64 for name in ('ciphertext_sha256','manifest_sha256','verifier_sha256','trust_sha256','tls_cert_sha256')}
self.admission.update(issuer='https://issuer.example',consumer=dict(client_id='existing',secret_env='KEYCAPE_EXISTING_SECRET',audience='existing-api',scope='read',subject='svc:existing',tenant='existing',roles='reader',deny_scope='write'),
source_sha256={name:'sha256:'+hashlib.sha256((ROOT/name).read_bytes()).hexdigest() for name in p.SOURCE_PATHS})
def test_complete_public_bindings_admitted_without_reading_ciphertext(self):
original=Path.read_bytes
def read(path):
self.assertNotEqual(path,self.path)
return original(path)
with patch.object(Path,'read_bytes',read):self.assertTrue(p.validate(self.transfer,self.admission,ROOT))
def test_oversize_metadata_refused_before_any_content_read(self):
self.path.write_bytes(b'x'*(128*1024+1))
with patch.object(Path,'read_bytes',side_effect=AssertionError('unbounded read')):
with self.assertRaises(p.Refused):p.read_metadata(self.path)
def test_duplicate_json_keys_refused_at_every_level(self):
for raw in (b'{"issuer":"one","issuer":"two"}',b'{"consumer":{"client_id":"one","client_id":"two"}}'):
with self.subTest(raw=raw),self.assertRaises(p.Refused):p.strict_json(raw)
def test_bounded_metadata_reader_accepts_exact_limit(self):
self.path.write_bytes(b'x'*128)
self.assertEqual(p.read_metadata(self.path,128),b'x'*128)
with self.assertRaises(p.Refused):p.read_metadata(self.path,127)
def test_wrong_archive_binding_refused(self):
self.admission['ciphertext_sha256']='b'*64
with self.assertRaises(p.Refused):p.validate(self.transfer,self.admission,ROOT)
def test_ciphertext_size_mismatch_refused_without_decryption(self):
self.transfer['ciphertext_bytes']+=1
with self.assertRaises(p.Refused):p.validate(self.transfer,self.admission,ROOT)
def test_missing_independent_transfer_refused(self):
self.transfer['independent_owner_retrieval_verified']=False
with self.assertRaises(p.Refused):p.validate(self.transfer,self.admission,ROOT)
def test_missing_or_drifted_source_refused(self):
for source in p.SOURCE_PATHS:
bad=copy.deepcopy(self.admission);bad['source_sha256'][source]='0'*64
with self.subTest(source=source),self.assertRaises(p.Refused):p.validate(self.transfer,bad,ROOT)
bad=copy.deepcopy(self.admission);bad['source_sha256'].pop(next(iter(p.SOURCE_PATHS)))
with self.assertRaises(p.Refused):p.validate(self.transfer,bad,ROOT)
def test_unreviewed_extra_dependency_refused(self):
self.admission['source_sha256']['unreviewed.py']='a'*64
with self.assertRaises(p.Refused):p.validate(self.transfer,self.admission,ROOT)
def test_ambiguous_or_unsupported_issuer_refused(self):
for issuer in ('http://issuer.example','https://u:p@issuer.example','https://issuer.example:8443','https://issuer.example/path','https://issuer.example?x=1','https://issuer.example#fragment'):
self.admission['issuer']=issuer
with self.subTest(issuer=issuer),self.assertRaises(p.Refused):p.validate(self.transfer,self.admission,ROOT)
def test_missing_consumer_or_overlapping_denial_refused(self):
self.admission['consumer']['deny_scope']='read'
with self.assertRaises(p.Refused):p.validate(self.transfer,self.admission,ROOT)
self.admission['consumer'].pop('roles')
with self.assertRaises(p.Refused):p.validate(self.transfer,self.admission,ROOT)
def test_symlink_private_input_refused_without_read(self):
link=self.path.parent/'link';link.symlink_to(self.path)
with self.assertRaises(p.Refused):p.metadata_path(link)
def test_publicly_readable_private_input_refused(self):
self.path.chmod(0o644)
with self.assertRaises(p.Refused):p.metadata_path(self.path)
def test_bad_admission_digest_never_reads_tls_or_starts_custody(self):
admission=self.path.parent/'admission';admission.write_text(json.dumps(self.admission));admission.chmod(0o600)
argv=['prepare','--transfer-receipt',str(self.path),'--admission',str(admission),'--admission-sha256','b'*64,
'--tls-cert','/never/read-cert','--tls-key','/never/read-key','--trust','/never/read-trust']
with patch('sys.argv',argv),patch('builtins.print') as output:
self.assertEqual(p.main(),1)
self.assertEqual(output.call_args.args,('ERROR: reviewed metadata/source/dependency preflight refused; no key requested.',))
def test_actual_launcher_bad_digest_stops_before_hidden_prompt(self):
master,slave=pty.openpty()
try:
args=['bash',str(ROOT/'tools/bootstrap-recovery/run-isolated-keycape.sh'),str(self.path),str(self.path),'b'*64,str(self.path),str(self.path),str(self.path)]
process=subprocess.Popen(args,stdin=slave,stdout=slave,stderr=slave)
process.wait(timeout=5);os.close(slave);slave=None
output=b''
while True:
try:
block=os.read(master,4096)
if not block:break
output+=block
except OSError:break
self.assertEqual(process.returncode,1)
self.assertIn(b'preflight refused; no key requested.',output)
self.assertNotIn(b'Paste custody',output)
self.assertNotIn(b'Enter only',output)
finally:
if slave is not None:os.close(slave)
os.close(master)
def test_launcher_prepares_before_sealed_descriptor_custody(self):
script=(ROOT/'tools/bootstrap-recovery/run-isolated-keycape.sh').read_text()
self.assertLess(script.index('python3 tools/bootstrap-recovery/prepare-isolated-keycape.py'),script.index('bash sso-mfa/bootstrap/sops-custody-unlock.sh'))
self.assertIn('--sealed-fd --prompt -- python3',script)

View file

@ -0,0 +1,98 @@
"""Generated archive/key only; actual age authentication + archive restore.
Docker and issuer behavior are intentionally mocked here, not native evidence.
"""
import base64
from datetime import datetime,timedelta,timezone
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import subprocess
import time
from unittest.mock import MagicMock,patch
import pytest
from cryptography import x509
from cryptography.hazmat.primitives import hashes,serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
from test_identity_bundle import stage,b
ROOT=Path(__file__).parents[1]
def load(name,path):
spec=importlib.util.spec_from_file_location(name,path);m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m);return m
s=load('session_manifest',ROOT/'tools/bootstrap-recovery/isolated-keycape-session.py')
h=load('helper_manifest',ROOT/'sso-mfa/bootstrap/sops-custody-fd.py')
@pytest.fixture
def envelope(stage,tmp_path,monkeypatch):
raw=subprocess.run(['age-keygen'],stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,check=True).stdout
key=h.sealed_key(next(line for line in raw.splitlines() if line.startswith(b'AGE-SECRET-KEY-'))+b'\n')
recipient=subprocess.run(['age-keygen','-y',f'/proc/self/fd/{key}'],pass_fds=(key,),stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,check=True).stdout.decode().strip()
profile=b.profile();profile['public_recipient']=recipient;monkeypatch.setattr(b,'profile',lambda:profile);monkeypatch.setattr(s.bundle,'profile',lambda:profile)
artifact=tmp_path/'identity.tar.age';sealed=b.seal(stage,artifact,{'test_only':True})
signer=rsa.generate_private_key(public_exponent=65537,key_size=2048);name=x509.Name([x509.NameAttribute(NameOID.COMMON_NAME,'synthetic.invalid')]);now=datetime.now(timezone.utc)
cert=(x509.CertificateBuilder().subject_name(name).issuer_name(name).public_key(signer.public_key()).serial_number(1).not_valid_before(now-timedelta(minutes=1)).not_valid_after(now+timedelta(hours=1)).sign(signer,hashes.SHA256())).public_bytes(serialization.Encoding.PEM)
tls={'/recovery-tls/server.pem':cert,'/recovery-tls/trust.pem':cert,'/recovery-tls/server-key.pem':signer.private_bytes(serialization.Encoding.PEM,serialization.PrivateFormat.TraditionalOpenSSL,serialization.NoEncryption())}
transfer=dict(schema='netkingdom.identity-offsite.v1',status='readback_verified',provider='Storage Share',folder='Backup/railiance-backups/net-kingdom-identity',artifact='net-kingdom-identity-20261016T063159Z-5882771aa2ce.tar.age',etag='"synthetic"',ciphertext_sha256=sealed['ciphertext_sha256'],ciphertext_bytes=artifact.stat().st_size,fetched_path=str(artifact),independent_owner_retrieval_verified=True)
admission=dict(ciphertext_sha256=sealed['ciphertext_sha256'],manifest_binding_mode='ciphertext-derived-v1',issuer='https://synthetic.invalid',tls_cert_sha256=hashlib.sha256(cert).hexdigest(),trust_sha256=hashlib.sha256(cert).hexdigest(),verifier_sha256=hashlib.sha256(b'synthetic executable').hexdigest())
try:yield artifact,key,transfer,admission,tls
finally:os.close(key)
def execute(envelope,*,wrong_key=False):
artifact,key,transfer,admission,tls=envelope;receipt={};seen={};checkpoints=[]
if wrong_key:
raw=subprocess.run(['age-keygen'],stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,check=True).stdout
key=h.sealed_key(next(line for line in raw.splitlines() if line.startswith(b'AGE-SECRET-KEY-'))+b'\n')
actualrun=s.run
def run(argv,**kwargs):return actualrun(argv,**kwargs) if argv[0]=='age-keygen' else b''
def native(root,manifest,checked,receipt,**kwargs):seen.update(checked);receipt['status']='synthetic_flow_checked'
execution={'pid':os.getpid(),'sid':os.getsid(0),'pgid':os.getpgrp(),'uid':os.getuid(),'deadline':time.monotonic()+60}
try:
with patch.object(s,'verify_execution'),patch.object(s.local,'preflight',return_value={}),patch.object(s.local.verify.custody,'private_key_fd',side_effect=lambda:os.dup(key)),patch.object(s,'start_reaper',return_value=MagicMock()),patch.object(s,'run',side_effect=run),patch.object(s.adapter,'container_file',return_value=b'synthetic executable'),patch.object(s.adapter,'private_payload',return_value={'files':{'/etc/keycape/config.yaml':base64.b64encode(b'issuer: https://synthetic.invalid\nport: 8080').decode()},'env':{}}),patch.object(s,'provision'),patch.object(s.adapter,'check_jwks'),patch.object(s.adapter,'native_checks',side_effect=native),patch.object(s,'cleanup_all'):
# Key file must be valid at the later public signer construction.
payload_key=rsa.generate_private_key(public_exponent=65537,key_size=2048).private_bytes(serialization.Encoding.PEM,serialization.PrivateFormat.TraditionalOpenSSL,serialization.NoEncryption())
with patch.object(s.adapter,'private_payload',return_value={'files':{'/etc/keycape/config.yaml':base64.b64encode(b'issuer: https://synthetic.invalid\nport: 8080').decode(),'/etc/keycape/key.pem':base64.b64encode(payload_key).decode()},'env':{}}):
s.execute(transfer,admission,tls,receipt,execution,checkpoint=lambda r:checkpoints.append(json.loads(json.dumps(r))))
return receipt,seen,checkpoints
finally:
if wrong_key:os.close(key)
def test_exact_ciphertext_authenticated_restore_derives_private_manifest_without_safe_leak(envelope):
receipt,seen,checkpoints=execute(envelope)
derived=seen['manifest_sha256'];assert len(derived)==64
safe=json.dumps([receipt,checkpoints]);assert derived not in safe;assert 'manifest_sha256' not in safe
assert receipt['all_manifest_hashes_verified'] is True
assert receipt['temporary_plaintext_cleanup_verified'] is True
def test_changed_ciphertext_refuses_before_archive_decryption(envelope):
artifact,*_=envelope;artifact.write_bytes(artifact.read_bytes()[:-1]+b'!')
with patch.object(s.adapter,'process') as process,pytest.raises(s.adapter.Refused):execute(envelope)
assert not process.called
def test_modified_ciphertext_rebound_metadata_still_fails_authenticated_age(envelope):
artifact,key,transfer,admission,tls=envelope
raw=artifact.read_bytes();artifact.write_bytes(raw[:-32]+b'!'*32)
digest=hashlib.sha256(artifact.read_bytes()).hexdigest()
transfer['ciphertext_sha256']=digest;admission['ciphertext_sha256']=digest
with pytest.raises(s.adapter.Refused):execute(envelope)
def test_wrong_key_refuses_before_decryption_and_never_exposes_manifest(envelope):
with pytest.raises(s.adapter.Refused):execute(envelope,wrong_key=True)
def test_wrong_explicit_manifest_refuses_after_full_restore(envelope):
admission=envelope[3];admission['manifest_binding_mode']='explicit-digest-v1';admission['manifest_sha256']='0'*64
with pytest.raises(s.adapter.Refused):execute(envelope)
@pytest.mark.parametrize('mode',['derived','ciphertext-derived-v2',None,True,{}])
def test_unknown_binding_mode_refuses_before_effects(envelope,mode):
envelope[3]['manifest_binding_mode']=mode
with patch.object(s.local,'preflight') as preflight,pytest.raises(s.adapter.Refused):execute(envelope)
assert not preflight.called
def test_conflicting_binding_and_unverified_transfer_refuse(envelope):
admission=envelope[3];transfer=envelope[2];admission['manifest_sha256']='0'*64
with pytest.raises(s.adapter.Refused):s.adapter.manifest_binding_mode(admission,transfer)
admission.pop('manifest_sha256');transfer['independent_owner_retrieval_verified']=False
with pytest.raises(s.adapter.Refused):s.adapter.manifest_binding_mode(admission,transfer)

View file

@ -0,0 +1,143 @@
import fcntl
import importlib.util
import json
import os
from pathlib import Path
import signal
import pty
import select
import subprocess
import sys
import tempfile
import time
import unittest
from unittest.mock import patch
ROOT=Path(__file__).parents[1]
def load(name,path):
spec=importlib.util.spec_from_file_location(name,path);m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m);return m
h=load('fdhelper',ROOT/'sso-mfa/bootstrap/sops-custody-fd.py')
c=load('custody',ROOT/'sso-mfa/bootstrap/sops-private.py')
class SealedCustodyTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
# Capture generated test key in memory only, never into test output/path.
r=subprocess.run(['age-keygen'],stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,check=True)
cls.material=next(x for x in r.stdout.splitlines() if x.startswith(b'AGE-SECRET-KEY-'))+b'\n'
fd=h.sealed_key(cls.material)
try:cls.recipient=subprocess.run(['age-keygen','-y',f'/proc/self/fd/{fd}'],pass_fds=(fd,),stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,check=True).stdout.decode().strip()
finally:os.close(fd)
def test_sealed_descriptor_private_duplicate_is_cloexec_and_decrypts(self):
encrypted=subprocess.run(['age','-r',self.recipient],input=b'synthetic recovery material',stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,check=True).stdout
original=h.sealed_key(self.material)
try:
with patch.dict(os.environ,{'NETKINGDOM_CUSTODY_AGE_KEY_FD':str(original)},clear=True):fd=c.private_key_fd()
try:
self.assertFalse(os.get_inheritable(fd));self.assertEqual(os.fstat(fd).st_nlink,0)
r=subprocess.run(['age','-d','-i',f'/proc/self/fd/{fd}'],pass_fds=(fd,),input=encrypted,stdout=subprocess.PIPE,stderr=subprocess.DEVNULL)
self.assertEqual(r.returncode,0);self.assertEqual(r.stdout,b'synthetic recovery material')
finally:os.close(fd)
finally:os.close(original)
def test_incomplete_seals_or_named_file_refused(self):
for named in (False,True):
if named:
file=tempfile.TemporaryFile();fd=file.fileno();file.write(self.material);file.flush()
else:fd=os.memfd_create('synthetic-unsealed',os.MFD_CLOEXEC|os.MFD_ALLOW_SEALING);os.write(fd,self.material)
try:
with patch.dict(os.environ,{'NETKINGDOM_CUSTODY_AGE_KEY_FD':str(fd)},clear=True),self.assertRaises((c.Refused,OSError)):
c.private_key_fd()
finally:
if named:file.close()
else:os.close(fd)
def test_ambiguous_sources_refused(self):
with patch.dict(os.environ,{'NETKINGDOM_CUSTODY_AGE_KEY_FD':'3','NETKINGDOM_CUSTODY_AGE_KEY_FILE':'/not/opened'},clear=True),self.assertRaises(c.Refused):c.private_key_fd()
def test_malformed_descriptor_text_refused_before_dup(self):
for value in ('-1','2','03',' 3','3 ','1000000000','3/4'):
with patch.dict(os.environ,{'NETKINGDOM_CUSTODY_AGE_KEY_FD':value},clear=True),patch.object(c.os,'dup') as duplicate:
with self.subTest(value=value),self.assertRaises(c.Refused):c.private_key_fd()
duplicate.assert_not_called()
def test_bad_expected_recipient_refused_before_prompt(self):
with patch('sys.argv',['sealed','--expected-recipient','age1bad','--','true']),patch.object(h.os,'isatty',return_value=True),patch.object(h.getpass,'getpass') as prompt,patch('builtins.print'):
self.assertEqual(h.main(),1);prompt.assert_not_called()
def test_existing_file_mode_still_accepts_private_owner_file(self):
with tempfile.TemporaryDirectory() as temp:
path=Path(temp)/'synthetic-key';path.write_bytes(self.material);path.chmod(0o600)
with patch.dict(os.environ,{'NETKINGDOM_CUSTODY_AGE_KEY_FILE':str(path)},clear=True):fd=c.private_key_fd()
try:self.assertEqual(os.read(fd,16384),self.material)
finally:os.close(fd)
def test_helper_passes_exact_fd_and_clears_ambient_sources(self):
code="""import os,sys
fd=int(os.environ['NETKINGDOM_CUSTODY_AGE_KEY_FD'])
assert os.fstat(fd).st_nlink==0
assert all(k not in os.environ for k in ('NETKINGDOM_CUSTODY_AGE_KEY_FILE','SOPS_AGE_KEY_FILE','SOPS_AGE_KEY','SOPS_AGE_KEY_CMD'))
"""
with patch.dict(os.environ,{'SOPS_AGE_KEY':'synthetic-competing','NETKINGDOM_CUSTODY_AGE_KEY_FILE':'/not/read'}):
self.assertEqual(h.run(self.material,self.recipient,[sys.executable,'-c',code]),0)
def test_normal_and_error_return_close_parent_fd(self):
actual=h.sealed_key
for status in (0,7):
fds=[]
def capture(material):
fd=actual(material);fds.append(fd);return fd
with patch.object(h,'sealed_key',side_effect=capture):self.assertEqual(h.run(self.material,self.recipient,[sys.executable,'-c',f'raise SystemExit({status})']),status)
with self.assertRaises(OSError):os.fstat(fds[0])
def test_wrong_recipient_does_not_start_child_and_closes_fd(self):
other=subprocess.run(['age-keygen'],stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,check=True).stdout
other=next(x for x in other.splitlines() if x.startswith(b'AGE-SECRET-KEY-'))+b'\n';fd=h.sealed_key(other)
try:recipient=subprocess.run(['age-keygen','-y',f'/proc/self/fd/{fd}'],pass_fds=(fd,),stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,check=True).stdout.decode().strip()
finally:os.close(fd)
with tempfile.TemporaryDirectory() as temp:
marker=Path(temp)/'child'
with self.assertRaises(h.Refused):h.run(self.material,recipient,[sys.executable,'-c','import pathlib,sys;pathlib.Path(sys.argv[1]).touch()',str(marker)])
self.assertFalse(marker.exists())
def test_actual_attended_shell_sealed_mode_hides_key_and_runs_original_fd_consumer(self):
code="import importlib.util,os,sys;spec=importlib.util.spec_from_file_location('c',sys.argv[1]);c=importlib.util.module_from_spec(spec);spec.loader.exec_module(c);fd=c.private_key_fd();assert os.fstat(fd).st_nlink==0;os.close(fd)"
master,slave=pty.openpty()
child=subprocess.Popen(['bash','-x',str(ROOT/'sso-mfa/bootstrap/sops-custody-unlock.sh'),'--expected-recipient',self.recipient,'--sealed-fd','--prompt','--',sys.executable,'-c',code,str(ROOT/'sso-mfa/bootstrap/sops-private.py')],stdin=slave,stdout=slave,stderr=slave)
os.close(slave);captured=b'';sent=False;end=time.monotonic()+5
try:
while time.monotonic()<end:
if select.select([master],[],[],.05)[0]:
try:chunk=os.read(master,65536)
except OSError:break
if not chunk:break
captured+=chunk
if b'input hidden): ' in captured and not sent:os.write(master,self.material);sent=True
self.assertEqual(child.wait(timeout=2),0)
self.assertTrue(sent);self.assertNotIn(self.material.strip(),captured)
finally:
if child.poll() is None:child.kill();child.wait(timeout=2)
os.close(master)
def test_actual_hard_parent_loss_leaves_no_named_key_and_closes_on_child_exit(self):
# Public child metadata only crosses the test pipe. The generated key is
# supplied over stdin and never becomes an argv/environment/path value.
child_code="import json,os,time;fd=int(os.environ['NETKINGDOM_CUSTODY_AGE_KEY_FD']);print(json.dumps({'pid':os.getpid(),'fd':fd,'nlink':os.fstat(fd).st_nlink}),flush=True);time.sleep(30)"
parent_code="""import importlib.util,sys
spec=importlib.util.spec_from_file_location('h',sys.argv[1]);h=importlib.util.module_from_spec(spec);spec.loader.exec_module(h)
h.run(sys.stdin.buffer.read(),sys.argv[2],[sys.executable,'-c',sys.argv[3]])
"""
parent=subprocess.Popen([sys.executable,'-c',parent_code,str(ROOT/'sso-mfa/bootstrap/sops-custody-fd.py'),self.recipient,child_code],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,start_new_session=True)
info=None;child_pidfd=None
try:
parent.stdin.write(self.material);parent.stdin.close()
info=json.loads(parent.stdout.readline());self.assertEqual(info['nlink'],0)
fdpath=Path('/proc')/str(info['pid'])/'fd'/str(info['fd']);self.assertTrue(fdpath.exists())
child_pidfd=os.pidfd_open(info['pid'])
parent.kill();parent.wait(timeout=5)
# Admitted child retains its inherited FD until it exits; do not
# confuse parent loss with revocation of that active child.
self.assertTrue(fdpath.exists())
signal.pidfd_send_signal(child_pidfd,signal.SIGKILL)
# pidfd exit readiness proves the exact child's descriptor table was
# released. /proc may become inaccessible during process teardown;
# do not interpret permission errors or a reused numeric PID as proof.
poller=select.poll();poller.register(child_pidfd,select.POLLIN)
self.assertTrue(poller.poll(5000))
finally:
if parent.poll() is None:parent.kill();parent.wait(timeout=5)
if child_pidfd is not None:
try:signal.pidfd_send_signal(child_pidfd,signal.SIGKILL)
except ProcessLookupError:pass
finally:os.close(child_pidfd)
parent.stdout.close()

View file

@ -0,0 +1,276 @@
"""Service helpers against synthetic files and a recording Kubernetes API stub."""
import base64
import json
import os
from pathlib import Path
import subprocess
import bcrypt
import pytest
ROOT = Path(__file__).resolve().parents[1]
PASSWORD = 'SyntheticServiceCredential12345678901234567890'
CHANGED = 'ReplacementSyntheticCredential12345678901234567890'
VALUES = {
'privacyidea': ['PI_SECRET_KEY', 'PI_PEPPER', 'PI_DB_PASSWORD'],
'lldap': ['LLDAP_JWT_SECRET', 'LLDAP_LDAP_USER_PASS'],
'authelia': ['AUTHELIA_JWT_SECRET', 'AUTHELIA_SESSION_SECRET', 'AUTHELIA_STORAGE_ENCRYPTION_KEY',
'AUTHELIA_OIDC_HMAC_SECRET', 'AUTHELIA_KEYCAPE_CLIENT_SECRET'],
}
COMPONENTS = ['postgresql', 'privacyidea', 'lldap', 'authelia']
ROTATIONS = [('postgresql','PI_DB_PASSWORD','password'), ('privacyidea','PI_SECRET_KEY','PI_SECRET_KEY'),
('privacyidea','PI_DB_PASSWORD','PI_SQLALCHEMY_DATABASE_URI'),
('lldap','LLDAP_JWT_SECRET','LLDAP_JWT_SECRET'), ('authelia','AUTHELIA_SESSION_SECRET','session_secret')]
@pytest.fixture
def case(tmp_path):
private=tmp_path/'private';private.mkdir(mode=0o700)
for component,names in VALUES.items():
directory=private/component;directory.mkdir(mode=0o700)
env=directory/'secrets.env';env.write_text(''.join(f'{name}={PASSWORD}\n' for name in names));env.chmod(0o600)
binary=tmp_path/'bin';binary.mkdir()
(binary/'kubectl').write_text('''#!/usr/bin/python3
import json,os,sys
from pathlib import Path
args=sys.argv[1:]
raw=sys.stdin.read() if '-f' in args or '--patch-file=/dev/stdin' in args else ''
with open(os.environ['CALLS'],'a') as f:f.write(json.dumps({'args':args,'stdin':raw})+'\\n')
if os.environ.get('FAIL')==args[0]:
print(os.environ['MARKER']);print(os.environ['MARKER'],file=sys.stderr);sys.exit(1)
p=Path(os.environ['STATE'])
if args[0]=='get':
if os.environ.get('BAD_GET'):print(os.environ['BAD_GET'])
elif p.exists():print(p.read_text())
sys.exit(0)
if args[0]=='create' and raw:
if p.exists() or os.environ.get('RACE'):sys.exit(1)
state=json.loads(raw);state['metadata'].update(uid='fixture-uid',resourceVersion='1')
elif args[0]=='patch':
if os.environ.get('RACE'):sys.exit(1)
state=json.loads(p.read_text())
for op in json.loads(raw):
parts=[s.replace('~1','/').replace('~0','~') for s in op['path'].split('/')[1:]]
parent=state
for part in parts[:-1]:parent=parent[part]
if op['op']=='test':
if parent.get(parts[-1])!=op['value']:sys.exit(1)
elif op['op']=='remove':del parent[parts[-1]]
elif op['op']=='replace':parent[parts[-1]]=op['value']
else:sys.exit(1)
state['metadata']['resourceVersion']=str(int(state['metadata']['resourceVersion'])+1)
else:sys.exit(2)
p.write_text(json.dumps(state))
if os.environ.get('BAD_RESPONSE'):state['data']['unexpected']='wrong'
print(json.dumps(state))
''')
(binary/'kubectl').chmod(0o700)
env={'PATH':str(binary)+':'+os.environ['PATH'],'HOME':str(tmp_path),
'CALLS':str(tmp_path/'calls'),'STATE':str(tmp_path/'state'),'MARKER':PASSWORD}
return private,env
def invoke(case,component,rotate=None,**extra):
args=['bash','-x',str(ROOT/f'sso-mfa/k8s/{component}/create-secrets.sh'),str(case[0])]
if rotate:args+=['--rotate-key',rotate]
return subprocess.run(args,env={**case[1],**extra},capture_output=True,text=True,timeout=20)
def calls(case):
p=Path(case[1]['CALLS']);return [json.loads(line) for line in p.read_text().splitlines()] if p.exists() else []
def state(case):return json.loads(Path(case[1]['STATE']).read_text())
def put_state(case,obj):Path(case[1]['STATE']).write_text(json.dumps(obj))
def check_private_result(result):
assert PASSWORD not in result.stdout+result.stderr
assert CHANGED not in result.stdout+result.stderr
assert 'BEGIN PRIVATE KEY' not in result.stdout+result.stderr
def source(component):return 'privacyidea' if component=='postgresql' else component
@pytest.mark.parametrize('component',COMPONENTS)
def test_create_uses_literal_inputs_stdin_and_no_apply_annotation(case,component):
result=invoke(case,component);assert result.returncode==0,result.stderr;check_private_result(result)
assert PASSWORD not in json.dumps([c['args'] for c in calls(case)])
writes=[c for c in calls(case) if c['args'][0]!='get']
assert len(writes)==1 and writes[0]['args']==['create','-o','json','-f','-']
obj=state(case);assert 'annotations' not in obj['metadata']
values={k:base64.b64decode(v).decode() for k,v in obj['data'].items()}
if component=='postgresql':assert values=={'username':'privacyidea','password':PASSWORD}
elif component=='privacyidea':
assert values['PI_PEPPER']==PASSWORD and values['PI_SECRET_KEY']==PASSWORD
assert values['PI_SQLALCHEMY_DATABASE_URI']==f'postgresql://privacyidea:{PASSWORD}@net-kingdom-pg-rw.databases.svc.cluster.local:5432/privacyidea_db'
elif component=='lldap':assert values=={k:PASSWORD for k in VALUES['lldap']}
else:
key=case[0]/'authelia/oidc_private_key.pem'
assert key.stat().st_mode & 0o077==0 and values['oidc_issuer_private_key']==key.read_text()
assert bcrypt.checkpw(PASSWORD.encode(),values['keycape_client_secret_hash'].encode())
assert values['ldap_password']==PASSWORD
@pytest.mark.parametrize('component',COMPONENTS)
def test_repeated_apply_is_noop_and_preserves_other_data(case,component):
assert invoke(case,component).returncode==0
old=state(case);old['data']['owner-added']='c3ludGhldGlj';old['metadata']['annotations']={'owner':'retained'};put_state(case,old)
count=len(calls(case));result=invoke(case,component)
assert result.returncode==0;check_private_result(result)
assert state(case)==old
assert all(c['args'][0]=='get' for c in calls(case)[count:])
@pytest.mark.parametrize('component,key,field',ROTATIONS)
def test_explicit_rotation_is_conditional_and_changes_only_named_field(case,component,key,field):
assert invoke(case,component).returncode==0
old=state(case);old['data']['owner-added']='c3ludGhldGlj';old['metadata']['annotations']={'owner':'retained','kubectl.kubernetes.io/last-applied-configuration':'synthetic-old-copy'};put_state(case,old)
file=case[0]/source(component)/'secrets.env';file.write_text(file.read_text().replace(f'{key}={PASSWORD}',f'{key}={CHANGED}'))
result=invoke(case,component,rotate=key);assert result.returncode==0,result.stderr;check_private_result(result)
updated=state(case)
assert updated['metadata']['annotations']=={'owner':'retained'}
assert updated['data'][field]!=old['data'][field]
assert {k:v for k,v in updated['data'].items() if k!=field}=={k:v for k,v in old['data'].items() if k!=field}
patch=next(c for c in reversed(calls(case)) if c['args'][0]=='patch')
tests={op['path']:op['value'] for op in json.loads(patch['stdin']) if op['op']=='test'}
assert tests['/metadata/uid']==old['metadata']['uid'] and tests['/metadata/resourceVersion']==old['metadata']['resourceVersion']
assert PASSWORD not in json.dumps(patch['args']) and CHANGED not in json.dumps(patch['args'])
@pytest.mark.parametrize('component',COMPONENTS)
def test_differing_values_require_explicit_rotation(case,component):
assert invoke(case,component).returncode==0;old=state(case)
file=case[0]/source(component)/'secrets.env';file.write_text(file.read_text().replace(PASSWORD,CHANGED))
count=len(calls(case));result=invoke(case,component)
assert result.returncode!=0;check_private_result(result);assert state(case)==old
assert all(c['args'][0]=='get' for c in calls(case)[count:])
@pytest.mark.parametrize('component',COMPONENTS)
@pytest.mark.parametrize('defect',['shell','duplicate','public','symlink','hardlink','public-parent'])
def test_bad_private_input_never_reaches_cluster(case,component,defect):
file=case[0]/source(component)/'secrets.env';marker=case[0]/'executed'
if defect=='shell':file.write_text(file.read_text()+f'$(touch {marker})\n')
elif defect=='duplicate':file.write_text(file.read_text()*2)
elif defect=='public':file.chmod(0o644)
elif defect=='public-parent':file.parent.chmod(0o755)
elif defect=='hardlink':os.link(file,file.with_name('alias'))
else:
target=file.with_name('other');file.rename(target);file.symlink_to(target)
result=invoke(case,component);assert result.returncode!=0;check_private_result(result)
assert not marker.exists() and not calls(case)
@pytest.mark.parametrize('component',COMPONENTS)
@pytest.mark.parametrize('failure',['get','create'])
def test_failed_child_diagnostics_never_escape(case,component,failure):
result=invoke(case,component,FAIL=failure);assert result.returncode!=0;check_private_result(result)
assert not Path(case[1]['STATE']).exists()
@pytest.mark.parametrize('defect',['owner','eso-label','identity','type','basic-auth','data','immutable','missing-uid'])
def test_wrong_or_controller_owned_existing_secret_is_refused(case,defect):
assert invoke(case,'lldap').returncode==0;old=state(case)
if defect=='owner':old['metadata']['ownerReferences']=[{'kind':'ExternalSecret','name':'fixture'}]
elif defect=='eso-label':old['metadata']['labels']={'reconcile.external-secrets.io/managed':'true'}
elif defect=='missing-uid':old['metadata']['uid']=None
elif defect=='identity':old['metadata']['namespace']='other'
elif defect=='type':old['type']='kubernetes.io/tls'
elif defect=='basic-auth':old['type']='kubernetes.io/basic-auth'
elif defect=='data':old['data']['LLDAP_JWT_SECRET']='invalid-base64'
else:old['immutable']=True
put_state(case,old);count=len(calls(case));result=invoke(case,'lldap')
assert result.returncode!=0 and state(case)==old
assert all(c['args'][0]=='get' for c in calls(case)[count:])
@pytest.mark.parametrize('component,key,field',ROTATIONS)
def test_concurrent_change_stops_rotation_without_retry(case,component,key,field):
assert invoke(case,component).returncode==0;old=state(case)
file=case[0]/source(component)/'secrets.env';file.write_text(file.read_text().replace(f'{key}={PASSWORD}',f'{key}={CHANGED}'))
count=len(calls(case));result=invoke(case,component,rotate=key,RACE='1')
assert result.returncode!=0 and state(case)==old;check_private_result(result)
assert sum(c['args'][0]=='patch' for c in calls(case)[count:])==1
def test_create_race_never_switches_to_overwrite(case):
result=invoke(case,'lldap',RACE='1');assert result.returncode!=0
assert [c['args'][0] for c in calls(case)]==['get','create']
def test_authelia_failure_retains_same_key_on_retry(case):
result=invoke(case,'authelia',FAIL='create');assert result.returncode!=0
key=case[0]/'authelia/oidc_private_key.pem';original=key.read_bytes()
assert invoke(case,'authelia').returncode==0 and key.read_bytes()==original
def test_rotating_session_never_generates_or_replaces_existing_signing_key(case):
assert invoke(case,'authelia').returncode==0;old=state(case)
(case[0]/'authelia/oidc_private_key.pem').unlink()
file=case[0]/'authelia/secrets.env';file.write_text(file.read_text().replace(f'AUTHELIA_SESSION_SECRET={PASSWORD}',f'AUTHELIA_SESSION_SECRET={CHANGED}'))
assert invoke(case,'authelia',rotate='AUTHELIA_SESSION_SECRET').returncode==0
assert state(case)['data']['oidc_issuer_private_key']==old['data']['oidc_issuer_private_key']
assert not (case[0]/'authelia/oidc_private_key.pem').exists()
def test_wrong_response_is_not_reported_as_success(case):
result=invoke(case,'lldap',BAD_RESPONSE='1');assert result.returncode!=0;check_private_result(result)
@pytest.mark.parametrize('annotation',['', 'synthetic-old-copy'])
def test_annotation_cleanup_preserves_secret_values_and_other_annotations(case,annotation):
assert invoke(case,'lldap').returncode==0;old=state(case)
old['metadata']['annotations']={'owner':'retained','kubectl.kubernetes.io/last-applied-configuration':annotation};put_state(case,old)
result=invoke(case,'lldap');assert result.returncode==0
assert state(case)['data']==old['data']
assert state(case)['metadata']['annotations']=={'owner':'retained'}
@pytest.mark.parametrize('component,key,field',ROTATIONS)
def test_rotation_refuses_missing_secret_before_create(case,component,key,field):
result=invoke(case,component,rotate=key);assert result.returncode!=0
assert all(c['args'][0]=='get' for c in calls(case))
assert not (case[0]/'authelia/oidc_private_key.pem').exists()
def test_rotation_cannot_change_an_unrelated_field(case):
assert invoke(case,'privacyidea').returncode==0;old=state(case)
file=case[0]/'privacyidea/secrets.env';file.write_text(file.read_text().replace(PASSWORD,CHANGED))
result=invoke(case,'privacyidea',rotate='PI_SECRET_KEY');assert result.returncode!=0
assert state(case)==old
assert not any(c['args'][0]=='patch' for c in calls(case))
@pytest.mark.parametrize('defect',['symlink','hardlink','public','public-parent','invalid'])
def test_explicit_authelia_key_custody_is_validated_without_overwrite(case,defect):
key=case[0]/'authelia/external.pem';key.write_text('synthetic-invalid-key');key.chmod(0o600)
if defect=='symlink':
target=key.with_name('other');key.rename(target);key.symlink_to(target)
elif defect=='hardlink':os.link(key,key.with_name('alias'))
elif defect=='public':key.chmod(0o644)
elif defect=='public-parent':key.parent.chmod(0o755)
file=case[0]/'authelia/secrets.env';file.write_text(file.read_text()+f'AUTHELIA_OIDC_PRIVATE_KEY_FILE={key}\n')
result=invoke(case,'authelia');assert result.returncode!=0
assert key.read_text()=='synthetic-invalid-key'
assert not any(c['args'][0]!='get' for c in calls(case))
def test_native_kubectl_applies_stdin_patch_and_refuses_stale_version(case,tmp_path):
import shutil
binary=shutil.which('kubectl')
if binary is None:pytest.skip('native kubectl not available')
assert invoke(case,'lldap').returncode==0;old=state(case)
file=case[0]/'lldap/secrets.env';file.write_text(file.read_text().replace(f'LLDAP_JWT_SECRET={PASSWORD}',f'LLDAP_JWT_SECRET={CHANGED}'))
assert invoke(case,'lldap',rotate='LLDAP_JWT_SECRET').returncode==0
patch=next(c['stdin'] for c in reversed(calls(case)) if c['args'][0]=='patch')
original=tmp_path/'original.json';original.write_text(json.dumps(old));original.chmod(0o600)
env={**os.environ,'KUBECONFIG':str(tmp_path/'no-cluster-config'),'HOME':str(tmp_path)}
command=[binary,'patch','--local','--type=json','--patch-file=/dev/stdin','-f',str(original),'-o','json']
result=subprocess.run(command,input=patch,env=env,capture_output=True,text=True,timeout=20)
assert result.returncode==0
assert json.loads(result.stdout)['data']==state(case)['data']
old['metadata']['resourceVersion']='999';original.write_text(json.dumps(old))
refused=subprocess.run(command,input=patch,env=env,capture_output=True,text=True,timeout=20)
assert refused.returncode!=0

View file

@ -0,0 +1,227 @@
"""Use real SOPS with disposable age custody; no repository secret inputs."""
import json
import os
from pathlib import Path
import shutil
import subprocess
import pytest
ROOT = Path(__file__).resolve().parents[1]
SOPS = shutil.which('sops')
pytestmark = pytest.mark.skipif(SOPS is None, reason='SOPS required for real crypto regression')
@pytest.fixture
def case(tmp_path):
repo = tmp_path / 'repo'; (repo / 'sso-mfa/bootstrap').mkdir(parents=True)
shutil.copyfile(ROOT / 'Makefile', repo / 'Makefile')
shutil.copyfile(ROOT / 'sso-mfa/bootstrap/sops-private.py', repo / 'sso-mfa/bootstrap/sops-private.py')
(repo / 'keys').mkdir()
key = tmp_path / 'disposable.key'
subprocess.run(['age-keygen', '-o', str(key)], capture_output=True, check=True)
public = subprocess.check_output(['age-keygen', '-y', str(key)], text=True).strip()
(repo / 'keys/age.pub').write_text(public + '\n')
home = tmp_path / 'home'; home.mkdir(mode=0o700)
env = {'HOME': str(home), 'PATH': os.environ['PATH'], 'NETKINGDOM_CUSTODY_AGE_KEY_FILE': str(key)}
clear = tmp_path / 'input.json'; clear.write_text(json.dumps({'credential': 'synthetic-secret-payload'}))
cipher = repo / 'cipher.json'
encrypted = subprocess.run([SOPS, '--encrypt', '--age', public, str(clear)], cwd=tmp_path,
env=env, capture_output=True, check=True)
cipher.write_bytes(encrypted.stdout)
output_dir = tmp_path / 'private-output'; output_dir.mkdir(mode=0o700)
return repo, key, env, cipher, output_dir / 'restored.json'
def make(case, target='sops-decrypt', *, env=None, source=None, output=None):
repo, _, default_env, cipher, destination = case
return subprocess.run(['make', '-s', target, 'FILE=' + str(source or cipher),
'OUTPUT=' + str(output or destination)],
env=env or default_env, cwd=repo, capture_output=True, text=True, timeout=15)
def ambient_key(case):
_, key, env, _, _ = case
path = Path(env['HOME']) / '.config/sops/age/keys.txt'
path.parent.mkdir(parents=True); path.write_bytes(key.read_bytes()); path.chmod(0o600)
def test_setup_never_installs_private_key(case):
_, key, env, _, _ = case
legacy = Path(env['HOME']) / '.config/age/key.txt'
legacy.parent.mkdir(parents=True); legacy.write_bytes(key.read_bytes())
result = make(case, 'sops-setup')
assert result.returncode == 0
assert not (Path(env['HOME']) / '.config/sops').exists()
def test_decrypt_real_sops_to_private_output_without_transcript(case):
ambient_key(case) # Old target succeeds through ambient discovery and leaks.
result = make(case)
assert result.returncode == 0
assert 'synthetic-secret-payload' not in result.stdout + result.stderr
assert json.loads(case[4].read_text()) == {'credential': 'synthetic-secret-payload'}
assert case[4].stat().st_mode & 0o077 == 0
assert not list(case[4].parent.glob('.netkingdom-sops-*'))
def test_wrong_explicit_key_never_falls_back_to_home_or_inline_keys(case, tmp_path):
ambient_key(case)
wrong = tmp_path / 'wrong.key'
subprocess.run(['age-keygen', '-o', str(wrong)], capture_output=True, check=True)
env = {**case[2], 'NETKINGDOM_CUSTODY_AGE_KEY_FILE': str(wrong), 'SOPS_AGE_KEY': case[1].read_text()}
result = make(case, env=env)
assert result.returncode != 0
assert 'synthetic-secret-payload' not in result.stdout + result.stderr
assert not case[4].exists()
def test_absent_explicit_custody_refused_even_if_home_has_key(case):
ambient_key(case)
env = dict(case[2]); del env['NETKINGDOM_CUSTODY_AGE_KEY_FILE']
assert make(case, env=env).returncode != 0
assert not case[4].exists()
@pytest.mark.parametrize('kind', ['file', 'symlink'])
def test_existing_output_is_not_replaced(case, kind):
ambient_key(case)
if kind == 'file': case[4].write_bytes(b'original-output')
else: case[4].symlink_to(case[4].parent / 'absent')
assert make(case).returncode != 0
if kind == 'file': assert case[4].read_bytes() == b'original-output'
else: assert case[4].is_symlink()
def test_git_worktree_output_is_refused(case):
ambient_key(case)
subprocess.run(['git', 'init', str(case[0])], env=case[2], capture_output=True, check=True)
assert make(case, output=case[0] / 'plaintext.json').returncode != 0
assert not (case[0] / 'plaintext.json').exists()
def test_shared_output_directory_is_refused(case):
ambient_key(case)
case[4].parent.chmod(0o755)
assert make(case).returncode != 0
assert not case[4].exists()
def noisy_tool(case, tmp_path):
bin_dir = tmp_path / 'bin'; bin_dir.mkdir()
tool = bin_dir / 'sops'
tool.write_text('#!/bin/sh\necho synthetic-tool-secret\necho synthetic-error-secret >&2\nexit 23\n')
tool.chmod(0o755)
return {**case[2], 'PATH': str(bin_dir) + ':' + case[2]['PATH']}
def test_child_output_and_errors_are_not_exposed_on_failure(case, tmp_path):
result = make(case, env=noisy_tool(case, tmp_path))
assert result.returncode != 0
assert 'synthetic-tool-secret' not in result.stdout + result.stderr
assert 'synthetic-error-secret' not in result.stdout + result.stderr
assert not case[4].exists()
assert not list(case[4].parent.glob('.netkingdom-sops-*'))
def test_non_age_metadata_refuses_before_tool(case, tmp_path):
data = json.loads(case[3].read_text()); data['sops']['kms'] = [{'arn': 'synthetic'}]
case[3].write_text(json.dumps(data))
result = make(case, env=noisy_tool(case, tmp_path))
assert result.returncode != 0
assert 'age-only metadata' in result.stderr
assert 'synthetic-tool-secret' not in result.stdout
def test_filename_is_not_shell_code(case):
ambient_key(case)
odd = case[0] / 'cipher; touch SHELL_EXECUTED; #.json'
shutil.copyfile(case[3], odd)
result = make(case, source=odd)
assert result.returncode == 0
assert not (case[0] / 'SHELL_EXECUTED').exists()
assert case[4].exists()
def test_yaml_roundtrip(case, tmp_path):
public = (case[0] / 'keys/age.pub').read_text().strip()
clear = tmp_path / 'input.yaml'; clear.write_text('credential: synthetic-yaml-secret\n')
cipher = case[0] / 'cipher.yaml'
result = subprocess.run([SOPS, '--encrypt', '--age', public, str(clear)], cwd=tmp_path,
env=case[2], capture_output=True, check=True)
cipher.write_bytes(result.stdout)
result = make(case, source=cipher)
assert result.returncode == 0
assert 'synthetic-yaml-secret' not in result.stdout + result.stderr
assert 'synthetic-yaml-secret' in case[4].read_text()
def test_parser_error_never_echoes_input(case):
case[3].write_text('synthetic-malformed-secret')
result = make(case)
assert result.returncode != 0
assert 'synthetic-malformed-secret' not in result.stdout + result.stderr
assert not case[4].exists()
def test_shared_custody_key_is_refused(case):
case[1].chmod(0o644)
result = make(case)
assert result.returncode != 0
assert not case[4].exists()
def test_creds_init_has_no_permanent_key_requirement(case):
subprocess.run(['git', 'init', str(case[0])], env=case[2], capture_output=True, check=True)
hooks = case[0] / '.githooks'; hooks.mkdir()
(hooks / 'pre-commit').write_text('#!/bin/sh\nexit 0\n')
result = make(case, 'creds-init')
assert result.returncode == 0
assert not (Path(case[2]['HOME']) / '.config/sops').exists()
def test_real_sops_does_not_execute_ambient_key_commands(case, tmp_path):
marker = tmp_path / 'ambient-command-executed'
command = tmp_path / 'ambient-key-command'
command.write_text('#!/bin/sh\ntouch ' + str(marker) + '\nexit 1\n')
command.chmod(0o755)
env = {**case[2], 'SOPS_AGE_KEY_CMD': str(command), 'SOPS_AGE_SSH_PRIVATE_KEY_CMD': str(command)}
result = make(case, env=env)
assert result.returncode == 0
assert not marker.exists()
def test_yaml_parser_error_is_sanitized(case):
bad = case[0] / 'broken.yaml'
bad.write_text('secret: [synthetic-malformed-yaml-secret\n')
result = make(case, source=bad)
assert result.returncode != 0
assert 'synthetic-malformed-yaml-secret' not in result.stdout + result.stderr
assert 'Traceback' not in result.stderr
@pytest.mark.parametrize('kind', ['json', 'yaml'])
def test_duplicate_metadata_refused_before_sops(case, kind):
source = case[0] / ('duplicate.' + kind)
source.write_text('{"sops": {}, "sops": {}}' if kind == 'json' else 'sops: {}\nsops: {}\n')
result = make(case, source=source)
assert result.returncode != 0
assert 'duplicate' in result.stderr
@pytest.mark.parametrize('kind', ['bare', 'linked'])
def test_bare_and_linked_git_outputs_are_refused(case, kind):
location = case[4].parent / 'git-output'
if kind == 'bare':
subprocess.run(['git', 'init', '--bare', str(location)], env=case[2], capture_output=True, check=True)
else:
subprocess.run(['git', 'init', str(case[0])], env=case[2], capture_output=True, check=True)
subprocess.run(['git', '-c', 'user.name=Test', '-c', 'user.email=test@example.invalid',
'commit', '--allow-empty', '-m', 'fixture'], cwd=case[0], env=case[2],
capture_output=True, check=True)
subprocess.run(['git', 'worktree', 'add', '--detach', str(location)], cwd=case[0], env=case[2],
capture_output=True, check=True)
location.chmod(0o700)
result = make(case, output=location / 'plaintext.json')
assert result.returncode != 0
assert not (location / 'plaintext.json').exists()

View file

@ -0,0 +1,244 @@
"""Real SOPS write-path regressions with disposable keys and plaintext only."""
import json
import os
from pathlib import Path
import shutil
import subprocess
import pytest
ROOT=Path(__file__).resolve().parents[1]
SOPS=shutil.which('sops')
pytestmark=pytest.mark.skipif(SOPS is None,reason='real SOPS required')
PLAIN='synthetic-sops-write-value'
@pytest.fixture
def case(tmp_path):
repo=tmp_path/'repo';boot=repo/'sso-mfa/bootstrap';boot.mkdir(parents=True)
shutil.copyfile(ROOT/'Makefile',repo/'Makefile')
for name in ['sops-private.py','sops-write.py']:
if (ROOT/'sso-mfa/bootstrap'/name).exists():shutil.copyfile(ROOT/'sso-mfa/bootstrap'/name,boot/name)
(repo/'keys').mkdir()
key=tmp_path/'disposable.key';subprocess.run(['age-keygen','-o',str(key)],capture_output=True,check=True)
public=subprocess.check_output(['age-keygen','-y',str(key)],text=True).strip()
(repo/'keys/age.pub').write_text(public+'\n')
(repo/'.sops.yaml').write_text('creation_rules:\n - age: '+public+'\n')
home=tmp_path/'home';home.mkdir(mode=0o700)
private=tmp_path/'private';private.mkdir(mode=0o700)
replacement=private/'replacement.json';replacement.write_text(json.dumps({'credential':PLAIN+'-edited'}));replacement.chmod(0o600)
cipher=repo/'cipher.json'
env={'HOME':str(home),'PATH':os.environ['PATH'],'NETKINGDOM_CUSTODY_AGE_KEY_FILE':str(key),'SOPS_AGE_KEY_FILE':str(key)}
encrypted=subprocess.run([SOPS,'--encrypt','--age',public,'--input-type','json','--output-type','json','/dev/stdin'],input=json.dumps({'credential':PLAIN}).encode(),cwd=tmp_path,env=env,capture_output=True,check=True)
cipher.write_bytes(encrypted.stdout)
return {'repo':repo,'key':key,'public':public,'env':env,'cipher':cipher,'input':replacement,'tmp':tmp_path}
def invoke(case,action,*,file=None,input=None,env=None):
return subprocess.run(['make','-s','sops-'+action,'FILE='+str(file or case['cipher']),'INPUT='+str(input or case['input'])],cwd=case['repo'],env=env or case['env'],capture_output=True,text=True,timeout=15)
def decode(case,file=None):
result=subprocess.run([SOPS,'--decrypt',str(file or case['cipher'])],env=case['env'],capture_output=True,check=True)
return json.loads(result.stdout)
def quiet(result):
assert PLAIN not in result.stdout+result.stderr
assert 'Traceback' not in result.stdout+result.stderr
def plaintext(case,name='plain.json'):
path=case['repo']/name;path.write_text(json.dumps({'credential':PLAIN}));path.chmod(0o600);return path
def test_public_only_encrypt_roundtrip(case):
path=plaintext(case);env=dict(case['env']);env.pop('NETKINGDOM_CUSTODY_AGE_KEY_FILE');env.pop('SOPS_AGE_KEY_FILE')
result=invoke(case,'encrypt',file=path,env=env);assert result.returncode==0,result.stderr;quiet(result)
assert decode(case,path)=={'credential':PLAIN}
assert json.loads(path.read_text())['sops']['age'][0]['recipient']==case['public']
assert path.stat().st_mode & 0o077==0
def test_rotate_changes_ciphertext_not_plaintext_or_recipient(case):
before=case['cipher'].read_bytes();result=invoke(case,'rotate');assert result.returncode==0,result.stderr;quiet(result)
assert case['cipher'].read_bytes()!=before and decode(case)=={'credential':PLAIN}
assert [r['recipient'] for r in json.loads(case['cipher'].read_text())['sops']['age']]==[case['public']]
def test_edit_uses_private_replacement_and_never_launches_ambient_editor(case):
marker=case['tmp']/'editor-called';editor=case['tmp']/'editor'
editor.write_text('#!/bin/sh\ntouch '+str(marker)+'\nprintf '+PLAIN+'\\n >&2\n');editor.chmod(0o700)
result=invoke(case,'edit',env={**case['env'],'SOPS_EDITOR':str(editor),'EDITOR':str(editor)})
assert result.returncode==0,result.stderr;quiet(result)
assert not marker.exists() and decode(case)=={'credential':PLAIN+'-edited'}
assert json.loads(case['input'].read_text())=={'credential':PLAIN+'-edited'}
@pytest.mark.parametrize('name',['cipher; touch SHELL_EXECUTED; #.json','$(shell touch MAKE_EXECUTED).json','with spaces.json'])
@pytest.mark.parametrize('action',['encrypt','rotate','edit'])
def test_file_names_are_data_not_shell_or_make_programs(case,name,action):
file=case['repo']/name
if action=='encrypt':file=plaintext(case,name)
else:file.write_bytes(case['cipher'].read_bytes())
result=invoke(case,action,file=file);assert result.returncode==0,result.stderr;quiet(result)
assert not (case['repo']/'SHELL_EXECUTED').exists() and not (case['repo']/'MAKE_EXECUTED').exists()
assert decode(case,file)=={'credential':PLAIN+('-edited' if action=='edit' else '')}
@pytest.mark.parametrize('action',['rotate','edit'])
def test_explicit_key_required_despite_ambient_identity(case,action):
before=case['cipher'].read_bytes();env=dict(case['env']);env.pop('NETKINGDOM_CUSTODY_AGE_KEY_FILE')
result=invoke(case,action,env=env);assert result.returncode!=0;quiet(result)
assert case['cipher'].read_bytes()==before
@pytest.mark.parametrize('action',['rotate','edit'])
def test_unauthorized_recipient_metadata_is_not_a_rotation_grant(case,action):
other=case['tmp']/'other.key';subprocess.run(['age-keygen','-o',str(other)],capture_output=True,check=True)
public=subprocess.check_output(['age-keygen','-y',str(other)],text=True).strip()
data=json.loads(case['cipher'].read_text());data['sops']['age'].append({**data['sops']['age'][0],'recipient':public})
case['cipher'].write_text(json.dumps(data));before=case['cipher'].read_bytes()
result=invoke(case,action);assert result.returncode!=0;quiet(result);assert case['cipher'].read_bytes()==before
@pytest.mark.parametrize('defect',['public','symlink','hardlink','git','missing'])
def test_edit_refuses_unsafe_input_without_changing_cipher(case,defect):
file=case['input'];before=case['cipher'].read_bytes()
if defect=='public':file.chmod(0o644)
elif defect=='missing':file.unlink()
elif defect=='symlink':
target=file.with_name('other');file.rename(target);file.symlink_to(target)
elif defect=='hardlink':os.link(file,file.with_name('alias'))
else:subprocess.run(['git','init',str(file.parent)],capture_output=True,check=True)
result=invoke(case,'edit');assert result.returncode!=0;quiet(result);assert case['cipher'].read_bytes()==before
@pytest.mark.parametrize('defect',['symlink','hardlink','public-write'])
def test_cipher_path_cannot_redirect_or_alias_replacement(case,defect):
file=case['cipher'];before=file.read_bytes()
if defect=='symlink':
target=file.with_name('other.json');file.rename(target);file.symlink_to(target)
elif defect=='hardlink':os.link(file,file.with_name('alias.json'))
else:file.chmod(0o666)
result=invoke(case,'rotate');assert result.returncode!=0;quiet(result);assert file.read_bytes()==before
def test_tracked_plaintext_refused_before_encryption(case):
file=plaintext(case)
subprocess.run(['git','init',str(case['repo'])],capture_output=True,check=True)
subprocess.run(['git','add',file.name],cwd=case['repo'],capture_output=True,check=True)
before=file.read_bytes();result=invoke(case,'encrypt',file=file)
assert result.returncode!=0;quiet(result);assert file.read_bytes()==before
@pytest.mark.parametrize('action', ['edit', 'rotate'])
def test_tampered_cipher_cannot_be_replaced(case, action):
data=json.loads(case['cipher'].read_text());data['credential']='tampered';case['cipher'].write_text(json.dumps(data));before=case['cipher'].read_bytes()
result=invoke(case,action);assert result.returncode!=0;quiet(result);assert case['cipher'].read_bytes()==before
def test_encryption_does_not_run_ambient_key_command(case):
path=plaintext(case);marker=case['tmp']/'key-command';command=case['tmp']/'command'
command.write_text('#!/bin/sh\ntouch '+str(marker)+'\n');command.chmod(0o700)
env={**case['env'],'SOPS_AGE_KEY_CMD':str(command),'SOPS_KMS_ARN':'unadmitted','SOPS_CONFIG':str(case['repo']/'.sops.yaml')}
result=invoke(case,'encrypt',file=path,env=env);assert result.returncode==0,result.stderr;quiet(result);assert not marker.exists()
def test_malformed_plaintext_is_not_echoed_or_replaced(case):
path=plaintext(case);path.write_text('invalid '+PLAIN);before=path.read_bytes()
result=invoke(case,'encrypt',file=path);assert result.returncode!=0;quiet(result);assert path.read_bytes()==before
def test_make_input_is_literal(case):
replacement=case['input'].with_name('$(shell touch MAKE_INPUT_EXECUTED).json');case['input'].rename(replacement)
result=invoke(case,'edit',input=replacement);assert result.returncode==0,result.stderr
assert not (case['repo']/'MAKE_INPUT_EXECUTED').exists()
assert decode(case)=={'credential':PLAIN+'-edited'}
@pytest.mark.parametrize('defect',['hardlink','fifo','parent-symlink'])
def test_custody_key_alias_or_nonregular_input_is_refused(case,defect):
key=case['key'];env=dict(case['env']);before=case['cipher'].read_bytes()
if defect=='hardlink':os.link(key,key.with_name('alias.key'))
elif defect=='fifo':
key.unlink();os.mkfifo(key,0o600)
else:
parent=case['tmp']/'key-directory';parent.mkdir(mode=0o700);key.rename(parent/'key');alias=case['tmp']/'alias';alias.symlink_to(parent);env['NETKINGDOM_CUSTODY_AGE_KEY_FILE']=str(alias/'key')
result=invoke(case,'rotate',env=env);assert result.returncode!=0;quiet(result)
assert case['cipher'].read_bytes()==before
def test_edit_preserves_declared_encryption_policy(case):
raw=json.dumps({'credential':PLAIN,'visible_unencrypted':'public-label'}).encode()
encrypted=subprocess.run([SOPS,'--encrypt','--age',case['public'],'--input-type','json','--output-type','json','/dev/stdin'],input=raw,cwd=case['tmp'],env=case['env'],capture_output=True,check=True)
case['cipher'].write_bytes(encrypted.stdout)
case['input'].write_text(json.dumps({'credential':PLAIN+'-edited','visible_unencrypted':'public-label'}))
result=invoke(case,'edit');assert result.returncode==0,result.stderr
obj=json.loads(case['cipher'].read_text())
assert obj['visible_unencrypted']=='public-label' and obj['credential'].startswith('ENC[')
assert decode(case)=={'credential':PLAIN+'-edited','visible_unencrypted':'public-label'}
def test_child_diagnostics_and_plaintext_output_are_suppressed(case):
directory=case['tmp']/'bin';directory.mkdir();binary=directory/'sops'
binary.write_text('#!/bin/sh\nprintf "'+PLAIN+'\\n"\nprintf "'+PLAIN+'\\n" >&2\nexit 1\n');binary.chmod(0o700)
file=plaintext(case);before=file.read_bytes()
result=invoke(case,'encrypt',file=file,env={**case['env'],'PATH':str(directory)+':'+case['env']['PATH']})
assert result.returncode!=0;quiet(result);assert file.read_bytes()==before
assert not list(file.parent.glob('.netkingdom-sops-write-*'))
def test_source_change_during_encryption_is_not_overwritten(case):
file=plaintext(case);directory=case['tmp']/'bin';directory.mkdir();binary=directory/'sops'
binary.write_text('#!/usr/bin/python3\nimport subprocess,sys\nfrom pathlib import Path\nr=subprocess.run('+repr([SOPS])+ '+sys.argv[1:],input=sys.stdin.buffer.read(),capture_output=True)\nPath('+repr(str(file))+').write_text("concurrent-update")\nsys.stdout.buffer.write(r.stdout)\nsys.exit(r.returncode)\n');binary.chmod(0o700)
result=invoke(case,'encrypt',file=file,env={**case['env'],'PATH':str(directory)+':'+case['env']['PATH']})
assert result.returncode!=0;quiet(result);assert file.read_text()=='concurrent-update'
assert not list(file.parent.glob('.netkingdom-sops-write-*'))
def test_make_custody_command_cannot_execute_before_handoff(case):
helper=case['repo']/'sso-mfa/bootstrap/sops-custody-unlock.sh'
helper.write_text('#!/bin/bash\npython3 - "$@" <<\'PYTHON\'\nimport json,sys\nfrom pathlib import Path\nPath("handoff.json").write_text(json.dumps(sys.argv[1:]))\nPYTHON\n')
command="printf 'safe'; touch OUTSIDE_CUSTODY; #"
result=subprocess.run(['make','-s','sops-custody-run','COMMAND='+command],cwd=case['repo'],env=case['env'],capture_output=True,text=True,timeout=10)
assert result.returncode==0,result.stderr
assert not (case['repo']/'OUTSIDE_CUSTODY').exists()
assert json.loads((case['repo']/'handoff.json').read_text())[-3:]==['bash','-lc',command]
def test_yaml_encrypt_rotate_edit_roundtrip(case):
import yaml
file = case['repo'] / 'plain.yaml'
file.write_text('credential: ' + PLAIN + '\n'); file.chmod(0o600)
replacement = case['input'].with_suffix('.yaml')
replacement.write_text('credential: ' + PLAIN + '-edited\n'); replacement.chmod(0o600)
for action in ('encrypt', 'rotate', 'edit'):
result = invoke(case, action, file=file, input=replacement)
assert result.returncode == 0, result.stderr
quiet(result)
decrypted = subprocess.run([SOPS, '--decrypt', str(file)], env=case['env'], capture_output=True, check=True)
assert yaml.safe_load(decrypted.stdout) == {'credential': PLAIN + ('-edited' if action == 'edit' else '')}
def test_make_decrypt_output_is_literal(case):
output = case['input'].with_name('$(shell touch MAKE_OUTPUT_EXECUTED).json')
result = subprocess.run(['make', '-s', 'sops-decrypt', 'FILE=' + str(case['cipher']), 'OUTPUT=' + str(output)],
cwd=case['repo'], env=case['env'], capture_output=True, text=True, timeout=15)
assert result.returncode == 0, result.stderr
quiet(result)
assert not (case['repo'] / 'MAKE_OUTPUT_EXECUTED').exists()
assert json.loads(output.read_text()) == {'credential': PLAIN}
def test_make_custody_values_are_not_evaluated(case):
helper = case['repo'] / 'sso-mfa/bootstrap/sops-custody-unlock.sh'
helper.write_text('#!/bin/bash\nexec python3 -c \'import json,sys; from pathlib import Path; Path("handoff.json").write_text(json.dumps(sys.argv[1:]))\' "$@"\n')
command = '$(shell touch MAKE_COMMAND_EXECUTED)'
recipient = '$(shell touch MAKE_RECIPIENT_EXECUTED)'
result = subprocess.run(['make', '-s', 'sops-custody-run', 'COMMAND=' + command, 'OPERATOR_AGE_PUBKEY=' + recipient],
cwd=case['repo'], env=case['env'], capture_output=True, text=True, timeout=15)
assert result.returncode == 0, result.stderr
assert not (case['repo'] / 'MAKE_COMMAND_EXECUTED').exists()
assert not (case['repo'] / 'MAKE_RECIPIENT_EXECUTED').exists()
assert json.loads((case['repo'] / 'handoff.json').read_text()) == ['--expected-recipient', recipient, '--', 'bash', '-lc', command]

View file

@ -0,0 +1,296 @@
#!/usr/bin/env python3
"""Owner-run identity capture: metadata plan first, exact bounded execution later.
Use the admitted Railiance caller. Flags bind reviewed source/targets; they do
not grant authority. Private child output and provider diagnostics never print.
"""
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):
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
bundle=load('identity-bundle');prepare=load('prepare-identity-coverage')
IMAGE='ghcr.io/gpappsoft/privacyidea-docker@sha256:af7841adad262f129e0c1d4f553af13f21cb2f4dc713533f316cfe43ed0b4473'
SOURCES=['capture-identity.py','identity-files.py','identity-bundle.py','prepare-identity-coverage.py','identity-backup-profile.json']
KUBE=['kubectl']
class Refused(Exception):pass
def command(args,payload=None,timeout=60):
result=subprocess.run(KUBE+['--request-timeout=30s']+args,input=payload,
stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,timeout=timeout)
if result.returncode:raise Refused()
return result.stdout
def get(ns,kind,name):
scope=['-n',ns] if ns else []
return json.loads(command(scope+['get',kind,name,'-o','json']))
def selects(selector,labels):
if any(labels.get(k)!=v for k,v in selector.get('matchLabels',{}).items()):return False
for expression in selector.get('matchExpressions',[]):
key=expression['key'];op=expression['operator'];values=expression.get('values',[])
if op=='In' and labels.get(key) not in values:return False
if op=='NotIn' and key in labels and labels[key] in values:return False
if op=='Exists' and key not in labels:return False
if op=='DoesNotExist' and key in labels:return False
if op not in {'In','NotIn','Exists','DoesNotExist'}:raise Refused()
return True
def network_review(run_id,actual_labels=None):
rows=[]
for ns in ('sso','mfa'):
policies=json.loads(command(['-n',ns,'get','networkpolicies','-o','json']))['items']
for obj in policies:
m=obj['metadata'];s=obj['spec']
labels=(actual_labels or {}).get(ns,{'net-kingdom/capture':run_id})
if selects(s['podSelector'],labels) and (s.get('ingress') or s.get('egress')):
rows.append({'namespace':ns,'name':m['name'],'uid':m['uid'],
'spec_sha256':hashlib.sha256(json.dumps(s,sort_keys=True).encode()).hexdigest()})
return rows
def validate_helper(actual,wanted):
a=actual['spec'];w=wanted['spec']
if (any(a.get(k) for k in ('hostNetwork','hostPID','hostIPC','initContainers','ephemeralContainers'))
or a.get('automountServiceAccountToken') is not False or a.get('activeDeadlineSeconds')!=900
or len(a['containers'])!=1 or a['volumes']!=w['volumes']):raise Refused()
c=a['containers'][0];expected=w['containers'][0]
if any(c.get(k)!=expected[k] for k in ('name','image','command','volumeMounts')):raise Refused()
if c.get('env') or c.get('envFrom') or c.get('securityContext',{}).get('privileged'):raise Refused()
if any(c.get('securityContext',{}).get(k)!=v for k,v in expected['securityContext'].items()):raise Refused()
if any(a.get('securityContext',{}).get(k)!=v for k,v in w['securityContext'].items()):raise Refused()
def manifests(run_id):
result=[]
for ns,claims in [('sso',[('lldap','lldap-data'),('authelia','authelia-data'),('keycape','keycape-authentication-policy')]),
('mfa',[('privacyidea','privacyidea-data')])]:
label={'net-kingdom/capture':run_id};name='nk49-capture-'+run_id
result.append({'apiVersion':'networking.k8s.io/v1','kind':'NetworkPolicy',
'metadata':{'name':name,'namespace':ns},
'spec':{'podSelector':{'matchLabels':label},'policyTypes':['Ingress','Egress'],'ingress':[],'egress':[]}})
result.append({'apiVersion':'v1','kind':'Pod','metadata':{'name':name,'namespace':ns,'labels':label},
'spec':{'restartPolicy':'Never','activeDeadlineSeconds':900,'automountServiceAccountToken':False,
'securityContext':{'runAsUser':0,'runAsGroup':0},
'containers':[{'name':'capture','image':IMAGE,'imagePullPolicy':'IfNotPresent',
'command':['python3','-c','import time; time.sleep(900)'],
'securityContext':{'allowPrivilegeEscalation':False,'readOnlyRootFilesystem':True,
'capabilities':{'drop':['ALL'],'add':['DAC_OVERRIDE']}},
'resources':{'requests':{'cpu':'50m','memory':'128Mi'},'limits':{'cpu':'1','memory':'2Gi'}},
'volumeMounts':[{'name':key,'mountPath':'/source/'+key,'readOnly':True} for key,_ in claims]+
[{'name':'scratch','mountPath':'/scratch'}]}],
'volumes':[{'name':key,'persistentVolumeClaim':{'claimName':claim,'readOnly':True}} for key,claim in claims]+
[{'name':'scratch','emptyDir':{'medium':'Memory','sizeLimit':'512Mi'}}]}})
return result
def plan():
p=bundle.profile();binding=prepare.prepare(p,get=get)
cluster=get('databases','cluster','net-kingdom-pg')
primary=cluster['status']['currentPrimary'];pod=get('databases','pod',primary)
if (not any(o.get('uid')==cluster['metadata']['uid'] for o in pod['metadata'].get('ownerReferences',[]))
or not any(c['type']=='Ready' and c['status']=='True' for c in pod['status'].get('conditions',[]))):raise Refused()
run_id=uuid.uuid4().hex[:12]
return {'schema':'netkingdom.identity-capture-plan.v1','status':'requires_owner_execution_admission',
'created_at':datetime.now(timezone.utc).isoformat(),'run_id':run_id,'binding':binding,'kubeconfig':KUBE[2],
'sources':[{'path':name,'sha256':hashlib.sha256((HERE/name).read_bytes()).hexdigest()} for name in SOURCES],
'database':{'namespace':'databases','cluster':'net-kingdom-pg','cluster_uid':cluster['metadata']['uid'],
'pod':primary,'pod_uid':pod['metadata']['uid'],'database':'privacyidea_db'},
'create_resources':manifests(run_id),'service_changes':False,'credential_grants':False,
'matching_network_allow_policies':network_review(run_id),
'network_policy_enforcement_requires_owner_confirmation':True,
'offsite_delivery_included':False,'global_atomic_snapshot_claimed':False}
def checked_plan(path,expected):
raw=path.read_bytes()
if hashlib.sha256(raw).hexdigest()!=expected:raise Refused()
p=json.loads(raw)
if p.get('schema')!='netkingdom.identity-capture-plan.v1':raise Refused()
if p.get('kubeconfig')!=KUBE[2]:raise Refused()
if network_review(p['run_id']):raise Refused()
if p['sources']!=[{'path':name,'sha256':hashlib.sha256((HERE/name).read_bytes()).hexdigest()} for name in SOURCES]:raise Refused()
if p['create_resources']!=manifests(p['run_id']):raise Refused()
now=datetime.now(timezone.utc)
if not 0<=(now-datetime.fromisoformat(p['created_at'])).total_seconds()<3600:raise Refused()
# Recheck the exact immutable source/target bindings before private reads.
current=prepare.prepare(bundle.profile(),get=get)
if current['bindings']!=p['binding']['bindings'] or current['profile_sha256']!=p['binding']['profile_sha256']:raise Refused()
return p
def delete_created(obj):
m=obj['metadata'];ns=m['namespace'];name=m['name'];uid=m['uid']
if obj['kind']=='Pod':path=f'/api/v1/namespaces/{ns}/pods/{name}'
elif obj['kind']=='NetworkPolicy':path=f'/apis/networking.k8s.io/v1/namespaces/{ns}/networkpolicies/{name}'
else:raise Refused()
payload=json.dumps({'apiVersion':'v1','kind':'DeleteOptions','preconditions':{'uid':uid},'gracePeriodSeconds':1}).encode()
command(['delete','--raw',path,'-f','-'],payload)
if obj['kind']=='Pod':command(['-n',ns,'wait','--for=delete','pod/'+name,'--timeout=90s'],timeout=100)
def bounded_file(args,path,limit):
# Stream binary dump to private staging, with a byte bound and suppressed
# diagnostics. API command argv contains only public resource/database names.
proc=subprocess.Popen(KUBE+['--request-timeout=600s']+args,stdout=subprocess.PIPE,stderr=subprocess.DEVNULL)
try:
count=0
with path.open('xb') as out:
while block:=proc.stdout.read(1024*1024):
count+=len(block)
if count>limit:raise Refused()
out.write(block)
if proc.wait(timeout=600):raise Refused()
finally:
if proc.poll() is None:proc.kill();proc.wait()
def capture(p,output,receipt,decision,checkpoint=lambda:None):
created=[];receipt.update(status='capture_started',decision_ref=decision,plan_run_id=p['run_id'],
created_resources=[],unverified_create_outcomes=[],helper_cleanup_verified=False,temporary_plaintext_cleanup_verified=False)
checkpoint()
with tempfile.TemporaryDirectory(prefix='nk49-identity-',dir='/tmp') as temporary:
receipt['temporary_plaintext_directory']=temporary
checkpoint()
root=Path(temporary)/'payload';root.mkdir(mode=0o700)
held={}
try:
for c in bundle.profile()['components']:
for kind,names in [('deployment',[c['deployment']]),('secret',c['secret_refs']),('configmap',c['configmap_refs'])]:
for name in names:
obj=get(c['namespace'],kind,name)
# Keep reconstructable deployed resources inside encryption,
# without unrelated stale last-applied annotations.
m=obj['metadata'];obj['metadata']={k:m[k] for k in ['name','namespace','uid','resourceVersion']}
obj.pop('status',None)
raw=json.dumps(obj,sort_keys=True).encode();slot=f"resources/{c['namespace']}/{kind}/{name}.json"
held[(c['namespace'],kind,name)]=(m['uid'],m['resourceVersion'])
bundle.save(root,slot,raw)
for proposed in p['create_resources']:
# create, never adopt/apply an existing resource. Unknown create
# outcomes stop; only returned exact UIDs are eligible for cleanup.
pending={'kind':proposed['kind'],**proposed['metadata']}
receipt['unverified_create_outcomes'].append(pending)
checkpoint()
obj=json.loads(command(['create','-f','-','-o','json'],json.dumps(proposed).encode()))
if (obj['kind']!=proposed['kind'] or obj['metadata']['name']!=proposed['metadata']['name']
or obj['metadata']['namespace']!=proposed['metadata']['namespace'] or not obj['metadata'].get('uid')):raise Refused()
created.append(obj)
receipt['unverified_create_outcomes'].remove(pending)
receipt['created_resources'].append({'namespace':obj['metadata']['namespace'],
'kind':obj['kind'],'name':obj['metadata']['name'],'uid':obj['metadata']['uid']})
checkpoint()
for ns in ('sso','mfa'):
name='nk49-capture-'+p['run_id']
command(['-n',ns,'wait','--for=condition=Ready','pod/'+name,'--timeout=180s'],timeout=190)
live=get(ns,'pod',name)
expected=next(o for o in created if o['kind']=='Pod' and o['metadata']['namespace']==ns)
if live['metadata']['uid']!=expected['metadata']['uid']:raise Refused()
wanted=next(o for o in p['create_resources'] if o['kind']=='Pod' and o['metadata']['namespace']==ns)
validate_helper(live,wanted)
if network_review(p['run_id'],{ns:live['metadata']['labels']}):raise Refused()
raw=command(['-n',ns,'exec','-i',name,'-c','capture','--','python3','-c',
(HERE/'identity-files.py').read_text(),ns],timeout=600)
if len(raw)>bundle.LIMIT+2*1024*1024:raise Refused()
bundle.import_files(raw,root,ns)
if get(ns,'pod',name)['metadata']['uid']!=expected['metadata']['uid']:raise Refused()
db=p['database'];current=get('databases','cluster','net-kingdom-pg')
if current['metadata']['uid']!=db['cluster_uid'] or current['status']['currentPrimary']!=db['pod']:raise Refused()
if get('databases','pod',db['pod'])['metadata']['uid']!=db['pod_uid']:raise Refused()
(root/'databases').mkdir(mode=0o700)
bounded_file(['-n','databases','exec',db['pod'],'-c','postgres','--','pg_dump','-U','postgres',
'-d','privacyidea_db','-Fc','--no-owner','--no-acl'],root/'databases/privacyidea.dump',bundle.FILE_LIMIT)
if get('databases','pod',db['pod'])['metadata']['uid']!=db['pod_uid']:raise Refused()
for (ns,kind,name),binding in held.items():
m=get(ns,kind,name)['metadata']
if (m['uid'],m['resourceVersion'])!=binding:raise Refused()
# Re-read original keys after the database dump. No rotation is
# inferred from file presence; a changed key set refuses publication.
raw=command(['-n','mfa','exec','-i','nk49-capture-'+p['run_id'],'-c','capture','--','python3','-c',
(HERE/'identity-files.py').read_text(),'mfa'],timeout=60)
with tempfile.TemporaryDirectory(prefix='nk49-key-recheck-',dir='/tmp') as keytemp:
again=Path(keytemp);bundle.import_files(raw,again,'mfa')
for name in ('enckey','private.pem','public.pem'):
if (root/'data/privacyidea'/name).read_bytes()!=(again/'data/privacyidea'/name).read_bytes():raise Refused()
receipt.update(bundle.seal(root,output,{'started_at':receipt['started_at'],
'finished_at':datetime.now(timezone.utc).isoformat(),'database':db,'decision_ref':decision,
'cluster_uid':prepare.CLUSTER_UID,'global_atomic_snapshot_claimed':False,
'omissions':['historical local backups','SQLite WAL/SHM/journal represented by native snapshots',
'Authelia notification.txt','privacyIDEA service logs; audit data retained in database',
'OpenBao root/unseal/age private key custody remains independently held']}))
finally:
failures=[]
for obj in reversed(created):
try:delete_created(obj)
except Exception:failures.append(obj['metadata']['uid'])
receipt['helper_cleanup_verified']=not failures and not receipt['unverified_create_outcomes']
checkpoint()
if not receipt['helper_cleanup_verified']:raise Refused()
receipt['temporary_plaintext_cleanup_verified']=not Path(temporary).exists()
checkpoint()
if not receipt['temporary_plaintext_cleanup_verified']:raise Refused()
def main():
global KUBE
parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('action',choices=['plan','capture'])
parser.add_argument('--kubeconfig',required=True)
parser.add_argument('--receipt',required=True);parser.add_argument('--plan');parser.add_argument('--plan-sha256')
parser.add_argument('--output');parser.add_argument('--decision-id');a=parser.parse_args();os.umask(0o077)
KUBE=['kubectl','--kubeconfig',str(Path(a.kubeconfig).absolute())]
fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
os.close(fd)
r={'status':'failed','started_at':datetime.now(timezone.utc).isoformat(),'secret_values_exported':False}
def checkpoint():
fd,temp=tempfile.mkstemp(prefix='.capture-receipt-',dir=Path(a.receipt).absolute().parent)
try:
with os.fdopen(fd,'w') as out:json.dump(r,out,indent=2);out.write('\n');out.flush();os.fsync(out.fileno())
os.replace(temp,a.receipt)
finally:Path(temp).unlink(missing_ok=True)
code=1
try:
if a.action=='plan':
if any([a.plan,a.plan_sha256,a.output,a.decision_id]):raise Refused()
r=plan();code=0
else:
if not all([a.plan,a.plan_sha256,a.output,a.decision_id]):raise Refused()
uuid.UUID(a.decision_id)
p=checked_plan(Path(a.plan),a.plan_sha256)
capture(p,Path(a.output),r,a.decision_id,checkpoint);code=0
except Exception:
r.update(status='failed',error='capture_refused_or_failed_private_diagnostics_suppressed')
if r.get('temporary_plaintext_directory') and not Path(r['temporary_plaintext_directory']).exists():r['temporary_plaintext_cleanup_verified']=True
finally:
r['finished_at']=datetime.now(timezone.utc).isoformat()
checkpoint()
print('OK: identity '+a.action+' complete; see safe receipt.' if code==0 else 'ERROR: identity operation failed; inspect safe receipt before retry.')
return code
def interrupted(*args):raise Refused()
if __name__=='__main__':
for sig in (signal.SIGHUP,signal.SIGTERM,signal.SIGINT):signal.signal(sig,interrupted)
raise SystemExit(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,88 @@
#!/usr/bin/env python3
"""Fetch one fixed ciphertext through Platform's attended Backup-account lane.
No key read, plaintext, redirects, proxies, grants, deletion or production writes.
The object and byte count are bound to the reviewed inventory. Provider diagnostics
and credentials remain private. Output is a fresh private directory under /tmp.
"""
import argparse
import base64
from datetime import datetime, timezone
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import tempfile
import urllib.request
HERE=Path(__file__).resolve().parent
spec=importlib.util.spec_from_file_location('inventory',HERE/'inventory-storage-share.py')
owner=importlib.util.module_from_spec(spec);spec.loader.exec_module(owner)
OBJECT='r01-net-kingdom-pg/net-kingdom-pg-privacyidea_db-20261015T023000Z.dump.age'
SIZE=89129309
def metadata(response,url):
etag=response.headers.get('ETag','')
if (response.status!=200 or response.url!=url or
response.headers.get('Content-Length')!=str(SIZE) or
len(etag)>200 or not etag.startswith('"') or not etag.endswith('"') or
any(ord(c)<32 for c in etag)):
raise owner.Refused()
return etag
def transfer(opener,auth,destination):
url=owner.HOST+owner.BASE+OBJECT
headers={'Authorization':'Basic '+auth,'Accept-Encoding':'identity'}
with opener.open(urllib.request.Request(url,method='HEAD',headers=headers),timeout=30) as response:
etag=metadata(response,url)
headers['If-Match']=etag
digest=hashlib.sha256();count=0
with opener.open(urllib.request.Request(url,headers=headers),timeout=60) as response:
if metadata(response,url)!=etag:raise owner.Refused()
fd=os.open(destination,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
with os.fdopen(fd,'wb') as out:
while chunk:=response.read(1024*1024):
count+=len(chunk)
if count>SIZE:raise owner.Refused()
digest.update(chunk);out.write(chunk)
out.flush();os.fsync(out.fileno())
if count!=SIZE:raise owner.Refused()
with opener.open(urllib.request.Request(url,method='HEAD',headers=headers),timeout=30) as response:
if metadata(response,url)!=etag:raise owner.Refused()
# ETag used for request binding only; it is not asserted to be a content hash.
return {'ciphertext_bytes':count,'ciphertext_sha256':digest.hexdigest(),
'object_unchanged_during_transfer':True}
def main():
p=argparse.ArgumentParser(description=__doc__);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.privacyidea-offhost-transfer.v1','status':'failed',
'started_at':datetime.now(timezone.utc).isoformat(),'provider':'Storage Share',
'object':OBJECT,'age_keys_read':False,'plaintext_created':False,
'production_writes':False,'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
'owner_accessor_sha256':hashlib.sha256((HERE/'inventory-storage-share.py').read_bytes()).hexdigest()}
destination=None
try:
identity,credentials=owner.caller_credentials()
r['caller_auth_path']=identity['path'];r['caller_entity_id']=identity.get('entity_id')
auth=base64.b64encode((credentials['BACKUP_USERNAME']+':'+credentials['BACKUP_PASSWORD']).encode()).decode()
destination=Path(tempfile.mkdtemp(prefix='nk49-offhost-',dir='/tmp'))/'privacyidea.dump.age'
opener=urllib.request.build_opener(urllib.request.ProxyHandler({}),owner.NoRedirect())
r.update(transfer(opener,auth,destination))
r.update(status='ciphertext_fetched',ciphertext_path=str(destination))
except Exception:
r['error']='bounded_transfer_failed_diagnostics_suppressed'
if destination is not None:
destination.unlink(missing_ok=True);destination.parent.rmdir()
finally:
r['finished_at']=datetime.now(timezone.utc).isoformat()
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,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,254 @@
#!/usr/bin/env python3
"""Attended Platform/Backup owner ceremony for two exact new backup shares.
Requires approved, hash-bound CCRs. No operator password/age key is delivered to
the runtime; only a folder-scoped share token is stored with initial KV CAS=0.
"""
import argparse
import base64
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import subprocess
import sys
import urllib.error
import urllib.parse
import urllib.request
import uuid
HERE=Path(__file__).resolve().parent
PLATFORM=HERE.parents[1].parent/'railiance-platform'
sys.path.insert(0,str(PLATFORM/'scripts'))
from state_hub_preflight_lane import bao,data,capabilities,revoke
from repair_eso_kubernetes_auth import role_payload,check_role
from telemetry_smtp_delivery import ensure
def load(name,path):
spec=importlib.util.spec_from_file_location(name,path);m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m);return m
inventory=load('storage_inventory',HERE/'inventory-storage-share.py')
offsite=load('offsite',HERE/'identity-offsite.py')
FOLDER='/railiance-backups/net-kingdom-identity'
PROFILES={'publisher':(48,21),'retention':(49,25)}
class Refused(Exception):pass
def contract(action,expected):
number,permission=PROFILES[action]
path=PLATFORM/f'credential-change-requests/CCR-2026-{number:04d}-net-kingdom-backup-{action}.yaml'
raw=path.read_bytes()
if hashlib.sha256(raw).hexdigest()!=expected:raise Refused()
module=load('credential_change',PLATFORM/'scripts/credential-change.py')
ccr,errors,_=module.validate_ccr(path)
if errors or ccr['status'] not in {'approved','applied','verified','active'}:raise Refused()
required={'platform-owner','net-kingdom-owner','backup-share-owner'}
approved={r['reviewer'].removeprefix('User (').removesuffix(')') for r in ccr.get('review',{}).get('comments',[])
if r.get('decision')=='approved' and r.get('source_ref')}
name='net-kingdom-backup-'+action+'-eso'
expected_path='platform/workloads/net-kingdom/identity-backup/'+action
a=ccr['openbao'];provider=ccr['provider']
if (not required<=approved or a['kv_path']!=expected_path or a['fields']!=['NC_WEBDAV_TOKEN']
or a['policy_name']!=name or a['auth']['role']!=name
or a['auth']['bound_claims']!={'service_account_names':[name],'service_account_namespaces':['sso']}
or provider['folder']!=FOLDER or provider['share_permissions']!=permission or provider['share_type']!=3
or provider['label']!='net-kingdom-identity-'+action
or provider.get('provider_added_resharing_approved') is not True):raise Refused()
if (a.get('metadata_read') is not False or a.get('token_self_lifecycle') is not True
or a['auth'].get('audience')!='openbao' or a['auth'].get('token_no_default_policy') is not True
or any(a['auth'].get(k)!='15m' for k in ['ttl','token_max_ttl','token_explicit_max_ttl'])
or a['auth'].get('policies')!=[name] or provider.get('host')!='nx4069.your-storageshare.de'):raise Refused()
lane={'service_account':name,'namespace':'sso','role':name,'policy':name,
'kv_path':expected_path.replace('platform/','platform/data/',1)}
wanted=('path "'+lane['kv_path']+'" { capabilities = ["read"] }\n'
'path "auth/token/lookup-self" { capabilities = ["read"] }\n'
'path "auth/token/revoke-self" { capabilities = ["update"] }\n')
if (PLATFORM/a['policy_file']).read_text()!=wanted:raise Refused()
return ccr,lane
def request(client,url,auth,method='GET',body=None,extra=None):
if not url.startswith(offsite.HOST+'/'):raise Refused()
h={'Authorization':'Basic '+base64.b64encode((auth[0]+':'+auth[1]).encode()).decode(),
'X-Requested-With':'XMLHttpRequest'};h.update(extra or {})
try:
with client.open(urllib.request.Request(url,data=body,method=method,headers=h),timeout=60) as response:
if response.url!=url:raise Refused()
raw=response.read(1024*1024+1)
if len(raw)>1024*1024:raise Refused()
return response.status,raw
except urllib.error.HTTPError as error:return error.code,b''
def ocs(client,auth,suffix='',fields=None):
url=offsite.HOST+'/ocs/v2.php/apps/files_sharing/api/v1/shares'+suffix
body=urllib.parse.urlencode(fields).encode() if fields is not None else None
status,raw=request(client,url,auth,'POST' if fields is not None else 'GET',body,
{'OCS-APIRequest':'true','Accept':'application/json','Content-Type':'application/x-www-form-urlencoded'})
if status not in (200,201):raise Refused()
parsed=json.loads(raw)['ocs']
if parsed['meta']['statuscode'] not in (100,200):raise Refused()
return parsed['data']
def share(client,auth,action,receipt):
owner=offsite.HOST+'/remote.php/dav/files/Backup'+FOLDER
status,_=request(client,owner,auth,'MKCOL')
if status not in (201,405):raise Refused()
receipt['folder_created']=status==201
matches=[s for s in ocs(client,auth,'?path='+urllib.parse.quote(FOLDER,safe=''))
if s.get('label')=='net-kingdom-identity-'+action]
if len(matches)>1:raise Refused()
if matches:result=matches[0];receipt['share_created']=False
else:
result=ocs(client,auth,fields={'path':FOLDER,'shareType':3,'permissions':PROFILES[action][1],
'label':'net-kingdom-identity-'+action});receipt['share_created']=True
if (result.get('path')!=FOLDER or int(result.get('permissions',-1))!=PROFILES[action][1]
or int(result.get('share_type',-1))!=3 or not result.get('token')):raise Refused()
offsite.headers(result['token'])
receipt.update(share_id=str(result['id']),share_permissions=int(result['permissions']),folder=FOLDER)
return result['token']
def boundary(client,auth,token,action,receipt):
name='nk49-boundary-'+uuid.uuid4().hex+'.age'
payload=subprocess.run(['age','-r',json.loads((HERE/'identity-backup-profile.json').read_text())['public_recipient']],
input=b'NK49 disposable provider boundary fixture',stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,timeout=10)
if payload.returncode:raise Refused()
owner=offsite.HOST+'/remote.php/dav/files/Backup'+FOLDER+'/'+name
public=offsite.BASE+name;created=False;deleted=False
receipt['provider_test_artifacts']=[name]
receipt['boundary_fixture_cleanup_verified']=False
try:
receipt['boundary_phase']='create_owner_fixture'
status,_=request(client,owner,auth,'PUT',payload.stdout,{'If-None-Match':'*'})
if status!=201:raise Refused()
created=True
receipt['boundary_phase']='read_public_fixture'
status,raw=request(client,public,(token,''))
if status!=200 or raw!=payload.stdout:raise Refused()
receipt['boundary_phase']='deny_public_overwrite'
status,_=request(client,public,(token,''),'PUT',payload.stdout)
if status not in (401,403):raise Refused()
denied_name='nk49-absent-'+uuid.uuid4().hex+'.age'
if action=='retention':receipt['provider_test_artifacts'].append(denied_name)
status,_=request(client,offsite.BASE+denied_name,(token,''),'PUT',payload.stdout,{'If-None-Match':'*'}) if action=='retention' else (403,b'')
if status not in (401,403):raise Refused()
receipt['boundary_phase']='public_delete'
status,_=request(client,public,(token,''),'DELETE')
if action=='publisher':
if status not in (401,403):raise Refused()
new=offsite.BASE+'nk49-boundary-'+uuid.uuid4().hex+'.age'
receipt['provider_test_artifacts'].append(new.rsplit('/',1)[-1])
receipt['boundary_phase']='create_public_fixture'
code,_=request(client,new,(token,''),'PUT',payload.stdout,{'If-None-Match':'*'})
if code!=201:raise Refused()
# This fixture belongs to the current ceremony, not retained data.
receipt['boundary_phase']='cleanup_public_fixture'
remove_fixture(client,offsite.HOST+'/remote.php/dav/files/Backup'+FOLDER+'/'+new.rsplit('/',1)[-1],auth,payload.stdout)
else:
if status!=204:raise Refused()
deleted=True
receipt['boundary_phase']='deny_parent_path'
status,_=request(client,offsite.BASE+'%2e%2e/r01-net-kingdom-pg/',(token,''))
receipt['parent_probe_status']=status
if status not in (400,401,403,404):raise Refused()
receipt['parent_path_rejected']=True
# A malformed-path400 proves request rejection, not authorization.
# Separately probe a known existing account folder with only this share
# token: it must not authenticate as the Backup account.
receipt['boundary_phase']='deny_other_account_folder'
status,_=request(client,offsite.HOST+'/remote.php/dav/files/Backup/r01-net-kingdom-pg/',(token,''))
receipt['other_account_folder_status']=status
if status not in (401,403,404):raise Refused()
receipt['provider_boundary_verified']=True
finally:
if created and not deleted:
remove_fixture(client,owner,auth,payload.stdout)
receipt['owner_fixture_cleanup_verified']=True
# A rejected operation may have had an unexpected side effect. Only a
# successful complete boundary test claims cleanup; names remain in the
# receipt for owner reconciliation on every failure.
receipt['boundary_fixture_cleanup_verified']=receipt.get('provider_boundary_verified',False)
def remove_fixture(client,url,auth,expected):
h={'Authorization':'Basic '+base64.b64encode((auth[0]+':'+auth[1]).encode()).decode()}
with client.open(urllib.request.Request(url,method='HEAD',headers=h),timeout=30) as response:
version=offsite.etag(response,url,len(expected))
code,raw=request(client,url,auth,extra={'If-Match':version})
if code!=200 or raw!=expected:raise Refused()
code,_=request(client,url,auth,'DELETE',extra={'If-Match':version})
if code!=204:raise Refused()
def run(action,expected,receipt):
ccr,lane=contract(action,expected)
identity,credentials=inventory.caller_credentials()
receipt['caller_entity_id']=identity.get('entity_id');receipt['caller_auth_path']=identity['path']
client=offsite.opener();auth=(credentials['BACKUP_USERNAME'],credentials['BACKUP_PASSWORD'])
# Use the owner quota accessor only with proxies removed from its environment.
quota=load('owner_quota',PLATFORM/'scripts/migrate_nextcloud_backup_account.py')
quota.request=lambda url,method='GET',body=None,auth=None,headers=None:request(client,url,auth,method,body,headers)
receipt['quota_before']=quota.quota(auth)
if receipt['quota_before']['available_bytes']<offsite.HEADROOM+offsite.LIMIT:raise Refused()
token=share(client,auth,action,receipt)
boundary(client,auth,token,action,receipt)
if action=='publisher':
receipt['publisher_quota_available']=offsite.quota(client,token)
if receipt['publisher_quota_available']<offsite.HEADROOM+offsite.LIMIT:raise Refused()
# Fence agent reads before any initial secret publication.
boundary_path='sys/policies/acl/agent-high-risk-boundary'
current=data(bao(['read','-format=json',boundary_path]))['data']['policy'];added=''
for path in [lane['kv_path'],lane['kv_path'].replace('/data/','/metadata/',1)]:
stanza='path "'+path+'" { capabilities = ["deny"] }\n'
if '"'+path+'"' not in current:added+=stanza
elif stanza.strip() not in current:raise Refused()
if added:
if data(bao(['read','-format=json',boundary_path]))['data']['policy']!=current:raise Refused()
bao(['write',boundary_path,'-'],payload={'policy':current+'\n'+added})
if data(bao(['read','-format=json',boundary_path]))['data']['policy']!=current+'\n'+added:raise Refused()
receipt['coding_agent_fence_verified_before_publication']=True
existing=bao(['read','-format=json',lane['kv_path']],allow_failure=True)
if existing.returncode==0:
native=data(existing)['data']
if native['data']!={'NC_WEBDAV_TOKEN':token}:raise Refused()
receipt['kv_version']=native['metadata']['version'];receipt['existing_identical_custody']=True
else:
if b'No value found' not in existing.stdout+existing.stderr:raise Refused()
native=data(bao(['write','-format=json',lane['kv_path'],'-'],payload={'options':{'cas':0},'data':{'NC_WEBDAV_TOKEN':token}}))
receipt['kv_version']=native['data']['version']
policy=(PLATFORM/ccr['openbao']['policy_file']).read_text()
expected_policy=('path "'+lane['kv_path']+'" { capabilities = ["read"] }\n'
'path "auth/token/lookup-self" { capabilities = ["read"] }\n'
'path "auth/token/revoke-self" { capabilities = ["update"] }\n')
if policy!=expected_policy:raise Refused()
ensure('sys/policies/acl/'+lane['policy'],{'policy':policy},lambda actual: None if actual['policy']==policy else (_ for _ in ()).throw(Refused()))
ensure('auth/kubernetes/role/'+lane['role'],role_payload(lane),lambda actual:check_role(actual,lane))
child=data(bao(['token','create','-format=json','-policy='+lane['policy'],'-policy=agent-high-risk-boundary','-no-default-policy','-ttl=60s']))['auth']['client_token']
try:
if any(v!=['deny'] for v in capabilities(child,[lane['kv_path'],lane['kv_path'].replace('/data/','/metadata/',1)]).values()):raise Refused()
finally:revoke(child)
receipt.update(status='provider_and_reader_prepared',coding_agents_denied=True,escrow_read=False,
operator_password_delivered=False,retained_backups_deleted=False,source_ccr=ccr['id'],ccr_sha256=expected,
eso_delivery_and_native_login_verification_pending=True)
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('action',choices=PROFILES)
p.add_argument('--ccr-sha256',required=True);p.add_argument('--receipt',required=True);a=p.parse_args()
fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
r={'status':'failed','action':a.action,'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest()}
try:run(a.action,a.ccr_sha256,r)
except Exception:r.update(status='failed',error='contained_setup_failed_inspect_partial_state_before_retry')
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,144 @@
{
"apiVersion": "v1",
"kind": "List",
"items": [
{
"apiVersion": "v1",
"kind": "ServiceAccount",
"metadata": {
"name": "net-kingdom-backup-publisher-eso",
"namespace": "sso"
},
"automountServiceAccountToken": false
},
{
"apiVersion": "external-secrets.io/v1",
"kind": "SecretStore",
"metadata": {
"name": "net-kingdom-backup-publisher",
"namespace": "sso"
},
"spec": {
"provider": {
"vault": {
"server": "http://openbao.openbao.svc:8200",
"path": "platform",
"version": "v2",
"auth": {
"kubernetes": {
"mountPath": "kubernetes",
"role": "net-kingdom-backup-publisher-eso",
"serviceAccountRef": {
"name": "net-kingdom-backup-publisher-eso",
"audiences": [
"openbao"
]
}
}
}
}
}
}
},
{
"apiVersion": "external-secrets.io/v1",
"kind": "ExternalSecret",
"metadata": {
"name": "net-kingdom-backup-publisher",
"namespace": "sso"
},
"spec": {
"refreshInterval": "1h",
"secretStoreRef": {
"kind": "SecretStore",
"name": "net-kingdom-backup-publisher"
},
"target": {
"name": "net-kingdom-backup-publisher",
"creationPolicy": "Owner",
"template": {
"type": "Opaque"
}
},
"data": [
{
"secretKey": "token",
"remoteRef": {
"key": "workloads/net-kingdom/identity-backup/publisher",
"property": "NC_WEBDAV_TOKEN"
}
}
]
}
},
{
"apiVersion": "v1",
"kind": "ServiceAccount",
"metadata": {
"name": "net-kingdom-backup-retention-eso",
"namespace": "sso"
},
"automountServiceAccountToken": false
},
{
"apiVersion": "external-secrets.io/v1",
"kind": "SecretStore",
"metadata": {
"name": "net-kingdom-backup-retention",
"namespace": "sso"
},
"spec": {
"provider": {
"vault": {
"server": "http://openbao.openbao.svc:8200",
"path": "platform",
"version": "v2",
"auth": {
"kubernetes": {
"mountPath": "kubernetes",
"role": "net-kingdom-backup-retention-eso",
"serviceAccountRef": {
"name": "net-kingdom-backup-retention-eso",
"audiences": [
"openbao"
]
}
}
}
}
}
}
},
{
"apiVersion": "external-secrets.io/v1",
"kind": "ExternalSecret",
"metadata": {
"name": "net-kingdom-backup-retention",
"namespace": "sso"
},
"spec": {
"refreshInterval": "1h",
"secretStoreRef": {
"kind": "SecretStore",
"name": "net-kingdom-backup-retention"
},
"target": {
"name": "net-kingdom-backup-retention",
"creationPolicy": "Owner",
"template": {
"type": "Opaque"
}
},
"data": [
{
"secretKey": "token",
"remoteRef": {
"key": "workloads/net-kingdom/identity-backup/retention",
"property": "NC_WEBDAV_TOKEN"
}
}
]
}
}
]
}

View file

@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""Private in-helper PVC capture. Stdout is a secret-bearing tar: parent MUST capture.
Sources are mounted read-only; SQLite uses its online backup API, never raw copy
or immutable=1. No production file is generated, replaced, chmod'd or removed.
"""
import hashlib
import io
import os
from pathlib import Path
import signal
import sqlite3
import stat
import sys
import tarfile
import tempfile
MAX_FILE=256*1024*1024
MAX_TOTAL=512*1024*1024
MAX_COUNT=2048
class Refused(Exception):pass
def read(path):
if any(p.is_symlink() for p in (path,*path.parents)):raise Refused()
fd=os.open(path,os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK)
try:
before=os.fstat(fd)
if not stat.S_ISREG(before.st_mode) or before.st_nlink!=1 or before.st_size>MAX_FILE:raise Refused()
with os.fdopen(os.dup(fd),'rb') as stream:data=stream.read(MAX_FILE+1)
after=os.stat(path,follow_symlinks=False)
if (len(data)>MAX_FILE or b'AGE-SECRET-KEY-' in data or
any(getattr(before,k)!=getattr(after,k) for k in ('st_dev','st_ino','st_size','st_mtime_ns','st_ctime_ns'))):raise Refused()
return data
finally:os.close(fd)
def tree(root,database=None,omit_notification=False,omit_backups=True):
result={}
for directory,dirs,files in os.walk(root,followlinks=False):
path=Path(directory)
if path.is_symlink():raise Refused()
# Historical local snapshots are redundant; do not recursively include
# accumulated backups. WAL/SHM are represented by the native snapshot.
if omit_backups:dirs[:]=[name for name in dirs if name!='backups']
for name in dirs:
if (path/name).is_symlink():raise Refused()
for name in files:
relative=(path/name).relative_to(root).as_posix()
if database and relative in {database,database+'-wal',database+'-shm',database+'-journal'}:continue
if omit_notification and relative=='notification.txt':continue
result[relative]=read(path/name)
if len(result)>MAX_COUNT or sum(map(len,result.values()))>MAX_TOTAL:raise Refused()
return result
def snapshot(source,target):
if source.is_symlink() or not source.is_file():raise Refused()
# A read-only URI intentionally fails if the engine needs a write to its
# production WAL/SHM. Never fall back to a raw copy or mutate the mount.
with sqlite3.connect(source.as_uri()+'?mode=ro',uri=True,timeout=10) as origin:
with sqlite3.connect(target) as dest:
origin.backup(dest,pages=128,sleep=0.05)
if dest.execute('PRAGMA integrity_check').fetchall()!=[('ok',)]:raise Refused()
return read(target)
def capture(mode,root=Path('/source'),scratch=Path('/scratch')):
values={}
with tempfile.TemporaryDirectory(prefix='snapshot-',dir=scratch) as temporary:
if mode=='sso':
for name,database in [('lldap','users.db'),('authelia','db.sqlite3'),('keycape',None)]:
source=root/name
if source.is_symlink() or not source.is_dir():raise Refused()
first=tree(source,database,name=='authelia',name!='keycape')
if database:first[database]=snapshot(source/database,Path(temporary)/(name+'.sqlite'))
second=tree(source,database,name=='authelia',name!='keycape')
if {k:v for k,v in first.items() if k!=database}!=second:raise Refused()
values.update({f'data/{name}/{k}':v for k,v in first.items()})
elif mode=='mfa':
first=tree(root/'privacyidea')
if not {'enckey','private.pem','public.pem'}<=set(first) or first!=tree(root/'privacyidea'):raise Refused()
values={f'data/privacyidea/{name}':value for name,value in first.items()}
else:raise Refused()
if len(values)>MAX_COUNT or sum(map(len,values.values()))>MAX_TOTAL:raise Refused()
return values
def emit(values,destination):
with tarfile.open(fileobj=destination,mode='w|') as archive:
for name,value in sorted(values.items()):
member=tarfile.TarInfo(name);member.size=len(value);member.mode=0o600
archive.addfile(member,io.BytesIO(value))
def interrupted(*args):raise Refused()
if __name__=='__main__':
for sig in (signal.SIGTERM,signal.SIGHUP,signal.SIGINT):signal.signal(sig,interrupted)
try:
os.umask(0o077)
if len(sys.argv)!=2:raise Refused()
emit(capture(sys.argv[1]),sys.stdout.buffer)
except Exception:
print('ERROR: private file capture refused; diagnostics suppressed.',file=sys.stderr)
raise SystemExit(1)

View file

@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""Plan two exact DNS-selector exclusions; apply only an owner-admitted hash-bound plan."""
import argparse
import hashlib
import importlib.util
import json
import os
from pathlib import Path
HERE=Path(__file__).resolve().parent
spec=importlib.util.spec_from_file_location('capture',HERE/'capture-identity.py')
capture=importlib.util.module_from_spec(spec);spec.loader.exec_module(capture)
SELECTOR={'matchExpressions':[{'key':'net-kingdom/capture','operator':'DoesNotExist'}]}
def plan():
if capture.get(None,'namespace','kube-system')['metadata']['uid']!=capture.prepare.CLUSTER_UID:raise capture.Refused()
rows=[]
for ns in ('sso','mfa'):
obj=capture.get(ns,'networkpolicy','allow-egress-dns');s=obj['spec'];m=obj['metadata']
if s['podSelector'] not in ({},SELECTOR):raise capture.Refused()
pods=json.loads(capture.command(['-n',ns,'get','pods','-o','json']))['items']
if any('net-kingdom/capture' in p['metadata'].get('labels',{}) for p in pods):raise capture.Refused()
# Preserve every peer, port and policy type. Existing unlabelled service
# pods retain their exact selector match. UID/RV/spec tests guard drift.
patch=[{'op':'test','path':'/metadata/uid','value':m['uid']},
{'op':'test','path':'/metadata/resourceVersion','value':m['resourceVersion']},
{'op':'test','path':'/spec','value':s},
{'op':'replace','path':'/spec/podSelector','value':SELECTOR}]
rows.append({'namespace':ns,'name':m['name'],'uid':m['uid'],'patch':patch})
return {'schema':'netkingdom.identity-isolation.v1','status':'prepared_not_applied',
'kubeconfig':capture.KUBE[2],'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),'policies':rows}
def apply(packet,receipt,dry_run=False):
if packet!=plan():raise capture.Refused()
for row in packet['policies']:
args=['-n',row['namespace'],'patch','networkpolicy',row['name'],'--type=json','-p',json.dumps(row['patch']),'-o','json']
if dry_run:args+=['--dry-run=server']
receipt['attempted'].append({'namespace':row['namespace'],'name':row['name'],'uid':row['uid']})
actual=json.loads(capture.command(args));expected=row['patch'][2]['value'].copy();expected['podSelector']=SELECTOR
if actual['metadata']['uid']!=row['uid'] or actual['spec']!=expected:raise capture.Refused()
receipt['verified'].append({'namespace':row['namespace'],'name':row['name'],'uid':row['uid']})
receipt['status']='server_dry_run_passed' if dry_run else 'capture_dns_exclusions_verified'
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('action',choices=['plan','dry-run','apply'])
p.add_argument('--kubeconfig',required=True);p.add_argument('--plan');p.add_argument('--plan-sha256');p.add_argument('--receipt',required=True)
a=p.parse_args();os.umask(0o077);capture.KUBE=['kubectl','--kubeconfig',a.kubeconfig]
fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
r={'status':'failed','attempted':[],'verified':[]}
try:
if a.action=='plan':r=plan()
else:
raw=Path(a.plan).read_bytes()
if hashlib.sha256(raw).hexdigest()!=a.plan_sha256:raise capture.Refused()
apply(json.loads(raw),r,a.action=='dry-run')
except Exception:r.update(status='failed',error='isolation_refused_review_partial_attempts')
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,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,106 @@
#!/usr/bin/env python3
"""Separate folder-scoped read/delete executor for verified new identity backups.
Unknown/unindexed files are never eligible. The recovered point, newest point,
seven daily and two weekly points remain protected. No grant or account login.
"""
from datetime import datetime,timezone
import hashlib
import importlib.util
import json
from pathlib import Path
import re
import tempfile
import urllib.parse
import urllib.request
import xml.etree.ElementTree as ET
spec=importlib.util.spec_from_file_location('offsite',Path(__file__).with_name('identity-offsite.py'))
offsite=importlib.util.module_from_spec(spec);spec.loader.exec_module(offsite)
def listing(client,token):
h=offsite.headers(token);h.update({'Depth':'1','Content-Type':'application/xml'})
body=b'<d:propfind xmlns:d="DAV:"><d:prop><d:resourcetype/><d:getcontentlength/><d:getetag/></d:prop></d:propfind>'
with client.open(urllib.request.Request(offsite.BASE,data=body,method='PROPFIND',headers=h),timeout=30) as response:
if response.status!=207 or response.url!=offsite.BASE:raise offsite.Refused()
raw=response.read(4*1024*1024+1)
if len(raw)>4*1024*1024:raise offsite.Refused()
result={}
for response in ET.fromstring(raw).findall('{DAV:}response'):
href=urllib.parse.urlsplit(response.findtext('{DAV:}href') or '')
if href.netloc and href.netloc!=urllib.parse.urlsplit(offsite.HOST).netloc:raise offsite.Refused()
name=urllib.parse.unquote(href.path.rstrip('/').rsplit('/',1)[-1])
for row in response.findall('{DAV:}propstat'):
if ' 200 ' not in (row.findtext('{DAV:}status') or ''):continue
p=row.find('{DAV:}prop')
if p is None or p.find('{DAV:}resourcetype/{DAV:}collection') is not None:continue
is_index=name.endswith('.receipt.json') and offsite.NAME.fullmatch(name.removesuffix('.receipt.json'))
if not offsite.NAME.fullmatch(name) and not is_index:continue
size=p.findtext('{DAV:}getcontentlength') or '';version=p.findtext('{DAV:}getetag') or ''
if not size.isdigit() or not re.fullmatch(r'"[^"\x00-\x20]{1,180}"',version) or name in result:raise offsite.Refused()
result[name]={'bytes':int(size),'etag':version}
if len(result)>10000:raise offsite.Refused()
return result
def generations(client,token,objects):
result=[]
for name,meta in objects.items():
if not offsite.NAME.fullmatch(name) or name+'.receipt.json' not in objects:continue
index=objects[name+'.receipt.json']
if not 0<index['bytes']<=65536:raise offsite.Refused()
url=offsite.BASE+name+'.receipt.json';h=offsite.headers(token);h['If-Match']=index['etag']
with client.open(urllib.request.Request(url,headers=h),timeout=30) as response:
if offsite.etag(response,url,index['bytes'])!=index['etag']:raise offsite.Refused()
raw=response.read(65537)
if len(raw)!=index['bytes']:raise offsite.Refused()
r=json.loads(raw)
if (r.get('schema')!='netkingdom.identity-offsite-index.v1' or r.get('status')!='readback_verified'
or r.get('artifact')!=name or r.get('ciphertext_bytes')!=meta['bytes'] or not 0<meta['bytes']<=offsite.LIMIT
or not re.fullmatch('[a-f0-9]{64}',r.get('ciphertext_sha256',''))):raise offsite.Refused()
stamp=datetime.strptime(name.removeprefix('net-kingdom-identity-')[:16],'%Y%m%dT%H%M%SZ').replace(tzinfo=timezone.utc)
captured=datetime.fromisoformat(r['captured_at'])
if captured.tzinfo is None or abs((captured-stamp).total_seconds())>3600:raise offsite.Refused()
if stamp>datetime.now(timezone.utc):raise offsite.Refused()
result.append({**r,'stamp':stamp,'etag':meta['etag'],'index_etag':index['etag']})
return sorted(result,key=lambda r:r['stamp'],reverse=True)
def select(rows,protected):
if not rows or protected not in {r['artifact'] for r in rows}:raise offsite.Refused()
keep={protected,rows[0]['artifact']};days=set();weeks=set()
for row in rows:
day=row['stamp'].date();week=row['stamp'].isocalendar()[:2]
if day not in days and len(days)<7:keep.add(row['artifact']);days.add(day)
if week not in weeks and len(weeks)<2:keep.add(row['artifact']);weeks.add(week)
return [r for r in rows if r['artifact'] not in keep],keep
def run(client,token,accepted,receipt,execute=False,checkpoint=lambda:None):
if (accepted.get('status')!='identity_archive_verified' or accepted.get('container_cleanup_verified') is not True
or accepted.get('temporary_plaintext_cleanup_verified') is not True or not offsite.NAME.fullmatch(accepted.get('artifact',''))):raise offsite.Refused()
objects=listing(client,token);rows=generations(client,token,objects)
candidates,keep=select(rows,accepted['artifact'])
recovered=next(r for r in rows if r['artifact']==accepted['artifact'])
if recovered['ciphertext_sha256']!=accepted['ciphertext_sha256']:raise offsite.Refused()
receipt.update(status='retention_preview',protected=accepted['artifact'],keep=sorted(keep),
candidates=[r['artifact'] for r in candidates],deleted=[],unknown_objects_untouched=True)
checkpoint()
# Validate retained known-good and newest ciphertext before any deletion.
with tempfile.TemporaryDirectory(prefix='nk49-retained-check-',dir='/tmp') as temporary:
for index,row in enumerate({r['artifact']:r for r in [recovered,rows[0]]}.values()):
offsite.readback(client,token,row['artifact'],row['ciphertext_bytes'],row['ciphertext_sha256'],Path(temporary)/str(index))
if not execute:return
for index,row in enumerate(candidates):
version=offsite.readback(client,token,row['artifact'],row['ciphertext_bytes'],row['ciphertext_sha256'],Path(temporary)/('expire-'+str(index)))
for name,etag in [(row['artifact'],version),(row['artifact']+'.receipt.json',row['index_etag'])]:
h=offsite.headers(token);h['If-Match']=etag
receipt['deletion_in_progress']=name
checkpoint()
with client.open(urllib.request.Request(offsite.BASE+name,method='DELETE',headers=h),timeout=30) as response:
if response.status!=204 or response.url!=offsite.BASE+name:raise offsite.Refused()
receipt['deleted'].append(name)
checkpoint()
receipt.pop('deletion_in_progress',None)
receipt['status']='retention_applied'

View file

@ -0,0 +1,215 @@
#!/usr/bin/env python3
"""Root-owned Railiance backup job. No operator password or age private key.
Publisher and retention are separate processes with separate scoped delivery.
Scheduled execution is gated by an independently recovered archive receipt.
"""
import argparse
import base64
from datetime import datetime,timezone
import fcntl
import importlib.util
import json
import os
from pathlib import Path
import signal
import shutil
import socket
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
capture=load('capture-identity');offsite=load('identity-offsite');retention=load('identity-retention')
def write(path,value):
fd,name=tempfile.mkstemp(prefix='.identity-receipt-',dir=path.parent)
try:
with os.fdopen(fd,'w') as out:json.dump(value,out,indent=2);out.write('\n');out.flush();os.fsync(out.fileno())
os.replace(name,path)
finally:Path(name).unlink(missing_ok=True)
def token(action):
name='net-kingdom-backup-'+action
es=capture.get('sso','externalsecret',name)
entries=[]
for entry in es['spec'].get('data',[]):
item=dict(entry);ref=dict(item.get('remoteRef',{}))
for key,default in [('conversionStrategy','Default'),('decodingStrategy','None'),('metadataPolicy','None')]:
if ref.pop(key,default)!=default:raise offsite.Refused()
item['remoteRef']=ref;entries.append(item)
if (es['spec']['target']['name']!=name or entries!=[{'secretKey':'token','remoteRef':{
'key':'workloads/net-kingdom/identity-backup/'+action,'property':'NC_WEBDAV_TOKEN'}}]
or es['spec'].get('dataFrom') or es['spec'].get('secretStoreRef')!={'kind':'SecretStore','name':name}
or not any(c['type']=='Ready' and c['status']=='True' for c in es['status'].get('conditions',[]))):raise offsite.Refused()
secret=capture.get('sso','secret',name)
if (set(secret['data'])!={'token'} or not any(o['uid']==es['metadata']['uid'] for o in secret['metadata'].get('ownerReferences',[]))):raise offsite.Refused()
value=base64.b64decode(secret['data']['token'],validate=True).decode();offsite.headers(value)
return value
def accepted(path):
capture.bundle.private(path)
r=json.loads(path.read_text())
if (r.get('schema')!='netkingdom.identity-recovery.v1' or r.get('status')!='identity_archive_verified'
or r.get('all_manifest_hashes_verified') is not True or r.get('container_cleanup_verified') is not True
or r.get('temporary_plaintext_cleanup_verified') is not True or not offsite.NAME.fullmatch(r.get('artifact',''))):raise offsite.Refused()
return r
def post_event(state,summary):
# Non-secret durable reporting: replay retained notes at the next run.
directory=state/'event-outbox';directory.mkdir(mode=0o700,exist_ok=True)
import uuid
path=directory/(uuid.uuid4().hex+'.json')
write(path,{'summary':summary,'event_type':'note','author':'net-kingdom-identity-backup',
'workplan_id':'17d315f0-829e-57c7-85cc-3a7914152804','task_id':'d68cd4d3-1ba3-530e-88f9-26ebb8dd6eac'})
for pending in sorted(directory.glob('*.json'))[:64]:
request=urllib.request.Request('http://10.43.68.154:8000/progress/',data=pending.read_bytes(),
headers={'Content-Type':'application/json'},method='POST')
try:
with urllib.request.build_opener(urllib.request.ProxyHandler({})).open(request,timeout=10) as response:
if response.status!=201:break
pending.unlink()
except Exception:break
def backup(state,decision,first=False):
if not first:accepted(state/'accepted-recovery.json')
if (state/'capture-in-progress.json').exists():raise offsite.Refused()
publisher=token('publisher')
client=offsite.opener()
if offsite.quota(client,publisher)<offsite.HEADROOM+offsite.LIMIT:raise offsite.Refused()
if any(shutil.disk_usage(path).free<2*offsite.LIMIT+offsite.HEADROOM for path in [state,Path('/tmp')]):raise offsite.Refused()
p=capture.plan();stamp=datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')
name='net-kingdom-identity-'+stamp+'-'+p['run_id']+'.tar.age'
directory=state/'runs'/name;directory.mkdir(mode=0o700,parents=True)
plan_path=directory/'plan.json';write(plan_path,p)
import hashlib
capture.checked_plan(plan_path,hashlib.sha256(plan_path.read_bytes()).hexdigest())
source=directory/name;r={'started_at':datetime.now(timezone.utc).isoformat(),'status':'capture_started'}
record=directory/'capture.json';write(record,r)
write(state/'capture-in-progress.json',{'receipt':str(record),'artifact':name})
try:capture.capture(p,source,r,decision,lambda:write(record,r))
except Exception:
if r.get('temporary_plaintext_directory') and not Path(r['temporary_plaintext_directory']).exists():r['temporary_plaintext_cleanup_verified']=True
r['status']='failed';write(record,r);raise
finally:
if r.get('helper_cleanup_verified') and r.get('temporary_plaintext_cleanup_verified'):
(state/'capture-in-progress.json').unlink()
write(record,r)
r['finished_at']=datetime.now(timezone.utc).isoformat();write(record,r)
write(directory/'transfer-attempt.json',{'status':'pending','artifact':name,'capture_receipt':'capture.json',
'ciphertext_sha256':r['ciphertext_sha256'],'ciphertext_bytes':r['ciphertext_bytes']})
remote=offsite.publish(client,publisher,source,r,directory/'fetched.tar.age')
write(directory/'offsite.json',remote)
# The original private local ciphertext and safe receipts remain. Every
# successful fetched duplicate is unnecessary after first acceptance and
# may be removed by the owner under a separate local-cache policy.
write(state/'last-success.json',{'finished_at':datetime.now(timezone.utc).isoformat(),
'artifact':name,'directory':str(directory),'ciphertext_sha256':remote['ciphertext_sha256'],
'offsite_verified':True})
post_event(state,'Identity backup completed: encrypted full coverage captured, Storage Share upload and downloaded digest verified. Native service authentication remains separate evidence.')
def expire(state,execute=False):
if (state/'retention-blocked.json').exists():raise offsite.Refused()
proof=accepted(state/'accepted-recovery.json')
r={'status':'pending','started_at':datetime.now(timezone.utc).isoformat()}
stamp=datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S%fZ')
record=state/('retention-'+stamp+'.json')
write(record,r)
try:
retention.run(offsite.opener(),token('retention'),proof,r,execute,lambda:write(record,r))
if execute:r['local_generations_removed']=prune_local(state,proof)
except Exception:
r['status']='failed';write(record,r)
# Stop future automatic expiration after an unknown/partial outcome.
# Owner review must explicitly clear this marker; no blind retries.
if execute:write(state/'retention-blocked.json',{'receipt':str(record),'requires_owner_reconciliation':True})
raise
write(record,r)
def prune_local(state,proof):
root=state/'runs';rows=[];known={}
if not root.exists():return []
capture.bundle.private(root,True)
for directory in root.iterdir():
if directory.is_symlink() or not directory.is_dir() or not offsite.NAME.fullmatch(directory.name):continue
capture.bundle.private(directory,True)
allowed={'plan.json','capture.json','transfer-attempt.json','offsite.json','fetched.tar.age',directory.name}
if {p.name for p in directory.iterdir()}-allowed:continue
try:
remote=json.loads((directory/'offsite.json').read_text());local=json.loads((directory/'capture.json').read_text())
if (remote.get('status')!='readback_verified' or remote.get('verified_index_published') is not True
or remote.get('artifact')!=directory.name or remote.get('ciphertext_sha256')!=local.get('ciphertext_sha256')
or local.get('helper_cleanup_verified') is not True or local.get('temporary_plaintext_cleanup_verified') is not True):continue
for path in directory.iterdir():capture.bundle.private(path,max_bytes=offsite.LIMIT)
if offsite.digest(directory/directory.name)!=remote['ciphertext_sha256']:continue
stamp=datetime.strptime(directory.name.removeprefix('net-kingdom-identity-')[:16],'%Y%m%dT%H%M%SZ').replace(tzinfo=timezone.utc)
rows.append({'artifact':directory.name,'stamp':stamp});known[directory.name]=directory
except Exception:continue
rows.sort(key=lambda r:r['stamp'],reverse=True)
if not rows:return []
protected=proof['artifact'] if proof['artifact'] in known else rows[0]['artifact']
candidates,_=retention.select(rows,protected)
removed=[]
for row in candidates:
directory=known[row['artifact']]
for path in directory.iterdir():path.unlink()
directory.rmdir();removed.append(row['artifact'])
return removed
def main():
p=argparse.ArgumentParser(description=__doc__)
p.add_argument('action',choices=['first-capture','scheduled','retention-preview','retention','status'])
p.add_argument('--kubeconfig',required=True);p.add_argument('--state',default='/var/lib/net-kingdom-backup')
p.add_argument('--decision-id');a=p.parse_args();os.umask(0o077)
state=Path(a.state).absolute()
ready=False
try:
capture.bundle.require_runtime_host()
capture.KUBE=['kubectl','--kubeconfig',a.kubeconfig]
uid=capture.get(None,'namespace','kube-system')['metadata']['uid']
if uid!=capture.prepare.CLUSTER_UID:raise offsite.Refused()
state.mkdir(mode=0o700,parents=True,exist_ok=True);capture.bundle.private(state,True);ready=True
fd=None
try:
if a.action!='status':
fd=os.open(state/'.lock',os.O_RDWR|os.O_CREAT|os.O_NOFOLLOW,0o600)
fcntl.flock(fd,fcntl.LOCK_EX|fcntl.LOCK_NB)
if a.action in {'first-capture','scheduled'}:
import uuid
uuid.UUID(a.decision_id or '')
backup(state,a.decision_id,a.action=='first-capture')
elif a.action.startswith('retention'):expire(state,a.action=='retention')
else:
last=json.loads((state/'last-success.json').read_text())
age=(datetime.now(timezone.utc)-datetime.fromisoformat(last['finished_at'])).total_seconds()
if not 0<=age<=25*3600:raise offsite.Refused()
finally:
if fd is not None:os.close(fd)
except Exception:
if ready:
try:post_event(state,'Identity backup '+a.action+' failed or refused. Private diagnostics suppressed; inspect root-owned receipts and reconcile partial state before retry.')
except Exception:pass
print('ERROR: identity backup operation failed; inspect private owner receipts. No secret diagnostics emitted.');return 1
print('OK: identity backup '+a.action+' completed.');return 0
def interrupted(*args):raise offsite.Refused()
if __name__=='__main__':
for sig in (signal.SIGHUP,signal.SIGTERM,signal.SIGINT):signal.signal(sig,interrupted)
raise SystemExit(main())

View file

@ -0,0 +1,145 @@
#!/usr/bin/env python3
"""Render/install a source-bound node runtime; activation needs actual recovery.
Install does not enable timers. Owner applies the separately reviewed delivery
and capture-isolation changes, performs first capture and independent recovery,
then explicitly activates. No credential values enter this package.
"""
import argparse
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import shutil
import socket
import subprocess
import uuid
ROOT=Path(__file__).resolve().parents[2]
HERE=Path(__file__).resolve().parent
FILES=[f'tools/bootstrap-recovery/{name}' for name in ['capture-identity.py','identity-files.py','identity-bundle.py',
'identity-runtime.py','identity-offsite.py','identity-retention.py','identity-backup-profile.json',
'prepare-identity-coverage.py','install-identity-backup.py']]+['sso-mfa/k8s/privacyidea/key-custody-remote.py']
STATE='/var/lib/net-kingdom-backup'
KUBECONFIG='/etc/rancher/k3s/k3s.yaml'
class Refused(Exception):pass
def render(decision):
uuid.UUID(decision)
files=[{'path':name,'sha256':hashlib.sha256((ROOT/name).read_bytes()).hexdigest()} for name in FILES]
release=hashlib.sha256(json.dumps(files,sort_keys=True).encode()).hexdigest()
directory='/opt/net-kingdom-backup/releases/'+release
runtime=directory+'/tools/bootstrap-recovery/identity-runtime.py'
common=f' --kubeconfig {KUBECONFIG} --state {STATE}'
base=('User=root\nGroup=root\nUMask=0077\nStateDirectory=net-kingdom-backup\nStateDirectoryMode=0700\n'
'NoNewPrivileges=true\nPrivateTmp=true\nPrivateDevices=true\nProtectSystem=strict\nProtectHome=true\n'
f'ReadWritePaths={STATE}\nCapabilityBoundingSet=\nRestrictAddressFamilies=AF_UNIX AF_INET AF_INET6\n'
'ProtectKernelTunables=true\nProtectKernelModules=true\nProtectControlGroups=true\n'
'MemoryMax=3G\nCPUQuota=200%\nTasksMax=128\nTimeoutStartSec=45min\nTimeoutStopSec=5min\n')
command='/usr/bin/python3 -I '+runtime
service=('[Unit]\nDescription=Encrypted full identity backup and verified Storage Share readback\nAfter=network-online.target k3s.service\n'
f'ConditionPathExists={STATE}/accepted-recovery.json\n[Service]\nType=oneshot\n'+base+
f'ExecStartPre={command} retention{common}\nExecStart={command} scheduled{common} --decision-id {decision}\n'
f'ExecStartPost={command} retention{common}\n')
health=('[Unit]\nDescription=Identity backup age check with durable failure reporting\nAfter=network-online.target k3s.service\n'
'[Service]\nType=oneshot\n'+base+f'ExecStart={command} status{common}\n')
units={'net-kingdom-identity-backup.service':service,
'net-kingdom-identity-backup.timer':'[Unit]\nDescription=Daily full identity recovery bundle\n[Timer]\nOnCalendar=*-*-* 03:45:00 UTC\nPersistent=true\nRandomizedDelaySec=5min\nUnit=net-kingdom-identity-backup.service\n[Install]\nWantedBy=timers.target\n',
'net-kingdom-identity-backup-health.service':health,
'net-kingdom-identity-backup-health.timer':'[Unit]\nDescription=Hourly identity backup freshness check\n[Timer]\nOnCalendar=hourly\nPersistent=true\nUnit=net-kingdom-identity-backup-health.service\n[Install]\nWantedBy=timers.target\n'}
return {'schema':'netkingdom.identity-runtime-install.v1','status':'prepared_not_installed','decision_ref':decision,
'release_id':release,'release_directory':directory,'runtime_files':files,'units':units,
'timers_automatically_enabled':False,'first_capture_command':command+' first-capture'+common+' --decision-id '+decision,
'activation_requires_independent_recovery':True,'source_contains_credentials':False}
def run(args):
r=subprocess.run(args,stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,timeout=120)
if r.returncode:raise Refused()
return r.stdout
def module(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
def owner():
module('identity-bundle').require_runtime_host()
capture=module('capture-identity');capture.KUBE=['kubectl','--kubeconfig',KUBECONFIG]
if capture.get(None,'namespace','kube-system')['metadata']['uid']!=capture.prepare.CLUSTER_UID:raise Refused()
def install(packet):
owner();expected=render(packet['decision_ref'])
if packet!=expected:raise Refused()
destination=Path(packet['release_directory'])
# Refuse differing prior units before changing files. Installation never
# overwrites a running/unknown version as an implicit upgrade.
for name,value in packet['units'].items():
path=Path('/etc/systemd/system')/name
if path.is_symlink() or (path.exists() and path.read_text()!=value):raise Refused()
destination.mkdir(mode=0o700,parents=True,exist_ok=True)
for row in packet['runtime_files']:
target=destination/row['path'];target.parent.mkdir(mode=0o700,parents=True,exist_ok=True)
if target.exists():
if target.is_symlink() or hashlib.sha256(target.read_bytes()).hexdigest()!=row['sha256']:raise Refused()
else:
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((ROOT/row['path']).read_bytes())
if hashlib.sha256(target.read_bytes()).hexdigest()!=row['sha256']:raise Refused()
for name,value in packet['units'].items():
path=Path('/etc/systemd/system')/name
if not path.exists():
fd=os.open(path,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o644)
with os.fdopen(fd,'w') as out:out.write(value)
run(['systemd-analyze','verify',*[str(Path('/etc/systemd/system')/name) for name in packet['units']]])
run(['systemctl','daemon-reload'])
def activate(proof):
owner();runtime=module('identity-runtime');r=runtime.accepted(proof)
state=Path(STATE);runtime.capture.bundle.private(state,True)
last=json.loads((state/'last-success.json').read_text())
if last.get('offsite_verified') is not True:raise Refused()
# Restore proof names the exact protected artifact; require its local
# off-site receipt too, not a synthetic JSON worksheet.
path=state/'runs'/r['artifact']/'offsite.json'
runtime.capture.bundle.private(path);transfer=json.loads(path.read_text())
if transfer.get('status')!='readback_verified' or transfer.get('ciphertext_sha256')!=r['ciphertext_sha256']:raise Refused()
target=state/'accepted-recovery.json'
if target.exists():
if target.is_symlink() or json.loads(target.read_text())!=r:raise Refused()
else:
fd=os.open(target,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
with os.fdopen(fd,'w') as out:json.dump(r,out,indent=2);out.write('\n')
run(['systemctl','start','net-kingdom-identity-backup-health.service'])
run(['systemctl','enable','--now','net-kingdom-identity-backup.timer','net-kingdom-identity-backup-health.timer'])
for unit in ['net-kingdom-identity-backup.timer','net-kingdom-identity-backup-health.timer']:
if run(['systemctl','is-active',unit]).strip()!=b'active':raise Refused()
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('action',choices=['render','install','activate'])
p.add_argument('--decision-id');p.add_argument('--packet');p.add_argument('--packet-sha256');p.add_argument('--accepted-receipt')
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'}
try:
if a.action=='render':r=render(a.decision_id)
elif a.action=='install':
raw=Path(a.packet).read_bytes()
if hashlib.sha256(raw).hexdigest()!=a.packet_sha256:raise Refused()
packet=json.loads(raw);install(packet)
r={'status':'installed_timers_disabled','release_id':packet['release_id']}
else:
activate(Path(a.accepted_receipt));r={'status':'timers_active_after_independent_recovery'}
except Exception:r={'status':'failed','error':'owner_install_or_activation_refused_private_diagnostics_suppressed'}
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,104 @@
#!/usr/bin/env python3
"""Read-only bounded S3 backup metadata through existing scoped runtime delivery.
No bootstrap API key, new grant, object content read, age key, provider write or
retention action. The caller must already have admitted cluster read authority.
"""
import argparse
import base64
from datetime import datetime, timezone
import hashlib
import json
import os
from pathlib import Path
import re
import subprocess
BUCKET='railiance-platform-pg-backup'
ENDPOINT='https://s3.nl-ams.scw.cloud'
PREFIX='platform-pg/'
CLUSTER='a553c742-0115-43d4-99a4-a5ca56fe0786'
class Refused(Exception):pass
def kube(args):
p=subprocess.run(['kubectl',*args,'-o','json'],stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,timeout=30)
if p.returncode:raise Refused()
return json.loads(p.stdout)
def list_prefix(client,prefix):
if not prefix.startswith(PREFIX) or not re.fullmatch(r'[A-Za-z0-9._/-]+',prefix) or '..' in prefix.split('/'):raise Refused()
directories=[];files=[];continuation=None
for _ in range(20):
args={'Bucket':BUCKET,'Prefix':prefix,'Delimiter':'/','MaxKeys':1000}
if continuation:args['ContinuationToken']=continuation
page=client.list_objects_v2(**args)
for row in page.get('CommonPrefixes',[]):
value=row['Prefix']
if not value.startswith(prefix) or not value.endswith('/') or not re.fullmatch(r'[A-Za-z0-9._/-]+',value) or '..' in value.split('/'):raise Refused()
directories.append(value)
for row in page.get('Contents',[]):
name=row['Key']
if not name.startswith(prefix) or not re.fullmatch(r'[A-Za-z0-9._/-]+',name) or '..' in name.split('/'):raise Refused()
if name.endswith(('.age','.json','.yaml')):
size=row['Size']
if type(size) is not int or size<0:raise Refused()
files.append({'key':name,'bytes':size})
if not page.get('IsTruncated'):break
continuation=page.get('NextContinuationToken')
if not continuation:raise Refused()
else:raise Refused()
return {'directories':sorted(set(directories)),'files':sorted(files,key=lambda x:x['key'])}
def inventory(client):
pending=[(PREFIX,0)];result={}
while pending:
prefix,depth=pending.pop(0)
if prefix in result:raise Refused()
if len(result)>=64:raise Refused()
row=list_prefix(client,prefix);result[prefix]=row
if depth<3:
for child in row['directories']:
# Native database WAL/base trees are not application/key-file
# archives. Record their presence without enumerating every WAL.
if any(x in child.split('/') for x in ['wals','base']):continue
pending.append((child,depth+1))
return result
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--receipt',required=True)
a=p.parse_args();fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
r={'schema':'netkingdom.scaleway-backup-inventory.v1','status':'failed',
'started_at':datetime.now(timezone.utc).isoformat(),'read_only':True,
'provider':'Scaleway','bucket':BUCKET,'prefix':PREFIX,'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
'bootstrap_api_key_used':False,'age_keys_read':False,'object_contents_downloaded':False,
'secret_values_exported':False,'scope':'Bounded prefix metadata, at most three child levels; WAL/base trees not enumerated; other buckets/prefixes unassessed'}
try:
if kube(['get','namespace','kube-system'])['metadata']['uid']!=CLUSTER:raise Refused()
delivered=kube(['-n','databases','get','secret','platform-pg-backup-s3'])
if delivered['metadata']['namespace']!='databases' or delivered['metadata']['name']!='platform-pg-backup-s3':raise Refused()
values=delivered['data']
# Same governed runtime delivery used by Platform's archive verifier;
# explicit credentials avoid SDK ambient/profile/metadata discovery.
import boto3
from botocore.config import Config
client=boto3.client('s3',endpoint_url=ENDPOINT,region_name='nl-ams',
aws_access_key_id=base64.b64decode(values['ACCESS_KEY_ID'],validate=True).decode(),
aws_secret_access_key=base64.b64decode(values['ACCESS_SECRET_KEY'],validate=True).decode(),
config=Config(signature_version='s3v4',proxies={},connect_timeout=15,read_timeout=30,
retries={'max_attempts':1},s3={'addressing_style':'path'}))
r['prefixes']=inventory(client);r['status']='metadata_inventory_complete'
r['runtime_delivery_uid']=delivered['metadata']['uid']
except Exception:r['error']='inventory_failed_diagnostics_suppressed'
finally:
r['finished_at']=datetime.now(timezone.utc).isoformat()
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,114 @@
#!/usr/bin/env python3
"""Silent read-only inventory in the existing Platform Backup-account procedure.
Runs only inside the attended platform-admin envelope. Reuses Platform's owner
custody accessor and fixed provider/account contract; no age key is read and no
backup is downloaded, uploaded, deleted, rotated or granted.
"""
import argparse
import base64
from datetime import datetime, timezone
import hashlib
import json
import os
from pathlib import Path
import re
import sys
import urllib.parse
import urllib.request
import xml.etree.ElementTree as ET
ROOT=Path(__file__).resolve().parents[2]
OWNER=ROOT.parent/'railiance-platform/scripts'
sys.path.insert(0,str(OWNER))
from state_hub_preflight_lane import bao, data
from migrate_nextcloud_backup_account import HOST, OPERATOR_PATH
BASE='/remote.php/dav/files/Backup/railiance-backups/'
LIMIT=4*1024*1024
NAME=re.compile(r'[A-Za-z0-9._-]{1,160}\Z')
class Refused(Exception):pass
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self,*args,**kwargs):raise Refused()
def parse_listing(raw, relative):
if len(raw)>LIMIT:raise Refused()
rows=[]
for item in ET.fromstring(raw).findall('{DAV:}response'):
href=urllib.parse.urlsplit(item.findtext('{DAV:}href') or '')
if href.netloc and href.netloc!=urllib.parse.urlsplit(HOST).netloc:raise Refused()
path=urllib.parse.unquote(href.path)
if not path.startswith(BASE+relative):raise Refused()
name=path[len(BASE+relative):].rstrip('/')
if not name:continue
if not NAME.fullmatch(name) or name in {'.','..'}:raise Refused()
for row in item.findall('{DAV:}propstat'):
if ' 200 ' not in (row.findtext('{DAV:}status') or ''):continue
prop=row.find('{DAV:}prop')
if prop is None:continue
folder=prop.find('{DAV:}resourcetype/{DAV:}collection') is not None
size=prop.findtext('{DAV:}getcontentlength')
if size is not None and not size.isdigit():raise Refused()
# Export only known-safe operational names/size, not arbitrary DAV
# properties, URLs, account metadata or raw provider responses.
if folder or name.endswith(('.age','.json','.yaml')):
rows.append({'name':name,'directory':folder,'bytes':int(size) if size else None})
if len(rows)>10000 or len({r['name'] for r in rows})!=len(rows):raise Refused()
return sorted(rows,key=lambda x:x['name'])
def listing(relative,credentials):
if relative and (not relative.endswith('/') or not NAME.fullmatch(relative[:-1])):raise Refused()
auth=base64.b64encode((credentials['BACKUP_USERNAME']+':'+credentials['BACKUP_PASSWORD']).encode()).decode()
req=urllib.request.Request(HOST+BASE+relative,
data=b'<d:propfind xmlns:d="DAV:"><d:prop><d:resourcetype/><d:getcontentlength/></d:prop></d:propfind>',
method='PROPFIND',headers={'Authorization':'Basic '+auth,'Depth':'1','Content-Type':'application/xml'})
opener=urllib.request.build_opener(urllib.request.ProxyHandler({}),NoRedirect())
with opener.open(req,timeout=30) as response:
if response.status!=207 or response.url!=HOST+BASE+relative:raise Refused()
raw=response.read(LIMIT+1)
return parse_listing(raw,relative)
def caller_credentials():
# Lookup payload contains a token: retain only safe caller/role facts.
identity=data(bao(['token','lookup','-format=json']))['data']
if 'platform-admin' not in identity.get('policies',[]) or 'root' in identity.get('policies',[]):raise Refused()
if not str(identity.get('path','')).startswith('auth/netkingdom/'):raise Refused()
credentials=data(bao(['read','-format=json',OPERATOR_PATH]))['data']['data']
if credentials.get('BACKUP_USERNAME')!='Backup' or not isinstance(credentials.get('BACKUP_PASSWORD'),str) or not credentials['BACKUP_PASSWORD']:raise Refused()
return identity,credentials
def inventory():
identity,credentials=caller_credentials()
root=listing('',credentials)
folders={}
for row in root:
if row['directory'] and re.match(r'^(?:r01-|core-|identity|sso|bootstrap|net-kingdom|openbao)',row['name']):
if len(folders)>=32:raise Refused()
folders[row['name']]=listing(row['name']+'/',credentials)
return {'root':root,'folders':folders,'caller_auth_path':identity['path'],
'caller_entity_id':identity.get('entity_id'),'scope':'Backup/railiance-backups root and bounded identity/database subfolders; other accounts/folders unassessed'}
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--receipt',required=True)
a=p.parse_args();fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
r={'schema':'netkingdom.storage-share-inventory.v1','status':'failed','started_at':datetime.now(timezone.utc).isoformat(),
'read_only':True,'age_keys_read':False,'backup_contents_downloaded':False,'secret_values_exported':False,
'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
'owner_sources':[{'path':str(OWNER/name),'sha256':hashlib.sha256((OWNER/name).read_bytes()).hexdigest()} for name in ['state_hub_preflight_lane.py','migrate_nextcloud_backup_account.py']]}
try:r.update(inventory());r['status']='metadata_inventory_complete'
except Exception:r['error']='inventory_failed_diagnostics_suppressed'
finally:
r['finished_at']=datetime.now(timezone.utc).isoformat()
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,512 @@
#!/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 and spec['target']!='/var/lib/keycape-policy' else '')]
return args
PROVISION_PHASES=(
'volume_creation_and_verification','worker_launch','worker_containment',
'private_import','issuer_launch','issuer_containment','tls_transport_launch','complete')
def provision(session,payload,tls,issuer,phase=None):
"""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()
if phase is None:phase=lambda value:None
phase('volume_creation_and_verification')
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'])
phase('worker_launch')
args=base_args(session,session['worker'],'none')+mounts(session)
# This pinned helper declares an image VOLUME here. Mask it explicitly so
# Docker cannot allocate an unregistered disk-backed anonymous volume.
args+=['--tmpfs','/privacyidea/etc/persistent:rw,nosuid,nodev,noexec,size=16777216,uid=1000,gid=1000,mode=0700']
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.
phase('worker_containment')
adapter.inspect_session({**session,'containers':[session['worker']],'issuer':session['worker']},worker_only=True)
phase('private_import')
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()
phase('issuer_launch')
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)
phase('issuer_containment')
adapter.inspect_session(session)
phase('tls_transport_launch')
run(['docker','exec','--detach',session['worker'],'python3','-c',TLS])
phase('complete')
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 process_facts(pid):
raw=Path('/proc')/str(pid)/'stat'
try:
fields=raw.read_text().rsplit(')',1)[1].split()
return {'pid':pid,'state':fields[0],'pgid':int(fields[2]),'sid':int(fields[3]),
'start_ticks':int(fields[19]),'uid':raw.parent.stat().st_uid}
except FileNotFoundError:return None
def isolate_execution():
# Called by main before reading TLS/key inputs. Never reuse an inherited
# shell, custody-helper, user or unrelated process group.
os.setsid()
pid=os.getpid();facts=process_facts(pid)
if os.getsid(0)!=pid or os.getpgrp()!=pid or facts['sid']!=pid or facts['pgid']!=pid:raise adapter.Refused()
return {k:facts[k] for k in ('pid','pgid','sid','start_ticks','uid')}|{'deadline':time.monotonic()+1800}
def verify_execution(execution):
if execution['pid']!=execution['sid'] or execution['pid']!=execution['pgid'] or execution['uid']!=os.getuid():raise adapter.Refused()
current=process_facts(execution['pid'])
if current is not None and any(current[k]!=execution[k] for k in ('pid','pgid','sid','start_ticks','uid')):
# Numeric leader PID reuse is not authority to signal a new group.
raise adapter.Refused()
def group_members(execution):
verify_execution(execution)
members=[]
for path in Path('/proc').iterdir():
if not path.name.isdecimal():continue
facts=process_facts(int(path.name))
if facts is None or facts['pgid']!=execution['pgid']:continue
if facts['sid']!=execution['sid'] or facts['uid']!=execution['uid'] or facts['start_ticks']<execution['start_ticks']:raise adapter.Refused()
if facts['state'] not in ('Z','X'):members.append(facts)
return members
def pidfd_dead(pidfd):
poller=select.poll();poller.register(pidfd,select.POLLIN)
return bool(poller.poll(0))
def quiesce_execution(execution,parent_pidfd,receipt):
"""Stop only this fresh execution session; cleanup needs no live creator.
Exact parent pidfd prevents leader reuse. Each remaining session member gets
its own pidfd after metadata comparison; no inherited/user-group kill occurs.
Unknown membership or uninterruptible survivors refuse cleanup altogether.
"""
verify_execution(execution)
if not pidfd_dead(parent_pidfd):
signal.pidfd_send_signal(parent_pidfd,signal.SIGTERM)
poller=select.poll();poller.register(parent_pidfd,select.POLLIN);poller.poll(2000)
if not pidfd_dead(parent_pidfd):signal.pidfd_send_signal(parent_pidfd,signal.SIGKILL)
for attempt in range(50):
members=group_members(execution)
if pidfd_dead(parent_pidfd) and not members:
receipt['execution_quiescence_verified']=True;return
for original in members:
try:fd=os.pidfd_open(original['pid'])
except ProcessLookupError:continue
try:
current=process_facts(original['pid'])
if current is None or pidfd_dead(fd):continue
if current!=original: # state may naturally change; bind stable identity below
if any(current[k]!=original[k] for k in ('pid','pgid','sid','start_ticks','uid')):raise adapter.Refused()
verify_execution(execution)
signal.pidfd_send_signal(fd,signal.SIGKILL)
except ProcessLookupError:pass
finally:os.close(fd)
time.sleep(.05)
receipt['execution_quiescence_verified']=False
raise adapter.Refused()
def daemon_uncertain(staging):
return any(Path(staging['path']+suffix).exists() or Path(staging['path']+suffix).is_symlink() for suffix in ('-docker-inflight','-docker-uncertain'))
def reaper_main(session,staging,execution,parent_pidfd):
receipt={'status':'failed','execution_quiescence_verified':False}
verify_execution(execution)
if pidfd_dead(parent_pidfd):raise adapter.Refused()
print('registered',flush=True)
poller=select.poll();poller.register(parent_pidfd,select.POLLIN)
remaining=max(0,int((execution['deadline']-time.monotonic())*1000))
parent_lost=bool(poller.poll(remaining))
receipt['cleanup_trigger']='parent_exit' if parent_lost else 'execution_deadline'
try:
# No deletion races a live creator: first stop/wait this exact owner
# session and its children. A daemon request already accepted can still
# complete after CLI loss; retain that uncertainty even after readback.
quiesce_execution(execution,parent_pidfd,receipt)
uncertain=daemon_uncertain(staging)
receipt['in_flight_daemon_request_uncertain']=uncertain
recover_obligations(session,staging,receipt)
receipt['status']='bounded_cleanup_uncertain' if uncertain else 'bounded_cleanup_verified'
except Exception:receipt['status']='bounded_cleanup_uncertain'
finally:
os.close(parent_pidfd)
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
def start_reaper(session, staging, execution):
adapter.docker_environment()
verify_execution(execution)
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()
parent_pidfd=os.pidfd_open(execution['pid'])
code="""import importlib.util,json,sys
spec=importlib.util.spec_from_file_location('session',sys.argv[1]);m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
m.reaper_main(json.loads(sys.argv[2]),json.loads(sys.argv[3]),json.loads(sys.argv[4]),int(sys.argv[5]))
"""
try:
child=subprocess.Popen([sys.executable,'-c',code,str(Path(__file__).resolve()),json.dumps(session),json.dumps(staging),json.dumps(execution),str(parent_pidfd)],
stdin=subprocess.DEVNULL,stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,
env={'PATH':os.environ.get('PATH',os.defpath),'DOCKER_HOST':adapter.ENDPOINT},
close_fds=True,pass_fds=(parent_pidfd,),start_new_session=True)
finally:os.close(parent_pidfd)
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,execution,checkpoint=None):
"""Existing private custody/transfer/archive APIs; no second archive format."""
adapter.manifest_binding_mode(admission,transfer)
verify_execution(execution)
if execution['pid']!=os.getpid():raise adapter.Refused()
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,execution)
adapter.EXECUTION={**execution,'staging':str(work)}
receipt.update(execution_binding=execution,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=adapter.process(['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)
# Data binding comes from exact reviewed ciphertext, never a new grant.
admission=adapter.private_manifest_admission(admission,manifest)
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','--pull=never','--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'
def phase(value):
# Public enum only: no private payload, exception text or daemon logs.
if value not in PROVISION_PHASES:raise adapter.Refused()
receipt['provision_phase']=value
if checkpoint is not None:checkpoint(receipt)
provision(session,payload,tls,admission['issuer'],phase)
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:
uncertain=daemon_uncertain(staging)
receipt['in_flight_daemon_request_uncertain']=uncertain
recover_obligations(session,staging,receipt)
if uncertain:raise adapter.Refused()
if reaper is not None:
reaper.terminate();reaper.wait(timeout=10)
except Exception:
receipt['status']='failed'
raise
finally:
adapter.EXECUTION=None
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:
execution=isolate_execution()
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,execution,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,478 @@
#!/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 time
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 manifest_binding_mode(admission,transfer):
"""Versioned private-manifest binding; legacy explicit digests stay valid."""
mode=admission.get('manifest_binding_mode','explicit-digest-v1')
if mode not in ('explicit-digest-v1','ciphertext-derived-v1'):raise Refused()
if (not isinstance(admission.get('ciphertext_sha256'),str)
or not re.fullmatch('[a-f0-9]{64}',admission['ciphertext_sha256'])
or admission['ciphertext_sha256']!=transfer.get('ciphertext_sha256')
or transfer.get('independent_owner_retrieval_verified') is not True):raise Refused()
if mode=='explicit-digest-v1':
if not isinstance(admission.get('manifest_sha256'),str) or not re.fullmatch('[a-f0-9]{64}',admission['manifest_sha256']):raise Refused()
elif 'manifest_sha256' in admission:raise Refused()
return mode
def private_manifest_admission(admission,manifest):
# Only called after the fixed ciphertext bytes were hashed, authenticated
# decryption succeeded and bundle.restore checked the complete archive.
# Never return this enriched private object in a safe receipt/checkpoint.
digest=hashlib.sha256(json.dumps(manifest,sort_keys=True,separators=(',',':')).encode()).hexdigest()
mode=admission.get('manifest_binding_mode','explicit-digest-v1')
if mode=='explicit-digest-v1':
if digest!=admission['manifest_sha256']:raise Refused()
elif mode!='ciphertext-derived-v1' or 'manifest_sha256' in admission:raise Refused()
return {**admission,'manifest_sha256':digest}
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'
EXECUTION = None
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):
marker=None
if EXECUTION is not None and (kwargs.get('start_new_session') or kwargs.get('process_group') not in (None,-1)):raise Refused()
if argv[0]=='docker':
argv=['docker','--host',ENDPOINT,*argv[1:]]
kwargs['env']=docker_environment(kwargs.get('env'))
if EXECUTION is not None:
if (os.getpid()!=EXECUTION['pid'] or os.getsid(0)!=EXECUTION['sid'] or os.getpgrp()!=EXECUTION['pgid']
or time.monotonic()>=EXECUTION['deadline']):raise Refused()
marker=Path(EXECUTION['staging']+'-docker-inflight')
fd=os.open(marker,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
os.close(fd)
try:
result=subprocess.run(argv,**kwargs)
if marker is not None and result.returncode!=0:
fd=os.open(Path(EXECUTION['staging']+'-docker-uncertain'),os.O_WRONLY|os.O_CREAT|os.O_NOFOLLOW,0o600);os.close(fd)
return result
except BaseException:
# Parent loss leaves .docker-inflight; caught timeout/transport failures
# retain a sticky uncertainty marker even if later cleanup can run.
if marker is not None:
fd=os.open(Path(EXECUTION['staging']+'-docker-uncertain'),os.O_WRONLY|os.O_CREAT|os.O_NOFOLLOW,0o600);os.close(fd)
raise
finally:
if marker is not None:
try:marker.unlink()
except FileNotFoundError:pass
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'] or expected_spec['target']=='/var/lib/keycape-policy'):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}')
"""
# A failed public readiness probe is closed data, not a failing Docker RPC.
# Transport/CLI nonzero remains uncertain; no deliberately failing exec is
# retried as if daemon acceptance had been established.
JWKS_TRANSPORT = ("import contextlib,io\ntry:\n"
" with contextlib.redirect_stdout(io.StringIO()):\n"
" exec(" + repr(JWKS_PROBE) + ")\n"
"except Exception:\n print('{\"original_public_signing_key_in_live_jwks\":false}')\n"
"else:\n print('{\"original_public_signing_key_in_live_jwks\":true}')\n")
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_TRANSPORT],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,309 @@
#!/usr/bin/env python3
"""Private native privacyIDEA token-material and audit recovery checks.
This does not authenticate a user's MFA code. It restores only a disposable
database, decrypts token material internally and verifies stored audit rows.
"""
import argparse
import ast
import base64
from datetime import datetime, timezone
import hashlib
import json
import os
from pathlib import Path
import signal
import subprocess
import tempfile
import time
import uuid
# Import the already reviewed local target/transport boundary without changing
# the accepted LLDAP executor or its source digest.
import importlib.util
HERE = Path(__file__).resolve().parent
spec = importlib.util.spec_from_file_location('isolated_lldap', HERE / 'isolated-lldap.py')
local = importlib.util.module_from_spec(spec)
spec.loader.exec_module(local)
bundle = local.bundle
IMAGE = local.CLIENT
POSTGRES = local.verify.pg.IMAGE
def run(args, **kwargs):
return local.command(args, **kwargs)
def preflight():
target = local.preflight()
target['images'].append({'reference': POSTGRES,
'image_id': run(['docker', 'image', 'inspect', '--format', '{{.Id}}', POSTGRES]).decode().strip()})
return target
def isolation(name, expected_network, user):
info = json.loads(run(['docker', 'container', 'inspect', name, '--format',
'{"host":{{json .HostConfig}},"mounts":{{json .Mounts}},"user":{{json .Config.User}}}']))
host = info['host']
if (host['NetworkMode'] != expected_network 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 info['user'] != user
or any(m['Type'] != 'tmpfs' for m in info['mounts'])):
raise bundle.Refused()
def cleanup(names):
for name in names:
present = subprocess.run(['docker', 'container', 'inspect', name],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=15)
if present.returncode == 0:
run(['docker', 'rm', '--force', '--volumes', name])
absent = subprocess.run(['docker', 'container', 'inspect', name],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=15)
run(['docker', 'info', '--format', '{{.ID}}'])
if absent.returncode == 0:
raise bundle.Refused()
def start_database(name):
run(['docker', 'run', '--detach', '--pull=never', '--name', name, '--network', 'none',
'--read-only', '--cap-drop', 'ALL', '--security-opt', 'no-new-privileges',
'--user', '999:999', '--memory', '2g', '--cpus', '2', '--pids-limit', '128',
'--tmpfs', '/var/lib/postgresql/data:rw,nosuid,nodev,noexec,size=1073741824,uid=999,gid=999,mode=0700',
'--tmpfs', '/var/run/postgresql:rw,nosuid,nodev,noexec,size=16777216,uid=999,gid=999',
'--tmpfs', '/tmp:rw,nosuid,nodev,noexec,size=16777216,uid=999,gid=999',
'--env', 'POSTGRES_HOST_AUTH_METHOD=trust', '--env', 'POSTGRES_DB=recovery',
'--entrypoint', '/usr/bin/timeout', POSTGRES, '--kill-after=15s', '1800s',
'/usr/local/bin/docker-entrypoint.sh', 'postgres', '-c', 'listen_addresses=127.0.0.1'])
isolation(name, 'none', '999:999')
for _ in range(80):
# The entrypoint's temporary init server has TCP disabled. Require TCP
# readiness so we cannot mistake it for the final local-only server.
ready = subprocess.run(['docker', 'exec', name, 'pg_isready', '-h', '127.0.0.1', '-U', 'postgres', '-d', 'recovery'],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=10)
if ready.returncode == 0:
return
time.sleep(.25)
raise bundle.Refused()
def start_worker(name, database, root):
secret = bundle.resource(root, 'mfa', 'secret', 'privacyidea-config')['data']
config = {'SQLALCHEMY_DATABASE_URI': 'postgresql+psycopg2://postgres@127.0.0.1:5432/recovery',
'PI_AUDIT_SQL_URI': 'postgresql+psycopg2://postgres@127.0.0.1:5432/recovery',
'SECRET_KEY': base64.b64decode(secret['PI_SECRET_KEY'], validate=True).decode(),
'PI_PEPPER': base64.b64decode(secret['PI_PEPPER'], validate=True).decode(),
'PI_ENCFILE': '/recovery/enckey', 'PI_AUDIT_KEY_PRIVATE': '/recovery/private.pem',
'PI_AUDIT_KEY_PUBLIC': '/recovery/public.pem', 'PI_AUDIT_NO_SIGN': False,
'PI_CHECK_OLD_SIGNATURES': legacy_setting(root), 'PI_NO_RESPONSE_SIGN': True,
'PI_LOGFILE': '/recovery/runtime.log', 'PI_LOGLEVEL': 50,
'PI_UUID_FILE': '/recovery/node-uuid',
'PI_TRUSTED_JWT': []}
# This deliberately does not execute archived pi.cfg or reuse its live DB URI.
payload = {'pi.cfg': '\n'.join(k + '=' + repr(v) for k, v in config.items()).encode()}
for key in ('enckey', 'private.pem', 'public.pem'):
payload[key] = (root / 'data/privacyidea' / key).read_bytes()
run(['docker', 'run', '--detach', '--pull=never', '--name', name,
'--network', 'container:' + database, '--read-only', '--cap-drop', 'ALL',
'--security-opt', 'no-new-privileges', '--user', '1000:1000',
'--memory', '1g', '--cpus', '2', '--pids-limit', '128',
'--tmpfs', '/recovery:rw,nosuid,nodev,noexec,size=33554432,uid=1000,gid=1000,mode=0700',
'--tmpfs', '/tmp:rw,nosuid,nodev,noexec,size=16777216,uid=1000,gid=1000',
'--tmpfs', '/privacyidea/etc/persistent:rw,nosuid,nodev,noexec,size=1048576,uid=1000,gid=1000,mode=0700',
'--env', 'PRIVACYIDEA_CONFIGFILE=/recovery/pi.cfg',
'--entrypoint', 'python3', IMAGE, '-c', 'import time;time.sleep(1800)'])
# Docker normalizes container:<name> to container:<full ID> in HostConfig.
database_id = run(['docker', 'container', 'inspect', database, '--format', '{{.Id}}']).decode().strip()
isolation(name, 'container:' + database_id, '1000:1000')
run(['docker', 'exec', '-i', name, 'tar', '-xf', '-', '-C', '/recovery'],
input=local.archive_files(payload))
def legacy_setting(root):
path = root / 'resources/mfa/configmap/privacyidea-cfg.json'
if not path.exists():
return False # Only the lower-level synthetic fixture omits ConfigMaps.
raw = bundle.resource(root, 'mfa', 'configmap', 'privacyidea-cfg')['data']['pi.cfg']
selected = False
for node in ast.parse(raw).body:
if isinstance(node, ast.Assign) and any(isinstance(t, ast.Name) and t.id == 'PI_CHECK_OLD_SIGNATURES' for t in node.targets):
if not isinstance(node.value, ast.Constant) or type(node.value.value) is not bool:
raise bundle.Refused()
selected = node.value.value
return selected
PROBE = r'''
import contextlib,io,json,logging,os,sys
logging.disable(logging.CRITICAL)
os.umask(0o077)
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_application_configuration'
from privacyidea.app import create_app
app=create_app(config_name='testing',config_file='/recovery/pi.cfg',silent=True)
from privacyidea.models import db,Token
from privacyidea.lib.security.default import DefaultSecurityModule
from privacyidea.lib.auditmodules.sqlaudit import Audit,LogEntry
from privacyidea.lib.crypto import Sign
import binascii
with app.app_context():
assert str(db.engine.url)=='postgresql+psycopg2://postgres@127.0.0.1:5432/recovery'
stage='retained_token_decryption'
security=DefaultSecurityModule({'file':'/recovery/enckey'})
wrong_path=Path('/recovery/wrong-enckey');wrong_path.write_bytes(os.urandom(96))
wrong=DefaultSecurityModule({'file':str(wrong_path)})
total=Token.query.count()
assert 0<total<=10000
checked=0;negative=0
for token in Token.query.yield_per(100):
if not token.key_enc:continue
encrypted=binascii.unhexlify(token.key_enc);iv=binascii.unhexlify(token.key_iv)
material=security.decrypt(encrypted,iv)
assert isinstance(material,bytes) and 0<len(material)<=4096
try:
invalid=wrong.decrypt(encrypted,iv)
assert invalid!=material
except (ValueError,binascii.Error):pass
negative+=1;checked+=1
del material
assert checked>0
result.update(retained_tokens_total=total,retained_tokens_with_native_decryption=checked,
different_key_cannot_reproduce_material_count=negative,
retained_token_material_decrypted_inside_process=True,existing_token_mfa_proven=False)
stage='historical_audit_verification'
audit=Audit(app.config)
allow_legacy=bool(app.config['PI_CHECK_OLD_SIGNATURES'])
historical=audit.session.query(LogEntry).count()
assert 0<historical<=2000000
signed=0;unsigned=0;invalid=0;legacy=0
for entry in audit.session.query(LogEntry).yield_per(1000):
if not entry.signature:
unsigned+=1;continue
signed+=1
if not str(entry.signature).startswith(Sign.sig_ver+':'):legacy+=1
if not audit.sign_object.verify(audit._log_to_string(entry),entry.signature,allow_legacy):invalid+=1
result.update(historical_audit_rows=historical,historical_signed_rows=signed,
historical_unsigned_rows=unsigned,historical_invalid_signatures=invalid,
historical_legacy_signatures=legacy,all_stored_audit_rows_checked=True,
legacy_signature_verification_enabled=allow_legacy,
event_completeness_claimed=False)
stage='new_isolated_audit_entry'
audit.log({'action':'NK49 isolated recovery verification','success':True,
'info':'disposable restored database only','client':'127.0.0.1'})
audit.finalize_log()
fresh=Audit(app.config)
assert fresh.session.query(LogEntry).count()==historical+1
entry=fresh.session.query(LogEntry).order_by(LogEntry.id.desc()).first()
message=fresh._log_to_string(entry)
assert fresh.sign_object.verify(message,entry.signature,False)
assert not fresh.sign_object.verify(message+' tampered',entry.signature,False)
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
public=rsa.generate_private_key(public_exponent=65537,key_size=2048).public_key().public_bytes(
serialization.Encoding.PEM,serialization.PublicFormat.SubjectPublicKeyInfo)
assert not Sign(public_key=public).verify(message,entry.signature,False)
result.update(new_audit_entry_signed_verified=True,tampered_entry_refused=True,
wrong_audit_public_key_refused=True,
audit_signature_continuity_proven=signed==historical and invalid==0,
only_loopback_interface_verified=True,external_routes_absent=True,
status='native_checks_completed')
except Exception as error:
result.update(status='failed',failed_stage=stage,failure_type=type(error).__name__)
print(json.dumps(result))
'''
def native_checks(root, receipt):
stem = 'nk49-pi-check-' + uuid.uuid4().hex
database = stem + '-db'
worker = stem + '-worker'
try:
start_database(database)
with (root / 'databases/privacyidea.dump').open('rb') as dump:
run(['docker', 'exec', '-i', database, 'pg_restore', '-U', 'postgres', '-d', 'recovery',
'--no-owner', '--no-privileges', '--exit-on-error'], stdin=dump, timeout=600)
start_worker(worker, database, root)
receipt['container_isolation_verified_before_private_import'] = True
result = json.loads(run(['docker', 'exec', worker, 'python3', '-c', PROBE], timeout=600))
receipt.update(result)
if result.get('status') != 'native_checks_completed':
raise bundle.Refused()
finally:
cleanup([worker, database])
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)
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-privacyidea-recovery.v1', 'status': 'failed',
'production_contacted': False, 'secret_values_exported': False,
'existing_token_mfa_proven': False, 'audit_signature_continuity_proven': 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 = 'transfer_validation'
transfer = json.loads(Path(args.transfer_receipt).read_text())
local.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 local.offline.offsite.digest(path) != transfer['ciphertext_sha256']:
raise bundle.Refused()
keyfd = local.verify.custody.private_key_fd()
if run(['age-keygen', '-y', f'/proc/self/fd/{keyfd}'], pass_fds=(keyfd,)).strip() != bundle.profile()['public_recipient'].encode():
raise bundle.Refused()
with tempfile.TemporaryDirectory(prefix='nk49-pi-recovery-', dir='/dev/shm') as temporary:
work = Path(temporary)
archive = work / 'archive.tar'
stage = 'archive_decryption'
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 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_privacyidea_checks'
native_checks(restored, result)
result['status'] = 'isolated_privacyidea_checked'
except (Exception, KeyboardInterrupt):
result.update(status='failed', error='isolated_privacyidea_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')
if result['status'] == 'failed':
print('ERROR: isolated privacyIDEA check failed; review safe receipt. No production change.')
return 1
if not result['audit_signature_continuity_proven']:
print('REVIEW: token-material checks completed; historical audit exceptions require owner review. Cleanup passed; see safe receipt.')
return 2
print('OK: retained token material decrypted internally; historical/new audit signatures and negative checks passed; cleanup passed. Existing-token MFA remains separate.')
return 0
if __name__ == '__main__':
for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP):signal.signal(sig, local.offline.interrupted)
raise SystemExit(main())

View file

@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""Prepare exact identity backup coverage from read-only workload/PVC metadata.
This produces a proposal, never reads Secrets, executes in pods, changes cluster
resources, captures private files, uploads, schedules or grants credentials.
"""
import argparse
from datetime import datetime,timezone
import hashlib
import json
import os
from pathlib import Path
import subprocess
ROOT=Path(__file__).resolve().parents[2]
CLUSTER_UID='a553c742-0115-43d4-99a4-a5ca56fe0786'
PROFILE=ROOT/'tools/bootstrap-recovery/identity-backup-profile.json'
class Refused(Exception):pass
def query(namespace,kind,name):
# Closed read-only types; Secret/ConfigMap data and arbitrary API paths have
# no transport in this preparation command.
if kind not in {'namespace','deployment','pvc'}:raise Refused()
args=['kubectl','--request-timeout=20s']
if namespace:args+=['-n',namespace]
r=subprocess.run(args+['get',kind,name,'-o','json'],capture_output=True,timeout=25)
if r.returncode:raise Refused()
return json.loads(r.stdout)
def references(spec):
secrets=set();configmaps=set();pvcs=set()
for c in spec.get('containers',[])+spec.get('initContainers',[]):
for e in c.get('env',[]):
v=e.get('valueFrom',{})
if 'secretKeyRef' in v:secrets.add(v['secretKeyRef']['name'])
if 'configMapKeyRef' in v:configmaps.add(v['configMapKeyRef']['name'])
for e in c.get('envFrom',[]):
if 'secretRef' in e:secrets.add(e['secretRef']['name'])
if 'configMapRef' in e:configmaps.add(e['configMapRef']['name'])
for v in spec.get('volumes',[]):
if 'secret' in v:secrets.add(v['secret']['secretName'])
if 'configMap' in v:configmaps.add(v['configMap']['name'])
if 'persistentVolumeClaim' in v:pvcs.add(v['persistentVolumeClaim']['claimName'])
for p in v.get('projected',{}).get('sources',[]):
if 'secret' in p:secrets.add(p['secret']['name'])
if 'configMap' in p:configmaps.add(p['configMap']['name'])
return secrets,configmaps,pvcs
def prepare(profile,get=query):
if get(None,'namespace','kube-system')['metadata']['uid']!=CLUSTER_UID:raise Refused()
bindings=[]
for component in profile['components']:
ns=component['namespace'];name=component['deployment']
d=get(ns,'deployment',name);m=d['metadata']
if (m['uid']!=component['deployment_uid'] or m.get('deletionTimestamp') or
d['status'].get('observedGeneration')!=m['generation'] or d['status'].get('readyReplicas')!=1):raise Refused()
refs=references(d['spec']['template']['spec'])
expected_pvcs={p['name']:p['uid'] for p in component['pvc_refs']}
if refs[0]!=set(component['secret_refs']) or refs[1]!=set(component['configmap_refs']) or refs[2]!=set(expected_pvcs):raise Refused()
pvcs=[]
for pvc,expected in expected_pvcs.items():
obj=get(ns,'pvc',pvc)
if obj['metadata']['uid']!=expected or obj['metadata'].get('deletionTimestamp') or obj['status'].get('phase')!='Bound':raise Refused()
pvcs.append({'name':pvc,'uid':expected})
bindings.append({'namespace':ns,'deployment':name,'uid':m['uid'],'generation':m['generation'],
'pvc_bindings':pvcs,'secret_names_only':sorted(refs[0]),'configmap_names_only':sorted(refs[1])})
if get(None,'namespace','kube-system')['metadata']['uid']!=CLUSTER_UID:raise Refused()
return {'schema':'netkingdom.identity-backup-preparation.v1','status':'prepared_not_admitted',
'observed_at':datetime.now(timezone.utc).isoformat(),'cluster_uid':CLUSTER_UID,
'profile_sha256':hashlib.sha256(PROFILE.read_bytes()).hexdigest(),
'source_sha256':hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
'bindings':bindings,'secret_values_read':False,'backup_captured':False,'uploaded':False,
'runtime_changed':False,'credential_grant_created':False,
'owner_admission_required':profile['admission_gates']}
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--receipt',required=True)
a=p.parse_args()
try:
r=prepare(json.loads(PROFILE.read_text()))
fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600)
with os.fdopen(fd,'w') as out:json.dump(r,out,indent=2);out.write('\n')
except Exception:
print('ERROR: coverage preparation refused; private diagnostics suppressed.');return 1
print('OK: exact coverage proposal prepared; capture, delivery and owner admission remain pending.');return 0
if __name__=='__main__':raise SystemExit(main())

View file

@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""Validate reviewed public recovery metadata before requesting custody input.
Never opens TLS material, archive plaintext or any key. This validates bindings,
not owner assent, archive contents or recovery authority.
"""
import argparse
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import re
import stat
from urllib.parse import urlsplit
ROOT=Path(__file__).resolve().parents[2]
# The executable closure extends the synthetic fixture closure with the attended
# custody entry point and this validator. Tests and historical receipts do not run.
SOURCE_PATHS=tuple(sorted([
'sso-mfa/bootstrap/sops-custody-unlock.sh','sso-mfa/bootstrap/sops-private.py','sso-mfa/bootstrap/sops-custody-fd.py',
'sso-mfa/k8s/privacyidea/key-custody-remote.py',
*('tools/bootstrap-recovery/'+name for name in (
'isolated-keycape.py','isolated-keycape-session.py','run-isolated-keycape.sh',
'prepare-isolated-keycape.py','isolated-lldap.py','identity-bundle.py',
'identity-backup-profile.json','retrieve-identity-offline.py','identity-offsite.py',
'verify-identity.py','identity-custody.json','identity-files.py',
'check-privacyidea-database.py'))]))
class Refused(Exception):pass
def require(value):
if not value:raise Refused()
def metadata_path(path):
path=Path(os.path.abspath(path))
require(not any(p.is_symlink() for p in (path,*path.parents)))
st=path.stat()
require(stat.S_ISREG(st.st_mode) and st.st_uid==os.getuid() and st.st_nlink==1 and not st.st_mode&0o077)
return path
def read_metadata(path,limit=128*1024):
path=metadata_path(path)
require(0<path.stat().st_size<=limit)
fd=os.open(path,os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK)
with os.fdopen(fd,'rb') as source:
info=os.fstat(source.fileno())
require(stat.S_ISREG(info.st_mode) and info.st_uid==os.getuid() and info.st_nlink==1 and not info.st_mode&0o077 and 0<info.st_size<=limit)
raw=source.read(limit+1)
require(0<len(raw)<=limit)
return raw
def strict_json(raw):
def unique(pairs):
result={}
for key,value in pairs:
require(key not in result)
result[key]=value
return result
return json.loads(raw,object_pairs_hook=unique)
def validate(transfer,admission,root=ROOT):
require(isinstance(admission,dict))
for field in ('ciphertext_sha256','verifier_sha256','trust_sha256','tls_cert_sha256'):
require(isinstance(admission.get(field),str) and re.fullmatch('[a-f0-9]{64}',admission[field]))
require(admission['ciphertext_sha256']==transfer.get('ciphertext_sha256'))
require(transfer.get('independent_owner_retrieval_verified') is True)
u=urlsplit(admission.get('issuer',''))
require(u.scheme=='https' and u.hostname and not u.username and not u.password and not u.query and not u.fragment and u.port in (None,443) and u.path in ('','/'))
consumer=admission.get('consumer',{})
fields=('client_id','secret_env','audience','scope','subject','tenant','roles','deny_scope')
require(isinstance(consumer,dict) and set(consumer)==set(fields))
for field in fields:
value=consumer[field]
require(isinstance(value,str) and 0<len(value)<=1024 and value.strip()==value and not any(ord(c)<32 or ord(c)==127 for c in value))
require(re.fullmatch('KEYCAPE_[A-Z0-9_]+',consumer['secret_env']))
require(not set(consumer['scope'].split())&set(consumer['deny_scope'].split()))
sources=admission.get('source_sha256')
require(isinstance(sources,dict) and set(sources)==set(SOURCE_PATHS))
for name,expected in sources.items():
require(isinstance(expected,str) and re.fullmatch('sha256:[a-f0-9]{64}',expected))
path=root/name
require(not any(p.is_symlink() for p in (path,*path.parents)))
require('sha256:'+hashlib.sha256(path.read_bytes()).hexdigest()==expected)
spec=importlib.util.spec_from_file_location('adapter',root/'tools/bootstrap-recovery/isolated-keycape.py')
adapter=importlib.util.module_from_spec(spec);spec.loader.exec_module(adapter)
adapter.manifest_binding_mode(admission,transfer)
# Validate provider receipt through the existing fixed contract, with no I/O.
spec=importlib.util.spec_from_file_location('offline',root/'tools/bootstrap-recovery/retrieve-identity-offline.py')
offline=importlib.util.module_from_spec(spec);spec.loader.exec_module(offline)
offline.validate(transfer)
ciphertext=metadata_path(transfer['fetched_path']) # no content read
require(ciphertext.stat().st_size==transfer['ciphertext_bytes'])
return True
def main():
p=argparse.ArgumentParser(description=__doc__)
for name in ('transfer-receipt','admission','admission-sha256','tls-cert','tls-key','trust'):p.add_argument('--'+name,required=True)
args=p.parse_args()
try:
require(re.fullmatch('[a-f0-9]{64}',args.admission_sha256))
raw=read_metadata(args.admission)
require(hashlib.sha256(raw).hexdigest()==args.admission_sha256)
transfer_raw=read_metadata(args.transfer_receipt)
validate(strict_json(transfer_raw),strict_json(raw))
for path in (args.tls_cert,args.tls_key,args.trust):metadata_path(path)
# Import all third-party dependencies before the attended custody prompt.
import yaml
from cryptography import x509
from cryptography.hazmat.primitives import serialization
except Exception:
print('ERROR: reviewed metadata/source/dependency preflight refused; no key requested.')
return 1
print('OK: reviewed metadata and source closure verified; private contents not inspected.')
return 0
if __name__=='__main__':raise SystemExit(main())

View file

@ -0,0 +1,176 @@
#!/usr/bin/env python3
"""Build a secret-free offline recovery kit; optionally publish exact new files.
Publication creates only three fixed versioned objects beside identity backups,
never changes shares, credentials, existing files, retention or runtime jobs.
"""
import argparse
import base64
from datetime import datetime, timezone
import hashlib
import importlib.util
import io
import json
import os
from pathlib import Path
import sys
import urllib.error
import urllib.request
import zipfile
ROOT = Path(__file__).resolve().parents[2]
HERE = Path(__file__).resolve().parent
spec = importlib.util.spec_from_file_location('offline', HERE / 'retrieve-identity-offline.py')
offline = importlib.util.module_from_spec(spec)
spec.loader.exec_module(offline)
offsite = offline.offsite
FILES = [
'docs/disaster-recovery-storage-share.md',
'docs/identity-service-restore-packet.md',
'docs/evidence/2026-10-16-identity-backup-live/actual-offsite.json',
'docs/evidence/2026-10-16-identity-outage-recovery/image-observation.json',
'tools/bootstrap-recovery/retrieve-identity-offline.py',
'tools/bootstrap-recovery/identity-offsite.py',
'tools/bootstrap-recovery/verify-identity.py',
'tools/bootstrap-recovery/identity-bundle.py',
'tools/bootstrap-recovery/check-privacyidea-database.py',
'tools/bootstrap-recovery/identity-backup-profile.json',
'sso-mfa/bootstrap/sops-custody-unlock.sh',
'sso-mfa/bootstrap/sops-private.py',
'keys/age.pub',
]
def build():
content = {name: (ROOT / name).read_bytes() for name in FILES}
if any(b'AGE-SECRET-KEY-1' in value for name, value in content.items()
if not name.endswith(('.py', '.sh', '.md'))):
raise offsite.Refused()
sums = ''.join(hashlib.sha256(value).hexdigest() + ' ' + name + '\n'
for name, value in content.items()).encode()
output = io.BytesIO()
with zipfile.ZipFile(output, 'w', compression=zipfile.ZIP_DEFLATED) as archive:
for name, value in {**content, 'SHA256SUMS': sums}.items():
entry = zipfile.ZipInfo(name, date_time=(2026, 10, 16, 0, 0, 0))
entry.external_attr = 0o100600 << 16
entry.compress_type = zipfile.ZIP_DEFLATED
archive.writestr(entry, value)
kit = output.getvalue()
version = hashlib.sha256(kit).hexdigest()
stem = 'net-kingdom-recovery-' + version[:16]
return {
stem + '-START-HERE.md': content['docs/disaster-recovery-storage-share.md'],
stem + '.zip': kit,
stem + '.zip.sha256': (version + ' ' + stem + '.zip\n').encode(),
}
def publish(files, headers, client=None):
client = client or offsite.opener()
result = []
for name, raw in files.items():
url = offline.ACCOUNT_BASE + name
digest = hashlib.sha256(raw).hexdigest()
# A retry may accept an identical object after exact readback, but must
# never overwrite a conflicting object or retry an unknown PUT outcome.
present = False
try:
with client.open(urllib.request.Request(url, method='HEAD', headers=headers), timeout=30) as response:
version = offsite.etag(response, url, len(raw))
present = True
except urllib.error.HTTPError as error:
if error.code != 404:
raise
if not present:
h = dict(headers, **{'If-None-Match': '*', 'Content-Length': str(len(raw)),
'Content-Type': 'application/zip' if name.endswith('.zip') else 'text/plain; charset=utf-8'})
with client.open(urllib.request.Request(url, data=raw, method='PUT', headers=h), timeout=60) as response:
if response.status != 201 or response.url != url:
raise offsite.Refused()
with client.open(urllib.request.Request(url, method='HEAD', headers=headers), timeout=30) as response:
version = offsite.etag(response, url, len(raw))
h = dict(headers, **{'If-Match': version})
with client.open(urllib.request.Request(url, headers=h), timeout=60) as response:
if offsite.etag(response, url, len(raw)) != version:
raise offsite.Refused()
downloaded = response.read(len(raw) + 1)
if downloaded != raw:
raise offsite.Refused()
result.append({'name': name, 'bytes': len(raw), 'sha256': digest,
'etag': version, 'readback_verified': True})
return result
def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--output-dir', required=True)
p.add_argument('--publish', action='store_true')
p.add_argument('--auth-source', choices=('prompt', 'attended-openbao'), default='prompt')
p.add_argument('--receipt', required=True)
p.add_argument('--offline-transfer-receipt', help='After prompted publication, retrieve the known recovered generation using the same independently held access')
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.recovery-kit.v1', 'status': 'failed',
'secret_values_exported': False, 'new_grants_created': False}
stage = 'option_validation'
try:
if a.offline_transfer_receipt and (not a.publish or a.auth_source != 'prompt'):
raise offsite.Refused()
stage = 'build_kit'
files = build()
stage = 'write_local_kit'
directory = Path(a.output_dir)
directory.mkdir(mode=0o700, exist_ok=False)
for name, raw in files.items():
(directory / name).write_bytes(raw)
r.update(status='built', files=[{'name': n, 'bytes': len(v), 'sha256': hashlib.sha256(v).hexdigest()}
for n, v in files.items()])
if a.publish:
if a.auth_source == 'prompt':
stage = 'private_provider_prompt'
secret = offline.private_prompt('Independently held Storage Share Backup app password: ')
else:
stage = 'attended_owner_custody'
spec = importlib.util.spec_from_file_location('inventory', HERE / 'inventory-storage-share.py')
owner = importlib.util.module_from_spec(spec)
spec.loader.exec_module(owner)
_, credentials = owner.caller_credentials()
secret = credentials['BACKUP_PASSWORD']
if not secret or any(c in secret for c in '\r\n\x00'):
raise offsite.Refused()
try:
headers = {'Authorization': 'Basic ' + base64.b64encode(('Backup:' + secret).encode()).decode(),
'Accept-Encoding': 'identity'}
stage = 'provider_publication'
r.update(status='published_readback_verified', files=publish(files, headers),
folder='Backup/railiance-backups/net-kingdom-identity',
provider='Storage Share', credential_source=a.auth_source)
if a.offline_transfer_receipt:
stage = 'offline_provider_download'
transfer_fd = os.open(a.offline_transfer_receipt, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
transfer = {'status': 'failed', 'error': 'offline_retrieval_failed_private_diagnostics_suppressed'}
try:
record = json.loads((ROOT / FILES[2]).read_text())
transfer = offline.retrieve(record, secret, 'account')
finally:
with os.fdopen(transfer_fd, 'w') as out:
json.dump(transfer, out, indent=2)
out.write('\n')
r['offline_retrieval_verified'] = transfer['status'] == 'readback_verified'
finally:
secret = None
r['finished_at'] = datetime.now(timezone.utc).isoformat()
except (Exception, KeyboardInterrupt) as error:
r.update(status='failed', error='kit_operation_failed_private_diagnostics_suppressed',
failed_stage=stage, **offline.failure_metadata(error))
finally:
with os.fdopen(fd, 'w') as output:
json.dump(r, output, indent=2)
output.write('\n')
# Silent: safe to use in the existing captured attended-owner conduit.
return int(r['status'] == 'failed')
if __name__ == '__main__':
sys.exit(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,43 @@
#!/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 tools/bootstrap-recovery/prepare-isolated-keycape.py \
--transfer-receipt "$1" --admission "$2" --admission-sha256 "$3" \
--tls-cert "$4" --tls-key "$5" --trust "$6"
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' 'Isolated original issuer/trust and selected 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 \
--sealed-fd --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,139 @@
#!/usr/bin/env python3
"""Isolated ciphertext recovery check; no cluster, credentials or values emitted.
This verifies the handoff transport and exact requested file coverage. Current
service validity, independent source custody and native consumer recovery need
separate owner evidence; this tool cannot infer them from a decrypted archive.
"""
import argparse
import hashlib
import importlib.util
import io
import json
import os
from pathlib import Path, PurePosixPath
import re
import signal
import subprocess
import sys
import tarfile
import tempfile
from datetime import datetime, timezone
ROOT = Path(__file__).resolve().parents[2]
spec = importlib.util.spec_from_file_location('private_age_recovery', ROOT / 'sso-mfa/bootstrap/sops-private.py')
custody = importlib.util.module_from_spec(spec); spec.loader.exec_module(custody)
LIMIT = 64 * 1024 * 1024
ALLOWED = {f'{c}/secrets.env' for c in ('privacyidea', 'postgres', 'lldap', 'authelia', 'keycape', 'breakglass')} | {
'privacyidea/pi.enc', 'privacyidea/private.pem', 'privacyidea/public.pem',
'keycape/key.pem', 'authelia/oidc_private_key.pem', 'openbao/init.json'}
class Refused(Exception):
pass
def recover(ciphertext, keyfd, required, work):
# Exclude ambient age keys/plugins and unrelated credential environments.
env = {'PATH': os.environ.get('PATH', os.defpath), 'HOME': str(work), 'XDG_CONFIG_HOME': str(work)}
result = subprocess.run(['age', '-d', '-i', f'/proc/self/fd/{keyfd}'], input=ciphertext,
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, pass_fds=(keyfd,),
timeout=60, env=env, cwd=work)
if result.returncode or not result.stdout or len(result.stdout) > LIMIT:
raise Refused()
restored = set(); names = set(); total = 0
with tarfile.open(fileobj=io.BytesIO(result.stdout), mode='r:') as archive:
for index, member in enumerate(archive):
if index >= 128 or member.name in names:
raise Refused()
names.add(member.name)
path = PurePosixPath(member.name)
if (path.is_absolute() or '..' in path.parts or '.' in path.parts
or not path.parts or path.parts[0] != 'secrets'):
raise Refused()
if member.isdir():
if len(path.parts) > 2 or (len(path.parts) == 2 and path.parts[1] not in {p.split('/')[0] for p in ALLOWED}):
raise Refused()
continue
relative = '/'.join(path.parts[1:])
if not member.isfile() or relative not in ALLOWED or not 0 <= member.size <= 16 * 1024 * 1024:
raise Refused()
total += member.size
if total > LIMIT or member.size == 0:
raise Refused()
content = archive.extractfile(member).read()
if len(content) != member.size or b'AGE-SECRET-KEY-' in content:
raise Refused()
target = work / 'restored' / relative
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 dest:
dest.write(content); dest.flush(); os.fsync(dest.fileno())
if target.read_bytes() != content:
raise Refused()
restored.add(relative)
if not required.issubset(restored):
raise Refused()
return sorted(restored)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--bundle', required=True)
parser.add_argument('--bundle-sha256', required=True)
parser.add_argument('--required-file', action='append', required=True)
parser.add_argument('--receipt', required=True)
args = parser.parse_args()
if not re.fullmatch('[a-f0-9]{64}', args.bundle_sha256): raise Refused()
required = set(args.required_file)
if not required or required - ALLOWED or len(required) != len(args.required_file): raise Refused()
path = Path(args.bundle).absolute()
if any(p.is_symlink() for p in (path, *path.parents)): raise Refused()
with path.open('rb') as file:
data = file.read(LIMIT + 1)
if len(data) > LIMIT or hashlib.sha256(data).hexdigest() != args.bundle_sha256: raise Refused()
recipient = custody.public_recipient()
keyfd = custody.private_key_fd()
try:
derived = subprocess.run(['age-keygen', '-y', f'/proc/self/fd/{keyfd}'],
pass_fds=(keyfd,), stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, timeout=10)
if derived.returncode or derived.stdout.strip() != recipient.encode(): raise Refused()
# Check the negative path with a disposable unrelated key. It never
# replaces the registered key, leaves temporary custody, or contacts IAM.
with tempfile.TemporaryDirectory(prefix='netkingdom-recovery-', dir='/tmp') as temporary:
work = Path(temporary)
disposable = work / 'wrong.key'
created = subprocess.run(['age-keygen', '-o', str(disposable)], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, timeout=10)
if created.returncode: raise Refused()
wrong = subprocess.run(['age', '-d', '-i', str(disposable)], input=data,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=60,
env={'PATH': os.environ.get('PATH', os.defpath), 'HOME': temporary,
'XDG_CONFIG_HOME': temporary}, cwd=temporary)
if wrong.returncode == 0: raise Refused()
restored = recover(data, keyfd, required, work)
if work.exists(): raise Refused()
finally:
os.close(keyfd)
receipt = {'schema': 'netkingdom.bootstrap-recovery-transport.v1',
'observed_at': datetime.now(timezone.utc).isoformat(), 'bundle_sha256': args.bundle_sha256,
'registered_recipient_match': True, 'wrong_key_refused': True,
'required_files': sorted(required), 'restored_files': restored,
'isolated_restore_readback_passed': True, 'temporary_plaintext_cleanup_verified': True,
'secret_values_exported': False, 'cluster_contacted': False,
'independent_custody_proven': False, 'current_service_validity_proven': False,
'native_consumer_recovery_proven': False}
# Publish only the closed, safe receipt shape; preserve existing receipts.
fd = os.open(args.receipt, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
with os.fdopen(fd, 'w') as dest:
json.dump(receipt, dest, indent=2); dest.write('\n')
print('Ciphertext transport/isolated restore checks passed. Independent custody and current service recovery remain separate owner evidence.')
if __name__ == '__main__':
try:
for sig in (signal.SIGHUP, signal.SIGTERM, signal.SIGINT): signal.signal(sig, custody.interrupted)
main()
except (Refused, custody.Refused, OSError, ValueError, TypeError, KeyError, tarfile.TarError, subprocess.SubprocessError):
print('ERROR: recovery transport check refused or failed; no acceptance receipt. Private diagnostics suppressed.', file=sys.stderr)
raise SystemExit(1)

View file

@ -0,0 +1,93 @@
#!/usr/bin/env python3
"""Attended native reader/ESO verification; only closed booleans leave custody."""
import argparse
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import sys
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
setup=load('identity-credential-setup');runtime=load('identity-runtime')
from state_hub_preflight_lane import bao,data,capabilities,revoke
def verify(action,expected,receipt,kubeconfig):
receipt['phase']='validate_approved_contract'
ccr,lane=setup.contract(action,expected)
receipt['phase']='verify_attended_identity'
ident=data(bao(['token','lookup','-format=json']))['data']
if 'platform-admin' not in ident.get('policies',[]) or 'root' in ident.get('policies',[]) or not ident.get('path','').startswith('auth/netkingdom/'):raise setup.Refused()
capture=runtime.capture;capture.KUBE=['kubectl','--kubeconfig',kubeconfig]
receipt['phase']='verify_cluster_identity'
if capture.get(None,'namespace','kube-system')['metadata']['uid']!=capture.prepare.CLUSTER_UID:raise setup.Refused()
def login(sa,audience,negative=False,namespace='sso'):
jwt=capture.command(['-n',namespace,'create','token',sa,'--audience='+audience,'--duration=10m']).decode().strip()
result=bao(['write','-format=json','auth/kubernetes/login','-'],payload={'role':lane['role'],'jwt':jwt},allow_failure=negative)
if negative:
if result.returncode==0:
revoke(data(result)['auth']['client_token']);raise setup.Refused()
if b'403' not in result.stderr and b'400' not in result.stderr:raise setup.Refused()
return None
return data(result)['auth']
receipt['phase']='native_reader_login'
auth=login(lane['service_account'],'openbao');token=auth['client_token']
receipt['native_reader_metadata']={k:auth.get(k) for k in ['token_policies','identity_policies','policies','lease_duration']}
try:
receipt['phase']='effective_reader_policy_ttl'
if (auth['token_policies']!=[lane['policy']] or auth.get('identity_policies')
or auth.get('policies',auth['token_policies'])!=[lane['policy']] or not 0<auth['lease_duration']<=900):raise setup.Refused()
paths=[lane['kv_path'],lane['kv_path'].replace('/data/','/metadata/',1),lane['kv_path']+'-sibling',
'platform/data/workloads/railiance/backup/offsite-lane','auth/token/create']
receipt['phase']='reader_capabilities'
cap=capabilities(token,paths)
receipt['capabilities_checked']=cap
if cap[paths[0]]!=['read'] or any(cap[p]!=['deny'] for p in paths[1:]):raise setup.Refused()
receipt['phase']='native_kv_eso_equality'
native=data(bao(['read','-format=json',lane['kv_path']],token=token))['data']
if set(native['data'])!={'NC_WEBDAV_TOKEN'} or native['data']['NC_WEBDAV_TOKEN']!=runtime.token(action):raise setup.Refused()
receipt['kv_version']=native['metadata']['version']
finally:revoke(token)
receipt['phase']='negative_reader_logins'
login('default','openbao',True);login(lane['service_account'],'wrong-audience',True)
proposed={'apiVersion':'v1','kind':'ServiceAccount','metadata':{'name':lane['service_account'],'namespace':'default'},'automountServiceAccountToken':False}
receipt['wrong_namespace_probe_attempted']=proposed['metadata']
test=json.loads(capture.command(['create','-f','-','-o','json'],json.dumps(proposed).encode()))
if test['metadata']['name']!=lane['service_account'] or test['metadata']['namespace']!='default':raise setup.Refused()
try:login(lane['service_account'],'openbao',True,'default')
finally:
path='/api/v1/namespaces/default/serviceaccounts/'+lane['service_account']
capture.command(['delete','--raw',path,'-f','-'],json.dumps({'apiVersion':'v1','kind':'DeleteOptions',
'preconditions':{'uid':test['metadata']['uid']}}).encode())
receipt['wrong_namespace_probe_cleanup_verified']=True
row=data(bao(['read','-format=json','auth/kubernetes/role/'+lane['role']]))['data']
setup.check_role(row,lane)
receipt.update(status='reader_and_delivery_verified',exact_namespace_binding_verified=True,
exact_policy_and_ttl_verified=True,wrong_service_account_denied=True,wrong_audience_denied=True,
wrong_namespace_denied=True,
metadata_sibling_escrow_and_token_create_denied=True,kv_write_denied=True,
eso_delivery_equality_verified=True,temporary_reader_revoked=True,secret_values_exported=False,
source_ccr=ccr['id'],ccr_sha256=expected)
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('action',choices=setup.PROFILES)
p.add_argument('--ccr-sha256',required=True);p.add_argument('--kubeconfig',required=True);p.add_argument('--receipt',required=True)
a=p.parse_args();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:verify(a.action,a.ccr_sha256,r,a.kubeconfig)
except Exception:r.update(status='failed',error='native_delivery_verification_failed_diagnostics_suppressed')
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,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,31 @@
{
"status": "proposed_not_integrated_not_projected",
"owner_head": "878abc93588e12b6da1bac055cfbb3abb8d7adf1",
"source_path": "workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md",
"baseline_sha256": "sha256:cfe7ee36c6c4cae677eb9f43121e3910e1274e0f5e3589cd147ede25a991947d",
"proposal_sha256": "sha256:c1798c52aeff150f672fbe2ad097aaadc2bb98107741b2784fb09993d509703c",
"patch_sha256": "sha256:aa11ac3b3cbb36febc436736e7eaa611f8769fbae70b0f026fa9534954aa604c",
"baseline_counts": {
"done": 23,
"progress": 2,
"wait": 6,
"todo": 2
},
"proposed_counts": {
"done": 24,
"progress": 2,
"wait": 7,
"todo": 3
},
"all_existing_task_ids_and_hub_uuids_preserved": true,
"only_existing_task_status_changed": {
"NK-WP-0049-T33": "todo -> wait"
},
"new_tasks": {
"NK-WP-0049-T34": "done: source preparation only",
"NK-WP-0049-T35": "todo: original TLS/trust recovery ownership",
"NK-WP-0049-T36": "todo: selected registration/private admission bindings"
},
"canonical_writes": false,
"cross_lane": "Coordinated group_provenance; that lane owns NK-WP-0048 only."
}

View file

@ -0,0 +1,209 @@
--- a/workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md
+++ b/workplans/NK-WP-0049-bootstrap-secret-free-recovery-handoff.md
@@ -8,7 +8,7 @@
owner: codex
topic_slug: netkingdom
created: "2026-10-12"
-updated: "2026-10-16"
+updated: "2026-10-17"
flavor: implementation
depends_on: []
state_hub_workstream_id: "17d315f0-829e-57c7-85cc-3a7914152804"
@@ -28,8 +28,8 @@
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
+**Current position, source refinement 2026-10-17:** 24 tasks done; 2 in
+progress; 3 ready to start (`todo`); 7 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;
@@ -958,27 +958,147 @@
```task
id: NK-WP-0049-T33
+status: wait
+priority: high
+assignee: codex
+depends_on: [NK-WP-0049-T32, NK-WP-0049-T34, NK-WP-0049-T35, NK-WP-0049-T36]
+blocking_reason: "Source custody/preflight preparation is evidenced in T34; await original TLS/trust recovery disposition T35 and selected original registration/private-admission metadata T36 before final reviewed command and existing owner process."
+state_hub_task_id: "56c8d469-7a10-5d50-a30e-95cbf3cd4756"
+```
+
+**Accountable worker:** NetKingdom codex with KeyCape and selected consumer owners.
+This umbrella retains final attempt readiness. Completed custody/preflight source
+preparation is extracted into T34; T35 resolves original TLS/trust recovery and
+T36 prepares the selected original registration and private admission. This split
+records distinct outcomes without counting source preparation as recovery.
+
+After those deliverables, assemble the exact existing six-argument attended
+command and admission digest, bind the integrated source/dependency revision and
+fixed local target, and record execution deadline, safe receipt location, abort,
+rollback and independent receipt verification through the existing owner process.
+Keep private manifest derivation inside custody and original TLS/client/key inputs
+in the owner's own terminal; the current null fields must refuse execution.
+Existing session authorization is retained within scope; this record does not
+supply missing owner assent or approve any actual recovery attempt.
+
+**Done when:** reviewed executable command and complete metadata-only/private
+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.
+
+## Deliver attended custody, metadata preflight and private manifest preparation source
+
+```task
+id: NK-WP-0049-T34
+status: done
+priority: high
+assignee: codex
+depends_on: [NK-WP-0049-T32]
+```
+
+**Accountable worker:** NetKingdom codex; independent Custodian source review.
+Completed 2026-10-17 as source preparation. Existing attended custody entry points
+now have a reviewed candidate sealed anonymous descriptor mode: hidden prompt,
+actual public-recipient verification, strict owned/private/full-seal descriptor
+admission, non-inheritable returned duplicate and explicit descriptor passing.
+The existing file mode stays compatible, with its original hard-loss limitation;
+no improved file-mode cleanup is claimed. Exact parent-loss tests prove descriptor
+lifetime without signalling inherited user shell groups. An admitted child retains
+its descriptor until it exits; parent loss is not child revocation.
+
+The candidate checks bounded, duplicate-free public JSON, exact admission digest,
+transfer/archive and selected service bindings, typed hashes for all 17 invoked
+local source/config dependencies and required Python imports before requesting a
+key. TLS and ciphertext preparation uses file metadata only. Legacy explicit
+manifest digests remain supported; versioned ciphertext-derived binding computes
+the canonical digest privately only after exact reviewed ciphertext, authenticated
+age decryption and full existing archive validation. The derived private digest
+never enters safe receipts/checkpoints and grants no new consumer authority.
+
+The coordinator independently passed 111 focused checks. The existing custody
+regression suite passed 94 checks; 61 real-SOPS tests skipped because that trusted
+runtime is unavailable, so that compatibility coverage remains unverified. Tests
+use generated keys/archive material only, including actual age cryptography,
+synthetic hidden-prompt and exact parent-loss processes. Manifest-builder tests
+mock Docker/issuer behavior and confer no new native proof. Frozen R6/R7 proof,
+intermediate test failures and scanner history remain retained.
+
+Bounded public image readback separately establishes the cached original image's
+13670142-byte `/keycape` executable as
+`sha256:cd89121393e1465aa76009763602a5b8c75f9dddb5679aea6242623f135b51b4`.
+This is executable identity metadata, not recovery or custody acceptance.
+
+Evidence: Custodian `docs/changes/netkingdom-target-2026-10-16/recovery-attempt-r1/`
+(`candidate.patch`, `review.json`, source closure, attempt/admission metadata and
+`public-verifier-image-readback.json`). Canonical integration/publication and
+projection receipts must be recorded separately when performed; this completed
+source-preparation record does not invent them. T33 remains open for complete
+attempt readiness; actual recovery/consumer acceptance remains T18 and MFA T19.
+
+## Establish original KeyCape TLS and trust recovery ownership for outage use
+
+```task
+id: NK-WP-0049-T35
status: todo
priority: high
assignee: codex
-depends_on: [NK-WP-0049-T32]
-state_hub_task_id: "56c8d469-7a10-5d50-a30e-95cbf3cd4756"
-```
-
-**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.
+depends_on: [NK-WP-0049-T34]
+```
+
+**Accountable worker:** NetKingdom codex, coordinating with the source owner of
+Ingress/cert-manager/Traefik and the original TLS custody/provider boundary.
+**Next action:** identify and record that actual owner's existing recovery path,
+then obtain its metadata/disposition through the ordinary owner process. A missing
+exact credential-routing catalog match is not evidence that no owner exists.
+
+Current source declares `kc.coulomb.social`, `sso/kc-tls`, cert-manager
+`ClusterIssuer/letsencrypt-prod` and Traefik TLS termination before KeyCape 8080.
+The identity backup profile excludes `kc-tls`; the verified archive therefore
+establishes no independent recovery of that TLS identity/trust. Determine whether
+the exact original certificate/key/trust is independently recoverable during an
+issuer/cluster outage, or whether a documented reissue path is acceptable for the
+selected continuity claim. Do not read or place the TLS key in work records, use
+a fresh self-signed trust substitute, mutate production or claim owner assent.
+
+**Done when:** named owner/provider, independent outage prerequisites, original
+public identity/certificate/trust hashes or accepted reissue disposition, private
+custody entry point and verification/abort limits are explicit and reviewed.
+Record any unavailable recovery capability as a live owner work record and bind
+its dependency/unblock condition; a prose-only gap or untested archive is not
+completion. This delivers preparation/disposition, not actual TLS recovery.
+
+## Prepare selected original service registration and private admission bindings
+
+```task
+id: NK-WP-0049-T36
+status: todo
+priority: high
+assignee: codex
+depends_on: [NK-WP-0049-T34]
+```
+
+**Accountable worker:** NetKingdom codex with KeyCape and the selected existing
+service consumer's owner. **Next action:** identify that owner and the exact
+original registration's non-secret selector metadata using existing source/owner
+evidence; preserve independent private custody and never retrieve a client secret.
+
+Bind reviewed ciphertext `7117b3fe1e4840c86b301c94c85e6b16709e5626bb0884bd0580bde630e30e30`,
+its independently verified transfer, source/image/executable closure and an
+explicit versioned manifest-binding mode. For the original service registration,
+record client identifier, secret environment name, audience, granted scope,
+subject, tenant, required roles and denied scope without values or new grants.
+Describe the native service-client dependency graph and distinguish unexercised
+LLDAP/Authelia/privacyIDEA/downstream flows. Keep private manifest material inside
+the existing custody process, never in Git, safe receipts, State Hub or chat.
+
+**Done when:** the selected existing service owner and exact non-secret registration
+bindings are independently established, the private admission builder validates
+positive/negative source/archive/selector cases, and its metadata is ready to
+combine with T35's original TLS/trust disposition under T33. This task may prepare
+registration metadata while T35 proceeds independently; it does not finalize the
+whole admission, perform actual recovery, redirect a consumer or confer assent.
## Accept remaining recovery and caller evidence and close the G08 handoff
@@ -999,7 +1119,7 @@
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
+**Done when:** T02 and T15–T36 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
@@ -1035,3 +1155,11 @@
remain retained. Custodian identity-restore-adapter-next/canonical-r7-integration.json
binds source/projection/native-fixture evidence separately. T18 remains waiting
for the actual owner-held attempt and selected consumer acceptance.
+
+
+2026-10-17 source/attempt refinement: T34 records completed attended custody,
+preflight/private-manifest source preparation and its bounded tests; T35 and T36
+make original TLS recovery ownership and selected original admission metadata live
+work. T33 retains final attempt readiness and its original Hub UUID. This adds no
+actual recovery, MFA, native authority or owner assent. Source task counts above
+refer to this file; projection has not been claimed for this proposed refinement.

View file

@ -0,0 +1,45 @@
{
"status": "derived_local_objects_not_central_publication",
"mode_source": "coordination-engine/spec/cross-owner-wait-mode-v0.1.md",
"new_driver_created": false,
"runtime_worker_injection_enabled": false,
"cases": [
{
"scenario_type": "cross_owner_wait",
"case_id": "nk49-t35-rcluster18",
"obligor_workplan_id": "RCLUSTER-WP-0018",
"obligor_task_id": "RCLUSTER-WP-0018-T02",
"beneficiary_workplan_id": "NK-WP-0049",
"beneficiary_task_id": "NK-WP-0049-T35",
"flavor": "implementation",
"state": "observed",
"reason": "Owner source handoff prepared; canonical projection pending",
"coordination_context": "CUST-WP-0080"
},
{
"scenario_type": "cross_owner_wait",
"case_id": "nk49-t36-secrets15",
"obligor_workplan_id": "SECRETS-WP-0015",
"obligor_task_id": "SECRETS-WP-0015-T03",
"beneficiary_workplan_id": "NK-WP-0049",
"beneficiary_task_id": "NK-WP-0049-T36",
"flavor": "implementation",
"state": "observed",
"reason": "Owner source handoff prepared; canonical projection pending",
"coordination_context": "CUST-WP-0080"
},
{
"scenario_type": "cross_owner_wait",
"case_id": "secrets15-nk49-readiness",
"obligor_workplan_id": "NK-WP-0049",
"obligor_task_id": "NK-WP-0049-T33",
"beneficiary_workplan_id": "SECRETS-WP-0015",
"beneficiary_task_id": "SECRETS-WP-0015-T04",
"flavor": "implementation",
"state": "waiting",
"reason": "Foreign source readiness remains incomplete; beneficiary also has a separate attended human gate",
"coordination_context": "CUST-WP-0080",
"human_gate_never_woken": true
}
]
}

View file

@ -0,0 +1,17 @@
{
"schema": "secrets-engine.recovery-selector-preflight.v1",
"status": "source_selector_matches_not_native_assent",
"source_sha256": "sha256:c0ade429edf452ad17f05d690b7ebef091db7f1a42b5ac350f1e40955c8586c0",
"consumer": {
"client_id": "secrets-engine-openbao",
"secret_env": "KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET",
"audience": "secrets-engine-openbao",
"scope": "openbao:login",
"subject": "service:secrets-engine",
"tenant": "tenant:platform",
"roles": "secrets-engine",
"deny_scope": "openbao:admin"
},
"credential_source_exercised": false,
"network_contacted": false
}

View file

@ -0,0 +1,166 @@
{
"schema": "nk49.recovery-source-owner-handoffs.v1",
"status": "isolated_patches_ready_for_root_review_not_integrated",
"owners": {
"railiance-cluster": {
"baseline_head": "cac8eabef5a0d0990a7b0de8f5e2c762666ac7f0",
"paths": {
"tools/public-tls-metadata.py": "sha256:87c167e44f0fcf154c9f2afae2187bebd33e5f220c05f579c1233b58bfe4f30b",
"tests/test_public_tls_metadata.py": "sha256:200a5be79dc6a715e22798b63e481431c3afeb2c4d7b71b26375d016a0c45866",
"docs/keycape-tls-recovery-boundary.md": "sha256:f4bcb1d1832c566f9c87793ed2a461c8cd8a00859dc4ae66cd3c160d7ceefa70",
"workplans/RCLUSTER-WP-0018-keycape-original-tls-recovery.md": "sha256:4a0d616ccbea199053531e82276441be21d0f168a63056a456375e27f58fc9af"
},
"workplan": "workplans/RCLUSTER-WP-0018-keycape-original-tls-recovery.md",
"tasks": [
"RCLUSTER-WP-0018-T01",
"RCLUSTER-WP-0018-T02",
"RCLUSTER-WP-0018-T03",
"RCLUSTER-WP-0018-T04",
"RCLUSTER-WP-0018-T05"
],
"canonical_additions_all_absent": true
},
"secrets-engine": {
"baseline_head": "535681052cd55ca613706ecc60c39d299929b3c5",
"paths": {
"tools/check-keycape-recovery-selector.py": "sha256:e3e5e3ee743a212247701d6cb40fcf5b7f19349126fa9b32cbef9a3cc9459150",
"tests/test_keycape_recovery_selector.py": "sha256:221112ba303c58bc4205b577359ca90d8a6f60d09eb0b538990b1e53a156e262",
"docs/keycape-isolated-recovery-selector.md": "sha256:ed23b0df3d4b49e4b76df795455153c805971ccd37886836c960f550709e0739",
"docs/keycape-recovery-selector.json": "sha256:02077af3c18b24f5e4a85992a3d4d0df51a8e458de1e48b48f7eb5f482257012",
"workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md": "sha256:22b148f0f403b8a9d471f34b2fae6938066123203d5c6f21ad463c5c262705c5"
},
"workplan": "workplans/SECRETS-WP-0015-original-keycape-recovery-selector.md",
"tasks": [
"SECRETS-WP-0015-T01",
"SECRETS-WP-0015-T02",
"SECRETS-WP-0015-T03",
"SECRETS-WP-0015-T04"
],
"canonical_additions_all_absent": true
}
},
"patches": {
"railiance-cluster": "sha256:7a8e4c9620881b18a5752424f5f46e6bd263eba2cc0bbf24a96968b7f9d04287",
"secrets-engine": "sha256:a797d299c719f8c998a7514c8b6685a3f75652e38c12e940569d37bb6bd03da2"
},
"prior_public_sources_revalidated": [
{
"repo": "net-kingdom",
"path": "sso-mfa/k8s/keycape/ingress.yaml",
"recorded_sha256": "sha256:bc5c7524a82de5e8b9755e7d7d408b4a66e034424cb2663c1d21352d292d5a72",
"current_bytes_match": true
},
{
"repo": "net-kingdom",
"path": "tools/bootstrap-recovery/identity-backup-profile.json",
"recorded_sha256": "sha256:40f9e5b5bb957e5b23e03b31701702b850502d16a92a0fa0ebf92618cc3f18a1",
"current_bytes_match": true
},
{
"repo": "net-kingdom",
"path": "sso-mfa/k8s/keycape/secrets-engine-openbao-client-rollout.py",
"recorded_sha256": "sha256:8b834e5f0353a5e11797637420609e06e7bf7de47777c7e78b774f74d4a6afbd",
"current_bytes_match": true
},
{
"repo": "net-kingdom",
"path": "sso-mfa/k8s/keycape/deployment.yaml",
"recorded_sha256": "sha256:83777942bc922c7d1719b0ad0b09e873105c342dae3b013bb65eeccf6d905626",
"current_bytes_match": true
},
{
"repo": "net-kingdom",
"path": "workplans/NK-WP-0046-secrets-engine-openbao-client-registration.md",
"recorded_sha256": "sha256:ef38481d26e599802d94f8b06a4d89c7c99a83c81ca5be9bcf7422b885867479",
"current_bytes_match": true
},
{
"repo": "net-kingdom",
"path": "sso-mfa/k8s/keycape/README.md",
"recorded_sha256": "sha256:e7a002996fd57477f6bcd054264076fe04da17f8685095163c1c80dd57b753f2",
"current_bytes_match": true
},
{
"repo": "key-cape",
"path": "workplans/KEY-WP-0037-secrets-engine-openbao-registration-custody.md",
"recorded_sha256": "sha256:3a90a0c460fc439570820b96d1ddc3b7f8646c557ca3a8aa27c6d546bb1a845a",
"current_bytes_match": true
},
{
"repo": "key-cape",
"path": "docs/evidence/2026-10-07-secrets-engine-openbao-issuance.json",
"recorded_sha256": "sha256:a405ef5289159c525695352ba71da7eca8e897b6f2dc7e5795f9b239361d8068",
"current_bytes_match": true
},
{
"repo": "secrets-engine",
"path": "docs/evidence/2026-10-10-keycape-openbao-probe.json",
"recorded_sha256": "sha256:bf0b5df9f030499d96770f1d9fb2091a8d37da71422a6d456be7200d3a1a1782",
"current_bytes_match": true
},
{
"repo": "secrets-engine",
"path": "docs/keycape-openbao-consumer-custody.md",
"recorded_sha256": "sha256:73efdd549f3feaccc53eefd6f1b95c8b9eb7091648f94ad80978ba5381be9124",
"current_bytes_match": true
},
{
"repo": "railiance-cluster",
"path": "SCOPE.md",
"recorded_sha256": "sha256:e4f6a8dd3d72a1f04f0d94721d57ca2a4c7e98790a8373876a5005b87ec5d8a6",
"current_bytes_match": true
},
{
"repo": "railiance-cluster",
"path": "docs/rail-kubernetes-substrate-profile.md",
"recorded_sha256": "sha256:75beeca7ef9670efc0b92c1ef8a33e24b72acbcdee06ee8de3c6492cb69d4f6b",
"current_bytes_match": true
},
{
"repo": "railiance-cluster",
"path": "tools/cmd/railiance-backup-s2",
"recorded_sha256": "sha256:35c60b7ccccd7b4d5a1cf2134c1f2ca78c45040a7ffcc55f2c32c385a0a682a3",
"current_bytes_match": true
},
{
"repo": "railiance-cluster",
"path": "tools/cmd/railiance-restore-s2",
"recorded_sha256": "sha256:25be58968fee3eeb6069e5af08abe601e8f60f04241e8de4bb917a7c639882e5",
"current_bytes_match": true
},
{
"repo": "railiance-cluster",
"path": "docs/backup-restore.md",
"recorded_sha256": "sha256:4d0bbaad3d69f10179ba9590d3fa12fba2967202c756ecf1952436d1e27de902",
"current_bytes_match": true
},
{
"repo": "railiance-master",
"path": "docs/rail-kubernetes-boundary.md",
"recorded_sha256": "sha256:b7a382622e241c174b11a8645aba1801693f22950bb132760b2170fcb828ced1",
"current_bytes_match": true
},
{
"repo": "railiance-platform",
"path": "docs/backup-provider-coverage.md",
"recorded_sha256": "sha256:f5983aac1e8cc1828e9f73340b1f66c0e67cdcddc1a1e49cbe6f5e39e90c6641",
"current_bytes_match": true
}
],
"tests": {
"public_tls_and_selector_checks_passed": 18,
"actual_current_consumer_source_preflight": "passed",
"real_certificate_or_key_read": false,
"network_contacted": false
},
"supervision": "Custodian assignment remains supervised; no owner-specific .kaizen record found, no promotion/scope authority inherited. Source preparation only.",
"existing_records": {
"cluster": "0017 signer only;0007 backlog/HA gated;archived safety-net local backup only; no live original TLS recovery record located",
"secrets": "0012 remains finished;0008T06+0013 retain functional/backend entitlement scope"
},
"canonical_mutations": false,
"secrets_retrieved": false,
"messages_sent": false,
"native_assent": false,
"publication_next": "Root independent review and authorized canonical apply; normal per-repo consistency projection; verify new IDs/statuses and attach derived case receipts without claiming queued/absent projection."
}

View file

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

View file

@ -0,0 +1,51 @@
# Original KeyCape TLS recovery boundary
Source disposition, 2026-10-17, scoped to railiance-cluster INTENT: cluster runtime,
ingress/certificate plumbing and backup posture. This is source preparation only.
It is not native acceptance, an issuer/key custody assent, a reissue decision or
permission to mutate the cluster.
NetKingdom's ingress source names `kc.coulomb.social`, `sso/kc-tls`,
`ClusterIssuer/letsencrypt-prod` and Traefik termination before KeyCape8080.
NetKingdom owns that namespace intent; railiance-cluster owns cert-manager,
ingress controller and cluster backup posture. No inspected source establishes
who independently holds the original private TLS recovery material when the
ordinary issuer/cluster is unavailable. A missing exact routing-catalog entry
cannot prove absence of an owning subsystem.
The identity archive excludes `kc-tls`. Current S2 backup source encrypts SQLite
state, Helm values and kubeconfig to the same host's `/opt/backup/railiance/cluster`
and retains seven versions. No inspected source proves an actually fresh,
independent offsite object or original certificate/key/trust recovery. Archived
RAIL-BS-WP-0004 is historical local-backup delivery; backlog RCLUSTER-WP-0007-T05
is gated HA issuance/renewal; active RCLUSTER-WP-0017 is signer runtime/storage.
None is an existing live task for this original TLS recovery demand.
`railiance-backup-s2` also stages raw state/Helm output in named `/tmp` paths
without early hard-loss cleanup; `railiance-restore-s2` prints whole-state restore
instructions. Do not use either as an isolated TLS extraction path. RCLUSTER-WP-0018
tracks exact inventory, safe private staging/extraction preparation where required,
independent outage proof and future archive-boundary disposition. No original
ciphertext or historical outcome is rewritten by that work.
`tools/public-tls-metadata.py` is an offline public-only step. After the source
owner identifies independent original **public certificate/chain and trust files**:
```sh
python3 tools/public-tls-metadata.py --public-cert ORIGINAL_PUBLIC_CERT_CHAIN.pem --public-trust ORIGINAL_PUBLIC_TRUST.pem
```
It accepts no private-key argument, bounds regular-file reads, refuses symlinks and
non-certificate PEM material, and reports public hashes/SANs/validity/counts only.
The file names are owner-local placeholders, not a key request. Output establishes
public metadata, not originality, hostname/chain acceptance, reissue permission or
custody. Do not supply a Secret object or combined certificate/key PEM. Fail closed
when original source, trust or exact bindings are missing; no fresh self-signed
trust workaround is admitted.
For FUTURE NetKingdom archives, the source owners must explicitly disposition
original TLS coverage or a reviewed reissue contract: producer/custody owner,
private artifact boundary, offline recovery independence, original public hashes,
scoped capture/read permissions, retention and native verification. Merely adding
`kc-tls` to secret_refs would change capture/permission/custody scope and is not
authorized by this source review. It cannot repair the current archive retroactively.

View file

@ -0,0 +1,34 @@
import importlib.util
from pathlib import Path
from datetime import datetime,timedelta,timezone
from unittest.mock import patch
import pytest
from cryptography import x509
from cryptography.hazmat.primitives import hashes,serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
spec=importlib.util.spec_from_file_location('metadata',Path(__file__).parents[1]/'tools/public-tls-metadata.py');m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
@pytest.fixture
def cert(tmp_path):
key=rsa.generate_private_key(public_exponent=65537,key_size=2048);name=x509.Name([x509.NameAttribute(NameOID.COMMON_NAME,'synthetic.invalid')]);now=datetime.now(timezone.utc)
cert=(x509.CertificateBuilder().subject_name(name).issuer_name(name).public_key(key.public_key()).serial_number(1).not_valid_before(now-timedelta(minutes=1)).not_valid_after(now+timedelta(hours=1)).add_extension(x509.SubjectAlternativeName([x509.DNSName('synthetic.invalid')]),critical=False).sign(key,hashes.SHA256()))
p=tmp_path/'public.pem';p.write_bytes(cert.public_bytes(serialization.Encoding.PEM));return p
def test_public_only_metadata_and_hashes(cert):
result=m.describe(cert,cert)
assert result['leaf_dns_sans']==['synthetic.invalid'];assert result['network_contacted'] is False
assert '-----BEGIN' not in str(result)
def test_mixed_or_private_pem_is_refused(cert):
cert.write_bytes(cert.read_bytes()+b'\n-----BEGIN PRIVATE KEY-----\nsynthetic\n-----END PRIVATE KEY-----')
with pytest.raises(ValueError):m.public_bundle(cert)
def test_oversize_is_refused_before_read(cert):
with patch.object(m,'LIMIT',1),patch.object(m.os,'open') as opened:
with pytest.raises(ValueError):m.public_bundle(cert)
opened.assert_not_called()
def test_symlink_refused(cert):
link=cert.with_name('link.pem');link.symlink_to(cert)
with pytest.raises(ValueError):m.public_bundle(link)

View file

@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""Owner-run public certificate/trust metadata; no private-key argument or network.
The owner supplies public-only PEM files from the independently established
original recovery source. This does not identify that source or accept reissue.
"""
import argparse
from datetime import timezone
import hashlib
import json
import os
from pathlib import Path
import re
import stat
from cryptography import x509
from cryptography.hazmat.primitives import serialization
LIMIT=1024*1024
CERT=re.compile(br'-----BEGIN CERTIFICATE-----\r?\n[A-Za-z0-9+/=\r\n]+-----END CERTIFICATE-----')
def public_bundle(path):
# Bound the read before allocation; refuse every non-certificate PEM block.
path=Path(os.path.abspath(path))
if any(p.is_symlink() for p in (path,*path.parents)) or not stat.S_ISREG(path.stat().st_mode) or not 0<path.stat().st_size<=LIMIT:raise ValueError()
fd=os.open(path,os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK)
with os.fdopen(fd,'rb') as source:
if not stat.S_ISREG(os.fstat(source.fileno()).st_mode) or not 0<os.fstat(source.fileno()).st_size<=LIMIT:raise ValueError()
raw=source.read(LIMIT+1)
if len(raw)>LIMIT:raise ValueError()
blocks=CERT.findall(raw)
if not blocks or len(blocks)>512 or CERT.sub(b'',raw).strip():raise ValueError()
return raw,[x509.load_pem_x509_certificate(block) for block in blocks]
def describe(cert_path,trust_path):
certificate,chain=public_bundle(cert_path);trust,roots=public_bundle(trust_path);leaf=chain[0]
try:names=leaf.extensions.get_extension_for_class(x509.SubjectAlternativeName).value.get_values_for_type(x509.DNSName)
except x509.ExtensionNotFound:names=[]
spki=leaf.public_key().public_bytes(serialization.Encoding.DER,serialization.PublicFormat.SubjectPublicKeyInfo)
def utc_date(name):
value=getattr(leaf,name+'_utc',None)
if value is None:value=getattr(leaf,name).replace(tzinfo=timezone.utc)
return value.isoformat()
return {'schema':'nk49.public-tls-metadata.v1','status':'public_metadata_only_not_originality_or_owner_assent',
'tls_cert_sha256':hashlib.sha256(certificate).hexdigest(),'trust_sha256':hashlib.sha256(trust).hexdigest(),
'leaf_public_spki_sha256':'sha256:'+hashlib.sha256(spki).hexdigest(),'leaf_dns_sans':names,
'leaf_not_before':utc_date('not_valid_before'),'leaf_not_after':utc_date('not_valid_after'),
'server_chain_certificates':len(chain),'trust_certificates':len(roots),'network_contacted':False,'private_key_argument_supported':False}
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--public-cert',required=True);p.add_argument('--public-trust',required=True);a=p.parse_args()
try:result=describe(a.public_cert,a.public_trust)
except Exception:print('{"status":"public_metadata_refused","private_diagnostics_suppressed":true}');return 1
print(json.dumps(result,indent=2));return 0
if __name__=='__main__':raise SystemExit(main())

View file

@ -0,0 +1,126 @@
---
id: RCLUSTER-WP-0018
type: workplan
title: "Establish original KeyCape TLS recovery and future archive boundary"
domain: financials
repo: railiance-cluster
status: ready
owner: codex
topic_slug: railiance
created: "2026-10-17"
updated: "2026-10-17"
flavor: implementation
depends_on: []
related: [CUST-WP-0080, NK-WP-0049, RCLUSTER-WP-0007, RCLUSTER-WP-0017]
---
Required owner source work for NK-WP-0049-T35 and CUST-WP-0080 G08. Reviewed
against current INTENT/SCOPE, backup sources and existing work records; ready
means work can proceed, not privileged execute authority. This does not promote
HA/backlog work, reopen archived local backup delivery or duplicate signer0017.
Source-owner scope is certificate plumbing/backup posture; NetKingdom owns
namespace ingress, while exact original independent TLS custody remains to be
established. No secret read, reissue, live change or native assent occurred.
## Record source boundary and public-only metadata preparation
```task
id: RCLUSTER-WP-0018-T01
status: done
priority: high
assignee: codex
```
Completed 2026-10-17 as isolated source preparation. Independently reviewed the
17 public source references against current owner bytes, checked current owner
instructions/environment/supervision and existing plans, and recorded the exact
boundary in docs/keycape-tls-recovery-boundary.md. Prepared bounded public-only
certificate/trust metadata projection with generated-certificate tests; no actual
certificate/key read. Source integration/publication and task projection remain
separate receipts, not claimed by this completed preparation task.
Evidence: the-custodian docs/changes/netkingdom-target-2026-10-16/
recovery-owner-handoffs-r1/ and prior recovery-attempt-r1/t35-t36-source-next/.
## Establish exact independent original TLS artifact and provider custody metadata
```task
id: RCLUSTER-WP-0018-T02
status: todo
priority: high
assignee: codex
```
Next: identify the actual original sso/kc-tls custody owner and independent public
provenance through existing owner processes. Record artifact/provider, timestamp,
public certificate/trust hashes, private custody entry point and outage dependencies
without values. Source review cannot infer a fresh backup, original key coverage
or independent decryption custody from a local backup script. Use credential
routing before any later credential need; missing route is not proof of absence.
Done when exact independent recovery or an explicitly owned unavailable capability
is evidenced as metadata and a reviewed next action, with no guessed assent.
## Prepare safe isolated original extraction or explicit reissue alternative
```task
id: RCLUSTER-WP-0018-T03
status: wait
priority: high
assignee: codex
depends_on: [RCLUSTER-WP-0018-T02]
blocking_reason: "Exact original artifact/provider/custody disposition is required before selecting extraction or reissue source."
```
From T02, reuse the actual owner path to prepare an exact attended private-only
recovery command and full dependency/cleanup proof. Do not run whole-state restore
or expose raw state/Secret output. If existing backup staging is required, correct
only the needed path with bounded memory-only staging and early exact cleanup,
including error/parent-loss tests before use. Original recovery and accepted
reissue are distinct claims; no self-signed trust substitute, new broker or
implicit production action. Done when the exact source proposal, public metadata
preflight, negatives, timeout, abort and custody obligations are reviewed.
## Verify admitted original TLS recovery through the actual owner boundary
```task
id: RCLUSTER-WP-0018-T04
status: wait
priority: high
assignee: codex
needs_human: true
depends_on: [RCLUSTER-WP-0018-T02, RCLUSTER-WP-0018-T03]
blocking_reason: "Await exact reviewed attended owner custody path and its existing scoped authority; no private input or native attempt requested yet."
```
Only the actual admitted owner action can prove recovered certificate/key pairing,
original hostname/trust and independent outage operation, or the expressly accepted
reissue result. Retain exact source/artifact/target/identity/negative/cleanup receipts.
Human custody/authority gates are not worker wakeups. Source and historical local
backup proof never fill native assent. Return bounded public proof to NK49T35;
no production mutation, credential retrieval or approval is granted by this task.
## Disposition original TLS coverage in the future identity archive contract
```task
id: RCLUSTER-WP-0018-T05
status: todo
priority: high
assignee: codex
```
Next: deliver the source-owned requirement in docs/keycape-tls-recovery-boundary.md
to NetKingdom's existing contract process: original TLS custody or accepted reissue,
independent provider/key recovery, public identity hashes, scoped capture/read
permissions and verification/retention. Record named owner dependencies as live
work before closure. Do not append kc-tls to secret_refs, expand capture credentials
or rewrite a historical archive without those owner reviews/authority. Done when
the future contract requirement has an exact tracked disposition/handoff; it is
not proof of current archive coverage or actual future capture.
## Coordination and completion
Beneficiary NK-WP-0049-T35/CUST-WP-0080 remains linked to this obligor record by
derived cross_owner_wait case nk49-t35-rcluster18 in the packet. No new project
driver, live worker injection or message was created. Own T03 wait has an explicit
local prerequisite; T04 is a human gate. Close only after source/native obligations
are proved or an explicit bounded scope disposition hands off remaining live work.

View file

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

View file

@ -0,0 +1,38 @@
# KeyCape original service recovery selector
Source-owner disposition, 2026-10-17, limited to secrets-engine's current consumer
contract. Current src/secrets_engine/service_auth.py fixes client/audience
secrets-engine-openbao, subject service:secrets-engine, tenant tenant:platform,
role secrets-engine and exact openbao:login scope. The NetKingdom producer binds
KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET; original KeyCape native evidence
uses openbao:admin as the denied-scope case. This selector matches the current
consumer source. This disposition is not native assent or a credential grant.
SECRETS-WP-0012 finished on historical attended custody/probe evidence and stays
finished. SECRETS-WP-0008-T06 retains functional steady-state adoption and scoped
backend entitlement. SECRETS-WP-0013 retains task-scoped backend execution. The
new SECRETS-WP-0015 covers only this original archive/isolated consumer preparation
and later accepted continuity proof; those existing gates are not duplicated.
The current consumer profile forbids a client-secret file and requires its
attended in-memory OpenBao source. That source depends on ordinary KeyCape OIDC
and OpenBao availability. The isolated issuer's native checker can consume its
original archive-delivered issuer binding without a provider login, but that does
not prove the secrets-engine consumer path works during an OpenBao/issuer outage.
Do not bypass the no-file/source contract, retrieve a live credential, redirect a
production consumer or invoke probe-openbao to fill this recovery gap. An exact
owner-reviewed outage-compatible proof/custody plan remains required.
Offline public preflight, after integration of reviewed source:
```sh
python3 tools/check-keycape-recovery-selector.py --selector docs/keycape-recovery-selector.json --source-sha256 sha256:c0ade429edf452ad17f05d690b7ebef091db7f1a42b5ac350f1e40955c8586c0
```
Use the exact hash in the owner packet, not a digest inferred at invocation from
an unreviewed file. The tool parses source constants with AST and never imports
the credential/backend module or calls the network. It requires all eight public
selector fields, exact source hash and unambiguous bounded JSON. Successful output
means source correspondence only. Original archived presence/secret binding,
actual TLS trust, native claim negatives and selected consumer continuity remain
separately evidenced. Missing/changed bindings refuse before any custody request.

View file

@ -0,0 +1,15 @@
{
"schema": "secrets-engine.public-recovery-selector.v1",
"status": "source_correspondence_only_not_native_assent",
"issuer": "https://kc.coulomb.social",
"consumer": {
"client_id": "secrets-engine-openbao",
"secret_env": "KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET",
"audience": "secrets-engine-openbao",
"scope": "openbao:login",
"subject": "service:secrets-engine",
"tenant": "tenant:platform",
"roles": "secrets-engine",
"deny_scope": "openbao:admin"
}
}

View file

@ -0,0 +1,496 @@
"""KeyCape service-JWT exchange shared by two separately validated profiles.
The OpenBao login profile and approval consumer profile have distinct clients,
audiences and scopes; both belong to tenant:platform. Provider selection never falls back to another
identity. Token parsing is a claim preflight, not signature verification: the
receiving OpenBao or approval-engine service verifies RS256 before accepting it.
"""
from __future__ import annotations
import base64
import binascii
import json
import hashlib
import ipaddress
import os
import stat
import subprocess
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Callable
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode, urlsplit
from urllib.request import Request, urlopen
from secrets_engine.errors import BackendError, ProvisioningError
from secrets_engine.safe_paths import containing_git_worktree
CLIENT_ID = "secrets-engine-openbao"
SUBJECT = "service:secrets-engine"
PRINCIPAL_TYPE = "service"
TENANT = "tenant:platform"
ROLE = "secrets-engine"
SCOPE = "openbao:login"
MAX_TOKEN_SECONDS = 15 * 60
RENEW_WINDOW_SECONDS = 3 * 60
@dataclass(frozen=True)
class KeyCapeServiceAuthConfig:
token_url: str
issuer: str
# The OpenBao profile takes no file: its secret is read in memory from
# OpenBao per command (OpenBaoOidcClientSecretSource). The approval
# profile subclass still uses a consumer file.
client_secret_file: Path | None = None
client_id: str = CLIENT_ID
subject: str = SUBJECT
audience: str = CLIENT_ID
tenant: str = TENANT
required_role: str = ROLE
scope: str = SCOPE
timeout_seconds: float = 10.0
max_future_iat_seconds: int = field(default=60, init=False)
def __post_init__(self) -> None:
if not self.token_url.startswith("https://"):
raise BackendError("KeyCape token URL must use HTTPS")
if not self.issuer.startswith("https://"):
raise BackendError("KeyCape issuer must use HTTPS")
if self.client_id != CLIENT_ID or self.audience != CLIENT_ID:
raise BackendError("KeyCape service client/audience must match accepted contract")
if (
self.subject != SUBJECT
or self.tenant != TENANT
or self.required_role != ROLE
or self.scope != SCOPE
):
raise BackendError("KeyCape service identity claims must match accepted contract")
if self.timeout_seconds <= 0:
raise BackendError("KeyCape timeout must be positive")
if type(self) is KeyCapeServiceAuthConfig and self.client_secret_file is not None:
raise BackendError(
"secrets-engine-openbao client secret file source is refused; "
"the secret is read in memory from OpenBao per command"
)
@dataclass(frozen=True)
class ServiceJWT:
token: str = field(repr=False)
issued_at: int
expires_at: int
claims: dict[str, Any] = field(repr=False)
def needs_renewal(self, now: datetime | None = None) -> bool:
current = int((now or datetime.now(timezone.utc)).timestamp())
return self.expires_at - current <= RENEW_WINDOW_SECONDS
def _decode_segment(value: str, label: str) -> dict[str, Any]:
try:
padded = value + "=" * (-len(value) % 4)
decoded = base64.urlsafe_b64decode(padded.encode("ascii"))
parsed = json.loads(decoded)
except (UnicodeEncodeError, binascii.Error, json.JSONDecodeError) as e:
raise BackendError(f"KeyCape JWT has invalid {label}") from e
if not isinstance(parsed, dict):
raise BackendError(f"KeyCape JWT {label} must be an object")
return parsed
def preflight_service_jwt(
token: str,
config: KeyCapeServiceAuthConfig,
*,
now: datetime | None = None,
) -> ServiceJWT:
"""Validate non-cryptographic JWT shape/claims before resource-server use."""
parts = token.split(".")
if len(parts) != 3 or not all(parts):
raise BackendError("KeyCape access token is not a compact JWT")
header = _decode_segment(parts[0], "header")
claims = _decode_segment(parts[1], "payload")
if header.get("alg") != "RS256":
raise BackendError("KeyCape JWT algorithm is not RS256")
exact_claims = {
"iss": config.issuer,
"sub": config.subject,
"aud": config.audience,
"principal_type": PRINCIPAL_TYPE,
"tenant": config.tenant,
"groups": [],
}
for name, expected in exact_claims.items():
if claims.get(name) != expected:
raise BackendError(f"KeyCape JWT claim '{name}' does not match contract")
if claims.get("roles") != [config.required_role]:
raise BackendError("KeyCape JWT roles do not match contract")
if claims.get("scope") != config.scope:
raise BackendError("KeyCape JWT scope does not match contract")
assurance = claims.get("assurance")
if not isinstance(assurance, dict) or (
assurance.get("level") != "aal1"
or assurance.get("methods") != ["client_secret"]
or assurance.get("mfa") is not False
or assurance.get("source") != "key-cape"
):
raise BackendError("KeyCape JWT assurance does not match contract")
issued_at = claims.get("iat")
expires_at = claims.get("exp")
if (
not isinstance(issued_at, int)
or isinstance(issued_at, bool)
or not isinstance(expires_at, int)
or isinstance(expires_at, bool)
):
raise BackendError("KeyCape JWT iat/exp must be integer timestamps")
current = int((now or datetime.now(timezone.utc)).timestamp())
if issued_at > current + config.max_future_iat_seconds:
raise BackendError("KeyCape JWT issue time is in the future")
if expires_at <= current:
raise BackendError("KeyCape JWT has expired")
if expires_at <= issued_at or expires_at - issued_at > MAX_TOKEN_SECONDS:
raise BackendError("KeyCape JWT lifetime exceeds accepted 15-minute bound")
return ServiceJWT(
token=token,
issued_at=issued_at,
expires_at=expires_at,
claims=dict(claims),
)
Transport = Callable[..., Any]
@dataclass(frozen=True)
class SecretFileCheck:
"""One value-free consumer-file check. Never carries file contents."""
name: str
ok: bool
detail: str = ""
def check_client_secret_file(path: Path) -> list[SecretFileCheck]:
"""Inspect a confidential-client secret file without exposing its value.
The consumer copy must be an absolute, canonical (no symlink component),
regular, non-empty file owned by the effective user with no group/other
permission bits, in a parent directory owned by the same user with no
group/other permission bits, outside every Git worktree. Stops at the first
structural failure; later checks are then not reported as passed.
"""
checks: list[SecretFileCheck] = []
def add(name: str, ok: bool, detail: str = "") -> bool:
checks.append(SecretFileCheck(name, ok, detail))
return ok
raw = Path(path)
if not add("absolute", raw.is_absolute(), "" if raw.is_absolute() else "path must be absolute"):
return checks
try:
canonical = raw.resolve(strict=True)
except (OSError, RuntimeError):
add("exists", False, "file not found or unresolvable")
return checks
add("exists", True)
if not add(
"canonical",
canonical == raw,
"" if canonical == raw else "path has a symlink or non-canonical component",
):
return checks
st = os.lstat(raw)
if not add(
"regular_file",
stat.S_ISREG(st.st_mode),
"" if stat.S_ISREG(st.st_mode) else "not a regular file",
):
return checks
euid = os.geteuid()
add("file_owner", st.st_uid == euid, "" if st.st_uid == euid else "not owned by the running user")
mode = stat.S_IMODE(st.st_mode)
add("file_mode", not mode & 0o077, "" if not mode & 0o077 else f"group/other-accessible mode {oct(mode)}; must be 0600")
pst = os.lstat(raw.parent)
add("parent_owner", pst.st_uid == euid, "" if pst.st_uid == euid else "parent not owned by the running user")
pmode = stat.S_IMODE(pst.st_mode)
add(
"parent_mode",
not pmode & 0o077,
"" if not pmode & 0o077 else f"group/other-accessible parent mode {oct(pmode)}; must be 0700",
)
worktree = containing_git_worktree(raw)
add("outside_git_worktree", worktree is None, "" if worktree is None else "inside a Git worktree")
add("non_empty", st.st_size > 0, "" if st.st_size > 0 else "file is empty")
return checks
def read_client_secret_file(path: Path, *, purpose: str = "KeyCape client secret") -> str:
"""Read the consumer secret only after every value-free check passes."""
failed = [c for c in check_client_secret_file(path) if not c.ok]
if failed:
reasons = "; ".join(f"{c.name}: {c.detail}" for c in failed)
raise ProvisioningError(f"{purpose} file {path} refused ({reasons})")
value = Path(path).read_text(encoding="utf-8").strip()
if not value or any(ch.isspace() for ch in value):
raise ProvisioningError(
f"{purpose} file {path} refused (must hold exactly one non-empty token)"
)
return value
@dataclass(frozen=True)
class KeyCapeServiceAuthProvider:
config: KeyCapeServiceAuthConfig
transport: Transport = field(default=urlopen, repr=False, compare=False)
# In-memory secret source. Required for the OpenBao profile; the approval
# profile reads its consumer file when this is None.
secret_source: Callable[[], str] | None = field(default=None, repr=False, compare=False)
def exchange(self, *, now: datetime | None = None) -> ServiceJWT:
"""Perform one client-credentials exchange; never retry or fall back."""
if self.secret_source is not None:
secret = self.secret_source()
elif type(self.config) is KeyCapeServiceAuthConfig or self.config.client_secret_file is None:
raise BackendError(
"secrets-engine-openbao requires the in-memory OpenBao secret source; "
"no file fallback"
)
else:
secret = read_client_secret_file(self.config.client_secret_file)
basic = base64.b64encode(
f"{self.config.client_id}:{secret}".encode("utf-8")
).decode("ascii")
del secret
body = urlencode(
{"grant_type": "client_credentials", "scope": self.config.scope}
).encode("ascii")
request = Request(
self.config.token_url,
data=body,
method="POST",
headers={
"Authorization": f"Basic {basic}",
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json",
},
)
try:
response = self.transport(request, timeout=self.config.timeout_seconds)
with response:
status = getattr(response, "status", 200)
raw = response.read(65537)
if len(raw) > 65536:
raise BackendError("KeyCape token response exceeds size bound")
except HTTPError as e:
raise BackendError(f"KeyCape token exchange failed with HTTP {e.code}") from e
except (URLError, TimeoutError, OSError) as e:
raise BackendError("KeyCape token exchange failed") from e
if status != 200:
raise BackendError(f"KeyCape token exchange failed with HTTP {status}")
try:
payload = json.loads(raw)
except (UnicodeDecodeError, json.JSONDecodeError) as e:
raise BackendError("KeyCape token response is not valid JSON") from e
if not isinstance(payload, dict):
raise BackendError("KeyCape token response must be an object")
if payload.get("id_token") or payload.get("refresh_token"):
raise BackendError("KeyCape service exchange returned a forbidden extra token")
token = payload.get("access_token")
if not isinstance(token, str) or not token:
raise BackendError("KeyCape token response has no access token")
if str(payload.get("token_type", "")).lower() != "bearer":
raise BackendError("KeyCape token response type is not Bearer")
expires_in = payload.get("expires_in")
if (
not isinstance(expires_in, int)
or isinstance(expires_in, bool)
or not 0 < expires_in <= MAX_TOKEN_SECONDS
):
raise BackendError("KeyCape token response lifetime is outside contract")
return preflight_service_jwt(token, self.config, now=now)
# -- in-memory OpenBao source for the secrets-engine-openbao profile ---------
#
# ops-mason plan keycape-secrets-engine-openbao-client-custody (658cdbc),
# founder direction 2026-10-06: the value lives only in OpenBao. Per command an
# attended OIDC login on one narrow role yields a 5m/4-use operator token that
# is captured from a pipe, used once in a request header for the KV GET, and
# revoked with revoke-self. Nothing is written to file, env, argv or cache.
OIDC_MOUNT = "netkingdom"
OIDC_ROLE = "keycape-secrets-engine-openbao-operator-workload-kv-read"
KV_MOUNT = "platform"
KV_PATH = "workloads/keycape/secrets-engine-openbao"
KV_FIELD = "CLIENT_SECRET"
SOURCE_NAME = "openbao-oidc-in-memory"
_AMBIENT_TOKEN_ENV = ("BAO_TOKEN", "VAULT_TOKEN")
_MAX_KV_RESPONSE = 65536
Runner = Callable[[list[str], dict[str, str]], "tuple[int, bytes]"]
def _run_login(argv: list[str], env: dict[str, str]) -> tuple[int, bytes]:
"""Run the attended login. stdout is a pipe into memory; stderr (the
browser prompt, no token) stays on the operator's terminal."""
proc = subprocess.run( # noqa: S603 - fixed argv, no shell
argv,
env=env,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=None,
check=False,
)
return proc.returncode, proc.stdout
def _require_bao_address(addr: str) -> str:
try:
parts = urlsplit(addr)
host = parts.hostname or ""
port_ok = parts.port != 0
except ValueError:
raise BackendError("OpenBao address is invalid for the in-memory secret read") from None
if not host or parts.username or parts.password or not port_ok:
raise BackendError("OpenBao address is invalid for the in-memory secret read")
try:
loopback = ipaddress.ip_address(host).is_loopback
except ValueError:
loopback = host == "localhost"
if parts.scheme == "https" or (parts.scheme == "http" and loopback):
return addr.rstrip("/")
raise BackendError("OpenBao address must be HTTPS or a literal loopback forward")
@dataclass
class OpenBaoOidcClientSecretSource:
"""Read CLIENT_SECRET in memory through one attended OIDC operator login.
Evidence is value-free: source name, operator accessor fingerprint, and
whether revoke-self was attempted and succeeded.
"""
bao_addr: str
bao_bin: str = "bao"
oidc_mount: str = OIDC_MOUNT
oidc_role: str = OIDC_ROLE
kv_mount: str = KV_MOUNT
kv_path: str = KV_PATH
kv_field: str = KV_FIELD
timeout_seconds: float = 10.0
runner: Runner = field(default=_run_login, repr=False, compare=False)
transport: Transport = field(default=urlopen, repr=False, compare=False)
accessor_fingerprint: str = ""
revocation_attempted: bool = False
revocation_succeeded: bool = False
def __post_init__(self) -> None:
pinned = (OIDC_MOUNT, OIDC_ROLE, KV_MOUNT, KV_PATH, KV_FIELD)
if (self.oidc_mount, self.oidc_role, self.kv_mount, self.kv_path, self.kv_field) != pinned:
raise BackendError("OpenBao secret source coordinates do not match the reviewed plan")
self.bao_addr = _require_bao_address(self.bao_addr)
if not self.bao_bin:
raise BackendError("bao CLI is required for the attended OIDC login")
def evidence(self) -> dict[str, object]:
return {
"secret_source": SOURCE_NAME,
"operator_session_handle": self.accessor_fingerprint,
"operator_revocation_attempted": self.revocation_attempted,
"operator_revocation_succeeded": self.revocation_succeeded,
}
def argv(self) -> list[str]:
# Token never appears here; -no-store keeps it out of the token helper.
return [
self.bao_bin,
"login",
"-method=oidc",
f"-path={self.oidc_mount}",
"-no-store",
"-format=json",
f"role={self.oidc_role}",
]
def _login_env(self) -> dict[str, str]:
env = {k: v for k, v in os.environ.items() if k not in _AMBIENT_TOKEN_ENV}
env["BAO_ADDR"] = self.bao_addr
env.pop("VAULT_ADDR", None)
return env
def _login(self) -> str:
code, out = self.runner(self.argv(), self._login_env())
try:
if code != 0:
raise BackendError(f"attended OpenBao OIDC login failed (exit {code})")
try:
auth = json.loads(out)["auth"]
token = str(auth["client_token"])
accessor = str(auth.get("accessor", ""))
except (UnicodeDecodeError, json.JSONDecodeError, KeyError, TypeError) as e:
raise BackendError("malformed OpenBao OIDC login response") from e
finally:
del out
if not token or any(ch.isspace() for ch in token):
raise BackendError("OpenBao OIDC login returned no usable token")
self.accessor_fingerprint = (
hashlib.sha256(accessor.encode("utf-8")).hexdigest()[:12] if accessor else ""
)
return token
def _call(self, method: str, path: str, token: str) -> tuple[int, bytes]:
request = Request(
f"{self.bao_addr}/v1/{path}",
data=b"" if method == "POST" else None,
method=method,
headers={"X-Vault-Token": token, "Accept": "application/json"},
)
try:
response = self.transport(request, timeout=self.timeout_seconds)
with response:
status = getattr(response, "status", 200)
raw = response.read(_MAX_KV_RESPONSE + 1)
except HTTPError as e:
return e.code, b""
except (URLError, TimeoutError, OSError) as e:
raise BackendError(f"OpenBao request failed: {method} {path}") from e
if len(raw) > _MAX_KV_RESPONSE:
raise BackendError("OpenBao response exceeds size bound")
return status, raw
def _read_field(self, token: str) -> str:
status, raw = self._call("GET", f"{self.kv_mount}/data/{self.kv_path}", token)
if status != 200:
raise BackendError(f"OpenBao KV read of the client secret failed with HTTP {status}")
try:
value = json.loads(raw)["data"]["data"][self.kv_field]
except (UnicodeDecodeError, json.JSONDecodeError, KeyError, TypeError) as e:
raise BackendError("OpenBao KV response has no client-secret field") from e
finally:
del raw
if not isinstance(value, str) or not value or any(ch.isspace() for ch in value):
raise BackendError("OpenBao client-secret field must hold exactly one token")
return value
def _revoke_self(self, token: str) -> None:
self.revocation_attempted = True
try:
status, _ = self._call("POST", "auth/token/revoke-self", token)
except BackendError:
return
self.revocation_succeeded = status in (200, 204)
def __call__(self) -> str:
token = self._login()
try:
return self._read_field(token)
finally:
self._revoke_self(token)
del token

View file

@ -0,0 +1,31 @@
import hashlib
import importlib.util
import json
from pathlib import Path
import pytest
spec=importlib.util.spec_from_file_location('check',Path(__file__).parents[1]/'tools/check-keycape-recovery-selector.py');m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
SOURCE=b'CLIENT_ID="secrets-engine-openbao"\nSUBJECT="service:secrets-engine"\nTENANT="tenant:platform"\nROLE="secrets-engine"\nSCOPE="openbao:login"\nraise RuntimeError("source is parsed, never executed")\n'
SELECTOR={'client_id':'secrets-engine-openbao','secret_env':'KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET','audience':'secrets-engine-openbao','scope':'openbao:login','subject':'service:secrets-engine','tenant':'tenant:platform','roles':'secrets-engine','deny_scope':'openbao:admin'}
DIGEST='sha256:'+hashlib.sha256(SOURCE).hexdigest()
def test_source_parsed_not_executed():
result=m.verify(SELECTOR,SOURCE,DIGEST)
assert result['network_contacted'] is False
@pytest.mark.parametrize('field',list(SELECTOR))
def test_each_selector_binding_is_exact(field):
bad={**SELECTOR,field:'different'}
with pytest.raises(ValueError):m.verify(bad,SOURCE,DIGEST)
def test_source_drift_and_untyped_digest_refused():
for source,digest in ((SOURCE+b'\n',DIGEST),(SOURCE,DIGEST.removeprefix('sha256:'))):
with pytest.raises(ValueError):m.verify(SELECTOR,source,digest)
def test_duplicate_constants_refused():
source=SOURCE+b'CLIENT_ID="other"\n'
with pytest.raises(ValueError):m.verify(SELECTOR,source,'sha256:'+hashlib.sha256(source).hexdigest())
def test_duplicate_public_metadata_keys_refused():
with pytest.raises(ValueError):json.loads('{"consumer":{},"consumer":{}}',object_pairs_hook=m.unique)
def test_oversize_public_input_refused(tmp_path):
p=tmp_path/'selector.json';p.write_bytes(b'x'*(m.LIMIT+1))
with pytest.raises(ValueError):m.read(p)
def test_symlink_public_input_refused(tmp_path):
p=tmp_path/'selector.json';p.write_text('{}');link=tmp_path/'link';link.symlink_to(p)
with pytest.raises(ValueError):m.read(link)

View file

@ -0,0 +1,64 @@
#!/usr/bin/env python3
"""Offline public selector preflight; no imports of credential/backend clients.
Checks current consumer source constants and the producer's already-deployed
public env/denial selector. This is source metadata, not runtime/owner assent.
"""
import argparse
import ast
import hashlib
import json
import os
from pathlib import Path
import re
import stat
ROOT=Path(__file__).resolve().parents[1]
LIMIT=128*1024
def read(path):
path=Path(os.path.abspath(path))
if any(p.is_symlink() for p in (path,*path.parents)):raise ValueError()
fd=os.open(path,os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK)
with os.fdopen(fd,'rb') as source:
info=os.fstat(source.fileno())
if not stat.S_ISREG(info.st_mode) or not 0<info.st_size<=LIMIT:raise ValueError()
raw=source.read(LIMIT+1)
if len(raw)>LIMIT:raise ValueError()
return raw
def unique(pairs):
result={}
for key,value in pairs:
if key in result:raise ValueError()
result[key]=value
return result
def verify(selector,source,source_sha256):
if not re.fullmatch('sha256:[a-f0-9]{64}',source_sha256) or 'sha256:'+hashlib.sha256(source).hexdigest()!=source_sha256:raise ValueError()
needed={'CLIENT_ID','SUBJECT','TENANT','ROLE','SCOPE'};constants={}
for node in ast.parse(source).body:
if isinstance(node,ast.Assign):
for target in node.targets:
if isinstance(target,ast.Name) and target.id in needed:
if target.id in constants or not isinstance(node.value,ast.Constant) or not isinstance(node.value.value,str):raise ValueError()
constants[target.id]=node.value.value
if set(constants)!=needed:raise ValueError()
expected={'client_id':constants['CLIENT_ID'],'secret_env':'KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET',
'audience':constants['CLIENT_ID'],'scope':constants['SCOPE'],'subject':constants['SUBJECT'],
'tenant':constants['TENANT'],'roles':constants['ROLE'],'deny_scope':'openbao:admin'}
if selector!=expected:raise ValueError()
return {'schema':'secrets-engine.recovery-selector-preflight.v1','status':'source_selector_matches_not_native_assent',
'source_sha256':source_sha256,'consumer':expected,'credential_source_exercised':False,'network_contacted':False}
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--selector',required=True);p.add_argument('--source-sha256',required=True)
args=p.parse_args()
try:
proposal=json.loads(read(args.selector),object_pairs_hook=unique)
if not isinstance(proposal,dict) or not isinstance(proposal.get('consumer'),dict):raise ValueError()
result=verify(proposal['consumer'],read(ROOT/'src/secrets_engine/service_auth.py'),args.source_sha256)
except Exception:print('{"status":"source_selector_refused","private_diagnostics_suppressed":true}');return 1
print(json.dumps(result,indent=2));return 0
if __name__=='__main__':raise SystemExit(main())

View file

@ -0,0 +1,95 @@
---
id: SECRETS-WP-0015
type: workplan
title: "Prepare original KeyCape service selector and isolated consumer continuity"
domain: infotech
repo: secrets-engine
status: ready
owner: codex
topic_slug: custodian
created: "2026-10-17"
updated: "2026-10-17"
flavor: implementation
depends_on: []
related: [CUST-WP-0080, NK-WP-0049, SECRETS-WP-0012, SECRETS-WP-0008, SECRETS-WP-0013]
---
Needed source-owner work for NK-WP-0049-T36/T18. Reviewed against current
Lifecycle INTENT, explicit consumer constants/custody source and dated historical
proof. Ready means work can proceed, not native assent. Finished0012 remains
finished; functional adoption/backend entitlement stay0008T06/0013. No new grant,
secret fetch, provider login, production redirect or external message occurred.
## Establish exact current intended source selector
```task
id: SECRETS-WP-0015-T01
status: done
priority: high
assignee: codex
```
Completed 2026-10-17 as source review. Current consumer constants and config guards
agree with secrets-engine-openbao client/audience, service:secrets-engine,
tenant:platform, role secrets-engine and only openbao:login. Producer env selector
and historical denied openbao:admin case are independently referenced, without
accepting the producer's native role. Prior native issuance7Oct and consumer
probe10Oct predate reviewed archive16Oct; they neither prove exact archived bytes
nor current native assent. Recorded docs/keycape-isolated-recovery-selector.md.
Evidence: Custodian recovery-owner-handoffs-r1 and prior17-source review.
## Deliver bounded offline public source selector preflight
```task
id: SECRETS-WP-0015-T02
status: done
priority: high
assignee: codex
```
Completed 2026-10-17 as isolated source preparation. Prepared AST-only source-hash
and all-eight-selector-field checker with bounded strict JSON, source drift,
duplicate, oversize and symlink negatives. It imports no backend/credential client
and never exchanges a token. Tests use public/synthetic metadata only. Canonical
integration/publication/projection must be recorded separately; successful output
is source correspondence, not native acceptance or a new secret source.
## Prepare outage-compatible original consumer proof without custody bypass
```task
id: SECRETS-WP-0015-T03
status: todo
priority: high
assignee: codex
```
Next: map the exact original archive-delivered registration and producer-native
checker result to the intended consumer, then prepare a source-reviewed isolated
proof/custody plan. Current consumer source requires attended OpenBao memory read
and refuses files; ordinary OpenBao/KeyCape outage is therefore a real dependency.
Do not add a fallback, persistent consumer copy or unreviewed injection source.
Clarify which continuity claim can be proved offline and which requires an exact
accepted owner custody/provider path. Bind current original selectors, image/
source/TLS metadata, negative cases, timeout/abort and cleanup; retain protected
backend operation entitlement in existing0008/0013. Done when that exact plan and
remaining owner dependencies are reviewable, without retrieving any value.
## Verify the separately admitted actual isolated consumer result
```task
id: SECRETS-WP-0015-T04
status: wait
priority: high
assignee: codex
needs_human: true
depends_on: [SECRETS-WP-0015-T03, NK-WP-0049-T33]
blocking_reason: "Await exact consumer proof/custody plan plus original TLS/private admission readiness; actual attended owner path/authority remains required."
```
Only a separately admitted owner action can establish actual archived registration,
issuer trust, native negative checks and consumer continuity. Source selector and
historical provider proof do not fill this gate. No production consumer redirect,
new grant, live OpenBao read, backend write or retained-factor MFA is authorized.
Return exact bounded evidence to NK49T36/T18; MFA remains NK49T19. Human gate is
never a worker wakeup. Cross-owner dependency is attached to derived case
secrets15-nk49-readiness in the packet; do not strand the wait or invent a driver.

View file

@ -0,0 +1,562 @@
{
"READY": [
{
"id": "CUST-WP-0080-T03",
"status": "progress",
"priority": "high",
"reason": "",
"blocked_on": "",
"repo": "the-custodian"
},
{
"id": "CUST-WP-0080-T06",
"status": "progress",
"priority": "high",
"reason": "",
"blocked_on": "",
"repo": "the-custodian"
},
{
"id": "FLEX-WP-0024-T06",
"status": "progress",
"priority": "high",
"reason": "",
"blocked_on": "",
"repo": "access-engine"
},
{
"id": "KEY-WP-0040-T01",
"status": "progress",
"priority": "high",
"reason": "",
"blocked_on": "",
"repo": "key-cape"
},
{
"id": "NK-WP-0035-T04",
"status": "progress",
"priority": "high",
"reason": "",
"blocked_on": "",
"repo": "net-kingdom"
},
{
"id": "NK-WP-0048-T12",
"status": "todo",
"priority": "high",
"reason": "",
"blocked_on": "",
"repo": "net-kingdom"
},
{
"id": "NK-WP-0048-T14",
"status": "todo",
"priority": "high",
"reason": "",
"blocked_on": "",
"repo": "net-kingdom"
},
{
"id": "NK-WP-0048-T15",
"status": "todo",
"priority": "high",
"reason": "",
"blocked_on": "",
"repo": "net-kingdom"
},
{
"id": "NK-WP-0048-T16",
"status": "todo",
"priority": "high",
"reason": "",
"blocked_on": "",
"repo": "net-kingdom"
},
{
"id": "NK-WP-0049-T02",
"status": "progress",
"priority": "high",
"reason": "",
"blocked_on": "",
"repo": "net-kingdom"
},
{
"id": "NK-WP-0049-T15",
"status": "progress",
"priority": "high",
"reason": "",
"blocked_on": "",
"repo": "net-kingdom"
},
{
"id": "NK-WP-0050-T02",
"status": "progress",
"priority": "high",
"reason": "",
"blocked_on": "",
"repo": "net-kingdom"
},
{
"id": "RPF-WP-0035-T12",
"status": "progress",
"priority": "high",
"reason": "",
"blocked_on": "",
"repo": "railiance-platform"
},
{
"id": "RPF-WP-0053-T01",
"status": "progress",
"priority": "high",
"reason": "",
"blocked_on": "",
"repo": "railiance-platform"
},
{
"id": "RPF-WP-0053-T02",
"status": "todo",
"priority": "high",
"reason": "Platform must admit the exact Scaleway primary archive caller/prefix, reconcile its current custody front door and retention/capacity, and deliver bounded credentials before activation.",
"blocked_on": "",
"repo": "railiance-platform"
},
{
"id": "RPF-WP-0053-T03",
"status": "todo",
"priority": "high",
"reason": "Needs T01/T02 owner admission and founder go for the exact first quiescence/upload and schedule; independent offsite retrieval and recovery proof remain required.",
"blocked_on": "",
"repo": "railiance-platform"
},
{
"id": "RPF-WP-0053-T04",
"status": "todo",
"priority": "high",
"reason": "",
"blocked_on": "",
"repo": "railiance-platform"
},
{
"id": "RPF-WP-0059-T04",
"status": "progress",
"priority": "high",
"reason": "",
"blocked_on": "",
"repo": "railiance-platform"
},
{
"id": "SECRETS-WP-0014-T02",
"status": "progress",
"priority": "high",
"reason": "",
"blocked_on": "",
"repo": "secrets-engine"
},
{
"id": "USER-WP-0038-T01",
"status": "todo",
"priority": "high",
"reason": "",
"blocked_on": "",
"repo": "user-engine"
},
{
"id": "GH-WP-0004-T06",
"status": "progress",
"priority": "medium",
"reason": "",
"blocked_on": "",
"repo": "gate-house"
},
{
"id": "KEY-WP-0040-T02",
"status": "progress",
"priority": "medium",
"reason": "",
"blocked_on": "",
"repo": "key-cape"
},
{
"id": "NK-WP-0049-T24",
"status": "todo",
"priority": "medium",
"reason": "",
"blocked_on": "",
"repo": "net-kingdom"
},
{
"id": "RPF-WP-0057-T04",
"status": "progress",
"priority": "medium",
"reason": "",
"blocked_on": "",
"repo": "railiance-platform"
},
{
"id": "WARDEN-WP-0027-T02",
"status": "todo",
"priority": "medium",
"reason": "",
"blocked_on": "",
"repo": "ops-warden"
}
],
"UNBLOCKED": [
{
"id": "FLEX-WP-ADHOC-2026-10-03-T02",
"status": "wait",
"priority": "high",
"reason": "Install accepted retroactively (877c268b); steps 6-7 follow WARDEN-WP-0039-T03 token receipts and fetch URL.",
"blocked_on": "",
"repo": "access-engine"
},
{
"id": "MASON-WP-0004-T03",
"status": "wait",
"priority": "low",
"reason": "operators/nextcloud base entry removed 2026-10-14 after the founder retired it. Remaining draft gaps still need their owners.",
"blocked_on": "owners",
"repo": "ops-mason"
}
],
"ATTENDED": [
{
"id": "ACTIVITY-WP-0042-T08",
"status": "wait",
"priority": "high",
"reason": "KV4 provider repair is delivered; repo-manager historical-prefix acceptance is delivered (receipt 39f2c067, 2026-10-06). Separate approved CNPG hostPath/Forgejo GitOps bundle delivery, ESO/env refresh and native consumer receipt remain. Founder per-change go is required before live promotion.",
"blocked_on": "",
"repo": "activity-core",
"pending_dependencies": []
},
{
"id": "CUST-WP-0079-T06",
"status": "wait",
"priority": "high",
"reason": "Partly done 2026-10-11: list cut from 14 to 3; U05-U07 accepted, U08 waits on KEY-WP-0040-T01. Remaining founder acts: USER-WP-0026-T03 readback, RPF-WP-0044-T08 coulombcore check, the U08 re-run, and Sol's added RPF-WP-0044-T04 issuer login.",
"blocked_on": "",
"repo": "the-custodian",
"pending_dependencies": []
},
{
"id": "FLEX-WP-0036-T01",
"status": "wait",
"priority": "high",
"reason": "Exact new runtime principals/naming are included in the single daily red batch with the nine TokenReview grants; no per-item question.",
"blocked_on": "",
"repo": "access-engine",
"pending_dependencies": []
},
{
"id": "PNEX-WP-0006-T01",
"status": "wait",
"priority": "high",
"reason": "Public publication is red-lane; add PNEX-WP-0006 to the single daily founder batch. Local code and tests are prepared; no per-item approval request.",
"blocked_on": "",
"repo": "policy-nexus",
"pending_dependencies": []
},
{
"id": "PNEX-WP-0006-T02",
"status": "wait",
"priority": "high",
"reason": "Public publication is red-lane; add PNEX-WP-0006 to the single daily founder batch. Local code and tests are prepared; no per-item approval request.",
"blocked_on": "",
"repo": "policy-nexus",
"pending_dependencies": []
},
{
"id": "RPF-WP-0044-T04",
"status": "wait",
"priority": "high",
"reason": "Orange attended Platform OpenBao OIDC/MFA login for the already D2-approved exact repository reader; no second source-scope approval.",
"blocked_on": "",
"repo": "railiance-platform",
"pending_dependencies": []
},
{
"id": "RPF-WP-0053-T05",
"status": "progress",
"priority": "high",
"reason": "email-connect primary.env is delivered from the accepted reader. Retention, capacity, independent receipts, escrow and the first capture remain open. Coulomb node delivery was not requested.",
"blocked_on": "",
"repo": "railiance-platform",
"pending_dependencies": []
},
{
"id": "USER-WP-0026-T03",
"status": "wait",
"priority": "high",
"reason": "Signed-in readback live and correct for the deny path (tegwick: Informed Decision - Not available, founder-confirmed correct; docs/evidence/2026-10-11-signed-in-application-readback.md). Remaining: attended positive check with a net-kingdom-admins account (expect Available), then catalogue-management restoration before customers (FLEX-IN-0003).",
"blocked_on": "",
"repo": "user-engine",
"pending_dependencies": []
},
{
"id": "ACTIVITY-WP-0043-T04",
"status": "wait",
"priority": "medium",
"reason": "Founder accepted memo 2 (approval 77993081); trials wait for the first live C-25 owner return",
"blocked_on": "",
"repo": "activity-core",
"pending_dependencies": []
},
{
"id": "ACTIVITY-WP-0044-T03",
"status": "wait",
"priority": "medium",
"reason": "Activation follows RMGR-WP-0020-T04 and is a founder action",
"blocked_on": "",
"repo": "activity-core",
"pending_dependencies": [
"RMGR-WP-0020-T04"
]
},
{
"id": "FLEX-WP-0036-T04",
"status": "wait",
"priority": "medium",
"reason": "New public coulomb/access-engine image package publication is included in the single daily red batch. Local source/build checks pass; paired publication has not run.",
"blocked_on": "",
"repo": "access-engine",
"pending_dependencies": [
"FLEX-WP-0036-T01"
]
},
{
"id": "CUST-WP-0073-T05",
"status": "wait",
"priority": "low",
"reason": "Deferred by founder decision 2026-09-24; reopened only by the founder on one of the three triggers below, in one attended rotation window.",
"blocked_on": "",
"repo": "the-custodian",
"pending_dependencies": []
}
],
"STALLED": [
{
"id": "ACTIVITY-WP-0041-T03",
"status": "wait",
"priority": "high",
"reason": "RPF-IN-0011: return dedicated release principal/custody owner task and native acceptance; D2 scope already accepted",
"blocked_on": "message-from:railiance-platform",
"repo": "activity-core"
},
{
"id": "ACTIVITY-WP-0042-T05",
"status": "wait",
"priority": "high",
"reason": "Platform owner must accept the candidate request/receipt contract, execution authority checks, limits and credential scope before runtime wiring.",
"blocked_on": "message-from:railiance-platform",
"repo": "activity-core"
},
{
"id": "FLEX-WP-0020-T11",
"status": "wait",
"priority": "high",
"reason": "Cleanup approved conditionally (e071b34e): runs once the listed soak evidence, SBOM-IN-0001 receipt and residual handoffs exist.",
"blocked_on": "message-from:sbom-nexus",
"repo": "access-engine"
},
{
"id": "RAPP-POLICY-NEXUS-WP-0002-T02",
"status": "wait",
"priority": "high",
"reason": "Answered railiance-platform's RPF-WP-0043-T02 question on 2026-10-03; waiting on its confirmation and on railiance-apps' values path/revision.",
"blocked_on": "message-from:railiance-platform",
"repo": "rapp-policy-nexus"
},
{
"id": "RPF-WP-0027-T03",
"status": "wait",
"priority": "high",
"reason": "Owner work first: NetKingdom packages the complete resolver sequence as one receipt-producing command; then it enters the attended-window checklist (D1 standing rule).",
"blocked_on": "message-from:net-kingdom",
"repo": "railiance-platform"
},
{
"id": "RPF-WP-0027-T06",
"status": "wait",
"priority": "high",
"reason": "Owner work first: the OpenBao owner confirms mount/path/fields and writer/reader authority and publishes the non-secret routing receipt; the handoff enters the attended window only when a command exists.",
"blocked_on": "message-from:rapp-openbao",
"repo": "railiance-platform"
},
{
"id": "RPF-WP-0035-T08",
"status": "wait",
"priority": "high",
"reason": "Approved conditionally (ab85ae8a, standing change rule): executes once independent archive readback and the interim bearer revocation receipt exist; owner work with audit-core.",
"blocked_on": "message-from:audit-core",
"repo": "railiance-platform"
},
{
"id": "RPF-WP-0056-T03",
"status": "wait",
"priority": "high",
"reason": "User admitted all identified platform humans and coding agents; their supported Kubernetes authentication/group-mapping route remains unimplemented.",
"blocked_on": "message-from:railiance-cluster",
"repo": "railiance-platform"
},
{
"id": "SECRETS-WP-0006-T05",
"status": "wait",
"priority": "high",
"reason": "Owner work first: publish a reviewed scoped-apply command per lane. Per-lane approvals are no longer needed (D1 standing rule); each lane with a command enters the attended window.",
"blocked_on": "message-from:railiance-platform",
"repo": "secrets-engine"
},
{
"id": "SECRETS-WP-0009-T03",
"status": "wait",
"priority": "high",
"reason": "Next owner action: reconcile held parent reservation 6efa9436-6a91-47c0-94e5-b31b5a0e7e3a (EUR 10, observed USD still null) from existing provider records. No corrected runtime digest is admitted; do not retry or replay consumed approvals.",
"blocked_on": "message-from:glas-harness",
"repo": "secrets-engine"
},
{
"id": "SECRETS-WP-0013-T01",
"status": "wait",
"priority": "high",
"reason": "Independent Platform mediated-contract review and identity/approval/PDP owner returns remain required; D3 wrapped primitive source is implemented, no native entitlement is inferred.",
"blocked_on": "message-from:railiance-platform",
"repo": "secrets-engine"
},
{
"id": "USER-WP-0035-T02",
"status": "wait",
"priority": "high",
"reason": "Existing mail service lacks an admitted provider password-setup template and recipient-bound opaque issuer handoff. EMAIL-IN-0001 and NET-IN-0005 request exact owner implementation records; no new founder choice or credential request.",
"blocked_on": "message-from:email-connect",
"repo": "user-engine"
},
{
"id": "USER-WP-0037-T03",
"status": "wait",
"priority": "high",
"reason": "T02 delivered empty no-grant custody integration; real independently authenticated Hub caller credential admission and withdrawal/outage/private integration acceptance remain with state-hub (original f3bcc516; renewed request 350c3e12-284c-43a7-9ab1-116df8bcc007). Hub repository registration is not caller admission.",
"blocked_on": "message-from:state-hub",
"repo": "user-engine"
},
{
"id": "CUST-WP-0079-T05",
"status": "wait",
"priority": "medium",
"reason": "Local guard and repeatable shared-hook installation are implemented. Fleet generator adoption and the required full week of clean history remain unverified; repo-manager owner acceptance is needed.",
"blocked_on": "message-from:repo-manager",
"repo": "the-custodian"
},
{
"id": "RPF-WP-0035-T10",
"status": "wait",
"priority": "medium",
"reason": "Blue lane under the standing change rule (removing access, with reviewed order, rollback and receipts): executes once approval-engine confirms there is no presenter. No founder window needed.",
"blocked_on": "message-from:approval-engine",
"repo": "railiance-platform"
},
{
"id": "RPF-WP-0036-T06",
"status": "wait",
"priority": "medium",
"reason": "Owner-ready handoffs to railiance-forge, rapp-openbao and the ArgoCD bootstrap owner await acceptance; S3 custody contracts stay here until then.",
"blocked_on": "message-from:railiance-forge",
"repo": "railiance-platform"
}
],
"DEPENDENT": [
{
"id": "CUST-WP-0080-T04",
"status": "todo",
"priority": "high",
"reason": "",
"blocked_on": "",
"repo": "the-custodian",
"pending_dependencies": [
"CUST-WP-0080-T03"
]
},
{
"id": "CUST-WP-0080-T05",
"status": "progress",
"priority": "high",
"reason": "",
"blocked_on": "",
"repo": "the-custodian",
"pending_dependencies": [
"RPF-WP-0056",
"RAPP-PLAYWRIGHT-WP-0004"
]
},
{
"id": "FLEX-WP-0036-T02",
"status": "progress",
"priority": "high",
"reason": "",
"blocked_on": "",
"repo": "access-engine",
"pending_dependencies": [
"FLEX-WP-0036-T01"
]
},
{
"id": "MASON-WP-0005-T05",
"status": "progress",
"priority": "high",
"reason": "ops-mason checks and the non-member refusal pass. tegwick enrolled TOTP on 2026-10-11 (message 36b443ab). The founder still runs scripts/verify-fluid-telegram-lane.sh member via the 127.0.0.1:18200 tunnel. fluid-telegram runs the salt create-once probe (FT-WP-0002).",
"blocked_on": "human",
"repo": "ops-mason",
"pending_dependencies": []
},
{
"id": "MASON-WP-0010-T03",
"status": "progress",
"priority": "high",
"reason": "Typed exact structural evidence request is prepared. Native materialization waits on independently authenticated policy/role/layout/catalog observations and CCR42 consumer binding/application. Mint, delivery and first live verification are follow-through, not structural prerequisites.",
"blocked_on": "message-from:railiance-platform",
"repo": "ops-mason",
"pending_dependencies": []
},
{
"id": "WARDEN-WP-0044-T03",
"status": "progress",
"priority": "high",
"reason": "Local durable execution kernel and injected failure harness implemented. Native acceptance still requires authenticated owner machine descriptors/adapters, actual Forgejo materialization receipt, contained session integration, ordered verification and first native run.",
"blocked_on": "",
"repo": "ops-warden",
"pending_dependencies": [
"MASON-WP-0010-T03"
]
},
{
"id": "FLEX-WP-0036-T07",
"status": "todo",
"priority": "medium",
"reason": "",
"blocked_on": "",
"repo": "access-engine",
"pending_dependencies": [
"FLEX-WP-0036-T06"
]
},
{
"id": "MASON-WP-0009-T05",
"status": "progress",
"priority": "medium",
"reason": "ops-mason checks complete (policies, roles, invariant ii, login proof). Owner-run: live operator/ESO token capability checks and a non-member refusal; ops-warden activates the proposed pointer.",
"blocked_on": "human",
"repo": "ops-mason",
"pending_dependencies": []
},
{
"id": "FLEX-WP-0036-T08",
"status": "todo",
"priority": "low",
"reason": "",
"blocked_on": "",
"repo": "access-engine",
"pending_dependencies": [
"FLEX-WP-0036-T06"
]
}
]
}

View file

@ -0,0 +1,148 @@
{
"observed_at": "2026-10-16T21:15:42.219146+00:00",
"scope": "Read-only selected public source/work-record snapshot, not publication or runtime acceptance",
"repos": [
{
"repo": "the-custodian",
"head": "3607d3e08ffdbc473d19e9baf7225843c4075ae0",
"files": [
{
"path": "docs/netkingdom-target-security-architecture.md",
"sha256": "3a74cbd71dc22514033906f6029de1d3e113640bc5faab94f8d58c62e0c86203"
},
{
"path": "docs/netkingdom-target-implementation.md",
"sha256": "a59b906b7fa7dee5f11d4de7fa38c08b974ee2cafa260c44515238ac42e71176"
},
{
"path": "workplans/CUST-WP-0079-deterministic-security-flow.md",
"sha256": "f4ad3299c90f84f49e5867b3d358582c3faea65db4a2a4f595cf1a7f2b806097"
},
{
"path": "scripts/security_flow_daily.py",
"sha256": "4a4e84702b3fb38d086695bb6d601430d8914bd79286b4f683794206f110c353"
},
{
"path": "scripts/sync_resolved_decisions.py",
"sha256": "182433ca7cd31e2b2773246ec1be02569ba6f089ac009add6543e5e48777b5ec"
}
]
},
{
"repo": "repo-manager",
"head": "97ea2489257de08911cc5da1e1b84d8e15780dc3",
"files": [
{
"path": "src/repo_manager/secret_scan.py",
"sha256": "4559d7714308eaa70ce5f8fa91dfcc6b10fe74b607e9f2a94269d31ff76bce9d"
},
{
"path": "workplans/RMGR-WP-0021-secret-detection-controls.md",
"sha256": "fa649c2f3da205627f98a1dfc87ea6cf44dd78fd34e601f8c0597ec55e179d96"
}
]
},
{
"repo": "railiance-forge",
"head": "d322b9c9e8a1caa2b3479b270fff8a0e9f6c1f59",
"files": [
{
"path": "tools/receive-secret-scan/main.go",
"sha256": "5eee28fbe830ebec7abbdfc93275c34d126f92d1b144df93e72aec0dfb821bc6"
},
{
"path": "workplans/FORGE-WP-0006-independent-secret-detection.md",
"sha256": "fbcf3f75e884f6c90554c05525d787f61cfc2b915a206e6010bed447e7a720eb"
}
]
},
{
"repo": "activity-core",
"head": "f9f8df039258a0d8b6fc29dc1f7c4bcc75fdc8c0",
"files": [
{
"path": "activity-definitions/daily-security-flow.md",
"sha256": "0281f0cd818b02d1cbd3a732dfb72145e8cb5e9881c4d80c6f6eb3d664da9375"
},
{
"path": "workplans/ACTIVITY-WP-0047-daily-security-flow-adoption.md",
"sha256": "e0b83b570da688abb405bed165d81db7cf49bc7ff11ef9a8ad86cbee2dc651db"
}
]
},
{
"repo": "rein-aharness",
"head": "9b88cadaa817e5abea7950b4a264d4e2739a5ddb",
"files": [
{
"path": "rein_aharness/claim_loop.py",
"sha256": "5bb6ae75374858d9f515576d0cb8d25e9e555a35f3a89dff9dbe8cd4985f5974"
},
{
"path": "rein_aharness/approaches.py",
"sha256": "d095ed3bd817fee1fede545342e32f7ddbc9e20dad6f90fd7807906d26a031cd"
},
{
"path": "rein_aharness/readiness.py",
"sha256": "9d6945a0ef597c84846e378df8109c88c1d33a4f16c3650da2b110b8501729b1"
},
{
"path": "rein_aharness/glas_execution.py",
"sha256": "860bec6d2225cefb8e6abbab092f2e8521e015fd3a2e5cd76a42569cc8026f6b"
}
]
},
{
"repo": "glas-harness",
"head": "7357115c8311e55ee3d495f5266f501a17ec3c89",
"files": [
{
"path": "profiles/harness.agent-dev-local-1.1.1.yaml",
"sha256": "382c64531a92254c0bcc0d0d6ab059cf190f1dabb7ee2e2ec445974de9ef7edb"
},
{
"path": "src/glas_harness/gateway.py",
"sha256": "5e998bfa53ade5dafbae799dc04b77031b6414e2af49797f45727d59d7397d42"
},
{
"path": "src/glas_harness/profiles.py",
"sha256": "40fba829dfb2a9b3acac4d3bf0539025e4cb5315a2fdbedb8e2ec76e16b29b5b"
}
]
},
{
"repo": "secrets-engine",
"head": "535681052cd55ca613706ecc60c39d299929b3c5",
"files": [
{
"path": "workplans/SECRETS-WP-0014-pdp-transport-and-responder-authentication.md",
"sha256": "6cb4ec3500cddd5156235f1a493d7a979227a6b04eb037bacd23073fd1328efe"
},
{
"path": "src/secrets_engine/lifecycle.py",
"sha256": "c17644356524cd83e96f58a1022638e71fa373cd338c1bf38980ed6cd45a757c"
}
]
},
{
"repo": "access-engine",
"head": "d328a2601095686f74376d87791bff01e2a3a2a3",
"files": [
{
"path": "workplans/FLEX-WP-0024-decision-envelope-authenticity.md",
"sha256": "cc94a493c23c16a3baa718b75ef5d48a596c6386c11534d50fd4839a6f0448f2"
}
]
},
{
"repo": "audit-core",
"head": "64509d2c4039ca1249f9e8aabf2bae030e3640cf",
"files": [
{
"path": "docs/flex-auth-secrets-engine-admission.md",
"sha256": "0825d33b4ed33a0c10eda778a2a7d23764f34086756607a4f5d8a12018ec9a38"
}
]
}
]
}

View file

@ -0,0 +1,62 @@
<!-- custodian-brief: generated by fix-consistency — do not edit manually -->
# Custodian Brief — the-custodian
**Domain:** infotech
**Last synced:** 2026-10-15 23:45 UTC
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
## Active Workstreams
### NetKingdom target security architecture and SEC-DSM implementation
Progress: 2/6 done | workplan_id: `dfadb19b-445e-59c6-bcd6-48e87b912c52`
**Open tasks:**
- ► Close source defects and reconcile uncovered lifecycle gaps `87637402`
- ► Coordinate self-service Playwright identity delivery `ed8b909a`
- ► Establish the target architecture and drive the restructuring `6aca534c`
- · Verify native closure against every requirement `82378b0f`
### Separate agent credentials and establish supervised privileged execution
Progress: 2/5 done | workplan_id: `a98a9f34-83b4-5c8c-8107-e05f7806d50d`
**Open tasks:**
- ! Build and hand out the agent identity `4b88b0b7`
*(wait: Await GLAS-WP-0012 (glas-harness) production acceptance of the local supervised profile; then railiance-platform RBAC, railiance-enablement k3s config, railiance-infra principal mapping and ops-warden certificate issuance.)*
- ! Fleet guidance and harness guards `90725511`
*(wait: Await CUST-WP-0073-T02: the verified agent identity and execution path must exist before it can be documented; Codex/Grok read-denial guard needs glas-harness or harness-owner delivery.)*
- ! Rotate what was exposed `1b41b532`
*(wait: Deferred by founder decision 2026-09-24; reopened only by the founder on one of the three triggers below, in one attended rotation window.)*
### Size Railiance workloads from actual demand and establish a weekly allocation review
Progress: 1/5 done | workplan_id: `2249bddb-7524-5add-bd5c-c4163a6ca0f3`
**Open tasks:**
- ! Measure the Vergabe prototype under representative pilot use `82192370`
*(wait: Await RAPPS-WP-0014-T03 (blocked, needs_human): populated two-user/document workflow run by Bernd and the company contact, giving representative load for p95 <= 2s / zero failures.)*
- ! Review ecosystem allocations and reserve operating room `6dc67558`
*(wait: Await CUST-WP-0071-T02 representative sample; Forgejo/runner demand and zero-request workload review need owner input (railiance-cluster, railiance-forge, resource-control).)*
- ! Apply accepted sizing changes and verify their effects `944f98e2`
*(wait: Await reviewed allocation proposals and applicable workload-owner authority.)*
- ! Establish the regular weekly workload-demand and allocation assessment `20ddd4d7`
*(wait: Await the repeatable observation and accepted sizing/review procedure.)*
### Deterministic security flow: standing authorizations, flow report, and agent re-engagement
Progress: 3/7 done | workplan_id: `ab288f92-d364-5c24-9362-aee451f266c3`
**Open tasks:**
- ! Schedule the flow report and decision sync `1be0569c`
*(wait: Activity Core accepted the schedule in ACTIVITY-WP-0047. Await claiming rein workstation/hub acceptance, owner activation and the first unassisted fire with report/checkpoint evidence.)*
- ! Encode the standing change rule as a policy-nexus guardrail `5dd52156`
*(wait: policy-nexus owns guardrail publication; handoff written as PNEX-WP-0006.)*
- ! Stop agent-authored index and sync commits `8a3dc219`
*(wait: Local guard and repeatable shared-hook installation are implemented. Fleet generator adoption and the required full week of clean history remain unverified; repo-manager owner acceptance is needed.)*
- ! Attended window #1 `e3da807f`
*(wait: Partly done 2026-10-11: list cut from 14 to 3; U05-U07 accepted, U08 waits on KEY-WP-0040-T01. Remaining founder acts: USER-WP-0026-T03 readback, RPF-WP-0044-T08 coulombcore check, the U08 re-run, and Sol's added RPF-WP-0044-T04 issuer login.)*
---
## 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 6aad3a5788fad3abd60c0f1270f160062dc0168b 0 .custodian-brief.md

View file

@ -0,0 +1,62 @@
<!-- custodian-brief: generated by fix-consistency — do not edit manually -->
# Custodian Brief — the-custodian
**Domain:** infotech
**Last synced:** 2026-10-15 23:45 UTC
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
## Active Workstreams
### NetKingdom target security architecture and SEC-DSM implementation
Progress: 2/6 done | workplan_id: `dfadb19b-445e-59c6-bcd6-48e87b912c52`
**Open tasks:**
- ► Close source defects and reconcile uncovered lifecycle gaps `87637402`
- ► Coordinate self-service Playwright identity delivery `ed8b909a`
- ► Establish the target architecture and drive the restructuring `6aca534c`
- · Verify native closure against every requirement `82378b0f`
### Separate agent credentials and establish supervised privileged execution
Progress: 2/5 done | workplan_id: `a98a9f34-83b4-5c8c-8107-e05f7806d50d`
**Open tasks:**
- ! Build and hand out the agent identity `4b88b0b7`
*(wait: Await GLAS-WP-0012 (glas-harness) production acceptance of the local supervised profile; then railiance-platform RBAC, railiance-enablement k3s config, railiance-infra principal mapping and ops-warden certificate issuance.)*
- ! Fleet guidance and harness guards `90725511`
*(wait: Await CUST-WP-0073-T02: the verified agent identity and execution path must exist before it can be documented; Codex/Grok read-denial guard needs glas-harness or harness-owner delivery.)*
- ! Rotate what was exposed `1b41b532`
*(wait: Deferred by founder decision 2026-09-24; reopened only by the founder on one of the three triggers below, in one attended rotation window.)*
### Size Railiance workloads from actual demand and establish a weekly allocation review
Progress: 1/5 done | workplan_id: `2249bddb-7524-5add-bd5c-c4163a6ca0f3`
**Open tasks:**
- ! Measure the Vergabe prototype under representative pilot use `82192370`
*(wait: Await RAPPS-WP-0014-T03 (blocked, needs_human): populated two-user/document workflow run by Bernd and the company contact, giving representative load for p95 <= 2s / zero failures.)*
- ! Review ecosystem allocations and reserve operating room `6dc67558`
*(wait: Await CUST-WP-0071-T02 representative sample; Forgejo/runner demand and zero-request workload review need owner input (railiance-cluster, railiance-forge, resource-control).)*
- ! Apply accepted sizing changes and verify their effects `944f98e2`
*(wait: Await reviewed allocation proposals and applicable workload-owner authority.)*
- ! Establish the regular weekly workload-demand and allocation assessment `20ddd4d7`
*(wait: Await the repeatable observation and accepted sizing/review procedure.)*
### Deterministic security flow: standing authorizations, flow report, and agent re-engagement
Progress: 3/7 done | workplan_id: `ab288f92-d364-5c24-9362-aee451f266c3`
**Open tasks:**
- ! Schedule the flow report and decision sync `1be0569c`
*(wait: Activity Core accepted the schedule in ACTIVITY-WP-0047. Await claiming rein workstation/hub acceptance, owner activation and the first unassisted fire with report/checkpoint evidence.)*
- ! Encode the standing change rule as a policy-nexus guardrail `5dd52156`
*(wait: policy-nexus owns guardrail publication; handoff written as PNEX-WP-0006.)*
- ! Stop agent-authored index and sync commits `8a3dc219`
*(wait: Local guard and repeatable shared-hook installation are implemented. Fleet generator adoption and the required full week of clean history remain unverified; repo-manager owner acceptance is needed.)*
- ! Attended window #1 `e3da807f`
*(wait: Partly done 2026-10-11: list cut from 14 to 3; U05-U07 accepted, U08 waits on KEY-WP-0040-T01. Remaining founder acts: USER-WP-0026-T03 readback, RPF-WP-0044-T08 coulombcore check, the U08 re-run, and Sol's added RPF-WP-0044-T04 issuer login.)*
---
## 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,131 @@
{
"repo_slug": "the-custodian",
"repo_path": "/home/worsch/the-custodian",
"source_revision": "fee99c24bd7dedef98d16035996ba0ea902c624a",
"source_revision_reason": null,
"issues": [
{
"severity": "WARN",
"check_id": "C-35",
"message": "Repo Manager warning: workplans/: repository uses more than one workplan prefix: ['CUST-WP', 'THE-WP']",
"file_path": "workplans/",
"db_id": "",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-35",
"message": "Repo Manager contradictory: workplans/ADHOC-2026-08-25-retired.md: workplan id 'CUST-WP-ADHOC-2026-08-25-RETIRED' is not accepted by the canon kind registry",
"file_path": "workplans/ADHOC-2026-08-25-retired.md",
"db_id": "",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-20",
"message": "Workplan dependency target 'STATE-WP-0092' is not linked to State Hub",
"file_path": "workplans/CUST-WP-0072-fleet-flavor-and-depends-on-backfill.md",
"db_id": "",
"file_value": "STATE-WP-0092",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-40",
"message": "blocker satisfied, task still wait: 'CUST-WP-0073-T05' in workplan 'CUST-WP-0073' \u2014 decision 1591daa1-6e85-4dfd-aadd-5a9fe1ef47fa is resolved",
"file_path": "workplans/CUST-WP-0073-agent-credential-separation.md#CUST-WP-0073-T05",
"db_id": "",
"file_value": "1591daa1-6e85-4dfd-aadd-5a9fe1ef47fa",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-39",
"message": "Human wait task 'CUST-WP-0079-T06' in workplan 'CUST-WP-0079' has needs_human: true without decision_id \u2014 unrouted human gate",
"file_path": "workplans/CUST-WP-0079-deterministic-security-flow.md#CUST-WP-0079-T06",
"db_id": "",
"file_value": "needs_human: true",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-22",
"message": "Task description drift 'CUST-WP-0080-T06': file content differs from DB (file wins)",
"file_path": "workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md#CUST-WP-0080-T06",
"db_id": "6aca534c-5c7a-556f-b6b6-406433670c29",
"file_value": "Founder implementation request, 2026-10-16. Document the optimized target and\ncoordinate GPT-6.1-Sol medium agents again",
"db_value": "Founder implementation request, 2026-10-16. Document the optimized target and\ncoordinate GPT-6.1-Sol medium agents again",
"fixable": true
},
{
"severity": "INFO",
"check_id": "C-08",
"message": "Closed DB workstream 'cust-wp-0010b' (id=416ff571\u2026, status=finished) has no backing workplan file",
"file_path": "",
"db_id": "416ff571-18cf-5cb2-9c8b-836f68a301b3",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "INFO",
"check_id": "C-08",
"message": "Retired DB workstream 'interactive-the-custodian@retired-20260828' (id=370c2481\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work",
"file_path": "",
"db_id": "370c2481-6806-41eb-a917-f8874f03184f",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "INFO",
"check_id": "C-08",
"message": "Retired DB workstream 'adhoc-2026-05-02@retired-20260827' (id=3f54ce9c\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work",
"file_path": "",
"db_id": "3f54ce9c-1f95-42de-894f-0f81a52ba2e8",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "INFO",
"check_id": "C-08",
"message": "Retired DB workstream 'the-custodian-adhoc-2026-07-02@retired-20260827' (id=5c53733b\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work",
"file_path": "",
"db_id": "5c53733b-c077-4a93-be66-9c3af8f9b6fd",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-26",
"message": "Workplan id 'THE-WP-0001' uses prefix 'THE-WP' but repo canonical prefix is 'CUST-WP' \u2014 new plans must conform",
"file_path": "workplans/THE-WP-0001-federation-interface.md",
"db_id": "",
"file_value": "THE-WP-0001",
"db_value": "CUST-WP",
"fixable": false
}
],
"fixes_applied": [
"bindings: synced 89/89",
"C-17 cleared: pushed 2 backlogged commit(s)",
"C-22 fixed: task 6aca534c\u2026 description updated",
"push: pushed"
],
"summary": {
"fail": 0,
"automation_error": 0,
"warn": 7,
"info": 4
},
"result": "warn"
}

View file

@ -0,0 +1 @@
WARN: working-memory commit failed: fatal: Unable to create '/home/worsch/the-custodian/.git/index.lock': Read-only file system

View file

@ -0,0 +1,16 @@
{
"head": "fee99c24bd7dedef98d16035996ba0ea902c624a",
"remote_main": "fee99c24bd7dedef98d16035996ba0ea902c624a",
"sync_summary": {
"fail": 0,
"automation_error": 0,
"warn": 7,
"info": 4
},
"task_count": 6,
"all_statuses_match": true,
"original_staged_and_working_brief_restored": true,
"retained_refinement": "Initial index restore hit sandbox read-only filesystem; exact update-index rerun under required escalation succeeded.",
"goal_status": "active",
"all_16_target_gates_remain_open": true
}

View file

@ -0,0 +1,23 @@
{
"owner_head": "878abc93588e12b6da1bac055cfbb3abb8d7adf1",
"remote_main": "878abc93588e12b6da1bac055cfbb3abb8d7adf1",
"both_source_commits_in_published_history": true,
"workplans": [
{
"workplan": "NK-WP-0048",
"task_count": 19,
"all_statuses_match": true
},
{
"workplan": "NK-WP-0049",
"task_count": 33,
"all_statuses_match": true
}
],
"final_sync_summary": {
"fail": 0,
"automation_error": 0,
"warn": 27,
"info": 5
}
}

View file

@ -0,0 +1,14 @@
{
"workplan_id": "dfadb19b-445e-59c6-bcd6-48e87b912c52",
"id": "578371e4-ffc2-4303-a8b6-86b0a0717b6a",
"topic_id": null,
"task_id": "6aca534c-5c7a-556f-b6b6-406433670c29",
"decision_id": null,
"event_type": "note",
"summary": "Resumed NetKingdom target architecture with GPT-6.1-Sol medium delegates. Published browser checker immutable snapshot source8790dd18 and recovery source5fcd5952. Independent49 source+12 real checker scenarios,73 recovery/source checks and four actual synthetic Docker cases passed. Host-bound exact cleanup and three reaper receipts retained; uncertain Docker response remains uncertain. Normal source scanner false-positive public hashes resolved with strictly typed SHA256 metadata, no exemptions. NK-WP0048 has19 projected tasks; NK-WP0049 has33, T32 source proof done/T33 actual-attempt preparation todo/T18 real recovery wait. Scanner baseline and host/rein schedule readiness measured. All16 TSA gates remain open; full goal active. Two delegates reached model usage limit after completed commands; coordinator verified and finished source publication. Custodian commits a3e98b1 and fee99c2 await normal sync.",
"detail": null,
"author": "codex",
"session_id": null,
"created_at": "2026-10-16T21:39:32.450075Z",
"workstream_id": "dfadb19b-445e-59c6-bcd6-48e87b912c52"
}

View file

@ -0,0 +1,70 @@
"""Apply only independently reviewed local source patches; no stage or publish."""
from pathlib import Path
import argparse
import hashlib
import json
import subprocess
from datetime import datetime, timezone
BASE = Path(__file__).resolve().parent.parent
LANES = {
'groups': ('net-kingdom', '878abc93588e12b6da1bac055cfbb3abb8d7adf1', 'group-provenance-r1/source.patch', '1f3f3ca94c46dadac8f24896053d4d23179de795e50a24298a0cfbcc46cca30e'),
'recovery': ('net-kingdom', '878abc93588e12b6da1bac055cfbb3abb8d7adf1', 'recovery-attempt-r1/candidate.patch', '99cc938d80d64049f927049a5e09a698641e121cf0af2bfa1cbb54ebe7528266'),
'scanner': ('repo-manager', '97ea2489257de08911cc5da1e1b84d8e15780dc3', 'scanner-batch-r1/candidate.patch', 'd2f65f2af5444b241ddcb85a3e71e66d1bbe071bd9a8a7be415be84b5e2f8fdb'),
}
def sha(path): return hashlib.sha256(path.read_bytes()).hexdigest()
def main():
parser = argparse.ArgumentParser()
parser.add_argument('lane', choices=LANES)
parser.add_argument('--apply', action='store_true')
args = parser.parse_args()
slug, revision, relative, digest = LANES[args.lane]
owner = Path('/home/worsch') / slug
patch = BASE / relative
def git(*argv):
return subprocess.check_output(['git', '-C', str(owner), *argv], stderr=subprocess.PIPE)
assert git('rev-parse', 'HEAD').decode().strip() == revision, 'owner revision changed'
assert sha(patch) == digest, 'reviewed patch changed'
paths = [line[6:] for line in patch.read_text().splitlines() if line.startswith('+++ b/')]
assert paths and len(paths) == len(set(paths))
for path in paths:
assert not Path(path).is_absolute() and '..' not in Path(path).parts
assert not git('status', '--porcelain', '--', path).strip(), 'owner source is dirty: ' + path
if args.lane == 'groups':
entries = json.loads((BASE / 'group-provenance-r1/source-manifest.json').read_text())['files']
expected = {e['path']: e['postimage_sha256'] for e in entries}
for e in entries:
target = owner / e['path']
assert (sha(target) if target.exists() else None) == e['preimage_sha256']
elif args.lane == 'scanner':
entries = json.loads((BASE / 'scanner-batch-r1/manifest.json').read_text())['files']
expected = {e['path']: e['candidate_sha256'] for e in entries}
for e in entries: assert sha(owner / e['path']) == e['base_sha256']
else:
candidate = BASE / 'recovery-attempt-r1/test-root'
expected = {p: sha(candidate / p) for p in paths}
closure = json.loads((BASE / 'recovery-attempt-r1/source-closure.json').read_text())
for path, typed in closure.items():
target = candidate / path if path in paths else owner / path
assert 'sha256:' + sha(target) == typed, 'source closure changed: ' + path
assert set(paths) == set(expected)
git('apply', '--check', str(patch))
before_index = git('ls-files', '--stage', '--', '.custodian-brief.md')
before_brief = (owner / '.custodian-brief.md').read_bytes()
if args.apply:
git('apply', str(patch))
for path, wanted in expected.items(): assert sha(owner / path) == wanted, 'postimage changed: ' + path
assert before_index == git('ls-files', '--stage', '--', '.custodian-brief.md')
assert before_brief == (owner / '.custodian-brief.md').read_bytes()
receipt = {'observed_at': datetime.now(timezone.utc).isoformat(), 'lane': args.lane,
'owner': str(owner), 'base_revision': revision, 'patch_sha256': 'sha256:' + digest,
'applied': args.apply, 'source_postimages': {p: 'sha256:' + h for p, h in expected.items()},
'preexisting_brief_preserved': True, 'staged': False, 'published': False,
'native_acceptance': False}
out = Path(__file__).resolve().parent / (args.lane + ('-integration.json' if args.apply else '-preflight.json'))
out.write_text(json.dumps(receipt, indent=2) + '\n')
print(json.dumps({'lane': args.lane, 'applied': args.apply, 'files': len(paths)}))
if __name__ == '__main__': main()

View file

@ -0,0 +1,53 @@
from pathlib import Path
from datetime import datetime, timezone
import hashlib, json, re
base = Path(__file__).resolve().parent
packets = base.parent
def load(path): return json.loads(path.read_text())
def sha(path): return 'sha256:' + hashlib.sha256(path.read_bytes()).hexdigest()
def typed(value):
if isinstance(value, dict): return {k: typed(v) for k,v in value.items()}
if isinstance(value, list): return [typed(v) for v in value]
if isinstance(value,str) and re.fullmatch('[0-9a-f]{64}',value): return 'sha256:' + value
return value
native_path = packets / 'group-provenance-r1/native-fixture/execution-result.json'
native = load(native_path)
assert native['status'] == 'synthetic_native_compatibility_passed'
assert len(native['checks']) == 14 and all(native['checks'].values())
assert native['container_cleanup_verified'] and all(x['status']=='absence_verified' for x in native['container_cleanup_obligations'])
hook_path = packets / 'scanner-batch-r1/configured-hook-results.json'
hooks = load(hook_path)
assert hooks['status'] == 'verified' and hooks['fixture_removed'] and hooks['new_scan_storage_absent']
assert sum(len(x['cases']) for x in hooks['cases']) == 4
benchmark_path = packets / 'scanner-batch-r1/scanner-candidate.json'
benchmark = load(benchmark_path)
measurements = [{k:v for k,v in item.items() if k != 'samples'} for item in benchmark['observations']]
result = {
'schema':'custodian.target-source-results.v1','observed_at':datetime.now(timezone.utc).isoformat(),
'scope':'Reviewed owner source plus generated local/disposable compatibility fixtures; no target gate closed',
'canonical_tests':{'group_provisioner':{'passed':65,'seconds':8.55},
'recovery_preparation':{'passed':111,'seconds':14.50},
'staged_scanner':{'passed':42,'seconds':31.66}},
'recovery_limitations':{'legacy_custody_tests_passed':94,'legacy_real_sops_skipped':61,
'actual_owner_held_recovery':False,'private_admission_complete':False,
'named_file_mode_parent_loss_fixed':False,'sealed_child_fd_lives_until_child_exit':True},
'group_native_fixture':native,'original_group_receipt_sha256':sha(native_path),
'independent_group_cleanup':{'engine':'74f93783-6cef-43a8-a3c4-f62586cd3bc1',
'exact_container_names_absent':True,'exact_run_label_inventory_empty':True,
'hard_parent_loss_claim':False},
'configured_hooks':hooks,'original_hook_receipt_sha256':sha(hook_path),
'scanner_parity':{'exact_old_new_cases':13,'fixture_removed':True},
'scanner_performance':{'observations':measurements,'original_receipt_sha256':sha(benchmark_path),
'warm_repetitions':7,'percentiles':'nearest rank; descriptive small sample',
'baseline_p50_ms':{'clean-1x1KiB':702.358,'clean-16x1KiB':12065.334,'clean-64x1KiB':49171.849,'clean-1x1MiB':2548.887},
'single_blob_slowdown_retained':True,'receive_ci_or_workload_claim':False},
'review_refinements_retained':['Group exactly-one intent row update and database identity checks',
'Definite before-write refusal preserves evidence and releases active fence',
'Scanner batches and reports moved to verified private tmpfs with parent-death cleanup',
'SQLite-on-PVC production guidance corrected before publication against accepted ADR-0015'],
'production_deployments':False,'actual_credentials_used':False,'native_target_acceptance':False,
'all_sixteen_target_gates_remain_open':True,
'encoding':'Published checksum strings carry explicit sha256: type prefixes; original receipt bytes remain workspace-local and separately hashed.'
}
(base / 'verified-results.json').write_text(json.dumps(typed(result),indent=2)+'\n')
print('Retained typed results for 65/111/42 canonical checks, 14 LLDAP oracles and four actual-hook cases.')

View file

@ -0,0 +1,17 @@
from pathlib import Path
import json, subprocess
base=Path(__file__).resolve().parent;root=Path('/home/worsch/the-custodian')
backup=base/'custodian-publication-backup'
try:
result=subprocess.run(['statehub','fix-consistency','--repo','the-custodian','--json'],cwd=root,capture_output=True,text=True,timeout=300)
(base/'custodian-consistency.json').write_text(result.stdout)
(base/'custodian-consistency.stderr.txt').write_text(result.stderr)
(base/'custodian-consistency.exit.json').write_text(json.dumps({'exit_code':result.returncode})+'\n')
finally:
for name,index in json.loads((backup/'originals.json').read_text()).items():
(root/name).write_bytes((backup/(name+'.worktree')).read_bytes())
mode,oid,stage,path=index.split();assert stage=='0'
subprocess.run(['git','-C',str(root),'update-index','--cacheinfo',mode,oid,path],check=True,capture_output=True)
assert subprocess.check_output(['git','-C',str(root),'ls-files','--stage','--',name]).decode()==index
print(json.dumps({'consistency_exit':result.returncode,'original_other_work_restored':True}))
raise SystemExit(result.returncode)

View file

@ -0,0 +1,70 @@
"""Verify owner source, pushed ancestry and exact file-backed task projection."""
from pathlib import Path
from collections import Counter
from datetime import datetime, timezone
import hashlib, json, re, subprocess, urllib.request
import yaml
base = Path(__file__).resolve().parent
def load(path): return json.loads(path.read_text())
def get(path):
with urllib.request.urlopen('http://127.0.0.1:8000'+path,timeout=30) as response: return json.load(response)
def git(root,*args): return subprocess.check_output(['git','-C',str(root),*args],stderr=subprocess.PIPE)
out = {'schema':'custodian.owner-publication-verification.v1','observed_at':datetime.now(timezone.utc).isoformat(),
'native_target_acceptance':False,'all_sixteen_target_gates_remain_open':True,'owners':[]}
health=get('/state/health')
assert health['instance_role']=='primary' and health['instance_label']=='railiance01'
out['projection_instance']={k:health[k] for k in ['instance_role','instance_label']}
for owner in ['net-kingdom','repo-manager']:
root=Path('/home/worsch')/owner
original=load(base/(owner+'-commit.json')); final=load(base/(owner+'-followthrough-commit.json'))
assert original['exit_code']==0 and final['exit_code']==0
head=git(root,'rev-parse','HEAD').decode().strip()
assert head==final['head']
remote=git(root,'ls-remote','origin','refs/heads/main').decode().split()[0]
assert head==remote
git(root,'merge-base','--is-ancestor',original['head'],head)
source_count=0
for path,expected in original['files'].items():
if path.startswith('workplans/'):continue
assert 'sha256:'+hashlib.sha256((root/path).read_bytes()).hexdigest()==expected
source_count+=1
backups=base/(owner+'-publication-backup')
for name,index in load(backups/'originals.json').items():
assert (root/name).read_bytes()==(backups/(name+'.worktree')).read_bytes()
assert git(root,'ls-files','--stage','--',name).decode()==index
assert load(base/(owner+'-sync.exit.json'))['exit_code']==0
sync=load(base/(owner+'-sync.stdout.json'))
if owner=='repo-manager':
assert sync['ok'] and sync['status']=='applied' and sync['commit']==head
assert sync['receipt']['derived_commit']==head
sync_summary={'status':sync['status'],'commit':head,'refused':sync['receipt']['outcome']['counts']['refused']}
assert sync_summary['refused']==0
else:
assert sync['source_revision']==head
assert sync['summary']['fail']==0 and sync['summary']['automation_error']==0
central=load(base/'net-kingdom-central-projection.stdout.json')
assert central['ok'] and central['status']=='applied' and central['commit']==head
assert central['receipt']['derived_commit']==head
assert central['receipt']['outcome']['counts']['refused']==0
sync_summary={'legacy_consistency':sync['summary'],'central_projection':central['status'],
'retained_audit_finding':'Legacy sync matched UUID/status but omitted task record IDs after T09; forge-derived reconciliation repaired projection.'}
workplans=[]
for path in [p for p in original['files'] if p.startswith('workplans/')]:
text=(root/path).read_text();front=yaml.safe_load(text.split('---',2)[1])
source=[yaml.safe_load(block) for block in re.findall(r'```task\s*\n(.*?)```',text,re.S)]
projected=get('/tasks/?workplan_id='+front['state_hub_workstream_id'])
assert isinstance(projected,list)
by_id={item['id']:item for item in projected if item.get('workplan_id')==front['state_hub_workstream_id']}
assert len(by_id)==len(source), (path,len(source),len(by_id))
for task in source:
found=by_id[task['state_hub_task_id']]
assert found['status']==task['status'], task['id']
assert found['record_id']==task['id'], task['id']
workplans.append({'id':front['id'],'tasks':len(source),'source_counts':dict(Counter(t['status'] for t in source)),
'all_ids_uuids_statuses_match':True})
out['owners'].append({'repo':owner,'source_commit':original['head'],'published_head':head,
'remote_main_exact':True,'source_ancestor_verified':True,'source_files_unchanged':source_count,
'normal_source_and_record_hooks_passed':True,'preexisting_other_work_preserved':True,
'sync':sync_summary,'workplans':workplans})
(base/'publication-verification.json').write_text(json.dumps(out,indent=2)+'\n')
print(json.dumps({'verified':True,'owners':out['owners']},indent=2))

View file

@ -0,0 +1,18 @@
from pathlib import Path
import hashlib, json, subprocess
base = Path(__file__).resolve().parent
group = json.loads((base.parent / 'group-provenance-r1/native-storage-proposal-r1/source-evidence.json').read_text())['sources']
recovery = json.loads((base.parent / 'recovery-attempt-r1/t35-t36-source-next/source-review.json').read_text())['source_refs']
out = []
for lane, entries in [('group_storage',group), ('tls_registration',recovery)]:
for entry in entries:
repo, path = entry['repo'], entry['path']
revision = entry.get('revision', entry.get('head'))
expected = entry.get('head_sha256', entry.get('sha256')).removeprefix('sha256:')
raw = subprocess.check_output(['git','-C','/home/worsch/'+repo,'show',revision+':'+path],stderr=subprocess.PIPE)
assert hashlib.sha256(raw).hexdigest() == expected, 'source changed: ' + repo + '/' + path
out.append({'lane':lane,'repo':repo,'path':path,'revision':revision,'sha256':'sha256:'+expected,
'source':entry.get('public_source',entry.get('public_source_ref'))})
assert len(group)==23 and len(recovery)==17
(base / 'source-references.json').write_text(json.dumps({'scope':'exact public Git sources; no live topology or owner assent','all_40_hashes_independently_verified':True,'references':out},indent=2)+'\n')
print('Independently verified 40 public-source references.')

View file

@ -0,0 +1,19 @@
from pathlib import Path
import hashlib, json, subprocess
base = Path(__file__).resolve().parent.parent
root = Path('/home/worsch/net-kingdom')
packet = base / 'group-provenance-r1/storage-policy-correction-r1'
manifest = json.loads((packet / 'manifest.json').read_text())
patch = packet / 'source.patch'
assert hashlib.sha256(patch.read_bytes()).hexdigest() == 'c3e5944a1dba097ddd16d9bfa0021a81002e30242df43129b24746bcdf9f6c2b'
for entry in manifest['files']:
assert hashlib.sha256((root / entry['path']).read_bytes()).hexdigest() == entry['preimage_sha256']
subprocess.run(['git', '-C', str(root), 'apply', '--check', str(patch)], check=True)
subprocess.run(['git', '-C', str(root), 'apply', str(patch)], check=True)
for entry in manifest['files']:
assert hashlib.sha256((root / entry['path']).read_bytes()).hexdigest() == entry['postimage_sha256']
receipt = {'patch_sha256': 'sha256:' + manifest['patch_sha256'], 'applied': True,
'source_code_changed': False, 'production_storage_admitted': False,
'files': {e['path']: 'sha256:' + e['postimage_sha256'] for e in manifest['files']}}
(base / 'resume-integration-r1/storage-guidance-correction.json').write_text(json.dumps(receipt, indent=2) + '\n')
print('Applied three exact documentation corrections; local source proof retained and native admission open.')

View file

@ -0,0 +1,293 @@
{
"ok": true,
"status": "applied",
"repo_slug": "the-custodian",
"commit": "d834d0c4b71ea7ddf81317d5c2746ca514a810c8",
"requests": 2,
"elapsed_ms": 9184.2,
"git": {
"upstream": "origin/main",
"behind": 0,
"ahead": 0
},
"identifiers": {
"schema": "repo-manager.identifier-assignment.v1",
"ok": true,
"executed": true,
"repo": "the-custodian",
"namespace": "helixforge",
"assignments": [],
"historical_invalid_identifiers": [
{
"kind": "workplan",
"id": "CUST-WP-ADHOC-2026-08-25-RETIRED",
"source": "workplans/ADHOC-2026-08-25-retired.md"
}
],
"files_changed": []
},
"receipt": {
"schema": "state-hub.repository-projection-reconcile.v1",
"instance_role": "primary",
"instance_label": "railiance01",
"expected_commit": "d834d0c4b71ea7ddf81317d5c2746ca514a810c8",
"derived_commit": "d834d0c4b71ea7ddf81317d5c2746ca514a810c8",
"outcome": {
"schema": "state-hub.projection-reset.v1",
"repo_slug": "the-custodian",
"commit": "d834d0c4b71ea7ddf81317d5c2746ca514a810c8",
"status": "applied",
"counts": {
"created": 0,
"updated": 88,
"retired": 0,
"refused": 0,
"released": 0,
"created_tasks": 0,
"updated_tasks": 1,
"cancelled_tasks": 0,
"created_edges": 0,
"removed_edges": 0,
"unresolved_edges": 0,
"external_edges": 13
},
"created": [],
"updated": [
"CUST-WP-0000",
"CUST-WP-0000b",
"CUST-WP-0000c",
"CUST-WP-0001",
"CUST-WP-0002",
"CUST-WP-0004",
"CUST-WP-0005",
"CUST-WP-0006",
"CUST-WP-0007",
"CUST-WP-0008",
"CUST-WP-0009",
"CUST-WP-0010",
"CUST-WP-0010b",
"CUST-WP-0013",
"CUST-WP-0014",
"CUST-WP-0015",
"CUST-WP-0016",
"CUST-WP-0017",
"CUST-WP-0017-LEGACY",
"CUST-WP-0018",
"CUST-WP-0019",
"CUST-WP-0020",
"CUST-WP-0021",
"CUST-WP-0022",
"CUST-WP-0023",
"CUST-WP-0024",
"CUST-WP-0025",
"CUST-WP-0026",
"CUST-WP-0027",
"CUST-WP-0028",
"CUST-WP-0029",
"CUST-WP-0030",
"CUST-WP-0031",
"CUST-WP-0032",
"CUST-WP-0033",
"CUST-WP-0034",
"CUST-WP-0035",
"CUST-WP-0036",
"CUST-WP-0037",
"CUST-WP-0039",
"CUST-WP-0040",
"CUST-WP-0041",
"CUST-WP-0043",
"CUST-WP-0044",
"CUST-WP-0045",
"CUST-WP-0046",
"CUST-WP-0047",
"CUST-WP-0048",
"CUST-WP-0049",
"CUST-WP-0050",
"CUST-WP-0051",
"CUST-WP-0052",
"CUST-WP-0053",
"CUST-WP-0053-LEGACY",
"CUST-WP-0054",
"CUST-WP-0055",
"CUST-WP-0056",
"CUST-WP-0057",
"CUST-WP-0058",
"CUST-WP-0059",
"CUST-WP-0060",
"CUST-WP-0061",
"CUST-WP-0062",
"CUST-WP-0063",
"CUST-WP-0064",
"CUST-WP-0065",
"CUST-WP-0066",
"CUST-WP-0067",
"CUST-WP-0068",
"CUST-WP-0069",
"CUST-WP-0070",
"CUST-WP-0071",
"CUST-WP-0072",
"CUST-WP-0073",
"CUST-WP-0074",
"CUST-WP-0075",
"CUST-WP-0076",
"CUST-WP-0077",
"CUST-WP-0078",
"CUST-WP-0079",
"CUST-WP-0080",
"CUST-WP-0081",
"CUST-WP-ADHOC-2026-03-29",
"CUST-WP-ADHOC-2026-05-02",
"CUST-WP-ADHOC-2026-07-02",
"CUST-WP-ADHOC-2026-08-25",
"CUST-WP-ADHOC-2026-09-06",
"THE-WP-0001"
],
"retired": [],
"released": [],
"created_tasks": [],
"updated_tasks": [
"CUST-WP-0080-T06"
],
"cancelled_tasks": [],
"created_edges": [],
"removed_edges": [],
"unresolved_edges": [],
"external_edges": [
{
"workplan": "CUST-WP-0071",
"from": "CUST-WP-0071-T05",
"target": "CUST-WP-0071-T04",
"relationship_type": "starts_after",
"from_workplan_id": "2249bddb-7524-5add-bd5c-c4163a6ca0f3",
"from_task_id": "20ddd4d7-d85d-5b95-bb0a-7b0ca8af76ca",
"to_workplan_id": null,
"to_task_id": "944f98e2-7e42-5894-925b-752423f3a1cc"
},
{
"workplan": "CUST-WP-0071",
"from": "CUST-WP-0071-T03",
"target": "CUST-WP-0071-T01",
"relationship_type": "starts_after",
"from_workplan_id": "2249bddb-7524-5add-bd5c-c4163a6ca0f3",
"from_task_id": "6dc67558-eb1e-5bb6-a667-986f884dd495",
"to_workplan_id": null,
"to_task_id": "4ae8eb8d-f8c5-5256-ae50-c323cfd73d7a"
},
{
"workplan": "CUST-WP-0071",
"from": "CUST-WP-0071-T03",
"target": "CUST-WP-0071-T02",
"relationship_type": "starts_after",
"from_workplan_id": "2249bddb-7524-5add-bd5c-c4163a6ca0f3",
"from_task_id": "6dc67558-eb1e-5bb6-a667-986f884dd495",
"to_workplan_id": null,
"to_task_id": "82192370-2fd7-5363-88d2-3c67889d3d68"
},
{
"workplan": "CUST-WP-0071",
"from": "CUST-WP-0071-T02",
"target": "CUST-WP-0071-T01",
"relationship_type": "starts_after",
"from_workplan_id": "2249bddb-7524-5add-bd5c-c4163a6ca0f3",
"from_task_id": "82192370-2fd7-5363-88d2-3c67889d3d68",
"to_workplan_id": null,
"to_task_id": "4ae8eb8d-f8c5-5256-ae50-c323cfd73d7a"
},
{
"workplan": "CUST-WP-0071",
"from": "CUST-WP-0071-T02",
"target": "RAPPS-WP-0014-T03",
"relationship_type": "starts_after",
"from_workplan_id": "2249bddb-7524-5add-bd5c-c4163a6ca0f3",
"from_task_id": "82192370-2fd7-5363-88d2-3c67889d3d68",
"to_workplan_id": null,
"to_task_id": "dd069c6d-fcc1-5bac-b233-976f2f0d5cd1"
},
{
"workplan": "CUST-WP-0071",
"from": "CUST-WP-0071-T04",
"target": "CUST-WP-0071-T03",
"relationship_type": "starts_after",
"from_workplan_id": "2249bddb-7524-5add-bd5c-c4163a6ca0f3",
"from_task_id": "944f98e2-7e42-5894-925b-752423f3a1cc",
"to_workplan_id": null,
"to_task_id": "6dc67558-eb1e-5bb6-a667-986f884dd495"
},
{
"workplan": "CUST-WP-0073",
"from": "CUST-WP-0073-T02",
"target": "GLAS-WP-0012",
"relationship_type": "blocks",
"from_workplan_id": "a98a9f34-83b4-5c8c-8107-e05f7806d50d",
"from_task_id": "4b88b0b7-dc7e-5612-aa5d-1a08f0530c35",
"to_workplan_id": "170bf1ae-337f-5553-8d1e-03b07100e08f",
"to_task_id": null
},
{
"workplan": "CUST-WP-0073",
"from": "CUST-WP-0073-T04",
"target": "CUST-WP-0073-T02",
"relationship_type": "starts_after",
"from_workplan_id": "a98a9f34-83b4-5c8c-8107-e05f7806d50d",
"from_task_id": "90725511-4e31-549f-b567-47feff1a9ca4",
"to_workplan_id": null,
"to_task_id": "4b88b0b7-dc7e-5612-aa5d-1a08f0530c35"
},
{
"workplan": "CUST-WP-0079",
"from": "CUST-WP-0079-T03",
"target": "ACTIVITY-WP-0047",
"relationship_type": "blocks",
"from_workplan_id": "ab288f92-d364-5c24-9362-aee451f266c3",
"from_task_id": "1be0569c-221d-5925-a0ff-fd2af88e52b1",
"to_workplan_id": "ee97869b-dca2-54c9-aa18-0b479c244fb3",
"to_task_id": null
},
{
"workplan": "CUST-WP-0079",
"from": "CUST-WP-0079-T04",
"target": "PNEX-WP-0006",
"relationship_type": "blocks",
"from_workplan_id": "ab288f92-d364-5c24-9362-aee451f266c3",
"from_task_id": "5dd52156-09a8-5781-b288-99ae822f59a6",
"to_workplan_id": "774c5037-a3d0-5e65-893c-d18970acd66a",
"to_task_id": null
},
{
"workplan": "CUST-WP-0080",
"from": "CUST-WP-0080-T04",
"target": "CUST-WP-0080-T03",
"relationship_type": "starts_after",
"from_workplan_id": "dfadb19b-445e-59c6-bcd6-48e87b912c52",
"from_task_id": "82378b0f-7004-5c2f-9683-b356a1227576",
"to_workplan_id": null,
"to_task_id": "87637402-02f1-57c1-8fbc-4141598af8e3"
},
{
"workplan": "CUST-WP-0080",
"from": "CUST-WP-0080-T05",
"target": "RPF-WP-0056",
"relationship_type": "blocks",
"from_workplan_id": "dfadb19b-445e-59c6-bcd6-48e87b912c52",
"from_task_id": "ed8b909a-23bf-5e5f-b600-e477fbae136b",
"to_workplan_id": "31933d08-c877-5fda-aff9-6220c3705c92",
"to_task_id": null
},
{
"workplan": "CUST-WP-0080",
"from": "CUST-WP-0080-T05",
"target": "RAPP-PLAYWRIGHT-WP-0004",
"relationship_type": "blocks",
"from_workplan_id": "dfadb19b-445e-59c6-bcd6-48e87b912c52",
"from_task_id": "ed8b909a-23bf-5e5f-b600-e477fbae136b",
"to_workplan_id": "f9280c40-201e-59b5-a6f0-1aace8e0e2e2",
"to_task_id": null
}
],
"refused": [],
"notes": [
"Preserved bound historical row and task UUIDs: CUST-WP-ADHOC-2026-08-25-RETIRED"
]
}
}
}

View file

@ -0,0 +1,11 @@
{
"files": {
"docs/netkingdom-target-implementation.md": "sha256:a49de3368eb6f0b5429909797602be392c6e4982559358143180ab1f168e44a5",
"workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md": "sha256:b45d908e47d411e3fb56ec7099b5b646a9fbd6806422bf9a744d960217c9d01c",
"docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/README.md": "sha256:d4710b067289d8fa643beec6dab847c48ad276bd039aa9ad54192ebbe356fe44",
"docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/publication-verification.json": "sha256:4893515106b3cf92fdabda701713a3276884cda16c84049d9bebbdf92294d944",
"docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/verified-results.json": "sha256:2e840481872de8784eb0351ecb0aa09ad307339218f05e86b8222e22cd0afd44",
"docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/native-next-steps.md": "sha256:c468533e1bc0970e35409c3c71d30bc31e67c17a99b8db16d2e7046f06faf5d1",
"docs/changes/netkingdom-target-2026-10-16/resume-integration-r1/source-references.json": "sha256:6e12ae4931e41f0ff8162fd8226fc49dfad4a21a2f42677fbd7149f4dd086faa"
}
}

View file

@ -0,0 +1,131 @@
{
"repo_slug": "the-custodian",
"repo_path": "/home/worsch/the-custodian",
"source_revision": "d834d0c4b71ea7ddf81317d5c2746ca514a810c8",
"source_revision_reason": null,
"issues": [
{
"severity": "WARN",
"check_id": "C-35",
"message": "Repo Manager warning: workplans/: repository uses more than one workplan prefix: ['CUST-WP', 'THE-WP']",
"file_path": "workplans/",
"db_id": "",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-35",
"message": "Repo Manager contradictory: workplans/ADHOC-2026-08-25-retired.md: workplan id 'CUST-WP-ADHOC-2026-08-25-RETIRED' is not accepted by the canon kind registry",
"file_path": "workplans/ADHOC-2026-08-25-retired.md",
"db_id": "",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-20",
"message": "Workplan dependency target 'STATE-WP-0092' is not linked to State Hub",
"file_path": "workplans/CUST-WP-0072-fleet-flavor-and-depends-on-backfill.md",
"db_id": "",
"file_value": "STATE-WP-0092",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-40",
"message": "blocker satisfied, task still wait: 'CUST-WP-0073-T05' in workplan 'CUST-WP-0073' \u2014 decision 1591daa1-6e85-4dfd-aadd-5a9fe1ef47fa is resolved",
"file_path": "workplans/CUST-WP-0073-agent-credential-separation.md#CUST-WP-0073-T05",
"db_id": "",
"file_value": "1591daa1-6e85-4dfd-aadd-5a9fe1ef47fa",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-39",
"message": "Human wait task 'CUST-WP-0079-T06' in workplan 'CUST-WP-0079' has needs_human: true without decision_id \u2014 unrouted human gate",
"file_path": "workplans/CUST-WP-0079-deterministic-security-flow.md#CUST-WP-0079-T06",
"db_id": "",
"file_value": "needs_human: true",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-22",
"message": "Task description drift 'CUST-WP-0080-T06': file content differs from DB (file wins)",
"file_path": "workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md#CUST-WP-0080-T06",
"db_id": "6aca534c-5c7a-556f-b6b6-406433670c29",
"file_value": "Founder implementation request, 2026-10-16. Document the optimized target and\ncoordinate GPT-6.1-Sol medium agents again",
"db_value": "Founder implementation request, 2026-10-16. Document the optimized target and\ncoordinate GPT-6.1-Sol medium agents again",
"fixable": true
},
{
"severity": "INFO",
"check_id": "C-08",
"message": "Closed DB workstream 'cust-wp-0010b' (id=416ff571\u2026, status=finished) has no backing workplan file",
"file_path": "",
"db_id": "416ff571-18cf-5cb2-9c8b-836f68a301b3",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "INFO",
"check_id": "C-08",
"message": "Retired DB workstream 'interactive-the-custodian@retired-20260828' (id=370c2481\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work",
"file_path": "",
"db_id": "370c2481-6806-41eb-a917-f8874f03184f",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "INFO",
"check_id": "C-08",
"message": "Retired DB workstream 'adhoc-2026-05-02@retired-20260827' (id=3f54ce9c\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work",
"file_path": "",
"db_id": "3f54ce9c-1f95-42de-894f-0f81a52ba2e8",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "INFO",
"check_id": "C-08",
"message": "Retired DB workstream 'the-custodian-adhoc-2026-07-02@retired-20260827' (id=5c53733b\u2026, status=finished) has no backing workplan file \u2014 projection tombstone, not live work",
"file_path": "",
"db_id": "5c53733b-c077-4a93-be66-9c3af8f9b6fd",
"file_value": "",
"db_value": "",
"fixable": false
},
{
"severity": "WARN",
"check_id": "C-26",
"message": "Workplan id 'THE-WP-0001' uses prefix 'THE-WP' but repo canonical prefix is 'CUST-WP' \u2014 new plans must conform",
"file_path": "workplans/THE-WP-0001-federation-interface.md",
"db_id": "",
"file_value": "THE-WP-0001",
"db_value": "CUST-WP",
"fixable": false
}
],
"fixes_applied": [
"bindings: synced 89/89",
"C-17 cleared: pushed 1 backlogged commit(s)",
"C-22 fixed: task 6aca534c\u2026 description updated",
"push: pushed"
],
"summary": {
"fail": 0,
"automation_error": 0,
"warn": 7,
"info": 4
},
"result": "warn"
}

View file

@ -0,0 +1 @@
WARN: working-memory commit failed: Generated-only commits rejected: staged

View file

@ -0,0 +1,21 @@
{
"head": "d834d0c4b71ea7ddf81317d5c2746ca514a810c8",
"remote_main_exact": true,
"all_seven_committed_files_unchanged": true,
"all_six_custodian_task_bindings_match": true,
"task_counts": {
"done": 2,
"progress": 3,
"todo": 1
},
"sync_summary": {
"fail": 0,
"automation_error": 0,
"warn": 7,
"info": 4
},
"central_projection": "applied",
"retained_finding": "Legacy Custodian consistency also omitted some task record IDs; final audit failed until existing forge-derived reconciliation repaired them.",
"preexisting_staged_brief_and_work_records_preserved": true,
"goal_complete": false
}

View file

@ -0,0 +1,62 @@
<!-- custodian-brief: generated by fix-consistency — do not edit manually -->
# Custodian Brief — the-custodian
**Domain:** infotech
**Last synced:** 2026-10-15 23:45 UTC
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
## Active Workstreams
### NetKingdom target security architecture and SEC-DSM implementation
Progress: 2/6 done | workplan_id: `dfadb19b-445e-59c6-bcd6-48e87b912c52`
**Open tasks:**
- ► Close source defects and reconcile uncovered lifecycle gaps `87637402`
- ► Coordinate self-service Playwright identity delivery `ed8b909a`
- ► Establish the target architecture and drive the restructuring `6aca534c`
- · Verify native closure against every requirement `82378b0f`
### Separate agent credentials and establish supervised privileged execution
Progress: 2/5 done | workplan_id: `a98a9f34-83b4-5c8c-8107-e05f7806d50d`
**Open tasks:**
- ! Build and hand out the agent identity `4b88b0b7`
*(wait: Await GLAS-WP-0012 (glas-harness) production acceptance of the local supervised profile; then railiance-platform RBAC, railiance-enablement k3s config, railiance-infra principal mapping and ops-warden certificate issuance.)*
- ! Fleet guidance and harness guards `90725511`
*(wait: Await CUST-WP-0073-T02: the verified agent identity and execution path must exist before it can be documented; Codex/Grok read-denial guard needs glas-harness or harness-owner delivery.)*
- ! Rotate what was exposed `1b41b532`
*(wait: Deferred by founder decision 2026-09-24; reopened only by the founder on one of the three triggers below, in one attended rotation window.)*
### Size Railiance workloads from actual demand and establish a weekly allocation review
Progress: 1/5 done | workplan_id: `2249bddb-7524-5add-bd5c-c4163a6ca0f3`
**Open tasks:**
- ! Measure the Vergabe prototype under representative pilot use `82192370`
*(wait: Await RAPPS-WP-0014-T03 (blocked, needs_human): populated two-user/document workflow run by Bernd and the company contact, giving representative load for p95 <= 2s / zero failures.)*
- ! Review ecosystem allocations and reserve operating room `6dc67558`
*(wait: Await CUST-WP-0071-T02 representative sample; Forgejo/runner demand and zero-request workload review need owner input (railiance-cluster, railiance-forge, resource-control).)*
- ! Apply accepted sizing changes and verify their effects `944f98e2`
*(wait: Await reviewed allocation proposals and applicable workload-owner authority.)*
- ! Establish the regular weekly workload-demand and allocation assessment `20ddd4d7`
*(wait: Await the repeatable observation and accepted sizing/review procedure.)*
### Deterministic security flow: standing authorizations, flow report, and agent re-engagement
Progress: 3/7 done | workplan_id: `ab288f92-d364-5c24-9362-aee451f266c3`
**Open tasks:**
- ! Schedule the flow report and decision sync `1be0569c`
*(wait: Activity Core accepted the schedule in ACTIVITY-WP-0047. Await claiming rein workstation/hub acceptance, owner activation and the first unassisted fire with report/checkpoint evidence.)*
- ! Encode the standing change rule as a policy-nexus guardrail `5dd52156`
*(wait: policy-nexus owns guardrail publication; handoff written as PNEX-WP-0006.)*
- ! Stop agent-authored index and sync commits `8a3dc219`
*(wait: Local guard and repeatable shared-hook installation are implemented. Fleet generator adoption and the required full week of clean history remain unverified; repo-manager owner acceptance is needed.)*
- ! Attended window #1 `e3da807f`
*(wait: Partly done 2026-10-11: list cut from 14 to 3; U05-U07 accepted, U08 waits on KEY-WP-0040-T01. Remaining founder acts: USER-WP-0026-T03 readback, RPF-WP-0044-T08 coulombcore check, the U08 re-run, and Sol's added RPF-WP-0044-T04 issuer login.)*
---
## 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,536 @@
# Work Records — the-custodian
> Generated by `statehub fix-consistency` (CUST-WP-0061-T04, work-record
> stage 3). Do not edit by hand — edit the source file/block listed for
> each record and re-run fix-consistency to refresh this index. Archived
> workplans are omitted; closed decisions/intakes/engagements stay listed
> so recently-resolved work is still visible. [auto]
| Kind | ID | Status | Lane | Source |
| --- | --- | --- | --- | --- |
| workplan | CUST-WP-ADHOC-2026-05-02 | done | — | workplans/ADHOC-2026-05-02.md |
| workplan | CUST-WP-ADHOC-2026-07-02 | finished | — | workplans/ADHOC-2026-07-02.md |
| workplan | CUST-WP-ADHOC-2026-08-25-RETIRED | finished | — | workplans/ADHOC-2026-08-25-retired.md |
| workplan | CUST-WP-ADHOC-2026-08-25 | finished | — | workplans/ADHOC-2026-08-25.md |
| workplan | CUST-WP-ADHOC-2026-09-06 | finished | — | workplans/ADHOC-2026-09-06.md |
| workplan | CUST-WP-0000 | finished | — | workplans/CUST-WP-0000-state-hub-v0.1-build-deploy.md |
| workplan | CUST-WP-0000b | finished | — | workplans/CUST-WP-0000b-state-hub-v0.2-decisions-dependencies.md |
| workplan | CUST-WP-0001 | done | — | workplans/CUST-WP-0001-custodian-agent-runtime.md |
| workplan | CUST-WP-0002 | finished | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md |
| workplan | CUST-WP-0004 | finished | — | workplans/CUST-WP-0004-ep-td-tracking.md |
| workplan | CUST-WP-0005 | finished | — | workplans/CUST-WP-0005-dynamic-domains.md |
| workplan | CUST-WP-0006 | done | — | workplans/CUST-WP-0006-gems-state-hub.md |
| workplan | CUST-WP-0007 | finished | — | workplans/CUST-WP-0007-gems-migration.md |
| workplan | CUST-WP-0008 | done | — | workplans/CUST-WP-0008-consistency-engine.md |
| workplan | CUST-WP-0009 | finished | — | workplans/CUST-WP-0009-human-interventions.md |
| workplan | CUST-WP-0010 | finished | — | workplans/CUST-WP-0010-workstream-lifecycle-docs.md |
| workplan | CUST-WP-0010b | finished | — | workplans/CUST-WP-0010b-domain-and-repo-goals.md |
| workplan | CUST-WP-0013 | finished | — | workplans/CUST-WP-0013-sbom-infra-expansion.md |
| workplan | CUST-WP-0015 | done | — | workplans/CUST-WP-0015-agent-inbox.md |
| workplan | CUST-WP-0016 | done | — | workplans/CUST-WP-0016-kaizen-agentic-integration.md |
| workplan | CUST-WP-0017 | done | — | workplans/CUST-WP-0017-scope-md-agent-and-coverage.md |
| workplan | CUST-WP-0018 | done | — | workplans/CUST-WP-0018-api-hardening-code-quality.md |
| workplan | CUST-WP-0019 | done | — | workplans/CUST-WP-0019-dashboard-ux-polish.md |
| workplan | CUST-WP-0020 | done | — | workplans/CUST-WP-0020-test-suite.md |
| workplan | CUST-WP-0021 | done | — | workplans/CUST-WP-0021-multi-host-repo-paths.md |
| workplan | CUST-WP-0022 | done | — | workplans/CUST-WP-0022-capability-requests.md |
| workplan | CUST-WP-0023 | done | — | workplans/CUST-WP-0023-tpsc.md |
| workplan | CUST-WP-0024 | done | — | workplans/CUST-WP-0024-repo-doi-gate.md |
| workplan | CUST-WP-0026 | done | — | workplans/CUST-WP-0026-distributed-consistency.md |
| workplan | CUST-WP-0027 | done | — | workplans/CUST-WP-0027-capability-request-dispute.md |
| workplan | CUST-WP-0029 | done | — | workplans/CUST-WP-0029-token-consumption-tracking.md |
| workplan | CUST-WP-0030 | done | — | workplans/CUST-WP-0030-dashboard-entity-list-ux.md |
| workplan | CUST-WP-0031 | done | — | workplans/CUST-WP-0031-capability-registry.md |
| workplan | CUST-WP-0032 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md |
| workplan | CUST-WP-0033 | done | — | workplans/CUST-WP-0033-interface-change-registry.md |
| workplan | CUST-WP-0039 | done | — | workplans/CUST-WP-0039-dashboard-poll-optimization.md |
| workplan | CUST-WP-0040 | finished | — | workplans/CUST-WP-0040-state-hub-nats-activity-core-integration.md |
| workplan | CUST-WP-0041 | done | — | workplans/CUST-WP-0041-api-performance.md |
| workplan | CUST-WP-0043 | finished | — | workplans/CUST-WP-0043-state-hub-repo-extraction.md |
| workplan | CUST-WP-0044 | finished | — | workplans/CUST-WP-0044-daily-statehub-wsjf-triage.md |
| workplan | CUST-WP-0045 | finished | — | workplans/CUST-WP-0045-activity-core-daily-triage-runner.md |
| workplan | CUST-WP-0069 | finished | — | workplans/CUST-WP-0045-cutover-runbook.md |
| workplan | CUST-WP-0046 | finished | — | workplans/CUST-WP-0046-hourly-recently-on-scope-activity-core.md |
| workplan | CUST-WP-0047 | finished | — | workplans/CUST-WP-0047-ops-hub-service-inventory-now-view.md |
| workplan | CUST-WP-0048 | finished | — | workplans/CUST-WP-0048-hub-core-state-hub-import-refactor.md |
| workplan | CUST-WP-0049 | finished | — | workplans/CUST-WP-0049-interhub-bootstrap-access-lane.md |
| workplan | CUST-WP-0050 | finished | — | workplans/CUST-WP-0050-repo-classification-registration-redesign.md |
| workplan | CUST-WP-0051 | finished | — | workplans/CUST-WP-0051-infrastructure-stabilization-metaplan.md |
| workplan | CUST-WP-0052 | finished | — | workplans/CUST-WP-0052-core-hub-fos-reset.md |
| workplan | CUST-WP-0053 | finished | — | workplans/CUST-WP-0053-coordination-hygiene-improvements.md |
| workplan | CUST-WP-0055 | finished | — | workplans/CUST-WP-0055-workplan-terminology-fleet-refactor.md |
| workplan | CUST-WP-0056 | finished | — | workplans/CUST-WP-0056-daily-todo-md-stale-review.md |
| workplan | CUST-WP-0059 | finished | — | workplans/CUST-WP-0059-forgejo-ci-publish-cutover-tail.md |
| workplan | CUST-WP-0060 | finished | — | workplans/CUST-WP-0060-work-record-canonization.md |
| workplan | CUST-WP-0061 | finished | — | workplans/CUST-WP-0061-work-record-stage3-intake-promotion-index.md |
| workplan | CUST-WP-0062 | finished | — | workplans/CUST-WP-0062-sbom-nexus-daily-catchup.md |
| workplan | CUST-WP-0063 | finished | — | workplans/CUST-WP-0063-inbox-governance-packets.md |
| workplan | CUST-WP-0064 | finished | — | workplans/CUST-WP-0064-sbom-controlled-scan-inputs.md |
| workplan | CUST-WP-0065 | finished | — | workplans/CUST-WP-0065-reclassify-repos-and-guidance-docs-to-the-new-sector-domain.md |
| workplan | CUST-WP-0066 | finished | — | workplans/CUST-WP-0066-repository-qualified-ad-hoc-identifiers.md |
| workplan | CUST-WP-0067 | finished | — | workplans/CUST-WP-0067-hub-authority-target-resolution.md |
| workplan | CUST-WP-0068 | finished | — | workplans/CUST-WP-0068-cache-only-work-record-recovery.md |
| workplan | CUST-WP-0070 | finished | — | workplans/CUST-WP-0070-publication-repo-category.md |
| workplan | CUST-WP-0071 | blocked | — | workplans/CUST-WP-0071-measured-workload-sizing-and-weekly-review.md |
| workplan | CUST-WP-0072 | finished | — | workplans/CUST-WP-0072-fleet-flavor-and-depends-on-backfill.md |
| workplan | CUST-WP-0073 | blocked | — | workplans/CUST-WP-0073-agent-credential-separation.md |
| workplan | CUST-WP-0074 | finished | — | workplans/CUST-WP-0074-qualified-wait-states.md |
| workplan | CUST-WP-0075 | finished | — | workplans/CUST-WP-0075-dependency-graph-driven-unblocking.md |
| workplan | CUST-WP-0076 | finished | — | workplans/CUST-WP-0076-provisioning-window-cadence.md |
| workplan | CUST-WP-0077 | finished | — | workplans/CUST-WP-0077-dependency-root-execution-wave.md |
| workplan | CUST-WP-0078 | finished | — | workplans/CUST-WP-0078-security-collaboration-consolidation.md |
| workplan | CUST-WP-0079 | blocked | — | workplans/CUST-WP-0079-deterministic-security-flow.md |
| workplan | CUST-WP-0080 | active | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| workplan | CUST-WP-0081 | finished | — | workplans/CUST-WP-0081-netkingdom-architecture-review-and-reuse.md |
| workplan | THE-WP-0001 | finished | — | workplans/THE-WP-0001-federation-interface.md |
| task | CUST-WP-ADHOC-2026-05-02-T01 | done | — | workplans/ADHOC-2026-05-02.md |
| task | CUST-WP-ADHOC-2026-07-02-T01 | done | — | workplans/ADHOC-2026-07-02.md |
| task | ADHOC-2026-08-25-T01 | done | — | workplans/ADHOC-2026-08-25-retired.md |
| task | CUST-WP-ADHOC-2026-08-25-T01 | done | — | workplans/ADHOC-2026-08-25.md |
| task | CUST-WP-ADHOC-2026-09-06-T01 | done | — | workplans/ADHOC-2026-09-06.md |
| task | CUST-WP-0001-T01 | done | — | workplans/CUST-WP-0001-custodian-agent-runtime.md |
| task | CUST-WP-0001-T02 | done | — | workplans/CUST-WP-0001-custodian-agent-runtime.md |
| task | CUST-WP-0002-T01 | done | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md |
| task | CUST-WP-0002-T02 | done | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md |
| task | CUST-WP-0002-T03 | done | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md |
| task | CUST-WP-0002-T04 | done | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md |
| task | CUST-WP-0002-T05 | done | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md |
| task | CUST-WP-0002-T06 | done | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md |
| task | CUST-WP-0002-T07 | done | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md |
| task | CUST-WP-0002-T08 | done | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md |
| task | CUST-WP-0002-T09 | done | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md |
| task | CUST-WP-0002-T10 | done | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md |
| task | CUST-WP-0002-T11 | done | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md |
| task | CUST-WP-0002-T12 | done | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md |
| task | CUST-WP-0002-T13 | done | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md |
| task | CUST-WP-0002-T14 | done | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md |
| task | CUST-WP-0002-T15 | done | — | workplans/CUST-WP-0002-contribution-tracking-sbom.md |
| task | CUST-WP-0004-T01 | done | — | workplans/CUST-WP-0004-ep-td-tracking.md |
| task | CUST-WP-0004-T02 | done | — | workplans/CUST-WP-0004-ep-td-tracking.md |
| task | CUST-WP-0004-T03 | done | — | workplans/CUST-WP-0004-ep-td-tracking.md |
| task | CUST-WP-0004-T04 | done | — | workplans/CUST-WP-0004-ep-td-tracking.md |
| task | CUST-WP-0004-T05 | done | — | workplans/CUST-WP-0004-ep-td-tracking.md |
| task | CUST-WP-0004-T06 | done | — | workplans/CUST-WP-0004-ep-td-tracking.md |
| task | CUST-WP-0004-T07 | done | — | workplans/CUST-WP-0004-ep-td-tracking.md |
| task | CUST-WP-0004-T08 | done | — | workplans/CUST-WP-0004-ep-td-tracking.md |
| task | CUST-WP-0004-T09 | done | — | workplans/CUST-WP-0004-ep-td-tracking.md |
| task | CUST-WP-0004-T10 | done | — | workplans/CUST-WP-0004-ep-td-tracking.md |
| task | CUST-WP-0005-T01 | done | — | workplans/CUST-WP-0005-dynamic-domains.md |
| task | CUST-WP-0005-T02 | done | — | workplans/CUST-WP-0005-dynamic-domains.md |
| task | CUST-WP-0005-T03 | done | — | workplans/CUST-WP-0005-dynamic-domains.md |
| task | CUST-WP-0005-T04 | done | — | workplans/CUST-WP-0005-dynamic-domains.md |
| task | CUST-WP-0005-T05 | done | — | workplans/CUST-WP-0005-dynamic-domains.md |
| task | CUST-WP-0005-T06 | done | — | workplans/CUST-WP-0005-dynamic-domains.md |
| task | CUST-WP-0005-T07 | done | — | workplans/CUST-WP-0005-dynamic-domains.md |
| task | CUST-WP-0005-T08 | done | — | workplans/CUST-WP-0005-dynamic-domains.md |
| task | CUST-WP-0005-T09 | done | — | workplans/CUST-WP-0005-dynamic-domains.md |
| task | CUST-WP-0005-T10 | done | — | workplans/CUST-WP-0005-dynamic-domains.md |
| task | CUST-WP-0005-T11 | done | — | workplans/CUST-WP-0005-dynamic-domains.md |
| task | T01 | done | — | workplans/CUST-WP-0006-gems-state-hub.md |
| task | T02 | done | — | workplans/CUST-WP-0006-gems-state-hub.md |
| task | T03 | done | — | workplans/CUST-WP-0006-gems-state-hub.md |
| task | T04 | done | — | workplans/CUST-WP-0006-gems-state-hub.md |
| task | T05 | done | — | workplans/CUST-WP-0006-gems-state-hub.md |
| task | T01 | done | — | workplans/CUST-WP-0007-gems-migration.md |
| task | T02 | done | — | workplans/CUST-WP-0007-gems-migration.md |
| task | T03 | done | — | workplans/CUST-WP-0007-gems-migration.md |
| task | T04 | done | — | workplans/CUST-WP-0007-gems-migration.md |
| task | T05 | done | — | workplans/CUST-WP-0007-gems-migration.md |
| task | T06 | done | — | workplans/CUST-WP-0007-gems-migration.md |
| task | T07 | done | — | workplans/CUST-WP-0007-gems-migration.md |
| task | T08 | done | — | workplans/CUST-WP-0007-gems-migration.md |
| task | T09 | done | — | workplans/CUST-WP-0007-gems-migration.md |
| task | T10 | done | — | workplans/CUST-WP-0007-gems-migration.md |
| task | T11 | done | — | workplans/CUST-WP-0007-gems-migration.md |
| task | T12 | done | — | workplans/CUST-WP-0007-gems-migration.md |
| task | T13 | done | — | workplans/CUST-WP-0007-gems-migration.md |
| task | T14 | done | — | workplans/CUST-WP-0007-gems-migration.md |
| task | T15 | done | — | workplans/CUST-WP-0007-gems-migration.md |
| task | T16 | done | — | workplans/CUST-WP-0007-gems-migration.md |
| task | T17 | done | — | workplans/CUST-WP-0007-gems-migration.md |
| task | CUST-WP-0008-T01 | done | — | workplans/CUST-WP-0008-consistency-engine.md |
| task | CUST-WP-0008-T02 | done | — | workplans/CUST-WP-0008-consistency-engine.md |
| task | CUST-WP-0008-T03 | done | — | workplans/CUST-WP-0008-consistency-engine.md |
| task | CUST-WP-0008-T04 | done | — | workplans/CUST-WP-0008-consistency-engine.md |
| task | CUST-WP-0009-T1 | done | — | workplans/CUST-WP-0009-human-interventions.md |
| task | CUST-WP-0009-T2 | done | — | workplans/CUST-WP-0009-human-interventions.md |
| task | CUST-WP-0009-T3 | done | — | workplans/CUST-WP-0009-human-interventions.md |
| task | CUST-WP-0010-T1 | done | — | workplans/CUST-WP-0010-workstream-lifecycle-docs.md |
| task | CUST-WP-0010-T2 | done | — | workplans/CUST-WP-0010-workstream-lifecycle-docs.md |
| task | CUST-WP-0010-T3 | done | — | workplans/CUST-WP-0010-workstream-lifecycle-docs.md |
| task | CUST-WP-0010b-T01 | done | — | workplans/CUST-WP-0010b-domain-and-repo-goals.md |
| task | CUST-WP-0010b-T02 | done | — | workplans/CUST-WP-0010b-domain-and-repo-goals.md |
| task | CUST-WP-0013-T01 | done | — | workplans/CUST-WP-0013-sbom-infra-expansion.md |
| task | CUST-WP-0013-T02 | done | — | workplans/CUST-WP-0013-sbom-infra-expansion.md |
| task | CUST-WP-0013-T03 | done | — | workplans/CUST-WP-0013-sbom-infra-expansion.md |
| task | CUST-WP-0013-T04 | done | — | workplans/CUST-WP-0013-sbom-infra-expansion.md |
| task | CUST-WP-0013-T05 | done | — | workplans/CUST-WP-0013-sbom-infra-expansion.md |
| task | CUST-WP-0013-T06 | done | — | workplans/CUST-WP-0013-sbom-infra-expansion.md |
| task | CUST-WP-0013-T07 | done | — | workplans/CUST-WP-0013-sbom-infra-expansion.md |
| task | CUST-WP-0013-T08 | done | — | workplans/CUST-WP-0013-sbom-infra-expansion.md |
| task | CUST-WP-0013-T09 | done | — | workplans/CUST-WP-0013-sbom-infra-expansion.md |
| task | CUST-WP-0013-T10 | done | — | workplans/CUST-WP-0013-sbom-infra-expansion.md |
| task | CUST-WP-0013-T11 | done | — | workplans/CUST-WP-0013-sbom-infra-expansion.md |
| task | CUST-WP-0013-T12 | done | — | workplans/CUST-WP-0013-sbom-infra-expansion.md |
| task | CUST-WP-0013-T13 | done | — | workplans/CUST-WP-0013-sbom-infra-expansion.md |
| task | CUST-WP-0013-T14 | done | — | workplans/CUST-WP-0013-sbom-infra-expansion.md |
| task | CUST-WP-0015-T01 | done | — | workplans/CUST-WP-0015-agent-inbox.md |
| task | CUST-WP-0015-T02 | done | — | workplans/CUST-WP-0015-agent-inbox.md |
| task | CUST-WP-0015-T03 | done | — | workplans/CUST-WP-0015-agent-inbox.md |
| task | CUST-WP-0015-T04 | done | — | workplans/CUST-WP-0015-agent-inbox.md |
| task | CUST-WP-0015-T05 | done | — | workplans/CUST-WP-0015-agent-inbox.md |
| task | CUST-WP-0016-T01 | done | — | workplans/CUST-WP-0016-kaizen-agentic-integration.md |
| task | CUST-WP-0016-T02 | done | — | workplans/CUST-WP-0016-kaizen-agentic-integration.md |
| task | CUST-WP-0016-T03 | done | — | workplans/CUST-WP-0016-kaizen-agentic-integration.md |
| task | CUST-WP-0016-T04 | done | — | workplans/CUST-WP-0016-kaizen-agentic-integration.md |
| task | CUST-WP-0016-T05 | done | — | workplans/CUST-WP-0016-kaizen-agentic-integration.md |
| task | CUST-WP-0017-T01 | done | — | workplans/CUST-WP-0017-scope-md-agent-and-coverage.md |
| task | CUST-WP-0017-T02 | done | — | workplans/CUST-WP-0017-scope-md-agent-and-coverage.md |
| task | CUST-WP-0017-T03 | done | — | workplans/CUST-WP-0017-scope-md-agent-and-coverage.md |
| task | CUST-WP-0017-T04 | done | — | workplans/CUST-WP-0017-scope-md-agent-and-coverage.md |
| task | CUST-WP-0017-T05 | done | — | workplans/CUST-WP-0017-scope-md-agent-and-coverage.md |
| task | CUST-WP-0017-T06 | done | — | workplans/CUST-WP-0017-scope-md-agent-and-coverage.md |
| task | CUST-WP-0018-T01 | done | — | workplans/CUST-WP-0018-api-hardening-code-quality.md |
| task | CUST-WP-0018-T02 | done | — | workplans/CUST-WP-0018-api-hardening-code-quality.md |
| task | CUST-WP-0018-T03 | done | — | workplans/CUST-WP-0018-api-hardening-code-quality.md |
| task | CUST-WP-0018-T04 | done | — | workplans/CUST-WP-0018-api-hardening-code-quality.md |
| task | CUST-WP-0018-T05 | done | — | workplans/CUST-WP-0018-api-hardening-code-quality.md |
| task | CUST-WP-0018-T06 | done | — | workplans/CUST-WP-0018-api-hardening-code-quality.md |
| task | CUST-WP-0018-T07 | done | — | workplans/CUST-WP-0018-api-hardening-code-quality.md |
| task | CUST-WP-0019-T01 | done | — | workplans/CUST-WP-0019-dashboard-ux-polish.md |
| task | CUST-WP-0019-T02 | done | — | workplans/CUST-WP-0019-dashboard-ux-polish.md |
| task | CUST-WP-0019-T03 | done | — | workplans/CUST-WP-0019-dashboard-ux-polish.md |
| task | CUST-WP-0020-T01 | done | — | workplans/CUST-WP-0020-test-suite.md |
| task | CUST-WP-0020-T02 | done | — | workplans/CUST-WP-0020-test-suite.md |
| task | CUST-WP-0020-T03 | done | — | workplans/CUST-WP-0020-test-suite.md |
| task | CUST-WP-0020-T04 | done | — | workplans/CUST-WP-0020-test-suite.md |
| task | CUST-WP-0020-T05 | done | — | workplans/CUST-WP-0020-test-suite.md |
| task | CUST-WP-0021-T01 | done | — | workplans/CUST-WP-0021-multi-host-repo-paths.md |
| task | CUST-WP-0021-T02 | done | — | workplans/CUST-WP-0021-multi-host-repo-paths.md |
| task | CUST-WP-0021-T03 | done | — | workplans/CUST-WP-0021-multi-host-repo-paths.md |
| task | CUST-WP-0021-T04 | done | — | workplans/CUST-WP-0021-multi-host-repo-paths.md |
| task | CUST-WP-0021-T05 | done | — | workplans/CUST-WP-0021-multi-host-repo-paths.md |
| task | CUST-WP-0022-T01 | done | — | workplans/CUST-WP-0022-capability-requests.md |
| task | CUST-WP-0022-T02 | done | — | workplans/CUST-WP-0022-capability-requests.md |
| task | CUST-WP-0022-T03 | done | — | workplans/CUST-WP-0022-capability-requests.md |
| task | CUST-WP-0022-T04 | done | — | workplans/CUST-WP-0022-capability-requests.md |
| task | CUST-WP-0022-T05 | done | — | workplans/CUST-WP-0022-capability-requests.md |
| task | CUST-WP-0022-T06 | done | — | workplans/CUST-WP-0022-capability-requests.md |
| task | CUST-WP-0022-T07 | done | — | workplans/CUST-WP-0022-capability-requests.md |
| task | CUST-WP-0022-T08 | done | — | workplans/CUST-WP-0022-capability-requests.md |
| task | CUST-WP-0022-T09 | done | — | workplans/CUST-WP-0022-capability-requests.md |
| task | CUST-WP-0022-T10 | done | — | workplans/CUST-WP-0022-capability-requests.md |
| task | CUST-WP-0023-T01 | done | — | workplans/CUST-WP-0023-tpsc.md |
| task | CUST-WP-0023-T02 | done | — | workplans/CUST-WP-0023-tpsc.md |
| task | CUST-WP-0023-T03 | done | — | workplans/CUST-WP-0023-tpsc.md |
| task | CUST-WP-0023-T04 | done | — | workplans/CUST-WP-0023-tpsc.md |
| task | CUST-WP-0023-T05 | done | — | workplans/CUST-WP-0023-tpsc.md |
| task | CUST-WP-0023-T06 | done | — | workplans/CUST-WP-0023-tpsc.md |
| task | CUST-WP-0023-T07 | done | — | workplans/CUST-WP-0023-tpsc.md |
| task | CUST-WP-0023-T08 | done | — | workplans/CUST-WP-0023-tpsc.md |
| task | CUST-WP-0023-T09 | done | — | workplans/CUST-WP-0023-tpsc.md |
| task | CUST-WP-0023-T10 | done | — | workplans/CUST-WP-0023-tpsc.md |
| task | CUST-WP-0024-T01 | done | — | workplans/CUST-WP-0024-repo-doi-gate.md |
| task | CUST-WP-0024-T02 | done | — | workplans/CUST-WP-0024-repo-doi-gate.md |
| task | CUST-WP-0024-T03 | done | — | workplans/CUST-WP-0024-repo-doi-gate.md |
| task | CUST-WP-0024-T04 | done | — | workplans/CUST-WP-0024-repo-doi-gate.md |
| task | CUST-WP-0024-T05 | done | — | workplans/CUST-WP-0024-repo-doi-gate.md |
| task | CUST-WP-0026-T01 | done | — | workplans/CUST-WP-0026-distributed-consistency.md |
| task | CUST-WP-0026-T02 | done | — | workplans/CUST-WP-0026-distributed-consistency.md |
| task | CUST-WP-0026-T03 | done | — | workplans/CUST-WP-0026-distributed-consistency.md |
| task | CUST-WP-0026-T04 | done | — | workplans/CUST-WP-0026-distributed-consistency.md |
| task | CUST-WP-0026-T05 | done | — | workplans/CUST-WP-0026-distributed-consistency.md |
| task | CUST-WP-0027-T01 | done | — | workplans/CUST-WP-0027-capability-request-dispute.md |
| task | CUST-WP-0027-T02 | done | — | workplans/CUST-WP-0027-capability-request-dispute.md |
| task | CUST-WP-0027-T03 | done | — | workplans/CUST-WP-0027-capability-request-dispute.md |
| task | CUST-WP-0027-T04 | done | — | workplans/CUST-WP-0027-capability-request-dispute.md |
| task | CUST-WP-0027-T05 | done | — | workplans/CUST-WP-0027-capability-request-dispute.md |
| task | CUST-WP-0029-T01 | done | — | workplans/CUST-WP-0029-token-consumption-tracking.md |
| task | CUST-WP-0029-T02 | done | — | workplans/CUST-WP-0029-token-consumption-tracking.md |
| task | CUST-WP-0029-T03 | done | — | workplans/CUST-WP-0029-token-consumption-tracking.md |
| task | CUST-WP-0029-T04 | done | — | workplans/CUST-WP-0029-token-consumption-tracking.md |
| task | CUST-WP-0029-T05 | done | — | workplans/CUST-WP-0029-token-consumption-tracking.md |
| task | CUST-WP-0029-T06 | done | — | workplans/CUST-WP-0029-token-consumption-tracking.md |
| task | CUST-WP-0029-T07 | done | — | workplans/CUST-WP-0029-token-consumption-tracking.md |
| task | CUST-WP-0029-T08 | done | — | workplans/CUST-WP-0029-token-consumption-tracking.md |
| task | T01 | done | — | workplans/CUST-WP-0030-dashboard-entity-list-ux.md |
| task | T02 | done | — | workplans/CUST-WP-0030-dashboard-entity-list-ux.md |
| task | T03 | done | — | workplans/CUST-WP-0030-dashboard-entity-list-ux.md |
| task | T04 | done | — | workplans/CUST-WP-0030-dashboard-entity-list-ux.md |
| task | T05 | done | — | workplans/CUST-WP-0030-dashboard-entity-list-ux.md |
| task | T06 | done | — | workplans/CUST-WP-0030-dashboard-entity-list-ux.md |
| task | T07 | done | — | workplans/CUST-WP-0030-dashboard-entity-list-ux.md |
| task | T08 | done | — | workplans/CUST-WP-0030-dashboard-entity-list-ux.md |
| task | T09 | done | — | workplans/CUST-WP-0030-dashboard-entity-list-ux.md |
| task | T01 | done | — | workplans/CUST-WP-0031-capability-registry.md |
| task | T02 | done | — | workplans/CUST-WP-0031-capability-registry.md |
| task | T03 | done | — | workplans/CUST-WP-0031-capability-registry.md |
| task | T04 | done | — | workplans/CUST-WP-0031-capability-registry.md |
| task | T05 | done | — | workplans/CUST-WP-0031-capability-registry.md |
| task | T06 | done | — | workplans/CUST-WP-0031-capability-registry.md |
| task | T07 | done | — | workplans/CUST-WP-0031-capability-registry.md |
| task | CUST-WP-0032-T01 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md |
| task | CUST-WP-0032-T02 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md |
| task | CUST-WP-0032-T03 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md |
| task | CUST-WP-0032-T04 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md |
| task | CUST-WP-0032-T05 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md |
| task | CUST-WP-0032-T06 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md |
| task | CUST-WP-0032-T07 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md |
| task | CUST-WP-0032-T08 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md |
| task | CUST-WP-0032-T09 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md |
| task | CUST-WP-0032-T10 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md |
| task | CUST-WP-0032-T11 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md |
| task | CUST-WP-0032-T12 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md |
| task | CUST-WP-0032-T13 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md |
| task | CUST-WP-0032-T14 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md |
| task | CUST-WP-0032-T15 | done | — | workplans/CUST-WP-0032-haskell-build-machine.md |
| task | CUST-WP-0033-T01 | done | — | workplans/CUST-WP-0033-interface-change-registry.md |
| task | CUST-WP-0033-T02 | done | — | workplans/CUST-WP-0033-interface-change-registry.md |
| task | CUST-WP-0033-T03 | done | — | workplans/CUST-WP-0033-interface-change-registry.md |
| task | CUST-WP-0033-T04 | done | — | workplans/CUST-WP-0033-interface-change-registry.md |
| task | CUST-WP-0033-T05 | done | — | workplans/CUST-WP-0033-interface-change-registry.md |
| task | CUST-WP-0033-T06 | done | — | workplans/CUST-WP-0033-interface-change-registry.md |
| task | CUST-WP-0039-T1 | done | — | workplans/CUST-WP-0039-dashboard-poll-optimization.md |
| task | CUST-WP-0039-T2 | done | — | workplans/CUST-WP-0039-dashboard-poll-optimization.md |
| task | CUST-WP-0039-T3 | done | — | workplans/CUST-WP-0039-dashboard-poll-optimization.md |
| task | CUST-WP-0039-T4 | done | — | workplans/CUST-WP-0039-dashboard-poll-optimization.md |
| task | CUST-WP-0039-T5 | done | — | workplans/CUST-WP-0039-dashboard-poll-optimization.md |
| task | CUST-WP-0039-T6 | done | — | workplans/CUST-WP-0039-dashboard-poll-optimization.md |
| task | T01 | done | — | workplans/CUST-WP-0040-state-hub-nats-activity-core-integration.md |
| task | T02 | done | — | workplans/CUST-WP-0040-state-hub-nats-activity-core-integration.md |
| task | T03 | done | — | workplans/CUST-WP-0040-state-hub-nats-activity-core-integration.md |
| task | T04 | done | — | workplans/CUST-WP-0040-state-hub-nats-activity-core-integration.md |
| task | T05 | done | — | workplans/CUST-WP-0040-state-hub-nats-activity-core-integration.md |
| task | T06 | done | — | workplans/CUST-WP-0040-state-hub-nats-activity-core-integration.md |
| task | CUST-WP-0041-T1 | done | — | workplans/CUST-WP-0041-api-performance.md |
| task | CUST-WP-0041-T2 | done | — | workplans/CUST-WP-0041-api-performance.md |
| task | CUST-WP-0041-T3 | done | — | workplans/CUST-WP-0041-api-performance.md |
| task | CUST-WP-0041-T4 | done | — | workplans/CUST-WP-0041-api-performance.md |
| task | CUST-WP-0043-T01 | done | — | workplans/CUST-WP-0043-state-hub-repo-extraction.md |
| task | CUST-WP-0043-T02 | done | — | workplans/CUST-WP-0043-state-hub-repo-extraction.md |
| task | CUST-WP-0043-T03 | done | — | workplans/CUST-WP-0043-state-hub-repo-extraction.md |
| task | CUST-WP-0043-T04 | done | — | workplans/CUST-WP-0043-state-hub-repo-extraction.md |
| task | CUST-WP-0043-T05 | done | — | workplans/CUST-WP-0043-state-hub-repo-extraction.md |
| task | CUST-WP-0043-T06 | done | — | workplans/CUST-WP-0043-state-hub-repo-extraction.md |
| task | CUST-WP-0043-T07 | done | — | workplans/CUST-WP-0043-state-hub-repo-extraction.md |
| task | CUST-WP-0043-T08 | done | — | workplans/CUST-WP-0043-state-hub-repo-extraction.md |
| task | CUST-WP-0043-T09 | done | — | workplans/CUST-WP-0043-state-hub-repo-extraction.md |
| task | CUST-WP-0044-T01 | done | — | workplans/CUST-WP-0044-daily-statehub-wsjf-triage.md |
| task | CUST-WP-0044-T02 | done | — | workplans/CUST-WP-0044-daily-statehub-wsjf-triage.md |
| task | CUST-WP-0044-T03 | done | — | workplans/CUST-WP-0044-daily-statehub-wsjf-triage.md |
| task | CUST-WP-0044-T04 | done | — | workplans/CUST-WP-0044-daily-statehub-wsjf-triage.md |
| task | CUST-WP-0044-T05 | done | — | workplans/CUST-WP-0044-daily-statehub-wsjf-triage.md |
| task | CUST-WP-0044-T06 | done | — | workplans/CUST-WP-0044-daily-statehub-wsjf-triage.md |
| task | CUST-WP-0045-T01 | done | — | workplans/CUST-WP-0045-activity-core-daily-triage-runner.md |
| task | CUST-WP-0045-T02 | done | — | workplans/CUST-WP-0045-activity-core-daily-triage-runner.md |
| task | CUST-WP-0045-T03 | done | — | workplans/CUST-WP-0045-activity-core-daily-triage-runner.md |
| task | CUST-WP-0045-T04 | done | — | workplans/CUST-WP-0045-activity-core-daily-triage-runner.md |
| task | CUST-WP-0045-T05 | done | — | workplans/CUST-WP-0045-activity-core-daily-triage-runner.md |
| task | CUST-WP-0045-T06 | done | — | workplans/CUST-WP-0045-activity-core-daily-triage-runner.md |
| task | CUST-WP-0045-T07 | done | — | workplans/CUST-WP-0045-activity-core-daily-triage-runner.md |
| task | CUST-WP-0045-T08 | done | — | workplans/CUST-WP-0045-activity-core-daily-triage-runner.md |
| task | CUST-WP-0046-T01 | done | — | workplans/CUST-WP-0046-hourly-recently-on-scope-activity-core.md |
| task | CUST-WP-0046-T02 | done | — | workplans/CUST-WP-0046-hourly-recently-on-scope-activity-core.md |
| task | CUST-WP-0046-T03 | done | — | workplans/CUST-WP-0046-hourly-recently-on-scope-activity-core.md |
| task | CUST-WP-0046-T04 | done | — | workplans/CUST-WP-0046-hourly-recently-on-scope-activity-core.md |
| task | CUST-WP-0046-T05 | done | — | workplans/CUST-WP-0046-hourly-recently-on-scope-activity-core.md |
| task | CUST-WP-0046-T06 | done | — | workplans/CUST-WP-0046-hourly-recently-on-scope-activity-core.md |
| task | CUST-WP-0046-T07 | done | — | workplans/CUST-WP-0046-hourly-recently-on-scope-activity-core.md |
| task | CUST-WP-0047-T01 | done | — | workplans/CUST-WP-0047-ops-hub-service-inventory-now-view.md |
| task | CUST-WP-0047-T02 | done | — | workplans/CUST-WP-0047-ops-hub-service-inventory-now-view.md |
| task | CUST-WP-0047-T03 | done | — | workplans/CUST-WP-0047-ops-hub-service-inventory-now-view.md |
| task | CUST-WP-0047-T04 | done | — | workplans/CUST-WP-0047-ops-hub-service-inventory-now-view.md |
| task | CUST-WP-0047-T05 | cancel | — | workplans/CUST-WP-0047-ops-hub-service-inventory-now-view.md |
| task | CUST-WP-0047-T06 | done | — | workplans/CUST-WP-0047-ops-hub-service-inventory-now-view.md |
| task | CUST-WP-0047-T07 | done | — | workplans/CUST-WP-0047-ops-hub-service-inventory-now-view.md |
| task | CUST-WP-0048-T01 | done | — | workplans/CUST-WP-0048-hub-core-state-hub-import-refactor.md |
| task | CUST-WP-0048-T02 | done | — | workplans/CUST-WP-0048-hub-core-state-hub-import-refactor.md |
| task | CUST-WP-0048-T03 | done | — | workplans/CUST-WP-0048-hub-core-state-hub-import-refactor.md |
| task | CUST-WP-0048-T04 | done | — | workplans/CUST-WP-0048-hub-core-state-hub-import-refactor.md |
| task | CUST-WP-0048-T05 | done | — | workplans/CUST-WP-0048-hub-core-state-hub-import-refactor.md |
| task | CUST-WP-0048-T06 | done | — | workplans/CUST-WP-0048-hub-core-state-hub-import-refactor.md |
| task | CUST-WP-0048-T07 | done | — | workplans/CUST-WP-0048-hub-core-state-hub-import-refactor.md |
| task | CUST-WP-0048-T08 | done | — | workplans/CUST-WP-0048-hub-core-state-hub-import-refactor.md |
| task | CUST-WP-0049-T01 | done | — | workplans/CUST-WP-0049-interhub-bootstrap-access-lane.md |
| task | CUST-WP-0049-T02 | done | — | workplans/CUST-WP-0049-interhub-bootstrap-access-lane.md |
| task | CUST-WP-0049-T03 | done | — | workplans/CUST-WP-0049-interhub-bootstrap-access-lane.md |
| task | CUST-WP-0049-T04 | done | — | workplans/CUST-WP-0049-interhub-bootstrap-access-lane.md |
| task | CUST-WP-0049-T05 | done | — | workplans/CUST-WP-0049-interhub-bootstrap-access-lane.md |
| task | CUST-WP-0049-T06 | cancel | — | workplans/CUST-WP-0049-interhub-bootstrap-access-lane.md |
| task | CUST-WP-0050-T01 | done | — | workplans/CUST-WP-0050-repo-classification-registration-redesign.md |
| task | CUST-WP-0050-T02 | done | — | workplans/CUST-WP-0050-repo-classification-registration-redesign.md |
| task | CUST-WP-0050-T03 | cancel | — | workplans/CUST-WP-0050-repo-classification-registration-redesign.md |
| task | CUST-WP-0050-T04 | cancel | — | workplans/CUST-WP-0050-repo-classification-registration-redesign.md |
| task | CUST-WP-0050-T05 | cancel | — | workplans/CUST-WP-0050-repo-classification-registration-redesign.md |
| task | CUST-WP-0050-T06 | cancel | — | workplans/CUST-WP-0050-repo-classification-registration-redesign.md |
| task | CUST-WP-0050-T07 | cancel | — | workplans/CUST-WP-0050-repo-classification-registration-redesign.md |
| task | CUST-WP-0050-T08 | cancel | — | workplans/CUST-WP-0050-repo-classification-registration-redesign.md |
| task | CUST-WP-0050-T09 | cancel | — | workplans/CUST-WP-0050-repo-classification-registration-redesign.md |
| task | CUST-WP-0050-T10 | cancel | — | workplans/CUST-WP-0050-repo-classification-registration-redesign.md |
| task | CUST-WP-0050-T11 | done | — | workplans/CUST-WP-0050-repo-classification-registration-redesign.md |
| task | CUST-WP-0051-T01 | done | — | workplans/CUST-WP-0051-infrastructure-stabilization-metaplan.md |
| task | CUST-WP-0051-T02 | done | — | workplans/CUST-WP-0051-infrastructure-stabilization-metaplan.md |
| task | CUST-WP-0051-T03 | done | — | workplans/CUST-WP-0051-infrastructure-stabilization-metaplan.md |
| task | CUST-WP-0051-T04 | done | — | workplans/CUST-WP-0051-infrastructure-stabilization-metaplan.md |
| task | CUST-WP-0051-T05 | done | — | workplans/CUST-WP-0051-infrastructure-stabilization-metaplan.md |
| task | CUST-WP-0051-T06 | done | — | workplans/CUST-WP-0051-infrastructure-stabilization-metaplan.md |
| task | CUST-WP-0051-T07 | done | — | workplans/CUST-WP-0051-infrastructure-stabilization-metaplan.md |
| task | CUST-WP-0051-T08 | done | — | workplans/CUST-WP-0051-infrastructure-stabilization-metaplan.md |
| task | CUST-WP-0052-T01 | done | — | workplans/CUST-WP-0052-core-hub-fos-reset.md |
| task | CUST-WP-0052-T02 | done | — | workplans/CUST-WP-0052-core-hub-fos-reset.md |
| task | CUST-WP-0052-T03 | done | — | workplans/CUST-WP-0052-core-hub-fos-reset.md |
| task | CUST-WP-0052-T04 | done | — | workplans/CUST-WP-0052-core-hub-fos-reset.md |
| task | CUST-WP-0052-T05 | done | — | workplans/CUST-WP-0052-core-hub-fos-reset.md |
| task | CUST-WP-0052-T06 | done | — | workplans/CUST-WP-0052-core-hub-fos-reset.md |
| task | CUST-WP-0052-T07 | done | — | workplans/CUST-WP-0052-core-hub-fos-reset.md |
| task | CUST-WP-0053-T01 | done | — | workplans/CUST-WP-0053-coordination-hygiene-improvements.md |
| task | CUST-WP-0053-T02 | done | — | workplans/CUST-WP-0053-coordination-hygiene-improvements.md |
| task | CUST-WP-0053-T03 | done | — | workplans/CUST-WP-0053-coordination-hygiene-improvements.md |
| task | CUST-WP-0053-T04 | done | — | workplans/CUST-WP-0053-coordination-hygiene-improvements.md |
| task | CUST-WP-0053-T05 | done | — | workplans/CUST-WP-0053-coordination-hygiene-improvements.md |
| task | CUST-WP-0055-T01 | done | — | workplans/CUST-WP-0055-workplan-terminology-fleet-refactor.md |
| task | CUST-WP-0055-T02 | done | — | workplans/CUST-WP-0055-workplan-terminology-fleet-refactor.md |
| task | CUST-WP-0055-T03 | done | — | workplans/CUST-WP-0055-workplan-terminology-fleet-refactor.md |
| task | CUST-WP-0055-T04 | done | — | workplans/CUST-WP-0055-workplan-terminology-fleet-refactor.md |
| task | CUST-WP-0055-T05 | done | — | workplans/CUST-WP-0055-workplan-terminology-fleet-refactor.md |
| task | CUST-WP-0055-T06 | done | — | workplans/CUST-WP-0055-workplan-terminology-fleet-refactor.md |
| task | CUST-WP-0055-T07 | done | — | workplans/CUST-WP-0055-workplan-terminology-fleet-refactor.md |
| task | CUST-WP-0055-T08 | done | — | workplans/CUST-WP-0055-workplan-terminology-fleet-refactor.md |
| task | CUST-WP-0056-T01 | done | — | workplans/CUST-WP-0056-daily-todo-md-stale-review.md |
| task | CUST-WP-0056-T02 | done | — | workplans/CUST-WP-0056-daily-todo-md-stale-review.md |
| task | CUST-WP-0056-T03 | done | — | workplans/CUST-WP-0056-daily-todo-md-stale-review.md |
| task | CUST-WP-0059-T01 | done | — | workplans/CUST-WP-0059-forgejo-ci-publish-cutover-tail.md |
| task | CUST-WP-0059-T02 | done | — | workplans/CUST-WP-0059-forgejo-ci-publish-cutover-tail.md |
| task | CUST-WP-0059-T03 | done | — | workplans/CUST-WP-0059-forgejo-ci-publish-cutover-tail.md |
| task | CUST-WP-0059-T04 | done | — | workplans/CUST-WP-0059-forgejo-ci-publish-cutover-tail.md |
| task | CUST-WP-0059-T05 | done | — | workplans/CUST-WP-0059-forgejo-ci-publish-cutover-tail.md |
| task | CUST-WP-0059-T06 | done | — | workplans/CUST-WP-0059-forgejo-ci-publish-cutover-tail.md |
| task | CUST-WP-0060-T01 | done | — | workplans/CUST-WP-0060-work-record-canonization.md |
| task | CUST-WP-0060-T02 | done | — | workplans/CUST-WP-0060-work-record-canonization.md |
| task | CUST-WP-0060-T03 | done | — | workplans/CUST-WP-0060-work-record-canonization.md |
| task | CUST-WP-0060-T04 | done | — | workplans/CUST-WP-0060-work-record-canonization.md |
| task | CUST-WP-0060-T05 | done | — | workplans/CUST-WP-0060-work-record-canonization.md |
| task | CUST-WP-0060-T06 | done | — | workplans/CUST-WP-0060-work-record-canonization.md |
| task | CUST-WP-0060-T07 | done | — | workplans/CUST-WP-0060-work-record-canonization.md |
| task | CUST-WP-0061-T01 | done | — | workplans/CUST-WP-0061-work-record-stage3-intake-promotion-index.md |
| task | CUST-WP-0061-T02 | done | — | workplans/CUST-WP-0061-work-record-stage3-intake-promotion-index.md |
| task | CUST-WP-0061-T03 | done | — | workplans/CUST-WP-0061-work-record-stage3-intake-promotion-index.md |
| task | CUST-WP-0061-T04 | done | — | workplans/CUST-WP-0061-work-record-stage3-intake-promotion-index.md |
| task | CUST-WP-0061-T05 | done | — | workplans/CUST-WP-0061-work-record-stage3-intake-promotion-index.md |
| task | CUST-WP-0061-T06 | done | — | workplans/CUST-WP-0061-work-record-stage3-intake-promotion-index.md |
| task | CUST-WP-0062-T01 | done | — | workplans/CUST-WP-0062-sbom-nexus-daily-catchup.md |
| task | CUST-WP-0062-T02 | done | — | workplans/CUST-WP-0062-sbom-nexus-daily-catchup.md |
| task | CUST-WP-0062-T03 | done | — | workplans/CUST-WP-0062-sbom-nexus-daily-catchup.md |
| task | CUST-WP-0062-T04 | done | — | workplans/CUST-WP-0062-sbom-nexus-daily-catchup.md |
| task | CUST-WP-0062-T05 | done | — | workplans/CUST-WP-0062-sbom-nexus-daily-catchup.md |
| task | CUST-WP-0062-T06 | done | — | workplans/CUST-WP-0062-sbom-nexus-daily-catchup.md |
| task | CUST-WP-0063-T01 | done | — | workplans/CUST-WP-0063-inbox-governance-packets.md |
| task | CUST-WP-0063-T02 | done | — | workplans/CUST-WP-0063-inbox-governance-packets.md |
| task | CUST-WP-0063-T03 | done | — | workplans/CUST-WP-0063-inbox-governance-packets.md |
| task | CUST-WP-0063-T04 | done | — | workplans/CUST-WP-0063-inbox-governance-packets.md |
| task | CUST-WP-0063-T05 | done | — | workplans/CUST-WP-0063-inbox-governance-packets.md |
| task | CUST-WP-0064-T01 | done | — | workplans/CUST-WP-0064-sbom-controlled-scan-inputs.md |
| task | CUST-WP-0064-T02 | done | — | workplans/CUST-WP-0064-sbom-controlled-scan-inputs.md |
| task | CUST-WP-0064-T03 | done | — | workplans/CUST-WP-0064-sbom-controlled-scan-inputs.md |
| task | CUST-WP-0064-T04 | done | — | workplans/CUST-WP-0064-sbom-controlled-scan-inputs.md |
| task | CUST-WP-0065-T01 | done | — | workplans/CUST-WP-0065-reclassify-repos-and-guidance-docs-to-the-new-sector-domain.md |
| task | CUST-WP-0065-T02 | done | — | workplans/CUST-WP-0065-reclassify-repos-and-guidance-docs-to-the-new-sector-domain.md |
| task | CUST-WP-0065-T03 | done | — | workplans/CUST-WP-0065-reclassify-repos-and-guidance-docs-to-the-new-sector-domain.md |
| task | CUST-WP-0065-T04 | done | — | workplans/CUST-WP-0065-reclassify-repos-and-guidance-docs-to-the-new-sector-domain.md |
| task | CUST-WP-0066-T01 | done | — | workplans/CUST-WP-0066-repository-qualified-ad-hoc-identifiers.md |
| task | CUST-WP-0066-T02 | done | — | workplans/CUST-WP-0066-repository-qualified-ad-hoc-identifiers.md |
| task | CUST-WP-0066-T03 | done | — | workplans/CUST-WP-0066-repository-qualified-ad-hoc-identifiers.md |
| task | CUST-WP-0067-T01 | done | — | workplans/CUST-WP-0067-hub-authority-target-resolution.md |
| task | CUST-WP-0067-T02 | done | — | workplans/CUST-WP-0067-hub-authority-target-resolution.md |
| task | CUST-WP-0067-T03 | done | — | workplans/CUST-WP-0067-hub-authority-target-resolution.md |
| task | CUST-WP-0067-T04 | done | — | workplans/CUST-WP-0067-hub-authority-target-resolution.md |
| task | CUST-WP-0067-T05 | done | — | workplans/CUST-WP-0067-hub-authority-target-resolution.md |
| task | CUST-WP-0067-T06 | done | — | workplans/CUST-WP-0067-hub-authority-target-resolution.md |
| task | CUST-WP-0067-T07 | done | — | workplans/CUST-WP-0067-hub-authority-target-resolution.md |
| task | CUST-WP-0067-T08 | done | — | workplans/CUST-WP-0067-hub-authority-target-resolution.md |
| task | CUST-WP-0067-T09 | done | — | workplans/CUST-WP-0067-hub-authority-target-resolution.md |
| task | CUST-WP-0067-T10 | done | — | workplans/CUST-WP-0067-hub-authority-target-resolution.md |
| task | CUST-WP-0068-T01 | done | — | workplans/CUST-WP-0068-cache-only-work-record-recovery.md |
| task | CUST-WP-0068-T02 | done | — | workplans/CUST-WP-0068-cache-only-work-record-recovery.md |
| task | CUST-WP-0068-T03 | done | — | workplans/CUST-WP-0068-cache-only-work-record-recovery.md |
| task | CUST-WP-0068-T04 | done | — | workplans/CUST-WP-0068-cache-only-work-record-recovery.md |
| task | CUST-WP-0068-T05 | done | — | workplans/CUST-WP-0068-cache-only-work-record-recovery.md |
| task | CUST-WP-0068-T06 | done | — | workplans/CUST-WP-0068-cache-only-work-record-recovery.md |
| task | CUST-WP-0068-T07 | done | — | workplans/CUST-WP-0068-cache-only-work-record-recovery.md |
| task | CUST-WP-0068-T08 | done | — | workplans/CUST-WP-0068-cache-only-work-record-recovery.md |
| task | CUST-WP-0068-T09 | done | — | workplans/CUST-WP-0068-cache-only-work-record-recovery.md |
| task | CUST-WP-0070-T01 | done | — | workplans/CUST-WP-0070-publication-repo-category.md |
| task | CUST-WP-0070-T02 | done | — | workplans/CUST-WP-0070-publication-repo-category.md |
| task | CUST-WP-0070-T03 | done | — | workplans/CUST-WP-0070-publication-repo-category.md |
| task | CUST-WP-0071-T01 | done | — | workplans/CUST-WP-0071-measured-workload-sizing-and-weekly-review.md |
| task | CUST-WP-0071-T02 | wait | — | workplans/CUST-WP-0071-measured-workload-sizing-and-weekly-review.md |
| task | CUST-WP-0071-T03 | wait | — | workplans/CUST-WP-0071-measured-workload-sizing-and-weekly-review.md |
| task | CUST-WP-0071-T04 | wait | — | workplans/CUST-WP-0071-measured-workload-sizing-and-weekly-review.md |
| task | CUST-WP-0071-T05 | wait | — | workplans/CUST-WP-0071-measured-workload-sizing-and-weekly-review.md |
| task | CUST-WP-0072-T01 | done | — | workplans/CUST-WP-0072-fleet-flavor-and-depends-on-backfill.md |
| task | CUST-WP-0072-T02 | done | — | workplans/CUST-WP-0072-fleet-flavor-and-depends-on-backfill.md |
| task | CUST-WP-0072-T03 | done | — | workplans/CUST-WP-0072-fleet-flavor-and-depends-on-backfill.md |
| task | CUST-WP-0072-T04 | done | — | workplans/CUST-WP-0072-fleet-flavor-and-depends-on-backfill.md |
| task | CUST-WP-0073-T01 | done | — | workplans/CUST-WP-0073-agent-credential-separation.md |
| task | CUST-WP-0073-T02 | wait | — | workplans/CUST-WP-0073-agent-credential-separation.md |
| task | CUST-WP-0073-T03 | done | — | workplans/CUST-WP-0073-agent-credential-separation.md |
| task | CUST-WP-0073-T04 | wait | — | workplans/CUST-WP-0073-agent-credential-separation.md |
| task | CUST-WP-0073-T05 | wait | — | workplans/CUST-WP-0073-agent-credential-separation.md |
| task | CUST-WP-0074-T01 | done | — | workplans/CUST-WP-0074-qualified-wait-states.md |
| task | CUST-WP-0074-T02 | done | — | workplans/CUST-WP-0074-qualified-wait-states.md |
| task | CUST-WP-0074-T03 | done | — | workplans/CUST-WP-0074-qualified-wait-states.md |
| task | CUST-WP-0074-T04 | done | — | workplans/CUST-WP-0074-qualified-wait-states.md |
| task | CUST-WP-0074-T05 | done | — | workplans/CUST-WP-0074-qualified-wait-states.md |
| task | CUST-WP-0074-T06 | done | — | workplans/CUST-WP-0074-qualified-wait-states.md |
| task | CUST-WP-0075-T01 | done | — | workplans/CUST-WP-0075-dependency-graph-driven-unblocking.md |
| task | CUST-WP-0075-T02 | done | — | workplans/CUST-WP-0075-dependency-graph-driven-unblocking.md |
| task | CUST-WP-0075-T03 | done | — | workplans/CUST-WP-0075-dependency-graph-driven-unblocking.md |
| task | CUST-WP-0075-T04 | done | — | workplans/CUST-WP-0075-dependency-graph-driven-unblocking.md |
| task | CUST-WP-0075-T05 | done | — | workplans/CUST-WP-0075-dependency-graph-driven-unblocking.md |
| task | CUST-WP-0075-T06 | done | — | workplans/CUST-WP-0075-dependency-graph-driven-unblocking.md |
| task | CUST-WP-0076-T01 | done | — | workplans/CUST-WP-0076-provisioning-window-cadence.md |
| task | CUST-WP-0077-T01 | done | — | workplans/CUST-WP-0077-dependency-root-execution-wave.md |
| task | CUST-WP-0077-T02 | done | — | workplans/CUST-WP-0077-dependency-root-execution-wave.md |
| task | CUST-WP-0077-T03 | done | — | workplans/CUST-WP-0077-dependency-root-execution-wave.md |
| task | CUST-WP-0077-T04 | done | — | workplans/CUST-WP-0077-dependency-root-execution-wave.md |
| task | CUST-WP-0077-T05 | done | — | workplans/CUST-WP-0077-dependency-root-execution-wave.md |
| task | CUST-WP-0078-T01 | done | — | workplans/CUST-WP-0078-security-collaboration-consolidation.md |
| task | CUST-WP-0078-T02 | done | — | workplans/CUST-WP-0078-security-collaboration-consolidation.md |
| task | CUST-WP-0078-T03 | done | — | workplans/CUST-WP-0078-security-collaboration-consolidation.md |
| task | CUST-WP-0078-T04 | done | — | workplans/CUST-WP-0078-security-collaboration-consolidation.md |
| task | CUST-WP-0079-T01 | done | — | workplans/CUST-WP-0079-deterministic-security-flow.md |
| task | CUST-WP-0079-T02 | done | — | workplans/CUST-WP-0079-deterministic-security-flow.md |
| task | CUST-WP-0079-T03 | wait | — | workplans/CUST-WP-0079-deterministic-security-flow.md |
| task | CUST-WP-0079-T04 | wait | — | workplans/CUST-WP-0079-deterministic-security-flow.md |
| task | CUST-WP-0079-T05 | wait | — | workplans/CUST-WP-0079-deterministic-security-flow.md |
| task | CUST-WP-0079-T06 | wait | — | workplans/CUST-WP-0079-deterministic-security-flow.md |
| task | CUST-WP-0079-T07 | done | — | workplans/CUST-WP-0079-deterministic-security-flow.md |
| task | CUST-WP-0080-T01 | done | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T02 | done | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T03 | progress | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T04 | todo | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T05 | progress | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0080-T06 | progress | — | workplans/CUST-WP-0080-netkingdom-security-blueprint-and-gap-closure.md |
| task | CUST-WP-0081-T01 | done | — | workplans/CUST-WP-0081-netkingdom-architecture-review-and-reuse.md |
| task | CUST-WP-0081-T02 | done | — | workplans/CUST-WP-0081-netkingdom-architecture-review-and-reuse.md |
| task | CUST-WP-0081-T03 | done | — | workplans/CUST-WP-0081-netkingdom-architecture-review-and-reuse.md |
| task | THE-WP-0001-T01 | done | — | workplans/THE-WP-0001-federation-interface.md |
| task | THE-WP-0001-T02 | done | — | workplans/THE-WP-0001-federation-interface.md |
| intake | CUST-IN-0001 | closed | green | intake-legacy-suggestions-migration.md |
| intake | CUST-IN-0002 | closed | green | intake-legacy-suggestions-migration.md |
| intake | CUST-IN-0003 | closed | green | intake-legacy-suggestions-migration.md |
| intake | CUST-IN-0004 | routed | green | intake-legacy-suggestions-migration.md |
| intake | CUST-IN-0005 | closed | green | intake-legacy-suggestions-migration.md |
| intake | CUST-IN-0006 | closed | green | intake-legacy-suggestions-migration.md |
| intake | CUST-IN-0007 | closed | green | intake-legacy-suggestions-migration.md |
| intake | CUST-IN-0008 | closed | blue | intake-legacy-suggestions-migration.md |
| intake | CUST-IN-0009 | closed | blue | intake-legacy-suggestions-migration.md |
| intake | CUST-IN-0010 | closed | green | intake-legacy-suggestions-migration.md |
| intake | CUST-IN-0011 | routed | red | intakes.md |
| intake | CUST-IN-0012 | closed | green | intakes.md |
| intake | CUST-IN-0013 | closed | blue | intakes.md |
| intake | CUST-IN-0014 | closed | blue | intakes.md |
| intake | CUST-IN-0015 | open | blue | intakes.md |
| intake | CUST-IN-0016 | open | green | intakes.md |
| intake | CUST-IN-0017 | open | green | intakes.md |
| intake | CUST-IN-0018 | open | green | intakes.md |

View file

@ -0,0 +1 @@
{".custodian-brief.md": "100644 6aad3a5788fad3abd60c0f1270f160062dc0168b 0\t.custodian-brief.md\n", "WORK-RECORDS.md": "100644 d535e0f2dcc3d454e041a8f1b547b5dea4493380 0\tWORK-RECORDS.md\n"}

View file

@ -0,0 +1,14 @@
{
"workplan_id": "dfadb19b-445e-59c6-bcd6-48e87b912c52",
"id": "27a691e0-dab7-41ec-9d3b-b0deebe2535e",
"topic_id": null,
"task_id": "6aca534c-5c7a-556f-b6b6-406433670c29",
"decision_id": null,
"event_type": "note",
"summary": "Final source checkpoint d834d0c is published; all7 reviewed Custodian files match and67 source task ID/UUID/status bindings agree (NK-WP-0048:20, NK-WP-0049:36, RMGR-WP-0021:5, CUST-WP-0080:6). Custodian consistency completed fail0/automation_error0/warn7/info4; existing forge-derived reconciliation repaired missing legacy task record IDs. Original staged brief, working records and unrelated changes preserved. Delivered bounded source: NK-WP-0048-T20 and NK-WP-0049-T34. Live next work: NK-WP-0048-T14, NK-WP-0049-T35/T36, readiness NK-WP-0049-T33, real recovery/MFA NK-WP-0049-T18/T19, scanner runtime gap RMGR-WP-0021-T05. All16 target gates remain open. Native storage, writer isolation and original TLS/trust recovery require their recorded owner boundaries.",
"detail": null,
"author": "codex",
"session_id": null,
"created_at": "2026-10-16T22:49:48.009494Z",
"workstream_id": "dfadb19b-445e-59c6-bcd6-48e87b912c52"
}

View file

@ -0,0 +1,23 @@
{
"observed_at": "2026-10-16T22:10:25.662202+00:00",
"lane": "groups",
"owner": "/home/worsch/net-kingdom",
"base_revision": "878abc93588e12b6da1bac055cfbb3abb8d7adf1",
"patch_sha256": "sha256:1f3f3ca94c46dadac8f24896053d4d23179de795e50a24298a0cfbcc46cca30e",
"applied": true,
"source_postimages": {
"docs/service-group-provenance.md": "sha256:9cb866c1ba4f92d239ff37ea6be57bafe04cf5e9e811fda5842878ab9677678c",
"identity-provisioner/Containerfile": "sha256:c744f39ffb4c0dc2686c3e94d8233bc772b55faae9581557bee17f91c19d9ca6",
"identity-provisioner/group_provenance.py": "sha256:fbd4c88d7682ba3d46c5b325dad1bc4f4a7faac2cd3f6ce9e0f7a89283bf46d4",
"identity-provisioner/provisioner.py": "sha256:2f8e4368e7155465bbc4070b1019c7105edf43bdc2ad3bbc5aa9bc19dc6ed7a5",
"identity-provisioner/server.py": "sha256:b61c83da6c7ce20fa2790d5bb00aeca6a91dccd133526bc559ed26562f90eb29",
"identity-provisioner/tests/test_directory_contract.py": "sha256:250395dcf2e3dbeca863d1829a0ae14b86ee17fd4107605df409bf56fa48c836",
"identity-provisioner/tests/test_group_provenance.py": "sha256:0809300a8c0d20070dc9806c011d0b5ca3695468bd02521fb386aebc4dde499a",
"identity-provisioner/tests/test_service_groups.py": "sha256:815571fd92c0001dacbe250640acd99b2f2872b4171519ed54f8f469ee90aafe",
"sso-mfa/k8s/README.md": "sha256:0aa87dc25bef245eadc184ae58ef1e25b748e2541086d4de2ada15d38b17340f"
},
"preexisting_brief_preserved": true,
"staged": false,
"published": false,
"native_acceptance": false
}

View file

@ -0,0 +1,23 @@
{
"observed_at": "2026-10-16T22:09:04.829846+00:00",
"lane": "groups",
"owner": "/home/worsch/net-kingdom",
"base_revision": "878abc93588e12b6da1bac055cfbb3abb8d7adf1",
"patch_sha256": "sha256:1f3f3ca94c46dadac8f24896053d4d23179de795e50a24298a0cfbcc46cca30e",
"applied": false,
"source_postimages": {
"docs/service-group-provenance.md": "sha256:9cb866c1ba4f92d239ff37ea6be57bafe04cf5e9e811fda5842878ab9677678c",
"identity-provisioner/Containerfile": "sha256:c744f39ffb4c0dc2686c3e94d8233bc772b55faae9581557bee17f91c19d9ca6",
"identity-provisioner/group_provenance.py": "sha256:fbd4c88d7682ba3d46c5b325dad1bc4f4a7faac2cd3f6ce9e0f7a89283bf46d4",
"identity-provisioner/provisioner.py": "sha256:2f8e4368e7155465bbc4070b1019c7105edf43bdc2ad3bbc5aa9bc19dc6ed7a5",
"identity-provisioner/server.py": "sha256:b61c83da6c7ce20fa2790d5bb00aeca6a91dccd133526bc559ed26562f90eb29",
"identity-provisioner/tests/test_directory_contract.py": "sha256:250395dcf2e3dbeca863d1829a0ae14b86ee17fd4107605df409bf56fa48c836",
"identity-provisioner/tests/test_group_provenance.py": "sha256:0809300a8c0d20070dc9806c011d0b5ca3695468bd02521fb386aebc4dde499a",
"identity-provisioner/tests/test_service_groups.py": "sha256:815571fd92c0001dacbe250640acd99b2f2872b4171519ed54f8f469ee90aafe",
"sso-mfa/k8s/README.md": "sha256:0aa87dc25bef245eadc184ae58ef1e25b748e2541086d4de2ada15d38b17340f"
},
"preexisting_brief_preserved": true,
"staged": false,
"published": false,
"native_acceptance": false
}

View file

@ -0,0 +1,29 @@
from pathlib import Path
import hashlib, json, subprocess
base = Path(__file__).resolve().parent.parent
lanes = [
('net-kingdom', 'group-provenance-r1/workplan-refinement/source.patch', '8251911aa8d6ebf9be916ece22ae4cd8ddb179110c3f9348aaee67db4c3acf74'),
('net-kingdom', 'recovery-attempt-r1/workplan-refinement.patch', 'aa11ac3b3cbb36febc436736e7eaa611f8769fbae70b0f026fa9534954aa604c'),
('repo-manager', 'scanner-batch-r1/owner-workplan.patch', 'e0d08a2e571664a6ec8c8c22b67c3788d248878c1644f9b758ba57c30a321488'),
]
receipts = []
for slug, rel, expected in lanes:
root = Path('/home/worsch') / slug
patch = base / rel
assert hashlib.sha256(patch.read_bytes()).hexdigest() == expected
def git(*args): return subprocess.check_output(['git', '-C', str(root), *args], stderr=subprocess.PIPE)
paths = [s[6:] for s in patch.read_text().splitlines() if s.startswith('+++ b/')]
assert len(paths) == 1
assert not git('status', '--porcelain', '--', paths[0]).strip()
git('apply', '--check', str(patch))
git('apply', str(patch))
if slug == 'repo-manager':
path = root / paths[0]
text = path.read_text().replace('id: RMGR-WP-0021-T05\nstatus: todo', 'id: RMGR-WP-0021-T05\nstatus: progress')
text += '\n2026-10-17 integration started: exact reviewed source is applied in the owner\ncheckout. Canonical tests, configured-hook fixtures and publication are being\nverified separately; actual-hook scanner-unavailability remains pending.\n'
path.write_text(text)
receipts.append({'owner': slug, 'path': paths[0], 'reviewed_patch_sha256': 'sha256:' + expected,
'integrated_sha256': 'sha256:' + hashlib.sha256((root / paths[0]).read_bytes()).hexdigest()})
(base / 'resume-integration-r1/workplan-integration.json').write_text(json.dumps(receipts, indent=2) + '\n')
print(json.dumps(receipts, indent=2))

Some files were not shown because too many files have changed in this diff Show more