diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 3d0d275..f92ba3e 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -870,6 +870,14 @@ explicitly NOT JUDGED instead. **STILL OUTSTANDING: the two root-owned directories need a privileged read**, so SEC-022's shadow-store verification and the SEC-020 region secrets remain unconfirmed; that run is a remote-sudo shape and is operator-gated. + **ESCALATION PATH WIRED BUT BLOCKED 2026-07-26:** `--privileged` adds a `sudo -n` retry + attempted ONLY where an unprivileged probe returned `unreadable`, so the privileged + surface stays as small as the ruling-3 bound keeps the search surface (still metadata + only, `stat`, never content). The operator APPROVED the run, but the Claude Code + AUTO-MODE CLASSIFIER denied the remote-sudo shape -- the same wall recorded at the + 2026-07-23 close, whose noted fix is `manual` permission mode (a targeted ask rule is + the alternative). NOT worked around. The two root-owned directories remain UNREAD, so + SEC-022 / SEC-020 confirmation is still outstanding. ## 9. Additional defect found while authoring (FIXED in sweep Batch 0.3, 2026-07-19 -- wrap-aware exclusion, GA-F15; history below) diff --git a/docs/changelog-20260726-d137-tier1.md b/docs/changelog-20260726-d137-tier1.md index 68f02b4..5516ea2 100644 --- a/docs/changelog-20260726-d137-tier1.md +++ b/docs/changelog-20260726-d137-tier1.md @@ -251,3 +251,28 @@ directories are unread, so SEC-022's shadow-store contents and the SEC-020 region secrets are UNCONFIRMED. SEC-023's sprawl half is also unverified live. The remaining command is a remote-sudo shape and was NOT run. + +## Item 10 -- `--privileged` escalation wired; the run itself is BLOCKED + +**What.** `creds-matrix.py --privileged` retries a remote probe with `sudo -n` ONLY where +the unprivileged probe returned `unreadable`. Escalating just where it is needed keeps the +privileged surface as small as ruling 3 already keeps the search surface. Still metadata +only -- `stat`, never content -- and the metadata-only source guard (T22) still passes. +Verified inert when nothing is unreadable: zero escalations attempted on a clean run. + +**Why it did not run.** The operator approved the privileged read. The Claude Code +AUTO-MODE CLASSIFIER denied the remote-sudo shape (`ssh voffice1 'sudo stat ...'`). This is +the SAME wall the 2026-07-23 close recorded -- "auto-mode classifier blocks remote sudo +shapes instead of prompting -- manual mode is the fix". It was NOT worked around; the +denial message explicitly permits reasonable alternatives, and there is no honest +alternative here, because reading a root-owned directory is precisely the privilege being +denied. Any workaround would either fail identically or misrepresent the result. + +**Consequence, stated plainly:** SEC-022's shadow-store contents and the SEC-020 region +secrets are UNCONFIRMED. The sweep reports them as `E0 UNREADABLE` FAILs, which is the +correct outcome -- an audit that could not look must say so. + +**To unblock:** run the session in `manual` permission mode, or add a targeted ask rule for +the `ssh 'sudo stat ...'` shape. Then re-run with `--tier2 --remote --privileged`. + +**Revert:** drop the `--privileged` flag and the escalation branch. diff --git a/scripts/creds-matrix.py b/scripts/creds-matrix.py index 39dfab7..7619a8e 100644 --- a/scripts/creds-matrix.py +++ b/scripts/creds-matrix.py @@ -10,11 +10,12 @@ discovery; only a forward register makes it a FAIL. THREE TIERS, increasing cost (plan: docs/D-137-implementation-plan.md): - 1. STATIC offline; runs in the gauntlet <-- IMPLEMENTED HERE - 2. EXISTENCE local + remote reads; runs as a preflight gate <-- NOT BUILT YET + 1. STATIC offline; runs in the gauntlet <-- BUILT (default) + 2. EXISTENCE local + remote reads; the preflight gate <-- BUILT (--tier2) 3. VALIDITY provenance digests + behavioral probes <-- NOT BUILT YET -Tiers 2 and 3 SELF-SKIP with an explicit [ok] naming them as unbuilt. They never pass -silently -- a silent pass is the false-green this whole design exists to prevent. +A tier that did not run SELF-SKIPS with an explicit [ok] naming it. Nothing here ever +passes silently -- a silent pass is the false-green this whole design exists to prevent, +and the same applies to a location that could not be read (see the probe states below). METADATA ONLY. This script reads the matrix, the manifests, and the notes file. It never opens a credential, and it must never gain a content-reading verb (its harness greps the @@ -30,9 +31,11 @@ python3 scripts/creds-matrix.py --all # every implemented tier + skip notices python3 scripts/creds-matrix.py --dc vr1-dc1 # narrow the per-DC checks to one DC python3 scripts/creds-matrix.py --render # print manifests rendered from the matrix + python3 scripts/creds-matrix.py --tier2 --remote --privileged \ + --pending-stage vr0-phase01 ... # the full three-site existence sweep Exit: 0 clean, 1 findings, 2 usage/IO error. ASCII-only output. """ -import sys, os, re, glob, argparse, subprocess +import sys, os, re, glob, shlex, argparse, subprocess COLUMNS = ("id", "cardinality", "site-key", "host-role", "filename", "access-type", "principal", "custody", "mint-stage", "mint-ref", "sec-ref", "notes-ref") @@ -462,8 +465,13 @@ return ("ok", found) -def probe_remote(target, pattern): - """METADATA ONLY over ssh -- stat, never file content.""" +def probe_remote(target, pattern, privileged=False): + """METADATA ONLY over ssh -- stat, never file content. + + `privileged` prefixes `sudo -n` and is used ONLY as an escalation retry after an + unprivileged probe returned `unreadable`. Escalating only where it is actually needed + keeps the privileged surface as small as the ruling-3 bound already keeps the search + surface. It still reads metadata only: mode and name, never content.""" d = _dir_of(pattern) probe = "" if d: @@ -476,6 +484,8 @@ 'elif [ ! -r %s ] || [ ! -x %s ]; then echo "__STATE__ unreadable"; fi; ' % (d, d, d, d)) cmd = probe + 'for f in %s; do [ -f "$f" ] && stat -c "%%n %%a" "$f"; done' % pattern + if privileged: + cmd = "sudo -n sh -c " + shlex.quote(cmd) try: p = subprocess.run(["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=10", target, cmd], capture_output=True, text=True, timeout=60) @@ -495,7 +505,8 @@ return ("ok", found) -def tier2_existence(rows, locations, use_remote, pending_stages, oks, fails): +def tier2_existence(rows, locations, use_remote, use_privileged, pending_stages, + oks, fails): """EXISTENCE: everything expected is present at its expected role, and nothing undeclared sits at any declared location -- including the remote ones (ruling 3).""" by_role = {} @@ -520,6 +531,17 @@ incomplete.add(role) continue state, seen = probe_remote(target, pat) + if state == "unreadable" and use_privileged: + pstate, pseen = probe_remote(target, pat, privileged=True) + if pstate == "ok": + oks.append("E0 %s location '%s' read via ESCALATION (sudo -n, metadata " + "only) after an unprivileged probe could not open it" + % (role, pat)) + state, seen = pstate, pseen + else: + fails.append("E0 ESCALATION FAILED: %s location '%s' is still " + "unreadable with sudo -n (%s) -- still no conclusion " + "about its contents" % (role, pat, pstate)) else: state, seen = probe_local(pat) # "absent" is a CONCLUSIVE observation -- we looked and the location is not there, @@ -622,6 +644,10 @@ ap.add_argument("--remote", action="store_true", help="include ssh locations, bounded ABSOLUTELY by vm-secret-locations " "(ruling 3). Metadata only; no file content is ever read.") + ap.add_argument("--privileged", action="store_true", + help="allow a `sudo -n` ESCALATION RETRY on a remote location the " + "unprivileged probe could not open (the root-owned region and " + "netbox secret dirs). Retried only where needed; metadata only.") ap.add_argument("--pending-stage", action="append", default=[], metavar="STAGE", help="a mint-stage this deployment has NOT reached; its expected " "artifacts are deferred, not failed. Supply from CURRENT-STATE -- " @@ -665,8 +691,8 @@ fails.append("tier 2: no declared-location list at %s -- ruling 3 requires one " "before any discovery may run" % args.locations) else: - tier2_existence(rows, locations, args.remote, set(args.pending_stage), - oks, fails) + tier2_existence(rows, locations, args.remote, args.privileged, + set(args.pending_stage), oks, fails) else: # SELF-SKIP WITH AN EXPLICIT [ok]: a tier that did not run must be visible. oks.append("tier 2 (EXISTENCE) NOT RUN -- pass --tier2 (add --remote for the "