feat(sandbox): Seatbelt lockdown translation — deny-all outbound, allow loopback proxy port (#372)

* feat(sandbox): Seatbelt lockdown translation confines outbound to the PMG proxy

Under network_via_proxy_only the Seatbelt profile now denies all
network-outbound (with a target=direct violation marker) and allows only
the loopback proxy port. SBPL is last-match-wins, so the broad deny is
emitted first, specific allows after, and the allow_network_bind rules
last — keeping loopback-to-loopback dev traffic working under lockdown.
allow_direct_dns re-opens the /var/run/mDNSResponder unix socket that
the deny otherwise covers.

Replaces the temporary fail-closed rejection in the Seatbelt driver with
the real translation; non-lockdown profiles translate byte-identically
to before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqMU5GNBbQvQct9nxek1VS

* test(sandbox): assert deny marker presence before ordering comparison

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqMU5GNBbQvQct9nxek1VS

* feat(sandbox): render lockdown profiles without a proxy as deny-only with runtime note

pmg sandbox profile show renders profiles for debugging and must not
fail on lockdown profiles. Without a running proxy the translator keeps
the broad deny (rendered profile stays fail-closed, never looser than
runtime) and documents the runtime-only proxy-port allow in an SBPL
comment instead of fabricating a port. Execution is unaffected: the
driver validates the proxy address before translating.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqMU5GNBbQvQct9nxek1VS

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Abhisek Datta
2026-07-10 21:11:56 +05:30
committed by GitHub
co-authored by Claude Fable 5
parent 5131c3f641
commit 3ac83a436d
5 changed files with 210 additions and 60 deletions
+70 -32
View File
@@ -209,7 +209,7 @@ func (t *seatbeltPolicyTranslator) LogTag() string {
return t.logTag
}
func (t *seatbeltPolicyTranslator) translate(policy *sandbox.SandboxPolicy) (string, error) {
func (t *seatbeltPolicyTranslator) translate(policy *sandbox.SandboxPolicy, rt *sandbox.ExecutionContext) (string, error) {
var sb strings.Builder
// Header
@@ -392,7 +392,7 @@ func (t *seatbeltPolicyTranslator) translate(policy *sandbox.SandboxPolicy) (str
}
// Network rules
if err := t.translateNetwork(policy, &sb); err != nil {
if err := t.translateNetwork(policy, rt, &sb); err != nil {
return "", fmt.Errorf("failed to translate network rules: %w", err)
}
@@ -605,41 +605,79 @@ func (t *seatbeltPolicyTranslator) translateFilesystem(policy *sandbox.SandboxPo
return nil
}
// translateNetwork translates network access rules.
func (t *seatbeltPolicyTranslator) translateNetwork(policy *sandbox.SandboxPolicy, sb *strings.Builder) error {
// translateNetwork translates network access rules. SBPL is last-match-wins,
// so rule ordering is normative: the lockdown broad deny first, specific
// allows after, and the AllowNetworkBind rules last so loopback traffic
// keeps working under lockdown.
func (t *seatbeltPolicyTranslator) translateNetwork(policy *sandbox.SandboxPolicy, rt *sandbox.ExecutionContext, sb *strings.Builder) error {
sb.WriteString(";; Network access\n")
// Check if deny all is present
denyAll := false
for _, pattern := range policy.Network.DenyOutbound {
if pattern == "*:*" {
denyAll = true
break
if utils.SafelyGetValue(policy.NetworkViaProxyOnly) {
sb.WriteString(";; network_via_proxy_only: all outbound confined to the PMG proxy\n")
sb.WriteString("(deny network-outbound (with message \"")
sb.WriteString(seatbeltLogMessage(t.logTag, "network-outbound", "direct"))
sb.WriteString("\"))\n")
// Without a running proxy (render/inspection, e.g. `pmg sandbox
// profile show`) the profile stays fail-closed: the deny stands and
// the runtime-only allow is documented instead of emitted with a
// fabricated port.
if rt == nil || rt.ProxyAddr == "" {
sb.WriteString(";; Rendered without a running PMG proxy. At runtime an\n")
sb.WriteString(";; (allow network-outbound (remote ip \"localhost:<pmg-proxy-port>\"))\n")
sb.WriteString(";; rule permits traffic to the PMG proxy only.\n")
} else {
port, err := sandbox.ValidateNetworkLockdown(policy, rt)
if err != nil {
return err
}
sb.WriteString("(allow network-outbound (remote ip \"localhost:")
sb.WriteString(port)
sb.WriteString("\"))\n")
}
// macOS resolves names via the /var/run/mDNSResponder unix socket,
// which the deny above covers; the proxy resolves names, so direct
// DNS stays closed unless explicitly re-opened.
if utils.SafelyGetValue(policy.AllowDirectDNS) {
sb.WriteString("(allow network-outbound (remote unix-socket (path-literal \"/var/run/mDNSResponder\")))\n")
}
sb.WriteString("\n")
} else {
// Check if deny all is present
denyAll := false
for _, pattern := range policy.Network.DenyOutbound {
if pattern == "*:*" {
denyAll = true
break
}
}
// If there are allow outbound rules, allow network-outbound generally
// (Seatbelt doesn't support fine-grained host:port filtering in all cases)
// Note: This is a limitation of Seatbelt - for more fine-grained control,
// consider using a network filtering solution or firewall rules
if len(policy.Network.AllowOutbound) > 0 {
sb.WriteString(";; Network outbound allowed to specific hosts\n")
sb.WriteString(";; Note: Seatbelt has limited host-based filtering, consider using firewall rules for strict control\n")
sb.WriteString("(allow network-outbound)\n")
} else if denyAll {
// If there are no allow rules but deny all is set, explicitly deny network
// This handles the case where user wants to completely block network access
sb.WriteString(";; Network outbound denied (no allowed hosts specified)\n")
sb.WriteString("(deny network-outbound)\n")
}
// Note: We don't add an explicit deny rule when both allow and deny_all are present
// because the default (deny default) at the top of the profile handles blocking
// everything that isn't explicitly allowed. Adding an explicit deny here would
// override the allow rule above, breaking network access entirely.
sb.WriteString("\n")
}
// If there are allow outbound rules, allow network-outbound generally
// (Seatbelt doesn't support fine-grained host:port filtering in all cases)
// Note: This is a limitation of Seatbelt - for more fine-grained control,
// consider using a network filtering solution or firewall rules
if len(policy.Network.AllowOutbound) > 0 {
sb.WriteString(";; Network outbound allowed to specific hosts\n")
sb.WriteString(";; Note: Seatbelt has limited host-based filtering, consider using firewall rules for strict control\n")
sb.WriteString("(allow network-outbound)\n")
} else if denyAll {
// If there are no allow rules but deny all is set, explicitly deny network
// This handles the case where user wants to completely block network access
sb.WriteString(";; Network outbound denied (no allowed hosts specified)\n")
sb.WriteString("(deny network-outbound)\n")
}
// Note: We don't add an explicit deny rule when both allow and deny_all are present
// because the default (deny default) at the top of the profile handles blocking
// everything that isn't explicitly allowed. Adding an explicit deny here would
// override the allow rule above, breaking network access entirely.
sb.WriteString("\n")
// Network bind rules for local listening
if utils.SafelyGetValue(policy.AllowNetworkBind) {
sb.WriteString(";; Local network bind (localhost only)\n")