fix(recovery): distinguish malformed paths from outside-folder denial
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a13eab-1e6a-7b33-9c37-372e90ada3a2
This commit is contained in:
tegwick 2026-10-16 07:48:36 +02:00
parent a17aa7edc1
commit 72c598e7de
7 changed files with 470 additions and 2 deletions

View file

@ -409,3 +409,14 @@ no periodic capture/expiry is enabled by installation alone. One subsequent
OIDC attempt timed out before owner-command handoff; it did not run mutations,
and its possible session revocation was unconfirmed. Only the contained owner
login lane is retried, without switching to root or inspecting captured bytes.
The native publisher fixture subsequently proved read/create, denied overwrite
and delete, and exact fixture cleanup. Its malformed parent path was rejected
withHTTP400. The checker now records that separately and also requires a valid
outside-account-folder request to fail authentication; accepting a malformed
path rejection alone would not establish authorization confinement.16 provider
checks pass after this correction. A fresh metadata capture plan confirms zero
allow policies matching helper Pods. The next owner login timed out before
handoff; no corrected provider probe or KV/ESO publication ran in that attempt.
The installed runtime's timers remain disabled while attended admission resumes.

View file

@ -0,0 +1,380 @@
{
"schema": "netkingdom.identity-capture-plan.v1",
"status": "requires_owner_execution_admission",
"created_at": "2026-10-16T05:46:40.122854+00:00",
"run_id": "ba725bea599b",
"binding": {
"schema": "netkingdom.identity-backup-preparation.v1",
"status": "prepared_not_admitted",
"observed_at": "2026-10-16T05:46:39.447927+00:00",
"cluster_uid": "a553c742-0115-43d4-99a4-a5ca56fe0786",
"profile_sha256": "40f9e5b5bb957e5b23e03b31701702b850502d16a92a0fa0ebf92618cc3f18a1",
"source_sha256": "cd5f3067d28b4c7cc93ed08b8cf3869218bdecf7c54b5dffed22d697de71dbb1",
"bindings": [
{
"namespace": "sso",
"deployment": "lldap",
"uid": "743a86bd-5cb0-4ca0-9c56-a3438f4746a8",
"generation": 7,
"pvc_bindings": [
{
"name": "lldap-data",
"uid": "0a06b65a-e7b3-42d3-916e-e74dbe5fc7da"
}
],
"secret_names_only": [
"lldap-secrets"
],
"configmap_names_only": []
},
{
"namespace": "sso",
"deployment": "authelia",
"uid": "dc40a925-2306-4a3b-89c0-e714187503c3",
"generation": 28,
"pvc_bindings": [
{
"name": "authelia-data",
"uid": "86826554-b014-4df4-8b28-866aa64f6413"
}
],
"secret_names_only": [
"authelia-secrets",
"authelia-smtp"
],
"configmap_names_only": [
"authelia-config"
]
},
{
"namespace": "sso",
"deployment": "keycape",
"uid": "99ddd83c-cb3f-4847-bcf8-35f1aa87627f",
"generation": 65,
"pvc_bindings": [
{
"name": "keycape-authentication-policy",
"uid": "423f558f-8c11-4edf-956b-243559df4a8e"
}
],
"secret_names_only": [
"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_names_only": []
},
{
"namespace": "mfa",
"deployment": "privacyidea",
"uid": "58c7f96d-61cb-4dd4-bca2-54661c0ac375",
"generation": 11,
"pvc_bindings": [
{
"name": "privacyidea-data",
"uid": "65650fc1-02d8-4c0f-84fc-e0015c8f3ed9"
},
{
"name": "privacyidea-logs",
"uid": "39fa90d9-bc54-4a03-a0d6-ae85165e0cb9"
}
],
"secret_names_only": [
"privacyidea-config"
],
"configmap_names_only": [
"factor-recovery-01119187d90d7b76",
"keycape-onboarding-9465e5e0b98f7986",
"privacyidea-cfg"
]
}
],
"secret_values_read": false,
"backup_captured": false,
"uploaded": false,
"runtime_changed": false,
"credential_grant_created": false,
"owner_admission_required": [
"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"
]
},
"kubeconfig": "/home/worsch/.kube/config",
"sources": [
{
"path": "capture-identity.py",
"sha256": "47dca7c4dc96fa7eb9fb02c81fca9ca8dc5281aac918979eb7367148168ba705"
},
{
"path": "identity-files.py",
"sha256": "d0063a9eedd3610e3d45bd2d834aae83cff74e4b3504d138b3548f427e89331d"
},
{
"path": "identity-bundle.py",
"sha256": "524a03c065db4307fcde83d33af39503f3de19cb6ef3a8ca72c991d4a8c08ee6"
},
{
"path": "prepare-identity-coverage.py",
"sha256": "cd5f3067d28b4c7cc93ed08b8cf3869218bdecf7c54b5dffed22d697de71dbb1"
},
{
"path": "identity-backup-profile.json",
"sha256": "40f9e5b5bb957e5b23e03b31701702b850502d16a92a0fa0ebf92618cc3f18a1"
}
],
"database": {
"namespace": "databases",
"cluster": "net-kingdom-pg",
"cluster_uid": "0cd04a7c-a183-48ac-975a-9f244ea82c73",
"pod": "net-kingdom-pg-1",
"pod_uid": "0404c66b-338b-4446-a767-622d248ab0d3",
"database": "privacyidea_db"
},
"create_resources": [
{
"apiVersion": "networking.k8s.io/v1",
"kind": "NetworkPolicy",
"metadata": {
"name": "nk49-capture-ba725bea599b",
"namespace": "sso"
},
"spec": {
"podSelector": {
"matchLabels": {
"net-kingdom/capture": "ba725bea599b"
}
},
"policyTypes": [
"Ingress",
"Egress"
],
"ingress": [],
"egress": []
}
},
{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "nk49-capture-ba725bea599b",
"namespace": "sso",
"labels": {
"net-kingdom/capture": "ba725bea599b"
}
},
"spec": {
"restartPolicy": "Never",
"activeDeadlineSeconds": 900,
"automountServiceAccountToken": false,
"securityContext": {
"runAsUser": 0,
"runAsGroup": 0
},
"containers": [
{
"name": "capture",
"image": "ghcr.io/gpappsoft/privacyidea-docker@sha256:af7841adad262f129e0c1d4f553af13f21cb2f4dc713533f316cfe43ed0b4473",
"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": "lldap",
"mountPath": "/source/lldap",
"readOnly": true
},
{
"name": "authelia",
"mountPath": "/source/authelia",
"readOnly": true
},
{
"name": "keycape",
"mountPath": "/source/keycape",
"readOnly": true
},
{
"name": "scratch",
"mountPath": "/scratch"
}
]
}
],
"volumes": [
{
"name": "lldap",
"persistentVolumeClaim": {
"claimName": "lldap-data",
"readOnly": true
}
},
{
"name": "authelia",
"persistentVolumeClaim": {
"claimName": "authelia-data",
"readOnly": true
}
},
{
"name": "keycape",
"persistentVolumeClaim": {
"claimName": "keycape-authentication-policy",
"readOnly": true
}
},
{
"name": "scratch",
"emptyDir": {
"medium": "Memory",
"sizeLimit": "512Mi"
}
}
]
}
},
{
"apiVersion": "networking.k8s.io/v1",
"kind": "NetworkPolicy",
"metadata": {
"name": "nk49-capture-ba725bea599b",
"namespace": "mfa"
},
"spec": {
"podSelector": {
"matchLabels": {
"net-kingdom/capture": "ba725bea599b"
}
},
"policyTypes": [
"Ingress",
"Egress"
],
"ingress": [],
"egress": []
}
},
{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "nk49-capture-ba725bea599b",
"namespace": "mfa",
"labels": {
"net-kingdom/capture": "ba725bea599b"
}
},
"spec": {
"restartPolicy": "Never",
"activeDeadlineSeconds": 900,
"automountServiceAccountToken": false,
"securityContext": {
"runAsUser": 0,
"runAsGroup": 0
},
"containers": [
{
"name": "capture",
"image": "ghcr.io/gpappsoft/privacyidea-docker@sha256:af7841adad262f129e0c1d4f553af13f21cb2f4dc713533f316cfe43ed0b4473",
"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": "privacyidea",
"mountPath": "/source/privacyidea",
"readOnly": true
},
{
"name": "scratch",
"mountPath": "/scratch"
}
]
}
],
"volumes": [
{
"name": "privacyidea",
"persistentVolumeClaim": {
"claimName": "privacyidea-data",
"readOnly": true
}
},
{
"name": "scratch",
"emptyDir": {
"medium": "Memory",
"sizeLimit": "512Mi"
}
}
]
}
}
],
"service_changes": false,
"credential_grants": false,
"matching_network_allow_policies": [],
"network_policy_enforcement_requires_owner_confirmation": true,
"offsite_delivery_included": false,
"global_atomic_snapshot_claimed": false,
"finished_at": "2026-10-16T05:46:40.884474+00:00"
}

View file

@ -0,0 +1,24 @@
{
"status": "refused_malformed_parent_status_before_kv_publication",
"action": "publisher",
"caller_entity_id": "4a1f663c-0621-0483-edd0-f852274f4b6b",
"caller_auth_path": "auth/netkingdom/oidc/callback",
"quota_before": {
"quota_bytes": 10737418240,
"used_bytes": 7094108469,
"available_bytes": 3643309771
},
"folder_created": false,
"share_created": false,
"share_id": "26",
"share_permissions": 21,
"folder": "/railiance-backups/net-kingdom-identity",
"provider_test_artifacts": [
"nk49-boundary-b7c9df967f844454ba87fe73a5582bd8.age",
"nk49-boundary-7c9e4e216c124a1ea3178e56e63cce67.age"
],
"boundary_fixture_cleanup_verified": false,
"boundary_phase": "deny_parent_path",
"parent_probe_status": 400,
"owner_fixture_cleanup_verified": true
}

View file

@ -1,5 +1,5 @@
{
"status": "approved_native_provider_delivery_checks_in_progress",
"status": "runtime_installed_provider_admission_waiting_attended_login",
"observed_at": "2026-10-15T23:56:00.293983+00:00",
"approval_decision": "40bfae40-6108-4e51-8613-073339f5345f",
"approved_platform_source_commit": "69a792c",
@ -38,5 +38,14 @@
"focused_tests_passed": 52,
"temporary_dns_selector_exclusions_applied": true,
"network_policy_rule_lines_present": 649,
"installed_runtime_release": "bf8b38f185037515cfad9eebcddefc9f60ace724c32e10d89ae8307dc95a4eb7"
"installed_runtime_release": "bf8b38f185037515cfad9eebcddefc9f60ace724c32e10d89ae8307dc95a4eb7",
"current_login_session_active": false,
"latest_login_failure": "callback_timeout_before_handoff",
"latest_timeout_possible_session_revocation_confirmed": false,
"publisher_read_create_and_overwrite_delete_denial_verified": true,
"publisher_fixture_cleanup_verified": true,
"parent_path_rejection_status": 400,
"corrected_boundary_tests_passed": 16,
"corrected_native_boundary_and_delivery_not_yet_run": true,
"helper_network_allow_count": 0
}

View file

@ -157,3 +157,28 @@ def test_bootstrap_dispatcher_handoff_help_and_missing_checkout(tmp_path):
env=dict(os.environ,NETKINGDOM_RECOVERY_ROOT=str(tmp_path/'missing'))
failure=subprocess.run(['bash',str(bootstrap/'bin/railiance'),'identity-recovery','--transfer-receipt','missing','--receipt','new'],env=env,capture_output=True)
assert failure.returncode==1 and b'Missing reviewed NetKingdom' in failure.stderr
@pytest.mark.parametrize('parent_status,outside_status,accepted',[(400,401,True),(400,200,False),(500,401,False)])
def test_provider_parent_400_requires_independent_outside_folder_denial(monkeypatch,parent_status,outside_status,accepted):
store={};owner_auth=('owner','fixture');token=TOKEN
def request(client,url,auth,method='GET',body=None,extra=None):
if '%2e%2e' in url:return parent_status,b''
if url.endswith('/r01-net-kingdom-pg/'):return outside_status,b''
name=url.rsplit('/',1)[-1]
if method=='PUT':
if name in store:return 403,b''
store[name]=body;return 201,b''
if method=='DELETE':return 403,b''
return 200,store[name]
def remove(client,url,auth,expected):
name=url.rsplit('/',1)[-1];assert auth==owner_auth and store[name]==expected;del store[name]
monkeypatch.setattr(setup,'request',request);monkeypatch.setattr(setup,'remove_fixture',remove)
receipt={}
if accepted:
setup.boundary(None,owner_auth,token,'publisher',receipt)
assert receipt['provider_boundary_verified'] and receipt['other_account_folder_status']==401
else:
with pytest.raises(setup.Refused):setup.boundary(None,owner_auth,token,'publisher',receipt)
assert not receipt.get('provider_boundary_verified')
assert not store

View file

@ -157,6 +157,14 @@ def boundary(client,auth,token,action,receipt):
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:

View file

@ -547,6 +547,17 @@ Provider boundary testing and ESO/native login delivery remain in progress.
No identity capture or scheduled retention has occurred. T02 progress/T03 wait.
Evidence: `docs/evidence/2026-10-16-identity-backup-live/`.
2026-10-16 native publisher checkpoint: actual fixture read/create worked and
publisher overwrite/delete were denied. Both exact fixture objects were cleaned;
the remaining parent-path check returned400 for a malformed path. Corrected the
checker to distinguish that rejection from a separate valid outside-folder
request that must fail authentication.16 provider checks pass after correction.
The next attended login timed out before handoff, so corrected native checks,
KV/ESO delivery and first full capture remain pending the human identity act.
Fresh capture metadata reports zero helper allow policies. Installed timers are
disabled. No new coding-agent credential or root fallback; partial owner share26
is undistributed. Pending browser status from operator, without duplicate logins.
## Verify owner-approved recovery and current native callers
```task