#!/bin/sh /etc/rc.common START=11 STOP=90 USE_PROCD=1 PROG=/usr/sbin/usermngr # List of required .so files REQUIRED_MODULES=" /usr/lib/security/pam_faildelay.so /usr/lib/security/pam_faillock.so /usr/lib/security/pam_unix.so /usr/lib/security/pam_deny.so /usr/lib/security/pam_permit.so /usr/lib/security/pam_passwdqc.so " log() { echo "$*" | logger -t user.init -p info } log_err() { echo "$*" | logger -t user.init -p err } check_required_modules() { for mod in $REQUIRED_MODULES; do if [ ! -f "$mod" ]; then log_err "ERROR: Cannot setup security policy, missing PAM module: $mod" return 1 fi done return 0 } write_line() { local filepath="$1" local line="$2" echo "$line" >> "$filepath" } compare_and_replace() { local src dst src="$1" dst="$2" if [ ! -f "$dst" ] || ! cmp -s "$src" "$dst"; then cp "$src" "$dst" log "Updated $dst" fi } update_auth() { # Write /etc/pam.d/common-auth local tmp_file pam_file tmp_file="/tmp/common-auth" pam_file="/etc/pam.d/common-auth" local auth_enabled="${1}" local enabled="${2}" local faildelay="$(uci -q get users.authentication_policy.fail_delay)" local faillock_lockout_time="$(uci -q get users.authentication_policy.faillock_lockout_time)" local faillock_attempts="$(uci -q get users.authentication_policy.faillock_attempts)" [ -n "$faildelay" ] || faildelay=3 [ -n "$faillock_attempts" ] || faillock_attempts=6 [ -n "$faillock_lockout_time" ] || faillock_lockout_time=300 # Convert seconds to microseconds for pam_faildelay local faildelay_usec=$((faildelay * 1000000)) rm -f "$tmp_file" touch "$tmp_file" if [ "${auth_enabled}" -eq 1 ] && [ "${enabled}" -eq 1 ]; then write_line "$tmp_file" "auth optional pam_faildelay.so delay=$faildelay_usec" write_line "$tmp_file" "auth required pam_faillock.so preauth deny=$faillock_attempts even_deny_root unlock_time=$faillock_lockout_time" fi write_line "$tmp_file" "auth sufficient pam_unix.so nullok_secure" if [ "${auth_enabled}" -eq 1 ] && [ "${enabled}" -eq 1 ]; then write_line "$tmp_file" "auth [default=die] pam_faillock.so authfail audit deny=$faillock_attempts even_deny_root unlock_time=$faillock_lockout_time" write_line "$tmp_file" "" fi write_line "$tmp_file" "auth requisite pam_deny.so" write_line "$tmp_file" "auth required pam_permit.so" compare_and_replace "$tmp_file" "$pam_file" } build_pam_passwdqc_line() { local base="password requisite pam_passwdqc.so" local k v line for line in $(uci show users.passwdqc 2>/dev/null); do case "$line" in users.passwdqc=*) continue ;; users.passwdqc.enabled=*) continue ;; esac k="${line%%=*}" k="${k#users.passwdqc.}" v="${line#*=}" v="${v%\'}" v="${v#\'}" base="$base $k=$v" done base="$base match=0" echo "$base" } # NOTE: # for some reason setting min 8 makes passwdqc accept minimum 12 letter password with this configuration # if we set it to 12 then we need atleast 16 characters and so on # passphrase = 0 means no space separated words # passphrase = N means the number of words required for a passphrase or 0 to disable the support for user-chosen passphrases. # rest can be figured out from passwdqc man page update_password() { local tmp_file pam_file enabled line tmp_file="/tmp/common-password" pam_file="/etc/pam.d/common-password" local auth_enabled="${1}" rm -f "$tmp_file" touch "$tmp_file" # Check if section exists if uci -q get users.passwdqc >/dev/null 2>&1; then # if enabled is not present it is assumed to be 0 enabled=$(uci -q get users.passwdqc.enabled || echo "0") if [ "${auth_enabled}" -eq 1 ] && [ "${enabled}" -eq 1 ]; then line="$(build_pam_passwdqc_line)" write_line "$tmp_file" "$line" fi fi write_line "$tmp_file" "password [success=1 default=ignore] pam_unix.so sha512" write_line "$tmp_file" "" write_line "$tmp_file" "password requisite pam_deny.so" write_line "$tmp_file" "password required pam_permit.so" compare_and_replace "$tmp_file" "$pam_file" } update_account() { # Write /etc/pam.d/common-account local tmp_file pam_file tmp_file="/tmp/common-account" pam_file="/etc/pam.d/common-account" local auth_enabled="${1}" local enabled="${2}" rm -f "$tmp_file" touch "$tmp_file" if [ "${auth_enabled}" -eq 1 ] && [ "${enabled}" -eq 1 ]; then write_line "$tmp_file" "account required pam_faillock.so" fi write_line "$tmp_file" "account [success=1 new_authtok_reqd=done default=ignore] pam_unix.so" write_line "$tmp_file" "" write_line "$tmp_file" "account requisite pam_deny.so" write_line "$tmp_file" "account required pam_permit.so" compare_and_replace "$tmp_file" "$pam_file" } update_password_expiry() { local user password_expiry shadow_entry lastchg_days lastchg_epoch expiry_epoch expiry_days max_days current_max_days user="$1" # Fetch expiry (in yyyy-mm-dd format) from UCI config_get password_expiry "$user" password_expiry # Get /etc/shadow entry for the user shadow_entry="$(grep "^${user}:" /etc/shadow 2>/dev/null)" if [ -z "$shadow_entry" ]; then log_err "No shadow entry found for $user, skipping" return fi current_max_days="$(echo "$shadow_entry" | cut -d: -f5)" # Handle infinite lifetime (no expiry set) if [ -z "$password_expiry" ]; then if [ "$current_max_days" = "-1" ] || [ -z "$current_max_days" ]; then log "Password for $user already set to never expire, no action needed" else chage -M -1 "$user" log "Set password expiry for $user to never expire (previous max_days=$current_max_days)" fi return fi # option value is of the form 9999-12-31T23:59:59Z, so extract date # because shadow/chage only allow us to specify expiry date password_expiry="$(echo "$password_expiry" | cut -d 'T' -f 1)" # Validate date format (yyyy-mm-dd) if ! echo "$password_expiry" | grep -Eq '^[0-9]{4}-[0-9]{2}-[0-9]{2}$'; then log_err "Invalid password_expiry format for $user: '$password_expiry' (expected yyyy-mm-dd)" return fi # Convert expiry date to epoch seconds expiry_epoch="$(date -d "$password_expiry" +%s 2>/dev/null)" if [ -z "$expiry_epoch" ]; then log_err "Failed to parse expiry date '$password_expiry' for $user" return fi # Extract 'lastchg' field (3rd colon-separated field) lastchg_days="$(echo "$shadow_entry" | cut -d: -f3)" if [ -z "$lastchg_days" ] || [ "$lastchg_days" = "0" ]; then log_err "No valid last password change date for $user, skipping" return fi # Convert lastchg_days to epoch seconds lastchg_epoch=$(( lastchg_days * 86400 )) # Compute difference in days between expiry and last change expiry_days=$(( (expiry_epoch - lastchg_epoch) / 86400 )) # round up to full day, because of this expiry_days can never be 0, but I think that is a non-issue expiry_days=$(( expiry_days + 1 )) if [ "$expiry_days" -lt 0 ]; then log_err "Expiry date for $user ($password_expiry) is before last password change, skipping" return fi max_days="$expiry_days" # Apply update if changed if [ "$current_max_days" != "$max_days" ]; then chage -M "$max_days" "$user" log "Updated max_days for $user to $max_days (expiry=$password_expiry, lastchg_days=$lastchg_days)" else log "max_days for $user already set to $max_days (expiry=$password_expiry), no change" fi } handle_security_policy() { local auth_enabled enabled config_load users config_foreach update_password_expiry user # Read UCI values auth_enabled="$(uci -q get users.users.auth_policy_enable || echo 0)" enabled="$(uci -q get users.authentication_policy.enabled || echo 0)" # if any .so files are missing, then we cannot setup security if ! check_required_modules; then return fi update_auth "${auth_enabled}" "${enabled}" update_account "${auth_enabled}" "${enabled}" update_password "${auth_enabled}" } start_service() { local loglevel loglevel="$(uci -q get users.users.loglevel)" handle_security_policy procd_open_instance usermngr procd_set_param command $PROG if [ -n "${loglevel}" ]; then procd_append_param command -l "${loglevel}" fi procd_set_param respawn procd_close_instance } reload_service() { ret=$(ubus call service list '{"name":"users"}' | jsonfilter -qe '@.users.instances.usermngr.running') if [ "$ret" != "true" ]; then stop start else handle_security_policy ubus send usermngr.reload fi return 0 } service_triggers() { procd_add_reload_trigger "users" }