#!/bin/bash # ============================================================================== # AUTOMATED & IDEMPOTENT LINUX SERVER BOOTSTRAP SCRIPT (v5 - English Edition) # ============================================================================== # # HOW TO RUN DIRECTLY FROM THE COMMAND LINE WITHOUT DOWNLOADING: # # curl -sSL https://raw.githubusercontent.com/pniaps/linux-server-bootstrap/main/setup.sh | bash # # OR with predefined variables for headless execution: # curl -sSL https://raw.githubusercontent.com/pniaps/linux-server-bootstrap/main/setup.sh | GITHUB_USER="gh_user" NEW_USER="pnia" SSH_PORT="4815" HOSTNAME="my-vps" bash # # ============================================================================== set -e echo "=== Beginning foundational environment setup and initial hardening ===" # 1. Prompt for GitHub username if not provided via environment variable if [ -z "$GITHUB_USER" ]; then read -p "Enter your GitHub username to fetch public keys: " GITHUB_USER if [ -z "$GITHUB_USER" ]; then echo "[ERROR] GitHub username is required to safely inject SSH access." >&2 exit 1 fi fi # 2. Prompt for Sudoer username if not provided via environment variable if [ -z "$NEW_USER" ]; then read -p "Enter the name of the new administrative user (Sudoer): " NEW_USER if [ -z "$NEW_USER" ]; then echo "[ERROR] Sudoer username selection is required." >&2 exit 1 fi fi # 3. Prompt for SSH Port if not provided via environment variable if [ -z "$SSH_PORT" ]; then read -p "Enter the desired SSH port [Default: 22]: " SSH_PORT SSH_PORT=${SSH_PORT:-22} fi # 4. Prompt for Hostname if not provided via environment variable CURRENT_HOSTNAME=$(hostname) if [ -z "$HOSTNAME" ]; then read -p "Enter the desired hostname for this server [Default: ${CURRENT_HOSTNAME}]: " HOSTNAME HOSTNAME=${HOSTNAME:-$CURRENT_HOSTNAME} fi # 5. Configure Hostname if it changed if [ "$HOSTNAME" != "$CURRENT_HOSTNAME" ]; then echo "Updating system hostname to '${HOSTNAME}'..." sudo hostnamectl set-hostname "$HOSTNAME" # Extract the short name (e.g., if HOSTNAME is vps1.domain.com, SHORT_NAME becomes vps1) SHORT_NAME=$(echo "$HOSTNAME" | cut -d. -f1) # Update /etc/hosts cleanly without duplicating rows # First, we remove old entries assigned to 127.0.1.1 to keep it tidy sudo sed -i '/^127\.0\.1\.1/d' /etc/hosts # Append the new clean mapping echo "127.0.1.1 ${HOSTNAME} ${SHORT_NAME}" | sudo tee -a /etc/hosts > /dev/null echo "Hostname successfully updated." else echo "Keeping current hostname: ${CURRENT_HOSTNAME}" fi # 6. Establish System Timezone echo "Configuring system timezone to Europe/Madrid..." sudo timedatectl set-timezone Europe/Madrid echo "Current system time state: $(date)" # 7. Idempotent Sudoer Account Provisioning if id "$NEW_USER" >/dev/null 2>&1; then echo "User '$NEW_USER' already exists. Skipping user provisioning." else echo "Creating administrative account '$NEW_USER'..." sudo useradd -m -s /bin/bash "$NEW_USER" # Lock the interactive password field (*), demanding cryptographic keys for authentication sudo usermod -p '*' "$NEW_USER" # Grant passwordless sudo elevation for smoother configuration tasks echo "$NEW_USER ALL=(ALL) NOPASSWD:ALL" | sudo tee "/etc/sudoers.d/$NEW_USER" > /dev/null sudo chmod 0440 "/etc/sudoers.d/$NEW_USER" fi # 8. Idempotent Cryptographic Key Injection USER_HOME=$(eval echo ~$NEW_USER) SSH_DIR="$USER_HOME/.ssh" sudo mkdir -p "$SSH_DIR" sudo chmod 700 "$SSH_DIR" sudo touch "$SSH_DIR/authorized_keys" sudo chmod 600 "$SSH_DIR/authorized_keys" echo "Retrieving public keys from GitHub for profile: '$GITHUB_USER'..." TMP_KEYS=$(mktemp) curl -s "https://github.com/${GITHUB_USER}.keys" > "$TMP_KEYS" if [ ! -s "$TMP_KEYS" ]; then echo "[WARNING] No public keys found on GitHub for user '$GITHUB_USER' or network connection failed." else while IFS= read -r key; do if ! grep -qF "$key" "$SSH_DIR/authorized_keys"; then echo "$key" | sudo tee -a "$SSH_DIR/authorized_keys" > /dev/null echo "Successfully injected key string into $NEW_USER environment." else echo "Key signature already present in authorization file. Skipping duplicate." fi done < "$TMP_KEYS" fi rm -f "$TMP_KEYS" sudo chown -R "$NEW_USER:$NEW_USER" "$SSH_DIR" # 9. One-Time SSH Daemon Hardening & Conditional Port Assignment SSH_CONFIG="/etc/ssh/sshd_config" SSH_CONFIG_BAK="${SSH_CONFIG}.bak" if [ -f "$SSH_CONFIG_BAK" ]; then echo "[SSH] Backup file $SSH_CONFIG_BAK detected. Skipping SSH modification to prevent override loops." else echo "[SSH] Reconfiguring service parameters and hardening access..." sudo cp "$SSH_CONFIG" "$SSH_CONFIG_BAK" # Swap out defaults for hardened profiles (Password & Root restriction) sudo sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin no/' $SSH_CONFIG sudo sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' $SSH_CONFIG sudo sed -i 's/^#\?ChallengeResponseAuthentication.*/ChallengeResponseAuthentication no/' $SSH_CONFIG 2>/dev/null || true sudo sed -i 's/^#\?KbdInteractiveAuthentication.*/KbdInteractiveAuthentication no/' $SSH_CONFIG 2>/dev/null || true # Only modify port if a custom one (not 22) was explicitly selected if [ "$SSH_PORT" != "22" ]; then echo "[SSH] Changing standard port to $SSH_PORT. Make sure this port is open in your external firewall!" sudo sed -i "s/^#\?Port.*/Port $SSH_PORT/" $SSH_CONFIG fi # Address drop-in override directories (*.conf) standard in modern distributions SSH_CONFIG_D="/etc/ssh/sshd_config.d" if [ -d "$SSH_CONFIG_D" ]; then if [ -f "$SSH_CONFIG_D/50-cloud-init.conf" ]; then # Build configuration string based on choice CONF_PAYLOAD="PasswordAuthentication no\nPermitRootLogin no" if [ "$SSH_PORT" != "22" ]; then CONF_PAYLOAD="Port $SSH_PORT\n${CONF_PAYLOAD}" fi echo -e "$CONF_PAYLOAD" | sudo tee "$SSH_CONFIG_D/50-cloud-init.conf" > /dev/null fi fi echo "Restarting SSH daemon..." if systemctl is-active --quiet ssh; then sudo systemctl restart ssh elif systemctl is-active --quiet sshd; then sudo systemctl restart sshd fi fi # 10. Activate Automatic Security Patching Engine echo "Deploying unattended security update triggers..." if [ -f /usr/bin/apt-get ]; then sudo apt-get update -y > /dev/null sudo apt-get install -y unattended-upgrades apt-listchanges > /dev/null echo "APT::Periodic::Update-Package-Lists \"1\";" | sudo tee /etc/apt/apt.conf.d/20auto-upgrades > /dev/null echo "APT::Periodic::Unattended-Upgrade \"1\";" | sudo tee -a /etc/apt/apt.conf.d/20auto-upgrades > /dev/null sudo systemctl restart unattended-upgrades || true echo "Automated patching infrastructure configured and active." else echo "[NOTICE] APT package manager not found. If this is an RHEL/CentOS system, deploy 'dnf-automatic' manually." fi # 11. Fetch Public IP for Client Config Hint SERVER_IP=$(curl -s https://ifconfig.me || curl -s https://api.ipify.org || echo "") echo "========================================================================" echo " FOUNDATIONAL BOOTSTRAP PROCESS SUCCESSFUL!" echo "========================================================================" echo " " echo "🖥️ SSH CLIENT CONFIGURATION (LOCAL MACHINE)" echo "Add the following block to your local '~/.ssh/config' file (Windows/Linux/macOS)" echo "to connect seamlessly without typing the port or username every time:" echo " " echo "------------------------------------------------------------------------" echo "Host ${HOSTNAME}" echo " HostName ${SERVER_IP}" echo " User ${NEW_USER}" echo " Port ${SSH_PORT}" echo "------------------------------------------------------------------------" echo " " echo "Then, you can simply connect from your local terminal by running:" echo " ssh ${HOSTNAME}" echo " " echo "⚠️ CRITICAL POST-CHECK:" echo "Open a separate terminal shell locally right now and verify connection using:" echo " ssh -p ${SSH_PORT} ${NEW_USER}@${SERVER_IP}" echo " " echo "DO NOT terminate this active root session until new access is fully confirmed!" echo "========================================================================"