Files
CISS.debian.installer/func/cdi_4500_user/4520_accounts_setup.sh
2025-10-15 07:08:52 +01:00

1466 lines
50 KiB
Bash

#!/bin/bash
# SPDX-Version: 3.0
# SPDX-CreationInfo: 2025-06-17; WEIDNER, Marc S.; <msw@coresecret.dev>
# SPDX-ExternalRef: GIT https://git.coresecret.dev/msw/CISS.debian.installer.git
# SPDX-FileContributor: WEIDNER, Marc S.; Centurion Intelligence Consulting Agency
# SPDX-FileCopyrightText: 2024-2025; WEIDNER, Marc S.; <msw@coresecret.dev>
# SPDX-FileType: SOURCE
# SPDX-License-Identifier: EUPL-1.2 OR LicenseRef-CCLA-1.0
# SPDX-LicenseComment: This file is part of the CISS.debian.installer.secure framework.
# SPDX-PackageName: CISS.debian.installer
# SPDX-Security-Contact: security@coresecret.eu
guard_sourcing
# TODO: sudo TOTP deactivation
# TODO: PAM Module
# TODO: Check Password activation
# TODO: Check expiration and other dates
# TODO: Logic
#######################################
# Updating user accounts.
# Globals:
# TARGET
# VAR_SETUP_PATH
# VAR_TEMP_PLAIN_MFA_SEED
# VAR_USER_MAX
# user_root_authentication_2fa_ssh
# user_root_authentication_2fa_tty
# user_root_authentication_access_ssh
# user_root_authentication_access_tty
# user_root_authentication_password
# user_root_shell
# user_root_specific
# user_root_sshpubkey
# Arguments:
# None
# Returns:
# 0: on success
# ERR_ACCOUNT_CREATE: on failure
#######################################
accounts_setup() {
### Declare Arrays, HashMaps, and Variables.
declare -r var_logfile="/root/.ciss/cdi/log/4520_accounts_setup.log"
declare -i i=0
declare tmp_username="" tmp_fullname="" tmp_uid="" tmp_gid="" tmp_shell="" tmp_password="" tmp_sshpubkey="" \
tmp_access_tty="" tmp_auth_pwd="" tmp_2fa_ssh="" tmp_2fa_tty="" tmp_sudo="" tmp_restricted="" tmp_system="" \
tmp_specific=""
declare var_username="" var_fullname="" var_uid="" var_gid="" var_shell="" var_password="" var_sshpubkey="" \
var_access_tty="" var_auth_pwd="" var_2fa_ssh="" var_2fa_tty="" var_sudo="" var_restricted="" var_system="" \
var_specific=""
declare var_ssh_totp_update="false"
declare var_target="${TARGET}"
### Check for TARGET / RECOVERY.
[[ "${VAR_RUN_RECOVERY}" == "true" ]] && var_target="${RECOVERY}"
chroot_logger "${var_target}${var_logfile}"
### Prepare the '2fa'-seed variable.
read_totp_seed
do_log "debug" "file_only" "4520() Command: [read_totp_seed]"
### 0) The 'root' account is generated via debootstrap by default.
### 1) Prepare the 'root' account.
install -d -m 0700 -o root -g root "${var_target}/root/.ssh"
install -m 0600 -o root -g root /dev/null "${var_target}/root/.ssh/authorized_keys"
install -m 0600 -o root -g root "${VAR_SETUP_PATH}/includes/target/etc/skel/.bashrc" "${var_target}/root/"
install -m 0600 -o root -g root "${VAR_SETUP_PATH}/includes/target/etc/skel/.ciss/theme_eza_ciss.yml" "${var_target}/root/.ciss/"
install -m 0600 -o root -g root "${VAR_SETUP_PATH}/includes/target/root/.ciss/alias" "${var_target}/root/.ciss/"
install -m 0700 -o root -g root "${VAR_SETUP_PATH}/includes/target/etc/skel/.ciss/check_chrony.sh" "${var_target}/root/.ciss/"
install -m 0700 -o root -g root "${VAR_SETUP_PATH}/includes/target/root/.ciss/clean_logout.sh" "${var_target}/root/.ciss/"
install -m 0600 -o root -g root "${VAR_SETUP_PATH}/includes/target/root/.ciss/f2bchk" "${var_target}/root/.ciss/"
install -m 0600 -o root -g root "${VAR_SETUP_PATH}/includes/target/root/.ciss/scan_libwrap" "${var_target}/root/.ciss/"
install -m 0600 -o root -g root "${VAR_SETUP_PATH}/includes/target/root/.ciss/shortcuts" "${var_target}/root/.ciss/"
if [[ "${user_root_shell}" == "/bin/zsh" ]]; then
if [[ -x "${var_target}${user_root_shell}" ]]; then
case "${user_root_specific,,}" in
"ciss")
zsh_omz_installer "root" "${var_target}"
mv "${var_target}/root/.zshrc" "${var_target}/root/.zshrc.bak"
install -m 0600 -o root -g root "${VAR_SETUP_PATH}/includes/target/etc/skel/.zshrc" "${var_target}/root/"
;;
"physnet")
:
;;
"none"|*)
:
;;
esac
chroot_exec "${var_target}" chsh -s "${user_root_shell}" root
do_log "info" "file_only" "4520() Shell: '${user_root_shell}' used for: 'root'."
else
chroot_exec "${var_target}" chsh -s /bin/bash root
do_log "info" "file_only" "4520() Shell: '${user_root_shell}' not found for: 'root'. Using '/bin/bash' instead."
fi
fi
### To be able to copy/paste from vim, one needs to create a '.vimrc' with the following content:
echo 'set clipboard=unnamed' >| "${var_target}/root/.vimrc"
chmod 0600 "${var_target}/root/.vimrc"
do_log "info" "file_only" "4520() Skeleton: 'root' successfully generated."
### 2) Check SSH access capabilities.
case "${user_root_authentication_access_ssh,,}" in
false)
sed -i -E "s|^[[:space:]]*PermitRootLogin[[:space:]]+.*$|$(printf '%-29s%s' 'PermitRootLogin' 'no')|" "${var_target}/etc/ssh/sshd_config"
do_log "info" "file_only" "4520() User: 'root' SSH access: [PermitRootLogin no]"
;;
true)
sed -i -E "s|^[[:space:]]*PermitRootLogin[[:space:]]+.*$|$(printf '%-29s%s' 'PermitRootLogin' 'prohibit-password')|" "${var_target}/etc/ssh/sshd_config"
do_log "info" "file_only" "4520() User: 'root' SSH access: [PermitRootLogin prohibit-password]"
;;
*)
do_log "fatal" "file_only" "4520() Not set: user_root_authentication_access_ssh [${user_root_authentication_access_ssh}]"
return "${ERR_ACCOUNT_CREATE}"
;;
esac
### 3) Check tty access capabilities.
case "${user_root_authentication_access_tty,,}" in
false)
### 3) A) 1) Ensure the 'pam_access' line is not activated in '/etc/pam.d/login' and '/etc/pam.d/sshd' in parallel.
pam_access_sync_login_sshd
### 3) A) 2) Ensure 'pam_securetty' in the auth phase; requisite causes immediate fail for disallowed ttys.
chroot_stdin "${var_target}" "__payload__" <<'EOF'
export LC_ALL=C
if ! grep -Eq '^[[:space:]]*auth[[:space:]]+requisite[[:space:]]+pam_securetty[.]so([[:space:]]|$)' /etc/pam.d/login; then
tmp="$(mktemp /etc/pam.d/login.XXXXXX)"
awk '
BEGIN { ins=0 }
{
if (!ins && $0 ~ /^[[:space:]]*auth[[:space:]]+.*pam_unix[.]so/) {
print "auth requisite pam_securetty.so"
ins=1
}
print
}
END {
if (!ins) print "auth requisite pam_securetty.so"
}
' /etc/pam.d/login >| "${tmp}"
test -s "${tmp}"
mv -f "${tmp}" /etc/pam.d/login
rm -f -- "${tmp}"
fi
:
EOF
### 3) A) 3) Disallow all local access for root in '/etc/security/access.conf'.
printf -- '-: root:ALL\n' >> "${var_target}/etc/security/access.conf"
### 3) A) 4) Empty "/etc/securetty".
cat << 'EOF' >| "${var_target}/etc/securetty"
EOF
do_log "info" "file_only" "4520() User: 'root' tty access: [false]"
;;
true)
### 3) B) 1) Allow local access for 'root' only on 'tty1' in '/etc/security/access.conf'.
printf -- "+: root:tty1 \n" >> "${var_target}/etc/security/access.conf"
### 3) B) 2) Allow local access for 'root' only on 'tty1' in '/etc/securetty'.
cat << 'EOF' >| "${var_target}/etc/securetty"
tty1
EOF
do_log "info" "file_only" "4520() User: 'root' tty access: [true]"
;;
*)
do_log "fatal" "file_only" "4520() Not set: user_root_authentication_access_tty [${user_root_authentication_access_tty}]"
return "${ERR_ACCOUNT_CREATE}"
;;
esac
### 4) Check the password policy for the 'root' account.
case "${user_root_authentication_password,,}" in
false)
chroot_script "${var_target}" "passwd -l root"
do_log "info" "file_only" "4520() User: 'root' password access: [false]"
;;
true)
chroot_script "${var_target}" "printf '%s:%s\n' 'root' '${var_password}' | /usr/sbin/chpasswd -e"
#chroot_script "${var_target}" "/usr/sbin/usermod -p '${user_root_password}' root"
do_log "info" "file_only" "4520() User: 'root' password access: [true]"
;;
*)
do_log "fatal" "file_only" "4520() Not set: user_root_authentication_password [${user_root_authentication_password}]"
return "${ERR_ACCOUNT_CREATE}"
;;
esac
### 5) Update the 'root' SSH pubkey, if provided via 'preseed.yaml'.
if [[ -n "${user_root_sshpubkey:-}" ]]; then
printf "%s\n" "${user_root_sshpubkey}" >| "${var_target}/root/.ssh/authorized_keys"
do_log "info" "file_only" "4520() User: 'root' SSH public key: inserted."
fi
### 6) Update the 'root' 'totp'-policy and write the '.google_authenticator'-file.
if [[ "${user_root_authentication_2fa_ssh}" == "true" || "${user_root_authentication_2fa_tty}" == "true" ]]; then
write_google_authenticator_file "root" "0" "0" "${var_target}"
fi
if [[ "${user_root_authentication_2fa_ssh}" == "true" ]]; then
pam_access_totp_enable "root" "sshd" "${var_target}"
var_ssh_totp_update="true"
cat << EOF >> "${var_target}/etc/ssh/sshd_config"
Match User root
AuthenticationMethods publickey,keyboard-interactive:pam
EOF
fi
[[ "${user_root_authentication_2fa_tty}" == "true" ]] && pam_access_totp_enable "root" "login" "${var_target}"
### 7) Install eza themes.
eza_installer "root" "${var_target}"
### 8) Double check permissions.
### Directories: 0700
find "${var_target}/root" -type d -exec chmod 0700 {} +
### Executable files: 0700 (any x-bit set)
find "${var_target}/root" -type f -perm /111 -exec chmod 0700 {} +
### Non-executable files: 0600
find "${var_target}/root" -type f ! -perm /111 -exec chmod 0600 {} +
### Ownership: UID:GID (do not dereference symlinks; stay on this filesystem)
find "${var_target}/root" -xdev -exec chown -h root:root {} +
### 9) Final status logging.
do_log "info" "file_only" "4520() User: 'root' updated."
### Iterate through all remaining 'user' accounts and install them.
for ((i = 0; i <= VAR_USER_MAX; i++)); do
### Prepare all user-variables.
tmp_username="user_user${i}_name"
tmp_fullname="user_user${i}_fullname"
tmp_uid="user_user${i}_uid"
tmp_gid="user_user${i}_gid"
tmp_shell="user_user${i}_shell"
tmp_password="user_user${i}_password"
tmp_sshpubkey="user_user${i}_sshpubkey"
tmp_access_tty="user_user${i}_authentication_access_tty"
tmp_auth_pwd="user_user${i}_authentication_password"
tmp_2fa_ssh="user_user${i}_authentication_2fa_ssh"
tmp_2fa_tty="user_user${i}_authentication_2fa_tty"
tmp_sudo="user_user${i}_privileges_sudo"
tmp_system="user_user${i}_privileges_system"
tmp_restricted="user_user${i}_privileges_restricted"
tmp_specific="user_user${i}_specific"
var_username="${!tmp_username}"
var_fullname="${!tmp_fullname}"
var_uid="${!tmp_uid}"
var_gid="${!tmp_gid}"
var_shell="${!tmp_shell}"
var_password="${!tmp_password}"
var_sshpubkey="${!tmp_sshpubkey}"
var_access_tty="${!tmp_access_tty}"
var_auth_pwd="${!tmp_auth_pwd}"
var_2fa_ssh="${!tmp_2fa_ssh}"
var_2fa_tty="${!tmp_2fa_tty}"
var_sudo="${!tmp_sudo}"
var_system="${!tmp_system}"
var_restricted="${!tmp_restricted}"
var_specific="${!tmp_specific}"
### 0) A) Check if the 'group' of the 'user' already exists.
if ! chroot_exec "${var_target}" getent group "${var_username}" >/dev/null; then
chroot_exec "${var_target}" groupadd --gid "${var_gid}" "${var_username}"
fi
### 0) B) Generates the user account.
### If the 'user' is not restricted in scope, then generate the account accordingly, with a predefined expiry date.
### If the 'user' is a system user, then generate with flag '--system'.
case "${var_restricted}":"${var_system}" in
false:false)
chroot_exec "${var_target}" useradd \
--comment "${var_fullname}" \
--create-home \
--expiredate 2102-12-31 \
--gid "${var_gid}" \
--home-dir /home/"${var_username}" \
--inactive 0 \
--shell "${var_shell}" \
--uid "${var_uid}" \
"${var_username}"
eza_installer "${var_username}" "${var_target}"
;;
true:false)
chroot_exec "${var_target}" useradd \
--comment "${var_fullname}" \
--expiredate 2102-12-31 \
--gid "${var_gid}" \
--inactive 0 \
--no-create-home \
--shell "${var_shell}" \
--uid "${var_uid}" \
"${var_username}"
;;
false:true)
chroot_exec "${var_target}" useradd \
--comment "${var_fullname}" \
--create-home \
--expiredate 2102-12-31 \
--gid "${var_gid}" \
--home-dir /home/"${var_username}" \
--inactive 0 \
--shell "${var_shell}" \
--system \
--uid "${var_uid}" \
"${var_username}"
;;
true:true)
chroot_exec "${var_target}" useradd \
--comment "${var_fullname}" \
--expiredate 2102-12-31 \
--gid "${var_gid}" \
--inactive 0 \
--no-create-home \
--shell "${var_shell}" \
--system \
--uid "${var_uid}" \
"${var_username}"
;;
*)
do_log "fatal" "file_only" "4520() Not set: var_restricted:var_system [${var_restricted}:${var_system}]"
return "${ERR_ACCOUNT_CREATE}"
;;
esac
### 1) Prepare the 'user' account.
install -d -m 0700 -o "${var_uid}" -g "${var_gid}" "${var_target}/home/${var_username}/.ssh"
install -m 0600 -o "${var_uid}" -g "${var_gid}" /dev/null "${var_target}/home/${var_username}/.ssh/authorized_keys"
install -m 0600 -o "${var_uid}" -g "${var_gid}" "${VAR_SETUP_PATH}/includes/target/etc/skel/.bashrc" "${var_target}/home/${var_username}/"
if [[ "${var_shell}" == "/bin/zsh" ]]; then
if [[ -x "${var_target}${var_shell}" ]]; then
case "${var_specific,,}" in
"ciss")
zsh_omz_installer "${var_username}" "${var_target}"
mv "${var_target}/home/${var_username}/.zshrc" "${var_target}/home/${var_username}/.zshrc.bak"
install -m 0600 -o "${var_uid}" -g "${var_gid}" "${VAR_SETUP_PATH}/includes/target/etc/skel/.zshrc" "${var_target}/home/${var_username}"
;;
"physnet")
:
;;
"none"|*)
:
;;
esac
chroot_exec "${var_target}" chsh -s "${var_shell}" "${var_username}"
do_log "info" "file_only" "4520() Shell: '${var_shell}' used for: '${var_username}'."
else
chroot_exec "${var_target}" chsh -s /bin/bash "${var_username}"
do_log "info" "file_only" "4520() Shell: '${var_shell}' not found for: '${var_username}'. Using '/bin/bash' instead."
fi
fi
do_log "info" "file_only" "4520() Skeleton: '${var_username}' successfully generated."
### 2) Check SSH access capabilities.
### Nothing to do here as per-user SSH capabilities are already handled in '4330_installation_ssh.sh'.
### 3) Check tty access capabilities.
case "${var_access_tty,,}" in
false)
### 3) A) 1) Ensure the 'pam_access' line is not activated in '/etc/pam.d/login' and '/etc/pam.d/sshd' in parallel.
pam_access_sync_login_sshd
### 3) A) 2) This step is not required for user accounts.
### 3) A) 3) Disallow all local access for user in '/etc/security/access.conf'.
printf '%s\n' "-: ${var_username}:ALL" >> "${var_target}/etc/security/access.conf"
### 3) A) 4) This step is not required for user accounts.
do_log "info" "file_only" "4520() User: '${var_username}' tty access: [false]"
;;
true)
### 3) B) 1) Allow local access for 'user' only on 'tty1' in '/etc/security/access.conf'.
printf '%s\n' "+: ${var_username}:tty1" >> "${var_target}/etc/security/access.conf"
### 3) B) 2) This step is not required for user accounts.
do_log "info" "file_only" "4520() User: '${var_username}' tty access: [true]"
;;
*)
do_log "fatal" "file_only" "4520() Not set: var_access_tty [${var_access_tty}]"
return "${ERR_ACCOUNT_CREATE}"
;;
esac
### 4) Check the password policy for the 'user' account.
case "${var_auth_pwd}" in
false)
chroot_script "${var_target}" "passwd -l ${var_username}"
do_log "info" "file_only" "4520() User: '${var_username}' password access: [false]"
;;
true)
chroot_script "${var_target}" "printf '%s:%s\n' \"${var_username}\" '${var_password}' | /usr/sbin/chpasswd -e"
#chroot_script "${var_target}" "/usr/sbin/usermod -p '${var_password}' ${var_username}"
do_log "info" "file_only" "4520() User: '${var_username}' password access: [true]"
;;
*)
do_log "fatal" "file_only" "4520() Not set: var_auth_pwd [${var_auth_pwd}]"
return "${ERR_ACCOUNT_CREATE}"
;;
esac
### 5) Update the 'user' SSH pubkey, if provided via 'preseed.yaml'.
if [[ -n "${var_sshpubkey:-}" ]]; then
printf "%s\n" "${var_sshpubkey}" >| "${var_target}/home/${var_username}/.ssh/authorized_keys"
do_log "info" "file_only" "4520() User: '${var_username}' SSH public key: inserted."
fi
### 6) Update the 'root' 'totp'-policy and write the '.google_authenticator'-file.
if [[ "${var_2fa_ssh}" == "true" || "${var_2fa_tty}" == "true" ]]; then
write_google_authenticator_file "${var_username}" "${var_uid}" "${var_gid}" "${var_target}"
fi
if [[ "${var_2fa_ssh}" == "true" ]]; then
pam_access_totp_enable "${var_username}" "sshd" "${var_target}"
var_ssh_totp_update="true"
cat << EOF >> "${var_target}/etc/ssh/sshd_config"
Match User ${var_username}
AuthenticationMethods publickey,keyboard-interactive:pam
EOF
fi
[[ "${var_2fa_tty}" == "true" ]] && pam_access_totp_enable "${var_username}" "login" "${var_target}"
### 7) Check sudo membership for user.
if [[ "${var_sudo}" == "true" ]]; then
chroot_exec "${var_target}" usermod -aG sudo "${var_username}"
### Hardening sudo users (idempotent) and ensure WinSCP SFTP-as-root.
hardening_sudo "${var_username}" "${var_specific:-none}" "${var_target}"
### Enable per-user TOTP in a given PAM service (login, sshd, su, sudo).
pam_access_totp_enable "${var_username}" "sudo" "${var_target}"
fi
### 8) Double check permissions.
### Directories: 0700
find "${var_target}/home/${var_username}" -type d -exec chmod 0700 {} +
### Executable files: 0700 (any x-bit set)
find "${var_target}/home/${var_username}" -type f -perm /111 -exec chmod 0700 {} +
### Non-executable files: 0600
find "${var_target}/home/${var_username}" -type f ! -perm /111 -exec chmod 0600 {} +
### Ownership: UID:GID (do not dereference symlinks; stay on this filesystem)
find "${var_target}/home/${var_username}" -xdev -exec chown -h "${var_uid}:${var_gid}" {} +
### 9) Final status logging.
do_log "info" "file_only" "4520() Created user: [${var_username}] UID: [${var_uid}] GID: [${var_gid}]"
done
if [[ "${var_ssh_totp_update}" == "true" ]]; then
sed -i -E "s|^[[:space:]]*KbdInteractiveAuthentication[[:space:]]+.*$|$(printf '%-29s%s' 'KbdInteractiveAuthentication' 'yes')|" "${var_target}/etc/ssh/sshd_config"
fi
unset VAR_TEMP_PLAIN_MFA_SEED
if ! grep -Fqx -- '-: ALL:ALL' "${var_target}/etc/security/access.conf"; then
printf '%s\n' '-: ALL:ALL' >> "${var_target}/etc/security/access.conf"
fi
printf "# vim: number et ts=2 sw=2 sts=2 ai tw=128 ft=conf \n" >> "${var_target}/etc/security/access.conf"
printf "# vim: number et ts=2 sw=2 sts=2 ai tw=128 ft=conf \n" >> "${var_target}/etc/ssh/sshd_config"
### Hardening of '/bin/su': only members of the group 'sudo' can su to root.
hardening_su
guard_dir && return 0
}
### Prevents accidental 'unset -f'.
# shellcheck disable=SC2034
readonly -f accounts_setup
#######################################
# Install eza CISS theme for the respective user.
# Globals:
# None
# Arguments:
# 1: Username
# 2: Target
# Returns:
# 0: on success
#######################################
eza_installer() {
### Declare Arrays, HashMaps, and Variables.
declare -r var_user="${1}" var_target="${2}"
case "${1}" in
root) declare var_base="/root" ;;
*) declare var_base="/home/${var_user}" ;;
esac
chroot_script "${var_target}" "
cd ${var_base}
git clone https://github.com/eza-community/eza-themes.git
mkdir -p ${var_base}/.config/eza
ln -sf ${var_base}/.ciss/theme_eza_ciss.yml ${var_base}/.config/eza/theme.yml
"
return 0
}
### Prevents accidental 'unset -f'.
# shellcheck disable=SC2034
readonly -f eza_installer
#######################################
# Generates a deterministic TOTP secret based on:
# Username, FQDN, MFA salt, MFA master seed
# Globals:
# VAR_FINAL_FQDN
# VAR_TEMP_PLAIN_MFA_SEED
# user_mfa_info
# user_mfa_salt
# Arguments:
# 1: Username
# Returns:
# 0: on success
#######################################
generate_totp_secret() {
### Declare Arrays, HashMaps, and Variables.
declare var_user="${1}"
declare var_host_id="${VAR_FINAL_FQDN}"
declare var_salt="${user_mfa_salt}:${var_host_id}:${var_user}"
declare var_info="${user_mfa_info}"
declare var_secret=""
guard_trace on
### Derive 20 bytes via HKDF-SHA256 using OpenSSL 3 kdf, output as raw, then base32 (uppercase, no padding).
### NOTE: 'key' must be provided via '-kdfopt key:hex:<STRING>'; expects a hexstring (no spaces).
# shellcheck disable=SC2312
var_secret="$(
openssl kdf -keylen 20 \
-kdfopt digest:SHA256 \
-kdfopt key:hex:"${VAR_TEMP_PLAIN_MFA_SEED}" \
-kdfopt salt:"${var_salt}" \
-kdfopt info:"${var_info}" \
-binary HKDF \
| base32 \
| tr -d '=' \
| tr '[:lower:]' '[:upper:]'
)"
printf '%s\n' "${var_secret}"
guard_trace off
return 0
}
### Prevents accidental 'unset -f'.
# shellcheck disable=SC2034
readonly -f generate_totp_secret
#######################################
# Hardening of '/bin/su': only members of the group 'sudo' can su to root.
# Globals:
# None
# Arguments:
# None
# Returns:
# 0: on success
#######################################
hardening_su() {
### Declare Arrays, HashMaps, and Variables.
declare -r pam_su="/etc/pam.d/su"
[[ -f "${var_target}${pam_su}" ]] || return 0
chroot_stdin "${var_target}" "__payload__" -- "${pam_su}" <<'EOF'
export LC_ALL=C
pam="$1"
if grep -Eq '^[[:space:]]*auth[[:space:]]+required[[:space:]]+pam_wheel[.]so([[:space:]].*)?group=sudo([[:space:]].*)?use_uid' "${pam}"; then
:
else
tmp="$(mktemp "${pam}.XXXXXX")"
### 1) Insert rule before pam_unix.so or pam_rootok.so (fail early). Fallback: append.
awk '
BEGIN { ins=0 }
{
if (!ins && $0 ~ /^[[:space:]]*auth[[:space:]]+.*pam_(unix|rootok)[.]so/ ) {
print "auth required pam_wheel.so use_uid group=sudo"
ins=1
}
print
}
END {
if (!ins) {
print "auth required pam_wheel.so use_uid group=sudo"
}
}
' "${pam}" >| "${tmp}"
test -s "${tmp}"
mv -f "${tmp}" "${pam}"
rm -f -- "${tmp}" || :
fi
:
EOF
return 0
}
### Prevents accidental 'unset -f'.
# shellcheck disable=SC2034
readonly -f hardening_su
#######################################
# Hardening sudo users (idempotent) and ensure WinSCP SFTP-as-root.
# Globals:
# None
# Arguments:
# 1: username
# 2: user_specifics
# 3: target
# Returns:
# 0: on success
# ERR_VERIFY_LOGROTATE: on failure
# ERR_VERIFY_VISUDO: on failure
#######################################
hardening_sudo() {
### Declare Arrays, HashMaps, and Variables.
declare -r var_user="${1}" var_specific="${2}" var_target="${3}"
declare -r var_logfile="/root/.ciss/cdi/log/4520_accounts_setup.log"
declare -r var_sudo_iolog_dir="${var_target}/var/log/sudo-io"
declare -r var_sudoers_main="${var_target}/etc/sudoers"
declare -r var_sudoers_dir="${var_target}/etc/sudoers.d"
declare -r var_lr_conf="${var_target}/etc/logrotate.d/sudo"
declare -r var_sudoers_winscp_global="${var_target}/etc/sudoers.d/90-ciss-winscp-sftp"
declare -r var_sudoers_winscp_user="${var_target}/etc/sudoers.d/91-ciss-winscp-${var_user}"
declare -r var_sftp_bin="/usr/lib/openssh/sftp-server"
### Create sudo I/O log directory (idempotent).
if [[ ! -d "${var_sudo_iolog_dir}" ]]; then
mkdir -p "${var_sudo_iolog_dir}"
chmod 0700 "${var_sudo_iolog_dir}"
else
### Enforce restrictive perms on an existing tree.
chmod 0700 "${var_sudo_iolog_dir}"
fi
### Ensure sudoers Defaults are present only once. We key on 'iolog_dir' to avoid duplicate blocks.
if ! grep -qF 'iolog_dir="/var/log/sudo-io"' "${var_sudoers_main}" 2>/dev/null; then
cat << 'EOF' >> "${var_sudoers_main}"
##### Added by CISS.debian.installer
Defaults log_host, log_year, log_input, log_exit_status, log_subcmds, logfile="/var/log/sudo.log", iolog_dir="/var/log/sudo-io"
EOF
fi
case "${var_specific,,}" in
"ciss")
### Install global WinSCP SFTP-as-root command alias (idempotent).
if [[ -x "${var_target}${var_sftp_bin}" ]]; then
if [[ ! -f "${var_sudoers_winscp_global}" ]]; then
insert_header "${var_sudoers_winscp_global}"
insert_comments "${var_sudoers_winscp_global}"
cat << EOF >| "${var_sudoers_winscp_global}"
### Added by CISS.debian.installer. WinSCP SFTP-as-root (least privilege).
### Allow exactly the sftp-server binary, optionally with -e (stderr logging).
Cmnd_Alias CISS_SFTPROOT = ${var_sftp_bin}, ${var_sftp_bin} -e
# Command-scoped hardening for the alias:
# - noexec : disallow further exec()
# - !setenv : forbid env manipulation
# - timestamp_timeout=0: require re-auth each time (no caching)
Defaults!CISS_SFTPROOT noexec, !setenv, timestamp_timeout=0
# vim: number et ts=2 sw=2 sts=2 ai tw=128 ft=conf
EOF
chmod 0440 "${var_sudoers_winscp_global}"
fi
else
do_log "warn" "file_only" "4520() sftp-server not found at [${var_sftp_bin}] in TARGET; skipping global alias for now."
fi
### Grant this user access to the alias (idempotent). Only add if not already present; keep the file permissive correctness.
if [[ -f "${var_sudoers_winscp_user}" ]]; then
if ! grep -qE "^${var_user}\s+ALL=\(root\)\s+NOPASSWD:\s+CISS_SFTPROOT\b" "${var_sudoers_winscp_user}" 2>/dev/null; then
echo "${var_user} ALL=(root) NOPASSWD: CISS_SFTPROOT" >> "${var_sudoers_winscp_user}"
fi
else
insert_header "${var_sudoers_winscp_user}"
insert_comments "${var_sudoers_winscp_user}"
echo "${var_user} ALL=(root) PASSWD: CISS_SFTPROOT" >> "${var_sudoers_winscp_user}"
printf "# vim: number et ts=2 sw=2 sts=2 ai tw=128 ft=conf \n" >> "${var_sudoers_winscp_user}"
fi
chmod 0440 "${var_sudoers_winscp_user}"
;;
"physnet")
:
;;
"none"|*)
:
;;
esac
### Tighten perms on sudoers.d (idempotent).
find "${var_sudoers_dir}" -type f -exec chmod 0440 {} \;
### Verify sudoers syntax in chroot.
if ! chroot_script "${var_target}" "EDITOR=/usr/bin/nano /usr/sbin/visudo -q -c >> ${var_logfile}"; then
do_log "warn" "file_only" "4520() Command: [chroot_script ${var_target} EDITOR=/usr/bin/nano /usr/sbin/visudo -q -c] failed."
return "${ERR_VERIFY_VISUDO}"
else
do_log "info" "file_only" "4520() Command: [chroot_script ${var_target} EDITOR=/usr/bin/nano /usr/sbin/visudo -q -c] successful."
fi
### Ensure logrotate for '/var/log/sudo.log' exists once.
if ! grep -qF "/var/log/sudo.log {" "${var_lr_conf}" 2>/dev/null; then
insert_header "${var_lr_conf}"
insert_comments "${var_lr_conf}"
cat << EOF >> "${var_lr_conf}"
/var/log/sudo.log {
daily
rotate 128
maxage 384
compress
delaycompress
missingok
notifempty
create 600 root root
sharedscripts
postrotate
/usr/bin/systemctl reload sudo.service > /dev/null 2>&1 || true
endscript
}
EOF
### Verify logrotate config in chroot.
if ! chroot_script "${var_target}" "logrotate -d /etc/logrotate.conf >> ${var_logfile}"; then
do_log "warn" "file_only" "4520() Command: [chroot_script ${var_target} logrotate -d /etc/logrotate.conf] failed."
return "${ERR_VERIFY_LOGROTATE}"
else
do_log "info" "file_only" "4520() Command: [chroot_script ${var_target} logrotate -d /etc/logrotate.conf] successful."
fi
fi
return 0
}
### Prevents accidental 'unset -f'.
# shellcheck disable=SC2034
readonly -f hardening_sudo
#######################################
# Ensure the 'pam_access' line is not activated in '/etc/pam.d/login' and '/etc/pam.d/sshd' in parallel.
# MUST be executed inside chroot.
# Globals:
# None
# Arguments:
# None
# Returns:
# 0: on success
#######################################
pam_access_sync_login_sshd() {
### Declare Arrays, HashMaps, and Variables.
declare var_file_login="/etc/pam.d/login"
declare var_file_sshd="/etc/pam.d/sshd"
### Guard: The file must exist, no-op otherwise.
if [[ ! -f "${var_target}${var_file_login}" ]]; then
return 0
fi
### 1) If the 'pam_access' line is commented in '/etc/pam.d/login', uncomment exactly one occurrence.
chroot_stdin "${var_target}" "__payload__" <<'EOF'
tmp="$(mktemp /etc/pam.d/login.XXXXXX)"
export LC_ALL=C
awk '
BEGIN { done=0 }
{
if (!done) {
line=$0
sub(/^[[:space:]]*#+[[:space:]]*/, "", line)
if (line ~ /^[[:space:]]*account[[:space:]]+required[[:space:]]+pam_access[.]so([[:space:]]|$)/) {
print line; done=1; next
}
}
print
}
' /etc/pam.d/login >| "${tmp}"
test -s "${tmp}"
mv -f "${tmp}" /etc/pam.d/login
rm -f "${tmp}" || :
:
EOF
### 2) If '/etc/pam.d/login' now has an active pam_access line, ensure '/etc/pam.d/sshd' pam_access line(s) are commented out.
### No-op if '/etc/pam.d/sshd' is absent.
[[ -f "${var_target}${var_file_sshd}" ]] || return 0
chroot_stdin "${var_target}" "__payload__" <<'EOF'
export LC_ALL=C
if grep -Eq '^[[:space:]]*account[[:space:]]+required[[:space:]]+pam_access[.]so([[:space:]]|$)' /etc/pam.d/login; then
tmp="$(mktemp /etc/pam.d/sshd.XXXXXX)"
awk '
/^[[:space:]]*account[[:space:]]+required[[:space:]]+pam_access[.]so([[:space:]]|$)/ { print "# " $0; next }
{ print }
' /etc/pam.d/sshd >| "${tmp}"
test -s "${tmp}"
mv -f "${tmp}" /etc/pam.d/sshd
rm -f "${tmp}" || :
fi
:
EOF
return 0
}
### Prevents accidental 'unset -f'.
# shellcheck disable=SC2034
readonly -f pam_access_sync_login_sshd
#######################################
# Enable per-user TOTP in a given PAM service (login, sshd, su, sudo).
# Globals:
# None
# Arguments:
# 1: username
# 2: pam_module
# 3: target
# Returns:
# 0: on success
#######################################
pam_access_totp_enable() {
### Declare Arrays, HashMaps, and Variables.
declare -r var_user="${1}" var_module="${2}" var_target="${3}"
declare -r var_pam_file="/etc/pam.d/${var_module}"
declare -r var_users_file="${var_target}/etc/ciss/2fa.users"
declare -r var_allowlist="/etc/ciss/2fa.users"
### Basic sanitation; module must be a safe 'pam.d' filename.
[[ -n "${var_user:-}" && -n "${var_module:-}" ]] || return 0
[[ "${var_module}" =~ ^[A-Za-z0-9._+-]+$ ]] || return 0
[[ -f "${var_target}${var_pam_file}" ]] || return 0
### 0) Ensure the allowlist file contains the user (deduplicated).
if ! grep -Fxq "${var_user}" "${var_users_file}"; then
printf '%s\n' "${var_user}" >> "${var_users_file}"
fi
### 1) Ensure a single CISS TOTP framework block is present in the PAM file.
### The block gates GA by pam_listfile over '/etc/ciss/2fa.users'.
### We place it right after pam_unix.so or @include common-auth; fallback: append.
chroot_stdin "${var_target}" "__payload__" -- "${var_pam_file}" "${var_allowlist}" <<'EOF'
export LC_ALL=C
pam="$1"
allowlist="$2"
tmp="$(mktemp "${pam}.XXXXXX")"
awk -v MARK_S="# CISS TOTP START" -v MARK_E="# CISS TOTP END" -v allowlist="${allowlist}" '
BEGIN { ins=0 }
{
print
if (!ins && ($0 ~ /^[[:space:]]*auth[[:space:]]+.*pam_unix[.]so/ \
|| $0 ~ /^[[:space:]]*@include[[:space:]]+common-auth/)) {
print MARK_S
### Only users in allowlist must pass GA:
### pam_listfile sense=deny succeeds for non-listed > skip next line (GA)
print "auth [success=1 default=ignore] pam_listfile.so item=user sense=deny file=" allowlist " onerr=ignore"
print "auth required pam_google_authenticator.so"
print MARK_E
ins=1
}
}
END {
if (!ins) {
print MARK_S
print "auth [success=1 default=ignore] pam_listfile.so item=user sense=deny file=" allowlist " onerr=ignore"
print "auth required pam_google_authenticator.so"
print MARK_E
}
}
' "${pam}" >| "${tmp}"
test -s "${tmp}"
mv -f "${tmp}" "${pam}"
rm -f -- "${tmp}" || :
:
EOF
### 2) Comment out any other active GA lines to avoid double prompts.
chroot_stdin "${var_target}" "__payload__" -- "${var_pam_file}" <<'EOF'
export LC_ALL=C
pam="$1"
tmp="$(mktemp "${pam}.XXXXXX")"
awk '
BEGIN { in_ciss=0 }
/^# CISS TOTP START$/ { in_ciss=1; print; next }
/^# CISS TOTP END$/ { in_ciss=0; print; next }
{
if (!in_ciss && $0 ~ /^[[:space:]]*auth[[:space:]]+.*pam_google_authenticator[.]so/ && $0 !~ /^[[:space:]]*#/) {
print "# " $0
} else {
print
}
}
' "${pam}" >| "${tmp}"
test -s "${tmp}"
mv -f "${tmp}" "${pam}"
rm -f -- "${tmp}" || :
:
EOF
return 0
}
### Prevents accidental 'unset -f'.
# shellcheck disable=SC2034
readonly -f pam_access_totp_enable
#######################################
# Reads a 256-bit seed from '${DIR_CNF}/mfa_master.txt' (64 hex chars) into VAR_TEMP_PLAIN_MFA_SEED.
# Globals:
# DIR_CNF
# VAR_TEMP_PLAIN_MFA_SEED
# Arguments:
# None
# Returns:
# 0: on success
# ERR_READ_SEED_FILE
#######################################
read_totp_seed(){
### Declare Arrays, HashMaps, and Variables.
declare -r var_mfa_seed_file="${DIR_CNF}/mfa_master.txt"
declare -g VAR_TEMP_PLAIN_MFA_SEED=""
guard_trace on
if ! read_password_file "${var_mfa_seed_file}" VAR_TEMP_PLAIN_MFA_SEED; then
return "${ERR_READ_SEED_FILE}"
fi
### Validate: exactly 64 hex.
[[ "${VAR_TEMP_PLAIN_MFA_SEED}" =~ ^[0-9a-fA-F]{64}$ ]] || return "${ERR_READ_SEED_FILE}"
guard_trace off
return 0
}
### Prevents accidental 'unset -f'.
# shellcheck disable=SC2034
readonly -f read_totp_seed
#######################################
# Writes '.google_authenticator'-file for the respective user.
# Globals:
# DIR_TMP
# Arguments:
# 1: USERNAME
# 2: UID
# 3: GID
# 4: TARGET
# Returns:
# 0: on success
#######################################
write_google_authenticator_file() {
### Declare Arrays, HashMaps, and Variables.
declare -r var_user="${1}" var_user_id="${2}" var_group_id="${3}" var_target="${4}"
declare var_secret=""
case "${1}" in
root) declare var_base="${var_target}/root" ;;
*) declare var_base="${var_target}/home/${var_user}" ;;
esac
declare -i i=0
guard_trace on
var_secret="$(generate_totp_secret "${var_user}")"
umask 0077
{
declare accept hex val
printf '%s\n' "${var_secret}"
printf '" RATE_LIMIT 3 30\n'
printf '" WINDOW_SIZE 10\n'
printf '" DISALLOW_REUSE\n'
printf '" TOTP_AUTH\n'
### Emergency Codes (8x unbiased 8-digit, CSPRNG via OpenSSL).
for i in {1..8}; do
### Draw 32 bits; rejection sampling to avoid modulo bias.
while :; do
hex="$(openssl rand -hex 4)" || exit 1
val=$((16#${hex})) # 0..4294967295
accept=$(( (1<<32) / 100000000 * 100000000 )) # 4_200_000_000
if (( val < accept )); then
printf '%08d\n' "$(( val % 100000000 ))"
break
fi
done
done
} >| "${var_base}/.google_authenticator"
chown "${var_user_id}:${var_group_id}" "${var_base}/.google_authenticator"
chmod 0600 "${var_base}/.google_authenticator"
{
printf '%s\n' "${var_user}"
printf '%s\n' "${var_secret}"
} >| "${DIR_TMP}/TOTP_${var_user}.secret"
chmod 0400 "${DIR_TMP}/TOTP_${var_user}.secret"
umask 0022
guard_trace off
return 0
}
### Prevents accidental 'unset -f'.
# shellcheck disable=SC2034
readonly -f write_google_authenticator_file
#######################################
# Use the official ohmyzsh-installer but force non-interactive behavior; do not run zsh; do not chsh.
# Globals:
# None
# Arguments:
# 1: Username
# 2: Target
# Returns:
# 0: on success
#######################################
zsh_omz_installer() {
### Declare Arrays, HashMaps, and Variables.
declare -r var_user="${1}" var_target="${2}"
### Install Oh My Zsh and two plugins for a given user (non-interactive, idempotent).
### Args to payload: $1 = username (e.g., "root" or "alice")
chroot_stdin "${var_target}" "__payload__" -- "${var_user}" <<'EOF'
export LC_ALL=C
user="$1"
### Resolve account data
pwline="$(getent passwd "${user}" || true)"
[[ -n "${pwline}" ]] || { echo "User not found: ${user}" >&2; exit 1; }
IFS=: read -r _ _ uid gid _ home _ <<<"${pwline}"
if [[ "${uid}" -eq 0 ]]; then
### root user: no su needed
/bin/bash -s <<'ZSHROOT'
#!/bin/bash
set -Ceuo pipefail
export LC_ALL=C
umask 077
### We are running as the target user here
ZSH_DIR="${HOME}/.oh-my-zsh"
### If ZSH_DIR exists but is EMPTY (e.g., previous aborted run), remove it, so the installer can proceed.
if [[ -d "${ZSH_DIR}" ]] && [[ -z "$(ls -A "${ZSH_DIR}")" ]]; then
rm -rf "${ZSH_DIR}"
fi
### If already installed (git repo present), skip the installer.
if [ -d "${ZSH_DIR}/.git" ]; then
:
else
### Download installer to a temp file and run it with non-interactive env.
inst="$(mktemp)"
if command -v wget >/dev/null 2>&1; then
wget -qO "${inst}" https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh
else
curl -fsSL -o "${inst}" https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh
fi
### Ensure that ZSH is not set for the installer, and keep it fully non-interactive.
RUNZSH=no CHSH=no KEEP_ZSHRC=yes env -u ZSH sh "${inst}"
rm -f "${inst}"
fi
### Install plugins (shallow clone; idempotent)
ZSH_CUSTOM="${ZSH_DIR}/custom"
mkdir -p "${ZSH_CUSTOM}/plugins"
[[ -d "${ZSH_CUSTOM}/plugins/zsh-autosuggestions/.git" ]] || \
git clone --depth 1 https://github.com/zsh-users/zsh-autosuggestions "${ZSH_CUSTOM}/plugins/zsh-autosuggestions"
[[ -d "${ZSH_CUSTOM}/plugins/zsh-syntax-highlighting/.git" ]] || \
git clone --depth 1 https://github.com/zsh-users/zsh-syntax-highlighting "${ZSH_CUSTOM}/plugins/zsh-syntax-highlighting"
### '~/.zshrc' will be updated later in the main CISS.debian.installer environment.
### Do NOT start zsh here and do NOT chsh (RUNZSH/CHSH handled above).
:
ZSHROOT
### ----------------------------------------------------------------------------------------------------------------------------
else
su - "${user}" -s /bin/bash <<'ZSHUSER'
#!/bin/bash
set -Ceuo pipefail
export LC_ALL=C
umask 077
### We are running as the target user here
ZSH_DIR="${HOME}/.oh-my-zsh"
### If ZSH_DIR exists but is EMPTY (e.g., previous aborted run), remove it, so the installer can proceed.
if [[ -d "${ZSH_DIR}" ]] && [[ -z "$(ls -A "${ZSH_DIR}")" ]]; then
rm -rf "${ZSH_DIR}"
fi
### If already installed (git repo present), skip the installer.
if [ -d "${ZSH_DIR}/.git" ]; then
:
else
### Download installer to a temp file and run it with non-interactive env.
inst="$(mktemp)"
if command -v wget >/dev/null 2>&1; then
wget -qO "${inst}" https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh
else
curl -fsSL -o "${inst}" https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh
fi
### Ensure that ZSH is not set for the installer, and keep it fully non-interactive.
RUNZSH=no CHSH=no KEEP_ZSHRC=yes env -u ZSH sh "${inst}"
rm -f "${inst}"
fi
### Install plugins (shallow clone; idempotent)
ZSH_CUSTOM="${ZSH_DIR}/custom"
mkdir -p "${ZSH_CUSTOM}/plugins"
[[ -d "${ZSH_CUSTOM}/plugins/zsh-autosuggestions/.git" ]] || \
git clone --depth 1 https://github.com/zsh-users/zsh-autosuggestions "${ZSH_CUSTOM}/plugins/zsh-autosuggestions"
[[ -d "${ZSH_CUSTOM}/plugins/zsh-syntax-highlighting/.git" ]] || \
git clone --depth 1 https://github.com/zsh-users/zsh-syntax-highlighting "${ZSH_CUSTOM}/plugins/zsh-syntax-highlighting"
### '~/.zshrc' will be updated later in the main CISS.debian.installer environment.
### Do NOT start zsh here and do NOT chsh (RUNZSH/CHSH handled above).
:
ZSHUSER
### ----------------------------------------------------------------------------------------------------------------------------
fi
EOF
return 0
}
### Prevents accidental 'unset -f'.
# shellcheck disable=SC2034
readonly -f zsh_omz_installer
#######################################
# Writes CISS Header for '/etc/pam.d/login'.
# Globals:
# None
# Arguments:
# 1: TARGET
# Returns:
# 0: on success
#######################################
write_pam_login() {
### Declare Arrays, HashMaps, and Variables.
declare -r var_target="$1"
mv "${var_target}/etc/pam.d/sshd" "${var_target}/root/.ciss/cdi/backup/etc/pam.d/login"
insert_header "${var_target}/etc/pam.d/login"
insert_comments "${var_target}/etc/pam.d/login"
cat << EOF >> "${var_target}/etc/pam.d/login"
#
# The PAM configuration file for the Shadow 'login' service
#
# Enforce a minimal delay in case of failure (in microseconds). (Replaces the 'FAIL_DELAY' setting from login.defs).
# Note that other modules may require another minimal delay. (For example, to disable any delay, you should add the 'nodelay'
# option to pam_unix).
auth optional pam_faildelay.so delay=3000000
# Outputs an issue file prior to each login prompt (Replaces the ISSUE_FILE option from login.defs). Uncomment for use.
# auth required pam_issue.so issue=/etc/issue
# Disallows other than root logins when /etc/nologin exists. (Replaces the 'NOLOGINS_FILE' option from login.defs).
auth requisite pam_nologin.so
# SELinux needs to be the first session rule. This ensures that any lingering context has been cleared. Without this it is
# possible that a module could execute code in the wrong domain. When the module is present, "required" would be sufficient
# (When SELinux is disabled, this returns success.)
session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux.so close
# Sets the loginuid process attribute
session required pam_loginuid.so
# Prints the message of the day upon successful login. (Replaces the 'MOTD_FILE' option in login.defs). This includes a
# dynamically generated part from /run/motd.dynamic, and a static (admin-editable) part from /etc/motd.
session optional pam_motd.so motd=/run/motd.dynamic
session optional pam_motd.so noupdate
# SELinux needs to intervene at login time to ensure that the process starts in the proper default security context. Only
# sessions which are intended to run in the user's context should be run after this. The module pam_selinux.so changes the
# SELinux context of the used TTY and configures SELinux in order to transition to the user context with the next execve()
# call.
session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux.so open
# When the module is present, "required" would be sufficient (When SELinux is disabled, this returns success.)
# This module parses environment configuration file(s) and also allows you to use an extended config file
# /etc/security/pam_env.conf. Parsing /etc/environment needs "readenv=1"
session required pam_env.so readenv=1
# Locale variables can also be set in /etc/default/locale reading this file *in addition to /etc/environment* does not hurt.
session required pam_env.so readenv=1 envfile=/etc/default/locale
# Standard Un*x authentication.
@include common-auth
# ===== CISS 2FA block =====
# If user is NOT listed -> succeed and SKIP next two lines (no TOTP prompt).
auth [success=2 default=ignore] pam_listfile.so item=user sense=deny file=/etc/ciss/2fa onerr=ignore
# Listed users: show a clear hint and then require GA. No 'nullok': missing secret -> fail.
auth required pam_echo.so file=/etc/ciss/login_totp.prompt
auth required pam_google_authenticator.so
# ===== CISS 2FA block end =====
# This allows certain extra groups to be granted to a user based on things like time of day, tty, service, and user. Please
# edit /etc/security/group.conf to fit your needs (Replaces the 'CONSOLE_GROUPS' option in login.defs).
auth optional pam_group.so
# Uncomment and edit /etc/security/time.conf if you need to set time restraint on logins. (Replaces the 'PORTTIME_CHECKS_ENAB'
# option from login.defs as well as /etc/porttime).
# account requisite am_time.so
# Uncomment and edit /etc/security/access.conf if you need to set access limits. (Replaces /etc/login.access file).
# account required pam_access.so
# Sets up user limits according to /etc/security/limits.conf. (Replaces the use of /etc/limits in old login).
session required pam_limits.so
# Prints the status of the user's mailbox upon successful login (Replaces the 'MAIL_CHECK_ENAB' option from login.defs).
# This also defines the MAIL environment variable. However, userdel also needs MAIL_DIR and MAIL_FILE variables in
# /etc/login.defs to make sure that removing a user also removes the user's mail spool file. See comments in /etc/login.defs.
session optional pam_mail.so standard
# Create a new session keyring.
session optional pam_keyinit.so force revoke
# Standard Un*x account and session
@include common-account
@include common-session
@include common-password
# vim: number et ts=2 sw=2 sts=2 ai tw=128 ft=conf
EOF
do_log "info" "file_only" "4520() Written: [/etc/pam.d/login]."
cat << 'EOF' >| "${var_target}/etc/ciss/login_totp.prompt"
After your UNIX password, please enter your 6-digit TOTP code.
EOF
chmod 0444 "${var_target}/etc/ciss/login_totp.prompt"
do_log "info" "file_only" "4520() Written: [/etc/ciss/login_totp.prompt]."
return 0
}
### Prevents accidental 'unset -f'.
# shellcheck disable=SC2034
readonly -f write_pam_login
#######################################
# Writes CISS Header for '/etc/pam.d/sshd'.
# Globals:
# None
# Arguments:
# 1: TARGET
# Returns:
# 0: on success
#######################################
write_pam_sshd() {
### Declare Arrays, HashMaps, and Variables.
declare -r var_target="$1"
mv "${var_target}/etc/pam.d/sshd" "${var_target}/root/.ciss/cdi/backup/etc/pam.d/sshd"
insert_header "${var_target}/etc/pam.d/sshd"
insert_comments "${var_target}/etc/pam.d/sshd"
cat << EOF >> "${var_target}/etc/pam.d/sshd"
#
# PAM configuration for the Secure Shell service
#
# ===== CISS 2FA block =====
# If user is NOT listed -> succeed and SKIP next two lines (silent Keyboard-Interactive (KI) success).
auth [success=2 default=ignore] pam_listfile.so item=user sense=deny file=/etc/ciss/2fa onerr=ignore
# For listed users: enforce that the secret file exists, else deny without prompting.
# pam_google_authenticator will itself fail if the file is absent; we add a clear hint before it.
auth required pam_echo.so file=/etc/ciss/ssh_totp.prompt
auth required pam_google_authenticator.so
# No 'nullok' here: listed users MUST have a secret; missing -> hard fail.
# For non-2FA users KI must be a silent success to satisfy AuthenticationMethods.
auth sufficient pam_permit.so
# ===== CISS 2FA block end =====
# Keep the rest as shipped by Debian. It will be short-circuited by pam_permit for KI and never reached for 2FA users after
# successful GA.
# Standard Un*x authentication.
@include common-auth
# Disallow non-root logins when /etc/nologin exists.
account required pam_nologin.so
# Uncomment and edit /etc/security/access.conf if you need to set complex access limits that are hard to express in sshd_config.
# account required pam_access.so
# Standard Un*x authorization.
@include common-account
# SELinux needs to be the first session rule. This ensures that any lingering context has been cleared. Without this it is
# possible that a module could execute code in the wrong domain.
session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux.so close
# Set the loginuid process attribute.
session required pam_loginuid.so
# Create a new session keyring.
session optional pam_keyinit.so force revoke
# Standard Un*x session setup and teardown.
@include common-session
# Print the message of the day upon successful login. This includes a dynamically generated part from /run/motd.dynamic and a
# static (admin-editable) part from /etc/motd.
session optional pam_motd.so motd=/run/motd.dynamic
session optional pam_motd.so noupdate
# Print the status of the user's mailbox upon successful login.
session optional pam_mail.so standard noenv # [1]
# Set up user limits from /etc/security/limits.conf.
session required pam_limits.so
# Read environment variables from /etc/environment and /etc/security/pam_env.conf.
session required pam_env.so # [1]
# In Debian 4.0 (etch), locale-related environment variables were moved to /etc/default/locale, so read that as well.
session required pam_env.so envfile=/etc/default/locale
# SELinux needs to intervene at login time to ensure that the process starts in the proper default security context. Only
# sessions which are intended to run in the user's context should be run after this.
session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux.so open
# Standard Un*x password updating.
@include common-password
# vim: number et ts=2 sw=2 sts=2 ai tw=128 ft=conf
EOF
do_log "info" "file_only" "4520() Written: [/etc/pam.d/sshd]."
cat << 'EOF' >| "${var_target}/etc/ciss/ssh_totp.prompt"
Please enter your 6-digit TOTP code for %u@%H.
EOF
chmod 0444 "${var_target}/etc/ciss/ssh_totp.prompt"
do_log "info" "file_only" "4520() Written: [/etc/ciss/ssh_totp.prompt]."
return 0
}
### Prevents accidental 'unset -f'.
# shellcheck disable=SC2034
readonly -f write_pam_sshd
# vim: number et ts=2 sw=2 sts=2 ai tw=128 ft=sh