Skip to content
Merged
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
3 changes: 3 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ type Config struct {
Network NetworkConfig // Domains, localhost controls, proxy ports, Unix sockets
Filesystem FilesystemConfig // Read/write/execute restrictions
Devices DevicesConfig // Linux /dev policy
MacOS MacOSConfig // macOS-specific advanced sandbox controls
Command CommandConfig // Local command rules
SSH SSHConfig // SSH host / remote command rules
AllowPty bool // Allow pseudo-terminal access
Expand Down Expand Up @@ -141,6 +142,8 @@ type Config struct {
to localhost is not the same as connecting out to localhost services.
- On macOS, Unix socket access can be allowlisted with `allowUnixSockets` or
fully opened with `allowAllUnixSockets`.
- On macOS, additional Mach/XPC permissions can be granted with
`macos.mach.lookup` and `macos.mach.register`.
- `allowedDomains: ["*"]` enables relaxed direct-network mode. Fence still
configures proxies for proxy-aware clients, but the sandbox stops relying on
forced proxy-only routing. In that mode, `deniedDomains` only applies to
Expand Down
36 changes: 36 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,42 @@ Setting `allowedDomains: ["*"]` enables **relaxed network mode**:

Use this when you need to support apps that don't respect proxy environment variables.

## macOS Configuration

These settings apply only to the macOS Seatbelt backend and are ignored on
other platforms.

| Field | Description |
|-------|-------------|
| `mach.lookup` | Additional Mach/XPC services to allow for `mach-lookup`. Supports exact service names, trailing-wildcard prefixes like `org.chromium.*`, and `*` to allow all lookups. |
| `mach.register` | Additional Mach/XPC services to allow for `mach-register`. Supports exact service names, trailing-wildcard prefixes like `org.chromium.*`, and `*` to allow all registrations. |

Example:

```json
{
"macos": {
"mach": {
"lookup": [
"com.apple.CARenderServer",
"com.apple.windowserver.active",
"org.chromium.*"
],
"register": [
"org.chromium.Chromium.MachPortRendezvousServer"
]
}
}
}
```

Prefer exact service names when possible. Use trailing wildcards only when the
service name is dynamic, and reserve `["*"]` for compatibility debugging or as
a last resort when you intentionally want broad Mach access.

If you're unsure which services a tool needs, run with `-m` to surface blocked
`mach-lookup` / `mach-register` attempts.

## Filesystem Configuration

| Field | Description |
Expand Down
14 changes: 14 additions & 0 deletions docs/library.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ type Config struct {
Extends string // Template to extend (e.g., "code")
Network NetworkConfig
Filesystem FilesystemConfig
MacOS MacOSConfig
Command CommandConfig
SSH SSHConfig
AllowPty bool // Allow PTY allocation
Expand All @@ -198,6 +199,19 @@ type NetworkConfig struct {
}
```

### MacOSConfig

```go
type MacOSConfig struct {
Comment thread
jy-tan marked this conversation as resolved.
Mach MachConfig
}

type MachConfig struct {
Lookup []string // Additional Mach/XPC services allowed for mach-lookup
Register []string // Additional Mach/XPC services allowed for mach-register
}
```

### FilesystemConfig

```go
Expand Down
30 changes: 30 additions & 0 deletions docs/schema/fence.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,36 @@
"null"
]
},
"macos": {
"additionalProperties": false,
"description": "macOS-specific advanced sandbox controls. Ignored on non-macOS platforms.",
"properties": {
"mach": {
"additionalProperties": false,
"description": "Mach and XPC permissions for the macOS Seatbelt backend.",
"properties": {
"lookup": {
"description": "Additional Mach/XPC services the macOS sandbox may look up. Supports exact service names, trailing-wildcard prefixes like \"org.chromium.*\", and \"*\" to allow all Mach lookups.",
"items": {
"pattern": "^(\\*|[^*]+\\*?)$",
"type": "string"
},
"type": "array"
},
"register": {
"description": "Additional Mach/XPC services the macOS sandbox may register. Supports exact service names, trailing-wildcard prefixes like \"org.chromium.*\", and \"*\" to allow all Mach registrations.",
"items": {
"pattern": "^(\\*|[^*]+\\*?)$",
"type": "string"
},
"type": "array"
}
},
"type": "object"
}
},
"type": "object"
},
"network": {
"additionalProperties": false,
"description": "Network access restrictions. Controls which domains the sandbox may connect to and how local networking is handled.",
Expand Down
6 changes: 6 additions & 0 deletions docs/security-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ Localhost is separate from "external domains":

- `allowLocalOutbound=false` can intentionally block connections to local services like Redis on `127.0.0.1:6379` (see the dev-server example).

### macOS IPC

- `macos.mach.lookup` and `macos.mach.register` can allow additional XPC/Mach services inside the macOS Seatbelt sandbox.
- These are local IPC exceptions, not domain allowlists. Granting more Mach access can let sandboxed code interact with system services that sit outside Fence's normal proxy-based network model.
- Prefer exact service names. Trailing wildcard prefixes and especially `["*"]` are broader compatibility tradeoffs and should be used sparingly.

### Filesystem

- **Writes are denied by default**; you must opt in with `allowWrite`.
Expand Down
48 changes: 48 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type Config struct {
Network NetworkConfig `json:"network" description:"Network access restrictions. Controls which domains the sandbox may connect to and how local networking is handled."`
Filesystem FilesystemConfig `json:"filesystem" description:"Filesystem access restrictions. Controls which paths may be read, written, or executed inside the sandbox."`
Devices DevicesConfig `json:"devices,omitempty"`
MacOS MacOSConfig `json:"macos,omitempty" description:"macOS-specific advanced sandbox controls. Ignored on non-macOS platforms."`
Command CommandConfig `json:"command" description:"Command execution restrictions. Controls which commands are blocked or allowed at preflight and runtime."`
SSH SSHConfig `json:"ssh" description:"SSH command and host restrictions. Applies only to ssh invocations; does not affect other network access."`
AllowPty bool `json:"allowPty,omitempty" description:"Allow the sandboxed process to allocate a pseudo-terminal (PTY). Required for interactive programs that need terminal control (e.g. vim, less, top)."`
Expand Down Expand Up @@ -53,6 +54,17 @@ type DevicesConfig struct {
Allow []string `json:"allow,omitempty" schema:"itemsPattern=^/dev/.+"` // Extra /dev paths to pass through when using a minimal /dev
}

// MacOSConfig defines macOS-specific sandbox controls.
type MacOSConfig struct {
Mach MachConfig `json:"mach,omitempty" description:"Mach and XPC permissions for the macOS Seatbelt backend."`
}

// MachConfig defines additional Mach/XPC permissions for macOS sandboxes.
type MachConfig struct {
Lookup []string `json:"lookup,omitempty" schema:"itemsPattern=^(\\*|[^*]+\\*?)$" description:"Additional Mach/XPC services the macOS sandbox may look up. Supports exact service names, trailing-wildcard prefixes like \"org.chromium.*\", and \"*\" to allow all Mach lookups."`
Register []string `json:"register,omitempty" schema:"itemsPattern=^(\\*|[^*]+\\*?)$" description:"Additional Mach/XPC services the macOS sandbox may register. Supports exact service names, trailing-wildcard prefixes like \"org.chromium.*\", and \"*\" to allow all Mach registrations."`
}

// FilesystemConfig defines filesystem restrictions.
type FilesystemConfig struct {
DefaultDenyRead bool `json:"defaultDenyRead,omitempty" description:"If true, deny all filesystem reads by default. Only paths listed in allowRead (and essential system paths) remain readable. Use for strict read isolation."`
Expand Down Expand Up @@ -337,6 +349,16 @@ func (c *Config) Validate() error {
return fmt.Errorf("invalid denied domain %q: %w", domain, err)
}
}
for _, name := range c.MacOS.Mach.Lookup {
if err := validateMachServicePattern(name); err != nil {
return fmt.Errorf("invalid macos.mach.lookup entry %q: %w", name, err)
}
}
for _, name := range c.MacOS.Mach.Register {
if err := validateMachServicePattern(name); err != nil {
return fmt.Errorf("invalid macos.mach.register entry %q: %w", name, err)
}
}

// strictDenyRead implies defaultDenyRead
if c.Filesystem.StrictDenyRead {
Expand Down Expand Up @@ -470,6 +492,25 @@ func validateDomainPattern(pattern string) error {
return nil
}

func validateMachServicePattern(pattern string) error {
if pattern == "" {
return errors.New("empty Mach service pattern")
}
if pattern == "*" {
return nil
}

trimmed := strings.TrimSuffix(pattern, "*")
if trimmed == "" {
return errors.New("wildcards are only allowed as a single trailing '*'")
}
if strings.Contains(trimmed, "*") {
return errors.New("wildcards are only allowed as a single trailing '*'")
}

return nil
}

// validateHostPattern validates an SSH host pattern.
// Host patterns are more permissive than domain patterns:
// - Can contain wildcards anywhere (e.g., prod-*.example.com, *.example.com)
Expand Down Expand Up @@ -661,6 +702,13 @@ func Merge(base, override *Config) *Config {
Allow: mergeStrings(base.Devices.Allow, override.Devices.Allow),
},

MacOS: MacOSConfig{
Mach: MachConfig{
Lookup: mergeStrings(base.MacOS.Mach.Lookup, override.MacOS.Mach.Lookup),
Register: mergeStrings(base.MacOS.Mach.Register, override.MacOS.Mach.Register),
},
},

Command: CommandConfig{
// Append slices
Deny: mergeStrings(base.Command.Deny, override.Command.Deny),
Expand Down
27 changes: 27 additions & 0 deletions internal/config/config_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ type cleanDevicesConfig struct {
Allow []string `json:"allow,omitempty"`
}

// cleanMacOSConfig is used for JSON output with omitempty to skip empty fields.
type cleanMacOSConfig struct {
Mach *cleanMachConfig `json:"mach,omitempty"`
}

// cleanMachConfig is used for JSON output with omitempty to skip empty fields.
type cleanMachConfig struct {
Lookup []string `json:"lookup,omitempty"`
Register []string `json:"register,omitempty"`
}

// cleanCommandConfig is used for JSON output with omitempty to skip empty fields.
type cleanCommandConfig struct {
Deny []string `json:"deny,omitempty"`
Expand Down Expand Up @@ -72,6 +83,7 @@ type cleanConfig struct {
Network *cleanNetworkConfig `json:"network,omitempty"`
Filesystem *cleanFilesystemConfig `json:"filesystem,omitempty"`
Devices *cleanDevicesConfig `json:"devices,omitempty"`
MacOS *cleanMacOSConfig `json:"macos,omitempty"`
Command *cleanCommandConfig `json:"command,omitempty"`
SSH *cleanSSHConfig `json:"ssh,omitempty"`
}
Expand Down Expand Up @@ -125,6 +137,17 @@ func MarshalConfigJSON(cfg *Config) ([]byte, error) {
clean.Devices = &devices
}

// macOS config - only include if non-empty
mach := cleanMachConfig{
Lookup: cfg.MacOS.Mach.Lookup,
Register: cfg.MacOS.Mach.Register,
}
if !isMachEmpty(mach) {
clean.MacOS = &cleanMacOSConfig{
Mach: &mach,
}
}

// Command config - only include if non-empty
command := cleanCommandConfig{
Deny: cfg.Command.Deny,
Expand Down Expand Up @@ -180,6 +203,10 @@ func isDevicesEmpty(d cleanDevicesConfig) bool {
return d.Mode == "" && len(d.Allow) == 0
}

func isMachEmpty(m cleanMachConfig) bool {
return len(m.Lookup) == 0 && len(m.Register) == 0
}

func isCommandEmpty(c cleanCommandConfig) bool {
return len(c.Deny) == 0 &&
len(c.Allow) == 0 &&
Expand Down
8 changes: 8 additions & 0 deletions internal/config/config_file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ func TestMarshalConfigJSON_IncludesExtendedSections(t *testing.T) {
cfg.Filesystem.AllowExecute = []string{"/usr/bin/bash"}
cfg.Devices.Mode = DeviceModeMinimal
cfg.Devices.Allow = []string{"/dev/null"}
cfg.MacOS.Mach.Lookup = []string{"org.chromium.*"}
cfg.MacOS.Mach.Register = []string{"org.chromium.Chromium.MachPortRendezvousServer"}
cfg.Command.AcceptSharedBinaryCannotRuntimeDeny = []string{"python"}
cfg.Command.RuntimeExecPolicy = RuntimeExecPolicyArgv
cfg.SSH.AllowedHosts = []string{"*.example.com"}
Expand All @@ -87,6 +89,12 @@ func TestMarshalConfigJSON_IncludesExtendedSections(t *testing.T) {
assert.Contains(t, output, `"devices": {`)
assert.Contains(t, output, `"mode": "minimal"`)
assert.Contains(t, output, `"/dev/null"`)
assert.Contains(t, output, `"macos": {`)
assert.Contains(t, output, `"mach": {`)
assert.Contains(t, output, `"lookup": [`)
assert.Contains(t, output, `"org.chromium.*"`)
assert.Contains(t, output, `"register": [`)
assert.Contains(t, output, `"org.chromium.Chromium.MachPortRendezvousServer"`)
assert.Contains(t, output, `"acceptSharedBinaryCannotRuntimeDeny": [`)
assert.Contains(t, output, `"python"`)
assert.Contains(t, output, `"runtimeExecPolicy": "argv"`)
Expand Down
Loading
Loading