create the user account first, and fix the sudoers filename bug that found
The user account section is now the first thing that acts, ahead of disk space.
The reason is a real defect, not tidiness. If the account does not exist yet,
USER_HOME is a path that is not there — and the ballast offers to put its file in
it, where ballast_create's `mkdir -p` runs as root and creates /home/<name> owned
by root:root. adduser afterwards finds the directory already present and does not
populate or chown it, so the account ends up with a home it cannot write to.
Making the account before any step can write into its home removes the ordering
entirely.
USER_HOME is re-read from getent after adduser runs. Until that point it is the
/home/<name> guess, because there is nothing to look up; adduser is free to have
used something else and every later step writes there.
Found while testing that, and worse than the thing it was testing:
local user="$1" dest="/etc/sudoers.d/99-${user}-nopasswd"
bash expands ${user} before the assignment to user has happened, so dest came out
as /etc/sudoers.d/99--nopasswd with the name missing. The rule inside was correct,
which is what made it invisible — visudo passes, sudo works, and the account
really does get passwordless sudo. What breaks is everything around it: every
account granted this way writes to that same file, so a second grant silently
overwrites the first and revokes it; and has_passwordless_sudo looks for
99-<user>-nopasswd, never finds it, and re-grants on every run forever.
Split into separate declarations, with the reason recorded where it happened, and
an empty username is now refused outright. Scanned the other lib files for the
same shape — the remaining multi-assignment locals only read positional
parameters, which is safe.
The stray /etc/sudoers.d/99--nopasswd this created on the dev box during testing
has been removed and visudo -c re-verified.
Verified with a real throwaway account: correctly reports not-granted before,
writes 99-msdemo-nopasswd as root:root 0440 with the right rule, reports granted
after, and leaves sudoers valid.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -285,9 +285,18 @@ as_user() {
|
||||
# places it with its mode in a single install(1) — so what lands in /etc is
|
||||
# already known good and already 0440.
|
||||
grant_passwordless_sudo() {
|
||||
local user="$1" dest="/etc/sudoers.d/99-${user}-nopasswd" tmp
|
||||
# Declared separately, deliberately. In `local a="$1" b="${a}"` bash expands
|
||||
# $a before it has been assigned, so b comes out with the name missing — which
|
||||
# here meant every account's rule landing in the same /etc/sudoers.d/99--nopasswd,
|
||||
# each one silently overwriting the last, and has_passwordless_sudo never
|
||||
# finding the file it was looking for.
|
||||
local user="$1"
|
||||
local dest="/etc/sudoers.d/99-${user}-nopasswd"
|
||||
local tmp
|
||||
tmp="$(mktemp)"
|
||||
|
||||
[[ -n "$user" ]] || fail "grant_passwordless_sudo needs a username"
|
||||
|
||||
echo "${user} ALL=(ALL) NOPASSWD: ALL" >"$tmp"
|
||||
|
||||
if ! visudo -c -f "$tmp" >/dev/null 2>&1; then
|
||||
|
||||
@@ -96,7 +96,92 @@ info "Refreshing the package index..."
|
||||
pkg_refresh >/dev/null
|
||||
|
||||
# =============================================================================
|
||||
# 2. Disk space
|
||||
# 2. User account
|
||||
# =============================================================================
|
||||
|
||||
# First of the acting sections, and before anything writes into a home.
|
||||
#
|
||||
# If the account does not exist yet, USER_HOME is a path that is not there —
|
||||
# and the ballast's mkdir -p, running as root, would create it root-owned. adduser
|
||||
# then finds the directory already present and does not populate or chown it. So
|
||||
# the account is made before any step can put a file in its home.
|
||||
step "User account"
|
||||
if ! skip; then
|
||||
echo ""
|
||||
info "User account — the account you will actually log in and work as"
|
||||
|
||||
if id "$USERNAME" &>/dev/null; then
|
||||
echo " ${USERNAME} already exists"
|
||||
USER_EXISTED=true
|
||||
else
|
||||
echo " ${USERNAME} does not exist yet and will be created"
|
||||
echo " adduser will ask for a password and a few details"
|
||||
USER_EXISTED=false
|
||||
fi
|
||||
|
||||
IN_SUDO=false
|
||||
id -nG "$USERNAME" 2>/dev/null | tr ' ' '\n' | grep -qx sudo && IN_SUDO=true
|
||||
echo " sudo group: $($IN_SUDO && echo 'already a member' || echo 'will be added')"
|
||||
echo " passwordless: $(has_passwordless_sudo "$USERNAME" && echo 'already granted' || echo 'not granted')"
|
||||
|
||||
if $USER_EXISTED && $IN_SUDO; then
|
||||
SUMMARY+=("User: ${USERNAME} (already set up)")
|
||||
elif confirm "Proceed?"; then
|
||||
if ! $USER_EXISTED; then
|
||||
adduser --gecos "" "$USERNAME"
|
||||
SUMMARY+=("User: ${USERNAME} created")
|
||||
|
||||
# Re-read it now the account is real. Until this point USER_HOME was the
|
||||
# /home/<name> guess, since there was nothing to look up; adduser is free
|
||||
# to have used something else, and every step after this writes there.
|
||||
USER_HOME="$(getent passwd "$USERNAME" | cut -d: -f6)"
|
||||
ok "home is ${USER_HOME}"
|
||||
fi
|
||||
$IN_SUDO || usermod -aG sudo "$USERNAME"
|
||||
ok "${USERNAME} is in the sudo group"
|
||||
$USER_EXISTED && SUMMARY+=("User: ${USERNAME} added to sudo")
|
||||
else
|
||||
warn "skipped by request"
|
||||
SUMMARY+=("User: SKIPPED by request")
|
||||
fi
|
||||
|
||||
# ── passwordless sudo ──
|
||||
#
|
||||
# Asked separately because it is a security posture rather than part of
|
||||
# creating an account, and because Officer has an actual requirement here:
|
||||
# os-user-shell.ts runs `sudo -n` to provision a member's home, and a prompt it
|
||||
# cannot answer is a failure it reports as a permissions error.
|
||||
if has_passwordless_sudo "$USERNAME"; then
|
||||
echo ""
|
||||
echo " passwordless sudo is already granted to ${USERNAME}"
|
||||
SUMMARY+=("Sudo: passwordless (already)")
|
||||
else
|
||||
echo ""
|
||||
info "Passwordless sudo for ${USERNAME}?"
|
||||
echo " Means sudo never asks for a password again. Convenient, and the"
|
||||
echo " cost is real: anything that gets hold of this account, or of a key"
|
||||
echo " that can log into it, is root without another step."
|
||||
if is_role vps; then
|
||||
echo " Worth weighing on a ${MACHINE_ROLE}, which faces the open internet."
|
||||
fi
|
||||
echo ""
|
||||
echo " Officer needs it: it runs 'sudo -n' to provision a member's Linux"
|
||||
echo " account, and a password prompt it cannot answer surfaces as a"
|
||||
echo " permissions error rather than a question."
|
||||
if confirm "Grant it?"; then
|
||||
grant_passwordless_sudo "$USERNAME"
|
||||
ok "passwordless sudo granted — remove /etc/sudoers.d/99-${USERNAME}-nopasswd to undo"
|
||||
SUMMARY+=("Sudo: passwordless")
|
||||
else
|
||||
warn "skipped by request"
|
||||
SUMMARY+=("Sudo: password required (Officer's account provisioning will not work)")
|
||||
fi
|
||||
fi
|
||||
step_ok
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# 3. Disk space
|
||||
# =============================================================================
|
||||
#
|
||||
# First of the sections that change anything, because everything after it sizes
|
||||
@@ -191,7 +276,7 @@ if ! skip; then
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# 3. System update
|
||||
# 4. System update
|
||||
# =============================================================================
|
||||
#
|
||||
# Its own section because it is the only thing in the script that moves versions
|
||||
@@ -231,7 +316,7 @@ if ! skip; then
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# 4. Core utils
|
||||
# 5. Core utils
|
||||
# =============================================================================
|
||||
#
|
||||
# What the distribution provides: the six this script would break without, and
|
||||
@@ -246,7 +331,7 @@ if ! skip; then
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# 5. Command-line tools
|
||||
# 6. Command-line tools
|
||||
# =============================================================================
|
||||
#
|
||||
# A different thing from core utils, and kept apart from them: upstream binaries
|
||||
@@ -264,7 +349,7 @@ fi
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 6. Locale
|
||||
# 7. Locale
|
||||
# =============================================================================
|
||||
#
|
||||
# LOCALE in the environment overrides the default.
|
||||
@@ -302,7 +387,7 @@ if ! skip; then
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# 7. Timezone
|
||||
# 8. Timezone
|
||||
# =============================================================================
|
||||
#
|
||||
# TIMEZONE in the environment answers the prompt ahead of time.
|
||||
@@ -368,7 +453,7 @@ if ! skip; then
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# 8. Swap
|
||||
# 9. Swap
|
||||
# =============================================================================
|
||||
#
|
||||
# Disk the kernel can park cold pages on when RAM fills, so a spike costs
|
||||
@@ -422,7 +507,7 @@ if ! skip; then
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# 9. Emergency disk ballast
|
||||
# 10. Emergency disk ballast
|
||||
# =============================================================================
|
||||
#
|
||||
# Always offered, whatever the role — the role only decides which way the
|
||||
@@ -546,7 +631,7 @@ elif ! skip; then
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# 10. earlyoom
|
||||
# 11. earlyoom
|
||||
# =============================================================================
|
||||
|
||||
step "earlyoom"
|
||||
@@ -581,7 +666,7 @@ if ! skip; then
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# 11. inotify watch limit
|
||||
# 12. inotify watch limit
|
||||
# =============================================================================
|
||||
|
||||
step "inotify watch limit"
|
||||
@@ -620,7 +705,7 @@ if ! skip; then
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# 12. Sleep and suspend
|
||||
# 13. Sleep and suspend
|
||||
# =============================================================================
|
||||
|
||||
step "Sleep and suspend"
|
||||
@@ -688,7 +773,7 @@ elif ! skip; then
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# 13. Boot hang
|
||||
# 14. Boot hang
|
||||
# =============================================================================
|
||||
|
||||
step "Boot hang"
|
||||
@@ -743,79 +828,6 @@ elif ! skip; then
|
||||
step_ok
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# 14. User account
|
||||
# =============================================================================
|
||||
|
||||
step "User account"
|
||||
if ! skip; then
|
||||
echo ""
|
||||
info "User account — the account you will actually log in and work as"
|
||||
|
||||
if id "$USERNAME" &>/dev/null; then
|
||||
echo " ${USERNAME} already exists"
|
||||
USER_EXISTED=true
|
||||
else
|
||||
echo " ${USERNAME} does not exist yet and will be created"
|
||||
echo " adduser will ask for a password and a few details"
|
||||
USER_EXISTED=false
|
||||
fi
|
||||
|
||||
IN_SUDO=false
|
||||
id -nG "$USERNAME" 2>/dev/null | tr ' ' '\n' | grep -qx sudo && IN_SUDO=true
|
||||
echo " sudo group: $($IN_SUDO && echo 'already a member' || echo 'will be added')"
|
||||
echo " passwordless: $(has_passwordless_sudo "$USERNAME" && echo 'already granted' || echo 'not granted')"
|
||||
|
||||
if $USER_EXISTED && $IN_SUDO; then
|
||||
SUMMARY+=("User: ${USERNAME} (already set up)")
|
||||
elif confirm "Proceed?"; then
|
||||
if ! $USER_EXISTED; then
|
||||
adduser --gecos "" "$USERNAME"
|
||||
SUMMARY+=("User: ${USERNAME} created")
|
||||
fi
|
||||
$IN_SUDO || usermod -aG sudo "$USERNAME"
|
||||
ok "${USERNAME} is in the sudo group"
|
||||
$USER_EXISTED && SUMMARY+=("User: ${USERNAME} added to sudo")
|
||||
else
|
||||
warn "skipped by request"
|
||||
SUMMARY+=("User: SKIPPED by request")
|
||||
fi
|
||||
|
||||
# ── passwordless sudo ──
|
||||
#
|
||||
# Asked separately because it is a security posture rather than part of
|
||||
# creating an account, and because Officer has an actual requirement here:
|
||||
# os-user-shell.ts runs `sudo -n` to provision a member's home, and a prompt it
|
||||
# cannot answer is a failure it reports as a permissions error.
|
||||
if has_passwordless_sudo "$USERNAME"; then
|
||||
echo ""
|
||||
echo " passwordless sudo is already granted to ${USERNAME}"
|
||||
SUMMARY+=("Sudo: passwordless (already)")
|
||||
else
|
||||
echo ""
|
||||
info "Passwordless sudo for ${USERNAME}?"
|
||||
echo " Means sudo never asks for a password again. Convenient, and the"
|
||||
echo " cost is real: anything that gets hold of this account, or of a key"
|
||||
echo " that can log into it, is root without another step."
|
||||
if is_role vps; then
|
||||
echo " Worth weighing on a ${MACHINE_ROLE}, which faces the open internet."
|
||||
fi
|
||||
echo ""
|
||||
echo " Officer needs it: it runs 'sudo -n' to provision a member's Linux"
|
||||
echo " account, and a password prompt it cannot answer surfaces as a"
|
||||
echo " permissions error rather than a question."
|
||||
if confirm "Grant it?"; then
|
||||
grant_passwordless_sudo "$USERNAME"
|
||||
ok "passwordless sudo granted — remove /etc/sudoers.d/99-${USERNAME}-nopasswd to undo"
|
||||
SUMMARY+=("Sudo: passwordless")
|
||||
else
|
||||
warn "skipped by request"
|
||||
SUMMARY+=("Sudo: password required (Officer's account provisioning will not work)")
|
||||
fi
|
||||
fi
|
||||
step_ok
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# NOT PORTED YET
|
||||
# =============================================================================
|
||||
@@ -830,7 +842,7 @@ fi
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 12. Summary
|
||||
# 15. Summary
|
||||
# =============================================================================
|
||||
|
||||
echo ""
|
||||
|
||||
Reference in New Issue
Block a user