fix(installer): remove gateway-unit root-escalation; harden chown reclaim#174
Draft
ndee wants to merge 1 commit into
Draft
fix(installer): remove gateway-unit root-escalation; harden chown reclaim#174ndee wants to merge 1 commit into
ndee wants to merge 1 commit into
Conversation
…laim The installer granted the non-root SERVICE_USER passwordless `sudo tee /etc/systemd/system/sovereign-openclaw-gateway.service` plus `systemctl daemon-reload`/`enable --now`. Because tee writes whatever is piped to it, the service user controlled the unit's content — it could write `User=root` and an arbitrary `ExecStart=` and then enable it, so systemd ran the payload as root. That turned any service-user foothold (the email/Matrix/agent surface this non-root account exists to contain) into trivial, reliable root, defeating the whole privilege boundary. Replace the raw tee + enable grants with a root-owned validating helper (mirrors the existing install-docker.sh pattern): - scripts/install/install-gateway-unit.sh reads the proposed unit on stdin, FORCES User=/Group= to the trusted service identity (read from a root-owned /etc/sovereign-node/gateway-service-identity the service user cannot write), rejects a root identity, writes only the canonical unit path without following a symlink, and runs daemon-reload + enable as root. - The service user's sudoers now grants only that helper plus non-escalating systemctl restart/is-active/status; the tee grant and standalone daemon-reload/enable grants are removed. - real-service.ts uses the helper on the EACCES/EPERM fallback and skips the now-ungranted daemon-reload/enable when the helper handled them. The direct (root bootstrap) write path is unchanged. Also harden the related chown reclaim: the glob `sudo chown -R [0-9]*:[0-9]* .../bundled-matrix/*` and `.../secrets/*` grants let a planted symlink redirect the recursive chown. Replace them with scripts/install/chown-reclaim.sh, which takes an allowlist KEY (not a path), maps it to a fixed canonical path, and refuses symlinked targets. sudoReclaimOwnership passes a key instead of a raw path. Tests: new dependency-free bash suite (15 assertions) proves a malicious User=root unit is normalized to the service user, root identity / symlink / empty stdin are refused, and chown-reclaim rejects unknown keys, non-numeric ids, and missing/symlinked targets. shellcheck clean. biome, tsc, full unit suite (198 tests, thresholds held), and build all pass. End-to-end fresh-install + upgrade validated on an ephemeral VM.
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.
Problem
The installer drops a sudoers fragment granting the non-root
SERVICE_USERpasswordless:sudo tee /etc/systemd/system/sovereign-openclaw-gateway.servicesudo systemctl daemon-reload/enable --nowon that unitsudo chown -R [0-9]*:[0-9]* …/bundled-matrix/*and…/secrets/*Because
teewrites whatever is piped to it, the service user controlled the unit's content — it could writeUser=root+ an arbitraryExecStart=andenable --now, so systemd runs the payload as root. Any foothold as the service user (the email/Matrix/agent surface this non-root account exists to contain) becomes trivial, reliable root — defeating the entire privilege boundary. Separately, the globchown -Rlets a planted symlink redirect ownership changes outside the intended subtree.Fix — root-owned validating helper
Mirrors the existing
install-docker.shhelper pattern.scripts/install/install-gateway-unit.sh(new, root-owned):User=/Group=to the trusted service identity (read from root-owned/etc/sovereign-node/gateway-service-identity, which the service user cannot write),[Service]section,daemon-reload+enableas root.Sudoers (
scripts/install/lib-build.sh): the service user is now granted only the helper + non-escalatingsystemctl restart/is-active/status. Theteegrant and standalonedaemon-reload/enablegrants are removed.src/installer/real-service.ts: the EACCES/EPERM fallback pipes the unit to the helper (instead ofsudo tee) and skips the now-ungranteddaemon-reload/enablewhen the helper handled them. The direct (root bootstrap) write path is unchanged.chown hardening (same fragment)
scripts/install/chown-reclaim.sh(new): takes an allowlist key (not a path), maps it to a fixed canonical path, validates uid/gid are numeric, and refuses symlinked / non-directory targets.sudoReclaimOwnershippasses a key. The four globchown -Rsudoers lines are replaced by a single helper grant.Tests
scripts/install/install-gateway-unit.test.sh— 15 dependency-free bash assertions: the issue'sUser=root+ setuid-payload unit is normalized to the service user (not honored); root identity / symlink target / empty stdin refused; chown-reclaim rejects unknown keys, non-numeric ids, and missing/symlinked targets.shellcheck v0.11.0clean on all three new scripts;bash -n+visudo -cvalidate the generated fragment.biome,tsc --noEmit, full unit suite (198 tests, coverage thresholds held), andbuildpass. The existing gateway-fallback test still asserts the direct path writesUser=sovereign-nodeand issuesdaemon-reload/enable --now.Validation
End-to-end fresh-install + upgrade on an ephemeral Proxmox dev VM (in progress; results posted before marking ready), asserting the removed
teegrant is denied, the malicious-unit exploit no longer yields a root unit, the gateway still starts healthy, and symlink-redirected chown is refused.Notes
sudo teeEACCES fallback had no unit coverage either; the privileged fallback is covered by the bash suite + dev-VM run.