Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,4 @@ Thumbs.db
build/

# Jail binary
./jail
jail
53 changes: 31 additions & 22 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@ import (
type Config struct {
AllowStrings []string
LogLevel string
Unprivileged bool
}

// NewCommand creates and returns the root serpent command
func NewCommand() *serpent.Command {
// To make the top level jail command, we just make some minor changes to the base command
cmd := BaseCommand()
cmd.Use = "jail [flags] -- command [args...]" // Add the flags and args pieces to usage.

// Add example usage to the long description. This is different from usage as a subcommand because it
// may be called something different when used as a subcommand / there will be a leading binary (i.e. `coder jail` vs. `jail`).
cmd.Long += `Examples:
Expand All @@ -43,7 +44,7 @@ func NewCommand() *serpent.Command {

# Block everything by default (implicit)`

return cmd
return cmd
}

// Base command returns the jail serpent command without the information involved in making it the
Expand Down Expand Up @@ -74,6 +75,13 @@ user-defined rules.`,
Default: "warn",
Value: serpent.StringOf(&config.LogLevel),
},
{
Name: "unprivileged",
Flag: "unprivileged",
Env: "JAIL_UNPRIVILEGED",
Description: "Use unprivileged mode (proxy environment variables).",
Value: serpent.BoolOf(&config.Unprivileged),
},
},
Handler: func(inv *serpent.Invocation) error {
return Run(inv.Context(), config, inv.Args)
Expand Down Expand Up @@ -123,10 +131,12 @@ func Run(ctx context.Context, config Config, args []string) error {

// Create jail instance
jailInstance, err := jail.New(ctx, jail.Config{
RuleEngine: ruleEngine,
Auditor: auditor,
CertManager: certManager,
Logger: logger,
RuleEngine: ruleEngine,
Auditor: auditor,
CertManager: certManager,
Logger: logger,
UserInfo: userInfo,
Unprivileged: config.Unprivileged,
})
if err != nil {
return fmt.Errorf("failed to create jail instance: %v", err)
Expand Down Expand Up @@ -171,30 +181,29 @@ func Run(ctx context.Context, config Config, args []string) error {
return nil
}

// getUserInfo returns information about the current user, handling sudo scenarios
func getUserInfo() namespace.UserInfo {
// get the user info of the original user even if we are running under sudo
sudoUser := os.Getenv("SUDO_USER")

// If running under sudo, get original user information
if sudoUser != "" {
// Only consider SUDO_USER if we're actually running with elevated privileges
// In environments like Coder workspaces, SUDO_USER may be set to 'root'
// but we're not actually running under sudo
if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" && os.Geteuid() == 0 && sudoUser != "root" {
// We're actually running under sudo with a non-root original user
user, err := user.Lookup(sudoUser)
if err != nil {
// Fallback to current user if lookup fails
return getCurrentUserInfo()
return getCurrentUserInfo() // Fallback to current user
}

// Parse SUDO_UID and SUDO_GID
uid := 0
gid := 0
uid, _ := strconv.Atoi(os.Getenv("SUDO_UID"))
gid, _ := strconv.Atoi(os.Getenv("SUDO_GID"))

if sudoUID := os.Getenv("SUDO_UID"); sudoUID != "" {
if parsedUID, err := strconv.Atoi(sudoUID); err == nil {
// If we couldn't get UID/GID from env, parse from user info
if uid == 0 {
if parsedUID, err := strconv.Atoi(user.Uid); err == nil {
uid = parsedUID
}
}

if sudoGID := os.Getenv("SUDO_GID"); sudoGID != "" {
if parsedGID, err := strconv.Atoi(sudoGID); err == nil {
if gid == 0 {
if parsedGID, err := strconv.Atoi(user.Gid); err == nil {
gid = parsedGID
}
}
Expand All @@ -210,7 +219,7 @@ func getUserInfo() namespace.UserInfo {
}
}

// Not running under sudo, use current user
// Not actually running under sudo, use current user
return getCurrentUserInfo()
}

Expand Down
43 changes: 20 additions & 23 deletions jail.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ import (
)

type Config struct {
RuleEngine rules.Evaluator
Auditor audit.Auditor
CertManager tls.Manager
Logger *slog.Logger
RuleEngine rules.Evaluator
Auditor audit.Auditor
CertManager tls.Manager
Logger *slog.Logger
UserInfo namespace.UserInfo
Unprivileged bool
}

type Jail struct {
Expand All @@ -40,31 +42,22 @@ func New(ctx context.Context, config Config) (*Jail, error) {
// Create proxy server
proxyServer := proxy.NewProxyServer(proxy.Config{
HTTPPort: 8080,
HTTPSPort: 8443,
Auditor: config.Auditor,
RuleEngine: config.RuleEngine,
Auditor: config.Auditor,
Logger: config.Logger,
TLSConfig: tlsConfig,
})

// Create commander
// Create namespace
commander, err := newNamespaceCommander(namespace.Config{
Logger: config.Logger,
HttpProxyPort: 8080,
HttpsProxyPort: 8443,
Env: map[string]string{
// Set standard CA certificate environment variables for common tools
// This makes tools like curl, git, etc. trust our dynamically generated CA
"SSL_CERT_FILE": caCertPath, // OpenSSL/LibreSSL-based tools
"SSL_CERT_DIR": configDir, // OpenSSL certificate directory
"CURL_CA_BUNDLE": caCertPath, // curl
"GIT_SSL_CAINFO": caCertPath, // Git
"REQUESTS_CA_BUNDLE": caCertPath, // Python requests
"NODE_EXTRA_CA_CERTS": caCertPath, // Node.js
},
})
Logger: config.Logger,
HttpProxyPort: 8080,
TlsConfigDir: configDir,
CACertPath: caCertPath,
UserInfo: config.UserInfo,
}, config.Unprivileged)
if err != nil {
return nil, fmt.Errorf("failed to create commander: %v", err)
return nil, fmt.Errorf("failed to create namespace commander: %v", err)
}

// Create cancellable context for jail
Expand Down Expand Up @@ -118,7 +111,11 @@ func (j *Jail) Close() error {
}

// newNamespaceCommander creates a new namespace instance for the current platform
func newNamespaceCommander(config namespace.Config) (namespace.Commander, error) {
func newNamespaceCommander(config namespace.Config, unprivledged bool) (namespace.Commander, error) {
if unprivledged {
return namespace.NewUnprivileged(config)
}

switch runtime.GOOS {
case "darwin":
return namespace.NewMacOS(config)
Expand Down
96 changes: 29 additions & 67 deletions namespace/linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,44 +7,33 @@ import (
"log/slog"
"os"
"os/exec"
"strings"
"syscall"
"time"
)

// Linux implements jail.Commander using Linux network namespaces
type Linux struct {
namespace string
vethHost string // Host-side veth interface name for iptables rules
logger *slog.Logger
preparedEnv map[string]string
procAttr *syscall.SysProcAttr
httpProxyPort int
httpsProxyPort int
user string
homeDir string
uid int
gid int
logger *slog.Logger
namespace string
vethHost string // Host-side veth interface name for iptables rules
commandEnv []string
httpProxyPort int
tlsConfigDir string
caCertPath string
userInfo UserInfo
}

// NewLinux creates a new Linux network jail instance
func NewLinux(config Config) (*Linux, error) {
// Initialize preparedEnv with config environment variables
preparedEnv := make(map[string]string)
for key, value := range config.Env {
preparedEnv[key] = value
}

return &Linux{
namespace: newNamespaceName(),
logger: config.Logger,
preparedEnv: preparedEnv,
httpProxyPort: config.HttpProxyPort,
httpsProxyPort: config.HttpsProxyPort,
logger: config.Logger,
namespace: newNamespaceName(),
httpProxyPort: config.HttpProxyPort,
tlsConfigDir: config.TlsConfigDir,
caCertPath: config.CACertPath,
userInfo: config.UserInfo,
}, nil
}

// Setup creates network namespace and configures iptables rules
// Start creates network namespace and configures iptables rules
func (l *Linux) Start() error {
l.logger.Debug("Setup called")

Expand Down Expand Up @@ -75,30 +64,12 @@ func (l *Linux) Start() error {

// Prepare environment once during setup
l.logger.Debug("Preparing environment")

// Start with current environment
for _, envVar := range os.Environ() {
if parts := strings.SplitN(envVar, "=", 2); len(parts) == 2 {
// Only set if not already set by config
if _, exists := l.preparedEnv[parts[0]]; !exists {
l.preparedEnv[parts[0]] = parts[1]
}
}
}

// Set HOME to original user's home directory
l.preparedEnv["HOME"] = l.homeDir
// Set USER to original username
l.preparedEnv["USER"] = l.user
// Set LOGNAME to original username (some tools check this instead of USER)
l.preparedEnv["LOGNAME"] = l.user

l.procAttr = &syscall.SysProcAttr{
Credential: &syscall.Credential{
Uid: uint32(l.uid),
Gid: uint32(l.gid),
},
}
e := getEnvs(l.tlsConfigDir, l.caCertPath)
l.commandEnv = mergeEnvs(e, map[string]string{
"HOME": l.userInfo.HomeDir,
"USER": l.userInfo.Username,
"LOGNAME": l.userInfo.Username,
})

l.logger.Debug("Setup completed successfully")
return nil
Expand All @@ -116,23 +87,15 @@ func (l *Linux) Command(command []string) *exec.Cmd {

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

// Use prepared environment from Open method
env := make([]string, 0, len(l.preparedEnv))
for key, value := range l.preparedEnv {
env = append(env, fmt.Sprintf("%s=%s", key, value))
}
cmd.Env = env
cmd.Env = l.commandEnv
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

// Use prepared process attributes from Open method
cmd.SysProcAttr = l.procAttr

return cmd
}

// Cleanup removes the network namespace and iptables rules
// Close removes the network namespace and iptables rules
func (l *Linux) Close() error {
// Remove iptables rules
err := l.removeIptables()
Expand Down Expand Up @@ -246,23 +209,22 @@ func (l *Linux) setupIptables() error {
return fmt.Errorf("failed to add NAT rule: %v", err)
}

// COMPREHENSIVE APPROACH: Intercept ALL TCP traffic from namespace
// Use PREROUTING on host to catch traffic after it exits namespace but before routing
// This ensures NO TCP traffic can bypass the proxy
cmd = exec.Command("iptables", "-t", "nat", "-A", "PREROUTING", "-i", l.vethHost, "-p", "tcp", "-j", "REDIRECT", "--to-ports", fmt.Sprintf("%d", l.httpsProxyPort))
// COMPREHENSIVE APPROACH: Route ALL TCP traffic to HTTP proxy
// The HTTP proxy will intelligently handle both HTTP and TLS traffic
cmd = exec.Command("iptables", "-t", "nat", "-A", "PREROUTING", "-i", l.vethHost, "-p", "tcp", "-j", "REDIRECT", "--to-ports", fmt.Sprintf("%d", l.httpProxyPort))
err = cmd.Run()
if err != nil {
return fmt.Errorf("failed to add comprehensive TCP redirect rule: %v", err)
}

l.logger.Debug("Comprehensive TCP jailing enabled", "interface", l.vethHost, "proxy_port", l.httpsProxyPort)
l.logger.Debug("Comprehensive TCP jailing enabled", "interface", l.vethHost, "proxy_port", l.httpProxyPort)
return nil
}

// removeIptables removes iptables rules
func (l *Linux) removeIptables() error {
// Remove comprehensive TCP redirect rule
cmd := exec.Command("iptables", "-t", "nat", "-D", "PREROUTING", "-i", l.vethHost, "-p", "tcp", "-j", "REDIRECT", "--to-ports", fmt.Sprintf("%d", l.httpsProxyPort))
cmd := exec.Command("iptables", "-t", "nat", "-D", "PREROUTING", "-i", l.vethHost, "-p", "tcp", "-j", "REDIRECT", "--to-ports", fmt.Sprintf("%d", l.httpProxyPort))
cmd.Run() // Ignore errors during cleanup

// Remove NAT rule
Expand All @@ -280,4 +242,4 @@ func (l *Linux) removeNamespace() error {
return fmt.Errorf("failed to remove namespace: %v", err)
}
return nil
}
}
Loading
Loading