diff --git a/docs/sandbox.md b/docs/sandbox.md
index a26ed8e..0094384 100644
--- a/docs/sandbox.md
+++ b/docs/sandbox.md
@@ -5,6 +5,31 @@ PMG sandbox design goal is to protect against unknown supply chain attacks using
We do not want to re-invent sandbox and likely rely on OS native sandbox primitives. This is at the cost of developer experience,
where we have to work within the limitations of the sandbox implementations that we use.
+## Requirements
+
+- Bubblewrap on Linux
+- Seatbelt on MacOS
+
+
+Bubblewrap Installation on Linux
+
+For Debian-based Linux distributions, you can install Bubblewrap with the following command:
+
+```bash
+sudo apt install bubblewrap
+```
+
+For Arch Linux, you can install Bubblewrap with the following command:
+
+```bash
+sudo pacman -S bubblewrap
+```
+
+For other Linux distributions, you can install Bubblewrap from the package manager of your choice.
+See [Bubblewrap Installation](https://github.com/containers/bubblewrap#installation) for more details.
+
+
+
## Usage
- Make sure sandbox is enabled in your `config.yml` file.
diff --git a/sandbox/executor/apply.go b/sandbox/executor/apply.go
index e478736..55ba1e1 100644
--- a/sandbox/executor/apply.go
+++ b/sandbox/executor/apply.go
@@ -56,7 +56,12 @@ func ApplySandbox(ctx context.Context, cmd *exec.Cmd, pmName string, opts ...app
policy, err = registry.GetProfile(cfg.SandboxProfileOverride)
if err != nil {
- return nil, fmt.Errorf("failed to load override sandbox policy %s: %w", cfg.SandboxProfileOverride, err)
+ return nil, usefulerror.Useful().
+ WithCode("sandbox_policy_load_failed").
+ WithHumanError(fmt.Sprintf("failed to load override sandbox policy %s: %s", cfg.SandboxProfileOverride, err)).
+ WithHelp("Please check the sandbox profile path and try again.").
+ WithAdditionalHelp("See more at: https://github.com/safedep/pmg/blob/main/docs/sandbox.md").
+ Wrap(fmt.Errorf("failed to load override sandbox policy %s: %w", cfg.SandboxProfileOverride, err))
}
} else {
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
policyRef, exists := cfg.Config.Sandbox.Policies[pmName]
if !exists {
return nil, usefulerror.Useful().
+ WithCode("sandbox_policy_not_configured").
WithHumanError(fmt.Sprintf("no sandbox policy configured for %s", pmName)).
WithHelp("Please configure a sandbox policy for this package manager in the config file.").
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
}
if !sb.IsAvailable() {
- return nil, fmt.Errorf("sandbox %s is required but not available", sb.Name())
+ return nil, usefulerror.Useful().
+ WithCode("sandbox_not_available").
+ WithHumanError(fmt.Sprintf("sandbox %s is required but not available", sb.Name())).
+ WithHelp("Please install the sandbox provider and try again.").
+ WithAdditionalHelp("See more at: https://github.com/safedep/pmg/blob/main/docs/sandbox.md").
+ Wrap(fmt.Errorf("sandbox %s is required but not available", sb.Name()))
}
log.Debugf("Running %s in %s sandbox with policy %s", pmName, sb.Name(), policy.Name)
diff --git a/sandbox/platform/bubblewrap_config_linux.go b/sandbox/platform/bubblewrap_config_linux.go
index ce8ba3e..477f08b 100644
--- a/sandbox/platform/bubblewrap_config_linux.go
+++ b/sandbox/platform/bubblewrap_config_linux.go
@@ -5,7 +5,6 @@ package platform
import (
"os"
- "path/filepath"
)
// bubblewrapConfig contains configuration for Bubblewrap sandbox behavior.
@@ -63,10 +62,6 @@ type bubblewrapConfig struct {
// Seccomp filter configuration
seccomp seccompConfig
- // Mandatory deny file patterns (overrides user policy)
- // These files are always protected regardless of user configuration.
- mandatoryDenyPatterns []string
-
// Maximum depth to scan for mandatory deny patterns (e.g., .env files in subdirectories)
// Set to 0 to only check literal paths, higher values scan subdirectories.
mandatoryDenyScanDepth int
@@ -88,10 +83,8 @@ type seccompConfig struct {
// newDefaultBubblewrapConfig creates a bubblewrap config with safe default values.
// These defaults are based on:
-// - Common Linux filesystem layouts (FHS - Filesystem Hierarchy Standard)
+// - Common Linux filesystem layouts
// - Anthropic Sandbox Runtime implementation patterns
-// - Flatpak's bubblewrap usage
-// - Chrome/Docker seccomp profiles
func newDefaultBubblewrapConfig() *bubblewrapConfig {
return &bubblewrapConfig{
// Essential system paths (read-only)
@@ -169,95 +162,11 @@ func newDefaultBubblewrapConfig() *bubblewrapConfig {
},
},
- // Mandatory deny patterns
- // These files are ALWAYS protected, regardless of user policy
- mandatoryDenyPatterns: getMandatoryDenyPatterns(),
-
// Scan depth for finding dangerous files in project directories
- mandatoryDenyScanDepth: 3, // Check up to 3 levels deep
+ mandatoryDenyScanDepth: 3,
}
}
-// getMandatoryDenyPatterns returns file patterns that must always be denied write access.
-// These patterns protect credentials, secrets, and security-critical files.
-func getMandatoryDenyPatterns() []string {
- home, _ := os.UserHomeDir()
-
- patterns := []string{
- // Environment files (secrets, API keys)
- ".env",
- ".env.*", // .env.local, .env.production, etc.
-
- // Cloud provider credentials
- ".aws", // AWS credentials
- ".aws/**", // AWS config files
- ".gcloud", // Google Cloud credentials
- ".gcloud/**", // GCloud config
- ".azure", // Azure credentials
- ".azure/**", // Azure config
- ".config/gcloud", // Alternative GCloud location
-
- // SSH keys
- ".ssh", // SSH directory
- ".ssh/**", // All SSH files
- "id_rsa", // SSH private key
- "id_ed25519", // Ed25519 SSH key
- "id_ecdsa", // ECDSA SSH key
-
- // GPG/PGP keys
- ".gnupg",
- ".gnupg/**",
-
- // Kubernetes credentials
- ".kube",
- ".kube/**",
- ".kubeconfig",
-
- // Docker credentials
- ".docker/config.json",
-
- // Git security-critical files
- ".git/hooks", // Git hooks (can execute arbitrary code)
- ".git/hooks/**", // All git hooks
- ".gitconfig", // Global git config
-
- // Shell configurations (backdoor risk)
- ".bashrc",
- ".bash_profile",
- ".zshrc",
- ".zprofile",
- ".profile",
-
- // NPM/Node credentials
- ".npmrc", // May contain auth tokens (read-only is fine, write is dangerous)
-
- // Python credentials
- ".pypirc", // PyPI credentials
-
- // Database credentials
- ".pgpass", // PostgreSQL password file
- ".my.cnf", // MySQL config
- ".mysql_history", // MySQL command history (may contain passwords)
-
- // Browser/session data
- ".mozilla",
- ".chrome",
- ".chromium",
-
- // Additional credential stores
- ".netrc", // Network authentication
- ".docker", // Docker configs
- }
-
- // Add home-directory-prefixed versions for absolute path matching
- homePrefixed := make([]string, 0, len(patterns))
- for _, pattern := range patterns {
- homePrefixed = append(homePrefixed, filepath.Join(home, pattern))
- }
-
- return append(patterns, homePrefixed...)
-}
-
// shouldUnshareNetwork determines whether to isolate network based on policy.
// Returns true if network should be completely isolated (--unshare-net).
func (c *bubblewrapConfig) shouldUnshareNetwork(hasAllowRules bool, hasDenyAll bool) bool {
diff --git a/sandbox/platform/bubblewrap_linux.go b/sandbox/platform/bubblewrap_linux.go
index eea4d14..1b3d72f 100644
--- a/sandbox/platform/bubblewrap_linux.go
+++ b/sandbox/platform/bubblewrap_linux.go
@@ -10,6 +10,7 @@ import (
"github.com/safedep/dry/log"
"github.com/safedep/pmg/sandbox"
+ "github.com/safedep/pmg/usefulerror"
)
// bubblewrapSandbox implements the Sandbox interface using Bubblewrap (bwrap) on Linux.
@@ -42,7 +43,15 @@ func newBubblewrapSandbox() (*bubblewrapSandbox, error) {
// This implementation modifies the cmd in place and does NOT execute it.
// Returns ExecutionResult with executed=false, indicating the caller must run cmd.Run().
func (b *bubblewrapSandbox) Execute(ctx context.Context, cmd *exec.Cmd, policy *sandbox.SandboxPolicy) (*sandbox.ExecutionResult, error) {
- // Translate PMG policy to bwrap arguments
+ bwrapPath, err := exec.LookPath("bwrap")
+ if err != nil {
+ return nil, usefulerror.Useful().
+ WithCode("bubblewrap_not_found").
+ WithHumanError("Bubblewrap binary not found").
+ WithHelp("See more at: https://github.com/safedep/pmg/blob/main/docs/sandbox.md").
+ Wrap(fmt.Errorf("bubblewrap binary not found: %w", err))
+ }
+
bwrapArgs, err := b.translator.translate(policy)
if err != nil {
return nil, fmt.Errorf("failed to translate sandbox policy to bubblewrap arguments: %w", err)
@@ -50,16 +59,9 @@ func (b *bubblewrapSandbox) Execute(ctx context.Context, cmd *exec.Cmd, policy *
log.Debugf("Bubblewrap arguments: %v", bwrapArgs)
- // Store original command details
originalPath := cmd.Path
originalArgs := cmd.Args
- // Find bwrap binary
- bwrapPath, err := exec.LookPath("bwrap")
- if err != nil {
- return nil, fmt.Errorf("bubblewrap binary not found: %w (install with: apt install bubblewrap)", err)
- }
-
// Build bwrap command: bwrap [bwrap-args] --
// The "--" separator is important to distinguish bwrap args from command args
cmd.Path = bwrapPath
@@ -81,7 +83,6 @@ func (b *bubblewrapSandbox) Execute(ctx context.Context, cmd *exec.Cmd, policy *
log.Debugf("Sandboxed command: %s %v", cmd.Path, cmd.Args)
- // Return execution result with this sandbox instance for cleanup
return sandbox.NewExecutionResult(sandbox.WithExecutionResultSandbox(b)), nil
}
diff --git a/sandbox/platform/bubblewrap_translator_linux.go b/sandbox/platform/bubblewrap_translator_linux.go
index 8a7ba11..69a40d9 100644
--- a/sandbox/platform/bubblewrap_translator_linux.go
+++ b/sandbox/platform/bubblewrap_translator_linux.go
@@ -158,13 +158,13 @@ func (t *bubblewrapPolicyTranslator) addIsolationNamespaces(policy *sandbox.Sand
// - Paths not mounted are inaccessible (deny-by-default)
//
// Strategy:
-// 1. Start with essential system paths (added separately)
-// 2. Add user-specified allow_read paths FIRST (read-only bind mounts)
-// This establishes the base filesystem view (e.g., "/" for full access)
-// 3. Add user-specified allow_write paths SECOND (read-write bind mounts)
-// These OVERRIDE earlier read-only binds (bwrap: later mounts win)
-// 4. Handle deny patterns by mounting /dev/null or read-only for directories
-// 5. Add mandatory deny patterns
+// 1. Start with essential system paths (added separately)
+// 2. Add user-specified allow_read paths FIRST (read-only bind mounts)
+// This establishes the base filesystem view (e.g., "/" for full access)
+// 3. Add user-specified allow_write paths SECOND (read-write bind mounts)
+// These OVERRIDE earlier read-only binds (bwrap: later mounts win)
+// 4. Handle deny patterns by mounting /dev/null or read-only for directories
+// 5. Add mandatory deny patterns
func (t *bubblewrapPolicyTranslator) translateFilesystem(policy *sandbox.SandboxPolicy) ([]string, error) {
args := []string{}
diff --git a/sandbox/platform/bubblewrap_translator_linux_test.go b/sandbox/platform/bubblewrap_translator_linux_test.go
index 5900cfb..755f741 100644
--- a/sandbox/platform/bubblewrap_translator_linux_test.go
+++ b/sandbox/platform/bubblewrap_translator_linux_test.go
@@ -449,11 +449,6 @@ func TestBubblewrapConfigDefaults(t *testing.T) {
assert.True(t, config.unsharePID)
assert.True(t, config.unshareIPC)
assert.True(t, config.dieWithParent)
-
- // Mandatory deny patterns
- assert.NotEmpty(t, config.mandatoryDenyPatterns)
- assert.Contains(t, config.mandatoryDenyPatterns, ".env")
- assert.Contains(t, config.mandatoryDenyPatterns, ".ssh")
}
func TestBubblewrapConfigEssentialPaths(t *testing.T) {