Files
CISS.debian.installer/func/cdi_4500_user/4520_accounts_setup.sh
Marc S. Weidner 502c61900b
All checks were successful
🛡️ Shell Script Linting / 🛡️ Shell Script Linting (push) Successful in 1m6s
V8.00.000.2025.06.17
Signed-off-by: Marc S. Weidner <msw@coresecret.dev>
2025-09-16 15:08:08 +02:00

956 lines
31 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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
#######################################
# 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_password
# user_root_shell
# user_root_sshpubkey
# Arguments:
# None
# Returns:
# 0: on success
#######################################
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=""
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=""
declare var_chpasswd="" 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/"
if [[ "${user_root_shell}" == "/bin/zsh" ]]; then
if [[ -x "${TARGET}${user_root_shell}" ]]; then
zsh_omz_installer "root"
chroot_exec "${TARGET}" chsh -s "${user_root_shell}" 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/"
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]"
;;
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]"
;;
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)
var_chpasswd="root:${user_root_password}"
chroot_script "${TARGET}" "echo \"${var_chpasswd}\" | chpasswd -e"
var_chpasswd=""
do_log "info" "file_only" "4520() User: 'root' password access: [true]"
;;
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) 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_restricted="user_user${i}_privileges_restricted"
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_restricted="${!tmp_restricted}"
### 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 [[ "${var_restricted}" == "false" ]]; then
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}"
else
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}"
fi
### 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
zsh_omz_installer "${var_username}"
mv "${TARGET}/home/${var_username}/.zshrc" "${TARGET}/home/${var_username}/.zshrc.bak"
install -m 0600 -o root -g root "${VAR_SETUP_PATH}/includes/target/etc/skel/.zshrc" "${TARGET}/home/${var_username}"
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:ALL \n" "${var_username}" >> "${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:tty1 \n" "${var_username}" >> "${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]"
;;
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)
var_chpasswd="${var_username}:${var_password}"
chroot_script "${TARGET}" "echo \"${var_chpasswd}\" | chpasswd -e"
var_chpasswd=""
do_log "info" "file_only" "4520() User: '${var_username}' password access: [true]"
;;
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}"
### Enable per-user TOTP in a given PAM service (login, sshd, su, sudo).
pam_access_totp_enable "${var_username}" "sudo"
fi
### 8) 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 -qxF "-: ALL:ALL" "${TARGET}/etc/security/access.conf"; then
printf -- '-: ALL:ALL\n' >> "${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
}
#######################################
# 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
}
#######################################
# 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
}
#######################################
# Hardening sudo users (idempotent) and ensure WinSCP SFTP-as-root.
# Globals:
# TARGET
# Arguments:
# 1: <username>
# 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 -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
### Install global WinSCP SFTP-as-root command alias (idempotent).
if [[ -x "${TARGET}${var_sftp_bin}" ]]; then
if [[ ! -f "${var_sudoers_winscp_global}" ]]; then
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
# Harden per-command defaults: noexec to block execve(), and forbid env changes.
Defaults!CISS_SFTPROOT noexec, !setenv
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
echo "${var_user} ALL=(root) NOPASSWD: CISS_SFTPROOT" >| "${var_sudoers_winscp_user}"
fi
chmod 0440 "${var_sudoers_winscp_user}"
### 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 90
compress
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
}
#######################################
# 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
fi
rm -f "${tmp}" || :
:
EOF
return 0
}
#######################################
# Enable per-user TOTP in a given PAM service (login, sshd, su, sudo).
# Globals:
# TARGET
# Arguments:
# 1: <username>
# 2: <pam_module>
# 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
}
#######################################
# 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
}
#######################################
# Writes '.google_authenticator'-file for the respective user.
# Globals:
# DIR_TMP
# RANDOM
# 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 (8× 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
}
#######################################
# 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}"
### Prepare a small script that runs as the target user (correct HOME/ownership)
usr_script="$(mktemp /tmp/omz_user_install.XXXXXX)"
cat >|"${usr_script}" <<'USR'
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).
:
USR
### Execute as user (login shell to get proper env), then clean up the temp script.
if [[ "${uid}" -eq 0 ]]; then
### root user: no su needed
bash "${usr_script}"
else
su - "${user}" -s /bin/bash -c "bash '${usr_script}'"
fi
rm -f -- "${usr_script}" || :
:
EOF
return 0
}
# vim: number et ts=2 sw=2 sts=2 ai tw=128 ft=sh