Skip to content

Commit 80a1747

Browse files
authored
feat: Add support for Linux Sandbox using Bubblewrap (#120)
* feat: Add support for bubblewrap sandbox * fix: Glob pattern expansion limit for linux * fix: Bug in glob pattern expansion for bwrap * fix: README on trust * fix: Multiple bubblewrap translator fix * test: Add E2E for linux sandbox * fix: Refactor bwrap sandbox to use common dangerous files * fix: Path test case * fix: Non-existent path handling bug * refactor: Misc cleanup * fix: Avoid bind mount for non-existentent deny protection * fix: Off by one bug in path depth handling * ci: Disable AppArmor on GHA runner * fix: Disable apparmor userns restrictions
1 parent b97a4c2 commit 80a1747

File tree

13 files changed

+2495
-56
lines changed

13 files changed

+2495
-56
lines changed

.github/workflows/pmg-e2e.yml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,3 +443,61 @@ jobs:
443443
444444
- name: Run Sandbox E2E Test
445445
run: pmg --sandbox npm exec -- node test/sandbox-e2e.js
446+
447+
sandbox-e2e-linux:
448+
name: Sandbox E2E - Linux (Bubblewrap)
449+
runs-on: ubuntu-latest
450+
timeout-minutes: 10
451+
defaults:
452+
run:
453+
shell: bash
454+
steps:
455+
- name: Checkout Source
456+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
457+
458+
- name: Setup Go
459+
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5
460+
with:
461+
go-version-file: go.mod
462+
463+
- name: Setup Node.js
464+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
465+
with:
466+
node-version: 20
467+
check-latest: true
468+
469+
- name: Install Bubblewrap
470+
run: sudo apt-get update && sudo apt-get install -y bubblewrap
471+
472+
- name: Verify Bubblewrap Installation
473+
run: bwrap --version
474+
475+
- name: Build PMG
476+
run: make
477+
478+
- name: Add pmg to PATH
479+
run: echo "$GITHUB_WORKSPACE/bin" >> $GITHUB_PATH
480+
481+
- name: Setup PMG
482+
run: pmg setup install
483+
484+
- name: Create Test Directories for Sandbox Permissions Tests
485+
run: mkdir -p ~/.aws ~/.gcloud ~/.kube ~/.ssh ~/.gnupg ~/.docker
486+
487+
- name: Create Test Files for Sandbox Permissions Tests
488+
run: |
489+
touch ~/.aws/credentials
490+
touch ~/.gcloud/credentials.json
491+
touch ~/.kube/config
492+
touch ~/.ssh/id_rsa
493+
touch ~/.gnupg/pubring.kbx
494+
touch ~/.docker/config.json
495+
496+
- name: Disable AppArmor for Bubblewrap
497+
run: |
498+
sudo systemctl stop apparmor
499+
sudo systemctl disable apparmor
500+
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
501+
502+
- name: Run Sandbox E2E Test
503+
run: pmg --sandbox --sandbox-profile npm-restrictive npm exec -- node test/sandbox-e2e.js

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ See [example](https://safedep.io/malicious-npm-package-express-cookie-parser/)
1717
- Blocks malicious packages at install time
1818
- No configuration required, just install and use
1919
- Maintains package installation event log for transparency and audit trail
20+
- Enforces least privilege and defense in depth using OS native sandboxing
21+
22+
PMG guarantees its own artifact integrity using GitHub and npm attestations. Users can cryptographically prove that the binary they run
23+
matches the source code they reviewed, eliminating the risk of tampered or malicious builds. See [why and how to trust PMG](docs/trust.md).
2024

2125
## PMG in Action
2226

config/config.template.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,14 @@ trusted_packages:
5757
#
5858
# Currently supported platforms:
5959
# - macOS (using Seatbelt sandbox-exec)
60-
# - Linux (planned: Bubblewrap or seccomp-bpf)
60+
# - Linux (using Bubblewrap with namespace isolation)
6161
# - Windows (planned)
62+
#
63+
# Platform-specific limitations:
64+
# - Linux: Filesystem permissions use coarse-grained bind mounts. Glob patterns (e.g., *.txt)
65+
# are expanded at policy translation time, but entire directories may be mounted rather than
66+
# individual matching files. This is less precise than macOS regex-based filtering.
67+
# - macOS: Network filtering is limited (all-or-nothing for most policies).
6268
sandbox:
6369
# Enable sandbox mode (opt-in, default: false for backward compatibility)
6470
enabled: false

docs/sandbox.md

Lines changed: 81 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,31 @@ PMG sandbox design goal is to protect against unknown supply chain attacks using
55
We do not want to re-invent sandbox and likely rely on OS native sandbox primitives. This is at the cost of developer experience,
66
where we have to work within the limitations of the sandbox implementations that we use.
77

8+
## Requirements
9+
10+
- Bubblewrap on Linux
11+
- Seatbelt on MacOS
12+
13+
<details>
14+
<summary>Bubblewrap Installation on Linux</summary>
15+
16+
For Debian-based Linux distributions, you can install Bubblewrap with the following command:
17+
18+
```bash
19+
sudo apt install bubblewrap
20+
```
21+
22+
For Arch Linux, you can install Bubblewrap with the following command:
23+
24+
```bash
25+
sudo pacman -S bubblewrap
26+
```
27+
28+
For other Linux distributions, you can install Bubblewrap from the package manager of your choice.
29+
See [Bubblewrap Installation](https://github.com/containers/bubblewrap#installation) for more details.
30+
31+
</details>
32+
833
## Usage
934

1035
- Make sure sandbox is enabled in your `config.yml` file.
@@ -99,11 +124,39 @@ Next time you run `pmg pnpm install`, the custom policy template will be used in
99124

100125
## Supported Platforms
101126

102-
| Platform | Supported | Implementation |
103-
| -------- | --------- | ---------------------------------- |
104-
| MacOS | Yes | Seatbelt sandbox-exec |
105-
| Linux | No | Bubblewrap / seccomp-bpf (planned) |
106-
| Windows | No | Not yet supported |
127+
| Platform | Supported | Implementation |
128+
| -------- | --------- | ----------------------------------- |
129+
| MacOS | Yes | Seatbelt sandbox-exec |
130+
| Linux | Yes | Bubblewrap with namespace isolation |
131+
| Windows | No | Not yet supported |
132+
133+
### Platform-Specific Limitations
134+
135+
<details>
136+
<summary>Linux (Bubblewrap)</summary>
137+
138+
**Filesystem permissions are coarse-grained**: [Bubblewrap](https://github.com/containers/bubblewrap) uses bind mounts for filesystem isolation.
139+
140+
To prevent `Argument list too long` errors with large directory trees, PMG automatically uses
141+
coarse-grained fallback strategies when glob patterns match many files.
142+
143+
**Fallback Behavior:**
144+
145+
- **Small patterns** (< 100 matches): Individual files are mounted (fine-grained, most precise)
146+
- **Large patterns** (> 100 matches): Parent directory is mounted (coarse-grained, scalable)
147+
- **Threshold**: 100 paths per pattern triggers coarse-grained fallback
148+
149+
**Network filtering**: All-or-nothing network isolation (via `--unshare-net`). Host-specific
150+
filtering is not enforced.
151+
152+
</details>
153+
154+
<details>
155+
<summary>macOS (Seatbelt)</summary>
156+
157+
**Network filtering is limited**: Seatbelt supports network rules in policies, but fine-grained `host:port` filtering is not enforced.
158+
159+
</details>
107160

108161
## Concepts
109162

@@ -176,6 +229,29 @@ Use `log(1)` to filter the log file by the log tag or generic `PMG_SBX_` prefix.
176229
log show --last 5m --predicate 'message ENDSWITH "PMG_SBX_"' --style compact
177230
```
178231

232+
### Linux
233+
234+
Linux sandbox implementation uses Bubblewrap for namespace-based isolation. Enable debug logging to see translated sandbox arguments:
235+
236+
```bash
237+
APP_LOG_LEVEL=debug APP_LOG_FILE=/tmp/pmg-debug.log pmg --sandbox --sandbox-profile=npm-restrictive npm install express
238+
```
239+
240+
Review the debug log to see the translated `bwrap` command-line arguments:
241+
242+
```bash
243+
grep "Bubblewrap arguments" /tmp/pmg-debug.log
244+
```
245+
246+
To debug sandbox violations, you can manually test commands with increased verbosity by running the sandbox command directly:
247+
248+
```bash
249+
# Extract the bwrap command from debug logs and run with --verbose
250+
bwrap --verbose [arguments...] -- npm install express
251+
```
252+
253+
**Note**: Unlike macOS, Bubblewrap does not provide real-time violation logging. Policy violations typically manifest as `EACCES` (Permission denied) errors.
254+
179255
## References
180256

181257
- https://github.com/anthropic-experimental/sandbox-runtime

docs/trust.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ The assertion in [2] cannot be *implicit*. If so, it breaks the entire security
1010
## Security Goals
1111

1212
- Adopt software supply chain security best practices so that PMG users can *verify* and only then trust PMG
13-
- PMG is open source, built in public and reviewed by the community for trust in code
13+
- PMG is open source, built in public and reviewed by the community for verifiable source of truth
1414
- PMG leverages GitHub build attestation to verify the integrity of the PMG binary with source provenance
1515
- PMG npm package has build attestation to verify the integrity of the PMG binary and build environment with source provenance
1616
- PMG security model is multi-layered without single point of failure
@@ -48,7 +48,7 @@ Install verified binary for your platform:
4848
gh release download $RELEASE_TAG -R safedep/pmg --dir ./pmg-$RELEASE_TAG
4949
```
5050

51-
Install the platform specific binary from `./$pmg-$RELEASE_TAG`. To see binary specific attestation metadata, run:
51+
Install the platform specific binary from `./pmg-$RELEASE_TAG`. To see binary specific attestation metadata, run:
5252

5353
```bash
5454
gh attestation verify pmg_Linux_x86_64.tar.gz -R safedep/pmg --format json

sandbox/executor/apply.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,12 @@ func ApplySandbox(ctx context.Context, cmd *exec.Cmd, pmName string, opts ...app
5656

5757
policy, err = registry.GetProfile(cfg.SandboxProfileOverride)
5858
if err != nil {
59-
return nil, fmt.Errorf("failed to load override sandbox policy %s: %w", cfg.SandboxProfileOverride, err)
59+
return nil, usefulerror.Useful().
60+
WithCode("sandbox_policy_load_failed").
61+
WithHumanError(fmt.Sprintf("failed to load override sandbox policy %s: %s", cfg.SandboxProfileOverride, err)).
62+
WithHelp("Please check the sandbox profile path and try again.").
63+
WithAdditionalHelp("See more at: https://github.com/safedep/pmg/blob/main/docs/sandbox.md").
64+
Wrap(fmt.Errorf("failed to load override sandbox policy %s: %w", cfg.SandboxProfileOverride, err))
6065
}
6166
} else {
6267
log.Debugf("Looking up sandbox policy for %s", pmName)
@@ -68,6 +73,7 @@ func ApplySandbox(ctx context.Context, cmd *exec.Cmd, pmName string, opts ...app
6873
policyRef, exists := cfg.Config.Sandbox.Policies[pmName]
6974
if !exists {
7075
return nil, usefulerror.Useful().
76+
WithCode("sandbox_policy_not_configured").
7177
WithHumanError(fmt.Sprintf("no sandbox policy configured for %s", pmName)).
7278
WithHelp("Please configure a sandbox policy for this package manager in the config file.").
7379
WithAdditionalHelp("See https://github.com/safedep/pmg/blob/main/docs/sandbox.md for more information.").
@@ -123,7 +129,12 @@ func ApplySandbox(ctx context.Context, cmd *exec.Cmd, pmName string, opts ...app
123129
}
124130

125131
if !sb.IsAvailable() {
126-
return nil, fmt.Errorf("sandbox %s is required but not available", sb.Name())
132+
return nil, usefulerror.Useful().
133+
WithCode("sandbox_not_available").
134+
WithHumanError(fmt.Sprintf("sandbox %s is required but not available", sb.Name())).
135+
WithHelp("Please install the sandbox provider and try again.").
136+
WithAdditionalHelp("See more at: https://github.com/safedep/pmg/blob/main/docs/sandbox.md").
137+
Wrap(fmt.Errorf("sandbox %s is required but not available", sb.Name()))
127138
}
128139

129140
log.Debugf("Running %s in %s sandbox with policy %s", pmName, sb.Name(), policy.Name)

0 commit comments

Comments
 (0)