#!/bin/bash # SPDX-Version: 3.0 # SPDX-CreationInfo: 2025-06-17; WEIDNER, Marc S.; # 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.; # 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 ####################################### # 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 # VAR_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" chroot_logger "${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 "${TARGET}/root/.ssh" install -m 0600 -o root -g root /dev/null "${TARGET}/root/.ssh/authorized_keys" install -m 0600 -o root -g root "${VAR_SETUP_PATH}/includes/target/etc/skel/.bashrc" "${TARGET}/root/" install -m 0600 -o root -g root "${VAR_SETUP_PATH}/includes/target/etc/skel/.ciss/theme_eza_ciss.yml" "${TARGET}/root/.ciss/" if [[ "${user_root_shell}" == "/bin/zsh" ]]; then if [[ -x "${TARGET}${user_root_shell}" ]]; then case "${VAR_USER_ROOT_SPECIFIC,,}" in "ciss") zsh_omz_installer "root" mv "${TARGET}/root/.zshrc" "${TARGET}/root/.zshrc.bak" install -m 0600 -o root -g root "${VAR_SETUP_PATH}/includes/target/etc/skel/.zshrc" "${TARGET}/root/" ;; "physnet") : ;; "none"|*) : ;; esac chroot_exec "${TARGET}" chsh -s "${user_root_shell}" root do_log "info" "file_only" "4520() Shell: '${user_root_shell}' used for: 'root'." else chroot_exec "${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 install -m 0600 -o root -g root "${VAR_SETUP_PATH}/includes/target/root/.ciss/alias" "${TARGET}/root/.ciss/" install -m 0700 -o root -g root "${VAR_SETUP_PATH}/includes/target/root/.ciss/clean_logout.sh" "${TARGET}/root/.ciss/" install -m 0600 -o root -g root "${VAR_SETUP_PATH}/includes/target/root/.ciss/shortcuts" "${TARGET}/root/.ciss/" ### To be able to copy/paste from vim, one needs to create a '.vimrc' with the following content: echo 'set clipboard=unnamed' >| "${TARGET}/root/.vimrc" chmod 0600 "${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')|" "${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')|" "${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 "${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' >> "${TARGET}/etc/security/access.conf" ### 3) A) 4) Empty "/etc/securetty". cat << 'EOF' >| "${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" >> "${TARGET}/etc/security/access.conf" ### 3) B) 2) Allow local access for 'root' only on 'tty1' in '/etc/securetty'. cat << 'EOF' >| "${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 "${TARGET}" "passwd -l root" do_log "info" "file_only" "4520() User: 'root' password access: [false]" ;; true) chroot_script "${TARGET}" "printf '%s:%s\n' root '${var_password}' | /usr/sbin/chpasswd -e" #chroot_script "${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}" >| "${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" fi if [[ "${user_root_authentication_2fa_ssh}" == "true" ]]; then pam_access_totp_enable "root" "sshd" var_ssh_totp_update="true" cat << EOF >> "${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" ### 7) Install eza themes. eza_installer "root" ### 8) Double check permissions. ### Directories: 0700 find "${TARGET}/root" -type d -exec chmod 0700 {} + ### Executable files: 0700 (any x-bit set) find "${TARGET}/root" -type f -perm /111 -exec chmod 0700 {} + ### Non-executable files: 0600 find "${TARGET}/root" -type f ! -perm /111 -exec chmod 0600 {} + ### Ownership: UID:GID (do not dereference symlinks; stay on this filesystem) find "${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 "${TARGET}" getent group "${var_username}" >/dev/null; then chroot_exec "${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 "${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}" ;; true:false) chroot_exec "${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 "${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 "${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}" "${TARGET}/home/${var_username}/.ssh" install -m 0600 -o "${var_uid}" -g "${var_gid}" /dev/null "${TARGET}/home/${var_username}/.ssh/authorized_keys" install -m 0600 -o "${var_uid}" -g "${var_gid}" "${VAR_SETUP_PATH}/includes/target/etc/skel/.bashrc" "${TARGET}/home/${var_username}/" if [[ "${var_shell}" == "/bin/zsh" ]]; then if [[ -x "${TARGET}${var_shell}" ]]; then case "${var_specific,,}" in "ciss") zsh_omz_installer "${var_username}" mv "${TARGET}/home/${var_username}/.zshrc" "${TARGET}/home/${var_username}/.zshrc.bak" install -m 0600 -o "${var_uid}" -g "${var_gid}" "${VAR_SETUP_PATH}/includes/target/etc/skel/.zshrc" "${TARGET}/home/${var_username}" ;; "physnet") : ;; "none"|*) : ;; esac chroot_exec "${TARGET}" chsh -s "${var_shell}" "${var_username}" do_log "info" "file_only" "4520() Shell: '${var_shell}' used for: '${var_username}'." else chroot_exec "${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" >> "${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" >> "${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 "${TARGET}" "passwd -l ${var_username}" do_log "info" "file_only" "4520() User: '${var_username}' password access: [false]" ;; true) chroot_script "${TARGET}" "printf '%s:%s\n' \"${var_username}\" '${var_password}' | /usr/sbin/chpasswd -e" #chroot_script "${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}" >| "${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}" fi if [[ "${var_2fa_ssh}" == "true" ]]; then pam_access_totp_enable "${var_username}" "sshd" var_ssh_totp_update="true" cat << EOF >> "${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" ### 7) Check sudo membership for user. if [[ "${var_sudo}" == "true" ]]; then chroot_exec "${TARGET}" usermod -aG sudo "${var_username}" ### Hardening sudo users (idempotent) and ensure WinSCP SFTP-as-root. hardening_sudo "${var_username}" "${var_specific:-none}" ### Enable per-user TOTP in a given PAM service (login, sshd, su, sudo). pam_access_totp_enable "${var_username}" "sudo" fi ### 8) Double check permissions. ### Directories: 0700 find "${TARGET}/home/${var_username}" -type d -exec chmod 0700 {} + ### Executable files: 0700 (any x-bit set) find "${TARGET}/home/${var_username}" -type f -perm /111 -exec chmod 0700 {} + ### Non-executable files: 0600 find "${TARGET}/home/${var_username}" -type f ! -perm /111 -exec chmod 0600 {} + ### Ownership: UID:GID (do not dereference symlinks; stay on this filesystem) find "${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')|" "${TARGET}/etc/ssh/sshd_config" fi unset VAR_TEMP_PLAIN_MFA_SEED if ! grep -Fqx -- '-: ALL:ALL' "${TARGET}/etc/security/access.conf"; then printf '%s\n' '-: ALL:ALL' >> "${TARGET}/etc/security/access.conf" fi printf "# vim: number et ts=2 sw=2 sts=2 ai tw=128 ft=conf \n" >> "${TARGET}/etc/security/access.conf" printf "# vim: number et ts=2 sw=2 sts=2 ai tw=128 ft=conf \n" >> "${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: # TARGET # Arguments: # 1: Username # Returns: # 0: on success ####################################### eza_installer() { ### Declare Arrays, HashMaps, and Variables. declare var_user="${1}" case "${1}" in root) declare var_base="/root" ;; *) declare var_base="/home/${var_user}" ;; esac chroot_script "${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:'; 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 "${TARGET}${pam_su}" ]] || return 0 chroot_stdin "${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: # TARGET # Arguments: # 1: # Returns: # 0: on success # ERR_VERIFY_LOGROTATE: on failure # ERR_VERIFY_VISUDO: on failure ####################################### hardening_sudo() { ### Declare Arrays, HashMaps, and Variables. declare var_user="$1" declare var_specific="$2" declare -r var_logfile="/root/.ciss/cdi/log/4520_accounts_setup.log" declare -r var_sudo_iolog_dir="${TARGET}/var/log/sudo-io" declare -r var_sudoers_main="${TARGET}/etc/sudoers" declare -r var_sudoers_dir="${TARGET}/etc/sudoers.d" declare -r var_lr_conf="${TARGET}/etc/logrotate.d/sudo" declare -r var_sudoers_winscp_global="${TARGET}/etc/sudoers.d/90-ciss-winscp-sftp" declare -r var_sudoers_winscp_user="${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 "${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 "${TARGET}" "EDITOR=/usr/bin/nano /usr/sbin/visudo -q -c >> ${var_logfile}"; then do_log "warn" "file_only" "4520() Command: [chroot_script ${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 ${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 "${TARGET}" "logrotate -d /etc/logrotate.conf >> ${var_logfile}"; then do_log "warn" "file_only" "4520() Command: [chroot_script ${TARGET} logrotate -d /etc/logrotate.conf] failed." return "${ERR_VERIFY_LOGROTATE}" else do_log "info" "file_only" "4520() Command: [chroot_script ${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 "${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 "${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 "${TARGET}${var_file_sshd}" ]] || return 0 chroot_stdin "${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: # TARGET # Arguments: # 1: # 2: # Returns: # 0: on success ####################################### pam_access_totp_enable() { ### Declare Arrays, HashMaps, and Variables. declare var_user="$1" declare var_module="$2" declare var_pam_file="/etc/pam.d/${var_module}" declare var_users_file="${TARGET}/etc/ciss/2fa.users" declare 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 "${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 "${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 "${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 # TARGET # Arguments: # 1: Username # 2: UID # 3: GID # Returns: # 0: on success ####################################### write_google_authenticator_file() { ### Declare Arrays, HashMaps, and Variables. declare var_user="${1}" var_user_id="${2}" var_group_id="${3}" var_secret="" case "${1}" in root) declare var_base="${TARGET}/root" ;; *) declare var_base="${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: # TARGET # Arguments: # 1: Username # Returns: # 0: on success ####################################### zsh_omz_installer() { ### Declare Arrays, HashMaps, and Variables. declare var_user="${1}" ### 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 "${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 # vim: number et ts=2 sw=2 sts=2 ai tw=128 ft=sh