diff --git a/README.md b/README.md index cea4cbe..087f186 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,43 @@ jail \ jail -- curl https://example.com ``` +### Unprivileged Mode (NEW!) + +jail now supports running without elevated privileges using the `--unprivileged` flag: + +```bash +# No sudo required! +jail --unprivileged --allow "github.com" -- curl https://github.com + +# Works with complex applications +jail --unprivileged --allow "*.npmjs.org" -- npm install + +# Same rule engine and proxy functionality as privileged mode +jail --unprivileged --allow "api.example.com" -- ./my-app +``` + +**⚠️ Important: Run as regular user, NOT with sudo** +```bash +# ✅ CORRECT - Run as regular user +jail --unprivileged --allow "github.com" -- curl https://github.com + +# ❌ WRONG - Don't use sudo with --unprivileged +sudo jail --unprivileged --allow "github.com" -- curl https://github.com + +# ✅ For privileged mode, use sudo WITHOUT --unprivileged +sudo jail --allow "github.com" -- curl https://github.com +``` + +**Requirements for Unprivileged Mode:** +- Linux (kernel 2.6+) +- Applications that respect proxy environment variables (HTTP_PROXY, HTTPS_PROXY) + +**Benefits:** +- ✅ **No sudo required** - Runs as regular user +- ✅ **No external dependencies** - Uses built-in proxy environment variables +- ✅ **Container-friendly** - Works in restricted environments +- ✅ **Same rule engine** - Identical allow/block logic as privileged mode + ## Allow Rules jail uses simple wildcard patterns for URL matching. @@ -121,10 +158,37 @@ For more help: https://github.com/coder/jail ## Platform Support | Platform | Implementation | Sudo Required | -|----------|----------------|---------------| -| Linux | Network namespaces + iptables | Yes | -| macOS | Process groups + PF rules | Yes | -| Windows | Not supported | - | +|----------|----------------|--------------| +| Linux | Network namespaces + iptables | Yes | +| **Linux (Unprivileged)** | **Proxy environment variables** | **No** | +| macOS | Process groups + PF rules | Yes | +| Windows | Not supported | - | + +## Troubleshooting Unprivileged Mode + +### "permission denied" for `/root/.config` +```bash +Error: failed to create certificate manager: failed to create config directory at /root/.config/coder_jail: mkdir /root/.config: permission denied +``` +**Solution**: Don't use `sudo` with `--unprivileged`. Run as regular user: +```bash +# ❌ Wrong +sudo jail --unprivileged --allow "github.com" -- curl https://github.com + +# ✅ Correct +jail --unprivileged --allow "github.com" -- curl https://github.com +``` + +### Applications not respecting proxy settings +```bash +# Some applications may ignore proxy environment variables +# Check your application's documentation for proxy configuration +``` +**Solution**: Use privileged mode for applications that don't respect proxy environment variables: +```bash +# For apps that ignore HTTP_PROXY/HTTPS_PROXY +sudo jail --allow "github.com" -- your-app +``` ## Installation @@ -174,6 +238,7 @@ OPTIONS: --allow Allow rule (repeatable) Format: "pattern" or "METHOD[,METHOD] pattern" --log-level Set log level (error, warn, info, debug) + --unprivileged Use unprivileged mode (no sudo required, Linux only) --no-tls-intercept Disable HTTPS interception -h, --help Print help ``` diff --git a/cli/cli.go b/cli/cli.go index 88ee226..ac6b444 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -8,6 +8,7 @@ import ( "os/signal" "os/user" "path/filepath" + "runtime" "strconv" "strings" "syscall" @@ -24,6 +25,7 @@ import ( type Config struct { AllowStrings []string LogLevel string + Unprivileged bool // Enable unprivileged mode (user namespace + iptables) } // NewCommand creates and returns the root serpent command @@ -37,14 +39,22 @@ func NewCommand() *serpent.Command { intercepting all HTTP/HTTPS traffic through a transparent proxy that enforces user-defined rules. +Modes: + Default (privileged): Uses network namespaces + iptables (requires sudo) + Unprivileged: Uses proxy environment variables (no sudo required) + Examples: - # Allow only requests to github.com - jail --allow "github.com" -- curl https://github.com + # Privileged mode (original behavior) + sudo jail --allow "github.com" -- curl https://github.com + + # Unprivileged mode (NEW!) + jail --unprivileged --allow "github.com" -- curl https://github.com - # Monitor all requests to specific domains (allow only those) - jail --allow "github.com/api/issues/*" --allow "GET,HEAD github.com" -- npm install + # Monitor all requests to specific domains + jail --unprivileged --allow "github.com/api/issues/*" --allow "GET,HEAD github.com" -- npm install - # Block everything by default (implicit)`, + # Block everything by default (implicit) + jail --unprivileged --allow "api.example.com" -- ./my-app`, Options: serpent.OptionSet{ { Name: "allow", @@ -61,6 +71,13 @@ Examples: Default: "warn", Value: serpent.StringOf(&config.LogLevel), }, + { + Name: "unprivileged", + Flag: "unprivileged", + Env: "JAIL_UNPRIVILEGED", + Description: "Use unprivileged mode (proxy environment variables, no sudo required, Linux only).", + Value: serpent.BoolOf(&config.Unprivileged), + }, }, Handler: func(inv *serpent.Invocation) error { return Run(inv.Context(), config, inv.Args) @@ -75,6 +92,20 @@ func Run(ctx context.Context, config Config, args []string) error { logger := setupLogging(config.LogLevel) userInfo := getUserInfo() + // Validate unprivileged mode if requested + if config.Unprivileged { + // Warn if running as root but don't block it (some container environments need this) + if os.Geteuid() == 0 { + logger.Warn("Running unprivileged mode as root - this may cause permission issues with config files") + } + if err := validateUnprivilegedMode(logger); err != nil { + return fmt.Errorf("unprivileged mode validation failed: %v", err) + } + logger.Info("Using unprivileged mode (proxy environment variables, no sudo required)") + } else { + logger.Info("Using privileged mode (network namespace + iptables, requires sudo)") + } + // Get command arguments if len(args) == 0 { return fmt.Errorf("no command specified") @@ -110,10 +141,11 @@ 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, + Unprivileged: config.Unprivileged, }) if err != nil { return fmt.Errorf("failed to create jail instance: %v", err) @@ -158,30 +190,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 } } @@ -197,7 +228,7 @@ func getUserInfo() namespace.UserInfo { } } - // Not running under sudo, use current user + // Not actually running under sudo, use current user return getCurrentUserInfo() } @@ -225,7 +256,6 @@ func setupLogging(logLevel string) *slog.Logger { return slog.New(handler) } -// getCurrentUserInfo gets information for the current user func getCurrentUserInfo() namespace.UserInfo { currentUser, err := user.Current() if err != nil { @@ -255,3 +285,14 @@ func getConfigDir(homeDir string) string { } return filepath.Join(homeDir, ".config", "coder_jail") } + +// validateUnprivilegedMode checks if the system supports unprivileged mode +func validateUnprivilegedMode(logger *slog.Logger) error { + // Check if we're on Linux + if runtime.GOOS != "linux" { + return fmt.Errorf("unprivileged mode only supports Linux, got: %s", runtime.GOOS) + } + + logger.Debug("Unprivileged mode validation passed") + return nil +} \ No newline at end of file diff --git a/go.mod b/go.mod index c7db86c..c847cf1 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,9 @@ require ( cdr.dev/slog v1.6.2-0.20240126064726-20367d4aede6 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 // indirect + github.com/gofrs/flock v0.8.1 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/gorilla/mux v1.8.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/kr/text v0.2.0 // indirect @@ -15,17 +18,21 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/moby/sys/mountinfo v0.6.2 // indirect github.com/muesli/termenv v0.15.2 // indirect github.com/pion/transport/v2 v2.0.0 // indirect github.com/pion/udp v0.1.4 // indirect github.com/rivo/uniseg v0.4.4 // indirect + github.com/rootless-containers/rootlesskit v1.1.1 // indirect + github.com/sirupsen/logrus v1.9.2 // indirect github.com/spf13/pflag v1.0.5 // indirect go.opentelemetry.io/otel v1.19.0 // indirect go.opentelemetry.io/otel/trace v1.19.0 // indirect - golang.org/x/crypto v0.19.0 // indirect + golang.org/x/crypto v0.36.0 // indirect golang.org/x/exp v0.0.0-20240213143201-ec583247a57a // indirect - golang.org/x/sys v0.17.0 // indirect - golang.org/x/term v0.17.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/term v0.30.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index d751167..d0b660d 100644 --- a/go.sum +++ b/go.sum @@ -25,10 +25,14 @@ github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -48,6 +52,8 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= +github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= @@ -67,11 +73,16 @@ github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rootless-containers/rootlesskit v1.1.1 h1:F5psKWoWY9/VjZ3ifVcaosjvFZJOagX85U22M0/EQZE= +github.com/rootless-containers/rootlesskit v1.1.1/go.mod h1:UD5GoA3dqKCJrnvnhVgQQnweMF2qZnf9KLw8EewcMZI= +github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= +github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -88,8 +99,8 @@ go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1 go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE= golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -97,31 +108,32 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/jail b/jail new file mode 100755 index 0000000..1a50065 Binary files /dev/null and b/jail differ diff --git a/jail.go b/jail.go index 0b2a4b5..f7fa2c1 100644 --- a/jail.go +++ b/jail.go @@ -16,10 +16,11 @@ 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 + Unprivileged bool // Enable unprivileged mode (user namespace + iptables) } type Jail struct { @@ -31,6 +32,11 @@ type Jail struct { } func New(ctx context.Context, config Config) (*Jail, error) { + // Validate unprivileged mode if requested + if config.Unprivileged && runtime.GOOS != "linux" { + return nil, fmt.Errorf("unprivileged mode only supports Linux, got: %s", runtime.GOOS) + } + // Setup TLS config and write CA certificate to file tlsConfig, caCertPath, configDir, err := config.CertManager.SetupTLSAndWriteCACert() if err != nil { @@ -62,7 +68,7 @@ func New(ctx context.Context, config Config) (*Jail, error) { "REQUESTS_CA_BUNDLE": caCertPath, // Python requests "NODE_EXTRA_CA_CERTS": caCertPath, // Node.js }, - }) + }, config.Unprivileged) if err != nil { return nil, fmt.Errorf("failed to create commander: %v", err) } @@ -118,13 +124,20 @@ 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, unprivileged bool) (namespace.Commander, error) { switch runtime.GOOS { case "darwin": + if unprivileged { + return nil, fmt.Errorf("unprivileged mode not available on macOS") + } return namespace.NewMacOS(config) case "linux": - return namespace.NewLinux(config) + if unprivileged { + return namespace.NewUserNamespaceLinux(config) + } else { + return namespace.NewLinux(config) + } default: return nil, fmt.Errorf("unsupported platform: %s", runtime.GOOS) } -} +} \ No newline at end of file diff --git a/namespace/user_namespace_linux.go b/namespace/user_namespace_linux.go new file mode 100644 index 0000000..b525fa1 --- /dev/null +++ b/namespace/user_namespace_linux.go @@ -0,0 +1,82 @@ +//go:build linux + +package namespace + +import ( + "fmt" + "log/slog" + "os" + "os/exec" + "strings" +) + +// UserNamespaceLinux implements Commander using simple proxy environment variables +type UserNamespaceLinux struct { + logger *slog.Logger + preparedEnv map[string]string + httpProxyPort int + httpsProxyPort int + userInfo UserInfo +} + +// NewUserNamespaceLinux creates a simple unprivileged jail that sets proxy variables +func NewUserNamespaceLinux(config Config) (*UserNamespaceLinux, error) { + preparedEnv := make(map[string]string) + for key, value := range config.Env { + preparedEnv[key] = value + } + + // Add proxy environment variables + httpProxy := fmt.Sprintf("http://127.0.0.1:%d", config.HttpProxyPort) + httpsProxy := fmt.Sprintf("http://127.0.0.1:%d", config.HttpsProxyPort) + + // Set both uppercase and lowercase proxy variables for maximum compatibility + preparedEnv["HTTP_PROXY"] = httpProxy + preparedEnv["http_proxy"] = httpProxy + preparedEnv["HTTPS_PROXY"] = httpsProxy + preparedEnv["https_proxy"] = httpsProxy + + return &UserNamespaceLinux{ + logger: config.Logger, + preparedEnv: preparedEnv, + httpProxyPort: config.HttpProxyPort, + httpsProxyPort: config.HttpsProxyPort, + userInfo: config.UserInfo, + }, nil +} + +func (u *UserNamespaceLinux) Start() error { + u.logger.Info("Unprivileged jail using proxy environment variables") + return nil +} + +func (u *UserNamespaceLinux) Command(command []string) *exec.Cmd { + u.logger.Debug("Creating command with proxy environment", "command", command) + + cmd := exec.Command(command[0], command[1:]...) + + // Build environment with proxy variables + env := make([]string, 0, len(u.preparedEnv)+10) + for key, value := range u.preparedEnv { + env = append(env, fmt.Sprintf("%s=%s", key, value)) + } + for _, envVar := range os.Environ() { + if parts := strings.SplitN(envVar, "=", 2); len(parts) == 2 { + key := parts[0] + if _, exists := u.preparedEnv[key]; !exists { + env = append(env, envVar) + } + } + } + + cmd.Env = env + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd +} + +func (u *UserNamespaceLinux) Close() error { + u.logger.Info("Closing unprivileged jail") + return nil +} \ No newline at end of file diff --git a/namespace/user_namespace_stub.go b/namespace/user_namespace_stub.go new file mode 100644 index 0000000..30f1010 --- /dev/null +++ b/namespace/user_namespace_stub.go @@ -0,0 +1,10 @@ +//go:build !linux + +package namespace + +import "fmt" + +// NewUserNamespaceLinux is not available on non-Linux platforms +func NewUserNamespaceLinux(config Config) (Commander, error) { + return nil, fmt.Errorf("user namespace jail not available on this platform") +}