All checks were successful
🛡️ Shell Script Linting / 🛡️ Shell Script Linting (push) Successful in 1m6s
Signed-off-by: Marc S. Weidner <msw@coresecret.dev>
824 lines
28 KiB
Bash
824 lines
28 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
|
|
|
|
#######################################
|
|
# 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
|
|
|
|
chroot_exec "${TARGET}" chsh -s "${user_root_shell}" root
|
|
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_script "${TARGET}" "
|
|
if ! grep -Eq '^[[:space:]]*auth[[:space:]]+requisite[[:space:]]+pam_securetty\.so' /etc/pam.d/login; then
|
|
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 >| /etc/pam.d/login.new && mv -f /etc/pam.d/login.new /etc/pam.d/login
|
|
fi
|
|
"
|
|
|
|
### 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"
|
|
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
|
|
|
|
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}"
|
|
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).
|
|
# shellcheck disable=SC2312
|
|
var_secret="$(
|
|
printf '%s' "${VAR_TEMP_PLAIN_MFA_SEED}" | xxd -r -p | openssl kdf -keylen 20 -kdfopt digest:SHA256 \
|
|
-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 "${pam_su}" ]] || return 0
|
|
|
|
### If the pam_wheel line already exists with the group=sudo and use_uid, then do nothing.
|
|
if grep -Eq '^[[:space:]]*auth[[:space:]]+required[[:space:]]+pam_wheel\.so([[:space:]].*)?\bgroup=sudo\b([[:space:]].*)?\buse_uid\b' "${pam_su}"; then
|
|
return 0
|
|
fi
|
|
|
|
### Insert 'auth required pam_wheel.so use_uid group=sudo' before pam_unix/rootok (fail early).
|
|
awk '
|
|
BEGIN{ins=0}
|
|
{
|
|
### Insert just before the first pam_unix or pam_rootok auth line.
|
|
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) {
|
|
### Fallback: append if no anchor found
|
|
print "auth required pam_wheel.so use_uid group=sudo"
|
|
}
|
|
}
|
|
' "${pam_su}" > "${pam_su}.new" && mv -f "${pam_su}.new" "${pam_su}"
|
|
|
|
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: files must exist, no-op otherwise.
|
|
if [[ ! -f "${TARGET}${var_file_login}" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
if [[ ! -f "${TARGET}${var_file_sshd}" ]]; then
|
|
: ### Still continue, only '/etc/pam.d/login' will be processed
|
|
fi
|
|
|
|
### 1) If the 'pam_access' line is commented in '/etc/pam.d/login', uncomment exactly one occurrence.
|
|
# shellcheck disable=SC2155
|
|
declare var_payload="$(
|
|
cat <<'CISS'
|
|
tmp="$(mktemp /etc/pam.d/login.XXXXXX)"
|
|
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
|
|
CISS
|
|
)"
|
|
|
|
chroot_script "${TARGET}" "${var_payload}"
|
|
|
|
#chroot_script "${TARGET}" "
|
|
# LC_ALL=C awk '
|
|
# BEGIN { done=0 }
|
|
# {
|
|
# if (!done) {
|
|
# tmp=\$0
|
|
# sub(/^[[:space:]]*#+[[:space:]]*/, \"\", tmp)
|
|
# if (tmp ~ /^[[:space:]]*account[[:space:]]+required[[:space:]]+pam_access[.]so([[:space:]]|$)/) {
|
|
# print tmp
|
|
# done=1
|
|
# next
|
|
# }
|
|
# }
|
|
# print
|
|
# }
|
|
# ' /etc/pam.d/login >| /etc/pam.d/login.new
|
|
# mv -f /etc/pam.d/login.new /etc/pam.d/login
|
|
# "
|
|
|
|
### 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.
|
|
chroot_script "${TARGET}" "
|
|
if grep -Eq '^[[:space:]]*account[[:space:]]+required[[:space:]]+pam_access\.so([[:space:]]|$)' /etc/pam.d/login; then
|
|
if [[ -f /etc/pam.d/sshd ]]; then
|
|
awk '
|
|
### Comment only active matches; keep pre-commented lines untouched.
|
|
/^[[:space:]]*account[[:space:]]+required[[:space:]]+pam_access\.so([[:space:]]|$)/ { print \"# \" \$0; next }
|
|
{ print }
|
|
' /etc/pam.d/sshd >| /etc/pam.d/sshd.new
|
|
mv -f /etc/pam.d/sshd.new /etc/pam.d/sshd
|
|
fi
|
|
fi
|
|
"
|
|
|
|
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"
|
|
|
|
### 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_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.
|
|
if ! grep -q '^# CISS TOTP START$' "${var_pam_file}"; then
|
|
awk -v START='# CISS TOTP START' -v END='# CISS TOTP END' '
|
|
BEGIN{ins=0}
|
|
{
|
|
print
|
|
if (!ins && ($0 ~ /^[[:space:]]*auth[[:space:]]+.*pam_unix\.so/ || $0 ~ /^[[:space:]]*@include[[:space:]]+common-auth/)) {
|
|
print START
|
|
print "auth [success=1 default=ignore] pam_listfile.so item=user sense=deny file=/etc/ciss/2fa.users onerr=ignore"
|
|
print "auth required pam_google_authenticator.so"
|
|
print END
|
|
ins=1
|
|
}
|
|
}
|
|
END{
|
|
if (!ins) {
|
|
print START
|
|
print "auth [success=1 default=ignore] pam_listfile.so item=user sense=allow file=/etc/ciss/2fa.users onerr=ignore"
|
|
print "auth required pam_google_authenticator.so"
|
|
print END
|
|
}
|
|
}
|
|
' "${var_pam_file}" > "${var_pam_file}.new" && mv -f "${var_pam_file}.new" "${var_pam_file}"
|
|
fi
|
|
|
|
### 2) Comment out any other active GA lines to avoid double prompts.
|
|
### We keep the CISS block intact (recognized by the START/END markers).
|
|
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
|
|
}
|
|
}
|
|
' "${var_pam_file}" > "${var_pam_file}.new" && mv -f "${var_pam_file}.new" "${var_pam_file}"
|
|
|
|
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
|
|
# Returns:
|
|
# 0: on success
|
|
#######################################
|
|
write_google_authenticator_file() {
|
|
### Declare Arrays, HashMaps, and Variables.
|
|
declare var_user="${1}" 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
|
|
{
|
|
printf '%s\n' "${var_secret}"
|
|
printf 'RATE_LIMIT 3 30 \n'
|
|
printf 'WINDOW 10 \n'
|
|
printf 'DISALLOW_REUSE \n'
|
|
printf 'TOTP_AUTH \n'
|
|
### Emergency Codes:
|
|
for i in {0..7}; do printf '%08d\n' "$(( RANDOM % 100000000 ))"; done
|
|
} >| "${var_base}/.google_authenticator"
|
|
chown "${var_user}:${var_user}" "${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
|
|
}
|
|
# vim: number et ts=2 sw=2 sts=2 ai tw=128 ft=sh
|