feat(cli): require dedicated host uid for accounts system units (#1873)
This commit is contained in:
1 parent
6db8fb2dce
commit
554bc5116d
18 files changed
+209
-18
No files matched your search
+26
-2
@@ -136,12 +136,19 @@ fi
|
||||
# The user, its uid, primary group and home, from the passwd database.
|
||||
[[ "$USER_NAME" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || die "--user must be a user name: $USER_NAME"
|
||||
PW="$("$GETENT" passwd "$USER_NAME")" || die "no user $USER_NAME in the passwd database"
|
||||
IFS=: read -r _ _ USER_UID USER_GID _ PW_HOME _ <<<"$PW"
|
||||
IFS=: read -r PW_NAME _ USER_UID USER_GID _ PW_HOME PW_SHELL <<<"$PW"
|
||||
[[ "$USER_UID" =~ ^[0-9]+$ ]] && [ "$USER_UID" -ne 0 ] || die "--user must not be root: $USER_NAME"
|
||||
[ "$PW_NAME" = "$USER_NAME" ] || die "passwd name does not match --user"
|
||||
case "$PW_SHELL" in /usr/sbin/nologin|/sbin/nologin|/bin/false|/usr/bin/false) ;; *) die "--user requires a non-login shell" ;; esac
|
||||
abs_plain "$HOME_DIR" '--home'
|
||||
[ "$HOME_DIR" = "$PW_HOME" ] || die "--home $HOME_DIR is not $USER_NAME's home in the passwd database ($PW_HOME)"
|
||||
GR="$("$GETENT" group "$USER_GID")" || die "no group $USER_GID in the group database"
|
||||
GROUP="${GR%%:*}"
|
||||
IFS=: read -r GROUP _ GROUP_GID GROUP_MEMBERS <<<"$GR"
|
||||
[ "$GROUP_GID" = "$USER_GID" ] || die "primary group id mismatch"
|
||||
IFS=, read -r -a PRIMARY_MEMBERS <<<"$GROUP_MEMBERS"
|
||||
for member in "${PRIMARY_MEMBERS[@]}"; do [ -z "$member" ] || [ "$member" = "$USER_NAME" ] || die "primary group must be sole-member"; done
|
||||
PASSWD_ALL="$("$GETENT" passwd)" || die "cannot enumerate primary group members"
|
||||
printf '%s\n' "$PASSWD_ALL" | awk -F: -v gid="$USER_GID" -v user="$USER_NAME" '$4 == gid && $1 != user { bad=1 } $4 == gid && $1 == user { seen++ } END { exit bad || seen != 1 }' || die "primary group must be sole-member"
|
||||
[[ "$GROUP" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || die "the primary group of $USER_NAME is not a group name: $GROUP"
|
||||
|
||||
case "$BUSINESS" in [a-z0-9]*) ;; *) die "--business must be a business id: $BUSINESS" ;; esac
|
||||
@@ -479,6 +486,23 @@ UNIT_PATH="$NODE_DIR"
|
||||
for d in /usr/local/bin /usr/bin /bin; do [ "$d" = "$NODE_DIR" ] || UNIT_PATH="$UNIT_PATH:$d"; done
|
||||
FLAGS="$(serve_flags)" || exit 1
|
||||
|
||||
# The config query runs after dropping uid, through the bounded supervisor.
|
||||
# Root reads only checked facts and stats the two owned directories.
|
||||
HOST_FACTS="$(tester_capture 5 7 4096 "$USER_NAME" "$HOME_DIR" "$UNIT_PATH" "$NODE" --input-type=module -e "$TESTER_QUERY" "$COPY/packages/cli/src/config.mjs" "$COPY/packages/webui/src/cli.mjs")" || die "host config query $(tester_capture_reason "$?")"
|
||||
tester_facts "$HOST_FACTS" || die "host config query has invalid facts"
|
||||
owned_directory() {
|
||||
local dir="$1" what="$2" real st
|
||||
abs_plain "$dir" "$what"
|
||||
[ -d "$dir" ] && [ ! -L "$dir" ] || die "$what directory must exist without a symlink"
|
||||
real="$(realpath -e -- "$dir")" || die "cannot resolve $what directory"
|
||||
[ "$real" = "$dir" ] || die "$what directory contains a symlink"
|
||||
st="$(stat -c '%u %a' -- "$dir")" || die "cannot stat $what directory"
|
||||
[ "${st% *}" = "$USER_UID" ] || die "$what directory must be owned by $USER_NAME uid $USER_UID"
|
||||
(( (8#${st#* } & 8#022) == 0 )) || die "$what directory must not be group or other writable"
|
||||
}
|
||||
owned_directory "$HOME_DIR/.config/mosaic-dev" config
|
||||
owned_directory "$TS_DATA" data-root
|
||||
|
||||
HARDENING="ProtectSystem=strict
|
||||
ReadWritePaths=$HOME_DIR
|
||||
ProtectHome=yes
|
||||
|
||||
@@ -318,6 +318,20 @@ render() {
|
||||
case "$REPO" in *[!A-Za-z0-9/._+-]*) die "repository path contains a character the unit cannot carry: $REPO" ;; esac
|
||||
local root
|
||||
root="$(unit_root)" || exit 1
|
||||
# Accounts instances use dedicated system services, never a human/seat's
|
||||
# systemd user manager. The check runs before rendering or installing.
|
||||
node --input-type=module -e '
|
||||
import { lstatSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
const [root, dir, file] = process.argv.slice(1);
|
||||
const present = p => { try { lstatSync(p); return true; } catch(e) { if(e.code === "ENOENT") return false; throw e; } };
|
||||
let accounts = present(join(root, ".stack-build.json")) || present(join(dir, "accounts.json"));
|
||||
if (present(file)) {
|
||||
const s = JSON.parse(readFileSync(file, "utf8"));
|
||||
accounts ||= Boolean(s.console?.sso) || present(join(s.dataRoot, ".deployed"));
|
||||
}
|
||||
if (accounts) { console.error("webui-service: accounts user-unit path refused; use system units from case2-unit.sh"); process.exit(1); }
|
||||
' "$root" "$MOSAIC_INSTANCE_CONFIG_DIR" "$MOSAIC_INSTANCE_CONFIG" || exit 1
|
||||
if [ "$TESTER_SOCKET" = 1 ]; then
|
||||
grep -q 'MOSAIC_TESTER_SOCKET_V1' "$root/packages/webui/src/cli.mjs" || die "tester socket target lacks support"
|
||||
fi
|
||||
|
||||
Reference in new issue
Block a user