From 0c0341a23c44b26c52e950e85dad3bf0c9b34f2d Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Sat, 26 Sep 2026 04:38:10 +0000 Subject: [PATCH] Retire PARSAR_NODE_ENROLLMENT_TOKEN in the node installer Web's Add node command passes the token on standard input in both modes (#148), and the Core installer no longer sets the variable (#152). The node installer now refuses the variable in every mode, with a one-line message naming --enrollment-token-stdin, instead of reading it. Standard input and the hidden prompt remain. A new node without a token is told to pass it on standard input. The operations upgrade table records the retirement. --- deploy/install/node_install.py | 24 ++++++++-------- deploy/install/node_spec.py | 2 +- deploy/install/test_node_install.py | 28 ++++++++++--------- docs/getting-started/operations.md | 1 + services/agents-api/HOSTED-SANDBOX-MANAGER.md | 4 +-- 5 files changed, 31 insertions(+), 28 deletions(-) diff --git a/deploy/install/node_install.py b/deploy/install/node_install.py index 72ebe01c5..941c55d6e 100644 --- a/deploy/install/node_install.py +++ b/deploy/install/node_install.py @@ -1323,17 +1323,15 @@ def wait_ready(root, args, timeout=60): raise InstallError(detail + "; state and service are retained. Inspect " + journal + ", then rerun the installation command") -def read_token(args, parser): - """The one-time token comes on standard input, never in argv or a sudo command line.""" - # Until the Web console's command sends it on standard input, its no-sudo form passes it here. - environment = os.environ.pop("PARSAR_NODE_ENROLLMENT_TOKEN", None) - if args.enrollment_token_stdin and environment is not None: - parser.error("pass the enrollment token on standard input only") - if environment is not None and os.geteuid() == 0: - # `sudo VAR=... python3` would put the token in sudo's command line and log. - parser.error("with sudo or as root, pass the enrollment token on standard input with --enrollment-token-stdin") +# A token in the environment could reach sudo's log (`sudo VAR=... python3`) and every +# program the installer starts; it is refused rather than read. +RETIRED_TOKEN_VARIABLE = "PARSAR_NODE_ENROLLMENT_TOKEN" + + +def read_token(args): + """The one-time token comes on standard input, never in argv, the environment or a sudo command line.""" if not args.enrollment_token_stdin: - return environment or "" + return "" # A registered node's rerun uses its retained credential. if sys.stdin.isatty(): return getpass.getpass("Enrollment token: ").strip() return sys.stdin.readline(4098).strip() @@ -1351,12 +1349,14 @@ def main(argv=None): parser.add_argument("--uninstall", action="store_true", help="Remove this host's node after it was removed on the Nodes page") parser.add_argument("--force", action="store_true", help="With --uninstall: skip the Core check, for a Core that no longer exists") args = parser.parse_args(argv) + if RETIRED_TOKEN_VARIABLE in os.environ: + parser.exit(2, RETIRED_TOKEN_VARIABLE + " is retired: pass the enrollment token on standard input with " + "--enrollment-token-stdin.\n") if str(uuid.UUID(args.installation_id)) != args.installation_id: raise InstallError("Installation ID must be a canonical UUID") if args.uninstall: if args.source_url or args.bundle or args.core_url or args.provider or args.enrollment_token_stdin: parser.error("--uninstall takes only --installation-id and --force") - os.environ.pop("PARSAR_NODE_ENROLLMENT_TOKEN", None) (uninstall_system if os.geteuid() == 0 else uninstall_user)(args) return if args.force: @@ -1365,7 +1365,7 @@ def main(argv=None): parser.error("--source-url (or --bundle) and --core-url are required") if args.bundle is not None and (not args.bundle.is_absolute() or args.bundle.resolve() != args.bundle): raise InstallError("Local bundle must be an absolute directory without symlinks") - token = read_token(args, parser) + token = read_token(args) if len(token) > 4096 or any(c.isspace() for c in token): raise InstallError("A valid one-time enrollment credential is required") if os.geteuid() == 0: diff --git a/deploy/install/node_spec.py b/deploy/install/node_spec.py index 7017dcd29..56af4e6f3 100644 --- a/deploy/install/node_spec.py +++ b/deploy/install/node_spec.py @@ -91,7 +91,7 @@ def fetch(args, token, retained, open_request, allow_enrollment=False): except (KeyError, ValueError, TypeError, AttributeError): raise SpecificationError("Retained node identity differs or is invalid; preserve its state") from None elif not token: - raise SpecificationError("A one-time enrollment credential is required for a new node") + raise SpecificationError("A new node needs its one-time enrollment token on standard input (--enrollment-token-stdin); copy the command from Add node") request = urllib.request.Request(args.core_url + "/api/v1/sandbox-node/configuration", headers=headers) try: try: diff --git a/deploy/install/test_node_install.py b/deploy/install/test_node_install.py index 950205811..a5f2b01c2 100644 --- a/deploy/install/test_node_install.py +++ b/deploy/install/test_node_install.py @@ -343,12 +343,6 @@ def test_symlink_installation_is_rejected(self): self.install() self.assertEqual(list(elsewhere.iterdir()), []) - def test_main_removes_token_environment_before_any_subprocess(self): - with mock.patch.dict(os.environ, {"PARSAR_NODE_ENROLLMENT_TOKEN": "synthetic-once-token"}): - installer.main(["--source-url", self.args.source_url, "--core-url", self.args.core_url, - "--provider", "docker", "--installation-id", self.args.installation_id]) - self.assertNotIn("PARSAR_NODE_ENROLLMENT_TOKEN", os.environ) - def sudo_host(self, enforcing=False): """Sudo mode against temporary system paths; the service-user step runs in-process.""" system = self.home / "system" @@ -636,13 +630,21 @@ def test_token_comes_on_standard_input_only(self): mock.patch.object(installer.os, "geteuid", return_value=1000): installer.main(arguments) self.assertEqual(install.call_args.args[1], "synthetic-once-token") - with mock.patch.dict(os.environ, {"PARSAR_NODE_ENROLLMENT_TOKEN": "other"}), self.assertRaises(SystemExit): - installer.main(arguments) - # As root, a token in the environment would come from `sudo VAR=...`, which sudo logs. - with mock.patch.dict(os.environ, {"PARSAR_NODE_ENROLLMENT_TOKEN": "other"}), mock.patch.object(installer.os, "geteuid", return_value=0), \ - mock.patch.object(installer, "install_system") as install_system, self.assertRaises(SystemExit): - installer.main(arguments[:-1]) - install_system.assert_not_called() + + def test_the_retired_token_variable_is_refused_in_every_mode(self): + install = ["--source-url", self.args.source_url, "--core-url", self.args.core_url, + "--installation-id", self.args.installation_id, "--enrollment-token-stdin"] + for euid in (1000, 0): + for arguments in (install, install[:-1], ["--uninstall", "--installation-id", self.args.installation_id]): + errors = io.StringIO() + with mock.patch.dict(os.environ, {"PARSAR_NODE_ENROLLMENT_TOKEN": "synthetic-once-token"}), \ + mock.patch.object(installer.os, "geteuid", return_value=euid), mock.patch.object(installer.sys, "stderr", errors), \ + mock.patch.multiple(installer, install=mock.DEFAULT, install_system=mock.DEFAULT, uninstall_user=mock.DEFAULT, + uninstall_system=mock.DEFAULT) as steps, self.assertRaises(SystemExit): + installer.main(arguments) + self.assertEqual(errors.getvalue().splitlines(), ["PARSAR_NODE_ENROLLMENT_TOKEN is retired: pass the enrollment " + "token on standard input with --enrollment-token-stdin."]) + self.assertFalse(any(step.called for step in steps.values())) def test_user_manager_bus_is_found_without_a_login_session(self): runtime = self.home / "run-user" diff --git a/docs/getting-started/operations.md b/docs/getting-started/operations.md index deba79b67..3f1ddfc71 100644 --- a/docs/getting-started/operations.md +++ b/docs/getting-started/operations.md @@ -149,6 +149,7 @@ services and rename them first: | `--admin-token-file` | `--core-key-file` | `install.sh --web-only` flag; the installer rejects the old flag | | `AGENTS_API_DAEMON_WS_URL` (a `wss://…/api/v1/agent-daemon/ws` URL) | `AGENTS_API_PUBLIC_URL` (the origin only, such as `https://core.example`) | `config/core.env` | | `AGENTS_API_CONFIG_FILE` | None; delete the line | `config/core.env` | +| `PARSAR_NODE_ENROLLMENT_TOKEN` | The token on standard input with `--enrollment-token-stdin`, as Web's Add node command does | Node installer; it refuses the variable | Also remove `CORE_CONSOLE_AUTH_MODE`, `CORE_CONSOLE_STATE_DIR` and `CORE_CONSOLE_PASSWORD_FILE` from Web's environment, with their `state/console` diff --git a/services/agents-api/HOSTED-SANDBOX-MANAGER.md b/services/agents-api/HOSTED-SANDBOX-MANAGER.md index 1c3901876..71d6370be 100644 --- a/services/agents-api/HOSTED-SANDBOX-MANAGER.md +++ b/services/agents-api/HOSTED-SANDBOX-MANAGER.md @@ -196,8 +196,8 @@ The installer chooses how the node runs from the user that runs it: mode. Add Docker nodes only on hosts dedicated to running sandboxes. Microsandbox nodes need only the `kvm` group. - Pass the token on standard input, as the command does. In sudo mode the installer - refuses `PARSAR_NODE_ENROLLMENT_TOKEN`, because `sudo VAR=… python3` records the + Pass the token on standard input, as the command does. The installer refuses a + token in the environment, because `sudo VAR=… python3` records the variable in sudo's log. A sudoers policy with `log_input` records standard input as well; the token is single-use and expires after ten minutes. - **As a normal user (for hosts without sudo).** The node runs as that user's