355 lines
16 KiB
Bash
355 lines
16 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
# Copyright (c) 2021-2026 community-scripts ORG
|
|
# Author: Joerg Heinemann (heinemannj)
|
|
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
|
|
|
function header_info() {
|
|
clear
|
|
cat <<"EOF"
|
|
____ _ ________ __ _ ________ _____ __ ___ __ _
|
|
/ __ \ | / / ____/ / / | |/ / ____/ / ___/__ _______/ /____ ____ ___ / | ____/ /___ ___ (_)___
|
|
/ /_/ / | / / __/ / / | / / \__ \/ / / / ___/ __/ _ \/ __ `__ \ / /| |/ __ / __ `__ \/ / __ \
|
|
/ ____/| |/ / /___ / /___/ / /___ ___/ / /_/ (__ ) /_/ __/ / / / / / / ___ / /_/ / / / / / / / / / /
|
|
/_/ |___/_____/ /_____/_/|_\____/ /____/\__, /____/\__/\___/_/ /_/ /_/ /_/ |_\__,_/_/ /_/ /_/_/_/ /_/
|
|
/____/
|
|
|
|
EOF
|
|
}
|
|
|
|
function whiptail_menu() {
|
|
MENU_ARRAY=()
|
|
MSG_MAX_LENGTH=0
|
|
while read -r TAG ITEM; do
|
|
OFFSET=2
|
|
((${#ITEM} + OFFSET > MSG_MAX_LENGTH)) && MSG_MAX_LENGTH=${#ITEM}+OFFSET
|
|
MENU_ARRAY+=("$TAG" "$ITEM " "OFF")
|
|
done < <(echo "$1")
|
|
}
|
|
|
|
function restart_container_service() {
|
|
local service=$1
|
|
local container=$2
|
|
local name=$3
|
|
if pct exec "${container}" -- bash -c "systemctl is-enabled ${service} >/dev/null 2>&1"; then
|
|
echo -e "\n${BL}[Info]${GN} Restarting ${service} inside${BL} ${name}${GN} with output: ${CL}\n"
|
|
pct exec "${container}" -- bash -c "systemctl restart ${service} && systemctl status ${service}" 2>&1
|
|
fi
|
|
}
|
|
|
|
function get_user_home() {
|
|
local user=$1
|
|
local container=$2
|
|
local command_1="grep ${user} /etc/passwd | awk -F ':' '{print \$6}'"
|
|
if [ "${container}" ]; then
|
|
pct exec "${container}" -- bash -c "${command_1}" 2>&1
|
|
else
|
|
eval "${command_1}"
|
|
fi
|
|
}
|
|
|
|
function set_permissions() {
|
|
local user=$1
|
|
local user_home=$2
|
|
local container=$3
|
|
local command_1="mkdir -p ${user_home}/.ssh && chown -R ${user} ${user_home} && chgrp -R ${user} ${user_home} && chmod 700 ${user_home}/.ssh && chmod -f 600 ${user_home}/.ssh/* ; true"
|
|
if [ "${container}" ]; then
|
|
pct exec "${container}" -- bash -c "${command_1}" 2>&1
|
|
else
|
|
eval "${command_1}"
|
|
fi
|
|
}
|
|
|
|
function delete_user() {
|
|
local user=$1
|
|
local container=$2
|
|
local user_home
|
|
user_home=$(get_user_home "${user}" "${container}")
|
|
local command_1="userdel ${user} && rm -R ${user_home}"
|
|
if [ "${container}" ]; then
|
|
local name
|
|
name=$(pct exec "${container}" hostname)
|
|
if [ "${user}" ] && [ "${user_home}" ]; then
|
|
echo -e "${BL}[Info]${GN} Delete User ${user} inside${BL} ${name}${GN} ${CL}$(pct exec "${container}" -- bash -c "${command_1}" 2>&1)"
|
|
else
|
|
echo -e "${BL}[Info]${GN} User ${user} not exists inside${BL} ${name}${GN}: ${CL} Skipping"
|
|
fi
|
|
else
|
|
eval "${command_1}"
|
|
fi
|
|
}
|
|
|
|
function copy_authorized_keys() {
|
|
local user=$1
|
|
local container=$2
|
|
local name=$3
|
|
local user_home
|
|
user_home=$(get_user_home "${user}" "${container}")
|
|
local command_1="ls -l ${user_home}/.ssh/authorized_keys"
|
|
local command_2="cat /etc/ssh/sshd_config | grep \"^PubkeyAuthentication yes\""
|
|
local command_3="sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/g' /etc/ssh/sshd_config"
|
|
local command_4="cat /etc/ssh/sshd_config | grep PubkeyAuthentication"
|
|
if pct push "${container}" "${user_home}"/.ssh/authorized_keys "${user_home}"/.ssh/authorized_keys >/dev/null 2>&1; then
|
|
set_permissions "${user}" "${user_home}" "${container}"
|
|
echo -e "${BL}[Info]${GN} Copy authorized_keys inside${BL} ${name}${GN}: ${CL}$(pct exec "${container}" -- bash -c "${command_1}" 2>&1)"
|
|
if ! pct exec "${container}" -- bash -c "${command_2}" >/dev/null 2>&1; then
|
|
pct exec "${container}" -- bash -c "${command_3}" 2>&1
|
|
echo -e "${BL}[Info]${GN} Copy ${user}'s authorized_keys inside${BL} ${name}${GN} ${CL}"
|
|
restart_container_service "ssh.service" "${container}" "${name}"
|
|
echo -e ""
|
|
fi
|
|
echo -e "${BL}[Info]${GN} sshd configuration inside${BL} ${name}${GN}: ${CL}$(pct exec "${container}" -- bash -c "${command_4}" 2>&1)"
|
|
else
|
|
echo -e "${BL}[ERROR]${GN} Copy authorized_keys inside${BL} ${name}${GN}: ${RD}Unexpected error for user ${user}${CL}"
|
|
fi
|
|
}
|
|
|
|
function maintain_container_user() {
|
|
local user=$1
|
|
local user_comment=$2
|
|
local container=$3
|
|
local name
|
|
name=$(pct exec "${container}" hostname)
|
|
local command_1="groups ${user}"
|
|
local command_2="useradd -G 100 -m -s /bin/bash -c \"${user_comment}\" ${user} && usermod -aG sudo ${user}"
|
|
local command_3="cat /etc/sudoers | grep '%sudo ALL=(ALL:ALL) NOPASSWD:ALL'"
|
|
local command_4="sed -i 's/%sudo ALL=(ALL:ALL) ALL/%sudo ALL=(ALL:ALL) NOPASSWD:ALL/g' /etc/sudoers"
|
|
if pct exec "${container}" -- bash -c "${command_1} >/dev/null 2>&1"; then
|
|
echo -e "${BL}[Info]${GN} User with group memberships already exists inside${BL} ${name}${GN}: ${CL}$(pct exec "${container}" -- bash -c "${command_1}" 2>&1)"
|
|
else
|
|
pct exec "${container}" -- bash -c "${command_2}" 2>&1
|
|
local user_home
|
|
user_home=$(get_user_home "${user}" "${container}")
|
|
set_permissions "${user}" "${user_home}" "${container}"
|
|
echo -e "${BL}[Info]${GN} Add user with group memberships inside${BL} ${name}${GN}: ${CL}$(pct exec "${container}" -- bash -c "${command_1}" 2>&1)"
|
|
fi
|
|
if ! pct exec "${container}" -- bash -c "${command_3} >/dev/null 2>&1"; then
|
|
pct exec "${container}" -- bash -c "${command_4}" 2>&1
|
|
fi
|
|
echo -e "${BL}[Info]${GN} sudoers configuration inside${BL} ${name}${GN}: ${CL}$(pct exec "${container}" -- bash -c "${command_3}" 2>&1)"
|
|
copy_authorized_keys "${user}" "${container}" "${name}"
|
|
}
|
|
|
|
function add_node_user() {
|
|
USER_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Proxmox VE LXC System Admin" --inputbox "User Name?" 10 58 3>&1 1>&2 2>&3)
|
|
USER_COMMENT=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Proxmox VE LXC System Admin" --inputbox "User Comment for User '${USER_NAME}'?" 10 58 3>&1 1>&2 2>&3)
|
|
useradd -G 100 -m -s /bin/bash -c "${USER_COMMENT}" "${USER_NAME}" && usermod -aG sudo "${USER_NAME}"
|
|
keygen_node_user "${USER_NAME}" "New Key"
|
|
}
|
|
|
|
function keygen_node_user() {
|
|
local user=$1
|
|
local user_domain
|
|
user_domain=$(hostname -d)
|
|
local user_home
|
|
user_home=$(get_user_home "${user}" "")
|
|
local private_key="${user_home}/.ssh/id_ed25519_${user}"
|
|
mkdir -p "${user_home}"/.ssh
|
|
rm -f "${private_key}"*
|
|
echo -e "${BL}[Info]${GN} Create/Renew Private Key for user '${user}':${CL}\n"
|
|
ssh-keygen -q -t ed25519 -C "${user}@${user_domain}" -f "${private_key}"
|
|
cp "${private_key}".pub "${user_home}"/.ssh/authorized_keys
|
|
set_permissions "${user}" "${user_home}" ""
|
|
sed -i 's/%sudo ALL=(ALL:ALL) ALL/%sudo ALL=(ALL:ALL) NOPASSWD:ALL/g' /etc/sudoers
|
|
sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/g' /etc/ssh/sshd_config
|
|
echo -e ""
|
|
}
|
|
|
|
function show_private_key() {
|
|
local user=$1
|
|
local user_home
|
|
user_home=$(get_user_home "${user}" "")
|
|
echo -e "Private Key of User '${user}':\n"
|
|
cat "${user_home}"/.ssh/id_ed25519_"${user}"
|
|
echo -e ""
|
|
exit
|
|
}
|
|
|
|
function authorization() {
|
|
local user=$1
|
|
local action=$2
|
|
local user_home
|
|
user_home=$(get_user_home "${user}" "")
|
|
local private_key=".ssh/id_ed25519_${user}"
|
|
local id_file="IdentityFile ~/${private_key}"
|
|
|
|
whiptail_menu "$(awk -F ':' '$3 == 0 || $3>=1000 && $7 == "/bin/bash" && NR>1 {print $1 " " $2 ":" $3 ":" $4 ":" $5 ":" $6 ":" $7}' /etc/passwd)"
|
|
local user_lxc
|
|
user_lxc=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Users on ${NODE}" --radiolist "\nSelect a User to ${action} SSH access with Private Key of User '${user}':\n" 16 $((MSG_MAX_LENGTH + 23)) 6 "${MENU_ARRAY[@]}" 3>&1 1>&2 2>&3 | tr -d '"')
|
|
[[ -z ${user_lxc} ]] && echo -e "${BL}[ERROR]${GN} ${RD}No User selected!${CL}\n" && exit
|
|
|
|
whiptail_menu "$(pct list | awk 'NR>1')"
|
|
local container
|
|
container=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Containers on ${NODE}" --radiolist "\nSelect Container whose local User '${user_lxc}' to ${action} SSH access using the Private Key of User '${user}':\n" 16 $((MSG_MAX_LENGTH + 23)) 6 "${MENU_ARRAY[@]}" 3>&1 1>&2 2>&3 | tr -d '"')
|
|
[[ -z ${container} ]] && echo -e "${BL}[ERROR]${GN} ${RD}No Container selected!${CL}\n" && exit
|
|
|
|
local name
|
|
name=$(pct exec "${container}" hostname)
|
|
local user_lxc_home
|
|
user_lxc_home=$(get_user_home "${user_lxc}" "${container}")
|
|
|
|
# both
|
|
local command_1="grep '${id_file}' ${user_lxc_home}/.ssh/config"
|
|
# add
|
|
local command_2="echo '${id_file}' >> ${user_lxc_home}/.ssh/config"
|
|
local command_3="ls -l ${user_lxc_home}/${private_key}"
|
|
# remove
|
|
local command_4="mv ${user_lxc_home}/.ssh/config ${user_lxc_home}/.ssh/config.bak"
|
|
local command_5="grep -v '${id_file}' ${user_lxc_home}/.ssh/config.bak > ${user_lxc_home}/.ssh/config ; true"
|
|
local command_6="rm -f ${user_lxc_home}/.ssh/id_ed25519_${user}"
|
|
# both
|
|
local command_7="ls -l ${user_lxc_home}/.ssh"
|
|
local command_8="cat ${user_lxc_home}/.ssh/config"
|
|
|
|
if [ "${action}" == "add" ]; then
|
|
if ! pct push "${container}" "${user_home}"/"${private_key}" "${user_lxc_home}"/"${private_key}" >/dev/null 2>&1; then
|
|
echo -e "\n${BL}[ERROR]${GN} Copy ${private_key} inside${BL} ${name}${GN}: ${RD}Unexpected error for user ${user_lxc}${CL}" && exit
|
|
fi
|
|
if ! pct exec "${container}" -- bash -c "${command_1}" >/dev/null 2>&1; then
|
|
pct exec "${container}" -- bash -c "${command_2}" 2>&1
|
|
fi
|
|
set_permissions "${user_lxc}" "${user_lxc_home}" "${container}"
|
|
echo -e "${BL}[Info]${GN} Copy Private Key of user '${user}' inside${BL} ${name}${GN}: ${CL}$(pct exec "${container}" -- bash -c "${command_3}" 2>&1)"
|
|
else
|
|
! [[ -f ${user_home}/${private_key} ]] && echo -e "${BL}[ERROR]${GN} ${RD}No Private Key found!${CL}\n" && exit
|
|
if pct exec "${container}" -- bash -c "${command_1}" >/dev/null 2>&1; then
|
|
pct exec "${container}" -- bash -c "${command_4}" 2>&1
|
|
pct exec "${container}" -- bash -c "${command_5}" 2>&1
|
|
set_permissions "${user_lxc}" "${user_lxc_home}" "${container}"
|
|
fi
|
|
echo -e "${BL}[Info]${GN} Delete Private Key of user '${user}' inside${BL} ${name}${GN}: ${CL}$(pct exec "${container}" -- bash -c "${command_6}" 2>&1)"
|
|
fi
|
|
echo -e "${BL}[Info]${GN} ssh configuration of User '${user_lxc}' inside${BL} ${name}${GN}: ${CL}"
|
|
echo
|
|
pct exec "${container}" -- bash -c "${command_7}" 2>&1
|
|
echo
|
|
pct exec "${container}" -- bash -c "${command_8}" 2>&1
|
|
echo -e "\n${GN}The process of ${USER_OPTION} is complete, and the container has been successfully modified.${CL}\n" && exit
|
|
}
|
|
|
|
set -eEuo pipefail
|
|
# shellcheck disable=SC2034
|
|
# shellcheck disable=SC2116
|
|
# shellcheck disable=SC2028
|
|
YW=$(echo "\033[33m")
|
|
# shellcheck disable=SC2116
|
|
# shellcheck disable=SC2028
|
|
BL=$(echo "\033[36m")
|
|
# shellcheck disable=SC2116
|
|
# shellcheck disable=SC2028
|
|
RD=$(echo "\033[01;31m")
|
|
# shellcheck disable=SC2034
|
|
CM='\xE2\x9C\x94\033'
|
|
# shellcheck disable=SC2116
|
|
# shellcheck disable=SC2028
|
|
GN=$(echo "\033[1;92m")
|
|
# shellcheck disable=SC2116
|
|
# shellcheck disable=SC2028
|
|
CL=$(echo "\033[m")
|
|
|
|
# Telemetry
|
|
# shellcheck disable=SC1090
|
|
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/api.func) 2>/dev/null || true
|
|
declare -f init_tool_telemetry &>/dev/null && init_tool_telemetry "pve-lxc-system-admin" "pve"
|
|
|
|
NODE=$(hostname)
|
|
|
|
header_info
|
|
whiptail --backtitle "Proxmox VE Helper Scripts" --title "Proxmox VE LXC System Admin" --yesno "This will maintain passwordless LXC System Admins with full SUDO Permissions and SSH Access via SSH Keys. Proceed?" 10 58
|
|
|
|
whiptail_menu "$(awk -F ':' '$3>=1000 && $7 == "/bin/bash" && NR>1 {print $1 " " $2 ":" $3 ":" $4 ":" $5 ":" $6 ":" $7}' /etc/passwd)"
|
|
MENU_ARRAY+=("" "Create a new User" "OFF")
|
|
|
|
USER_NAME=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Users on ${NODE}" --radiolist "\nSelect a User to maintain as a System Admin on LXCs:\n" 16 $((MSG_MAX_LENGTH + 23)) 6 "${MENU_ARRAY[@]}" 3>&1 1>&2 2>&3 | tr -d '"')
|
|
[[ -z ${USER_NAME} ]] && add_node_user
|
|
USER_COMMENT=$(grep "${USER_NAME}" /etc/passwd | awk -F ':' '{print $5}')
|
|
USER_HOME=$(get_user_home "${USER_NAME}" "")
|
|
|
|
MENU_ARRAY=("Deploy" "Deploy UID and Public Key of User '${USER_NAME}'." "ON")
|
|
MENU_ARRAY+=("Create/Renew Key Pair" "Create/Renew Key Pair and Deploy Public Key of User '${USER_NAME}'." "OFF")
|
|
MENU_ARRAY+=("Delete" "Delete User '${USER_NAME}'." "OFF")
|
|
MENU_ARRAY+=("Show" "Show Private Key of User '${USER_NAME}'." "OFF")
|
|
MENU_ARRAY+=("Add Authorization" "Add Authorization for SSH access with Private Key of User '${USER_NAME}'." "OFF")
|
|
MENU_ARRAY+=("Remove Authorization" "Remove Authorization for SSH access with Private Key of User '${USER_NAME}'." "OFF")
|
|
USER_ACTION=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Proxmox VE LXC System Admin" --radiolist "Select Maintenance Option for User '${USER_NAME}':" 16 148 6 "${MENU_ARRAY[@]}" 3>&1 1>&2 2>&3 | tr -d '"')
|
|
|
|
[[ -z ${USER_ACTION} ]] && echo -e "${BL}[ERROR]${GN} ${RD}No User Option selected!${CL}\n" && exit
|
|
|
|
case ${USER_ACTION} in
|
|
("Deploy")
|
|
USER_OPTION="Deploy UID and Public Key of User '${USER_NAME}'"
|
|
[[ -f ${USER_HOME}/.ssh/authorized_keys ]] || keygen_node_user "${USER_NAME}" "New Key"
|
|
;;
|
|
("Create/Renew Key Pair")
|
|
USER_OPTION="Deploy UID and Public Key of User '${USER_NAME}'"
|
|
keygen_node_user "${USER_NAME}"
|
|
;;
|
|
("Delete")
|
|
USER_OPTION="Delete User '${USER_NAME}'"
|
|
delete_user "${USER_NAME}" ""
|
|
;;
|
|
("Show")
|
|
USER_OPTION="Show Private Key of User '${USER_NAME}'"
|
|
show_private_key "${USER_NAME}"
|
|
;;
|
|
("Add Authorization")
|
|
USER_OPTION="Add Authorization for SSH access with Private Key of User '${USER_NAME}'"
|
|
echo -e "${BL}[Info]${GN} ${USER_OPTION}:${CL}\n"
|
|
authorization "${USER_NAME}" "add"
|
|
;;
|
|
("Remove Authorization")
|
|
USER_OPTION="Remove Authorization for SSH access with Private Key of User '${USER_NAME}''"
|
|
echo -e "${BL}[Info]${GN} ${USER_OPTION}:${CL}\n"
|
|
authorization "${USER_NAME}" "remove"
|
|
;;
|
|
*)
|
|
echo -e "${BL}[ERROR]${GN} ${RD}Unsupported Option!${CL}\n" && exit
|
|
;;
|
|
esac
|
|
|
|
if whiptail --backtitle "Proxmox VE Helper Scripts" --title "Skip Not-Running Containers" --yesno "Do you want to skip containers that are not currently running?" 10 58; then
|
|
SKIP_STOPPED="yes"
|
|
else
|
|
SKIP_STOPPED="no"
|
|
fi
|
|
|
|
whiptail_menu "$(pct list | awk 'NR>1')"
|
|
excluded_containers=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "Containers on ${NODE}" --checklist "\nSelect containers to skip from modifications:\n" 16 $((MSG_MAX_LENGTH + 23)) 6 "${MENU_ARRAY[@]}" 3>&1 1>&2 2>&3 | tr -d '"')
|
|
|
|
echo -e "${BL}[Info]${GN} ${USER_OPTION}:${CL}\n"
|
|
for container in $(pct list | awk '{if(NR>1) print $1}'); do
|
|
#if [[ " ${excluded_containers[@]} " =~ " ${container} " ]]; then
|
|
# shellcheck disable=SC2199
|
|
if [[ ${excluded_containers[@]} =~ ${container} ]]; then
|
|
echo -e "${BL}[Info]${GN} --- Skipping ${BL}${container}${CL}"
|
|
else
|
|
status=$(pct status "${container}")
|
|
if [ "$SKIP_STOPPED" == "yes" ] && [ "$status" == "status: stopped" ]; then
|
|
echo -e "${BL}[Info]${GN} --- Skipping ${BL}${container}${CL}${GN} (not running)${CL}"
|
|
continue
|
|
fi
|
|
template=$(pct config "${container}" | grep -q "template:" && echo "true" || echo "false")
|
|
if [ "$template" == "false" ] && [ "$status" == "status: stopped" ]; then
|
|
echo -e "${BL}[Info]${GN} --- Starting${BL} ${container} ${CL}"
|
|
pct start "${container}"
|
|
echo -e "${BL}[Info]${GN} --- Waiting For${BL} ${container}${CL}${GN} To Start ${CL}"
|
|
sleep 5
|
|
if [ "${USER_ACTION}" == "Delete" ]; then
|
|
delete_user "${USER_NAME}" "${container}"
|
|
else
|
|
maintain_container_user "${USER_NAME}" "${USER_COMMENT}" "${container}"
|
|
fi
|
|
echo -e "${BL}[Info]${GN} --- Shutting down${BL} ${container} ${CL}"
|
|
pct shutdown "${container}" --timeout 180 &
|
|
elif [ "$status" == "status: running" ]; then
|
|
if [ "${USER_ACTION}" == "Delete" ]; then
|
|
delete_user "${USER_NAME}" "${container}"
|
|
else
|
|
maintain_container_user "${USER_NAME}" "${USER_COMMENT}" "${container}"
|
|
fi
|
|
fi
|
|
fi
|
|
done
|
|
wait
|
|
echo -e "\n${GN}The process of ${USER_OPTION} is complete, and the containers have been successfully modified.${CL}\n"
|