Skip to content

fix: hand the enrollment token to the sudo re-exec in a file, not argv - #40

Merged
lr00rl merged 3 commits into
integrationfrom
fix/20261018_installer-token-argv
Oct 18, 2026
Merged

lr00rl merged 3 commits into
integrationfrom
fix/20261018_installer-token-argv

Conversation

@lr00rl

@lr00rl lr00rl commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Keeps the enrollment token off sudo's command line. Base: integration at 75e54b2. Not merged, tagged or released. lattice-server #212 (LatticeNet/lattice-server#212) vendors this head.

The problem

Run without root, the installer re-executed itself as exec sudo LATTICE_NODE_TOKEN=<token> ... sh install.sh. sudo stays alive for the whole install, so any local user could read the token in ps for as long as it ran. sudo also writes its command line to its log (auth.log or journald, or a sudo logfile), where the old form left LATTICE_NODE_TOKEN=<token>.

The fix

The token goes to the root child in a file. The non-root parent:

  • makes a 0700 directory with mktemp -d under TMPDIR;
  • writes token into it under umask 077;
  • passes only the path to sudo, as LATTICE_NODE_TOKEN_FILE.

The parent no longer execs sudo. It removes the directory when sudo returns, fails or is interrupted (EXIT, plus HUP, INT and TERM with 129, 130 and 143). It refuses a TMPDIR every user can write to without the sticky bit, before sudo runs, following a symlinked TMPDIR to its target. A file works whatever sudoers keeps or resets of the environment, which --preserve-env does not.

The root child reads only a file the re-exec could have made:

  • in a directory that is not a link, mode 0700, owned by SUDO_UID (0 without sudo);
  • a regular file, not a link, with exactly one link, mode 0600 and the same owner. An ACL is refused with its own message.

Anything else dies before any download. The child reads the token into a variable, removes the file, and removes the directory only if that leaves it empty. It never removes a path it was handed.

Tests

scripts/test-install-integrity.sh records the fake sudo's command line and fails when the token appears on it. Against the installer at 75e54b2 it fails with exactly that message. It also checks:

  • that the re-exec hands over a token file;
  • that the token reaches the service definition;
  • that no token file outlives an install or a sudo that fails like a wrong password;
  • that the child refuses a symlink, a 0644 file, a hard-linked file, a file in a 0755 directory, and a directory owned by another uid, while a refused file is left in place;
  • that a 0777 TMPDIR is refused before sudo runs and before any file is written.

Local, all pass: sh scripts/test-install-integrity.sh and sh scripts/check-release-workflow.sh under macOS /bin/sh and dash, and in debian:bookworm (dash) and alpine:3.20 (busybox ash) as uid 1000.

Review

security-reviewer on 5cea729: no Critical or High. The token no longer reaches argv on any installer path. A third user polling ps every 2 ms during a sudo install never saw it.

Fixed in d5d79c8 and f7deddc, from its Lows:

  • a swap between the checks and the read (now a private directory; a TMPDIR writable by every user without the sticky bit is refused);
  • the link count;
  • the misleading message on an ACL, at the file and at the directory;
  • the signal exit codes.

Not changed here:

  • Medium, the enrollment command. env LATTICE_NODE_TOKEN=... ./lattice-agent-install.sh shows the token in env's argv for about a millisecond, and it lands in shell history and in any wrapper's argv (ssh host '<cmd>', cloud-init). The fix needs a product decision: a single-use enrollment code exchanged by the installer, or a separate no-echo prompt. It is reported to the operator.
  • Low: a kill -9 of the parent during the password prompt leaves the token file behind, now inside a 0700 directory.

Re-checks of d5d79c8 and f7deddc: nothing at Critical, High or Medium. The reviewer ran the installer through sudo on debian:bookworm with several TMPDIRs:

  • refused: 0777 (the rename attack can no longer start), 0776, and a symlink to a 0777 directory;
  • accepted: 1777, a private 0700 directory, and the default /tmp;
  • a default ACL now gets the ACL message.

Left as is, on the reviewer's advice: a group-writable TMPDIR without the sticky bit is accepted. It needs the user to choose such a directory, and its worst effect is a wrong token sent to the real server.

Run without root, the installer re-executed itself as
sudo LATTICE_NODE_TOKEN=<token> ... sh install.sh, and sudo stays alive for
the whole install, so any local user could read the token in ps for as long
as it ran. The token now goes to the root child in a file only the invoking
user can read: mktemp makes it 0600 in TMPDIR, sudo receives only its path
in LATTICE_NODE_TOKEN_FILE, and the child reads it and removes it at once.
The parent no longer execs sudo, so it removes the file too when sudo
returns, fails or is interrupted. A file works whatever sudoers keeps or
resets of the environment, which --preserve-env does not.

The child reads only a file the re-exec could have made: a regular file,
not a symlink, mode 0600 with no ACL, owned by SUDO_UID (or by root without
sudo). Anything else dies before any download.

Tested: test-install-integrity.sh records the fake sudo's command line and
fails when the token is on it (it fails so on the previous installer),
checks that the token reaches the service definition and that no token
file outlives an install or a failed sudo, and refuses a symlinked, a 0644
and a foreign-owned token file; under macOS sh, dash, Debian bookworm and
Alpine 3.20. check-release-workflow.sh passes.
From the security review of the file handover. The token file now sits in a
0700 directory that mktemp -d makes, written under umask 077, so nobody else
can swap it between the root child's checks and its read, even where TMPDIR
lacks the sticky bit. The child checks the directory (mode 0700, owned by
SUDO_UID) and the file (regular, one link, mode 0600, the same owner), says
so plainly when a default ACL on TMPDIR is the cause, removes only the file
and then the directory if it is empty, and never removes a path it was
handed. The parent's traps exit with 129, 130 and 143 for HUP, INT and TERM.

Tested: test-install-integrity.sh also refuses a hard-linked token file and
one in a world-readable directory; under macOS sh, dash, Debian bookworm and
Alpine 3.20. check-release-workflow.sh passes.
…en directory

From the re-check of the private-directory handover. In a TMPDIR every user
can write to without the sticky bit, another user can rename the token
directory away and plant one of their own between the root child's checks
and its read. The installer now refuses such a TMPDIR before running sudo,
following a symlinked TMPDIR to the directory it names. The comment says
what the private directory does close: the file cannot be replaced or
linked. A default ACL on TMPDIR, inherited by the token directory, now gets
the ACL message at the directory check as well.

Tested: test-install-integrity.sh refuses a 0777 TMPDIR before sudo runs and
before any token file is written; under macOS sh, dash, Debian bookworm and
Alpine 3.20. check-release-workflow.sh passes.
@lr00rl
lr00rl merged commit 615a82d into integration Oct 18, 2026
1 check passed
@lr00rl
lr00rl deleted the fix/20261018_installer-token-argv branch October 18, 2026 12:56
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