From 8a516f039cb88b2c8a99dfd6eec65d16c4202f6b Mon Sep 17 00:00:00 2001 From: mk Date: Tue, 28 Jul 2026 05:05:03 +0300 Subject: [PATCH] create-user: passwordless sudo user (NOPASSWD, !tty_tickets, 7d cache) + retarget config; --create-user/--username flags --- modules/05-create-user.sh | 155 ++++++++++++++++++++++++++++++++++++++ setup.sh | 32 +++++--- 2 files changed, 178 insertions(+), 9 deletions(-) create mode 100755 modules/05-create-user.sh diff --git a/modules/05-create-user.sh b/modules/05-create-user.sh new file mode 100755 index 0000000..6d5183e --- /dev/null +++ b/modules/05-create-user.sh @@ -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/: 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" </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" +} \ No newline at end of file diff --git a/setup.sh b/setup.sh index 25d68b9..7e74d9d 100755 --- a/setup.sh +++ b/setup.sh @@ -4,13 +4,20 @@ # Detects distro/arch, lists modular steps, asks y/N per step, runs them. # # Flags: -# --yes run all steps without prompting -# --only NAME run only the step matching NAME -# --skip NAME run all steps except NAME (repeatable in future) -# -h|--help usage +# --yes run all steps without prompting +# --only NAME run only the step matching NAME +# --skip NAME run all steps except NAME +# --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 +# 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)" # shellcheck disable=SC1091 @@ -18,18 +25,25 @@ source "$BOOT_DIR/lib/detect.sh" source "$BOOT_DIR/lib/log.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 case "$1" in - --yes|-y) ALL=1; shift ;; - --only) ONLY="${2:-}"; shift 2 ;; - --skip) SKIP="${2:-}"; shift 2 ;; + --yes|-y) ALL=1; shift ;; + --only) ONLY="${2:-}"; shift 2 ;; + --skip) SKIP="${2:-}"; shift 2 ;; + --create-user) CREATE_USER=1; shift ;; + --username) USERNAME_ARG="${2:-}"; shift 2 ;; -h|--help) - echo "usage: setup.sh [--yes] [--only NAME] [--skip NAME]" + grep -E '^# --' "$0" | sed 's/^# //' exit 0 ;; *) echo "unknown arg: $1" >&2; shift ;; esac done +export CREATE_USER USERNAME_ARG # ---------------------------------------------------------------- collect steps NAMES=(); DESCS=(); RUNS=()