feat(agent-proxy): add sandboxed local run mode - #335
Conversation
Prepare the agent proxy for single-machine, single-user use (one developer running one agent locally) without changing any existing remote behavior. - proxy.go: split Start into New/Serve/Shutdown so the caller owns the listener, serving, and teardown; Serve accepts any net.Listener (loopback :0 or a pathname unix socket); Shutdown drains in-flight requests, stops the poll/lease loops, and drops cached credential values (idempotent). Start stays a thin wrapper so the remote command is unchanged. Add Options.Local: no Proxy-Authorization (fixed startup scope), always refuse egress to the Infisical API host, and honor an --allow-host allowlist under block mode. - ca.go: add a self-signed in-memory ECDSA P-256 root that mints leaves directly and never calls out; RootPEM exposes the public cert only. - cache.go: extract the shared list-to-services resolution; the existing resolver delegates with identical semantics; add close(). - local.go: a single-snapshot resolver for the fixed scope that uses one developer token for discovery and value-fetch, gates on Read Value only, skips dynamic secrets, and fails closed on auth error. Existing tests pass unchanged; new tests cover the local CA, resolver lifecycle, auth-free local serving vs the remote challenge, the API-host block, the allow-host allowlist, and the New/Serve/Shutdown lifecycle.
The trust boundary for local mode: it keeps the untrusted agent from reading the developer's real credentials even though they live on the same machine. - spec.go / sandbox.go: the SandboxSpec policy, the Backend interface, platform selection, and an uncontained passthrough backend for --no-sandbox. - seatbelt.go / seatbelt_profile.go (macOS): generate an SBPL profile and exec sandbox-exec. (deny default), loopback-only egress to the proxy port, broad read minus credential deny paths, and the keychain services omitted so the agent cannot read the login token; profile passed inline, never written to disk. - bwrap.go / bwrap_args.go / bridge.go (Linux): build a bubblewrap argv. Default hard fence is an empty network namespace with a small in-namespace bridge (loopback TCP -> the proxy's pathname unix socket); auto-falls back to shared host networking with a warning when unprivileged user namespaces are restricted. No --new-session, so the interactive TTY is preserved. - sandbox_unsupported.go: platforms without a sandbox report unsupported. Golden tests cover the SBPL profile and both bwrap argv shapes; an env-gated live test exercises sandbox-exec enforcement on macOS.
Run an untrusted agent on your own machine with Infisical secrets brokered on the wire and an OS sandbox as the trust boundary: infisical secrets agent-proxy run --env <e> --path <p> -- <cmd>. Pipeline: resolve the developer's own login (keyring or --token; no machine identity) -> list proxied services (Read Value gate) -> start an ephemeral proxy on loopback or a unix socket with a self-signed local CA -> build a scrubbed child env -> sandbox-wrap -> run with inherited stdio and signal forwarding -> tear down and propagate the exit code. - The child's proxy URL is credential-free; the token and scope stay in the parent. The child env drops INFISICAL_TOKEN, INFISICAL_DOMAIN, and secret-shaped vars (--pass-env re-admits, --set-env injects a literal). - Sandbox toggle resolves from flag or env only, never .infisical.json. - Non-refreshable credentials warn at startup when brokering will stop. - Supported agents' own state dirs (~/.claude, ~/.claude.json, ~/.codex) are writable by default so interactive sessions persist. - Hidden __sandbox-supervisor subcommand runs the in-namespace bridge on Linux. Tests cover the env scrub (no token leak), the credential-free proxy URL, the expiry parse, the secret-shaped-name heuristic, and the agent-state defaults.
- Linux: mask credential files with a /dev/null bind and directories with an empty tmpfs. Mounting a tmpfs onto an existing file aborts bwrap at startup (ENOTDIR), which would have blocked the sandbox for anyone with a regular-file deny path such as ~/.docker/config.json or ~/.netrc. buildBwrapArgv takes an injected file classifier so it stays a pure, golden-testable function; the Linux backend passes a real os.Stat-based one. - Linux: the in-namespace supervisor overrides the root PersistentPreRun with a no-op, so the internal re-exec no longer runs the human-facing preamble (update check, notices, keyring read) — a network call — inside the empty network namespace, where it wasted the timeout and printed stray notices. - run: clean up the per-run temp dir explicitly at every exit path. os.Exit and util.HandleError skip deferred funcs, so the previous `defer os.RemoveAll` never fired and a temp dir leaked on every run. - run: drop a stale comment describing the removed machine-identity refresh path. Golden test covers the file-vs-directory masking. Builds on darwin and linux; existing suites and the macOS live enforcement tests pass.
Cut the verbose explanatory comments added with the feature down to short notes only where the code isn't self-evident, matching the surrounding style. Also remove two bits of dead code found while trimming: the unused sandbox.CurrentOS helper and the SandboxSpec.AllowHosts field (host allow-listing is enforced by the proxy via Options.AllowedHosts, not the sandbox). No behavior change.
…terminal `run` wraps an interactive agent that owns the terminal, but it inherited the engine's per-request activity logging (brokered/blocked/passthrough/error), which interleaved with the agent's output. Route the engine's ongoing zerolog output to a file instead: `--log-file` picks a stable path, otherwise a per-run temp file (removed on exit; tail it live to watch brokering). One-time startup notices and lifecycle errors still go to stderr, and HandleError/PrintWarning are unaffected (they write to stderr independently). This is the deliberate difference from `start`, whose activity log IS its output and streams to console.
…t trusted CA macOS Go CLIs (e.g. gh) verify TLS through Security.framework, which ignores the injected CA env var, so they couldn't be brokered. Fix it without weakening the credential boundary by using a persistent local root that's trusted once in the keychain, and by allowing only the cert-evaluation service in the sandbox. - agentproxy: add a persistent local root (newPersistentLocalCaManager) stored under a caller-chosen dir (load-or-create, self-heals when missing/corrupt/near expiry, key written 0600, atomic writes, flock so concurrent first-runs don't race). LocalOptions.CADir selects it; empty keeps the ephemeral in-memory root. - sandbox: SandboxSpec.AllowTrustd gates the trustd (cert-trust) service in the SBPL profile. securityd/SecurityServer (keychain secret reads) stay omitted regardless, so the login token remains unreadable in the box. - cmd: on macOS, persist the root under ~/.infisical/agent-proxy (already a sandbox-denied path, so the agent can't read the key), trust it once in the login keychain (one-time prompt; self-heals on removal), and set AllowTrustd. Trust-install is non-fatal: env-CA tools (Claude Code, Codex, curl) work regardless. Linux keeps the ephemeral root + SSL_CERT_FILE (no keychain). Verified: persistent-CA lifecycle (reuse/heal/mint) and the trustd toggle (trustd allowed, keychain services still omitted) via unit/golden tests; the trustd-allowed / securityd-blocked split (Go TLS verifies, keychain read fails) empirically on macOS. NOT yet verified end-to-end: the keychain trust-install (needs the interactive prompt) and gh brokering through a live proxy.
Found by running the bwrap path on real Ubuntu 22.04/24.04 (never
exercised on hardware before):
- Skip deny paths that don't exist. Masking a missing path with
--tmpfs/--ro-bind aborted bwrap ("Can't mkdir ...: Read-only file
system") because the mountpoint can't be created under the read-only
root bind, so every run on a fresh account died before the agent
started.
- Emit deny mounts after the cwd/tempdir/write binds so they always win.
A cwd bind that is an ancestor of a deny path (e.g. launching the
agent from $HOME) previously re-exposed ~/.aws, ~/.ssh, etc.
- Deny ~/infisical-keyring, the file-vault credential store that is the
default on headless Linux; it held the login JWT and backup key and
was readable from inside the sandbox.
- Preflight: when unprivileged user namespaces are fully restricted
(e.g. Ubuntu 24.04 AppArmor), report a clear, actionable error instead
of falling back to shared-net and dying with a raw
"bwrap: setting up uid map: Permission denied".
…rding - bringLoopbackUp: bwrap already brings lo up in the new netns, and the SIOCSIFFLAGS write is refused with EPERM there even with CAP_NET_ADMIN. Treating that as fatal made the hard fence fall back to the weaker shared-net path on every Linux host. Treat an already-up lo as success so the empty-netns fence actually engages. - Forward only SIGINT/SIGTERM/SIGHUP/SIGQUIT to the sandboxed child rather than every signal. signal.Notify with no filter also relayed SIGURG (the Go runtime's async-preemption signal) and SIGCHLD, which raced the child's exit path and intermittently corrupted a clean exit 0 into 255 (and occasionally killed the child mid-run). Terminal-generated signals still reach the child directly through the shared controlling-terminal foreground group. Same fix applied to the non-sandboxed and remote agent-proxy paths.
- Drop the "Local coupled mode" notice: internal jargon, not actionable. - Replace the routine token-expiry warning with a fail-fast error only when the token is already expired; no "expires in Xh" banner on every run. Matches auth-bearing CLIs (kubectl, cloud CLIs) that surface an error at use rather than pre-warning. - Only print the proxy activity-log path when --log-file pins it to a stable location; the default log lives in the per-run tempdir and is removed on exit, so printing its path was useless.
infisicalAPIHost() used the lenient url.Parse, so a scheme-less --domain (e.g. "app.infisical.com/api") yielded an empty host and the control-plane egress fence silently turned off. Parse strictly with url.ParseRequestURI (the pattern user.go already uses) and abort the run with a clear error when the host can't be determined, rather than starting with the fence disabled.
|
💬 Discussion in Slack: #pr-review-cli-335-feat-agent-proxy-add-sandboxed-local-run-mode Posted by Review Police — reviews, comments, new commits, and CI failures will stream into this channel. |
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 7 · PR risk: 0/10 |
|
| Filename | Overview |
|---|---|
| packages/cmd/agent_proxy_run.go | Main entry point for the new local run mode; sound overall — credential scrubbing, token resolution, sandbox preflight, and proxy/listener wiring are all correct. Minor: only uppercase NO_PROXY is set in the child env; no_proxy (lowercase) is omitted. |
| packages/agentproxy/proxy.go | Local mode integration: requestScope short-circuits auth for the fixed startup scope; TRACE/TRACK blocking, hop-by-hop header stripping, and CONNECT MITM all look correct. Control-plane block cleanly removed. |
| packages/agentproxy/local.go | Single-scope resolver with fail-closed auth-error handling; minor benign TOCTOU (concurrent !valid callers trigger duplicate resolveSnapshot) but no security impact. |
| packages/agentproxy/local_ca_store.go | Persistent CA management with gofrs/flock (cross-platform) and atomic temp-then-rename writes; 0700 dir and 0600 key are correct. Previously flagged unix.Flock Windows issue resolved. |
| packages/sandbox/spec.go | Expanded DefaultDenyPaths now includes .config/gh, .git-credentials, .npmrc, .gnupg as requested. Struct and constants are clean. |
| packages/sandbox/seatbelt_profile.go | macOS SBPL profile with (deny default), egress fenced to loopback proxy port, credential paths denied for both reads and writes (re-deny after write allows); write-deny on credential paths is the newly addressed finding. |
| packages/sandbox/bwrap.go | Linux bubblewrap backend with hard-fence/shared-net preflight probing; graceful fallback on Ubuntu 24.04 AppArmor restrictions with clear user warning. |
| packages/sandbox/bwrap_args.go | Deny mounts ordered after write-bind mounts so cwd/write-path ancestors never re-expose a masked credential dir. Pure function, golden-testable. |
| packages/sandbox/bridge.go | In-namespace TCP-to-Unix bridge for the hard-fence path; loopback bring-up tolerates EPERM correctly; signal forwarding is narrow (no SIGURG/SIGCHLD spam). |
| packages/cmd/agent_proxy.go | authAgentEnvKeys now includes SSH_AUTH_SOCK, SSH_AGENT_PID, GPG_AGENT_INFO — previously raised finding resolved. CA trust vars, proxy keys, and credential keys all look correct. |
Reviews (2): Last reviewed commit: "fix(agent-proxy): address PR review find..." | Re-trigger Greptile
…nfig #319) Resolve packages/agentproxy/proxy.go, keeping both sides: - local coupled mode (New/Serve/Shutdown, serviceResolver, control-plane fence, graceful pollLoop stop) from this branch. - proxied-service usage reporting (recordUsage/flushUsage) from #322. Also flush usage on shutdown: `run` is frequently shorter than one poll interval, so the poll-tick flush alone would never report a short session's usage. The daemon (`start`) path still flushes each tick.
Use util.ResolveEnvironmentName/ResolveSecretPath so `run` honors INFISICAL_ENVIRONMENT / INFISICAL_SECRET_PATH and .infisical.json defaults (defaultEnvironment/defaultSecretPath), matching `agent-proxy connect` (#319).
- Scrub SSH_AUTH_SOCK / SSH_AGENT_PID / GPG_AGENT_INFO from the child so an agent can't use the developer's ssh/gpg agent as a signing oracle. - macOS: deny file-write* on credential paths (not just reads), so a run from $HOME can't write into ~/.ssh and friends. - Add .config/gh, .git-credentials, .npmrc, .gnupg to the default deny paths. - Use gofrs/flock for the local CA lock so the file builds on Windows. - Drop the redundant control-plane egress block: the child holds no token (env scrubbed, keyring/config masked), so reaching Infisical does nothing; the block broke legit use (Infisical as a proxied service, or --unmatched-host=allow) and was IP-bypassable anyway.
Structure:
- Add packages/sandbox/proc.go with newInheritedCmd, ForwardTerminationSignals
and WaitExitCode, collapsing four copies of the exec/stdio wiring and three
copies of the signal-forwarding block, whose SIGURG/SIGCHLD rationale now
lives in exactly one place.
- Rename SandboxSpec to sandbox.Spec and its Sandbox field to Enabled,
proxyServer.cache to resolver, and caManager's intermediate{Key,Cert,Exp} to
signing*. The remote-only resign path keeps the intermediate naming, where it
is accurate.
- Replace the hand-rolled itoa with strconv.Itoa, move dedupeSorted out of the
Seatbelt file, share bwrapBaseArgs between the real argv and Preflight's
probes so a probe cannot certify flags the real run does not use, and reuse a
single serveTestProxy harness in the agentproxy tests.
Fixes:
- Wire --allow-read, which previously parsed into a field nothing consumed, so
the flag silently did nothing. It now re-opens a path inside the denied set,
read-only, on both backends.
- Self-heal a mismatched local CA key/cert pair. A crash between the two file
writes left a new cert beside the old key; both parsed and neither was
expired, so the store never regenerated and every leaf was signed by a key
the cert did not match.
- Keep both bridge directions alive until each finishes, half-closing per
direction, so a client that half-closes its request side no longer sees a
truncated response.
- Set lowercase http_proxy/https_proxy/no_proxy alongside the uppercase forms
via a shared setProxyEnv. curl honours only the lowercase form for
plain-HTTP URLs, so those requests were resolving DNS instead of reaching the
proxy, then failing closed against the egress fence.
Also record that the DNS omission in the Seatbelt profile is deliberate (the
proxy resolves on the host, and a missing resolver keeps proxy bypass loud and
closes a DNS exfiltration channel), and state plainly in the shared-net
fallback warning that proxy routing becomes advisory rather than enforced.
Tests: read exceptions on both backends including precedence and read-only
enforcement, the CA pair check (confirmed to fail without the fix), and a live
test pinning the egress invariant, namely no resolver, no raw route out, and
the proxy as the only way to reach a hostname.
Dynamic secrets were skipped in local mode with a warning, but nothing about the lease machinery was ever mode-specific: newLeaseStore already receives the developer's token (newProxyServer defaults ProxyToken to Local.UserToken), refreshLoop already runs, revokeAll already fires on shutdown, and value() already mints lazily. Two unwired spots in the local resolver were the whole limitation: - registerDynamic returned nil, so resolveServices dropped the credential. - activeJWTs returned an empty map, which is the liveness gate in refreshPass, so even a minted lease would never have been renewed and would have been dropped at its first expiry. The resolver now holds the lease store, registers a real lease, and reports the single liveness key derived from the empty wire JWT plus the fixed scope, which matches the registered leaseKey. It reports nothing while the snapshot is invalid, so a dropped authorization stops lease renewal as well as credential serving. Leases are gated on the API's per-credential callerCanLease, which the backend computes from the caller's Lease permission on that dynamic secret. Locally the caller is the minter, so that is the right gate, and skipping up front with one warning naming the secret beats registering a lease that fails on every request. Note this reads the opposite way from `connect`, where the same flag being true is a misconfiguration, because there the agent is a separate identity that could mint for itself and bypass the proxy. Locally there is no second identity and the sandbox is what prevents that. Tests: resolveSnapshot against a stubbed proxied-services API asserts the leasable credential is registered in the run's scope and the unleasable one is dropped; an end-to-end request asserts the minted value reaches the wire, minted once. The existing "local mode must register no lease JWTs" assertion encoded the old limitation and now asserts the liveness contract in both directions.
A pass over every comment this branch introduced. 284 comment lines down to 229, with no loss of the reasoning that is actually load-bearing. What went: - The 13-line header on buildBwrapArgv, which restated what the inline comments at each mount step already said. The specific rationale now sits next to the code it explains (tmpfs-onto-a-file aborts with ENOTDIR; a missing deny path cannot get a mountpoint under the read-only root bind). - Speculation and editorialising: "so a future refresher can rotate it", the comparison to how kubectl surfaces auth errors. - Comments restating an obvious signature, e.g. containsString and a test helper whose name already said it. - Duplicated explanations of the macOS keychain trust, which appeared twice in the same function. What stayed, deliberately: - The SIGURG/SIGCHLD rationale in proc.go. It documents a real bug (exit status corrupted to 255) and now lives in one place instead of three. - The CallerCanLease inversion in local.go: the same flag means the opposite thing in `connect`, which reads like a bug unless explained. - The loopback EPERM workaround in bridge.go, and the SBPL last-match-wins ordering rules, both of which are silently breakable. Comments that state a constraint the code cannot show are kept. Comments that narrate the code are gone.
Went through every file in the diff looking for dead code, unused symbols, unused parameters and leftover scaffolding. Four small things, plus one finding worth recording rather than "fixing". Fixed: - errUnsupportedPlatform was declared in sandbox.go but only ever used by the !darwin && !linux build, so it was dead weight on the two platforms we actually ship the sandbox for. Moved it next to its only user, which also drops the now-unneeded errors import from sandbox.go. - writeFileAtomic ignored three error returns implicitly. The calls are deliberately best-effort (cleanup, and Close on an already-failing path), so they now say so with an explicit `_ =` instead of tripping errcheck. - Dropped two redundant cobra fields from the supervisor command: DisableFlagParsing was set to its own default, and FParseErrWhitelist was not needed because the agent's argv sits after `--` and is never flag-parsed. Verified on a real Linux host that an agent command containing flags (`ls -la`, `curl -sS -o ... -w ...`) still runs correctly through the supervisor. Recorded, not fixed: - staticcheck on darwin reports buildBwrapArgv, bwrapBaseArgs, containsString and underAny as unused, because it discounts test-only callers and the Linux backend is compiled out. The fix it implies, a //go:build linux tag, would stop bwrap_args_test.go running on macOS, and being able to unit-test the Linux argv from a Mac already caught a real ordering bug in this branch. Left untagged with a comment at the top of the file so nobody "fixes" it later. Verified clean: gofmt; builds on darwin, linux and windows; staticcheck clean on linux and windows; no unused exported symbols, no unused Spec fields, no unused function parameters, no unused test helpers; go mod tidy is a no-op; all suites and the live macOS enforcement tests pass.
…l-sandboxing-for-same-host-agent-vault-runs
…l leaks
Three findings from the security review, all verified against a real Linux host
before and after the change.
1. Sandbox escape via TIOCSTI (High, Linux only)
The bwrap flags deliberately omitted --new-session, so the agent kept the
caller's controlling terminal and could push characters into its input queue
with the TIOCSTI ioctl. The host shell then executes them once the sandbox
exits, outside every protection this feature adds. Reproduced on Ubuntu
22.04 (kernel 5.15, before the 6.2 dev.tty.legacy_tiocsti switch):
before: TIOCSTI *** SUCCEEDED (escape possible) ***
after: TIOCSTI blocked: PermissionError
The flag was omitted on the belief that setsid breaks interactive TUIs. That
was wrong, and measured: stdin and stdout stay ttys, only the controlling
terminal goes, and TUIs read keys in raw mode. Claude Code was run end to end
under the flag, including Ctrl-C and clean exit. Anthropic's sandbox-runtime
passes it unconditionally with no PTY handling, which is the same bet.
macOS was never affected: Seatbelt already denies the ioctl.
The test that asserted --new-session must never appear now asserts the
opposite, with the reason.
2. INFISICAL_JWT reached the agent
It authenticates a JWT/OIDC machine identity, so it is as good as a token,
and it matched none of the secret-shaped name patterns. Now scrubbed, along
with INFISICAL_OIDC_AUTH_JWT.
3. Host IPC endpoints reachable
XDG_RUNTIME_DIR and DBUS_SESSION_BUS_ADDRESS were passed through, and
/run/user/<uid> was readable. Unix sockets ignore the network namespace, so
the agent could reach the session bus, where systemd --user
StartTransientUnit starts a process outside the sandbox. Same family as the
SSH_AUTH_SOCK hole closed earlier. The variables are scrubbed and the
runtime directory is masked; Linux only, macOS has neither.
Verified on the droplet after the change: all of INFISICAL_JWT,
XDG_RUNTIME_DIR, DBUS_SESSION_BUS_ADDRESS, INFISICAL_TOKEN and SSH_AUTH_SOCK
report SCRUBBED, and /run/user is empty inside the sandbox.
Declined from the same review: restricting network-bind to loopback. The agent
never holds a credential, so an inbound listener can only carry data, which is
outside what this feature protects. On the Linux hard fence it is moot anyway,
since an empty netns has no interface to bind.
The message a Windows user gets said the sandbox is "macOS and Linux only in
v1". That is design-discussion language in a user-facing string: there is no
"v1" the user can see (the CLI ships as 0.43.x), it implies Windows support is
planned, and it would go stale silently if that ever shipped.
Now names the platform actually detected instead of "this platform", and says
what --no-sandbox still gives you. The old wording made it sound like turning
the sandbox off gives up everything, when brokering and environment scrubbing
are unaffected.
the OS sandbox is not available on windows; re-run with --no-sandbox to run
the agent uncontained (credentials are still brokered and the environment is
still scrubbed)
Checked the rest of the PR for the same kind of language (v1/v2, TODO, "for
now", "planned", "coming soon"): this was the only occurrence.
…of the CLI
The messages on `run` had drifted into internal vocabulary and were longer than
anything else in the CLI. Rewritten to match the existing patterns: lowercase
terse flag help with "(can be specified multiple times)", warnings in the
"Unable to X, doing Y" shape, and short HandleError labels.
- Command summary was "Run an untrusted agent locally with secrets brokered on
the wire and an OS sandbox as the trust boundary". Three pieces of jargon in
one line, now "Launch an agent on this machine, sandboxed, with credentials
brokered on the wire", which reads alongside `start` and `connect`.
- --env and --path now state their env-var and .infisical.json fallbacks, which
`connect` already did and `run` did not.
- Dropped meta-commentary from flag help ("prints a warning", "used as-is; not
refreshed") and swapped "repeatable" for the house phrasing.
- The shared-networking warning was 60 words of hard-fence / env-scrub /
keyring-block vocabulary, and its remediation advice was wrong: it pointed at
an AppArmor setting that actually produces a refusal to start, not this
fallback. Now says what happened and what it costs, nothing else.
- Removed detail that only applied to the machines this was developed against:
a distro-specific install command, a named distro and its AppArmor knob, and a
list of specific tools that would or would not be brokered. Messages state the
condition and the action, not the environment they were found in.
- Errors no longer leak internals ("bind the ephemeral proxy on its unix
socket", "wrap the agent command", "preflight") since none of it is
actionable.
…ed usage report `run` opened a log file unconditionally, defaulting to one inside the per-run tempdir. The path was never printed and the directory is removed on exit, so the warnings that matter most (a dynamic secret that cannot be leased, a dropped authorization) were written somewhere the operator could not find and then deleted. On distributions where /tmp is a tmpfs it was also memory. Without --log-file nothing is written now: the logger goes to stderr filtered to warnings and errors, so problems surface while per-request activity is dropped and an agent's TUI stays intact. An explicit --log-level lifts the filter for debugging without a file. A rejected usage report logged at debug and vanished with it, so a role missing Report Usage silently stopped stamping last-used. It now warns once per run, naming the permission, with the per-attempt detail left at debug.
go.mod and go.sum conflicted: main added the LDAP, MySQL, and MSSQL drivers with their indirects and bumped golang.org/x/*, while this branch had promoted gofrs/flock to a direct dependency for the local CA store's lockfile. Kept both, with go mod tidy regenerating the sums.
No conflicts this time: main only bumped klauspost/compress, grpc, and pion/dtls for CVEs, and tidied e2e's go.mod to match. The gofrs/flock promotion this branch needs is untouched.
Description 📣
Adds
infisical secrets agent-proxy run, which brokers secrets for an agent running on the developer's own machine.start+connectneeds two hosts: the proxy holds the real credentials and the agent is kept away from them by being on a different machine. On a laptop there is no second machine, so an OS sandbox takes that role.The CLI resolves config and secret values with the developer's own login, starts an ephemeral proxy on loopback, and launches the agent inside a sandbox with a scrubbed environment. The agent gets placeholders, the proxy swaps in the real values on the wire. Everything dies with the command, nothing is left behind.
The sandbox
macOS uses Seatbelt (
sandbox-exec), Linux uses bubblewrap.~/.ssh,~/.aws,~/.infisical,~/.config/gh,~/.npmrc,/run/user/<uid>and friends are denied for read and write. Writable: cwd, a per-run temp dir,/tmp, and the agent's own state dirs.127.0.0.1inside it is not the host's loopback, the proxy listens on a unix socket with a small in-namespace bridge forwarding to it. Where a network namespace is unavailable the run falls back to shared host networking with a warning, and if the sandbox cannot start at all it refuses to run rather than running uncontained.INFISICAL_TOKEN/INFISICAL_JWT, and the SSH/GPG/DBUS agent socket addresses are dropped.--pass-envre-admits a specific one.--new-sessionis passed so a sandboxed process cannot inject keystrokes into the caller's terminal viaTIOCSTI.Permissions
No agent identity, everything runs as the developer.
Read Valueis the gate for static secrets,Leasefor dynamic ones. Dynamic secrets are minted lazily, renewed on the poll loop and revoked on exit.Certificates
Local mode mints its own root and never touches the org CA. On macOS it is persisted under
~/.infisical/agent-proxy(a denied path) and trusted once in the login keychain, so Go-based tools likeghthat ignoreSSL_CERT_FILEare brokered too.Flags
--no-sandbox,--allow-read,--allow-write,--allow-host,--pass-env,--set-env,--unmatched-host,--log-fileNotes for review
seatbelt_profile.goandbwrap_args.goare pure functions of aSpec, so the Linux argv and its mount ordering are unit-tested from any platform. Mount order is load-bearing: deny mounts must come after the write binds, or a run from$HOMEre-exposes the credential paths underneath it.seatbelt_live_test.gospawns realsandbox-execand asserts the boundary actually holds (credential reads denied, egress denied, keychain denied, read exceptions precise). Gated behindINFISICAL_SANDBOX_LIVE=1.~/.sshis denied, use HTTPS with a brokered token), and non-HTTP protocols are not brokered.Type ✨
Tests 🛠️
# Here's some code block to paste some code snippets