#!/usr/bin/env bash # * input file # user_list.txt # ;;[,sub group,sub group];|;[override password];[override hostname];[override ssh key type] # lines with # are skipped # already created users are skipped # Mandatory: ;;; # can be # allow (full login access) # forward (forward/jump host only) # if in this column with pipe (|) the flag "no_login" is set then the default shell will change to "/sbin/nologin" # * output file # ;;;;; # If already existing PEM key is used then is [ALREADY SET] # # * PEM KEY # ###.pem # * PUBLIC KEY # ###.pem.pub # stored as zip in # zip/ # # If a previously exsting PEM key should be used, put the public pem file # into the ssh-keygen/ folder # They pem pub key must follow the set rules above # SET TO 1 to TEST [will not create user/group/folder] TEST=0; # no actions will be run INFO=0; # no creation of anything, just print info strings GO=0; # without this flag the script will exit with an info box while getopts ":gtih:" opt; do case "${opt}" in g) # go GO=1; ;; t) # test TEST=1; ;; i) # info INFO=1; ;; h) # home HOME_LOCATION="${OPTARG}"; ;; \?) echo -e "\n Option does not exist: ${OPTARG}\n"; echo "Use -t for test and -i for info"; echo "Use -g for actually creation run"; echo "Override default /home/ folder location with -h "; exit 1; ;; esac; done; error=0; # hostname for output file only host=$(hostname); timestamp=$(date +%Y%m%d-%H%M%S) # character to set getween info blocks separator="#"; # base folder for all data BASE_FOLDER=$(dirname "$(readlink -f "$0")")"/"; # home folder is always thome HOME_BASE="/home/"; # config location CONFIG_BASE="${BASE_FOLDER}../config/"; # check config folder for .env file with HOME_LOCATION # only use if HOME_LOCATION not yet set if [ -z "${HOME_LOCATION}" ] && [ -f "${CONFIG_BASE}create_user.cfg" ]; then # shellcheck source=../config/create_user.cfg" # shellcheck disable=SC1091 source <(grep "=" "${CONFIG_BASE}create_user.cfg" | sed 's/ *= */=/g') fi; if [ -n "${HOME_LOCATION}" ]; then # must start with / as it has to be from root if [ "${HOME_LOCATION##/*}" ]; then echo "Home location folder must start with a slash (/): ${HOME_LOCATION}"; error=1; fi; # must be valid folder if [ ! -d "${HOME_LOCATION}" ]; then echo "Folder for home location does not exists: ${HOME_LOCATION}"; error=1; fi; fi; # the new location for home, if override is set will be created in this folder HOME_FOLDER="${HOME_LOCATION}${HOME_BASE}" if [ ! -d "${HOME_FOLDER}" ]; then echo "Home folder location not found: ${HOME_FOLDER}"; error=1; fi; # allow 10 to 39 length for password if [ -n "${PASSWORD_LENGTH}" ] && ! [[ "${PASSWORD_LENGTH}" =~ ^[13][0-9]$ ]]; then echo "Password length set error, can only be a value between 10 and 39"; error=1; elif [ -z "${PASSWORD_LENGTH}" ]; then PASSWORD_LENGTH=14; fi; # home dir error abort if [ $error -eq 1 ]; then exit; fi; ROOT_FOLDER="${BASE_FOLDER}../"; input_file='user_list.txt'; output_file="user_password.${timestamp}.txt"; output_zip_folder='zip/'; output_zip="users.${timestamp}.zip" SSH_KEYGEN_FOLDER='ssh-keygen/'; SSH_KEYGEN_FOLDER_CREATED_PUB='ssh-keygen-created-pub/'; # set default key tpye default_ssh_keytype='ed25519'; ssh_keytype=''; # sshallow or sshforward ssh_group=''; ssh_forward_ok=0; # login shells login_shell="/bin/bash"; no_login_shell="/sbin/nologin"; user_login_shell=""; # detect ssh authorized_keys setting SSH_CENTRAL_AUTHORIZED_FILE_FOLDER=''; SSH_AUTHORIZED_FILE=''; # shellcheck disable=SC2013 for cf in $(grep "^AuthorizedKeysFile" /etc/ssh/sshd_config | grep "%u"); do if echo "$cf" | grep -q "%u"; then SSH_CENTRAL_AUTHORIZED_FILE_FOLDER="${cf/%%u//}"; if [ ! -d "${SSH_CENTRAL_AUTHORIZED_FILE_FOLDER}" ]; then echo "ssh central authorized_file folder could not be found: ${SSH_CENTRAL_AUTHORIZED_FILE_FOLDER}"; exit; fi; fi; done; # check if ssh key folder exists if [ ! -d "${ROOT_FOLDER}${SSH_KEYGEN_FOLDER}" ]; then mkdir "${ROOT_FOLDER}${SSH_KEYGEN_FOLDER}"; fi; # check if zip folder is missing if [ ! -d "${ROOT_FOLDER}${output_zip_folder}" ]; then mkdir "${ROOT_FOLDER}${output_zip_folder}"; fi; # check if password generate software is installed if [ -z "$(command -v pwgen)" ]; then echo "Missing pwgen application, aborting"; error=1; fi; # check for zip if [ -z "$(command -v zip)" ]; then echo "Missing zip application, aborting"; error=1; fi; if ! grep -q "sshallow:" "/etc/group"; then echo "Missing ssh access group: sshallow"; error=1; fi; # flag if we can set ssh forward if ! grep -q "sshforward:" "/etc/group"; then ssh_forward_ok=1; fi; # check if user list file exists if [ ! -f "${ROOT_FOLDER}${input_file}" ]; then echo "Missing ${ROOT_FOLDER}${input_file}"; error=1; fi; # make sure my own folder is owned by root and 600 (except for testing) if [ "$(stat -c %a .)" != "600" ]; then echo "!!!! RECOMMENDED TO HAVE BASE FOLDER SET TO '600' AND USER 'root' !!!!" fi; if [ "$(whoami)" != "root" ]; then if [ ${TEST} -eq 0 ] && [ ${INFO} -eq 0 ]; then echo "Script must be run as root user"; error=1; else echo "!!!! Script must be run as root user !!!!"; fi; fi; # do not allow test and info at the same if [ ${TEST} -eq 1 ] && [ ${INFO} -eq 1 ]; then echo "Cannot have --test and --info option at the same time"; error=1; fi; # exit if not -g parameter set if [ $GO -eq 0 ]; then echo "Script has to be run with -g option for actual user creation."; echo "It is recommended to run -t for testing prior to user creation."; error=1; fi; if [ $error -eq 1 ]; then exit; fi; LOG="${BASE_FOLDER}/../log/user_management.log"; function write_log() { text="${1}"; do_echo="${2}"; log_prefix=""; # log prefix for testing if [ ${TEST} -eq 1 ]; then log_prefix="[TEST] "; fi; # write log not in info run if [ ${INFO} -eq 0 ]; then echo "[$(date +"%F %T")] [$0] ${log_prefix}${text}" >> "${LOG}"; fi; if [ "${do_echo}" = "1" ]; then echo "${text}"; fi; } write_log "START SCRIPT RUN"; # used for test run only overall_run_error=0; # MARK: LOOP START # create users while read -r i; do # run error for one row run_error=0; # skip rows start with # (comment) if [[ "${i}" =~ ^\# ]]; then continue; fi; # log create inly on not info if [ ${INFO} -eq 0 ]; then write_log "[CREATE] ROW: $i"; fi; # MARK: VALUES CHECK # POS 2: make lower case, remove spaces username=$(echo "${i}" | cut -d ";" -f 2 | tr "[:upper:]" "[:lower:]" | tr -d ' '); # check username is alphanumeric with . if ! [[ "${username}" =~ ^[a-z0-9]+([.a-z0-9_-]+[a-z0-9])?$ ]]; then run_error=1; write_log "[ERROR] User name can only be a-z 0-9 - _ . and cannot start or end with - . or _: ${username}" "1"; fi; # POS 3: groups _group=$(echo "${i}" | cut -d ";" -f 3 | tr "[:upper:]" "[:lower:]" | tr -d ' '); group=$(echo "${_group}" | cut -d "," -f 1); sub_group=""; # POS 4: ssh access type and no login flag # no login flag no_login_flag=""; # if there is a pipe, check, else ignore if echo "${i}" | cut -d ";" -f 4 | grep -q "|"; then no_login_flag=$(echo "${i}" | cut -d ";" -f 4 | cut -d "|" -f 2); fi; # anything set in no login shell flag, we set no login shell if [ -n "${no_login_flag}" ]; then user_login_shell="${no_login_shell}"; else user_login_shell="${login_shell}"; fi; # ssh access type ssh_access_type=$(echo "${i}" | cut -d ";" -f 4 | cut -d "|" -f 1 | tr "[:upper:]" "[:lower:]" | tr -d ' '); # if not allow or forward, set to access if [ "${ssh_access_type}" != "allow" ] && [ "${ssh_access_type}" != "forward" ]; then run_error=1; write_log "[ERROR] Not valid ssh access type ${ssh_access_type}" "1"; fi; if [ $ssh_forward_ok -eq 0 ] && [ "${ssh_access_type}" = "forward" ]; then write_log "[ERROR] sshforward group does not exsts, cannot set user ${username}" "1"; run_error=1; fi; ssh_group="ssh${ssh_access_type}"; # sshallow group is always added sub_group_opt=("${ssh_group}"); # check if "," inside and extract sub groups if [ -z "${_group##*,*}" ]; then sub_group=$(echo "${_group}" | cut -d "," -f 2-); sub_group_opt+=("${sub_group}"); fi; # POS 5: do we have a password preset _password=$(echo "${i}" | cut -d ";" -f 5); # POS 6: override host name, lowercase and spaces removed _hostname=$(echo "${i}" | cut -d ";" -f 6 | tr "[:upper:]" "[:lower:]" | tr -d ' '); if [ -z "${_hostname}" ]; then hostname=${host}; else hostname=${_hostname}; fi; # POS 7: ssh keytype override _ssh_keytype=$(echo "${i}" | cut -d ";" -f 7 | tr "[:upper:]" "[:lower:]" | tr -d ' '); if [ "${_ssh_keytype}" = "rsa" ]; then ssh_keytype="${_ssh_keytype}"; #echo "[!!] BACKWARDS COMPATIBLE RSA TYPE SELECTION [!!]"; else ssh_keytype=${default_ssh_keytype}; fi; # user & group not set if [ -z "${username}" ] || [ -z "${_group}" ]; then run_error=1; write_log "[ERROR] Missing user or group entry for ${username}/${_group}" "1"; else group_error=0; # check group names valid for create_group in ${_group//,/ }; do if ! [[ "${create_group}" =~ ^[a-z0-9]+([a-z0-9_-]+[a-z0-9])?$ ]]; then write_log "[ERROR] Group name can only be a-z 0-9 - _ and cannot start or end with - or _: ${create_group}" "1"; group_error=1; fi; done; if [ $group_error -eq 1 ]; then run_error=1; fi; fi; # error & test -> break if [ ${run_error} -eq 1 ]; then overall_run_error=1; write_log "[*** ABORT RUN ***]" "1"; # end if not test and not info if [ ${TEST} -eq 0 ] && [ ${INFO} -eq 0 ]; then break; else continue; fi; fi; # MARK: SSH NAMES SET # SSH file name part without folder ssh_keygen_id="${hostname}${separator}${group}${separator}${username}${separator}${ssh_keytype}.pem"; # the full file including folder name ssh_keyfile="${ROOT_FOLDER}${SSH_KEYGEN_FOLDER}${ssh_keygen_id}"; # publ file if new ssh_keyfile_pub="${ssh_keyfile}.pub"; # check existing pub file ssh_keyfile_check_pub="${ROOT_FOLDER}${SSH_KEYGEN_FOLDER_CREATED_PUB}${ssh_keygen_id}.pub"; # MARK: INFO if [ ${INFO} -eq 1 ]; then info_string="User: '${username}:${group}(${sub_group});${ssh_group}', SSH: ${ssh_keygen_id}"; # test if pub file exists or not, test if user exists if getent passwd "${username}" > /dev/null 2>&1; then info_string="${info_string}, User exists"; fi; if [ -f "${ssh_keyfile_check_pub}" ]; then info_string="${info_string}, SSH Pub key OK"; fi; echo "${info_string}"; continue; fi; # MARK: CREATE # add group for each entry in _group for create_group in ${_group//,/ }; do if [ ${TEST} -eq 0 ]; then groupadd -f "${create_group}"; else echo "$> groupadd -f ${create_group}"; fi; done; # check if user is not already created # if getent passwd ${username} > /dev/null 2>&1; then if id "${username}" &>/dev/null; then write_log "-- Skip '${username}:${group}(${sub_group})'" "1"; else write_log "++ Create '${username}:${group}(${sub_group})'" "1"; params=( "-c" "$(date +"%F")" "-s" "${user_login_shell}" "-g" "${group}" "-G" "$(IFS=, ; echo "${sub_group_opt[*]}")" "-d" "${HOME_FOLDER}${username}" "-m" "${username}" ); if [ ${TEST} -eq 0 ]; then # comment is user create time useradd "${params[@]}"; else echo "$> useradd ${params[*]}"; fi; fi; # set the auth file if [ -z "${SSH_CENTRAL_AUTHORIZED_FILE_FOLDER}" ]; then SSH_AUTHORIZED_FILE="${HOME_FOLDER}${username}/.ssh/authorized_keys"; else SSH_AUTHORIZED_FILE="${SSH_CENTRAL_AUTHORIZED_FILE_FOLDER}${username}"; fi; skip_ssh=0; # if public pem already exists skip creation if [ ! -f "${ssh_keyfile_check_pub}" ]; then # Note we only create a password if we need it # password + store pwgen 10 1 -1 if [ -z "${_password}" ]; then password=$(printf "%s" "$(pwgen "${PASSWORD_LENGTH}" 1)"); elif [ "${_password}" = "SET_NO_PASSWORD" ]; then # set empty echo "* No password set"; password=""; else echo "! Override password set"; password=${_password}; fi; # create SSH key write_log " > Create ssh key-pair: ${ssh_keyfile}" "1"; if [ ${TEST} -eq 0 ]; then ssh-keygen \ -t "${ssh_keytype}" \ -f "${ssh_keyfile}" \ -C "${hostname}: ${username}@${group}" \ -a 100 -N "${password}" else echo "$> ssh-keygen -t ${ssh_keytype} -f ${ssh_keyfile} -C ${hostname}: ${username}@${group} -a 100 -N ${password}"; fi; else found=''; if [ -f "${SSH_AUTHORIZED_FILE}" ]; then found=$(grep "$(cat "${ssh_keyfile_check_pub}")" "${SSH_AUTHORIZED_FILE}"); fi; if [ -n "${found}" ]; then skip_ssh=1; write_log "-- Skip SSH Key creation: ${ssh_keygen_id}.pub" "1"; else # override previously set with stored one ssh_keyfile_pub=${ssh_keyfile_check_pub}; write_log " < Use existing public ssh key: ${ssh_keygen_id}.pub" "1"; # Password already set notification fi; password="[ALREADY SET]"; fi; if [ ${skip_ssh} -eq 0 ]; then # MARK: SSH CREATE # write login info to output file if [ ${TEST} -eq 0 ]; then create_output_file="${ROOT_FOLDER}${output_file}"; else create_output_file="${ROOT_FOLDER}${output_file}.TEST"; fi; echo "$(date +"%F %T");${host};${_hostname};${username};${password};${ssh_access_type}" >> "${create_output_file}"; # create folder only if we do not have central # create the SSH foler and authorized access file with correct permissions if [ -z "${SSH_CENTRAL_AUTHORIZED_FILE_FOLDER}" ]; then echo " > Create .ssh folder"; if [ ${TEST} -eq 0 ]; then mkdir "${HOME_FOLDER}${username}/.ssh/"; else echo "$> mkdir \"${HOME_FOLDER}${username}/.ssh/\""; fi; fi; # add echo " > Add public into authorized_keys file"; if [ ${TEST} -eq 0 ]; then if [ -n "${SSH_CENTRAL_AUTHORIZED_FILE_FOLDER}" ] && [ -f "${SSH_AUTHORIZED_FILE}" ]; then chattr -i "${SSH_AUTHORIZED_FILE}"; fi; cat "${ssh_keyfile_pub}" > "${SSH_AUTHORIZED_FILE}"; else if [ -n "${SSH_CENTRAL_AUTHORIZED_FILE_FOLDER}" ] && [ -f "${SSH_AUTHORIZED_FILE}" ]; then echo "$> chattr -i ${SSH_AUTHORIZED_FILE}"; fi; echo "$> cat ${ssh_keyfile_pub} > ${SSH_AUTHORIZED_FILE}"; fi; # secure if [ -z "${SSH_CENTRAL_AUTHORIZED_FILE_FOLDER}" ]; then echo " > Secure home directory folder .ssh and authorized_keys file"; if [ ${TEST} -eq 0 ]; then chown -R "${username}":"${group}" "${HOME_FOLDER}${username}/.ssh/"; chmod 700 "${HOME_FOLDER}${username}/.ssh/"; chmod 600 "${SSH_AUTHORIZED_FILE}"; else echo "$> chown -R \"${username}\":\"${group}\" \"${HOME_FOLDER}${username}/.ssh/\""; echo "$> chmod 700 \"${HOME_FOLDER}${username}/.ssh/\""; echo "$> chmod 600 \"${SSH_AUTHORIZED_FILE}\""; fi; else echo " > Secure central authorized_keys file"; if [ ${TEST} -eq 0 ]; then chown "${username}":root "${SSH_AUTHORIZED_FILE}"; chmod 400 "${SSH_AUTHORIZED_FILE}"; # set +i so user can't change file chattr +i "${SSH_AUTHORIZED_FILE}"; else echo "$> chown \"${username}\":root \"${SSH_AUTHORIZED_FILE}\""; echo "$> chmod 400 \"${SSH_AUTHORIZED_FILE}\""; echo "$> chattr +i \"${SSH_AUTHORIZED_FILE}\""; fi; fi; fi; done <<< "$(cat "${ROOT_FOLDER}${input_file}")"; # End before anything because this is just info run if [ ${INFO} -eq 1 ]; then exit; fi; # MARK: ZIP FILE CREATE # check if there are any files in the SSH_KEYGEN_FOLDER, else skip zip file creation and file move has_pem_files=0; if (shopt -s nullglob dotglob; f=("${SSH_KEYGEN_FOLDER}"*".pem"*); ((${#f[@]}))); then has_pem_files=1; fi; # zip everything and remove data in ssh key folder, delete output file with passwords if [ ${TEST} -eq 0 ]; then if [ "${has_pem_files}" -eq 1 ]; then zip -r \ "${ROOT_FOLDER}${output_zip_folder}${output_zip}" \ "${input_file}" \ "${output_file}" \ "${SSH_KEYGEN_FOLDER}" \ -x\*.gitignore; echo "Download: ${ROOT_FOLDER}${output_zip_folder}${output_zip}"; else echo "Skip ZIP file creation, no pem files"; fi; else echo "zip -r \\" echo "${ROOT_FOLDER}${output_zip_folder}${output_zip} \\" echo "${input_file} \\" echo "${output_file} \\" echo "${SSH_KEYGEN_FOLDER} \\" echo "-x\*.gitignore;" echo "Download: ${ROOT_FOLDER}${output_zip_folder}${output_zip}"; fi; # cleam up user log file and ssh keys if [ ${TEST} -eq 0 ]; then if [ "${has_pem_files}" -eq 1 ]; then # move pub to created folders mv "${ROOT_FOLDER}${SSH_KEYGEN_FOLDER}"*.pub "${ROOT_FOLDER}${SSH_KEYGEN_FOLDER_CREATED_PUB}"; # delete the rest rm "${ROOT_FOLDER}${output_file}"; rm "${ROOT_FOLDER}${SSH_KEYGEN_FOLDER}"*; else echo "Skip pub file move and cleanup, no pem files"; fi; else echo "$> mv ${ROOT_FOLDER}${SSH_KEYGEN_FOLDER}*.pub ${ROOT_FOLDER}${SSH_KEYGEN_FOLDER_CREATED_PUB};"; echo "$> rm ${ROOT_FOLDER}${output_file}"; echo "$> rm ${ROOT_FOLDER}${SSH_KEYGEN_FOLDER}*"; fi; # MARK: TEST ERROR INFO if [ ${TEST} -eq 1 ] && [ ${overall_run_error} -eq 1 ]; then echo "[ERROR] Some errors occoured during the run, they will prohibit the live run of this script"; fi; # __END__