Repository navigation
Add a node with one pasted command: sudo mode, stdin token, safe uninstall - #153
Merged
Merged
Conversation
…nloads - Run as root, the node installer prepares the host: it creates the parsar-node system user when missing, adds it to the group that owns the Docker socket or /dev/kvm, and runs the node as a root-owned system service (User=parsar-node, enabled for boot). No lingering or login session is needed. Node work runs in a child with the service user's credentials; root never runs a file that user can write. - It never installs Docker, KVM or packages and never changes device permissions. Missing Docker or KVM, Docker without CPU and memory limits, too little capacity, SELinux enforcing, a foreign parsar-node account, or a node for the same installation already on the host stop it with one line before anything changes. Reruns change nothing. - The token can come on standard input (--enrollment-token-stdin), never in argv, the environment or sudo's log. The installer's local node still passes it in its variable until the installer rework. - --uninstall removes a node only after Core answers 401 to its credential (or with --force when Core is gone). It removes the service, node state and Docker network, and deletes the parsar-node account only when the installer created it and no node remains. It never removes sandboxes, volumes or images. - Without sudo, the installer finds the user's systemd manager from su or sudo -iu. A registered node that Core removed is told to uninstall. - Artifact downloads resume from a private partial file with HTTP Range; the complete file is still verified by size and SHA-256 before use.
docker info's JSON reports CpuCfsQuota, not the Go field name CPUCfsQuota, so every Docker host was refused as lacking CPU limits. Found on mx2.
Found on mx2: - /etc/parsar-node was created 0700 under the command's umask 077, so a no-sudo run could not see a sudo-mode node and failed with a generic error instead of refusing. The directory is now 0755 (records stay 0644), and an unreadable one counts as present. - A removed node's service ends failed (exit 78); uninstall now resets that state so systemd lists no leftover unit. - The service-user step no longer repeats root's first two progress lines.
…de node Review round 1 of the sudo mode: - Uninstall no longer follows links the service user controls. The Core check and the node-file deletion run with parsar-node's own credentials and refuse any path reached through a symbolic link. Root keeps only systemctl, the Docker network, the account, and the home it removes after userdel succeeds. - The Core address for that check comes from the root-owned record, checked like a command-line origin. The retained identity is read without following links and with the owner and size checked on the open file. - The account is validated before any path is derived from it or it is deleted: its recorded uid, home and nologin shell must match. /etc/parsar-node/account.json records whether the installer created or adopted it and which groups it added. An adopted account is left as found, minus those groups. - A microsandbox store (images and sandbox state) is kept on uninstall, with instructions; a created account stays until it is gone. - The service user joins only the docker or kvm group; a device owned by any other group is refused. Every check, including the home directory and a stale account record, runs before useradd. One host-wide lock serializes installs and uninstalls. Root mode sets its own PATH and refuses a token in the environment (sudo would log it). A second node is detected in any account's home. The service-user step reports unexpected failures clearly. - Resumed downloads send If-Range, so a changed file restarts from zero. - Docs state that Docker mode makes the node root-equivalent, that sudo log_input records standard input, the checked uninstall command, and what uninstall keeps.
Found on mx2: the account and the home directory the installer created for it stay; only the groups the installer added are removed.
Review round 2, P1: the child that runs as parsar-node kept root's controlling terminal. A program the service user can replace (ldd, msb, parsar-sandbox-node, a docker CLI plugin) could then open /dev/tty and inject commands into the administrator's root shell with TIOCSTI. The child now starts its own session (setsid) before dropping privileges, takes /dev/null as standard input, and writes to pipes the parent relays, so it holds no terminal and cannot open /dev/tty. The token was already read by the parent and stays in memory. Its own session gets no terminal signals, so the parent stops the child's process group on Ctrl-C or any error and reaps it. The service user's docker calls read a root-owned, empty DOCKER_CONFIG directory, so no CLI plugin or credential helper it controls runs. A test runs as_service_user and checks the new session, /dev/null input and DOCKER_CONFIG.
Review round 2, P3s: - account.json records a created account right after useradd, so a later failure cannot make it look adopted. Adoption is refused for uid or gid 0 and for groups beyond its own, docker and kvm. An adopted home's mode is restored on uninstall. - Root no longer follows the service user's links for decisions. Whether a node is installed comes from root-owned records and the unit. The microsandbox-store check opens each component without following links. The second-node check uses /etc/parsar-node, the invoking user's home and the Docker network instead of statting every home. - Sudo mode serves one Core per host, since its nodes share parsar-node; a second Core's node is refused. - The host-wide lock is created only after the refusal checks, re-checks the records it protects, has a bounded wait, and is shared by non-root installs and uninstalls. - Cleanup instructions use sudo -u parsar-node rm -rf. gpasswd is skipped when the user is no longer a member. The unit uses the account's own primary group (no Group=). uninstall_user tolerates an identity file without core_url. - Tests: node work goes through run_as, one Core per host, the account record written after useradd, adoption criteria, and the adopted home's mode.
…erminal closes The child runs in its own session, so a closed terminal's SIGHUP reached only the root parent. The parent died and left the child running as parsar-node, still downloading and holding the host lock; a broken output pipe even counted as a network error and was retried. While the child runs, the parent now turns SIGINT, SIGHUP and SIGTERM (unless ignored, as under nohup) into an error that stops and reaps the child. The child always reaches its exit, even when its output pipe is closed.
The Core installer no longer enrolls its own host, so local_node.py is gone. Resolve the doc overlaps: node state lives under the home of the account that runs the node, /var/lib/parsar-node for a node added with sudo. The token variable remains only for the Web console's no-sudo command until it sends the token on standard input.
Under nohup, as in the gate, SIGHUP is ignored and the installer rightly leaves it ignored, so the test must restore the terminal default itself.
…mask The Web command runs under umask 077, so root created /run/parsar-node.lock as 0600 and a no-sudo install on the same host could not open it to share the lock; it then went ahead without waiting. Root now sets the lock file to 0644.
…tep further Review round 3 of the sudo-mode node installer: - Relay the child's output through an incremental UTF-8 decoder and replace C0 controls other than tab and newline, DEL and C1 with "?", so nothing the service user controls can write OSC 52, retitle, erase or forge lines on root's terminal. It also writes in the terminal's own encoding and keeps characters split across reads intact. Uninstall prints the kept Runtime image only when provider.json holds an image ID, and store names only as plain text. - After setuid the child joins a new, empty session keyring, so it cannot use the administrator's keys (keyrings(7)). - Block SIGINT, SIGHUP and SIGTERM across the fork; each side installs its own handlers before unblocking. - The child dies with the parent (PR_SET_PDEATHSIG) and exits at once if the parent is already gone. A closed output pipe stops a download instead of being retried as a network error. - stop_child keeps the child a zombie while it waits, then SIGKILLs its whole process group, so programs that ignore SIGTERM end too. - Ctrl-C outside the service-user steps, as while waiting for the host lock, prints the interrupted message instead of a traceback; a child ended by a signal is reported as such. - A download that brings less than 64 KiB in 60 s stops, keeping its part for the next run to resume.
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.
Summary
Phase 2b-node of the new-user install work: add a node by pasting one command. When the node installer runs as root, it prepares the host itself.
deploy/install/node_install.py) picks sudo mode when it runs as root, and keeps the user-service mode otherwise.parsar-nodesystem user, or adopts a matching pre-created one, refusing foreign or dangerous accounts.dockerorkvm, whichever owns the device; privileged groups are refused.User=parsar-node, enabled at boot.parsar-nodein a child that has its own session,/dev/nullon stdin, relayed and sanitized output, and a new session keyring. It is killed if root dies.--enrollment-token-stdinkeeps it out of argv, the environment, logs and the sudo log. Rerunning with a used token is idempotent.--uninstall:--force.parsar-nodeand never follow symlinks.userdel.Review
1273d6d6/220f0675, together with the P3s.0ef0d4bd. Those were verified by tests, the gate and a real pty check, per the small-fix rule.Verification
0ef0d4bd, the exact head, which includes main2b5c99a3, passed. The Playwright browser cases were skipped on the server (Chrome unavailable; approved skip).umask 077and undernohup.parsar-nodeprocess had no controlling terminal (tty=?), and the output shown to root had no escape bytes.Need help on this PR? Tag
@codesmithwith what you need. Autofix is disabled.