fix: Refactor bwrap sandbox to use common dangerous files

This commit is contained in:
Abhisek Datta
2026-01-14 21:25:27 +05:30
parent 0eec922a15
commit eeaac7c3bc
6 changed files with 57 additions and 116 deletions
+25
View File
@@ -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
<details>
<summary>Bubblewrap Installation on Linux</summary>
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.
</details>
## Usage
- Make sure sandbox is enabled in your `config.yml` file.
+13 -2
View File
@@ -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)
+2 -93
View File
@@ -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 {
+10 -9
View File
@@ -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] -- <original-command> <original-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
}
@@ -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) {