diff --git a/scripts/install.sh b/scripts/install.sh
index bd3e0a4..52a62b2 100755
--- a/scripts/install.sh
+++ b/scripts/install.sh
@@ -5,7 +5,8 @@
# Everything lives under /opt/lattice (binary, env file, state); nothing is
# scattered into system paths. Idempotent: a re-run upgrades in place. Supports
# `--uninstall`. The enrollment token is kept OUT of the process argv (it is read
-# from the 0600 env file / launchd plist), so it never shows up in `ps`.
+# from the 0600 env file / launchd plist), so it never shows up in `ps`; the
+# sudo re-exec hands it over in a 0600 file, not on sudo's command line.
set -eu
LATTICE_HOME="${LATTICE_HOME:-/opt/lattice}"
@@ -69,10 +70,39 @@ if [ "$(id -u)" -ne 0 ]; then
echo "lattice-agent installer needs root to write $LATTICE_HOME and register a boot service."
if have sudo; then
log "re-running with sudo ..."
- exec sudo \
+ # The enrollment token never rides sudo's command line, which any local
+ # user can read in ps (and sudo writes to its log) for as long as the
+ # install runs. It goes to the root child in a 0600 file inside a 0700
+ # directory of this user's, which the child checks, reads and removes;
+ # this shell removes the directory too when sudo returns or is
+ # interrupted. A file works whatever sudoers keeps or resets of the
+ # environment. The private directory means nobody else can replace or
+ # link the file between the child's checks and its read. Its own name is
+ # safe only in a TMPDIR other users cannot write to, or one with the
+ # sticky bit, so any other TMPDIR is refused.
+ token_dir=""
+ token_file=""
+ if [ -n "$token" ]; then
+ # The trailing slash makes ls describe the directory a symlink points at.
+ case "$(ls -ldn "${TMPDIR:-/tmp}/" 2>/dev/null | awk '{ print $1 }')" in
+ d???????w[tT]*) ;;
+ d???????w*) die "TMPDIR ${TMPDIR:-/tmp} is writable by every user without the sticky bit; set TMPDIR to a private directory" ;;
+ esac
+ token_dir="$(mktemp -d "${TMPDIR:-/tmp}/lattice-agent-token.XXXXXX")" || \
+ die "cannot create a private directory to hand the enrollment token to sudo"
+ trap 'rm -rf "$token_dir"' EXIT
+ trap 'rm -rf "$token_dir"; exit 129' HUP
+ trap 'rm -rf "$token_dir"; exit 130' INT
+ trap 'rm -rf "$token_dir"; exit 143' TERM
+ chmod 0700 "$token_dir" || die "cannot make $token_dir private"
+ token_file="$token_dir/token"
+ ( umask 077; printf '%s' "$token" >"$token_file" ) || die "cannot write the enrollment token to $token_file"
+ fi
+ sudo_rc=0
+ sudo \
LATTICE_HOME="$LATTICE_HOME" LATTICE_AGENT_REPO="$repo" LATTICE_AGENT_VERSION="$version" \
LATTICE_AGENT_SOURCES="${LATTICE_AGENT_SOURCES:-}" LATTICE_AGENT_SHA256="${LATTICE_AGENT_SHA256:-}" \
- LATTICE_SERVER="$server" LATTICE_NODE_ID="$node_id" LATTICE_NODE_TOKEN="$token" \
+ LATTICE_SERVER="$server" LATTICE_NODE_ID="$node_id" LATTICE_NODE_TOKEN_FILE="$token_file" \
LATTICE_AGENT_BIN="$bin_path" LATTICE_AGENT_ENV="$env_path" LATTICE_AGENT_STATE="$state_dir" \
LATTICE_AGENT_SERVICE="$service_name" \
LATTICE_AGENT_RUN_USER="${LATTICE_AGENT_RUN_USER:-}" \
@@ -100,11 +130,46 @@ if [ "$(id -u)" -ne 0 ]; then
LATTICE_PROXY_USAGE_FILE="${LATTICE_PROXY_USAGE_FILE:-}" LATTICE_PROXY_USAGE_URL="${LATTICE_PROXY_USAGE_URL:-}" \
LATTICE_PROXY_USAGE_XRAY_API="${LATTICE_PROXY_USAGE_XRAY_API:-}" LATTICE_PROXY_USAGE_XRAY_BIN="${LATTICE_PROXY_USAGE_XRAY_BIN:-}" \
LATTICE_PROXY_USAGE_XRAY_PATTERN="${LATTICE_PROXY_USAGE_XRAY_PATTERN:-}" \
- sh "$0" "$@"
+ sh "$0" "$@" || sudo_rc=$?
+ [ -z "$token_dir" ] || rm -rf "$token_dir"
+ exit "$sudo_rc"
fi
die "please re-run as root (e.g. sudo sh $0)"
fi
+# ---- the enrollment token handed over by the sudo re-exec ---------------------
+# Only a file the re-exec above could have made is read: in a directory only
+# the user who ran sudo (or root, without sudo) can enter, a regular file, not
+# a link, with one name, mode 0600 and the same owner. Nobody else can then
+# swap it between these checks and the read. The file is removed as soon as it
+# is read, and its directory only when that leaves it empty.
+if [ -n "${LATTICE_NODE_TOKEN_FILE:-}" ]; then
+ token_file="$LATTICE_NODE_TOKEN_FILE"
+ unset LATTICE_NODE_TOKEN_FILE
+ token_owner="${SUDO_UID:-0}"
+ case "$token_owner" in ""|*[!0-9]*) die "SUDO_UID is not a user id: $token_owner" ;; esac
+ token_dir="$(dirname -- "$token_file")"
+ # mode, link count and numeric owner, as ls -ln prints them everywhere
+ ls_mode_links_owner() { ls -ldn "$1" 2>/dev/null | awk '{ print $1 " " $2 " " $3 }'; }
+ [ -d "$token_dir" ] && [ ! -L "$token_dir" ] || \
+ die "LATTICE_NODE_TOKEN_FILE is not in a directory: $token_file"
+ case "$(ls_mode_links_owner "$token_dir")" in
+ "drwx------ "*" $token_owner"|"drwx------. "*" $token_owner"|"drwx------@ "*" $token_owner") ;;
+ "drwx------+ "*) die "LATTICE_NODE_TOKEN_FILE is in a directory that carries an ACL, likely a default ACL on TMPDIR; set TMPDIR to a directory without one: $token_dir" ;;
+ *) die "LATTICE_NODE_TOKEN_FILE must be in a directory only uid $token_owner can enter (mode 0700): $token_dir" ;;
+ esac
+ [ -f "$token_file" ] && [ ! -L "$token_file" ] || \
+ die "LATTICE_NODE_TOKEN_FILE is not a regular file: $token_file"
+ case "$(ls_mode_links_owner "$token_file")" in
+ "-rw------- 1 $token_owner"|"-rw-------. 1 $token_owner"|"-rw-------@ 1 $token_owner") ;;
+ "-rw-------+ "*) die "LATTICE_NODE_TOKEN_FILE carries an ACL, likely a default ACL on TMPDIR; set TMPDIR to a directory without one: $token_file" ;;
+ *) die "LATTICE_NODE_TOKEN_FILE must be mode 0600, with one link, owned by uid $token_owner: $token_file" ;;
+ esac
+ token="$(cat "$token_file")" || die "cannot read LATTICE_NODE_TOKEN_FILE: $token_file"
+ rm -f "$token_file"
+ rmdir "$token_dir" 2>/dev/null || true
+fi
+
# ---- existing install adoption ---------------------------------------------
# New installs use:
# binary: /opt/lattice/lattice-agent
diff --git a/scripts/test-install-integrity.sh b/scripts/test-install-integrity.sh
index 7fdb035..e645ec1 100755
--- a/scripts/test-install-integrity.sh
+++ b/scripts/test-install-integrity.sh
@@ -654,19 +654,81 @@ if [ "${1:-}" = "-u" ]; then
fi
exit 1
SH
+# It records its command line, which any local user could read in ps, and
+# sets SUDO_UID as sudo does; FAKE_SUDO_FAIL makes it fail as a wrong password
+# would.
cat >"$SUDO_BIN/sudo" <<'SH'
#!/usr/bin/env sh
+printf '%s\n' "$*" >>"$FAKE_CALLS.sudo"
+[ -z "${FAKE_SUDO_FAIL:-}" ] || exit 1
exec env -i PATH="$PATH" FAKE_ROOT=1 FAKE_DIR="$FAKE_DIR" FAKE_CALLS="$FAKE_CALLS" \
- LATTICE_AGENT_PLIST="$LATTICE_AGENT_PLIST" "$@"
+ SUDO_UID="$(/usr/bin/id -u)" LATTICE_AGENT_PLIST="$LATTICE_AGENT_PLIST" "$@"
SH
chmod +x "$SUDO_BIN/id" "$SUDO_BIN/sudo"
-SRC_PATH="$SUDO_BIN:/usr/bin:/bin" run_src sudo LATTICE_AGENT_SHA256="$TABLE" \
+mkdir -p "$SRC_TMP/sudo-tmp" "$SRC_TMP/sudo-fail-tmp"
+SRC_PATH="$SUDO_BIN:/usr/bin:/bin" run_src sudo TMPDIR="$SRC_TMP/sudo-tmp" LATTICE_AGENT_SHA256="$TABLE" \
LATTICE_AGENT_SOURCES="m=https://mirror-good.example.com/agent $REL" || src_fail sudo "install failed"
SRC_PATH=""
expect_ok sudo
expect_log sudo "re-running with sudo"
expect_calls sudo 1
expect_call sudo 1 "https://mirror-good.example.com/$MIRROR_PATH" 0
+# The enrollment token never rides sudo's command line. The root child reads
+# it from the 0600 file the re-exec made, installs it, and the file is gone.
+[ -s "$SRC_TMP/sudo.calls.sudo" ] || src_fail sudo "the fake sudo recorded no command line"
+if grep -Fq node-token-src "$SRC_TMP/sudo.calls.sudo"; then
+ src_fail sudo "the enrollment token was on sudo's command line: $(cat "$SRC_TMP/sudo.calls.sudo")"
+fi
+grep -Fq "LATTICE_NODE_TOKEN_FILE=$SRC_TMP/sudo-tmp/lattice-agent-token." "$SRC_TMP/sudo.calls.sudo" || \
+ src_fail sudo "the re-exec handed over no token file"
+grep -Fq 'LATTICE_NODE_TOKENnode-token-src' "$SRC_TMP/sudo/agent.plist" || \
+ src_fail sudo "the token from the file did not reach the service definition"
+if ls "$SRC_TMP/sudo-tmp" | grep -q lattice-agent-token; then src_fail sudo "the token file outlived the install"; fi
+
+# A sudo that fails (a wrong password) leaves no token file behind.
+if SRC_PATH="$SUDO_BIN:/usr/bin:/bin" run_src sudo-fail TMPDIR="$SRC_TMP/sudo-fail-tmp" FAKE_SUDO_FAIL=1; then
+ SRC_PATH=""
+ src_fail sudo-fail "the installer succeeded without root"
+fi
+SRC_PATH=""
+[ -z "$(ls "$SRC_TMP/sudo-fail-tmp")" ] || src_fail sudo-fail "the token file outlived a failed sudo"
+
+# A TMPDIR every user can write to without the sticky bit would let another
+# user swap the token directory itself: the installer refuses it before sudo.
+mkdir -p "$SRC_TMP/sudo-open-tmp" && chmod 0777 "$SRC_TMP/sudo-open-tmp"
+if SRC_PATH="$SUDO_BIN:/usr/bin:/bin" run_src sudo-open TMPDIR="$SRC_TMP/sudo-open-tmp"; then
+ SRC_PATH=""
+ src_fail sudo-open "the installer handed the token over through a TMPDIR anyone can rename in"
+fi
+SRC_PATH=""
+expect_log sudo-open "is writable by every user without the sticky bit"
+[ ! -s "$SRC_TMP/sudo-open.calls.sudo" ] || src_fail sudo-open "sudo ran before the refusal"
+[ -z "$(ls "$SRC_TMP/sudo-open-tmp")" ] || src_fail sudo-open "a token file was written"
+
+# The root child reads only a file the re-exec could have made: in a 0700
+# directory of the user who ran sudo, a regular file, not a link, with one
+# name, mode 0600 and the same owner.
+TOKEN_DIR="$SRC_TMP/token-files"
+mkdir -p "$TOKEN_DIR" "$SRC_TMP/token-open-dir"
+chmod 0700 "$TOKEN_DIR"
+chmod 0755 "$SRC_TMP/token-open-dir"
+MY_UID="$(/usr/bin/id -u)"
+printf 'node-token-file' >"$TOKEN_DIR/ok" && chmod 0600 "$TOKEN_DIR/ok"
+ln -s "$TOKEN_DIR/ok" "$TOKEN_DIR/link"
+printf 'node-token-file' >"$TOKEN_DIR/open" && chmod 0644 "$TOKEN_DIR/open"
+printf 'node-token-file' >"$TOKEN_DIR/twice" && chmod 0600 "$TOKEN_DIR/twice" && ln "$TOKEN_DIR/twice" "$TOKEN_DIR/twice-again"
+printf 'node-token-file' >"$SRC_TMP/token-open-dir/ok" && chmod 0600 "$SRC_TMP/token-open-dir/ok"
+expect_refused tokfile-link "LATTICE_NODE_TOKEN_FILE is not a regular file" \
+ LATTICE_NODE_TOKEN="" LATTICE_NODE_TOKEN_FILE="$TOKEN_DIR/link" SUDO_UID="$MY_UID"
+expect_refused tokfile-mode "LATTICE_NODE_TOKEN_FILE must be mode 0600" \
+ LATTICE_NODE_TOKEN="" LATTICE_NODE_TOKEN_FILE="$TOKEN_DIR/open" SUDO_UID="$MY_UID"
+expect_refused tokfile-links "with one link" \
+ LATTICE_NODE_TOKEN="" LATTICE_NODE_TOKEN_FILE="$TOKEN_DIR/twice" SUDO_UID="$MY_UID"
+expect_refused tokfile-dir "in a directory only uid $MY_UID can enter" \
+ LATTICE_NODE_TOKEN="" LATTICE_NODE_TOKEN_FILE="$SRC_TMP/token-open-dir/ok" SUDO_UID="$MY_UID"
+expect_refused tokfile-owner "only uid 4242 can enter" \
+ LATTICE_NODE_TOKEN="" LATTICE_NODE_TOKEN_FILE="$TOKEN_DIR/ok" SUDO_UID=4242
+[ -f "$TOKEN_DIR/ok" ] || { echo "a refused token file was removed" >&2; exit 1; }
# Malformed tables and sources die before any download.
upper_sha="$(printf '%s' "$GOOD_SHA" | tr 'abcdef' 'ABCDEF')"