mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
fix: Landlock probe environment and autofallback to BW
This commit is contained in:
@@ -0,0 +1,24 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package landlock
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/safedep/pmg/sandbox/platform"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewLandlockProbeCommand returns the hidden Cobra command used to detect
|
||||||
|
// whether the Landlock shim can install seccomp without NO_NEW_PRIVS.
|
||||||
|
func NewLandlockProbeCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "__landlock_probe",
|
||||||
|
Hidden: true,
|
||||||
|
SilenceUsage: true,
|
||||||
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
},
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return platform.RunLandlockProbe()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
//go:build !linux
|
||||||
|
|
||||||
|
package landlock
|
||||||
|
|
||||||
|
import "github.com/spf13/cobra"
|
||||||
|
|
||||||
|
// NewLandlockProbeCommand returns nil on non-Linux platforms.
|
||||||
|
func NewLandlockProbeCommand() *cobra.Command { return nil }
|
||||||
@@ -132,7 +132,11 @@ constant tax that maps to most of the decisions above:
|
|||||||
## Limitations
|
## Limitations
|
||||||
|
|
||||||
- **Unprivileged user namespaces required.** On distros that disable them, `clone()`
|
- **Unprivileged user namespaces required.** On distros that disable them, `clone()`
|
||||||
returns EPERM. We don't yet probe and fall back to bubblewrap (TODO).
|
returns EPERM. PMG probes the shim path at driver-selection time and falls back
|
||||||
|
to Bubblewrap when it cannot work.
|
||||||
|
- **Host LSM policy must allow the user-ns capability path.** Ubuntu AppArmor can allow
|
||||||
|
user namespace creation while denying `CAP_SYS_ADMIN` inside the namespace through the
|
||||||
|
`unprivileged_userns` profile. The shim probe catches this before PMG selects Landlock.
|
||||||
- **Network filtering not enforced.** Landlock V4 does TCP ports, not hostnames. Use
|
- **Network filtering not enforced.** Landlock V4 does TCP ports, not hostnames. Use
|
||||||
proxy-mode.
|
proxy-mode.
|
||||||
- **PID/IPC namespace isolation is best-effort.** Retried without on EPERM.
|
- **PID/IPC namespace isolation is best-effort.** Retried without on EPERM.
|
||||||
|
|||||||
+10
-5
@@ -213,7 +213,9 @@ Next time you run `pmg pnpm install`, the custom policy template will be used in
|
|||||||
<summary>Linux (Landlock, default)</summary>
|
<summary>Linux (Landlock, default)</summary>
|
||||||
|
|
||||||
**Default sandbox on kernel 5.13+**: Landlock provides kernel-native filesystem access control
|
**Default sandbox on kernel 5.13+**: Landlock provides kernel-native filesystem access control
|
||||||
without requiring external binaries or unprivileged user namespaces.
|
without requiring external binaries. PMG's deny-rule enforcement also requires unprivileged
|
||||||
|
user namespaces because the shim needs `CAP_SYS_ADMIN` inside a fresh user namespace to install
|
||||||
|
seccomp-notify without `NO_NEW_PRIVS`.
|
||||||
|
|
||||||
For the architecture, design tradeoffs, and known limitations see
|
For the architecture, design tradeoffs, and known limitations see
|
||||||
[sandbox-landlock.md](./sandbox-landlock.md).
|
[sandbox-landlock.md](./sandbox-landlock.md).
|
||||||
@@ -243,8 +245,11 @@ refuse to run as root (npm's root-in-container warning) are unaffected because t
|
|||||||
outside-view uid never changes.
|
outside-view uid never changes.
|
||||||
|
|
||||||
**Requirements**: unprivileged user namespaces must be enabled (`unprivileged_userns_clone=1`
|
**Requirements**: unprivileged user namespaces must be enabled (`unprivileged_userns_clone=1`
|
||||||
on Debian/Ubuntu; default on most modern distros). If disabled, the helper fails with an
|
on Debian/Ubuntu; default on most modern distros), and any host LSM policy must allow the shim
|
||||||
EPERM on `clone()` and the sandbox falls back to Bubblewrap.
|
to use `CAP_SYS_ADMIN` inside that namespace. PMG probes this at driver-selection time. If the
|
||||||
|
probe fails, for example on Ubuntu AppArmor setups that deny capabilities inside
|
||||||
|
`unprivileged_userns`, the sandbox falls back to Bubblewrap unless
|
||||||
|
`PMG_SANDBOX_DRIVER=landlock` is set.
|
||||||
|
|
||||||
**Network filtering**: Not enforced. Landlock supports TCP port filtering only (V4+, no hostname).
|
**Network filtering**: Not enforced. Landlock supports TCP port filtering only (V4+, no hostname).
|
||||||
Use `--proxy-mode` for network control.
|
Use `--proxy-mode` for network control.
|
||||||
@@ -257,8 +262,8 @@ to force Bubblewrap if namespace isolation is required.
|
|||||||
isolation succeeds, `/proc` is scoped to the child's namespace. When it fails, `/proc`
|
isolation succeeds, `/proc` is scoped to the child's namespace. When it fails, `/proc`
|
||||||
exposes all system processes.
|
exposes all system processes.
|
||||||
|
|
||||||
**Fallback**: If Landlock is unavailable (kernel < 5.13), Bubblewrap is used automatically.
|
**Fallback**: If Landlock is unavailable (kernel < 5.13) or the no-NNP shim probe fails,
|
||||||
Set `PMG_SANDBOX_DRIVER=bubblewrap` to force Bubblewrap.
|
Bubblewrap is used automatically. Set `PMG_SANDBOX_DRIVER=bubblewrap` to force Bubblewrap.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|||||||
@@ -143,6 +143,9 @@ func main() {
|
|||||||
if subcmd := landlockCmd.NewLandlockShimCommand(); subcmd != nil {
|
if subcmd := landlockCmd.NewLandlockShimCommand(); subcmd != nil {
|
||||||
cmd.AddCommand(subcmd)
|
cmd.AddCommand(subcmd)
|
||||||
}
|
}
|
||||||
|
if subcmd := landlockCmd.NewLandlockProbeCommand(); subcmd != nil {
|
||||||
|
cmd.AddCommand(subcmd)
|
||||||
|
}
|
||||||
|
|
||||||
// Print Banner on --help / -h
|
// Print Banner on --help / -h
|
||||||
cmd.SetHelpFunc(func(command *cobra.Command, args []string) {
|
cmd.SetHelpFunc(func(command *cobra.Command, args []string) {
|
||||||
|
|||||||
@@ -33,13 +33,17 @@ type landlockSandbox struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// newLandlockSandbox creates a new Landlock sandbox instance after verifying
|
// newLandlockSandbox creates a new Landlock sandbox instance after verifying
|
||||||
// that both Landlock and seccomp user notification are available on the system.
|
// that Landlock and the no-NNP seccomp shim path are available on the system.
|
||||||
func newLandlockSandbox() (sandbox.Sandbox, error) {
|
func newLandlockSandbox() (sandbox.Sandbox, error) {
|
||||||
abi, err := landlockDetectABI()
|
abi, err := landlockDetectABI()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("landlock not available: %w", err)
|
return nil, fmt.Errorf("landlock not available: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := landlockShimProbe(); err != nil {
|
||||||
|
return nil, fmt.Errorf("landlock shim not available: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
log.Debugf("Landlock ABI V%d detected (Refer=%v, Truncate=%v, Network=%v, IoctlDev=%v, Scoping=%v)",
|
log.Debugf("Landlock ABI V%d detected (Refer=%v, Truncate=%v, Network=%v, IoctlDev=%v, Scoping=%v)",
|
||||||
abi.Version, abi.HasRefer, abi.HasTruncate, abi.HasNetwork, abi.HasIoctlDev, abi.HasScoping)
|
abi.Version, abi.HasRefer, abi.HasTruncate, abi.HasNetwork, abi.HasIoctlDev, abi.HasScoping)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package platform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
var landlockShimProbe = runLandlockShimProbe
|
||||||
|
|
||||||
|
// runLandlockShimProbe verifies that the installed pmg binary can perform the
|
||||||
|
// same critical operation as the real Landlock shim: create a user namespace
|
||||||
|
// and install a seccomp-notify filter without setting NO_NEW_PRIVS.
|
||||||
|
func runLandlockShimProbe() error {
|
||||||
|
selfExe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("resolve self exe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(selfExe, "__landlock_probe")
|
||||||
|
uid := os.Getuid()
|
||||||
|
gid := os.Getgid()
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
Cloneflags: syscall.CLONE_NEWUSER,
|
||||||
|
UidMappings: []syscall.SysProcIDMap{
|
||||||
|
{ContainerID: 0, HostID: uid, Size: 1},
|
||||||
|
},
|
||||||
|
GidMappings: []syscall.SysProcIDMap{
|
||||||
|
{ContainerID: 0, HostID: gid, Size: 1},
|
||||||
|
},
|
||||||
|
GidMappingsEnableSetgroups: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
msg := strings.TrimSpace(string(output))
|
||||||
|
if msg == "" {
|
||||||
|
return fmt.Errorf("run shim probe: %w", err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("run shim probe: %w: %s", err, msg)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunLandlockProbe is the hidden self-reexec entry point used by
|
||||||
|
// runLandlockShimProbe. It intentionally reuses shimInstallSeccomp so the probe
|
||||||
|
// stays coupled to the exact no-NNP seccomp path the real shim needs.
|
||||||
|
func RunLandlockProbe() error {
|
||||||
|
runtime.LockOSThread()
|
||||||
|
defer runtime.UnlockOSThread()
|
||||||
|
|
||||||
|
notifyFd, err := shimInstallSeccomp(false)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("probe: install seccomp: %w", err)
|
||||||
|
}
|
||||||
|
if err := unix.Close(notifyFd); err != nil {
|
||||||
|
return fmt.Errorf("probe: close notify fd: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -10,6 +10,13 @@ import (
|
|||||||
"github.com/safedep/pmg/sandbox"
|
"github.com/safedep/pmg/sandbox"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
landlockSandboxFactory = newLandlockSandbox
|
||||||
|
bubblewrapSandboxFactory = func() (sandbox.Sandbox, error) {
|
||||||
|
return newBubblewrapSandbox()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// NewSandbox creates a platform-specific sandbox instance for Linux.
|
// NewSandbox creates a platform-specific sandbox instance for Linux.
|
||||||
// Prefers Landlock (kernel 5.13+) with seccomp-notify for deny enforcement.
|
// Prefers Landlock (kernel 5.13+) with seccomp-notify for deny enforcement.
|
||||||
// Falls back to Bubblewrap if Landlock or seccomp-notify is unavailable.
|
// Falls back to Bubblewrap if Landlock or seccomp-notify is unavailable.
|
||||||
@@ -20,18 +27,22 @@ func NewSandbox() (sandbox.Sandbox, error) {
|
|||||||
switch os.Getenv("PMG_SANDBOX_DRIVER") {
|
switch os.Getenv("PMG_SANDBOX_DRIVER") {
|
||||||
case "bubblewrap":
|
case "bubblewrap":
|
||||||
log.Debugf("PMG_SANDBOX_DRIVER=bubblewrap: forcing Bubblewrap sandbox")
|
log.Debugf("PMG_SANDBOX_DRIVER=bubblewrap: forcing Bubblewrap sandbox")
|
||||||
return newBubblewrapSandbox()
|
return bubblewrapSandboxFactory()
|
||||||
case "landlock":
|
case "landlock":
|
||||||
log.Debugf("PMG_SANDBOX_DRIVER=landlock: forcing Landlock sandbox")
|
log.Debugf("PMG_SANDBOX_DRIVER=landlock: forcing Landlock sandbox")
|
||||||
return newLandlockSandbox()
|
return landlockSandboxFactory()
|
||||||
}
|
}
|
||||||
|
|
||||||
sb, err := newLandlockSandbox()
|
sb, err := landlockSandboxFactory()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
log.Debugf("Using Landlock sandbox driver (ABI V%d)", sb.(*landlockSandbox).abi.Version)
|
if ll, ok := sb.(*landlockSandbox); ok {
|
||||||
|
log.Debugf("Using Landlock sandbox driver (ABI V%d)", ll.abi.Version)
|
||||||
|
} else {
|
||||||
|
log.Debugf("Using Landlock sandbox driver")
|
||||||
|
}
|
||||||
return sb, nil
|
return sb, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("Landlock not available (%v), falling back to Bubblewrap", err)
|
log.Debugf("Landlock not available (%v), falling back to Bubblewrap", err)
|
||||||
return newBubblewrapSandbox()
|
return bubblewrapSandboxFactory()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package platform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os/exec"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/safedep/pmg/sandbox"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testSandbox struct {
|
||||||
|
name string
|
||||||
|
available bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testSandbox) Name() string {
|
||||||
|
return s.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testSandbox) IsAvailable() bool {
|
||||||
|
return s.available
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testSandbox) Execute(context.Context, *exec.Cmd, *sandbox.SandboxPolicy) (*sandbox.ExecutionResult, error) {
|
||||||
|
return sandbox.NewExecutionResult(sandbox.WithExecutionResultSandbox(s)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testSandbox) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewSandbox_DefaultFallsBackToBubblewrapWhenLandlockUnavailable(t *testing.T) {
|
||||||
|
restoreFactories := replaceSandboxFactories(t,
|
||||||
|
func() (sandbox.Sandbox, error) {
|
||||||
|
return nil, errors.New("landlock shim not available")
|
||||||
|
},
|
||||||
|
func() (sandbox.Sandbox, error) {
|
||||||
|
return &testSandbox{name: "bubblewrap", available: true}, nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
defer restoreFactories()
|
||||||
|
t.Setenv("PMG_SANDBOX_DRIVER", "")
|
||||||
|
|
||||||
|
sb, err := NewSandbox()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "bubblewrap", sb.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewSandbox_DefaultUsesLandlockWhenAvailable(t *testing.T) {
|
||||||
|
restoreFactories := replaceSandboxFactories(t,
|
||||||
|
func() (sandbox.Sandbox, error) {
|
||||||
|
return &testSandbox{name: "landlock", available: true}, nil
|
||||||
|
},
|
||||||
|
func() (sandbox.Sandbox, error) {
|
||||||
|
return nil, errors.New("bubblewrap should not be used")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
defer restoreFactories()
|
||||||
|
t.Setenv("PMG_SANDBOX_DRIVER", "")
|
||||||
|
|
||||||
|
sb, err := NewSandbox()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "landlock", sb.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewSandbox_ForcedLandlockDoesNotFallback(t *testing.T) {
|
||||||
|
bubblewrapCalled := false
|
||||||
|
restoreFactories := replaceSandboxFactories(t,
|
||||||
|
func() (sandbox.Sandbox, error) {
|
||||||
|
return nil, errors.New("landlock shim not available")
|
||||||
|
},
|
||||||
|
func() (sandbox.Sandbox, error) {
|
||||||
|
bubblewrapCalled = true
|
||||||
|
return &testSandbox{name: "bubblewrap", available: true}, nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
defer restoreFactories()
|
||||||
|
t.Setenv("PMG_SANDBOX_DRIVER", "landlock")
|
||||||
|
|
||||||
|
sb, err := NewSandbox()
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Nil(t, sb)
|
||||||
|
assert.False(t, bubblewrapCalled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewSandbox_ForcedBubblewrapSkipsLandlock(t *testing.T) {
|
||||||
|
landlockCalled := false
|
||||||
|
restoreFactories := replaceSandboxFactories(t,
|
||||||
|
func() (sandbox.Sandbox, error) {
|
||||||
|
landlockCalled = true
|
||||||
|
return &testSandbox{name: "landlock", available: true}, nil
|
||||||
|
},
|
||||||
|
func() (sandbox.Sandbox, error) {
|
||||||
|
return &testSandbox{name: "bubblewrap", available: true}, nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
defer restoreFactories()
|
||||||
|
t.Setenv("PMG_SANDBOX_DRIVER", "bubblewrap")
|
||||||
|
|
||||||
|
sb, err := NewSandbox()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "bubblewrap", sb.Name())
|
||||||
|
assert.False(t, landlockCalled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func replaceSandboxFactories(
|
||||||
|
t *testing.T,
|
||||||
|
landlockFactory func() (sandbox.Sandbox, error),
|
||||||
|
bubblewrapFactory func() (sandbox.Sandbox, error),
|
||||||
|
) func() {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
origLandlock := landlockSandboxFactory
|
||||||
|
origBubblewrap := bubblewrapSandboxFactory
|
||||||
|
landlockSandboxFactory = landlockFactory
|
||||||
|
bubblewrapSandboxFactory = bubblewrapFactory
|
||||||
|
|
||||||
|
return func() {
|
||||||
|
landlockSandboxFactory = origLandlock
|
||||||
|
bubblewrapSandboxFactory = origBubblewrap
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user