Skip to content

Commit 5705c09

Browse files
committed
unpriv
1 parent 1e687be commit 5705c09

File tree

8 files changed

+153
-106
lines changed

8 files changed

+153
-106
lines changed

cli/cli.go

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,15 @@ import (
2424
type Config struct {
2525
AllowStrings []string
2626
LogLevel string
27+
Unprivileged bool
2728
}
2829

2930
// NewCommand creates and returns the root serpent command
3031
func NewCommand() *serpent.Command {
3132
// To make the top level jail command, we just make some minor changes to the base command
3233
cmd := BaseCommand()
3334
cmd.Use = "jail [flags] -- command [args...]" // Add the flags and args pieces to usage.
34-
35+
3536
// Add example usage to the long description. This is different from usage as a subcommand because it
3637
// may be called something different when used as a subcommand / there will be a leading binary (i.e. `coder jail` vs. `jail`).
3738
cmd.Long += `Examples:
@@ -43,7 +44,7 @@ func NewCommand() *serpent.Command {
4344
4445
# Block everything by default (implicit)`
4546

46-
return cmd
47+
return cmd
4748
}
4849

4950
// Base command returns the jail serpent command without the information involved in making it the
@@ -74,6 +75,13 @@ user-defined rules.`,
7475
Default: "warn",
7576
Value: serpent.StringOf(&config.LogLevel),
7677
},
78+
{
79+
Name: "unprivileged",
80+
Flag: "unprivileged",
81+
Env: "JAIL_UNPRIVILEGED",
82+
Description: "Use unprivileged mode (proxy environment variables, no sudo required, Linux only).",
83+
Value: serpent.BoolOf(&config.Unprivileged),
84+
},
7785
},
7886
Handler: func(inv *serpent.Invocation) error {
7987
return Run(inv.Context(), config, inv.Args)
@@ -112,7 +120,7 @@ func Run(ctx context.Context, config Config, args []string) error {
112120
auditor := audit.NewLoggingAuditor(logger)
113121

114122
// Create certificate manager
115-
certManager, err := tls.NewCertificateManager(tls.Config{
123+
certManager, err := tls.NewCertificateManager(tls.Options{
116124
Logger: logger,
117125
ConfigDir: userInfo.ConfigDir,
118126
})
@@ -171,30 +179,29 @@ func Run(ctx context.Context, config Config, args []string) error {
171179
return nil
172180
}
173181

182+
// getUserInfo returns information about the current user, handling sudo scenarios
174183
func getUserInfo() namespace.UserInfo {
175-
// get the user info of the original user even if we are running under sudo
176-
sudoUser := os.Getenv("SUDO_USER")
177-
178-
// If running under sudo, get original user information
179-
if sudoUser != "" {
184+
// Only consider SUDO_USER if we're actually running with elevated privileges
185+
// In environments like Coder workspaces, SUDO_USER may be set to 'root'
186+
// but we're not actually running under sudo
187+
if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" && os.Geteuid() == 0 && sudoUser != "root" {
188+
// We're actually running under sudo with a non-root original user
180189
user, err := user.Lookup(sudoUser)
181190
if err != nil {
182-
// Fallback to current user if lookup fails
183-
return getCurrentUserInfo()
191+
return getCurrentUserInfo() // Fallback to current user
184192
}
185193

186-
// Parse SUDO_UID and SUDO_GID
187-
uid := 0
188-
gid := 0
194+
uid, _ := strconv.Atoi(os.Getenv("SUDO_UID"))
195+
gid, _ := strconv.Atoi(os.Getenv("SUDO_GID"))
189196

190-
if sudoUID := os.Getenv("SUDO_UID"); sudoUID != "" {
191-
if parsedUID, err := strconv.Atoi(sudoUID); err == nil {
197+
// If we couldn't get UID/GID from env, parse from user info
198+
if uid == 0 {
199+
if parsedUID, err := strconv.Atoi(user.Uid); err == nil {
192200
uid = parsedUID
193201
}
194202
}
195-
196-
if sudoGID := os.Getenv("SUDO_GID"); sudoGID != "" {
197-
if parsedGID, err := strconv.Atoi(sudoGID); err == nil {
203+
if gid == 0 {
204+
if parsedGID, err := strconv.Atoi(user.Gid); err == nil {
198205
gid = parsedGID
199206
}
200207
}
@@ -210,7 +217,7 @@ func getUserInfo() namespace.UserInfo {
210217
}
211218
}
212219

213-
// Not running under sudo, use current user
220+
// Not actually running under sudo, use current user
214221
return getCurrentUserInfo()
215222
}
216223

jail.go

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ import (
1616
)
1717

1818
type Config struct {
19-
RuleEngine rules.Evaluator
20-
Auditor audit.Auditor
21-
CertManager tls.Manager
22-
Logger *slog.Logger
19+
RuleEngine rules.Evaluator
20+
Auditor audit.Auditor
21+
CertManager tls.Manager
22+
Logger *slog.Logger
23+
Unprivileged bool
2324
}
2425

2526
type Jail struct {
@@ -52,17 +53,9 @@ func New(ctx context.Context, config Config) (*Jail, error) {
5253
Logger: config.Logger,
5354
HttpProxyPort: 8080,
5455
HttpsProxyPort: 8443,
55-
Env: map[string]string{
56-
// Set standard CA certificate environment variables for common tools
57-
// This makes tools like curl, git, etc. trust our dynamically generated CA
58-
"SSL_CERT_FILE": caCertPath, // OpenSSL/LibreSSL-based tools
59-
"SSL_CERT_DIR": configDir, // OpenSSL certificate directory
60-
"CURL_CA_BUNDLE": caCertPath, // curl
61-
"GIT_SSL_CAINFO": caCertPath, // Git
62-
"REQUESTS_CA_BUNDLE": caCertPath, // Python requests
63-
"NODE_EXTRA_CA_CERTS": caCertPath, // Node.js
64-
},
65-
})
56+
TlsConfigDir: configDir,
57+
CACertPath: caCertPath,
58+
}, config.Unprivileged)
6659
if err != nil {
6760
return nil, fmt.Errorf("failed to create commander: %v", err)
6861
}
@@ -118,7 +111,11 @@ func (j *Jail) Close() error {
118111
}
119112

120113
// newNamespaceCommander creates a new namespace instance for the current platform
121-
func newNamespaceCommander(config namespace.Config) (namespace.Commander, error) {
114+
func newNamespaceCommander(config namespace.Config, unprivledged bool) (namespace.Commander, error) {
115+
if unprivledged {
116+
return namespace.NewUnprivileged(config)
117+
}
118+
122119
switch runtime.GOOS {
123120
case "darwin":
124121
return namespace.NewMacOS(config)

namespace/linux.go

Lines changed: 14 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"log/slog"
88
"os"
99
"os/exec"
10-
"strings"
1110
"syscall"
1211
"time"
1312
)
@@ -17,28 +16,20 @@ type Linux struct {
1716
namespace string
1817
vethHost string // Host-side veth interface name for iptables rules
1918
logger *slog.Logger
20-
preparedEnv map[string]string
2119
procAttr *syscall.SysProcAttr
20+
commandEnv []string
2221
httpProxyPort int
2322
httpsProxyPort int
24-
user string
25-
homeDir string
26-
uid int
27-
gid int
23+
tlsConfigDir string
24+
caCertPath string
25+
userInfo UserInfo
2826
}
2927

3028
// NewLinux creates a new Linux network jail instance
3129
func NewLinux(config Config) (*Linux, error) {
32-
// Initialize preparedEnv with config environment variables
33-
preparedEnv := make(map[string]string)
34-
for key, value := range config.Env {
35-
preparedEnv[key] = value
36-
}
37-
3830
return &Linux{
3931
namespace: newNamespaceName(),
4032
logger: config.Logger,
41-
preparedEnv: preparedEnv,
4233
httpProxyPort: config.HttpProxyPort,
4334
httpsProxyPort: config.HttpsProxyPort,
4435
}, nil
@@ -75,28 +66,17 @@ func (l *Linux) Start() error {
7566

7667
// Prepare environment once during setup
7768
l.logger.Debug("Preparing environment")
78-
79-
// Start with current environment
80-
for _, envVar := range os.Environ() {
81-
if parts := strings.SplitN(envVar, "=", 2); len(parts) == 2 {
82-
// Only set if not already set by config
83-
if _, exists := l.preparedEnv[parts[0]]; !exists {
84-
l.preparedEnv[parts[0]] = parts[1]
85-
}
86-
}
87-
}
88-
89-
// Set HOME to original user's home directory
90-
l.preparedEnv["HOME"] = l.homeDir
91-
// Set USER to original username
92-
l.preparedEnv["USER"] = l.user
93-
// Set LOGNAME to original username (some tools check this instead of USER)
94-
l.preparedEnv["LOGNAME"] = l.user
69+
e := getEnvs(l.tlsConfigDir, l.caCertPath)
70+
l.commandEnv = mergeEnvs(e, map[string]string{
71+
"HOME": l.userInfo.HomeDir,
72+
"USER": l.userInfo.Username,
73+
"LOGNAME": l.userInfo.Username,
74+
})
9575

9676
l.procAttr = &syscall.SysProcAttr{
9777
Credential: &syscall.Credential{
98-
Uid: uint32(l.uid),
99-
Gid: uint32(l.gid),
78+
Uid: uint32(l.userInfo.Uid),
79+
Gid: uint32(l.userInfo.Gid),
10080
},
10181
}
10282

@@ -116,12 +96,7 @@ func (l *Linux) Command(command []string) *exec.Cmd {
11696

11797
cmd := exec.Command("ip", cmdArgs[1:]...)
11898

119-
// Use prepared environment from Open method
120-
env := make([]string, 0, len(l.preparedEnv))
121-
for key, value := range l.preparedEnv {
122-
env = append(env, fmt.Sprintf("%s=%s", key, value))
123-
}
124-
cmd.Env = env
99+
cmd.Env = l.commandEnv
125100
cmd.Stdin = os.Stdin
126101
cmd.Stdout = os.Stdout
127102
cmd.Stderr = os.Stderr
@@ -280,4 +255,4 @@ func (l *Linux) removeNamespace() error {
280255
return fmt.Errorf("failed to remove namespace: %v", err)
281256
}
282257
return nil
283-
}
258+
}

namespace/macos.go

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@ type MacOSNetJail struct {
2323
pfRulesPath string
2424
mainRulesPath string
2525
logger *slog.Logger
26-
preparedEnv map[string]string
26+
commandEnv []string
2727
procAttr *syscall.SysProcAttr
2828
httpProxyPort int
2929
httpsProxyPort int
30+
tlsConfigDir string
31+
caCertPath string
3032
userInfo UserInfo
3133
}
3234

@@ -36,17 +38,10 @@ func NewMacOS(config Config) (*MacOSNetJail, error) {
3638
pfRulesPath := fmt.Sprintf("/tmp/%s.pf", ns)
3739
mainRulesPath := fmt.Sprintf("/tmp/%s_main.pf", ns)
3840

39-
// Initialize preparedEnv with config environment variables
40-
preparedEnv := make(map[string]string)
41-
for key, value := range config.Env {
42-
preparedEnv[key] = value
43-
}
44-
4541
return &MacOSNetJail{
4642
pfRulesPath: pfRulesPath,
4743
mainRulesPath: mainRulesPath,
4844
logger: config.Logger,
49-
preparedEnv: preparedEnv,
5045
httpProxyPort: config.HttpProxyPort,
5146
httpsProxyPort: config.HttpsProxyPort,
5247
userInfo: config.UserInfo,
@@ -74,22 +69,12 @@ func (m *MacOSNetJail) Start() error {
7469
// Prepare environment once during setup
7570
m.logger.Debug("Preparing environment")
7671

77-
// Start with current environment
78-
for _, envVar := range os.Environ() {
79-
if parts := strings.SplitN(envVar, "=", 2); len(parts) == 2 {
80-
// Only set if not already set by config
81-
if _, exists := m.preparedEnv[parts[0]]; !exists {
82-
m.preparedEnv[parts[0]] = parts[1]
83-
}
84-
}
85-
}
86-
87-
// Set HOME to original user's home directory
88-
m.preparedEnv["HOME"] = m.userInfo.HomeDir
89-
// Set USER to original username
90-
m.preparedEnv["USER"] = m.userInfo.Username
91-
// Set LOGNAME to original username (some tools check this instead of USER)
92-
m.preparedEnv["LOGNAME"] = m.userInfo.Username
72+
e := getEnvs(m.tlsConfigDir, m.caCertPath)
73+
m.commandEnv = mergeEnvs(e, map[string]string{
74+
"HOME": m.userInfo.HomeDir,
75+
"USER": m.userInfo.Username,
76+
"LOGNAME": m.userInfo.Username,
77+
})
9378

9479
// Prepare process credentials once during setup
9580
m.logger.Debug("Preparing process credentials")
@@ -117,12 +102,7 @@ func (m *MacOSNetJail) Command(command []string) *exec.Cmd {
117102
cmd := exec.Command(command[0], command[1:]...)
118103
m.logger.Debug("Full command args", "args", command)
119104

120-
// Use prepared environment from Open method
121-
env := make([]string, 0, len(m.preparedEnv))
122-
for key, value := range m.preparedEnv {
123-
env = append(env, fmt.Sprintf("%s=%s", key, value))
124-
}
125-
cmd.Env = env
105+
cmd.Env = m.commandEnv
126106
cmd.Stdout = os.Stdout
127107
cmd.Stderr = os.Stderr
128108
cmd.Stdin = os.Stdin

namespace/name.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package namespace
22

33
import (
44
"fmt"
5+
"os"
6+
"strings"
57
"time"
68
)
79

@@ -12,3 +14,41 @@ const (
1214
func newNamespaceName() string {
1315
return fmt.Sprintf("%s_%d", prefix, time.Now().UnixNano()%10000000)
1416
}
17+
18+
func getEnvs(configDir string, caCertPath string) []string {
19+
e := os.Environ()
20+
21+
mergeEnvs(e, map[string]string{
22+
// Set standard CA certificate environment variables for common tools
23+
// This makes tools like curl, git, etc. trust our dynamically generated CA
24+
"SSL_CERT_FILE": caCertPath, // OpenSSL/LibreSSL-based tools
25+
"SSL_CERT_DIR": configDir, // OpenSSL certificate directory
26+
"CURL_CA_BUNDLE": caCertPath, // curl
27+
"GIT_SSL_CAINFO": caCertPath, // Git
28+
"REQUESTS_CA_BUNDLE": caCertPath, // Python requests
29+
"NODE_EXTRA_CA_CERTS": caCertPath, // Node.js
30+
})
31+
32+
return e
33+
}
34+
35+
func mergeEnvs(base []string, extra map[string]string) []string {
36+
envMap := make(map[string]string)
37+
for _, env := range base {
38+
parts := strings.SplitN(env, "=", 2)
39+
if len(parts) == 2 {
40+
envMap[parts[0]] = parts[1]
41+
}
42+
}
43+
44+
for key, value := range extra {
45+
envMap[key] = value
46+
}
47+
48+
merged := make([]string, 0, len(envMap))
49+
for key, value := range envMap {
50+
merged = append(merged, key+"="+value)
51+
}
52+
53+
return merged
54+
}

namespace/namespace.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ type Config struct {
1515
Logger *slog.Logger
1616
HttpProxyPort int
1717
HttpsProxyPort int
18-
Env map[string]string
18+
TlsConfigDir string
19+
CACertPath string
1920
UserInfo UserInfo
2021
}
2122

0 commit comments

Comments
 (0)