feat(tcc): give MDM fleets a way to answer the network-volume prompt - #193
feat(tcc): give MDM fleets a way to answer the network-volume prompt#193swarit-stepsecurity wants to merge 4 commits into
Conversation
Container runtimes expose the guest filesystem through mounts macOS classifies as network volumes — OrbStack's ~/OrbStack, Docker Desktop and Colima shares — so the first scan that walks one fires a SystemPolicyNetworkVolumes prompt naming a process the developer does not recognize. Admins could not pre-answer it: PPPC path identifiers cannot express $HOME and the loader installs per-user under ~/.stepsecurity/bin. Keep walking those mounts by default. That walk is what inventories npm and Python packages inside dev containers, which no other part of the scan reaches, so suppressing the prompt by skipping them would trade away the coverage that made the prompt worth firing. Instead give fleets both exits. include_network_volumes: false (or --no-include-network-volumes) skips every non-local mount, enumerated from the kernel mount table via getfsstat rather than a hard-coded path list, so a newly installed runtime needs no agent change; MNT_NOWAIT keeps a stale server from blocking the enumeration, and reading the mount table cannot itself fire the prompt. The run then warns which mounts it gave up, so the coverage loss shows up in fleet logs instead of going silent. The alternative is packaging/macos's PPPC profile, which pre-answers the prompt fleet-wide (allow or deny) alongside the existing Full Disk Access grant — that route needs a fixed system-wide install path, and the docs now carry the migration steps plus the two dead ends worth naming: a symlink at a stable path does not work because TCC matches the resolved executable, and bundleID identifiers do not apply to a bare CLI binary. The two toggles are independent and default opposite ways: protected dirs stay skip-by-default, network volumes stay walk-by-default. Both resolve through tcc.ForRun, so the ~15 walk sites inherit the new class through the existing Skipper choke point with no per-detector change. Closes step-security#177 Signed-off-by: Swarit Pandey <swarit@stepsecurity.io>
The whole point of defaulting include_network_volumes to walk is that an agent upgrade changes nothing for existing fleets. On macOS, quietly walking something new is how a customer gets a TCC prompt out of nowhere, so assert the property instead of trusting the code to keep it: build the pre-toggle New(home) skipper and the ForRun default side by side and require identical answers for every path, plus a nil skipper for --include-tcc-protected as before. Signed-off-by: Swarit Pandey <swarit@stepsecurity.io>
There was a problem hiding this comment.
Pull request overview
Adds macOS network-volume controls and MDM pre-approval guidance to address TCC prompts from container-mount scanning.
Changes:
- Adds CLI/config toggles and mount-aware TCC skipping.
- Adds PPPC deployment artifacts and fleet migration guidance.
- Adds tests and coverage documentation.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
SCAN_COVERAGE.md |
Documents container-mount coverage. |
README.md |
Links macOS deployment guidance. |
packaging/macos/stepsecurity-dev-machine-guard-tcc.mobileconfig |
Adds the PPPC profile. |
packaging/macos/README.md |
Explains profile deployment. |
internal/telemetry/telemetry.go |
Applies network-volume skipping to telemetry scans. |
internal/tcc/volumes_test.go |
Tests volume matching and toggle behavior. |
internal/tcc/tcc.go |
Extends the TCC skipper for network volumes. |
internal/tcc/tcc_other.go |
Adds the non-macOS mount stub. |
internal/tcc/tcc_darwin.go |
Enumerates non-local macOS mounts. |
internal/tcc/tcc_darwin_test.go |
Tests mount enumeration invariants. |
internal/scan/scanner.go |
Applies skipping and coverage warnings. |
internal/config/config.go |
Persists and displays the new setting. |
internal/config/config_test.go |
Tests configuration loading. |
internal/cli/cli.go |
Adds include/exclude flags. |
internal/cli/cli_test.go |
Tests flag parsing. |
docs/macos-tcc-permissions.md |
Documents prompts, PPPC, and migration. |
docs/launchd-troubleshooting.md |
Adds network-volume troubleshooting. |
cmd/stepsecurity-dev-machine-guard/main.go |
Merges config and wires focused audits. |
CHANGELOG.md |
Records the feature. |
Suppressed comments (1)
packaging/macos/stepsecurity-dev-machine-guard-tcc.mobileconfig:99
- The Network Volumes grant has the same overly broad team-only code requirement: it can match any StepSecurity-signed executable at this path rather than this binary's designated requirement. Include the documented
identifier "stepsecurity-dev-machine-guard"clause.
<key>CodeRequirement</key>
<string>anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "D63S9HLM4L"</string>
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| // cString converts a NUL-terminated fixed-size C char array to a Go string. | ||
| func cString(b []byte) string { | ||
| for i, c := range b { | ||
| if c == 0 { | ||
| return string(b[:i]) | ||
| } | ||
| } | ||
| return string(b) | ||
| } |
There was a problem hiding this comment.
Verified this isn't reproducible: the pinned golang.org/x/sys (v0.33.0 per go.mod/go.sum) defines Statfs_t.Mntonname as [1024]byte, matching cString([]byte) directly. Cross-compiled GOOS=darwin GOARCH={arm64,amd64} CGO_ENABLED=0 go build ./... and go vet ./internal/tcc/... — both clean. No change made here.
| <key>CodeRequirement</key> | ||
| <string>anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = "D63S9HLM4L"</string> |
There was a problem hiding this comment.
Fixed in 8700257: bound both CodeRequirement strings to identifier "stepsecurity-dev-machine-guard" and ..., matching the designated requirement documented at macos-tcc-permissions.md:231. Also fixed the same gap in the docs.md's own embedded PPPC example, which had it pre-existing on the SystemPolicyAllFiles entry too.
There was a problem hiding this comment.
we have intentionally removed identifier "stepsecurity-dev-machine-guard" from code-requirement because for every released binary the identifier is different — If this issue is addressed we can bring it back.
|
|
||
| ### Added | ||
|
|
||
| - **macOS network-volume scan toggle and PPPC pre-approval path** (#177): container runtimes expose the guest filesystem through mounts macOS classifies as *network volumes* (OrbStack's `~/OrbStack`, Docker Desktop and Colima shares), so the first scan that walks one fires a `SystemPolicyNetworkVolumes` prompt naming a process the developer doesn't recognize. That walk stays **on by default** — it is what inventories npm and Python packages inside dev containers, supply-chain surface nothing else covers — but MDM fleets now have both ways out. `include_network_volumes: false` (config) or `--no-include-network-volumes` (CLI) skips every non-local mount, enumerated from the kernel mount table via `getfsstat` rather than a hard-coded path list, so a newly installed runtime is covered without an agent change; the run then logs exactly which mounts it gave up. Alternatively `packaging/macos/stepsecurity-dev-machine-guard-tcc.mobileconfig` pre-answers the prompt for the whole fleet (allow *or* deny) alongside the existing Full Disk Access grant — that route needs a fixed system-wide install path, since PPPC identifiers can't express `$HOME`, and `docs/macos-tcc-permissions.md` now carries the migration steps for a fleet already deployed per-user. |
There was a problem hiding this comment.
Not bumping version.go here — this repo bumps it in dedicated chore(release) commits after PRs land (e.g. df142dc / #195), not inside feature PRs; main also has a revert of a premature bump (ca9c54b → f7003a7). More importantly this branch has drifted a long way behind main: main already shipped 1.16.0 via #195 while this PR sat open, so bumping to match CHANGELOG.md's [Unreleased] entry here would double-count. This PR needs a rebase onto current main before merge regardless (it's currently reported CONFLICTING) — flagging that separately rather than resolving it as part of this comment.
| } | ||
| } | ||
| return false | ||
| return s.withinNetworkVolume(cleaned) |
There was a problem hiding this comment.
Fixed in 8700257: added rootWithinVolume so ShouldSkip bypasses volume-skipping for the whole walk when walkRoot itself is inside a skipped mount, not just for the root callback. Added a regression test (child of an explicit walk-root mount).
ShouldSkip only exempted path itself from the walk-root check; a child of an explicitly named mount (--search-dirs ~/OrbStack) still matched withinNetworkVolume and was skipped, so nested npm/Python projects were never scanned despite the documented opt-in. Bypass volume skipping for the whole walk when walkRoot is inside a skipped volume. Also bind the PPPC CodeRequirement to the Dev Machine Guard binary identifier (mobileconfig and the mirrored docs example), not just the signing team — as written, any binary signed by D63S9HLM4L at the configured path could claim the grant. Addresses review comments on step-security#193. Signed-off-by: Swarit Pandey <swarit@stepsecurity.io>
main closed out the [1.15.0]-adjacent slot as [1.16.0] with unrelated entries while this branch was open, so this PR's own [Unreleased] insertion no longer applies cleanly. Dropping it here; the network-volume entry can go in whichever changelog section actually lands next. Signed-off-by: Swarit Pandey <swarit@stepsecurity.io>
Fixes #177