Nimbin[12]?Linux From Scratch / Packagemanager-LFS-PackageUser-System / stacks/shadow-pam.sh

Packagemanager-LFS-PackageUser-System git · main

Package-user based package manager for LFS/BLFS

lfs blfs package-manager bash · first commit 2026-08-18 · last commit 2026-10-26 (8 days ago) · synced 8 hours ago · upstream: github.com/nimbin2/Packagemanager-LFS-PackageUser-System

Shell 63.7% Markdown 35.8%
git clone https://git.christianimmanuel.de/linux-from-scratch/Packagemanager-LFS-PackageUser-System.gitwget https://git.christianimmanuel.de/linux-from-scratch/Packagemanager-LFS-PackageUser-System/archive/Packagemanager-LFS-PackageUser-System.tar.gz
stacks/shadow-pam.sh 16.5 KB · 360 lines raw
#!/bin/bash
# shadow-pam.sh -- make `su` actually use PAM, and put the setuid bits back.
#
# TWO THINGS THAT ONLY MAKE SENSE TOGETHER.
#
# Linux-PAM installs a library and changes nothing: until shadow is REBUILT
# against it, nothing reads /etc/pam.d at all -- so `su - u_claude` keeps
# asking for a password however right /etc/pam.d/su_u_user is.  The book's
# own Linux-PAM page ends with "You should now reinstall the Shadow
# package", and that is this step.
#
# And rebuilding shadow strips the setuid bit from /usr/bin/su, because the
# BLFS page never sets it (the LFS page does).  Warning about that and
# leaving it is no good: `su` is broken either way, and the fix is a mode.
# So the bits come from setid.conf and are APPLIED here.
#
# Both halves are idempotent and both check their own effect: pam linkage by
# `ldd`, the modes by reading them back.
set -u

CONF="${SETID_CONF:-/etc/pkgusr/stacks/setid.conf}"
PM="${SHADOW_PAM_PM:-packagemanager}"
SU="${SHADOW_PAM_SU:-/usr/bin/su}"
fail=0
note() { echo "#  $*"; }
warn() { echo "!! $*"; fail=1; }

# Defined before BOTH steps use them: step 1 asked _pam_installed forty
# lines above where it was defined, which under `set -u` is a command that
# does not exist yet -- the config would silently never be written.
_su_has_pam() { ldd "$SU" 2>/dev/null | grep -q 'libpam\.so'; }
_pam_installed() { [ -e /usr/lib/libpam.so ] || [ -e /lib/libpam.so ]; }

# CONFIG BEFORE REBUILD, AND THAT ORDER IS THE WHOLE POINT.
#
# The first version rebuilt shadow and THEN wrote /etc/pam.d.  Between the
# two, su links libpam and has no service file -- and every build in this
# system runs as its package user through `su - p_<pkg>`, so that window is
# one where NOTHING can be built, including the very configure phases that
# would write the missing files.  Root's own `su` survives it only because
# pam_rootok is sufficient.
#
# So: write the config while su still works, then rebuild.  A system that
# stops midway is then left with pam config and a non-pam su -- harmless --
# rather than a pam su and no config.

# ---- 1. the pam config files su will read --------------------------
# A PAM-LINKED su WITH NO /etc/pam.d/su IS WORSE THAN A NON-PAM ONE:
#
#     su: pam_start: error 26          (PAM_ABORT -- no service file)
#
# `update` runs install, not configure, and those files are written by the
# CONFIGURE phase of linux-pam (/etc/pam.d/system-auth and friends) and
# shadow (/etc/pam.d/su, login, passwd, chage...).  So a rebuild leaves su
# linked against a library with nothing to read.
# THE SET THE BOOK WRITES, NOT A SAMPLE OF IT.
#
# This checked su and system-auth.  Both existed, so it said "in place"
# while login, passwd, chage and twelve admin tools had no file:
#     passwd: pam_start() failed, error 26
#     login: PAM Failure, aborting
# Two files out of sixteen.  The set now comes from the same place the files
# do: the package's configure_pkg, generated from the book.  Every
# /etc/pam.d/<n> it names is expected, and a `for VAR in a b c` loop that
# writes /etc/pam.d/${VAR} contributes a, b and c.  Commented (skipped)
# lines are not the book's commands and are not counted.
PAM_PKGS="${SHADOW_PAM_PKGS:-linux-pam shadow}"
PAM_DIR="${SHADOW_PAM_DIR:-/etc/pam.d}"

HELPER="${SHADOW_PAM_HELPER:-lfs-helper}"
# ONE READER OF WHAT A SCRIPT WRITES.  1.14.262 had its own awk here; the
# lease in lfs-helper (1.14.263) needs the same list for every package, so
# this asks it.  Names under /etc/pam.d, one per line.
_book_pam_set() {       # $1 = generated install script
    $HELPER script-targets "$1" configure_pkg 2>/dev/null \
        | sed -n 's|^/etc/pam\.d/||p' | sort -u
}

# THE SCRIPT ON DISK MAY BE THE PROBLEM.  Shadow's was generated before
# blfs read sub-sections, so its configure phase is prose and a bare `:` --
# it names no pam file, and `script configure` would run it again and write
# nothing.  So: read the script that is there; if it names no pam file,
# regenerate it (an edited copy is kept as .edited) and read that.  A script
# that is already right is never regenerated just because this step ran.
_fetch_script() {       # $1 = package, $2 = optional --regenerate -> path
    $PM script fetch "$1" ${2:-} 2>/dev/null \
        | sed 's/\x1b\[[0-9;]*m//g' \
        | sed -n 's/^script: //p; s/^already there: //p' | tail -1
}

_missing_pam=""
_want_pam=""
_scripts=""
if _pam_installed; then
    for _cfgpkg in $PAM_PKGS; do
        _sp_path="$(_fetch_script "$_cfgpkg")"
        _set=""
        [ -f "$_sp_path" ] && _set="$(_book_pam_set "$_sp_path" | tr '\n' ' ')"
        if [ -z "$(printf '%s' "$_set" | tr -d ' ')" ]; then
            note "$_cfgpkg's script names no $PAM_DIR file -- regenerating it from the book"
            _sp_path="$(_fetch_script "$_cfgpkg" --regenerate)"
            [ -f "$_sp_path" ] && _set="$(_book_pam_set "$_sp_path" | tr '\n' ' ')"
        fi
        if [ -z "$_sp_path" ] || [ ! -f "$_sp_path" ]; then
            warn "could not get $_cfgpkg's install script -- cannot tell what it writes"
            note "   by hand:  packagemanager script fetch $_cfgpkg --regenerate"
            continue
        fi
        _scripts="$_scripts $_cfgpkg=$_sp_path"
        if [ -z "$(printf '%s' "$_set" | tr -d ' ')" ]; then
            # a pam package whose configure names no pam file is the bug
            # this check exists for, not an empty requirement
            warn "$_cfgpkg's configure phase writes no $PAM_DIR file"
            note "   the generator dropped them -- look at configure_pkg in $_sp_path"
            continue
        fi
        _want_pam="$_want_pam $_set"
        for _n in $_set; do
            [ -f "$PAM_DIR/$_n" ] || _missing_pam="$_missing_pam $_n"
        done
    done
fi

if _pam_installed && [ -n "$_want_pam" ]; then
    if [ -z "$_missing_pam" ]; then
        note "all $(echo $_want_pam | wc -w) $PAM_DIR files the book writes are in place"
    else
        note "missing from $PAM_DIR:$_missing_pam"
        note "   running the CONFIGURE phases that write them (update installs;"
        note "   it does not configure)"
        # THE su TRAP.  Shadow's page writes $PAM_DIR/su too, WITHOUT the
        # pam_exec line that makes application users passwordless -- so this
        # step silently undoes `cfg app-users`.  app-users is the next entry
        # in services.stack and puts it back (the suite asserts that order);
        # say so here, so `su - u_<n>` asking for a password is not a mystery.
        _had_exec=0
        grep -q 'pam_exec\.so' "$PAM_DIR/su" 2>/dev/null && _had_exec=1
        for _cfgpkg in $PAM_PKGS; do
            case "$_scripts" in *" $_cfgpkg="*) ;; *) continue ;; esac
            if $PM script configure "$_cfgpkg" >/dev/null 2>&1; then
                note "   configured $_cfgpkg"
            else
                warn "could not run $_cfgpkg's configure phase"
                note "     by hand:  packagemanager script configure $_cfgpkg"
            fi
        done
        # CONFIRM IT -- every file, by name.
        _still=""
        for _n in $_want_pam; do
            [ -f "$PAM_DIR/$_n" ] || _still="$_still $_n"
        done
        if [ -z "$_still" ]; then
            note "   all $(echo $_want_pam | wc -w) written"
        else
            warn "still missing from $PAM_DIR:$_still"
            note "     a missing service file is \"pam_start: error 26\" for that program"
        fi
        if [ "$_had_exec" = 1 ] && ! grep -q 'pam_exec\.so' "$PAM_DIR/su" 2>/dev/null; then
            note "   shadow's configure rewrote $PAM_DIR/su without the pam_exec line:"
            note "   application users need a password until app-users runs again."
            note "   services.stack runs it next; on its own:"
            note "     packagemanager stack services --only app-users --run"
        fi
    fi
elif _pam_installed; then
    warn "could not work out which $PAM_DIR files the book writes -- nothing checked"
fi
echo

# ---- 2. is su PAM-aware? -------------------------------------------------

if ! _pam_installed; then
    note "Linux-PAM is not installed -- nothing to rebuild shadow against."
    note "   passwordless su needs it:  packagemanager stack services --only linux-pam --run"
elif _su_has_pam; then
    note "$SU already links libpam -- no rebuild needed"
else
    note "$SU does not link libpam: shadow was built before PAM existed."
    note "   rebuilding it now (this is the step the book's Linux-PAM page asks for)"
    # REGENERATE, because the script on disk was written before pam was
    # here and its configure line has no --with-libpam to gain.
    if $PM update shadow --reinstall --regenerate --run --yes; then
        if _su_has_pam; then
            note "   $SU now links libpam"
        else
            warn "shadow rebuilt and $SU still does not link libpam"
            note "   check the configure line:  grep -n configure \\"
            note "     /usr/src/pkgusr/p_shadow/install_Shadow-*"
            note "   the book's page may need --with-libpam for this system."
        fi
    else
        warn "rebuilding shadow failed -- su is unchanged, which is the safe outcome"
        note "   logs:  packagemanager errors"
    fi
fi
echo

# ---- 3. the setuid bits, applied ----------------------------------------
# A rebuild of shadow resets these every time, so this runs whether or not
# the rebuild above did anything.
[ -r "$CONF" ] || { warn "no $CONF -- cannot restore the setuid bits"; exit 1; }

applied=0 checked=0
# FIVE COLUMNS.  This read four, so since the package column arrived
# (1.14.310) `group` held "- | shadow" -- "-|shadow" once the blanks were
# gone -- and every chown below asked for group "-|shadow" and failed.
while IFS='|' read -r path mode owner group _pkg; do
    path="$(printf '%s' "${path:-}" | tr -d '[:space:]')"
    case "$path" in ""|\#*) continue ;; esac
    mode="$(printf '%s' "${mode:-}" | tr -d '[:space:]')"
    owner="$(printf '%s' "${owner:-}" | tr -d '[:space:]')"
    group="$(printf '%s' "${group:-}" | tr -d '[:space:]')"
    [ -n "$mode" ] || { warn "$path: no mode in $CONF"; continue; }
    # "-": keep the mode the file has (a line that exists only so the lease
    # may hand the file to its package) -- nothing for this script to set
    [ "$mode" = "-" ] && continue
    if [ ! -e "$path" ]; then
        # NOT AN ERROR.  Not every package here installs every file --
        # unix_chkpwd only exists once Linux-PAM is in.
        note "$path is not installed -- skipped"
        continue
    fi
    checked=$((checked + 1))
    now="$(stat -c '%a %U %G' "$path" 2>/dev/null)"
    want_owner="${owner:--}"
    [ "$want_owner" = "-" ] && want_owner="$(printf '%s' "$now" | awk '{print $2}')"
    want_group="${group:--}"
    [ "$want_group" = "-" ] && want_group="$(printf '%s' "$now" | awk '{print $3}')"
    if [ "$now" = "$mode $want_owner $want_group" ]; then
        continue
    fi
    note "$path is $now, should be $mode $want_owner $want_group"
    # ORDER MATTERS: chown CLEARS the setuid bit, so chmod goes last.
    chown "$want_owner:$want_group" "$path" 2>/dev/null \
        || warn "   could not chown $path"
    if chmod "$mode" "$path" 2>/dev/null; then
        # CONFIRM IT.  A setuid bit that did not take is the whole failure
        # this step exists to end.
        after="$(stat -c '%a %U %G' "$path" 2>/dev/null)"
        if [ "$after" = "$mode $want_owner $want_group" ]; then
            note "   -> $after"
            applied=$((applied + 1))
        else
            warn "   $path is $after after chmod $mode -- not what was asked"
        fi
    else
        warn "   could not chmod $mode $path"
    fi
done < "$CONF"

echo

# ---- 4. accounts pam can actually look up --------------------------------
# EVERY BUILD IS A `su - p_<pkg>`, so this line is not cosmetic:
#
#     su: Authentication service cannot retrieve authentication info
#     (Ignored)
#
# pam_unix reads /etc/shadow.  `add_package_user` writes /etc/passwd and
# leaves no shadow entry, so pam_unix cannot find the account and says so --
# on every package build, in every log, for ever.  It is "(Ignored)" only
# because pam_rootok already succeeded for root; for any non-root caller it
# is a real failure.
#
# `pwconv` is shadow's own tool for exactly this: it adds a shadow entry for
# every passwd entry that lacks one, with the password field locked.  It
# rewrites /etc/shadow, so: back it up, run it, read it back.
#
# UNVERIFIED FROM WHERE THIS WAS WRITTEN: the detection, the backup and the
# read-back were exercised against a fixture; `pwconv` itself was not (it
# edits the real /etc/shadow and there is no safe way to run it in that
# sandbox).  If it turns out not to cover these accounts, the read-back
# below says so by name and the backup is beside the file -- which is why
# both are here.
_no_shadow() {
    # accounts in passwd with no /etc/shadow line
    while IFS=: read -r _n _rest; do
        case "$_n" in ""|\#*) continue ;; esac
        grep -q "^$_n:" /etc/shadow 2>/dev/null || printf '%s\n' "$_n"
    done < /etc/passwd
}

# NO /etc/shadow AT ALL IS THE CASE THIS MISSED.
#
# The guard was `[ -r /etc/passwd ] && [ -r /etc/shadow ]`, so on a system
# with no shadow file it skipped in silence -- which is exactly the system
# that needs it most: pam_unix has nowhere to look for ANY account, and
# every `su - p_<pkg>` says "cannot retrieve authentication info".
#
# pwconv CREATES /etc/shadow from /etc/passwd, moving each password hash out
# of passwd and leaving `x` behind.  That is a change to how this machine
# authenticates, so:
#   * /etc/passwd is backed up FIRST (it is the file that changes),
#   * root's own line is checked afterwards, by name,
#   * and if anything is wrong, passwd is put back and shadow removed.
if [ ! -r /etc/passwd ]; then
    warn "cannot read /etc/passwd -- skipping the shadow check"
elif [ ! -e /etc/shadow ]; then
    note "there is no /etc/shadow at all: pam_unix cannot look up ANY account,"
    note "   which is the \"cannot retrieve authentication info\" on every build."
    note "   pwconv creates it from /etc/passwd (hashes move; passwd keeps 'x')."
    _pwbak="/etc/passwd.bak-$(date +%Y%m%d-%H%M%S)"
    if ! cp -p /etc/passwd "$_pwbak"; then
        warn "could not back up /etc/passwd -- not running pwconv"
    else
        note "   backed up /etc/passwd -> $(basename "$_pwbak")"
        if pwconv && [ -s /etc/shadow ] && grep -q '^root:' /etc/shadow \
           && grep -q '^root:' /etc/passwd; then
            chmod 600 /etc/shadow 2>/dev/null
            note "   /etc/shadow created, root is in it, $(grep -c . /etc/shadow) entries"
            note "   VERIFY BEFORE LOGGING OUT, from another terminal:"
            note "     su - $(awk -F: '$3>=1000 && $3<60000 {print $1; exit}' /etc/passwd) -c id"
            note "   if login is broken:  cp -p $_pwbak /etc/passwd && rm -f /etc/shadow"
        else
            warn "pwconv did not produce a usable /etc/shadow -- putting"
            warn "   /etc/passwd back and removing the half-made file"
            cp -p "$_pwbak" /etc/passwd
            rm -f /etc/shadow
            note "   nothing changed; the backup is still at $_pwbak"
        fi
    fi
elif [ -r /etc/shadow ]; then
    _missing_sh="$(_no_shadow | tr '\n' ' ')"
    if [ -z "$(printf '%s' "$_missing_sh" | tr -d ' ')" ]; then
        note "every account has an /etc/shadow entry (pam_unix can read them)"
    else
        note "accounts with no /etc/shadow entry:$_missing_sh"
        note "   pam_unix cannot look those up -- that is the"
        note "   \"cannot retrieve authentication info\" line on every build."
        _shbak="/etc/shadow.bak-$(date +%Y%m%d-%H%M%S)"
        if cp -p /etc/shadow "$_shbak"; then
            note "   backed up /etc/shadow -> $(basename "$_shbak")"
            if pwconv; then
                _still="$(_no_shadow | tr '\n' ' ')"
                if [ -z "$(printf '%s' "$_still" | tr -d ' ')" ]; then
                    note "   pwconv added the missing entries (passwords locked)"
                else
                    warn "still without a shadow entry:$_still"
                    note "     pwconv ran and did not cover them; look at /etc/passwd"
                fi
            else
                warn "pwconv failed -- /etc/shadow is unchanged"
                note "     the backup is $_shbak if anything looks wrong"
            fi
        else
            warn "could not back up /etc/shadow -- not running pwconv"
        fi
    fi
fi

echo
if [ "$fail" = 0 ]; then
    echo "# shadow-pam: $checked file(s) checked, $applied corrected."
    echo "#   try it, in ANOTHER terminal, before closing this one:"
    echo "#     su - <an application user> -c id"
    echo "#   if su is broken, this is the file to look at: /etc/pam.d/su"
else
    echo "# the !! lines above need attention"
fi
exit $fail