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,
|
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.
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user