Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions deploy/install/node_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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:
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion deploy/install/node_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
28 changes: 15 additions & 13 deletions deploy/install/test_node_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions docs/getting-started/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
4 changes: 2 additions & 2 deletions services/agents-api/HOSTED-SANDBOX-MANAGER.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading