#!/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 ### Purpose: Unified CISS 2FA gate for PAM (auth phase), deciding per 'user' and 'service'. # Exit 0 => PAM_SUCCESS (skip next N lines via the control flag). # Exit 1 => non-success (fall through to GA lines). ### Policy : "strict" by default: if service=1 and secret missing => GA will fail (no prompt). # Set CISS_POLICY=permissive to skip GA when the secret is missing. set -Ceuo pipefail ### Declare Arrays, HashMaps, and Variables. declare -g VAR_MAP_FILE="/etc/ciss/2fa.map" declare -g VAR_POLICY="${CISS_POLICY:-strict}" ### PAM variables provided by pam_exec: declare -g VAR_U="${PAM_USER:-}" declare -g VAR_S="${PAM_SERVICE:-}" ####################################### # Read flag for user and service (0/1), default: empty (not found). # Globals: # VAR_MAP_FILE # VAR_U # Arguments: # 1: PAM module / service {login, sshd, su, sudo} # Returns: # 0: on success ####################################### read_flag() { declare -r var_col="${1}" declare line="" while IFS= read -r line; do ### Strip leading / trailing spaces. line="${line#"${line%%[![:space:]]*}"}" line="${line%"${line##*[![:space:]]}"}" [[ -z "${line}" || "${line}" == \#* ]] && continue declare user="" f_login="" f_sshd="" f_su="" f_sudo="" _rest="" IFS=: read -r user f_login f_sshd f_su f_sudo _rest <<<"${line}" [[ "${user}" != "${VAR_U}" ]] && continue case "${var_col}" in 2) echo "${f_login}"; return 0 ;; 3) echo "${f_sshd}"; return 0 ;; 4) echo "${f_su}"; return 0 ;; 5) echo "${f_sudo}"; return 0 ;; *) echo ""; return 0 ;; esac done < "${VAR_MAP_FILE}" echo "" return 0 } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f read_flag ####################################### # Map PAM service to column index in the map (1-based after USER). # Globals: # None # Arguments: # 1: PAM module / service {login, sshd, su, sudo} # Returns: # 0: on success ####################################### map_service_to_col() { declare -r var_s="${1}" case "${var_s}" in login) echo 2 ;; sshd) echo 3 ;; su) echo 4 ;; sudo) echo 5 ;; *) echo 0 ;; # Unknown services => behave as "not enforced". esac } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f map_service_to_col ####################################### # Main function wrapper. # Globals: # VAR_MAP_FILE # VAR_POLICY # VAR_S # VAR_U # Arguments: # None # Returns: # 0: on success # 1: on failure ####################################### main() { ### On missing map, behave like "not listed" (skip GA), analogous to onerr=ignore. if [[ ! -r "${VAR_MAP_FILE}" || -z "${VAR_U}" || -z "${VAR_S}" ]]; then exit 0 fi declare col; col="$(map_service_to_col "${VAR_S}")" ### Treat unknown service as "not enforced". [[ "${col}" -eq 0 ]] && exit 0 declare flag=""; flag="$(read_flag "${col}")" ### Not listed => skip GA. [[ -z "${flag}" ]] && exit 0 ### Listed with explicit 0 => skip GA. [[ "${flag}" == "0" ]] && exit 0 ### If the flag == "1" => enforces GA for this service. ### Optional permissive mode: If the secret is missing, then skip GA instead of enforcing. if [[ "${VAR_POLICY}" == "permissive" ]]; then declare home="" ### Resolve home via getent passwd (avoids $HOME spoofing). home="$(getent passwd -- "${VAR_U}" | cut -d: -f6)" if [[ -n "${home}" && ! -f "${home}/.google_authenticator" ]]; then exit 0 fi fi # Enforce GA: return non-zero so PAM control falls through to GA lines. exit 1 } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f main main "$@" # vim: number et ts=2 sw=2 sts=2 ai tw=128 ft=sh