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
73 changes: 69 additions & 4 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down Expand Up @@ -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:-}" \
Expand Down Expand Up @@ -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
Expand Down
66 changes: 64 additions & 2 deletions scripts/test-install-integrity.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 '<key>LATTICE_NODE_TOKEN</key><string>node-token-src</string>' "$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')"
Expand Down
Loading