fix(service): gate system-level install against a user-writable binary (#565 LPE)#46
Merged
Merged
Conversation
…y (#565 LPE) A system-level dig-node service runs as a privileged principal (Windows LocalSystem / a root daemon) and records the currently-running binary as its ExecStart / SCM binPath / launchd ProgramArguments. If that binary sits in a user-writable directory, a non-privileged local user can replace it and gain persistent SYSTEM/root code execution on the next service start — the §565 LPE class the dig-dns #23/#24 gates surfaced. `install` now verifies the program's directory is privileged-owned before registering a system-level service, refusing with PERMISSION_DENIED (before any side effect) otherwise. It reuses the existing spawn-free owner gate (crate::security::dir_is_privileged) shared with the self-heal spawn root (#565) and TLS material root (#661), so the three never drift. User-level installs (the Linux/macOS default) run as the invoking user, cross no privilege boundary, and stay allowed. Refs #700 Co-Authored-By: Claude <noreply@anthropic.com>
…stalls The §565 privileged-target gate would refuse the service-smoke CI job's system-level Windows install of target/release/dig-node from the runner's user-writable checkout (a legitimate test of an unreleased build). Add an explicit, default-off DIG_NODE_ALLOW_INSECURE_SERVICE_TARGET opt-out (truthy 1/true/yes) that bypasses the gate with a loud warning, set only in the service-smoke workflow; end-user installs via the native package land in a protected dir and never need it. SPEC §9.2c documents it. Refs #700 Co-Authored-By: Claude <noreply@anthropic.com>
Contributor
Author
|
Follow-up commit 450262c: added a default-off |
MichaelTaylor3d
added a commit
that referenced
this pull request
Jul 17, 2026
…is_privileged (#712) The shared privileged-owner gate (self-heal #565, TLS root #661, service install #46) checked only the LEAF directory's owner. A privileged-owned leaf under a user-writable — or symlinked/junctioned — ancestor is still tamperable: an intermediate rename/replace is governed by the parent's permissions, and a reparse point anywhere redirects the whole path. dir_is_privileged now walks EVERY existing ancestor component and holds each to the same bar, rejecting any symlink/junction/reparse component (no-follow symlink_metadata + FILE_ATTRIBUTE_REPARSE_POINT on Windows). Accepts the well-known NT SERVICE\TrustedInstaller SID that owns C:\Program Files so the canonical %ProgramFiles%\DIG\bin install root is not false-rejected. Aligns with dig-dns #701. Fails closed. One fix hardens all three gates. Refs #46 #565 #661 #701 Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d
added a commit
that referenced
this pull request
Jul 17, 2026
…is_privileged (#712) Hardens the shared privileged-owner trust gate `dir_is_privileged` (crates/dig-node-service/src/security.rs), backing the self-heal spawn root (#565), TLS material root (#661), and system-service install target (#46). Walks all parent components via `ancestors().all()` and rejects reparse points (symlinks/junctions) with no-follow `symlink_metadata` before the owner read; accepts the fixed TrustedInstaller SID so the canonical `%ProgramFiles%\DIG\bin` root does not false-reject. Gates: loop-reviewer PASS, adversarial loop-decider COULD-NOT-REFUTE, loop-security PASS. Follow-up hardening: DACL write-ACE inspection tracked in #731. Closes #712 Co-Authored-By: Claude <noreply@anthropic.com>
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.
TLDR
Closes dig-node's part of the #700 §565 service-ExecStart LPE sweep. dig-node's
installwas vulnerable on system-level (Windows LocalSystem) installs: it registered the service withprogram = current_exe()in place, with no owner check on that binary's directory. If a user ran an elevateddig-node installfrom a user-writable location (e.g.C:\Users\alice\Downloads\dig-node.exe), the LocalSystem service'sbinPathpointed there, so any local user could swap the binary → persistent SYSTEM code execution. Now gated + fail-closed. Refs #700 (do NOT close — multi-repo sweep; dig-node part done).Audit — per platform
installrecordedcurrent_exe()asbinPathwith no owner verification → VULNERABLE (§565 LPE). Fixed.PREFERS_USER_LEVEL = true); the service runs as the invoking user who already owns the binary — no privilege boundary crossed, so the LPE does not apply. The gate short-circuits (always allowed) for user-level installs. If a root systemd/launchd install is ever performed (system-level), the same gate applies.%ProgramFiles%\DIG Network\dig-node\,/usr/…), so they satisfy the gate. The exposure was the manualdig-node installpath from a user-writable dir, which is now refused with a clear message pointing the operator at the installer/native package.Fix
ensure_service_target_is_safe(program, user_level)inservice.rs, called FIRST ininstall()(before the state-dir harden / service create, so a refusal has no side effects). For a system-level install it requires the program's directory to be privileged-owned; refuses withPERMISSION_DENIED(fail-closed on indeterminate owner) otherwise. User-level installs are always allowed.crate::security::dir_is_privileged— the existing spawn-free owner gate shared with the self-heal spawn root (#565) and the TLS material root (#661). No owner-SID logic duplicated; the three checks can never drift.Tests (TDD)
system_level_install_from_a_user_writable_dir_is_refused— a user-owned tempdir as the program dir →PERMISSION_DENIED, message cites#565+ the offending path.user_level_install_is_always_allowed_regardless_of_binary_owner— user-level bypass.service::lib suite green (23 passed).cargo fmt --checkclean,cargo clippy -p dig-node-service -D warningsclean.SemVer
patch
0.37.0 → 0.37.1— afix:security hardening; no public-API or behaviour change beyond refusing a previously-insecure (and non-canonical) install path.Cargo.lockbumped in the same commit (--lockedCI).Cross-repo coherence
SPEC.md §9.2c added (the privileged-target gate). CHANGELOG updated. This is the dig-node slice of the #700 sweep (dig-dns fixed via #23/#24); no other repo touched here.
gitnexus blast radius
install()is reached only from thedig-node installCLI (entrypoint.rsCommand::Install); the new guard is a pre-check with no new callees beyondcrate::security::dir_is_privileged. Behaviour change is confined to the system-level manual-install path (now refused when the binary dir is user-writable); the installer/native-package path is unaffected.Co-Authored-By: Claude noreply@anthropic.com