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, 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. 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 ## Usage
- Make sure sandbox is enabled in your `config.yml` file. - 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) policy, err = registry.GetProfile(cfg.SandboxProfileOverride)
if err != nil { 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 { } else {
log.Debugf("Looking up sandbox policy for %s", pmName) 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] policyRef, exists := cfg.Config.Sandbox.Policies[pmName]
if !exists { if !exists {
return nil, usefulerror.Useful(). return nil, usefulerror.Useful().
WithCode("sandbox_policy_not_configured").
WithHumanError(fmt.Sprintf("no sandbox policy configured for %s", pmName)). WithHumanError(fmt.Sprintf("no sandbox policy configured for %s", pmName)).
WithHelp("Please configure a sandbox policy for this package manager in the config file."). 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."). 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() { 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) 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 ( import (
"os" "os"
"path/filepath"
) )
// bubblewrapConfig contains configuration for Bubblewrap sandbox behavior. // bubblewrapConfig contains configuration for Bubblewrap sandbox behavior.
@@ -63,10 +62,6 @@ type bubblewrapConfig struct {
// Seccomp filter configuration // Seccomp filter configuration
seccomp seccompConfig 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) // 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. // Set to 0 to only check literal paths, higher values scan subdirectories.
mandatoryDenyScanDepth int mandatoryDenyScanDepth int
@@ -88,10 +83,8 @@ type seccompConfig struct {
// newDefaultBubblewrapConfig creates a bubblewrap config with safe default values. // newDefaultBubblewrapConfig creates a bubblewrap config with safe default values.
// These defaults are based on: // These defaults are based on:
// - Common Linux filesystem layouts (FHS - Filesystem Hierarchy Standard) // - Common Linux filesystem layouts
// - Anthropic Sandbox Runtime implementation patterns // - Anthropic Sandbox Runtime implementation patterns
// - Flatpak's bubblewrap usage
// - Chrome/Docker seccomp profiles
func newDefaultBubblewrapConfig() *bubblewrapConfig { func newDefaultBubblewrapConfig() *bubblewrapConfig {
return &bubblewrapConfig{ return &bubblewrapConfig{
// Essential system paths (read-only) // 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 // 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. // shouldUnshareNetwork determines whether to isolate network based on policy.
// Returns true if network should be completely isolated (--unshare-net). // Returns true if network should be completely isolated (--unshare-net).
func (c *bubblewrapConfig) shouldUnshareNetwork(hasAllowRules bool, hasDenyAll bool) bool { 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/dry/log"
"github.com/safedep/pmg/sandbox" "github.com/safedep/pmg/sandbox"
"github.com/safedep/pmg/usefulerror"
) )
// bubblewrapSandbox implements the Sandbox interface using Bubblewrap (bwrap) on Linux. // 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. // This implementation modifies the cmd in place and does NOT execute it.
// Returns ExecutionResult with executed=false, indicating the caller must run cmd.Run(). // 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) { 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) bwrapArgs, err := b.translator.translate(policy)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to translate sandbox policy to bubblewrap arguments: %w", err) 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) log.Debugf("Bubblewrap arguments: %v", bwrapArgs)
// Store original command details
originalPath := cmd.Path originalPath := cmd.Path
originalArgs := cmd.Args 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> // Build bwrap command: bwrap [bwrap-args] -- <original-command> <original-args>
// The "--" separator is important to distinguish bwrap args from command args // The "--" separator is important to distinguish bwrap args from command args
cmd.Path = bwrapPath 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) 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 return sandbox.NewExecutionResult(sandbox.WithExecutionResultSandbox(b)), nil
} }
@@ -158,13 +158,13 @@ func (t *bubblewrapPolicyTranslator) addIsolationNamespaces(policy *sandbox.Sand
// - Paths not mounted are inaccessible (deny-by-default) // - Paths not mounted are inaccessible (deny-by-default)
// //
// Strategy: // Strategy:
// 1. Start with essential system paths (added separately) // 1. Start with essential system paths (added separately)
// 2. Add user-specified allow_read paths FIRST (read-only bind mounts) // 2. Add user-specified allow_read paths FIRST (read-only bind mounts)
// This establishes the base filesystem view (e.g., "/" for full access) // This establishes the base filesystem view (e.g., "/" for full access)
// 3. Add user-specified allow_write paths SECOND (read-write bind mounts) // 3. Add user-specified allow_write paths SECOND (read-write bind mounts)
// These OVERRIDE earlier read-only binds (bwrap: later mounts win) // These OVERRIDE earlier read-only binds (bwrap: later mounts win)
// 4. Handle deny patterns by mounting /dev/null or read-only for directories // 4. Handle deny patterns by mounting /dev/null or read-only for directories
// 5. Add mandatory deny patterns // 5. Add mandatory deny patterns
func (t *bubblewrapPolicyTranslator) translateFilesystem(policy *sandbox.SandboxPolicy) ([]string, error) { func (t *bubblewrapPolicyTranslator) translateFilesystem(policy *sandbox.SandboxPolicy) ([]string, error) {
args := []string{} args := []string{}
@@ -449,11 +449,6 @@ func TestBubblewrapConfigDefaults(t *testing.T) {
assert.True(t, config.unsharePID) assert.True(t, config.unsharePID)
assert.True(t, config.unshareIPC) assert.True(t, config.unshareIPC)
assert.True(t, config.dieWithParent) 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) { func TestBubblewrapConfigEssentialPaths(t *testing.T) {