fix(sandbox,e2e): protect docker.sock, harden fs assertions to per-path#84
Merged
Conversation
Two related security-testing gaps from issue #66: 1. Production defense-in-depth gap: /var/run/docker.sock (and /run/docker.sock) had no ProtectedPaths entry, unlike ~/.azure and ~/.gcloud. Under the default profile this was safe (allow-list sandbox model, nothing grants /var/run by default), but a user granting a broad path (~, /var, /, or learn mode) would expose the Docker socket with no backstop — a container-escape vector. Added to protectedCommon()-adjacent platform baselines. 2. Test-integrity gap: assertFilesystemReadDenied/WriteDenied/ SymlinkEscapeDenied checked "does ANY denial substring appear ANYWHERE in the whole probe section", not per-path. A single leaked path among the ~14 (read) or 4 (write) probed per section would go undetected as long as a different path in the same section was denied — silently neutering most of the assertion. fs_write was worse: a successful write is silent, so there was no leak signal to find at all. Fixed by giving probe_write (audit.sh) the same explicit success-marker pattern probe_read already used (WRITABLE, mirroring READABLE), then rewriting the three assertions to fail on marker presence rather than pass on denial-substring presence. This is a per-path-safe check: one leak anywhere in the section now fails the assertion. Added unit tests (security_assertions_test.go) that mutation-test this against synthetic single-leak fixtures — the pre-fix logic would have passed all of them. Also expanded fs_read coverage (audit.sh, allowance.go) to include docker.sock, ~/.azure, ~/.config/gcloud per issue #66's ask. Signed-off-by: Niclas Hülsmann <niclas.huelsmann@tngtech.com>
This was referenced Jul 14, 2026
nhuelstng
added a commit
that referenced
this pull request
Jul 15, 2026
…xposure (#100) The whole internal/e2e/ package sits behind //go:build e2e, and that tag is compiled only by e2e.yml (weekly cron + manual dispatch). ci.yml and release.yml run `go test ./...` without it, so the model-free security regression tests never ran on the merge path — including the ones explicitly built to run fast without a live model. A reintroduced dir_token leak (#74) or a silently-neutered assertion (#84) would pass CI and only surface the following Saturday. Add an `e2e_fast` build tag shared with the model-free files (`e2e || e2e_fast`) and a PR-time CI job that runs just that slice on ubuntu + macOS in ~3s: no harness install, no API token, no OS sandbox. The full live suite still runs weekly via e2e.yml (those files build under both tags). Also close the --no-sandbox gap (#66): runSecurityAudit no longer silently skips every negative assertion for a --no-sandbox harness. It now asserts the audit ran to completion and documents the per-property exposure surface, so a no-sandbox run that produced no audit fails loudly instead of passing green with nothing checked. The classification logic is a pure, unit-tested function (noSandboxExposureReport) that runs in the fast PR slice. Refactor: extract the pure assertion predicates (fs*Leaked, fsAllowDenied, secretLeaked, envVarsLeaked, netProbeDenied) into security_assertions.go and buildOmac into fixtures.go so both build modes share them; the *testing.T assert wrappers stay in e2e_test.go. Verification: - go test -tags=e2e_fast -race ./internal/e2e/ → 11 tests pass (~3s) - go vet -tags=e2e ./internal/e2e/ → full live suite still compiles - go build ./... && go vet ./... → untagged build unaffected Refs #66 Signed-off-by: Niclas Hülsmann <niclas.huelsmann@tngtech.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
/var/run/docker.sock(+/run/docker.sockon Linux) toProtectedPathsininternal/sandboxprofile/baseline.go.assertFilesystemReadDenied/assertFilesystemWriteDenied/assertSymlinkEscapeDeniednow fail on a per-path leak marker instead of passing when any denial substring appears anywhere in the whole probe section.fs_reade2e coverage to include docker.sock,~/.azure,~/.config/gcloud, per e2e: testing ideas to validate (security + creative) #66.Why
Investigating #66's "expand fs-deny targets" item surfaced two things:
~/.azure/~/.gcloudalready had a second-layerProtectedPathsbackstop against a broad user grant (~,/, learn mode, etc.), while/var/run/docker.sock— a container-escape vector — had no equivalent backstop. Same asymmetry, closed the same way.How
audit.sh: writes now go through a newprobe_writehelper that prints an explicitWRITABLEmarker on success, mirroring the existingREADABLEmarkerprobe_readalready used for reads (a denied write is otherwise silent — no marker, no signal).e2e_test.go: the three assertions were refactored to pure marker-presence predicates (fsReadLeaked,fsWriteLeaked,symlinkEscapeLeaked) that fail if the marker appears anywhere in the extracted probe section — a per-path-safe check, since any single leak trips it regardless of how many other paths were correctly denied.baseline.go: docker.sock added alongside the existing cloud-cred entries in each platform'sProtectedPaths; both/var/run/docker.sockand/run/docker.sockare listed sinceProtectedPathsmatching is a literal string-prefix check (no symlink resolution) and/var/runisn't a symlink to/runon every distro.grants_test.go: unit test confirmingResolveGrantsincludes docker.sock inProtectedPathseven with an empty profile.Verification
go build ./...— cleango vet -tags e2e ./...— cleango test ./...— full suite passesNew
internal/e2e/security_assertions_test.gomutation-tests the fixed assertion logic against synthetic single-leak fixtures (no live LLM harness needed): the pre-fix "any denial substring anywhere" logic would have passed every one of these fixtures; the new logic correctly fails them.gofmt -l/sh -n audit.sh— cleanUnit tests pass locally without live credentials
CI e2e run (live harnesses) — will confirm once CI runs on this PR
Relates to #66.
🤖 Generated with Claude Code