Conversation
shellz-n-stuff
left a comment
There was a problem hiding this comment.
This tested nicely on the security aspects. Obviously not a perfect sandbox but a great starting point that's lightweight and we can improve
| } | ||
|
|
||
| export function isSandboxAvailable(): boolean { | ||
| return process.platform === 'darwin' && fs.existsSync('/usr/bin/sandbox-exec'); |
There was a problem hiding this comment.
nice! Was looking for this
There was a problem hiding this comment.
Pull request overview
This PR implements macOS sandboxing for the goosed process using Apple's Seatbelt/Sandbox framework. The implementation blocks direct network access from the sandboxed process and routes all traffic through an HTTP/HTTPS CONNECT proxy running in the Electron main process. The proxy provides logging, domain blocking via a local blocklist, and optional LaunchDarkly-based egress control.
Changes:
- Added sandbox profile (sandbox.sb) that blocks direct network access and file write access to sensitive config files
- Implemented HTTP/HTTPS CONNECT proxy with domain blocking and LaunchDarkly integration
- Integrated sandbox execution into goosed startup flow when
GOOSE_SANDBOX=true
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/desktop/src/sandbox/sandbox.sb | Seatbelt profile that blocks direct network and protects config files |
| ui/desktop/src/sandbox/proxy.ts | HTTP CONNECT proxy with blocklist and LaunchDarkly egress control |
| ui/desktop/src/sandbox/index.ts | Sandbox initialization and proxy lifecycle management |
| ui/desktop/src/sandbox/blocked.txt | Template blocklist file for domain blocking |
| ui/desktop/src/goosed.ts | Integration of sandbox execution wrapper into goosed startup |
| ui/desktop/forge.config.ts | Added sandbox directory to packaged resources |
044a82b to
98bcf75
Compare
| lines.push( | ||
| '', | ||
| '(deny network*)', | ||
| '(allow network-outbound (literal "/private/var/run/mDNSResponder"))', | ||
| '(allow network-outbound (remote unix-socket))', | ||
| '(allow network-outbound (remote ip "localhost:*"))', | ||
| '(allow network-inbound (local ip "localhost:*"))' |
There was a problem hiding this comment.
Allowing outbound access to "localhost:*" means the sandboxed process can talk to any local service (including user-run local proxies/tunnels) and potentially reach the internet without going through this proxy; if the goal is to force all egress through the proxy, consider restricting outbound loopback to the proxy port (and optionally an explicit local-port allowlist) or clarify the intended threat model in the profile/docs.
| lines.push( | |
| '', | |
| '(deny network*)', | |
| '(allow network-outbound (literal "/private/var/run/mDNSResponder"))', | |
| '(allow network-outbound (remote unix-socket))', | |
| '(allow network-outbound (remote ip "localhost:*"))', | |
| '(allow network-inbound (local ip "localhost:*"))' | |
| const proxyPort = process.env.GOOSE_SANDBOX_PROXY_PORT; | |
| const loopbackIpPattern = | |
| proxyPort && /^\d+$/.test(proxyPort) ? `localhost:${proxyPort}` : 'localhost:*'; | |
| lines.push( | |
| '', | |
| '(deny network*)', | |
| '(allow network-outbound (literal "/private/var/run/mDNSResponder"))', | |
| '(allow network-outbound (remote unix-socket))', | |
| `(allow network-outbound (remote ip "${loopbackIpPattern}"))`, | |
| `(allow network-inbound (local ip "${loopbackIpPattern}"))` |
Adds a macOS sandbox for the goosed process using seatbelt (sandbox-exec) and an HTTP CONNECT proxy for egress filtering. All outbound network traffic from goosed is forced through a local proxy that enforces domain blocking, IP restrictions, and SSH allowlisting. ## Sandbox profile (seatbelt) The sandbox profile is built programmatically via buildSandboxProfile() rather than a static template, ensuring it is always regenerated fresh on each spawn and never becomes stale across app updates. Configurable protections (all default to enabled): - GOOSE_SANDBOX_PROTECT_FILES: write-protect ~/.ssh, shell configs - GOOSE_SANDBOX_BLOCK_RAW_SOCKETS: deny SOCK_RAW on AF_INET/AF_INET6 - GOOSE_SANDBOX_BLOCK_TUNNELING: block nc, ncat, netcat, socat, telnet Always-on protections: - All network denied except localhost (forces proxy usage) - Sandbox config and goose config are write-protected - Kernel extension loading denied ## Egress proxy An HTTP/CONNECT proxy runs in the Electron main process on 127.0.0.1. The sandboxed process uses it via HTTP_PROXY/HTTPS_PROXY env vars. Blocking layers checked in order: 1. Loopback detection (prevents proxy-as-relay bypass) 2. Raw IP address blocking (no domain = blocked) 3. Local blocklist (blocked.txt, live-reloaded via fs.watch) 4. SSH/Git host restriction (port 22/2222/7999) 5. LaunchDarkly egress-allowlist flag (optional, with configurable failover) Configurable proxy options: - GOOSE_SANDBOX_ALLOW_IP: allow raw IP connections (default: blocked) - GOOSE_SANDBOX_BLOCK_LOOPBACK: block loopback relay via proxy (default: off) - GOOSE_SANDBOX_ALLOW_SSH: enable/disable SSH (default: on) - GOOSE_SANDBOX_GIT_HOSTS: custom git host allowlist for SSH - GOOSE_SANDBOX_SSH_ALL_HOSTS: allow SSH to any host (default: off) - GOOSE_SANDBOX_LD_FAILOVER: LaunchDarkly failover mode (allow|deny|blocklist) ## Other changes - Startup now throws an error if GOOSE_SANDBOX=true but sandbox-exec is unavailable, instead of silently proceeding unsandboxed - Domain normalization handles trailing dots, punycode, and IPv6 brackets - Blocklist loaded once at startup and cached in memory with fs.watch for live reload (no per-request disk I/O) - Proxy resources cleaned up on app quit - Removed unused dns import and resolveIPToHostnames function - Formatted with prettier Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-019c551f-ba60-709e-b293-5dedad8d243f
98bcf75 to
d5e4bf4
Compare
| @@ -0,0 +1,43 @@ | |||
| #!/usr/bin/perl | |||
There was a problem hiding this comment.
Correct reaction. Mic and I were talking a bit about this, to proxy git you need to give it a "proxy command".
I couldn't get it working in Bash and we know Perl is available on MacOS machines by default (system internal usage). The alternatives we spoke about (and also happy to use) were:
- Spawn a Node server JIT (I opted against this as it seemed non-performant).
- Have another rust binary we produce and then bundle as part of this.
I'd be super open to other/better ideas.
It's worth noting this was actually blocked by Mic's initial commit not my additional self-ssh mitigations so for any for of network sandboxing (without just general bypass over port 22) we would need something shaped like this to allow Git over SSH (or just enforce HTTP only but that has CorpEnv implications for Block and likely others who may want to try this out)
There was a problem hiding this comment.
yeah - no other obvious solution yet... this was my reaction and still is!
There was a problem hiding this comment.
@DOsinga @shellz-n-stuff weirdly it does seem the idiomatic solution for this.
There was a problem hiding this comment.
and this is only for ssh, https will work the usual way. I think I have made peace with this approach.
| // --------------------------------------------------------------------------- | ||
|
|
||
| export function isSandboxEnabled(): boolean { | ||
| return process.env.GOOSE_SANDBOX === 'true' || process.env.GOOSE_SANDBOX === '1'; |
There was a problem hiding this comment.
should maybe also check here if we are on macos since the current implementation only works there
There was a problem hiding this comment.
Checked here:
We check if the user has requested (via env vars) then if it's actually available to provide)
|
Retested this works fine. It was a local config issue |
* main: fix: handle reasoning_content for Kimi/thinking models (#7252) feat: sandboxing for macos (#7197) fix(otel): use monotonic_counter prefix and support temporality env var (#7234) Streaming markdown (#7233) Improve compaction messages to enable better post-compaction agent behavior (#7259) fix: avoid shell-escaping special characters except quotes (#7242)
* origin/main: docs: playwright CLI skill tutorial (#7261) install node in goose dir (#7220) fix: relax test_basic_response assertion for providers returning reasoning_content (#7249) fix: handle reasoning_content for Kimi/thinking models (#7252) feat: sandboxing for macos (#7197) fix(otel): use monotonic_counter prefix and support temporality env var (#7234) Streaming markdown (#7233) Improve compaction messages to enable better post-compaction agent behavior (#7259) fix: avoid shell-escaping special characters except quotes (#7242) fix: use dynamic port for Tetrate auth callback server (#7228) docs: removing LLM Usage admonitions (#7227) feat(otel): respect standard OTel env vars for exporter selection (#7144) fix: fork session (#7219) Bump version numbers for 1.24.0 release (#7214) Move platform extensions into their own folder (#7210) fix: ignore deprecated skills extension (#7139) # Conflicts: # Cargo.lock # Cargo.toml
This uses seatbelt/sandbox to optionally enforce access.
This uses the approach shown here: https://github.com/michaelneale/agent-seatbelt-sandbox which uses a proven .db tool and a proxy on macos so you can limit things like: goose editing its own config, track or limit what urls are accessed etc. Has no overhead and works with any tools.
cc @shellz-n-stuff