Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/coder/boundary/config"
"github.com/coder/boundary/log"
"github.com/coder/boundary/privilege"
"github.com/coder/boundary/run"
"github.com/coder/coder/v2/agent/boundarylogproxy"
"github.com/coder/serpent"
Expand Down Expand Up @@ -173,6 +174,14 @@ func BaseCommand(version string) *serpent.Command {
return fmt.Errorf("failed to parse cli config file: %v", err)
}

// Ensure we have the necessary privileges only if using nsjail
// (landjail doesn't require the same privileges)
if appConfig.JailType == config.NSJailType {
if err := privilege.EnsurePrivileges(); err != nil {
return fmt.Errorf("failed to ensure privileges: %v", err)
}
}

// Get command arguments
if len(appConfig.TargetCMD) == 0 {
return fmt.Errorf("no command specified")
Expand Down
98 changes: 98 additions & 0 deletions privilege/privilege_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
//go:build linux

package privilege

import (
"fmt"
"os"
"os/exec"
"os/user"
"strconv"
"syscall"
)

// EnsurePrivileges ensures the process has the necessary privileges (CAP_NET_ADMIN and optionally CAP_SYS_ADMIN).
// If not running with sufficient privileges, it re-executes itself with sudo + setpriv.
// This function should be called early in main() before any privileged operations.
// Assumes the process is always started as a regular user.
func EnsurePrivileges() error {
// Check if we're already in the process of privilege escalation (to prevent infinite loops)
if os.Getenv("BOUNDARY_PRIV_ESCALATED") == "1" {
// We've already escalated, continue
return nil
}

// If we're already root, something went wrong (we shouldn't be root as a regular user)
// But continue anyway to avoid breaking existing setups
if os.Geteuid() == 0 {
return nil
}

// Not root, need to re-exec with sudo + setpriv
return reExecWithPrivileges()
}

// reExecWithPrivileges re-executes the current binary with sudo + setpriv
func reExecWithPrivileges() error {
// Find sudo binary
sudoPath, err := exec.LookPath("sudo")
if err != nil {
return fmt.Errorf("sudo not found in PATH. Please run with sudo or install sudo: %w", err)
}

// Find setpriv binary
setprivPath, err := exec.LookPath("setpriv")
if err != nil {
return fmt.Errorf("setpriv not found in PATH. Please install util-linux: %w", err)
}

// Get current user
currentUser, err := user.Current()
if err != nil {
return fmt.Errorf("failed to get current user: %w", err)
}

uid, err := strconv.Atoi(currentUser.Uid)
if err != nil {
return fmt.Errorf("failed to parse UID: %w", err)
}

gid, err := strconv.Atoi(currentUser.Gid)
if err != nil {
return fmt.Errorf("failed to parse GID: %w", err)
}

// Get current binary path
binaryPath, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to get executable path: %w", err)
}

// Get current args (skip program name)
args := os.Args[1:]

// Build sudo command: sudo -E env PATH=$PATH setpriv --reuid=UID --regid=GID --clear-groups --inh-caps=+net_admin,+sys_admin --ambient-caps=+net_admin,+sys_admin binary args...
cmd := exec.Command(sudoPath,
"-E",
"env",
"PATH="+os.Getenv("PATH"),
setprivPath,
"--reuid", strconv.Itoa(uid),
"--regid", strconv.Itoa(gid),
"--clear-groups",
"--inh-caps", "+net_admin,+sys_admin",
"--ambient-caps", "+net_admin,+sys_admin",
binaryPath,
)
cmd.Args = append(cmd.Args, args...)
env := os.Environ()
env = append(env, "BOUNDARY_PRIV_ESCALATED=1")
cmd.Env = env
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

// Execute and replace current process
return syscall.Exec(cmd.Path, cmd.Args, cmd.Env)
}

14 changes: 14 additions & 0 deletions privilege/privilege_stub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//go:build !linux

package privilege

import (
"fmt"
"runtime"
)

// EnsurePrivileges is a no-op on non-Linux platforms.
func EnsurePrivileges() error {
return fmt.Errorf("boundary is only supported on Linux, current platform: %s", runtime.GOOS)
}

3 changes: 3 additions & 0 deletions run/run.go → run/run_linux.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build linux

package run

import (
Expand All @@ -20,3 +22,4 @@ func Run(ctx context.Context, logger *slog.Logger, cfg config.AppConfig) error {
return fmt.Errorf("unknown jail type: %s", cfg.JailType)
}
}

17 changes: 17 additions & 0 deletions run/run_stub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//go:build !linux

package run

import (
"context"
"fmt"
"log/slog"
"runtime"

"github.com/coder/boundary/config"
)

func Run(ctx context.Context, logger *slog.Logger, cfg config.AppConfig) error {
return fmt.Errorf("boundary is only supported on Linux, current platform: %s", runtime.GOOS)
}

Loading