Skip to content

feat(tcc): give MDM fleets a way to answer the network-volume prompt - #193

Open
swarit-stepsecurity wants to merge 4 commits into
step-security:mainfrom
swarit-stepsecurity:swarit/fix/wt/177
Open

feat(tcc): give MDM fleets a way to answer the network-volume prompt#193
swarit-stepsecurity wants to merge 4 commits into
step-security:mainfrom
swarit-stepsecurity:swarit/fix/wt/177

Conversation

@swarit-stepsecurity

Copy link
Copy Markdown
Member

Fixes #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 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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +122 to +130
// 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)
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +83 to +84
<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>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread CHANGELOG.md Outdated

### 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 (ca9c54bf7003a7). 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.

Comment thread internal/tcc/tcc.go
}
}
return false
return s.withinNetworkVolume(cleaned)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
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.

MDM fleets: Network Volumes TCC prompt from container-mount scanning has no admin pre-approval path (per-user install defeats PPPC)

3 participants