Skip to content

sec(hardening): sandbox denies host secrets, firewall in-VM control plane, dind privileged off by default#107

Merged
ephpm-claude[bot] merged 1 commit into
mainfrom
sec/hardening-tier1
Jul 11, 2026
Merged

sec(hardening): sandbox denies host secrets, firewall in-VM control plane, dind privileged off by default#107
ephpm-claude[bot] merged 1 commit into
mainfrom
sec/hardening-tier1

Conversation

@ephpm-claude

@ephpm-claude ephpm-claude Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

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 .ssh deny on resolve(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).

  • Added (deny file-read-data (subpath "/Users")) + (deny file-write* (subpath "/Users")) — blocks reading file contents under every user's home (incl. ~/.ssh, dotfiles) while file-read-metadata stays allowed so realpath/getcwd path traversal still works. Mirrors the exact technique already used for the native/ subtree, including a re-allow of the /Users node ((allow file-read-data (literal "/Users"))) so getcwd can readdir it.
  • Threaded the configured GitHub App private_key_path down (scheduler.Config.PrivateKeyPathnative.NewGenerateSandboxProfile) and added belt-and-suspenders (deny file-read* (literal "<pem>")) + (deny file-read* (subpath "<dir of pem>")), resolved through the same resolve() 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 (bind 0.0.0.0, pkg/scheduler/dispatch.go) and containerd.

  • Added targeted INPUT DROP rules: subnet → gateway IP, TCP dport only, for containerd (10000), dispatch (10001), and the debug-exec server (10002). Ports sourced from containerdTCPPort (default 10000, see pkg/vm/vm.go) and the +1/+2 offsets already used in cmd/ephemerd/main.go; plumbed via the new networking.Config.ControlPorts. Not a blanket gateway block — NAT and DNS (dport 53) are untouched.
  • Added ip6tables mirrors for both the FORWARD private-range denies (fc00::/7 ULA, fe80::/10 link-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.
  • Idempotent throughout (-C check before -I/-A); rules are torn down in removeFirewallRules.

DIND-1 — privileged docker off by default (High)

pkg/config/config.go. ResolvedAllowPrivileged() defaulted to goruntime.GOOS != "linux" (privileged allowed by default on macOS/Windows). Now returns false on all platforms — privileged is opt-in everywhere via allow_privileged = true.

Tests

  • pkg/native/native_darwin_test.go: asserts the /Users denies + re-allow, the PEM literal/parent-dir denies (and their absence when unset), and that a job HOME under /var/lib is 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 ./..., and go test ./pkg/native/... ./pkg/config/... are green. pkg/networking is //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 in install_darwin.go / service_darwin.go are 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)

  • Deny-by-default sandbox posture.
  • RLIMIT resource limits.
  • Removing the /private/tmp write allow.
  • JIT-config-in-argv change (NATIVE-3).
  • gRPC auth interceptor — the firewall is this PR's mitigation for the unauthenticated dispatch server.

Do not merge — parent reviews first.

…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).
@ephpm-claude ephpm-claude Bot merged commit 1375c90 into main Jul 11, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant