fix(sandbox): resolve symlinked binary paths in network policy matching#774
Open
johntmyers wants to merge 4 commits intomainfrom
Open
fix(sandbox): resolve symlinked binary paths in network policy matching#774johntmyers wants to merge 4 commits intomainfrom
johntmyers wants to merge 4 commits intomainfrom
Conversation
Policy binary paths specified as symlinks (e.g., /usr/bin/python3) were silently denied because the kernel reports the canonical path via /proc/<pid>/exe (e.g., /usr/bin/python3.11). The strict string equality in Rego never matched. Expand policy binary paths by resolving symlinks through the container filesystem (/proc/<pid>/root/) after the entrypoint starts. The OPA data now contains both the original and resolved paths, so Rego's existing strict equality check naturally matches either. - Add resolve_binary_in_container() helper for Linux symlink resolution - Add from_proto_with_pid() and reload_from_proto_with_pid() to OpaEngine - Trigger one-shot OPA rebuild after entrypoint_pid is stored - Thread entrypoint_pid through run_policy_poll_loop for hot-reloads - Improve deny reason with symlink debugging hint - Add 18 new tests including hot-reload and Linux symlink e2e tests Closes #770
…naccessible The Linux-specific symlink resolution tests depend on /proc/<pid>/root/ being readable, which requires CAP_SYS_PTRACE or permissive ptrace scope. This is unavailable in CI containers, rootless containers, and hardened hosts. Add a procfs_root_accessible() guard that skips these tests gracefully instead of failing.
…improve deny messages
When /proc/<pid>/root/ is inaccessible (restricted ptrace, rootless
containers, hardened hosts), resolve_binary_in_container now logs a
per-binary warning with the specific error, the path it tried, and
actionable guidance (use canonical path or grant CAP_SYS_PTRACE).
Previously this was completely silent.
The Rego deny reason for binary mismatches now leads with 'SYMLINK HINT'
and includes a concrete fix command ('readlink -f' inside the sandbox)
plus what to look for in logs if automatic resolution isn't working.
…ution std::fs::canonicalize resolves /proc/<pid>/root itself (a kernel pseudo-symlink to /) which strips the prefix needed for path extraction. This caused resolution to silently fail in all environments, not just CI. Replace with an iterative read_link loop that walks the symlink chain within the container namespace without resolving the /proc mount point. Add normalize_path helper for relative symlink targets containing .. components. Update procfs_root_accessible test guard to actually probe the full resolution path instead of just checking path existence.
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
Policy binary paths specified as symlinks (e.g.,
/usr/bin/python3) were silently denied because the kernel reports the canonical path via/proc/<pid>/exe(e.g.,/usr/bin/python3.11). This fix resolves symlinks through the container filesystem after the entrypoint starts, expanding the OPA policy data so both the original and resolved paths match.Related Issue
Closes #770
Changes
opa.rs: Addedresolve_binary_in_container()helper that resolves symlinks via/proc/<pid>/root/on Linux using iterativeread_link(notcanonicalize, which resolves the procfs mount itself). Addedfrom_proto_with_pid()andreload_from_proto_with_pid()methods that expand binary paths during OPA data construction. Existingfrom_proto()/reload_from_proto()delegate withpid=0(backward-compatible, no expansion). Addednormalize_path()for relative symlink targets with..components.lib.rs:load_policy()now retains the proto for post-start OPA rebuild. Afterentrypoint_pid.store(), triggers a one-shot OPA rebuild with the real PID.run_policy_poll_loop()passes the PID on each hot-reload so symlinks are re-resolved.sandbox-policy.rego: Deny reason for binary mismatches now leads withSYMLINK HINTand includes actionable fix guidance (readlink -fcommand, what to check in logs).Design decisions
b.path == exec.pathstrict equality naturally matches the expanded entry.read_linkovercanonicalize—std::fs::canonicalizeresolves/proc/<pid>/rootitself (a kernel pseudo-symlink to/), stripping the prefix needed for path extraction. We use iterativeread_linkwhich reads only the specified symlink target, staying within the container namespace.Best-effort approach and known risks
Symlink resolution is opportunistic — it improves the common case but cannot be guaranteed in all environments. When resolution fails, we are loud about it: per-binary
WARN-level logs explain exactly what failed and what the operator should do. Deny reasons include prominentSYMLINK HINTtext with actionable fix commands. Both flow through the gRPCLogPushLayerand are visible viaopenshell logs.Environments where resolution will not work:
kernel.yama.ptrace_scope >= 2)/proc/<pid>/root/returnsEACCESeven for own PID--policy-rules/--policy-data, no--sandbox-id)readlink -finside sandboxIn all failure cases: the original user-specified path is preserved, the deny behavior is identical to pre-fix, and the operator gets a clear warning log explaining why resolution didn't work and what to do about it.
Testing
mise run pre-commitpassesnormalize_pathhelper for../.resolutionresolve_binary_in_containeredge cases (glob skip, pid=0, nonexistent paths)_with_pidvariantsSYMLINK HINTandreadlink -fcommandChecklist