From 8a740a8876f749897ec26ec5e1cdd4d565c8537a Mon Sep 17 00:00:00 2001 From: Joerg Heinemann Date: Thu, 26 Mar 2026 10:29:40 +0100 Subject: [PATCH] Create pve-lxc-system-admin.sh Initial commit --- tools/pve/pve-lxc-system-admin.sh | 354 ++++++++++++++++++++++++++++++ 1 file changed, 354 insertions(+) create mode 100644 tools/pve/pve-lxc-system-admin.sh diff --git a/tools/pve/pve-lxc-system-admin.sh b/tools/pve/pve-lxc-system-admin.sh new file mode 100644 index 00000000..78f6ce25 --- /dev/null +++ b/tools/pve/pve-lxc-system-admin.sh @@ -0,0 +1,354 @@ +#!/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"