Skip to content

Commit 8a10f19

Browse files
feat: handle privilige escalation in boundary
1 parent e36b6e0 commit 8a10f19

3 files changed

Lines changed: 121 additions & 0 deletions

File tree

cli/cli.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/coder/boundary/config"
1010
"github.com/coder/boundary/log"
11+
"github.com/coder/boundary/privilege"
1112
"github.com/coder/boundary/run"
1213
"github.com/coder/coder/v2/agent/boundarylogproxy"
1314
"github.com/coder/serpent"
@@ -173,6 +174,14 @@ func BaseCommand(version string) *serpent.Command {
173174
return fmt.Errorf("failed to parse cli config file: %v", err)
174175
}
175176

177+
// Ensure we have the necessary privileges only if using nsjail
178+
// (landjail doesn't require the same privileges)
179+
if appConfig.JailType == config.NSJailType {
180+
if err := privilege.EnsurePrivileges(); err != nil {
181+
return fmt.Errorf("failed to ensure privileges: %v", err)
182+
}
183+
}
184+
176185
// Get command arguments
177186
if len(appConfig.TargetCMD) == 0 {
178187
return fmt.Errorf("no command specified")

privilege/privilege_linux.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
//go:build linux
2+
3+
package privilege
4+
5+
import (
6+
"fmt"
7+
"os"
8+
"os/exec"
9+
"os/user"
10+
"strconv"
11+
"syscall"
12+
)
13+
14+
// EnsurePrivileges ensures the process has the necessary privileges (CAP_NET_ADMIN and optionally CAP_SYS_ADMIN).
15+
// If not running with sufficient privileges, it re-executes itself with sudo + setpriv.
16+
// This function should be called early in main() before any privileged operations.
17+
// Assumes the process is always started as a regular user.
18+
func EnsurePrivileges() error {
19+
// Check if we're already in the process of privilege escalation (to prevent infinite loops)
20+
if os.Getenv("BOUNDARY_PRIV_ESCALATED") == "1" {
21+
// We've already escalated, continue
22+
return nil
23+
}
24+
25+
// If we're already root, something went wrong (we shouldn't be root as a regular user)
26+
// But continue anyway to avoid breaking existing setups
27+
if os.Geteuid() == 0 {
28+
return nil
29+
}
30+
31+
// Not root, need to re-exec with sudo + setpriv
32+
return reExecWithPrivileges()
33+
}
34+
35+
// reExecWithPrivileges re-executes the current binary with sudo + setpriv
36+
func reExecWithPrivileges() error {
37+
// Find sudo binary
38+
sudoPath, err := exec.LookPath("sudo")
39+
if err != nil {
40+
return fmt.Errorf("sudo not found in PATH. Please run with sudo or install sudo: %w", err)
41+
}
42+
43+
// Find setpriv binary
44+
setprivPath, err := exec.LookPath("setpriv")
45+
if err != nil {
46+
return fmt.Errorf("setpriv not found in PATH. Please install util-linux: %w", err)
47+
}
48+
49+
// Get current user
50+
currentUser, err := user.Current()
51+
if err != nil {
52+
return fmt.Errorf("failed to get current user: %w", err)
53+
}
54+
55+
uid, err := strconv.Atoi(currentUser.Uid)
56+
if err != nil {
57+
return fmt.Errorf("failed to parse UID: %w", err)
58+
}
59+
60+
gid, err := strconv.Atoi(currentUser.Gid)
61+
if err != nil {
62+
return fmt.Errorf("failed to parse GID: %w", err)
63+
}
64+
65+
// Get current binary path
66+
binaryPath, err := os.Executable()
67+
if err != nil {
68+
return fmt.Errorf("failed to get executable path: %w", err)
69+
}
70+
71+
// Get current args (skip program name)
72+
args := os.Args[1:]
73+
74+
// 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...
75+
cmd := exec.Command(sudoPath,
76+
"-E",
77+
"env",
78+
"PATH="+os.Getenv("PATH"),
79+
setprivPath,
80+
"--reuid", strconv.Itoa(uid),
81+
"--regid", strconv.Itoa(gid),
82+
"--clear-groups",
83+
"--inh-caps", "+net_admin,+sys_admin",
84+
"--ambient-caps", "+net_admin,+sys_admin",
85+
binaryPath,
86+
)
87+
cmd.Args = append(cmd.Args, args...)
88+
env := os.Environ()
89+
env = append(env, "BOUNDARY_PRIV_ESCALATED=1")
90+
cmd.Env = env
91+
cmd.Stdin = os.Stdin
92+
cmd.Stdout = os.Stdout
93+
cmd.Stderr = os.Stderr
94+
95+
// Execute and replace current process
96+
return syscall.Exec(cmd.Path, cmd.Args, cmd.Env)
97+
}
98+

privilege/privilege_stub.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//go:build !linux
2+
3+
package privilege
4+
5+
import (
6+
"fmt"
7+
"runtime"
8+
)
9+
10+
// EnsurePrivileges is a no-op on non-Linux platforms.
11+
func EnsurePrivileges() error {
12+
return fmt.Errorf("boundary is only supported on Linux, current platform: %s", runtime.GOOS)
13+
}
14+

0 commit comments

Comments
 (0)