Merge pull request #1627 from heinemannj/pve-lxc-system-admin

Create pve-lxc-system-admin.sh
This commit is contained in:
CanbiZ (MickLesk)
2026-06-05 15:09:25 +02:00
committed by GitHub

View File

@@ -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"