Skip to content

fix(service): gate system-level install against a user-writable binary (#565 LPE)#46

Merged
MichaelTaylor3d merged 2 commits into
mainfrom
fix/service-execstart-ownership
Jul 17, 2026
Merged

fix(service): gate system-level install against a user-writable binary (#565 LPE)#46
MichaelTaylor3d merged 2 commits into
mainfrom
fix/service-execstart-ownership

Conversation

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor

TLDR

Closes dig-node's part of the #700 §565 service-ExecStart LPE sweep. dig-node's install was vulnerable on system-level (Windows LocalSystem) installs: it registered the service with program = current_exe() in place, with no owner check on that binary's directory. If a user ran an elevated dig-node install from a user-writable location (e.g. C:\Users\alice\Downloads\dig-node.exe), the LocalSystem service's binPath pointed 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

  • Windows (SCM): system-level only, runs as LocalSystem. install recorded current_exe() as binPath with no owner verification → VULNERABLE (§565 LPE). Fixed.
  • Linux (systemd) / macOS (launchd): install at user level by default (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.
  • Installer path: the canonical native OS packages (§9.7) place the binary in a protected admin-owned dir (%ProgramFiles%\DIG Network\dig-node\, /usr/…), so they satisfy the gate. The exposure was the manual dig-node install path from a user-writable dir, which is now refused with a clear message pointing the operator at the installer/native package.

Fix

  • New ensure_service_target_is_safe(program, user_level) in service.rs, called FIRST in install() (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 with PERMISSION_DENIED (fail-closed on indeterminate owner) otherwise. User-level installs are always allowed.
  • Reuses 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.
  • Full service:: lib suite green (23 passed). cargo fmt --check clean, cargo clippy -p dig-node-service -D warnings clean.

SemVer

patch 0.37.0 → 0.37.1 — a fix: security hardening; no public-API or behaviour change beyond refusing a previously-insecure (and non-canonical) install path. Cargo.lock bumped in the same commit (--locked CI).

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 the dig-node install CLI (entrypoint.rs Command::Install); the new guard is a pre-check with no new callees beyond crate::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

MichaelTaylor3d and others added 2 commits July 16, 2026 17:42
…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>
@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

Follow-up commit 450262c: added a default-off DIG_NODE_ALLOW_INSECURE_SERVICE_TARGET opt-out. Rationale: the service-smoke CI job installs the freshly-built target/release/dig-node from the runner's user-writable checkout; on Windows that is a system-level (LocalSystem) registration, which the new §565 gate correctly refuses. The explicit env opt-out (truthy 1/true/yes, warns loudly) permits that controlled test/dev install of an unreleased build and is set ONLY in the smoke workflow. End-user installs (native OS package into a protected admin-owned dir) never set it. SPEC §9.2c documents it; covered by insecure_override_permits_* + insecure_override_env_parses_only_truthy_values tests.

@MichaelTaylor3d
MichaelTaylor3d merged commit 238d6fe into main Jul 17, 2026
16 checks passed
@MichaelTaylor3d
MichaelTaylor3d deleted the fix/service-execstart-ownership branch July 17, 2026 01:16
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>
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