create-user: passwordless sudo user (NOPASSWD, !tty_tickets, 7d cache) + retarget config; --create-user/--username flags

This commit is contained in:
mk 2026-07-28 05:05:03 +03:00
parent a9fff32255
commit 8a516f039c
2 changed files with 178 additions and 9 deletions

155
modules/05-create-user.sh Executable file
View File

@ -0,0 +1,155 @@
#!/usr/bin/env bash
# 05-create-user — create a daily-driver sudo user when running as direct root.
#
# Why this exists: `makepkg` (yay / AUR) refuses to run as root. On a fresh VPS
# that lands you in as root with no SUDO_USER, we create a non-root sudoer,
# copy root's authorized_keys to it, configure passwordless sudo with
# !tty_tickets + a long timestamp_timeout, and retarget the config steps
# (zsh, nvim, nvm, docker group) at that user's $HOME.
#
# Behaviour:
# - Not root -> skip (you're already a normal user).
# - Root + SUDO_USER set -> skip (you sudoed in; that user is the target).
# - Root + no SUDO_USER + Arch -> auto-create (AUR needs it), confirm name.
# - Root + no SUDO_USER + Debian/Ubuntu -> ask y/N (optional, apt works as root).
# - --create-user flag -> force prompt regardless.
# - --username NAME -> override default name, no prompt.
#
# Passwordless, no user password (NOPASSWD). Idempotent: re-runs re-sync
# authorized_keys + sudoers rather than failing.
step_name="create-user"
step_desc="create a sudo user (passwordless) + copy root's ssh keys + retarget config"
step_run="run_create_user"
run_create_user() {
log "create-user step"
# not root -> nothing to do, target stays as current user
if (( EUID != 0 )); then
echo " not root — target stays $USER ($HOME)"
export TARGET_USER="$USER" TARGET_HOME="$HOME"
return
fi
# root but came in via sudo -> use that invoking user, don't create a new one
if [[ -z "${SUDO_USER:-}" ]]; then _handle_direct_root; else _handle_sudoed_root; fi
}
# ── direct root (logged in as root; the case that actually needs a new user) ─
_handle_direct_root() {
local should_create=0
if (( CREATE_USER )); then
should_create=1
elif [[ "$DISTRO" == "arch" ]]; then
# AUR/makepkg cannot run as root — we genuinely need a non-root sudoer.
echo " running as direct root on Arch: a non-root sudoer is needed for yay/AUR."
yn "Create a sudo user now? (recommended)" y && should_create=1
else
# Debian/Ubuntu: apt works as root, so this is convenience (key-first login).
yn "Create a daily-driver sudo user? (optional — apt works as root)" n && should_create=1
fi
if (( ! should_create )); then
echo " skipping user creation — config will target root ($HOME)"
warn "on Arch you won't be able to build AUR packages (yay/angie) as root"
export TARGET_USER="root" TARGET_HOME="$HOME"
return
fi
local user="${USERNAME_ARG:-}"
if [[ -z "$user" ]]; then
local default="mk"
read -rp " username [$default]: " user
[[ -z "$user" ]] && user="$default"
fi
_create_or_sync_user "$user"
}
# ── root via sudo: use the invoking user, don't create another ───────────────
_handle_sudoed_root() {
local user="$SUDO_USER"
local home; home="$(getent passwd "$user" | cut -d: -f6)"
log "running as root via sudo — using invoking user '$user' as target"
# ensure they have passwordless sudo + the ssh key + sudo group, then retarget
_create_or_sync_user "$user" skip_create
export TARGET_USER="$user" TARGET_HOME="$home"
}
# ── the meat: create if missing, sync sudoers, sync authorized_keys ──────────
_create_or_sync_user() { # username [skip_create]
local user="$1"
local skip_create="${2:-}"
if [[ "$skip_create" != "skip_create" ]]; then
if id "$user" >/dev/null 2>&1; then
ok "user '$user' already exists — syncing config only"
else
local shell_bin; shell_bin="$(command -v zsh 2>/dev/null || echo /bin/bash)"
local groups
case "$DISTRO" in
arch) groups="wheel" ;;
debian) groups="sudo,adm" ;;
esac
log "creating user '$user' (shell=$shell_bin, groups=$groups)"
useradd -m -s "$shell_bin" -G "$groups" "$user"
# passwordless account: clear the password field entirely
passwd -d "$user" >/dev/null 2>&1 || true
# unlock the account so SSH key login still works after passwd -d
usermod -p "" "$user" 2>/dev/null || true
ok "user '$user' created (no password set; ssh-key login only)"
fi
fi
local home; home="$(getent passwd "$user" | cut -d: -f6)"
_sync_authorized_keys "$user" "$home"
_install_sudoers "$user"
export TARGET_USER="$user" TARGET_HOME="$home"
ok "config target retargeted: user=$user home=$home"
}
# ── copy root's authorized_keys to the target user (idempotent) ──────────────
_sync_authorized_keys() { # user home
local user="$1" home="$2"
local src="/root/.ssh/authorized_keys"
if [[ ! -f "$src" ]]; then
warn "no /root/.ssh/authorized_keys — add your key manually:"
echo " install -d -m 700 -o $user -g $user $home/.ssh"
echo " # then: paste your pubkey into $home/.ssh/authorized_keys (chmod 600)"
return
fi
install -d -m 700 -o "$user" -g "$user" "$home/.ssh"
install -m 600 -o "$user" -g "$user" "$src" "$home/.ssh/authorized_keys"
local n; n=$(grep -cE '^[^#]' "$src" 2>/dev/null || echo 0)
ok "copied $n authorized key(s) to $home/.ssh/authorized_keys"
warn "sanity-check these are YOUR keys (fingerprints):"
ssh-keygen -lf "$src" 2>/dev/null | sed 's/^/ /' || true
}
# ── write /etc/sudoers.d/<user>: NOPASSWD, !tty_tickets, 7-day cache ─────────
_install_sudoers() { # user
local user="$1" file="/etc/sudoers.d/${user}"
log "configuring sudoers for '$user' (passwordless, !tty_tickets, 7d timeout)"
# write to a temp, validate with visudo -c, then move into place atomically
local tmp; tmp="$(mktemp)"
cat > "$tmp" <<EOF
# managed by bootstrap — passwordless sudo for $user
Defaults:$user !tty_tickets
Defaults:$user timestamp_timeout=10080
$user ALL=(ALL) NOPASSWD:ALL
EOF
# visudo must see proper perms (0440) to be happy
chmod 0440 "$tmp"
if visudo -cf "$tmp" >/dev/null 2>&1; then
install -m 0440 -o root -g root "$tmp" "$file"
ok "sudoers installed: $file"
else
err "visudo -c rejected the sudoers snippet — NOT installed, inspect aborted"
cat "$tmp" | sed 's/^/ /'
fi
rm -f "$tmp"
}

View File

@ -4,13 +4,20 @@
# Detects distro/arch, lists modular steps, asks y/N per step, runs them. # Detects distro/arch, lists modular steps, asks y/N per step, runs them.
# #
# Flags: # Flags:
# --yes run all steps without prompting # --yes run all steps without prompting
# --only NAME run only the step matching NAME # --only NAME run only the step matching NAME
# --skip NAME run all steps except NAME (repeatable in future) # --skip NAME run all steps except NAME
# -h|--help usage # --create-user force creating a sudo user (otherwise Arch auto-creates, Debian asks)
# --username NAME override the sudo user name (default: mk)
# -h|--help usage
set -euo pipefail set -euo pipefail
# Default config target = whoever is running this. The create-user step
# (05-create-user) may override these to point at a freshly-created sudoer.
export TARGET_USER="$USER"
export TARGET_HOME="$HOME"
BOOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" BOOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091 # shellcheck disable=SC1091
@ -18,18 +25,25 @@ source "$BOOT_DIR/lib/detect.sh"
source "$BOOT_DIR/lib/log.sh" source "$BOOT_DIR/lib/log.sh"
source "$BOOT_DIR/lib/prompt.sh" source "$BOOT_DIR/lib/prompt.sh"
ALL=0; ONLY=""; SKIP="" # helper: resolve the target user's real home even when we're root operating
# on a created user (whose home isn't $HOME).
target_home() { getent passwd "${TARGET_USER:-$USER}" | cut -d: -f6; }
ALL=0; ONLY=""; SKIP=""; CREATE_USER=0; USERNAME_ARG=""
while (( $# )); do while (( $# )); do
case "$1" in case "$1" in
--yes|-y) ALL=1; shift ;; --yes|-y) ALL=1; shift ;;
--only) ONLY="${2:-}"; shift 2 ;; --only) ONLY="${2:-}"; shift 2 ;;
--skip) SKIP="${2:-}"; shift 2 ;; --skip) SKIP="${2:-}"; shift 2 ;;
--create-user) CREATE_USER=1; shift ;;
--username) USERNAME_ARG="${2:-}"; shift 2 ;;
-h|--help) -h|--help)
echo "usage: setup.sh [--yes] [--only NAME] [--skip NAME]" grep -E '^# --' "$0" | sed 's/^# //'
exit 0 ;; exit 0 ;;
*) echo "unknown arg: $1" >&2; shift ;; *) echo "unknown arg: $1" >&2; shift ;;
esac esac
done done
export CREATE_USER USERNAME_ARG
# ---------------------------------------------------------------- collect steps # ---------------------------------------------------------------- collect steps
NAMES=(); DESCS=(); RUNS=() NAMES=(); DESCS=(); RUNS=()