diff --git a/cli/exp_boundary.go b/cli/exp_boundary.go
deleted file mode 100644
index a465e06eda..0000000000
--- a/cli/exp_boundary.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package cli
-
-import (
- boundarycli "github.com/coder/boundary/cli"
- "github.com/coder/serpent"
-)
-
-func (*RootCmd) boundary() *serpent.Command {
- cmd := boundarycli.BaseCommand() // Package coder/boundary/cli exports a "base command" designed to be integrated as a subcommand.
- cmd.Use += " [args...]" // The base command looks like `boundary -- command`. Serpent adds the flags piece, but we need to add the args.
- return cmd
-}
diff --git a/cli/exp_boundary_test.go b/cli/exp_boundary_test.go
deleted file mode 100644
index 228214e465..0000000000
--- a/cli/exp_boundary_test.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package cli_test
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
-
- boundarycli "github.com/coder/boundary/cli"
- "github.com/coder/coder/v2/cli/clitest"
- "github.com/coder/coder/v2/pty/ptytest"
- "github.com/coder/coder/v2/testutil"
-)
-
-// Actually testing the functionality of coder/boundary takes place in the
-// coder/boundary repo, since it's a dependency of coder.
-// Here we want to test basically that integrating it as a subcommand doesn't break anything.
-func TestBoundarySubcommand(t *testing.T) {
- t.Parallel()
- ctx := testutil.Context(t, testutil.WaitShort)
-
- inv, _ := clitest.New(t, "exp", "boundary", "--help")
- pty := ptytest.New(t).Attach(inv)
-
- go func() {
- err := inv.WithContext(ctx).Run()
- assert.NoError(t, err)
- }()
-
- // Expect the --help output to include the short description.
- // We're simply confirming that `coder boundary --help` ran without a runtime error as
- // a good chunk of serpents self validation logic happens at runtime.
- pty.ExpectMatch(boundarycli.BaseCommand().Short)
-}
diff --git a/cli/root.go b/cli/root.go
index 4557834a57..5a09cad853 100644
--- a/cli/root.go
+++ b/cli/root.go
@@ -151,7 +151,6 @@ func (r *RootCmd) AGPLExperimental() []*serpent.Command {
r.promptExample(),
r.rptyCommand(),
r.syncCommand(),
- r.boundary(),
}
}
@@ -333,6 +332,12 @@ func (r *RootCmd) Command(subcommands []*serpent.Command) (*serpent.Command, err
// support links.
return
}
+ if cmd.Name() == "boundary" {
+ // The boundary command is integrated from the boundary package
+ // and has YAML-only options (e.g., allowlist from config file)
+ // that don't have flags or env vars.
+ return
+ }
merr = errors.Join(
merr,
xerrors.Errorf("option %q in %q should have a flag or env", opt.Name, cmd.FullName()),
diff --git a/codersdk/deployment.go b/codersdk/deployment.go
index 90915b13b3..fa103750db 100644
--- a/codersdk/deployment.go
+++ b/codersdk/deployment.go
@@ -90,6 +90,7 @@ const (
FeatureManagedAgentLimit FeatureName = "managed_agent_limit"
FeatureWorkspaceExternalAgent FeatureName = "workspace_external_agent"
FeatureAIBridge FeatureName = "aibridge"
+ FeatureBoundary FeatureName = "boundary"
)
var (
@@ -119,6 +120,7 @@ var (
FeatureManagedAgentLimit,
FeatureWorkspaceExternalAgent,
FeatureAIBridge,
+ FeatureBoundary,
}
// FeatureNamesMap is a map of all feature names for quick lookups.
@@ -163,6 +165,7 @@ func (n FeatureName) AlwaysEnable() bool {
FeatureMultipleOrganizations: true,
FeatureWorkspacePrebuilds: true,
FeatureWorkspaceExternalAgent: true,
+ FeatureBoundary: true,
}[n]
}
diff --git a/docs/manifest.json b/docs/manifest.json
index cb52f9f98c..bbc0a0598a 100644
--- a/docs/manifest.json
+++ b/docs/manifest.json
@@ -1344,6 +1344,11 @@
"description": "Toggle auto-update policy for a workspace",
"path": "reference/cli/autoupdate.md"
},
+ {
+ "title": "boundary",
+ "description": "Network isolation tool for monitoring and restricting HTTP/HTTPS requests",
+ "path": "reference/cli/boundary.md"
+ },
{
"title": "coder",
"path": "reference/cli/index.md"
diff --git a/docs/reference/cli/boundary.md b/docs/reference/cli/boundary.md
new file mode 100644
index 0000000000..0c99605d8d
--- /dev/null
+++ b/docs/reference/cli/boundary.md
@@ -0,0 +1,147 @@
+
+# boundary
+
+Network isolation tool for monitoring and restricting HTTP/HTTPS requests
+
+## Usage
+
+```console
+coder boundary [flags] [args...]
+```
+
+## Description
+
+```console
+boundary creates an isolated network environment for target processes, intercepting HTTP/HTTPS traffic through a transparent proxy that enforces user-defined allow rules.
+```
+
+## Options
+
+### --config
+
+| | |
+|-------------|-------------------------------|
+| Type | yaml-config-path |
+| Environment | $BOUNDARY_CONFIG |
+
+Path to YAML config file.
+
+### --allow
+
+| | |
+|-------------|------------------------------|
+| Type | string |
+| Environment | $BOUNDARY_ALLOW |
+
+Allow rule (repeatable). These are merged with allowlist from config file. Format: "pattern" or "METHOD[,METHOD] pattern".
+
+### --
+
+| | |
+|------|---------------------------|
+| Type | string-array |
+| YAML | allowlist |
+
+Allowlist rules from config file (YAML only).
+
+### --log-level
+
+| | |
+|-------------|----------------------------------|
+| Type | string |
+| Environment | $BOUNDARY_LOG_LEVEL |
+| YAML | log_level |
+| Default | warn |
+
+Set log level (error, warn, info, debug).
+
+### --log-dir
+
+| | |
+|-------------|--------------------------------|
+| Type | string |
+| Environment | $BOUNDARY_LOG_DIR |
+| YAML | log_dir |
+
+Set a directory to write logs to rather than stderr.
+
+### --proxy-port
+
+| | |
+|-------------|--------------------------|
+| Type | int |
+| Environment | $PROXY_PORT |
+| YAML | proxy_port |
+| Default | 8080 |
+
+Set a port for HTTP proxy.
+
+### --pprof
+
+| | |
+|-------------|------------------------------|
+| Type | bool |
+| Environment | $BOUNDARY_PPROF |
+| YAML | pprof_enabled |
+
+Enable pprof profiling server.
+
+### --pprof-port
+
+| | |
+|-------------|-----------------------------------|
+| Type | int |
+| Environment | $BOUNDARY_PPROF_PORT |
+| YAML | pprof_port |
+| Default | 6060 |
+
+Set port for pprof profiling server.
+
+### --configure-dns-for-local-stub-resolver
+
+| | |
+|-------------|--------------------------------------------------------------|
+| Type | bool |
+| Environment | $BOUNDARY_CONFIGURE_DNS_FOR_LOCAL_STUB_RESOLVER |
+| YAML | configure_dns_for_local_stub_resolver |
+
+Configure DNS for local stub resolver (e.g., systemd-resolved). Only needed when /etc/resolv.conf contains nameserver 127.0.0.53.
+
+### --jail-type
+
+| | |
+|-------------|----------------------------------|
+| Type | string |
+| Environment | $BOUNDARY_JAIL_TYPE |
+| YAML | jail_type |
+| Default | nsjail |
+
+Jail type to use for network isolation. Options: nsjail (default), landjail.
+
+### --disable-audit-logs
+
+| | |
+|-------------|----------------------------------|
+| Type | bool |
+| Environment | $DISABLE_AUDIT_LOGS |
+| YAML | disable_audit_logs |
+
+Disable sending of audit logs to the workspace agent when set to true.
+
+### --log-proxy-socket-path
+
+| | |
+|-------------|----------------------------------------------------------|
+| Type | string |
+| Environment | $CODER_AGENT_BOUNDARY_LOG_PROXY_SOCKET_PATH |
+| Default | /tmp/boundary-audit.sock |
+
+Path to the socket where the boundary log proxy server listens for audit logs.
+
+### --version
+
+| | |
+|------|-------------------|
+| Type | bool |
+
+Print version information and exit.
diff --git a/docs/reference/cli/index.md b/docs/reference/cli/index.md
index 35c9bc1636..de3a5c2cb8 100644
--- a/docs/reference/cli/index.md
+++ b/docs/reference/cli/index.md
@@ -65,6 +65,7 @@ Coder — A tool for provisioning self-hosted development environments with Terr
| [support](./support.md) | Commands for troubleshooting issues with a Coder deployment. |
| [server](./server.md) | Start a Coder server |
| [provisioner](./provisioner.md) | View and manage provisioner daemons and jobs |
+| [boundary](./boundary.md) | Network isolation tool for monitoring and restricting HTTP/HTTPS requests |
| [features](./features.md) | List Enterprise features |
| [licenses](./licenses.md) | Add, delete, and list licenses |
| [groups](./groups.md) | Manage groups |
diff --git a/enterprise/cli/boundary.go b/enterprise/cli/boundary.go
new file mode 100644
index 0000000000..104b2c6de2
--- /dev/null
+++ b/enterprise/cli/boundary.go
@@ -0,0 +1,86 @@
+package cli
+
+import (
+ "net/http"
+ "os"
+ "runtime/debug"
+
+ "golang.org/x/xerrors"
+
+ boundarycli "github.com/coder/boundary/cli"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/serpent"
+)
+
+func isChild() bool {
+ return os.Getenv("CHILD") == "true"
+}
+
+func getBoundaryVersion() string {
+ const boundaryModulePath = "github.com/coder/boundary"
+
+ buildInfo, ok := debug.ReadBuildInfo()
+ if !ok {
+ return "unknown"
+ }
+
+ for _, module := range buildInfo.Deps {
+ if module.Path == boundaryModulePath {
+ return module.Version
+ }
+ }
+
+ return "unknown"
+}
+
+func (r *RootCmd) verifyLicense(inv *serpent.Invocation) error {
+ client, err := r.InitClient(inv)
+ if err != nil {
+ return err
+ }
+
+ entitlements, err := client.Entitlements(inv.Context())
+ if cerr, ok := codersdk.AsError(err); ok && cerr.StatusCode() == http.StatusNotFound {
+ return xerrors.Errorf("your deployment appears to be an AGPL deployment, so you cannot use the boundary command")
+ } else if err != nil {
+ return xerrors.Errorf("failed to get entitlements: %w", err)
+ }
+
+ feature := entitlements.Features[codersdk.FeatureBoundary]
+ if feature.Entitlement == codersdk.EntitlementNotEntitled {
+ return xerrors.Errorf("your license is not entitled to use the boundary feature")
+ }
+ if !feature.Enabled {
+ // Feature is entitled but disabled (shouldn't happen for FeatureBoundary
+ // since it's in AlwaysEnable(), but handle it gracefully).
+ return xerrors.Errorf("the boundary feature is disabled in your deployment configuration")
+ }
+
+ return nil
+}
+
+func (r *RootCmd) boundary() *serpent.Command {
+ version := getBoundaryVersion()
+ cmd := boundarycli.BaseCommand(version) // Package coder/boundary/cli exports a "base command" designed to be integrated as a subcommand.
+ cmd.Use += " [args...]" // The base command looks like `boundary -- command`. Serpent adds the flags piece, but we need to add the args.
+
+ // Wrap the handler to check for FeatureBoundary entitlement.
+ originalHandler := cmd.Handler
+ cmd.Handler = func(inv *serpent.Invocation) error {
+ // Boundary re-executes itself with CHILD=true to run the target process
+ // inside a jailed network namespace. Skip the license check for child
+ // processes since the parent already verified entitlement.
+ if isChild() {
+ return originalHandler(inv)
+ }
+
+ if err := r.verifyLicense(inv); err != nil {
+ return err
+ }
+
+ // Call the original handler if entitlement check passes.
+ return originalHandler(inv)
+ }
+
+ return cmd
+}
diff --git a/enterprise/cli/boundary_test.go b/enterprise/cli/boundary_test.go
new file mode 100644
index 0000000000..ac92c5bfe2
--- /dev/null
+++ b/enterprise/cli/boundary_test.go
@@ -0,0 +1,305 @@
+package cli_test
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "net/http/httputil"
+ "net/url"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ boundarycli "github.com/coder/boundary/cli"
+ "github.com/coder/coder/v2/cli/clitest"
+ "github.com/coder/coder/v2/coderd/coderdtest"
+ "github.com/coder/coder/v2/coderd/httpapi"
+ "github.com/coder/coder/v2/codersdk"
+ "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
+ "github.com/coder/coder/v2/enterprise/coderd/license"
+ "github.com/coder/coder/v2/pty/ptytest"
+ "github.com/coder/coder/v2/testutil"
+)
+
+// Actually testing the functionality of coder/boundary takes place in the
+// coder/boundary repo, since it's a dependency of coder.
+// Here we want to test basically that integrating it as a subcommand doesn't break anything.
+func TestBoundarySubcommand(t *testing.T) {
+ t.Parallel()
+ ctx := testutil.Context(t, testutil.WaitShort)
+
+ inv, _ := newCLI(t, "boundary", "--help")
+ pty := ptytest.New(t).Attach(inv)
+
+ go func() {
+ err := inv.WithContext(ctx).Run()
+ assert.NoError(t, err)
+ }()
+
+ // Expect the --help output to include the short description.
+ // We're simply confirming that `coder boundary --help` ran without a runtime error as
+ // a good chunk of serpents self validation logic happens at runtime.
+ pty.ExpectMatch(boundarycli.BaseCommand("dev").Short)
+}
+
+func TestBoundaryLicenseVerification(t *testing.T) {
+ t.Parallel()
+
+ t.Run("EntitledAndEnabled", func(t *testing.T) {
+ t.Parallel()
+
+ client, _ := coderdenttest.New(t, &coderdenttest.Options{
+ LicenseOptions: &coderdenttest.LicenseOptions{
+ Features: license.Features{
+ codersdk.FeatureBoundary: 1,
+ },
+ },
+ })
+
+ inv, conf := newCLI(t, "boundary", "--version")
+ //nolint:gocritic // requires owner
+ clitest.SetupConfig(t, client, conf)
+
+ ctx := testutil.Context(t, testutil.WaitShort)
+ err := inv.WithContext(ctx).Run()
+ // Should succeed - boundary --version should work with valid license.
+ require.NoError(t, err)
+ })
+
+ t.Run("NotEntitled", func(t *testing.T) {
+ t.Parallel()
+
+ // Create a proxy server that returns entitlements without boundary feature.
+ client, _ := coderdenttest.New(t, &coderdenttest.Options{
+ LicenseOptions: &coderdenttest.LicenseOptions{
+ Features: license.Features{
+ // No FeatureBoundary
+ },
+ },
+ })
+
+ proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == "/api/v2/entitlements" {
+ res := codersdk.Entitlements{
+ Features: map[codersdk.FeatureName]codersdk.Feature{},
+ Warnings: []string{},
+ Errors: []string{},
+ HasLicense: true,
+ Trial: false,
+ RequireTelemetry: false,
+ }
+ // Set boundary to not entitled, all other features to entitled.
+ for _, feature := range codersdk.FeatureNames {
+ if feature == codersdk.FeatureBoundary {
+ // Explicitly set boundary to not entitled.
+ res.Features[feature] = codersdk.Feature{
+ Entitlement: codersdk.EntitlementNotEntitled,
+ Enabled: false,
+ }
+ } else {
+ res.Features[feature] = codersdk.Feature{
+ Entitlement: codersdk.EntitlementEntitled,
+ Enabled: true,
+ }
+ }
+ }
+ httpapi.Write(r.Context(), w, http.StatusOK, res)
+ return
+ }
+
+ // Otherwise, proxy the request to the real API server.
+ rp := httputil.NewSingleHostReverseProxy(client.URL)
+ tp := &http.Transport{}
+ defer tp.CloseIdleConnections()
+ rp.Transport = tp
+ rp.ServeHTTP(w, r)
+ }))
+ defer proxy.Close()
+
+ proxyURL, err := url.Parse(proxy.URL)
+ require.NoError(t, err)
+ proxyClient := codersdk.New(proxyURL)
+ proxyClient.SetSessionToken(client.SessionToken())
+ t.Cleanup(proxyClient.HTTPClient.CloseIdleConnections)
+
+ inv, conf := newCLI(t, "boundary", "--version")
+ clitest.SetupConfig(t, proxyClient, conf)
+
+ ctx := testutil.Context(t, testutil.WaitShort)
+ err = inv.WithContext(ctx).Run()
+ require.Error(t, err)
+ require.ErrorContains(t, err, "your license is not entitled to use the boundary feature")
+ })
+
+ t.Run("FeatureDisabled", func(t *testing.T) {
+ t.Parallel()
+
+ // Create a proxy server that returns entitlements with boundary disabled.
+ client, _ := coderdenttest.New(t, &coderdenttest.Options{
+ LicenseOptions: &coderdenttest.LicenseOptions{
+ Features: license.Features{
+ codersdk.FeatureBoundary: 1,
+ },
+ },
+ })
+
+ proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == "/api/v2/entitlements" {
+ res := codersdk.Entitlements{
+ Features: map[codersdk.FeatureName]codersdk.Feature{},
+ Warnings: []string{},
+ Errors: []string{},
+ HasLicense: true,
+ Trial: false,
+ RequireTelemetry: false,
+ }
+ for _, feature := range codersdk.FeatureNames {
+ if feature == codersdk.FeatureBoundary {
+ // Feature is entitled but disabled.
+ res.Features[feature] = codersdk.Feature{
+ Entitlement: codersdk.EntitlementEntitled,
+ Enabled: false,
+ }
+ } else {
+ res.Features[feature] = codersdk.Feature{
+ Entitlement: codersdk.EntitlementEntitled,
+ Enabled: true,
+ }
+ }
+ }
+ httpapi.Write(r.Context(), w, http.StatusOK, res)
+ return
+ }
+
+ // Otherwise, proxy the request to the real API server.
+ rp := httputil.NewSingleHostReverseProxy(client.URL)
+ tp := &http.Transport{}
+ defer tp.CloseIdleConnections()
+ rp.Transport = tp
+ rp.ServeHTTP(w, r)
+ }))
+ defer proxy.Close()
+
+ proxyURL, err := url.Parse(proxy.URL)
+ require.NoError(t, err)
+ proxyClient := codersdk.New(proxyURL)
+ proxyClient.SetSessionToken(client.SessionToken())
+ t.Cleanup(proxyClient.HTTPClient.CloseIdleConnections)
+
+ inv, conf := newCLI(t, "boundary", "--version")
+ clitest.SetupConfig(t, proxyClient, conf)
+
+ ctx := testutil.Context(t, testutil.WaitShort)
+ err = inv.WithContext(ctx).Run()
+ require.Error(t, err)
+ require.ErrorContains(t, err, "the boundary feature is disabled in your deployment configuration")
+ })
+
+ t.Run("AGPLDeployment", func(t *testing.T) {
+ t.Parallel()
+
+ // Create an AGPL server (no enterprise features).
+ client := coderdtest.New(t, &coderdtest.Options{})
+
+ proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == "/api/v2/entitlements" {
+ // AGPL deployments return 404 for entitlements endpoint.
+ w.WriteHeader(http.StatusNotFound)
+ return
+ }
+
+ // Otherwise, proxy the request to the real API server.
+ rp := httputil.NewSingleHostReverseProxy(client.URL)
+ tp := &http.Transport{}
+ defer tp.CloseIdleConnections()
+ rp.Transport = tp
+ rp.ServeHTTP(w, r)
+ }))
+ defer proxy.Close()
+
+ proxyURL, err := url.Parse(proxy.URL)
+ require.NoError(t, err)
+ proxyClient := codersdk.New(proxyURL)
+ proxyClient.SetSessionToken(client.SessionToken())
+ t.Cleanup(proxyClient.HTTPClient.CloseIdleConnections)
+
+ inv, conf := newCLI(t, "boundary", "--version")
+ clitest.SetupConfig(t, proxyClient, conf)
+
+ ctx := testutil.Context(t, testutil.WaitShort)
+ err = inv.WithContext(ctx).Run()
+ require.Error(t, err)
+ require.ErrorContains(t, err, "your deployment appears to be an AGPL deployment")
+ })
+}
+
+// TestBoundaryChildProcessSkipsCheck verifies that when CHILD=true, the license
+// check is skipped. This simulates boundary re-executing itself to run the
+// target process. We use a proxy that would fail the license check to verify
+// it's skipped.
+func TestBoundaryChildProcessSkipsCheck(t *testing.T) {
+ // Cannot use t.Parallel() with t.Setenv().
+ client, _ := coderdenttest.New(t, &coderdenttest.Options{
+ LicenseOptions: &coderdenttest.LicenseOptions{
+ Features: license.Features{
+ // No FeatureBoundary - would normally fail
+ },
+ },
+ })
+
+ proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == "/api/v2/entitlements" {
+ // Return not entitled for boundary - this would normally cause failure.
+ res := codersdk.Entitlements{
+ Features: map[codersdk.FeatureName]codersdk.Feature{},
+ Warnings: []string{},
+ Errors: []string{},
+ HasLicense: true,
+ Trial: false,
+ RequireTelemetry: false,
+ }
+ for _, feature := range codersdk.FeatureNames {
+ if feature == codersdk.FeatureBoundary {
+ res.Features[feature] = codersdk.Feature{
+ Entitlement: codersdk.EntitlementNotEntitled,
+ Enabled: false,
+ }
+ } else {
+ res.Features[feature] = codersdk.Feature{
+ Entitlement: codersdk.EntitlementEntitled,
+ Enabled: true,
+ }
+ }
+ }
+ httpapi.Write(r.Context(), w, http.StatusOK, res)
+ return
+ }
+
+ // Otherwise, proxy the request to the real API server.
+ rp := httputil.NewSingleHostReverseProxy(client.URL)
+ tp := &http.Transport{}
+ defer tp.CloseIdleConnections()
+ rp.Transport = tp
+ rp.ServeHTTP(w, r)
+ }))
+ defer proxy.Close()
+
+ proxyURL, err := url.Parse(proxy.URL)
+ require.NoError(t, err)
+ proxyClient := codersdk.New(proxyURL)
+ proxyClient.SetSessionToken(client.SessionToken())
+ t.Cleanup(proxyClient.HTTPClient.CloseIdleConnections)
+
+ inv, conf := newCLI(t, "boundary", "--version")
+ clitest.SetupConfig(t, proxyClient, conf)
+
+ // Set CHILD=true to simulate boundary re-execution. This should skip the
+ // license check, so the command should succeed even though the proxy would
+ // return "not entitled".
+ t.Setenv("CHILD", "true")
+
+ ctx := testutil.Context(t, testutil.WaitShort)
+ err = inv.WithContext(ctx).Run()
+ // Should succeed because license check is skipped for child processes.
+ require.NoError(t, err)
+}
diff --git a/enterprise/cli/root.go b/enterprise/cli/root.go
index 78858ef48d..baba6830e6 100644
--- a/enterprise/cli/root.go
+++ b/enterprise/cli/root.go
@@ -18,6 +18,7 @@ func (r *RootCmd) enterpriseOnly() []*serpent.Command {
agplcli.ExperimentalCommand(append(r.AGPLExperimental(), r.enterpriseExperimental()...)),
// New commands that don't exist in AGPL:
+ r.boundary(),
r.workspaceProxy(),
r.features(),
r.licenses(),
diff --git a/enterprise/cli/testdata/coder_--help.golden b/enterprise/cli/testdata/coder_--help.golden
index e199e8cc27..b421002bc8 100644
--- a/enterprise/cli/testdata/coder_--help.golden
+++ b/enterprise/cli/testdata/coder_--help.golden
@@ -15,6 +15,8 @@ USAGE:
SUBCOMMANDS:
aibridge Manage AI Bridge.
+ boundary Network isolation tool for monitoring and restricting
+ HTTP/HTTPS requests
external-workspaces Create or manage external workspaces
features List Enterprise features
groups Manage groups
diff --git a/enterprise/cli/testdata/coder_boundary_--help.golden b/enterprise/cli/testdata/coder_boundary_--help.golden
new file mode 100644
index 0000000000..f3c8c87f34
--- /dev/null
+++ b/enterprise/cli/testdata/coder_boundary_--help.golden
@@ -0,0 +1,57 @@
+coder v0.0.0-devel
+
+USAGE:
+ coder boundary [flags] [args...]
+
+ Network isolation tool for monitoring and restricting HTTP/HTTPS requests
+
+ boundary creates an isolated network environment for target processes,
+ intercepting HTTP/HTTPS traffic through a transparent proxy that enforces
+ user-defined allow rules.
+
+OPTIONS:
+ --allow string, $BOUNDARY_ALLOW
+ Allow rule (repeatable). These are merged with allowlist from config
+ file. Format: "pattern" or "METHOD[,METHOD] pattern".
+
+ string-array
+ Allowlist rules from config file (YAML only).
+
+ --config yaml-config-path, $BOUNDARY_CONFIG
+ Path to YAML config file.
+
+ --configure-dns-for-local-stub-resolver bool, $BOUNDARY_CONFIGURE_DNS_FOR_LOCAL_STUB_RESOLVER
+ Configure DNS for local stub resolver (e.g., systemd-resolved). Only
+ needed when /etc/resolv.conf contains nameserver 127.0.0.53.
+
+ --disable-audit-logs bool, $DISABLE_AUDIT_LOGS
+ Disable sending of audit logs to the workspace agent when set to true.
+
+ --jail-type string, $BOUNDARY_JAIL_TYPE (default: nsjail)
+ Jail type to use for network isolation. Options: nsjail (default),
+ landjail.
+
+ --log-dir string, $BOUNDARY_LOG_DIR
+ Set a directory to write logs to rather than stderr.
+
+ --log-level string, $BOUNDARY_LOG_LEVEL (default: warn)
+ Set log level (error, warn, info, debug).
+
+ --log-proxy-socket-path string, $CODER_AGENT_BOUNDARY_LOG_PROXY_SOCKET_PATH (default: /tmp/boundary-audit.sock)
+ Path to the socket where the boundary log proxy server listens for
+ audit logs.
+
+ --pprof bool, $BOUNDARY_PPROF
+ Enable pprof profiling server.
+
+ --pprof-port int, $BOUNDARY_PPROF_PORT (default: 6060)
+ Set port for pprof profiling server.
+
+ --proxy-port int, $PROXY_PORT (default: 8080)
+ Set a port for HTTP proxy.
+
+ --version bool
+ Print version information and exit.
+
+———
+Run `coder --help` for a list of global options.
diff --git a/go.mod b/go.mod
index beb91d7af0..13f8ec270c 100644
--- a/go.mod
+++ b/go.mod
@@ -453,7 +453,7 @@ require (
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
howett.net/plist v1.0.0 // indirect
- kernel.org/pub/linux/libs/security/libcap/psx v1.2.73 // indirect
+ kernel.org/pub/linux/libs/security/libcap/psx v1.2.77 // indirect
sigs.k8s.io/yaml v1.5.0 // indirect
)
@@ -475,7 +475,7 @@ require (
github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225
github.com/coder/aibridge v0.3.1-0.20260121122740-e164b504fc52
github.com/coder/aisdk-go v0.0.9
- github.com/coder/boundary v0.0.1-alpha
+ github.com/coder/boundary v0.6.0
github.com/coder/preview v1.0.4
github.com/danieljoos/wincred v1.2.3
github.com/dgraph-io/ristretto/v2 v2.3.0
@@ -541,6 +541,7 @@ require (
github.com/jackmordaunt/icns/v3 v3.0.1 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
+ github.com/landlock-lsm/go-landlock v0.0.0-20251103212306-430f8e5cd97c // indirect
github.com/mattn/go-shellwords v1.0.12 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
diff --git a/go.sum b/go.sum
index 88d32b6873..df875709db 100644
--- a/go.sum
+++ b/go.sum
@@ -931,8 +931,8 @@ github.com/coder/aibridge v0.3.1-0.20260121122740-e164b504fc52 h1:UcsOXQH881tXPp
github.com/coder/aibridge v0.3.1-0.20260121122740-e164b504fc52/go.mod h1:x45BE/NNDesDN1eWy4bsg81QsL6ou7xXPIeQr0ePETQ=
github.com/coder/aisdk-go v0.0.9 h1:Vzo/k2qwVGLTR10ESDeP2Ecek1SdPfZlEjtTfMveiVo=
github.com/coder/aisdk-go v0.0.9/go.mod h1:KF6/Vkono0FJJOtWtveh5j7yfNrSctVTpwgweYWSp5M=
-github.com/coder/boundary v0.0.1-alpha h1:6shUQ2zkrWrfbgVcqWvpV2ibljOQvPvYqTctWBkKoUA=
-github.com/coder/boundary v0.0.1-alpha/go.mod h1:d1AMFw81rUgrGHuZzWdPNhkY0G8w7pvLNLYF0e3ceC4=
+github.com/coder/boundary v0.6.0 h1:DfYVBIH8/6EBfg9I0qz7rX2jo+4blUx4P4amd13nib8=
+github.com/coder/boundary v0.6.0/go.mod h1:jEXVbTGQP9JFoXkyzsnitj2rsWJuTt+VVej1Yzr2CkQ=
github.com/coder/bubbletea v1.2.2-0.20241212190825-007a1cdb2c41 h1:SBN/DA63+ZHwuWwPHPYoCZ/KLAjHv5g4h2MS4f2/MTI=
github.com/coder/bubbletea v1.2.2-0.20241212190825-007a1cdb2c41/go.mod h1:I9ULxr64UaOSUv7hcb3nX4kowodJCVS7vt7VVJk/kW4=
github.com/coder/clistat v1.2.0 h1:37KJKqiCllJsRvWqTHf3qiLIXX0JB6oqE5oxcqgdLkY=
@@ -1544,6 +1544,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/kyokomi/emoji/v2 v2.2.13 h1:GhTfQa67venUUvmleTNFnb+bi7S3aocF7ZCXU9fSO7U=
github.com/kyokomi/emoji/v2 v2.2.13/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE=
+github.com/landlock-lsm/go-landlock v0.0.0-20251103212306-430f8e5cd97c h1:QcKqiunpt7hooa/xIx0iyepA6Cs2BgKexaYOxHvHNCs=
+github.com/landlock-lsm/go-landlock v0.0.0-20251103212306-430f8e5cd97c/go.mod h1:stwyhp9tfeEy3A4bRJLdOEvjW/CetRJg/vcijNG8M5A=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
@@ -2847,8 +2849,9 @@ k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJ
k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
kernel.org/pub/linux/libs/security/libcap/cap v1.2.73 h1:Th2b8jljYqkyZKS3aD3N9VpYsQpHuXLgea+SZUIfODA=
kernel.org/pub/linux/libs/security/libcap/cap v1.2.73/go.mod h1:hbeKwKcboEsxARYmcy/AdPVN11wmT/Wnpgv4k4ftyqY=
-kernel.org/pub/linux/libs/security/libcap/psx v1.2.73 h1:SEAEUiPVylTD4vqqi+vtGkSnXeP2FcRO3FoZB1MklMw=
kernel.org/pub/linux/libs/security/libcap/psx v1.2.73/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=
+kernel.org/pub/linux/libs/security/libcap/psx v1.2.77 h1:Z06sMOzc0GNCwp6efaVrIrz4ywGJ1v+DP0pjVkOfDuA=
+kernel.org/pub/linux/libs/security/libcap/psx v1.2.77/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts
index 29bae4f057..a4c41ea053 100644
--- a/site/src/api/typesGenerated.ts
+++ b/site/src/api/typesGenerated.ts
@@ -2110,6 +2110,7 @@ export type FeatureName =
| "advanced_template_scheduling"
| "appearance"
| "audit_log"
+ | "boundary"
| "browser_only"
| "connection_log"
| "control_shared_ports"
@@ -2136,6 +2137,7 @@ export const FeatureNames: FeatureName[] = [
"advanced_template_scheduling",
"appearance",
"audit_log",
+ "boundary",
"browser_only",
"connection_log",
"control_shared_ports",