sec(hardening): sandbox denies host secrets, firewall in-VM control plane, dind privileged off by default#107
Merged
Conversation
…lane, dind privileged off by default Additive, low-regression security hardening from an audit. Three fixes, all secure-defaults / additive denies — no behaviour flips beyond the dind default. NATIVE-1 (Critical): the macOS native sandbox denied ~/.ssh anchored on the daemon's HOME, but the daemon runs as root with no HOME, so the deny targeted "/.ssh" and left every operator secret under /Users readable under (allow default) — including the GitHub App PEM. Added a host-wide `(deny file-read-data (subpath "/Users"))` (contents blocked, metadata allowed so path traversal/getcwd still work, mirroring the native/ subtree technique) plus a re-allow of the /Users node, and threaded the configured private_key_path down to the profile generator for an explicit belt-and-suspenders literal + parent-dir deny. LINUX-1/2 (Critical/High): the in-VM firewall only hooked FORWARD, so a container could reach the gateway's unauthenticated dispatch gRPC server (bind 0.0.0.0) and containerd. Added targeted INPUT DROP rules (subnet -> gateway, specific TCP dport only: containerd 10000, dispatch 10001, debug-exec 10002) so NAT and DNS stay intact. Added ip6tables mirrors for the FORWARD private-range denies and the control-plane INPUT drops — IPv6 was entirely unfiltered (v6 cloud metadata reachable). DIND-1 (High): ResolvedAllowPrivileged defaulted to true on macOS/Windows. Changed the default to false on all platforms — privileged docker is now opt-in everywhere. Deferred (out of scope, pending live-host testing): deny-by-default sandbox, RLIMIT limits, removing the /private/tmp write allow, the JIT-config-in-argv change (NATIVE-3), and a gRPC auth interceptor (the firewall is this PR's mitigation for the unauthenticated dispatch server).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Additive security hardening from an audit. Scope is deliberately limited to additive denies + secure defaults (low regression risk). The riskier items are explicitly deferred (below).
Fixes
NATIVE-1 — sandbox must deny reading host user secrets (Critical)
pkg/native/native_darwin.go. The profile anchored a.sshdeny onresolve(os.Getenv("HOME")), but the daemon runs as root with no HOME, so the deny targeted/.ssh— leaving the operator's real~/.ssh(including the GitHub App PEM) readable under(allow default).(deny file-read-data (subpath "/Users"))+(deny file-write* (subpath "/Users"))— blocks reading file contents under every user's home (incl.~/.ssh, dotfiles) whilefile-read-metadatastays allowed so realpath/getcwd path traversal still works. Mirrors the exact technique already used for thenative/subtree, including a re-allow of the/Usersnode ((allow file-read-data (literal "/Users"))) so getcwd can readdir it.private_key_pathdown (scheduler.Config.PrivateKeyPath→native.New→GenerateSandboxProfile) and added belt-and-suspenders(deny file-read* (literal "<pem>"))+(deny file-read* (subpath "<dir of pem>")), resolved through the sameresolve()helper.(allow default)is kept — deny-by-default is a deferred follow-up.LINUX-1/2 — firewall the in-VM control plane (Critical/High)
pkg/networking/firewall_linux.go. The firewall only hooked FORWARD, so a container could reach the gateway's unauthenticated gRPC dispatch server (bind0.0.0.0,pkg/scheduler/dispatch.go) and containerd.subnet → gateway IP, TCP dport only, for containerd (10000), dispatch (10001), and the debug-exec server (10002). Ports sourced fromcontainerdTCPPort(default 10000, seepkg/vm/vm.go) and the+1/+2offsets already used incmd/ephemerd/main.go; plumbed via the newnetworking.Config.ControlPorts. Not a blanket gateway block — NAT and DNS (dport 53) are untouched.fc00::/7ULA,fe80::/10link-local) and the control-plane INPUT drops. IPv6 was previously entirely unfiltered → v6 cloud-metadata reachable. Best-effort (logged, non-fatal) on hosts with IPv6 disabled.-Ccheck before-I/-A); rules are torn down inremoveFirewallRules.DIND-1 — privileged docker off by default (High)
pkg/config/config.go.ResolvedAllowPrivileged()defaulted togoruntime.GOOS != "linux"(privileged allowed by default on macOS/Windows). Now returnsfalseon all platforms — privileged is opt-in everywhere viaallow_privileged = true.Tests
pkg/native/native_darwin_test.go: asserts the/Usersdenies + re-allow, the PEM literal/parent-dir denies (and their absence when unset), and that a job HOME under/var/libis unaffected.pkg/networking/firewall_linux_test.go(new): asserts the INPUT control-plane DROP set (one per port, narrow scope, never dport 53) and the ip6tables FORWARD denies + v6 INPUT drops.pkg/config/config_test.go: updated the default-off assertions.Verification
go build ./...,go vet ./..., andgo test ./pkg/native/... ./pkg/config/...are green.pkg/networkingis//go:build linux— it vets and compile-checks clean cross-target (GOOS=linux); its pure rule-construction functions could not be executed on this darwin host (no Linux runtime available). golangci-lint is clean on all touched packages; the two pre-existing errcheck hits ininstall_darwin.go/service_darwin.goare untouched by this PR.Is the firewall safe for DNS/NAT?
Yes. Every control-plane rule is scoped to
-s <subnet> -d <gateway> -p tcp --dport <control-port> -j DROP. It only matches TCP to the gateway on 10000/10001/10002. Container→gateway DNS (udp/tcp dport 53) and container→internet NAT (masquerade, unrelated dports) never match these rules, so they are unaffected. The DROPs are inserted at INPUT position 1 so they precede any permissive INPUT rule from another subsystem.Deferred (out of scope — pending live-host testing)
/private/tmpwrite allow.Do not merge — parent reviews first.