Skip to content

Instantly share code, notes, and snippets.

@Sly777
Last active December 28, 2025 17:38
Show Gist options
  • Select an option

  • Save Sly777/d07b30d92f7d515ee8b50227fafa01ed to your computer and use it in GitHub Desktop.

Select an option

Save Sly777/d07b30d92f7d515ee8b50227fafa01ed to your computer and use it in GitHub Desktop.
Deploy SSH Configuration to All Debian LXC Containers with security audit, config comparison, user management, and logging
#!/bin/bash
# =============================================================================
# Deploy SSH Configuration to All Debian LXC Containers
# With security audit, config comparison, user management, and logging
# Usage: ./deploy-ssh-config.sh [CT_ID]
# No argument: Process all containers
# With CT_ID: Process only specified container
# =============================================================================
# =============================================================================
# CONFIGURATION - Edit these variables
# =============================================================================
SSH_USER="USERNAME_ON_DEBIAN"
SSHD_CONFIG="/root/sshd_config.template"
PUBKEY_FILE="/root/${SSH_USER}_authorized_keys"
LOG_DIR="/var/log/ssh-deploy"
DATE=$(date +%Y%m%d-%H%M%S)
# Secure groups for user
SECURE_GROUPS="sudo systemd-journal"
# Groups to REMOVE if found (security risks)
RISKY_GROUPS="disk www-data"
# Colors for terminal output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# =============================================================================
# PARSE COMMAND LINE ARGUMENTS
# =============================================================================
TARGET_CT=""
if [ $# -eq 1 ]; then
TARGET_CT="$1"
# Validate container ID is numeric
if ! [[ "$TARGET_CT" =~ ^[0-9]+$ ]]; then
echo -e "${RED}Error: Container ID must be numeric${NC}"
echo "Usage: $0 [CT_ID]"
exit 1
fi
# Verify container exists
if ! pct list | awk '{print $1}' | grep -q "^${TARGET_CT}$"; then
echo -e "${RED}Error: Container $TARGET_CT does not exist${NC}"
echo ""
echo "Available containers:"
pct list
exit 1
fi
echo -e "${BLUE}Target mode: Processing only CT ${TARGET_CT}${NC}"
elif [ $# -gt 1 ]; then
echo -e "${RED}Error: Too many arguments${NC}"
echo "Usage: $0 [CT_ID]"
echo " No argument: Process all containers"
echo " With CT_ID: Process only specified container (e.g., $0 112)"
exit 1
fi
# =============================================================================
# LOGGING SETUP
# =============================================================================
mkdir -p "$LOG_DIR"
LOG_FILE="${LOG_DIR}/ssh-deploy-$(date +%Y%m%d).log"
# Function to log with timestamp
log() {
local level="$1"
local message="$2"
local timestamp="[$(date '+%Y-%m-%d %H:%M:%S')]"
# Write to log file (plain text, no colors)
echo "${timestamp} [${level}] ${message}" >> "$LOG_FILE"
# Write to terminal with colors
case "$level" in
"INFO") echo -e "${timestamp} ${GREEN}[${level}]${NC} ${message}" ;;
"WARN") echo -e "${timestamp} ${YELLOW}[${level}]${NC} ${message}" ;;
"ERROR") echo -e "${timestamp} ${RED}[${level}]${NC} ${message}" ;;
"AUDIT") echo -e "${timestamp} ${BLUE}[${level}]${NC} ${message}" ;;
"SUCCESS") echo -e "${timestamp} ${GREEN}[${level}]${NC} ${message}" ;;
*) echo -e "${timestamp} [${level}] ${message}" ;;
esac
}
# Log section headers
log_section() {
local title="$1"
echo "" >> "$LOG_FILE"
echo "========================================" >> "$LOG_FILE"
echo "$title" >> "$LOG_FILE"
echo "========================================" >> "$LOG_FILE"
echo ""
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}${title}${NC}"
echo -e "${BLUE}========================================${NC}"
}
# Log container headers
log_container() {
local ct_id="$1"
local ct_name="$2"
local ct_status="$3"
echo "" >> "$LOG_FILE"
echo "----------------------------------------" >> "$LOG_FILE"
echo "CT ${ct_id} (${ct_name}) - Status: ${ct_status}" >> "$LOG_FILE"
echo "----------------------------------------" >> "$LOG_FILE"
echo ""
echo "----------------------------------------"
echo -e "CT ${ct_id} (${YELLOW}${ct_name}${NC}) - Status: ${ct_status}"
echo "----------------------------------------"
}
# =============================================================================
# HELPER FUNCTIONS
# =============================================================================
# Check if user is member of a group in container
user_in_group() {
local ct_id=$1
local user=$2
local group=$3
pct exec "$ct_id" -- id -nG "$user" 2>/dev/null | grep -qw "$group"
}
# Get public key hash from container
get_remote_pubkey_hash() {
local ct_id=$1
local user=$2
pct exec "$ct_id" -- cat "/home/$user/.ssh/authorized_keys" 2>/dev/null | md5sum | awk '{print $1}'
}
# =============================================================================
# MAIN SCRIPT START
# =============================================================================
if [ -n "$TARGET_CT" ]; then
log_section "SSH Config Deployment (Target: CT $TARGET_CT)"
else
log_section "SSH Config Deployment & Security Audit (All Containers)"
fi
log "INFO" "Date: $DATE"
log "INFO" "User: $SSH_USER"
log "INFO" "Log file: $LOG_FILE"
# Log to system journal
if [ -n "$TARGET_CT" ]; then
logger -t ssh-deploy "Starting SSH deployment for CT $TARGET_CT (user: $SSH_USER)"
else
logger -t ssh-deploy "Starting SSH deployment for all containers (user: $SSH_USER)"
fi
# =============================================================================
# PRE-FLIGHT CHECKS
# =============================================================================
if [ ! -f "$SSHD_CONFIG" ]; then
log "ERROR" "$SSHD_CONFIG not found!"
logger -p user.err -t ssh-deploy "SSHD config template not found"
exit 1
fi
if [ ! -f "$PUBKEY_FILE" ]; then
log "ERROR" "$PUBKEY_FILE not found!"
log "INFO" "Create it with: micro $PUBKEY_FILE"
log "INFO" "Then paste your SSH public key from Bitwarden"
logger -p user.err -t ssh-deploy "Public key file not found"
exit 1
fi
log "SUCCESS" "Pre-flight checks passed"
# Get template hashes for comparison
TEMPLATE_HASH=$(md5sum "$SSHD_CONFIG" | awk '{print $1}')
LOCAL_PUBKEY_HASH=$(md5sum "$PUBKEY_FILE" | awk '{print $1}')
# Get container list based on mode
if [ -n "$TARGET_CT" ]; then
containers="$TARGET_CT"
else
containers=$(pct list | tail -n +2 | awk '{print $1}')
fi
# Counters for summary
TOTAL_CONTAINERS=0
PROCESSED_CONTAINERS=0
SKIPPED_CONTAINERS=0
USERS_CREATED=0
KEYS_DEPLOYED=0
CONFIGS_DEPLOYED=0
ERRORS=0
for CT_ID in $containers; do
TOTAL_CONTAINERS=$((TOTAL_CONTAINERS + 1))
CT_STATUS=$(pct status "$CT_ID" | awk '{print $2}')
CT_NAME=$(pct list | awk -v id="$CT_ID" '$1==id {print $3}')
log_container "$CT_ID" "$CT_NAME" "$CT_STATUS"
# Skip stopped containers
if [ "$CT_STATUS" != "running" ]; then
log "WARN" "Container not running, skipping"
SKIPPED_CONTAINERS=$((SKIPPED_CONTAINERS + 1))
continue
fi
# Improved Debian check - verify dpkg exists
if ! pct exec "$CT_ID" -- test -f /usr/bin/dpkg 2>/dev/null; then
log "WARN" "Not Debian-based, skipping"
SKIPPED_CONTAINERS=$((SKIPPED_CONTAINERS + 1))
continue
fi
# Check if SSH server is installed
if ! pct exec "$CT_ID" -- test -f /etc/ssh/sshd_config 2>/dev/null; then
log "WARN" "SSH server not installed, skipping"
SKIPPED_CONTAINERS=$((SKIPPED_CONTAINERS + 1))
continue
fi
log "SUCCESS" "Debian container with SSH detected"
PROCESSED_CONTAINERS=$((PROCESSED_CONTAINERS + 1))
# =========================================================================
# SECURITY AUDIT: Check and fix user groups
# =========================================================================
log "AUDIT" "Starting security audit for $SSH_USER"
if pct exec "$CT_ID" -- id "$SSH_USER" > /dev/null 2>&1; then
# User exists - audit groups
CURRENT_GROUPS=$(pct exec "$CT_ID" -- groups "$SSH_USER" 2>/dev/null)
log "AUDIT" "Current groups: $CURRENT_GROUPS"
# Remove risky groups
for RISKY_GROUP in $RISKY_GROUPS; do
if user_in_group "$CT_ID" "$SSH_USER" "$RISKY_GROUP"; then
log "WARN" "Removing risky group: $RISKY_GROUP"
pct exec "$CT_ID" -- gpasswd -d "$SSH_USER" "$RISKY_GROUP" 2>/dev/null
fi
done
# Ensure secure groups are present (only if not already member)
GROUPS_CHANGED=false
for SECURE_GROUP in $SECURE_GROUPS; do
if ! pct exec "$CT_ID" -- getent group "$SECURE_GROUP" > /dev/null 2>&1; then
continue
fi
if user_in_group "$CT_ID" "$SSH_USER" "$SECURE_GROUP"; then
log "INFO" "Already in group: $SECURE_GROUP"
else
pct exec "$CT_ID" -- usermod -aG "$SECURE_GROUP" "$SSH_USER" 2>/dev/null
log "SUCCESS" "Added to group: $SECURE_GROUP"
GROUPS_CHANGED=true
fi
done
if [ "$GROUPS_CHANGED" = true ]; then
UPDATED_GROUPS=$(pct exec "$CT_ID" -- groups "$SSH_USER" 2>/dev/null)
log "SUCCESS" "Updated groups: $UPDATED_GROUPS"
fi
else
# User doesn't exist - create with secure groups
log "INFO" "Creating $SSH_USER user..."
pct exec "$CT_ID" -- useradd -m -s /bin/bash "$SSH_USER"
for SECURE_GROUP in $SECURE_GROUPS; do
if pct exec "$CT_ID" -- getent group "$SECURE_GROUP" > /dev/null 2>&1; then
pct exec "$CT_ID" -- usermod -aG "$SECURE_GROUP" "$SSH_USER"
log "SUCCESS" "Added to group: $SECURE_GROUP"
fi
done
pct exec "$CT_ID" -- mkdir -p "/home/$SSH_USER/.ssh"
pct exec "$CT_ID" -- chmod 700 "/home/$SSH_USER/.ssh"
pct exec "$CT_ID" -- chown -R "$SSH_USER:$SSH_USER" "/home/$SSH_USER"
log "SUCCESS" "User created with secure groups"
USERS_CREATED=$((USERS_CREATED + 1))
fi
# =========================================================================
# SSH PUBLIC KEY DEPLOYMENT (with comparison)
# =========================================================================
if [ -f "$PUBKEY_FILE" ]; then
REMOTE_PUBKEY_HASH=$(get_remote_pubkey_hash "$CT_ID" "$SSH_USER")
if [ "$LOCAL_PUBKEY_HASH" == "$REMOTE_PUBKEY_HASH" ]; then
log "INFO" "Public key already matches, skipping"
else
log "INFO" "Deploying SSH public key..."
pct push "$CT_ID" "$PUBKEY_FILE" "/home/$SSH_USER/.ssh/authorized_keys"
pct exec "$CT_ID" -- chmod 600 "/home/$SSH_USER/.ssh/authorized_keys"
pct exec "$CT_ID" -- chown "$SSH_USER:$SSH_USER" "/home/$SSH_USER/.ssh/authorized_keys"
log "SUCCESS" "Public key deployed"
KEYS_DEPLOYED=$((KEYS_DEPLOYED + 1))
fi
else
log "WARN" "No public key file found at $PUBKEY_FILE"
fi
# =========================================================================
# SSH CONFIG DEPLOYMENT
# =========================================================================
CURRENT_HASH=$(pct exec "$CT_ID" -- md5sum /etc/ssh/sshd_config 2>/dev/null | awk '{print $1}')
if [ "$TEMPLATE_HASH" == "$CURRENT_HASH" ]; then
log "INFO" "SSH config already matches template, skipping"
continue
fi
log "INFO" "Backing up existing config..."
pct exec "$CT_ID" -- cp /etc/ssh/sshd_config "/etc/ssh/sshd_config.backup.$DATE"
log "INFO" "Pushing new sshd_config..."
pct push "$CT_ID" "$SSHD_CONFIG" /etc/ssh/sshd_config
pct exec "$CT_ID" -- chmod 644 /etc/ssh/sshd_config
pct exec "$CT_ID" -- chown root:root /etc/ssh/sshd_config
log "INFO" "Validating config syntax..."
if pct exec "$CT_ID" -- sshd -t 2>&1; then
log "SUCCESS" "Config syntax valid"
log "INFO" "Restarting SSH service..."
pct exec "$CT_ID" -- systemctl restart sshd 2>/dev/null || \
pct exec "$CT_ID" -- systemctl restart ssh 2>/dev/null
if pct exec "$CT_ID" -- systemctl is-active sshd > /dev/null 2>&1 || \
pct exec "$CT_ID" -- systemctl is-active ssh > /dev/null 2>&1; then
log "SUCCESS" "SSH service running"
CONFIGS_DEPLOYED=$((CONFIGS_DEPLOYED + 1))
CT_IP=$(pct exec "$CT_ID" -- hostname -I 2>/dev/null | awk '{print $1}')
if [ -n "$CT_IP" ]; then
log "INFO" "Test: ssh $SSH_USER@$CT_IP"
fi
else
log "ERROR" "SSH service failed!"
ERRORS=$((ERRORS + 1))
fi
else
log "ERROR" "Config validation FAILED! Restoring backup..."
pct exec "$CT_ID" -- cp "/etc/ssh/sshd_config.backup.$DATE" /etc/ssh/sshd_config
pct exec "$CT_ID" -- systemctl restart sshd 2>/dev/null || \
pct exec "$CT_ID" -- systemctl restart ssh 2>/dev/null
ERRORS=$((ERRORS + 1))
fi
done
# =============================================================================
# DEPLOYMENT SUMMARY
# =============================================================================
log_section "Deployment Summary"
if [ -n "$TARGET_CT" ]; then
log "INFO" "Target mode: CT $TARGET_CT"
fi
log "INFO" "Total containers: $TOTAL_CONTAINERS"
log "INFO" "Processed: $PROCESSED_CONTAINERS"
log "INFO" "Skipped: $SKIPPED_CONTAINERS"
log "INFO" "Users created: $USERS_CREATED"
log "INFO" "Keys deployed: $KEYS_DEPLOYED"
log "INFO" "Configs deployed: $CONFIGS_DEPLOYED"
if [ "$ERRORS" -gt 0 ]; then
log "ERROR" "Errors encountered: $ERRORS"
else
log "SUCCESS" "Errors encountered: $ERRORS"
fi
log "INFO" ""
log "INFO" "User managed: $SSH_USER"
log "INFO" "Secure groups: $SECURE_GROUPS"
log "INFO" "Risky groups removed: $RISKY_GROUPS"
log "INFO" ""
log "INFO" "Log saved to: $LOG_FILE"
log "INFO" "Safety reminder: If SSH fails, use: pct enter <CT_ID>"
# Log completion to system journal
if [ -n "$TARGET_CT" ]; then
logger -t ssh-deploy "Completed CT $TARGET_CT: $PROCESSED_CONTAINERS processed, $ERRORS errors"
else
logger -t ssh-deploy "Completed: $PROCESSED_CONTAINERS processed, $ERRORS errors"
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment