Skip to content

Commit 992eaac

Browse files
blink-so[bot]f0ssel
andcommitted
Add TLS termination for HTTPS inspection
Implements TLS termination with dynamic certificate generation to enable HTTPS traffic inspection. Supports Linux, macOS, and unprivileged modes with automatic HTTP/HTTPS protocol detection on single port. Key features: - Single-port design with TLS protocol detection - Dynamic certificate generation with CA handling - Cross-platform support (Linux privileged/unprivileged, macOS) - Unified rule engine for both HTTP and HTTPS traffic - Network-level traffic redirection to HTTP proxy with TLS termination HTTPS traffic is now fully inspectable with the same rule engine as HTTP. Co-authored-by: f0ssel <[email protected]>
1 parent 1e687be commit 992eaac

File tree

10 files changed

+534
-286
lines changed

10 files changed

+534
-286
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,4 @@ Thumbs.db
4747
build/
4848

4949
# Jail binary
50-
./jail
50+
jail

cli/cli.go

Lines changed: 31 additions & 22 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).",
83+
Value: serpent.BoolOf(&config.Unprivileged),
84+
},
7785
},
7886
Handler: func(inv *serpent.Invocation) error {
7987
return Run(inv.Context(), config, inv.Args)
@@ -123,10 +131,12 @@ func Run(ctx context.Context, config Config, args []string) error {
123131

124132
// Create jail instance
125133
jailInstance, err := jail.New(ctx, jail.Config{
126-
RuleEngine: ruleEngine,
127-
Auditor: auditor,
128-
CertManager: certManager,
129-
Logger: logger,
134+
RuleEngine: ruleEngine,
135+
Auditor: auditor,
136+
CertManager: certManager,
137+
Logger: logger,
138+
UserInfo: userInfo,
139+
Unprivileged: config.Unprivileged,
130140
})
131141
if err != nil {
132142
return fmt.Errorf("failed to create jail instance: %v", err)
@@ -171,30 +181,29 @@ func Run(ctx context.Context, config Config, args []string) error {
171181
return nil
172182
}
173183

184+
// getUserInfo returns information about the current user, handling sudo scenarios
174185
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 != "" {
186+
// Only consider SUDO_USER if we're actually running with elevated privileges
187+
// In environments like Coder workspaces, SUDO_USER may be set to 'root'
188+
// but we're not actually running under sudo
189+
if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" && os.Geteuid() == 0 && sudoUser != "root" {
190+
// We're actually running under sudo with a non-root original user
180191
user, err := user.Lookup(sudoUser)
181192
if err != nil {
182-
// Fallback to current user if lookup fails
183-
return getCurrentUserInfo()
193+
return getCurrentUserInfo() // Fallback to current user
184194
}
185195

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

190-
if sudoUID := os.Getenv("SUDO_UID"); sudoUID != "" {
191-
if parsedUID, err := strconv.Atoi(sudoUID); err == nil {
199+
// If we couldn't get UID/GID from env, parse from user info
200+
if uid == 0 {
201+
if parsedUID, err := strconv.Atoi(user.Uid); err == nil {
192202
uid = parsedUID
193203
}
194204
}
195-
196-
if sudoGID := os.Getenv("SUDO_GID"); sudoGID != "" {
197-
if parsedGID, err := strconv.Atoi(sudoGID); err == nil {
205+
if gid == 0 {
206+
if parsedGID, err := strconv.Atoi(user.Gid); err == nil {
198207
gid = parsedGID
199208
}
200209
}
@@ -210,7 +219,7 @@ func getUserInfo() namespace.UserInfo {
210219
}
211220
}
212221

213-
// Not running under sudo, use current user
222+
// Not actually running under sudo, use current user
214223
return getCurrentUserInfo()
215224
}
216225

jail.go

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ 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+
UserInfo namespace.UserInfo
24+
Unprivileged bool
2325
}
2426

2527
type Jail struct {
@@ -40,31 +42,22 @@ func New(ctx context.Context, config Config) (*Jail, error) {
4042
// Create proxy server
4143
proxyServer := proxy.NewProxyServer(proxy.Config{
4244
HTTPPort: 8080,
43-
HTTPSPort: 8443,
44-
Auditor: config.Auditor,
4545
RuleEngine: config.RuleEngine,
46+
Auditor: config.Auditor,
4647
Logger: config.Logger,
4748
TLSConfig: tlsConfig,
4849
})
4950

50-
// Create commander
51+
// Create namespace
5152
commander, err := newNamespaceCommander(namespace.Config{
52-
Logger: config.Logger,
53-
HttpProxyPort: 8080,
54-
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-
})
53+
Logger: config.Logger,
54+
HttpProxyPort: 8080,
55+
TlsConfigDir: configDir,
56+
CACertPath: caCertPath,
57+
UserInfo: config.UserInfo,
58+
}, config.Unprivileged)
6659
if err != nil {
67-
return nil, fmt.Errorf("failed to create commander: %v", err)
60+
return nil, fmt.Errorf("failed to create namespace commander: %v", err)
6861
}
6962

7063
// Create cancellable context for jail
@@ -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: 29 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -7,44 +7,33 @@ import (
77
"log/slog"
88
"os"
99
"os/exec"
10-
"strings"
11-
"syscall"
1210
"time"
1311
)
1412

1513
// Linux implements jail.Commander using Linux network namespaces
1614
type Linux struct {
17-
namespace string
18-
vethHost string // Host-side veth interface name for iptables rules
19-
logger *slog.Logger
20-
preparedEnv map[string]string
21-
procAttr *syscall.SysProcAttr
22-
httpProxyPort int
23-
httpsProxyPort int
24-
user string
25-
homeDir string
26-
uid int
27-
gid int
15+
logger *slog.Logger
16+
namespace string
17+
vethHost string // Host-side veth interface name for iptables rules
18+
commandEnv []string
19+
httpProxyPort int
20+
tlsConfigDir string
21+
caCertPath string
22+
userInfo UserInfo
2823
}
2924

30-
// NewLinux creates a new Linux network jail instance
3125
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-
3826
return &Linux{
39-
namespace: newNamespaceName(),
40-
logger: config.Logger,
41-
preparedEnv: preparedEnv,
42-
httpProxyPort: config.HttpProxyPort,
43-
httpsProxyPort: config.HttpsProxyPort,
27+
logger: config.Logger,
28+
namespace: newNamespaceName(),
29+
httpProxyPort: config.HttpProxyPort,
30+
tlsConfigDir: config.TlsConfigDir,
31+
caCertPath: config.CACertPath,
32+
userInfo: config.UserInfo,
4433
}, nil
4534
}
4635

47-
// Setup creates network namespace and configures iptables rules
36+
// Start creates network namespace and configures iptables rules
4837
func (l *Linux) Start() error {
4938
l.logger.Debug("Setup called")
5039

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

7665
// Prepare environment once during setup
7766
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
95-
96-
l.procAttr = &syscall.SysProcAttr{
97-
Credential: &syscall.Credential{
98-
Uid: uint32(l.uid),
99-
Gid: uint32(l.gid),
100-
},
101-
}
67+
e := getEnvs(l.tlsConfigDir, l.caCertPath)
68+
l.commandEnv = mergeEnvs(e, map[string]string{
69+
"HOME": l.userInfo.HomeDir,
70+
"USER": l.userInfo.Username,
71+
"LOGNAME": l.userInfo.Username,
72+
})
10273

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

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

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
90+
cmd.Env = l.commandEnv
12591
cmd.Stdin = os.Stdin
12692
cmd.Stdout = os.Stdout
12793
cmd.Stderr = os.Stderr
12894

129-
// Use prepared process attributes from Open method
130-
cmd.SysProcAttr = l.procAttr
131-
13295
return cmd
13396
}
13497

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

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

258-
l.logger.Debug("Comprehensive TCP jailing enabled", "interface", l.vethHost, "proxy_port", l.httpsProxyPort)
220+
l.logger.Debug("Comprehensive TCP jailing enabled", "interface", l.vethHost, "proxy_port", l.httpProxyPort)
259221
return nil
260222
}
261223

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

268230
// Remove NAT rule
@@ -280,4 +242,4 @@ func (l *Linux) removeNamespace() error {
280242
return fmt.Errorf("failed to remove namespace: %v", err)
281243
}
282244
return nil
283-
}
245+
}

0 commit comments

Comments
 (0)