fix: enforce deny_read, deny_exec, and config protection inside sandbox#5
Merged
machado144 merged 2 commits intomainfrom Feb 13, 2026
Merged
Conversation
Real-world testing revealed three critical security gaps: - ~/.kube/config and ~/.npmrc were fully readable (leaked credentials) - All deny_exec commands ran normally inside the sandbox - ~/.aigate/config.yaml was readable from inside the sandbox Root cause: buildMountOverrides() joined all mounts with && — any single mount failure cascaded to skip ALL subsequent security setup (mount overrides, exec deny, config protection), while the target command still ran because it was separated by ; from the last for-loop. Fixes: - Add mount --make-rprivate / at sandbox entry (ensures bind mounts work in all environments, not just util-linux 2.27+) - Rewrite buildMountOverrides() with independent mounts (each on its own line with || true, no cascading && chains) - Rewrite buildExecDenyOverrides() as standalone commands (not chained to mount overrides) - Add tilde expansion (~/) in resolvePatterns() for home-dir paths - Update default config: ~/.ssh/, ~/.aws/, ~/.gcloud/, ~/.kube/config, ~/.npmrc, ~/.pypirc use ~/ prefix; add kubectl delete/exec examples - Protect ~/.aigate/ config dir: tmpfs on Linux, Seatbelt deny on macOS - Add Seatbelt (deny process-exec) rules for deny_exec on macOS - Quote all paths in mount commands for space safety - Use exec for target command in runUnshare (matches buildNetFilterScript)
Resource limits are defined in the config but not yet enforced. Update docs to reflect this rather than implying they work.
murilopmachado
pushed a commit
that referenced
this pull request
Mar 5, 2026
…ox (#5) Real-world security testing revealed that none of the sandbox policies were actually enforced inside the namespace — deny_read, deny_exec, and config protection all failed silently while the target command ran normally. Root cause: buildMountOverrides() joined all mount commands with && — any single mount failure cascaded to skip ALL subsequent security setup, but the target command still executed (separated by ; from the last for-loop). Fixes: - Add mount --make-rprivate / at sandbox entry for bind mount compatibility - Rewrite buildMountOverrides() with independent mounts (no cascading &&) - Add buildExecDenyOverrides() for kernel-level deny_exec enforcement - Add tilde expansion (~/) in resolvePatterns() for home-dir paths - Protect ~/.aigate/ config dir: tmpfs on Linux, Seatbelt deny on macOS - Add Seatbelt (deny process-exec) rules for deny_exec on macOS - Quote all paths in mount commands for space safety - Update default config with ~/ prefixes and subcommand examples - Mark resource_limits as coming soon in docs
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.
Summary
Real-world security testing revealed that none of the sandbox policies were actually enforced inside the namespace — deny_read, deny_exec, and config protection all failed silently while the target command ran normally.
buildMountOverrides()joined all mount commands with&&. Any single mount failure cascaded to skip ALL subsequent security setup, but the target command still executed (separated by;from the last for-loop)|| true), paths are quoted, andmount --make-rprivate /is called at sandbox entry for compatibilitybuildExecDenyOverrides()kernel-enforces blocks viamount --bindoverlays on Linux and Seatbelt(deny process-exec)on macOS~/.aigate/is hidden via tmpfs (Linux) / Seatbelt deny (macOS)resolvePatterns()now expands~/prefixes; default config updated with~/.ssh/,~/.kube/config,~/.npmrc, etc.Test plan
make test)make build-local)./aigate init --force && ./aigate run -- sh -c 'curl --version'→ blocked./aigate run -- sh -c 'cat ~/.aigate/config.yaml'→ denied./aigate run -- sh -c 'cat ~/.kube/config'→ denied./aigate run -- sh -c 'cat .env'→ denied (if .env exists in workdir)