Skip to content
Open
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
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,14 +161,22 @@ boundary [flags] -- command [args...]
--log-level <LEVEL> Set log level (error, warn, info, debug). Default: warn
--log-dir <DIR> Directory to write logs to (default: stderr)
--proxy-port <PORT> HTTP proxy port (default: 8080)
--upstream-proxy <URL> Forward allowed requests through an upstream HTTP(S) proxy
--pprof Enable pprof profiling server
--pprof-port <PORT> pprof server port (default: 6060)
--disable-audit-logs Disable sending audit logs to the workspace agent
--log-proxy-socket-path <PATH> Path to the audit log socket
-h, --help Print help
```

Environment variables: `BOUNDARY_CONFIG`, `BOUNDARY_ALLOW`, `BOUNDARY_LOG_LEVEL`, `BOUNDARY_LOG_DIR`, `PROXY_PORT`, `BOUNDARY_PPROF`, `BOUNDARY_PPROF_PORT`, `DISABLE_AUDIT_LOGS`, `CODER_AGENT_BOUNDARY_LOG_PROXY_SOCKET_PATH`
Environment variables: `BOUNDARY_CONFIG`, `BOUNDARY_ALLOW`, `BOUNDARY_LOG_LEVEL`, `BOUNDARY_LOG_DIR`, `PROXY_PORT`, `BOUNDARY_UPSTREAM_PROXY`, `BOUNDARY_PPROF`, `BOUNDARY_PPROF_PORT`, `DISABLE_AUDIT_LOGS`, `CODER_AGENT_BOUNDARY_LOG_PROXY_SOCKET_PATH`

When `--upstream-proxy` is set, Boundary still evaluates allow rules and writes audit logs before forwarding allowed requests through the upstream proxy:

```bash
boundary --upstream-proxy http://proxy.corp:3128 \
--allow "domain=github.com" -- curl https://github.com
```

## Development

Expand All @@ -188,4 +196,4 @@ For detailed information about how `boundary` works internally, see [docs/archit

## License

MIT License - see LICENSE file for details.
MIT License - see LICENSE file for details.
11 changes: 11 additions & 0 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ func NewCommand(version string) *serpent.Command {
# Use allowlist from config file with additional CLI allow rules
boundary --allow "domain=example.com" -- curl https://example.com

# Forward allowed requests through a corporate proxy
boundary --upstream-proxy http://proxy.corp:3128 \
--allow "domain=github.com" -- curl https://github.com

# Block everything by default (implicit)

# Enable session correlation inside a Coder workspace
Expand Down Expand Up @@ -122,6 +126,13 @@ func BaseCommand(version string) *serpent.Command {
Value: &cliConfig.ProxyPort,
YAML: "proxy_port",
},
{
Flag: config.UpstreamProxyFlag,
Env: config.UpstreamProxyEnv,
Description: "Forward allowed requests through this upstream HTTP(S) proxy URL.",
Value: &cliConfig.UpstreamProxy,
YAML: "upstream_proxy",
},
{
Flag: "pprof",
Env: "BOUNDARY_PPROF",
Expand Down
124 changes: 124 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ package config

import (
"fmt"
"net"
"net/url"
"path/filepath"
"strconv"
"strings"

"github.com/coder/serpent"
Expand All @@ -16,6 +19,12 @@ type JailType string
const (
NSJailType JailType = "nsjail"
LandjailType JailType = "landjail"

// UpstreamProxyEnv configures Boundary's parent-side forwarding transport.
UpstreamProxyEnv = "BOUNDARY_UPSTREAM_PROXY"

// UpstreamProxyFlag configures Boundary's parent-side forwarding transport.
UpstreamProxyFlag = "upstream-proxy"
)

func NewJailTypeFromString(str string) (JailType, error) {
Expand Down Expand Up @@ -64,6 +73,7 @@ type CliConfig struct {
LogLevel serpent.String `yaml:"log_level"`
LogDir serpent.String `yaml:"log_dir"`
ProxyPort serpent.Int64 `yaml:"proxy_port"`
UpstreamProxy serpent.String `yaml:"upstream_proxy"`
PprofEnabled serpent.Bool `yaml:"pprof_enabled"`
PprofPort serpent.Int64 `yaml:"pprof_port"`
JailType serpent.String `yaml:"jail_type"`
Expand All @@ -83,6 +93,7 @@ type AppConfig struct {
LogLevel string
LogDir string
ProxyPort int64
UpstreamProxy string `json:"-"`
PprofEnabled bool
PprofPort int64
JailType JailType
Expand Down Expand Up @@ -119,6 +130,34 @@ func confinedProcessName(targetCMD []string) string {
return filepath.Base(targetCMD[0])
}

// StripUpstreamProxyArgs removes Boundary's parent-only upstream proxy flag
// before argv is passed to the confined child helper process.
func StripUpstreamProxyArgs(args []string) []string {
stripped := make([]string, 0, len(args))
flag := "--" + UpstreamProxyFlag
flagWithValue := flag + "="

for i := 0; i < len(args); i++ {
arg := args[i]
if arg == "--" {
stripped = append(stripped, args[i:]...)
return stripped
}
if arg == flag {
if i+1 < len(args) {
i++
}
continue
}
if strings.HasPrefix(arg, flagWithValue) {
continue
}
stripped = append(stripped, arg)
}

return stripped
}

func NewAppConfigFromCliConfig(cfg CliConfig, targetCMD []string, environ []string) (AppConfig, error) {
// Merge allowlist from config file with allow from CLI flags
allowListStrings := cfg.AllowListStrings.Value()
Expand All @@ -140,11 +179,17 @@ func NewAppConfigFromCliConfig(cfg CliConfig, targetCMD []string, environ []stri
return AppConfig{}, fmt.Errorf("session correlation config: %w", err)
}

upstreamProxy, err := normalizeUpstreamProxy(cfg.UpstreamProxy.Value(), cfg.ProxyPort.Value())
if err != nil {
return AppConfig{}, err
}

return AppConfig{
AllowRules: allAllowStrings,
LogLevel: cfg.LogLevel.Value(),
LogDir: cfg.LogDir.Value(),
ProxyPort: cfg.ProxyPort.Value(),
UpstreamProxy: upstreamProxy,
PprofEnabled: cfg.PprofEnabled.Value(),
PprofPort: cfg.PprofPort.Value(),
JailType: jailType,
Expand All @@ -159,6 +204,85 @@ func NewAppConfigFromCliConfig(cfg CliConfig, targetCMD []string, environ []stri
}, nil
}

func normalizeUpstreamProxy(raw string, boundaryProxyPort int64) (string, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", nil
}

upstreamURL, err := url.Parse(raw)
if err != nil {
return "", fmt.Errorf("invalid upstream proxy URL")
}
if upstreamURL.Scheme != "http" && upstreamURL.Scheme != "https" {
return "", fmt.Errorf("upstream proxy URL must use http or https scheme")
}
if upstreamURL.Hostname() == "" {
return "", fmt.Errorf("upstream proxy URL must include a host")
}
if hasInvalidPort(upstreamURL) || hasOutOfRangePort(upstreamURL) {
return "", fmt.Errorf("upstream proxy URL has an invalid port")
}
if pointsToBoundaryProxy(upstreamURL, boundaryProxyPort) {
return "", fmt.Errorf("upstream proxy URL must not point to boundary's local proxy listener")
}

return upstreamURL.String(), nil
}

func hasInvalidPort(upstreamURL *url.URL) bool {
if upstreamURL.Port() != "" {
return false
}

host := upstreamURL.Host
if strings.HasPrefix(host, "[") {
bracket := strings.LastIndex(host, "]")
return bracket >= 0 && len(host) > bracket+1
}

return strings.Count(host, ":") > 0
}

func hasOutOfRangePort(upstreamURL *url.URL) bool {
port := upstreamURL.Port()
if port == "" {
return false
}

portNumber, err := strconv.Atoi(port)
return err != nil || portNumber < 1 || portNumber > 65535
}

func pointsToBoundaryProxy(upstreamURL *url.URL, boundaryProxyPort int64) bool {
port := upstreamURL.Port()
if port == "" {
port = defaultPortForScheme(upstreamURL.Scheme)
}
if port == "" || port != strconv.FormatInt(boundaryProxyPort, 10) {
return false
}

host := strings.ToLower(upstreamURL.Hostname())
if host == "localhost" {
return true
}

ip := net.ParseIP(host)
return ip != nil && (ip.IsLoopback() || ip.IsUnspecified())
}

func defaultPortForScheme(scheme string) string {
switch scheme {
case "http":
return "80"
case "https":
return "443"
default:
return ""
}
}

// buildSessionCorrelation merges CLI and YAML inject target sources
// and validates the resulting configuration. Inject targets use the same
// "domain=... path=..." syntax as --allow rules so that matching semantics
Expand Down
Loading