mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
fix: Refactor bwrap sandbox to use common dangerous files
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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) {
|
||||
|
||||
Reference in New Issue
Block a user