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')"