fix(recovery): distinguish malformed paths from outside-folder denial
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a13eab-1e6a-7b33-9c37-372e90ada3a2
This commit is contained in:
parent
a17aa7edc1
commit
72c598e7de
7 changed files with 470 additions and 2 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue