From 116f63f8cb16685dd838fb4b25efeacd7c714a3a Mon Sep 17 00:00:00 2001 From: Decaded Date: Wed, 17 Jun 2026 22:18:51 +0200 Subject: [PATCH 1/4] refactor: update README and install script for improved app options and structure - Enhanced app options in README to include alternatives for htop and screen. - Updated install script to improve structure, add new functions, and enhance error handling. - Refactored firewall and SSH configuration functions for better clarity and maintainability. --- README.md | 6 +- install.sh | 680 +++++++++++++++++++++++++++++++---------------------- 2 files changed, 399 insertions(+), 287 deletions(-) diff --git a/README.md b/README.md index a71ae85..656487c 100644 --- a/README.md +++ b/README.md @@ -59,10 +59,10 @@ A menu will appear with all available options. Install a curated pack of common system utilities: -1. **[htop](https://htop.dev/)** – process viewer -2. **[screen](https://www.gnu.org/software/screen/)** – terminal multiplexer +1. **[htop](https://htop.dev/)** or **[btop](https://github.com/aristocratos/btop)** – process viewer +2. **[screen](https://www.gnu.org/software/screen/)** or **[tmux](https://github.com/tmux/tmux/wiki)** – terminal multiplexer 3. **[nload](https://github.com/rolandriegel/nload)** – network traffic monitor -4. **[nano](https://www.nano-editor.org/)** – simple text editor +4. **[nano](https://www.nano-editor.org/)** or **[Neovim](https://neovim.io/)** – text editor 5. **[firewalld](https://firewalld.org/)** – firewall management - Automatically opens SSH diff --git a/install.sh b/install.sh index 979fbb4..ab70f23 100644 --- a/install.sh +++ b/install.sh @@ -5,13 +5,88 @@ # It allows users to install essential apps, set up a web server, configure NVM, enable passwordless sudo, # set up SSH key-based authentication and more. -# Author: Decaded (https://github.com/Decaded) - -# Script version -SCRIPT_VERSION="2.1.2" +# Author: Decaded (https://github.com/Decaded | https://decaded.dev) + +# Sections: +# - Metadata and Constants +# - Common Helpers +# - Menu and Routing +# - Update System +# - Essential Apps +# - Firewalld +# - SSH +# - Passwordless Sudo +# - Web Server +# - NVM +# - Git +# - Fail2ban +# - Static IP / Netplan +# - Main + +# ============================================================================== +# Metadata and Constants +# ============================================================================== + +SCRIPT_VERSION="3.0.0" SCRIPT_URL="https://raw.githubusercontent.com/Decaded/install-script/refs/heads/main/install.sh" -# Function to display a menu and get user's choice +SSH_CONFIG="/etc/ssh/sshd_config" +SSH_BACKUP_PATTERN="/etc/ssh/sshd_config_decoscript.backup.*" +FIREWALLD_CONF="/etc/firewalld/firewalld.conf" +NETPLAN_CONFIG="/etc/netplan/01-network-manager-all.yaml" +NGINX_DEFAULT_SITE="/etc/nginx/sites-available/default" +NGINX_CERT_DIR="/etc/nginx/cert" +WEB_ROOT="/var/www/html" + +# ============================================================================== +# Common Helpers +# ============================================================================== + +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +package_installed() { + dpkg-query -W -f='${Status}' "$1" 2>/dev/null | grep -q "install ok installed" +} + +apt_install() { + if [ "$#" -eq 0 ]; then + return 0 + fi + + sudo apt update && sudo apt install "$@" -y +} + +check_sudo_privileges() { + sudo -n true + if [ $? -ne 0 ]; then + echo "You need sudo privilege to run this script." + exit 1 + fi +} + +validate_port() { + local port="$1" + if ! [[ "$port" =~ ^[0-9]+$ ]] || ((port < 1 || port > 65535)); then + echo "Error: Invalid port number. Please enter a valid numeric port between 1 and 65535." + return 1 + fi + return 0 +} + +has_ssh_config_backup() { + compgen -G "$SSH_BACKUP_PATTERN" >/dev/null +} + +latest_ssh_config_backup() { + ls -1t $SSH_BACKUP_PATTERN 2>/dev/null | head -n1 +} + +# ============================================================================== +# Menu and Routing +# ============================================================================== + show_menu() { clear echo "╔════════════════════════════════════════════════════════╗" @@ -31,7 +106,7 @@ show_menu() { echo "u) Check for script updates" echo - if [ -f "/etc/ssh/sshd_config_decoscript.backup" ]; then + if has_ssh_config_backup; then echo "9) Restore SSH Configuration" fi @@ -49,7 +124,7 @@ show_menu() { r|R) revert_static_ip ;; u|U) check_for_updates ;; 9) - if [ -f "/etc/ssh/sshd_config_decoscript.backup" ]; then + if has_ssh_config_backup; then restore_ssh_config else echo "Invalid choice. Please select a valid option." @@ -78,34 +153,17 @@ show_menu() { esac } -# Function to check if the script has sudo privileges -check_sudo_privileges() { - sudo -n true - if [ $? -ne 0 ]; then - echo "You need sudo privilege to run this script." - exit 1 - fi -} +# ============================================================================== +# Update System +# ============================================================================== -# Bash-native CIDR to netmask conversion -cidr_to_netmask() { - local bits=$1 - local mask=$((0xffffffff ^ ((1 << (32 - bits)) - 1))) - printf "%d.%d.%d.%d\n" \ - $(((mask >> 24) & 0xff)) \ - $(((mask >> 16) & 0xff)) \ - $(((mask >> 8) & 0xff)) \ - $((mask & 0xff)) -} - -# Function to check for script updates check_for_updates() { clear echo "Checking for script updates..." echo # Check if curl is available - if ! command -v curl >/dev/null 2>&1; then + if ! command_exists curl; then echo "curl is not installed. Cannot check for updates." echo "Install curl to enable update checking: sudo apt install curl" return 0 @@ -182,82 +240,95 @@ update_script() { fi } -# Function to install essential apps using dialog +# ============================================================================== +# Essential Apps +# ============================================================================== + install_essential_apps() { clear - # Check if dialog is installed, and if not, install it - if ! [ -x "$(command -v dialog)" ]; then + if ! command_exists dialog; then echo "Dialog is not installed. Installing dialog..." - sudo apt update && sudo apt install dialog -y - - # Check if the installation was successful - if [ $? -ne 0 ]; then + if ! apt_install dialog; then echo "Error: Failed to install dialog. Exiting." return fi fi - if ! [ -x "$(command -v curl)" ]; then + if ! command_exists curl; then echo "Curl is not installed. Installing curl..." - sudo apt update && sudo apt install curl -y - - # Check if the installation was successful - if [ $? -ne 0 ]; then + if ! apt_install curl; then echo "Error: Failed to install curl. Exiting." return fi fi - # Define the dialog menu options - app_options=("1" "htop - Interactive process viewer" off - "2" "screen - Terminal multiplexer" off - "3" "nload - Network traffic monitor" off - "4" "nano - Text editor" off - "5" "firewalld - Firewall management" off - "6" "fail2ban - Intrusion prevention system" off - "7" "unattended-upgrades - Automatic updates" off - "8" "git - Version control system" off - "9" "pi-hole - Ad blocker and DHCP server" off) + local app_options=( + "htop" "Process monitor - htop" off + "btop" "Process monitor - btop" off + "screen" "Terminal multiplexer - screen" off + "tmux" "Terminal multiplexer - tmux" off + "nload" "Network traffic monitor" off + "nano" "Text editor - nano" off + "neovim" "Text editor - Neovim" off + "firewalld" "Firewall management" off + "fail2ban" "Intrusion prevention system" off + "unattended-upgrades" "Automatic updates" off + "git" "Version control system" off + "pi-hole" "Ad blocker and DHCP server" off + ) + local selected_packages=() + local choices + local choice + local install_pihole=false + local configure_firewalld=false + local configure_fail2ban_after_install=false + local configure_unattended_upgrades=false + local configure_git_after_install=false - # Display the dialog menu and store the user's choices choices=$(dialog --clear --title "Essential Apps Installer" --checklist "Choose which apps to install:" 0 0 0 "${app_options[@]}" 2>&1 >/dev/tty) - # Check if the user canceled or made no selection if [ $? -ne 0 ]; then clear echo "Canceled. Returning to the main menu." return fi - # Strip quotes from dialog output choices=$(echo "$choices" | tr -d '"') - # Process user choices and install selected apps - selected_applications="" - for choice in $choices; do case $choice in - 1) selected_applications+=" htop" ;; - 2) selected_applications+=" screen" ;; - 3) selected_applications+=" nload" ;; - 4) selected_applications+=" nano" ;; - 5) selected_applications+=" firewalld" ;; - 6) selected_applications+=" fail2ban" ;; - 7) selected_applications+=" unattended-upgrades" ;; - 8) selected_applications+=" git" ;; - 9) selected_applications+=" pi-hole" ;; + htop|btop|screen|tmux|nload|nano|neovim) + selected_packages+=("$choice") + ;; + firewalld) + selected_packages+=("firewalld") + configure_firewalld=true + ;; + fail2ban) + selected_packages+=("fail2ban") + configure_fail2ban_after_install=true + ;; + unattended-upgrades) + selected_packages+=("unattended-upgrades") + configure_unattended_upgrades=true + ;; + git) + selected_packages+=("git") + configure_git_after_install=true + ;; + pi-hole) + install_pihole=true + ;; esac done - if [ -z "$selected_applications" ]; then + if [ ${#selected_packages[@]} -eq 0 ] && ! $install_pihole; then echo "No apps selected. Returning to the main menu." return fi - # Check if Pi-hole was selected - if [[ "$selected_applications" == *"pi-hole"* ]]; then - # Pi-hole installation + if $install_pihole; then echo "Installing Pi-hole..." curl -sSL https://install.pi-hole.net | bash if [ $? -ne 0 ]; then @@ -266,52 +337,39 @@ install_essential_apps() { fi fi - # Remove Pi-hole from the list of selected applications - selected_applications="${selected_applications//pi-hole/}" - - # Check if there are any remaining selected applications - if [ -z "$selected_applications" ]; then - echo "No apps selected. Returning to the main menu." - return - fi - - # Install the remaining selected applications using apt - echo "Installing selected apps: $selected_applications" - sudo apt update && sudo apt install $selected_applications -y - - # Check if there was an error during installation - if [ $? -ne 0 ]; then - echo "Error: Failed to install some or all of the selected apps. Please check your internet connection and try again." - return + if [ ${#selected_packages[@]} -gt 0 ]; then + echo "Installing selected apps: ${selected_packages[*]}" + if ! apt_install "${selected_packages[@]}"; then + echo "Error: Failed to install some or all of the selected apps. Please check your internet connection and try again." + return + fi fi - # Check if firewalld was selected - if [[ "$selected_applications" == *"firewalld"* ]]; then + if $configure_firewalld; then configure_firewall fi - # Check if Fail2ban was selected - if [[ "$selected_applications" == *"fail2ban"* ]]; then + if $configure_fail2ban_after_install; then configure_fail2ban fi - # Check if unattended-upgrades was selected - if [[ "$selected_applications" == *"unattended-upgrades"* ]]; then + if $configure_unattended_upgrades; then sudo dpkg-reconfigure -plow unattended-upgrades fi - # Configure Git only if it was selected - if [[ "$selected_applications" == *"git"* ]]; then + if $configure_git_after_install; then configure_git fi echo "Installation complete." - return } -# Firewalld helpers +# ============================================================================== +# Firewalld +# ============================================================================== + ensure_firewalld_installed() { - command -v firewall-cmd &>/dev/null + command_exists firewall-cmd } is_firewalld_running() { @@ -331,41 +389,89 @@ is_armbian_system() { return 1 } +ensure_firewalld_iptables_dependencies() { + local missing_packages=() + + if ! package_installed iptables; then + missing_packages+=("iptables") + fi + + if ! package_installed ipset; then + missing_packages+=("ipset") + fi + + if [ ${#missing_packages[@]} -eq 0 ]; then + return 0 + fi + + echo "Installing firewalld iptables backend dependencies: ${missing_packages[*]}" + apt_install "${missing_packages[@]}" +} + set_firewalld_iptables_backend() { - local firewalld_conf="/etc/firewalld/firewalld.conf" - local backup_name="${firewalld_conf}.decoscript.backup.$(date +%Y%m%d%H%M%S)" + local backup_name="${FIREWALLD_CONF}.decoscript.backup.$(date +%Y%m%d%H%M%S)" + local vendor_conf="" echo "Trying firewalld iptables backend fallback..." - if [ ! -f "$firewalld_conf" ]; then - if ! sudo mkdir -p "$(dirname "$firewalld_conf")"; then + if ! ensure_firewalld_iptables_dependencies; then + echo "Error: Failed to install firewalld iptables backend dependencies." + return 1 + fi + + if [ -f "/usr/lib/firewalld/firewalld.conf" ]; then + vendor_conf="/usr/lib/firewalld/firewalld.conf" + elif [ -f "/lib/firewalld/firewalld.conf" ]; then + vendor_conf="/lib/firewalld/firewalld.conf" + fi + + if [ ! -f "$FIREWALLD_CONF" ]; then + if ! sudo mkdir -p "$(dirname "$FIREWALLD_CONF")"; then echo "Error: Failed to create /etc/firewalld." return 1 fi - if ! echo "FirewallBackend=iptables" | sudo tee "$firewalld_conf" >/dev/null; then - echo "Error: Failed to create $firewalld_conf." + if [ -n "$vendor_conf" ]; then + if ! sudo cp "$vendor_conf" "$FIREWALLD_CONF"; then + echo "Error: Failed to copy default firewalld config from $vendor_conf." + return 1 + fi + else + if ! echo "FirewallBackend=nftables" | sudo tee "$FIREWALLD_CONF" >/dev/null; then + echo "Error: Failed to create $FIREWALLD_CONF." + return 1 + fi + fi + elif [ -n "$vendor_conf" ] && ! sudo grep -q "^DefaultZone=" "$FIREWALLD_CONF"; then + if ! sudo cp "$FIREWALLD_CONF" "$backup_name"; then + echo "Error: Failed to back up incomplete $FIREWALLD_CONF." return 1 fi - echo "Created $firewalld_conf with FirewallBackend=iptables." - return 0 + if ! sudo cp "$vendor_conf" "$FIREWALLD_CONF"; then + echo "Error: Failed to restore default firewalld config from $vendor_conf." + return 1 + fi + + echo "Replaced incomplete $FIREWALLD_CONF. Backup saved as: $backup_name" fi - if sudo grep -q "^FirewallBackend=iptables$" "$firewalld_conf"; then + if sudo grep -q "^FirewallBackend=iptables$" "$FIREWALLD_CONF"; then echo "Firewalld already uses the iptables backend." return 0 fi - if ! sudo cp "$firewalld_conf" "$backup_name"; then - echo "Error: Failed to back up $firewalld_conf." - return 1 + if [ ! -f "$backup_name" ]; then + if ! sudo cp "$FIREWALLD_CONF" "$backup_name"; then + echo "Error: Failed to back up $FIREWALLD_CONF." + return 1 + fi fi - if sudo grep -q "^#\\?FirewallBackend=" "$firewalld_conf"; then - sudo sed -i "s/^#\\?FirewallBackend=.*/FirewallBackend=iptables/" "$firewalld_conf" + if sudo grep -q "^#\\?FirewallBackend=" "$FIREWALLD_CONF"; then + sudo sed -i "s/^#\\?FirewallBackend=.*/FirewallBackend=iptables/" "$FIREWALLD_CONF" else - echo "FirewallBackend=iptables" | sudo tee -a "$firewalld_conf" >/dev/null + echo "FirewallBackend=iptables" | sudo tee -a "$FIREWALLD_CONF" >/dev/null fi if [ $? -ne 0 ]; then @@ -373,7 +479,7 @@ set_firewalld_iptables_backend() { return 1 fi - echo "Updated $firewalld_conf. Backup saved as: $backup_name" + echo "Updated $FIREWALLD_CONF. Backup saved as: $backup_name" } start_firewalld() { @@ -438,7 +544,7 @@ add_firewalld_tcp_port() { return $? fi - if ! command -v firewall-offline-cmd &>/dev/null; then + if ! command_exists firewall-offline-cmd; then echo "Error: firewalld is not running and firewall-offline-cmd is not available." return 1 fi @@ -452,7 +558,33 @@ add_firewalld_tcp_port() { sudo firewall-offline-cmd --zone=public --add-port="$port"/tcp } -# Function to configure the firewall with checks +configure_firewalld_ports() { + local firewalld_was_running=false + local port + + if ! ensure_firewalld_installed; then + echo "Firewalld is not installed. Skipping." + return 0 + fi + + if is_firewalld_running; then + firewalld_was_running=true + fi + + for port in "$@"; do + if ! add_firewalld_tcp_port "$port"; then + echo "Warning: Failed to open port $port [TCP]." + fi + done + + if $firewalld_was_running; then + echo "Reload configuration..." + sudo firewall-cmd --reload + else + echo "Firewalld is not running. Rules were saved and will apply when it starts." + fi +} + configure_firewall() { clear @@ -506,20 +638,25 @@ configure_firewall() { echo } -# Function to set up SSH key-based authentication +# ============================================================================== +# SSH +# ============================================================================== + setup_ssh_key_authentication() { clear # Check if SSH service is installed - if ! dpkg -l | grep -q "openssh-server"; then + if ! package_installed openssh-server; then echo "SSH service (openssh-server) is not installed." # Ask the user if they want to install SSH service read -rp "Do you want to install SSH service? (Y/n): " install_ssh_service if [[ "$install_ssh_service" =~ ^[Yy]$ || "$install_ssh_service" == "" ]]; then - sudo apt update - sudo apt install openssh-server -y + if ! apt_install openssh-server; then + echo "Error: Failed to install openssh-server." + return + fi else echo "SSH service will not be installed. Returning to the main menu." return @@ -530,17 +667,17 @@ setup_ssh_key_authentication() { # Backup with max 5 kept local max_backups=5 - local backup_name="/etc/ssh/sshd_config_decoscript.backup.$(date +%Y%m%d%H%M%S)" + local backup_name="${SSH_CONFIG}_decoscript.backup.$(date +%Y%m%d%H%M%S)" # Create a backup of the sshd_config file - sudo cp /etc/ssh/sshd_config "$backup_name" + sudo cp "$SSH_CONFIG" "$backup_name" echo "Backup created: $backup_name" # Clean old backups, keep only the most recent $max_backups - local backup_count=$(ls -1t /etc/ssh/sshd_config_decoscript.backup.* 2>/dev/null | wc -l) + local backup_count=$(ls -1t $SSH_BACKUP_PATTERN 2>/dev/null | wc -l) if [ "$backup_count" -gt "$max_backups" ]; then echo "Cleaning old backups (keeping $max_backups most recent)..." - ls -1t /etc/ssh/sshd_config_decoscript.backup.* 2>/dev/null | tail -n +$((max_backups + 1)) | xargs -r sudo rm -f + ls -1t $SSH_BACKUP_PATTERN 2>/dev/null | tail -n +$((max_backups + 1)) | xargs -r sudo rm -f fi echo "#######################################################" @@ -583,11 +720,11 @@ setup_ssh_key_authentication() { fi # Enable key-based authentication and disable password-based authentication for SSH - sudo sed -i 's/^#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config - sudo sed -i 's/^PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config - sudo sed -i 's/^#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config - sudo sed -i 's/^#PubkeyAuthentication no/PubkeyAuthentication yes/' /etc/ssh/sshd_config - sudo sed -i 's/^PubkeyAuthentication no/PubkeyAuthentication yes/' /etc/ssh/sshd_config + sudo sed -i 's/^#PasswordAuthentication yes/PasswordAuthentication no/' "$SSH_CONFIG" + sudo sed -i 's/^PasswordAuthentication yes/PasswordAuthentication no/' "$SSH_CONFIG" + sudo sed -i 's/^#PubkeyAuthentication yes/PubkeyAuthentication yes/' "$SSH_CONFIG" + sudo sed -i 's/^#PubkeyAuthentication no/PubkeyAuthentication yes/' "$SSH_CONFIG" + sudo sed -i 's/^PubkeyAuthentication no/PubkeyAuthentication yes/' "$SSH_CONFIG" # Restart the SSH service for changes to take effect sudo service ssh restart @@ -602,7 +739,53 @@ setup_ssh_key_authentication() { echo } -# Function to enable passwordless sudo access +restore_ssh_config() { + clear + + local backup + backup=$(latest_ssh_config_backup) + + if [ -z "$backup" ]; then + echo "Error: No backup files found matching $SSH_BACKUP_PATTERN" + return + fi + + echo "Found backup: $backup" + read -rp "Do you want to restore SSH configuration from this backup? (y/N): " confirm_restore + + if [[ ! "$confirm_restore" =~ ^[Yy]$ ]]; then + echo "Restore cancelled." + return + fi + + echo "Restoring SSH configuration..." + sudo cp "$backup" "$SSH_CONFIG" + if [ $? -ne 0 ]; then + echo "Error: Failed to restore SSH configuration." + exit 1 + fi + + sudo service ssh restart + if [ $? -ne 0 ]; then + echo "Error: Failed to restart the SSH service." + exit 1 + fi + + echo "SSH configuration has been restored." + + read -rp "Do you want to keep the backup file? (Y/n): " keep_backup + if [[ "$keep_backup" =~ ^[Nn]$ ]]; then + sudo rm "$backup" + echo "Backup file deleted." + else + echo "Backup file kept at: $backup" + fi +} + +# ============================================================================== +# Passwordless Sudo +# ============================================================================== + enable_passwordless_sudo() { clear local username="$1" @@ -629,7 +812,10 @@ enable_passwordless_sudo() { echo } -# Function to install NGINX and PHP with firewall checks +# ============================================================================== +# Web Server +# ============================================================================== + install_nginx_and_php() { clear @@ -686,43 +872,19 @@ install_nginx_and_php() { echo "#######################################################" echo "Firewall configuration" echo "#######################################################" - - if ! ensure_firewalld_installed; then - echo "Firewalld is not installed. Skipping." - - else - firewalld_was_running=false - if is_firewalld_running; then - firewalld_was_running=true - fi - - if ! add_firewalld_tcp_port 80; then - echo "Warning: Failed to open port 80 [TCP]." - fi - - if ! add_firewalld_tcp_port 443; then - echo "Warning: Failed to open port 443 [TCP]." - fi - - if $firewalld_was_running; then - echo "Reload configuration..." - sudo firewall-cmd --reload - else - echo "Firewalld is not running. Rules were saved and will apply when it starts." - fi - echo - fi + configure_firewalld_ports 80 443 + echo # Create a directory for SSL certs if it doesn't exist - if [ ! -d "/etc/nginx/cert" ]; then - echo "Creating directory /etc/nginx/cert" - sudo mkdir -p /etc/nginx/cert - sudo chmod 700 /etc/nginx/cert + if [ ! -d "$NGINX_CERT_DIR" ]; then + echo "Creating directory $NGINX_CERT_DIR" + sudo mkdir -p "$NGINX_CERT_DIR" + sudo chmod 700 "$NGINX_CERT_DIR" fi echo echo "Finished setting up NGINX and PHP." - echo "You can upload SSL certificates into /etc/nginx/cert" + echo "You can upload SSL certificates into $NGINX_CERT_DIR" echo } @@ -800,9 +962,9 @@ install_lemp_stack() { echo " - PHP with PHP-FPM" echo echo "Next steps:" - echo " 1. Place your website files in /var/www/html/" + echo " 1. Place your website files in $WEB_ROOT/" echo " 2. Configure NGINX sites in /etc/nginx/sites-available/" - echo " 3. SSL certificates go in /etc/nginx/cert/" + echo " 3. SSL certificates go in $NGINX_CERT_DIR/" echo " 4. MySQL: sudo mysql -u root -p" echo "═══════════════════════════════════════════════════════" read -rp "Press Enter to continue..." @@ -829,9 +991,9 @@ install_nginx_php() { echo " - PHP with PHP-FPM" echo echo "Next steps:" - echo " 1. Place your website files in /var/www/html/" + echo " 1. Place your website files in $WEB_ROOT/" echo " 2. Configure NGINX sites in /etc/nginx/sites-available/" - echo " 3. SSL certificates go in /etc/nginx/cert/" + echo " 3. SSL certificates go in $NGINX_CERT_DIR/" echo "═══════════════════════════════════════════════════════" read -rp "Press Enter to continue..." } @@ -854,9 +1016,9 @@ install_nginx_only() { echo " - NGINX web server" echo echo "Next steps:" - echo " 1. Place your static files in /var/www/html/" + echo " 1. Place your static files in $WEB_ROOT/" echo " 2. Configure NGINX sites in /etc/nginx/sites-available/" - echo " 3. SSL certificates go in /etc/nginx/cert/" + echo " 3. SSL certificates go in $NGINX_CERT_DIR/" echo "═══════════════════════════════════════════════════════" read -rp "Press Enter to continue..." } @@ -918,8 +1080,8 @@ configure_nginx_php() { echo "Configuring NGINX for PHP..." # Backup default config - if [ -f /etc/nginx/sites-available/default ]; then - sudo cp /etc/nginx/sites-available/default /etc/nginx/sites-available/default.backup + if [ -f "$NGINX_DEFAULT_SITE" ]; then + sudo cp "$NGINX_DEFAULT_SITE" "${NGINX_DEFAULT_SITE}.backup" fi # Detect PHP-FPM socket @@ -931,12 +1093,12 @@ configure_nginx_php() { fi # Create a working default config - sudo tee /etc/nginx/sites-available/default >/dev/null </dev/null <" | sudo tee /var/www/html/info.php >/dev/null + echo "" | sudo tee "$WEB_ROOT/info.php" >/dev/null echo echo "Test PHP installation: http://your-server-ip/info.php" - echo "Remember to delete /var/www/html/info.php after testing!" + echo "Remember to delete $WEB_ROOT/info.php after testing!" } # Helper: Configure NGINX for static files only @@ -977,17 +1139,17 @@ configure_nginx_static() { echo "Configuring NGINX for static content..." # Backup default config - if [ -f /etc/nginx/sites-available/default ]; then - sudo cp /etc/nginx/sites-available/default /etc/nginx/sites-available/default.backup + if [ -f "$NGINX_DEFAULT_SITE" ]; then + sudo cp "$NGINX_DEFAULT_SITE" "${NGINX_DEFAULT_SITE}.backup" fi # Create a clean static config - sudo tee /etc/nginx/sites-available/default >/dev/null </dev/null </dev/null </dev/null < @@ -1047,38 +1209,21 @@ finalize_web_server_install() { EOF - sudo chown www-data:www-data /var/www/html/index.html + sudo chown www-data:www-data "$WEB_ROOT/index.html" fi # Configure firewall if available - if ensure_firewalld_installed; then - echo - echo "Configuring firewall..." - - firewalld_was_running=false - if is_firewalld_running; then - firewalld_was_running=true - fi - - if ! add_firewalld_tcp_port 80; then - echo "Warning: Failed to open port 80 (HTTP)." - fi - - if ! add_firewalld_tcp_port 443; then - echo "Warning: Failed to open port 443 (HTTPS)." - fi - - if $firewalld_was_running; then - sudo firewall-cmd --reload - else - echo "Firewalld is not running. Rules were saved and will apply when it starts." - fi - fi + echo + echo "Configuring firewall..." + configure_firewalld_ports 80 443 echo "Setup complete!" } -# Function to install Node Version Manager (NVM) +# ============================================================================== +# NVM +# ============================================================================== + install_nvm() { clear # Using master branch to always get latest NVM @@ -1102,62 +1247,10 @@ install_nvm() { echo } -# Function to restore SSH configuration -restore_ssh_config() { - clear - - # Find the most recent backup - local backup - backup=$(ls -1t /etc/ssh/sshd_config_decoscript.backup.* 2>/dev/null | head -n1) - - if [ -z "$backup" ]; then - echo "Error: No backup files found matching /etc/ssh/sshd_config_decoscript.backup.*" - return - fi - - echo "Found backup: $backup" - read -rp "Do you want to restore SSH configuration from this backup? (y/N): " confirm_restore - - if [[ ! "$confirm_restore" =~ ^[Yy]$ ]]; then - echo "Restore cancelled." - return - fi - - echo "Restoring SSH configuration..." - sudo cp "$backup" /etc/ssh/sshd_config - if [ $? -ne 0 ]; then - echo "Error: Failed to restore SSH configuration." - exit 1 - fi - - sudo service ssh restart - if [ $? -ne 0 ]; then - echo "Error: Failed to restart the SSH service." - exit 1 - fi +# ============================================================================== +# Git +# ============================================================================== - echo "SSH configuration has been restored." - - read -rp "Do you want to keep the backup file? (Y/n): " keep_backup - if [[ "$keep_backup" =~ ^[Nn]$ ]]; then - sudo rm "$backup" - echo "Backup file deleted." - else - echo "Backup file kept at: $backup" - fi -} - -# Function to validate if a given input is a valid port number -validate_port() { - local port="$1" - if ! [[ "$port" =~ ^[0-9]+$ ]] || ((port < 1 || port > 65535)); then - echo "Error: Invalid port number. Please enter a valid numeric port between 1 and 65535." - return 1 # Invalid port - fi - return 0 # Valid port -} - -# Function to configure Git configure_git() { while true; do clear @@ -1221,7 +1314,10 @@ configure_git_user() { echo "Git has been configured with name: $git_name, email: $git_email, and default branch: $git_default_branch." } -# Function to configure fail2ban +# ============================================================================== +# Fail2ban +# ============================================================================== + configure_fail2ban() { clear echo "Choose the Fail2ban configuration to use:" @@ -1260,21 +1356,34 @@ configure_fail2ban() { echo "Fail2ban configuration completed." } -# Function to configure a static IP address using Netplan +# ============================================================================== +# Static IP / Netplan +# ============================================================================== + +cidr_to_netmask() { + local bits=$1 + local mask=$((0xffffffff ^ ((1 << (32 - bits)) - 1))) + printf "%d.%d.%d.%d\n" \ + $(((mask >> 24) & 0xff)) \ + $(((mask >> 16) & 0xff)) \ + $(((mask >> 8) & 0xff)) \ + $((mask & 0xff)) +} + configure_static_ip() { clear echo "Configuring a static IP address using Netplan." # Check if Netplan is installed, and if not, install it - if ! [ -x "$(command -v netplan)" ]; then + if ! command_exists netplan; then echo "Netplan is not installed. Installing..." - sudo apt update && sudo apt install netplan.io -y + apt_install netplan.io fi # Check if ifconfig is installed, and if not, install it - if ! [ -x "$(command -v ifconfig)" ]; then + if ! command_exists ifconfig; then echo "Ifconfig (net-tools) is not installed. Installing..." - sudo apt update && sudo apt install net-tools -y + apt_install net-tools fi # Get network device information from 'ifconfig -a' @@ -1313,8 +1422,8 @@ configure_static_ip() { fi local backup_timestamp=$(date +%Y%m%d%H%M%S) - if [ -f "/etc/netplan/01-network-manager-all.yaml" ]; then - sudo cp "/etc/netplan/01-network-manager-all.yaml" "$backup_dir/01-network-manager-all.yaml.$backup_timestamp" + if [ -f "$NETPLAN_CONFIG" ]; then + sudo cp "$NETPLAN_CONFIG" "$backup_dir/01-network-manager-all.yaml.$backup_timestamp" echo "Backup created: $backup_dir/01-network-manager-all.yaml.$backup_timestamp" fi @@ -1328,7 +1437,7 @@ configure_static_ip() { fi # Create a Netplan configuration file for the static IP address - cat </dev/null + cat </dev/null network: version: 2 renderer: $renderer @@ -1343,7 +1452,7 @@ network: EOL # Set correct permissions (netplan requires 600 or 640) - sudo chmod 600 /etc/netplan/01-network-manager-all.yaml + sudo chmod 600 "$NETPLAN_CONFIG" echo "Set netplan file permissions to 600 (owner read/write only)." # Apply the Netplan configuration @@ -1359,14 +1468,14 @@ revert_static_ip() { echo "========================================" # Check if the netplan file exists - if [ ! -f "/etc/netplan/01-network-manager-all.yaml" ]; then - echo "No static IP configuration found at /etc/netplan/01-network-manager-all.yaml" + if [ ! -f "$NETPLAN_CONFIG" ]; then + echo "No static IP configuration found at $NETPLAN_CONFIG" echo "Nothing to revert." return fi echo "This will remove the static IP configuration and revert to DHCP." - echo "Current configuration file: /etc/netplan/01-network-manager-all.yaml" + echo "Current configuration file: $NETPLAN_CONFIG" echo read -rp "Do you want to proceed? (y/N): " confirm_revert @@ -1376,7 +1485,7 @@ revert_static_ip() { fi # Get network device information - if [ -x "$(command -v ifconfig)" ]; then + if command_exists ifconfig; then device_info=$(sudo ifconfig -a) echo "Available network devices:" echo "$device_info" @@ -1399,7 +1508,7 @@ revert_static_ip() { fi local backup_timestamp=$(date +%Y%m%d%H%M%S) - sudo cp "/etc/netplan/01-network-manager-all.yaml" "$backup_dir/01-network-manager-all.yaml.$backup_timestamp" + sudo cp "$NETPLAN_CONFIG" "$backup_dir/01-network-manager-all.yaml.$backup_timestamp" echo "Backup created: $backup_dir/01-network-manager-all.yaml.$backup_timestamp" # Determine renderer @@ -1412,7 +1521,7 @@ revert_static_ip() { fi # Create DHCP configuration - cat </dev/null + cat </dev/null network: version: 2 renderer: $renderer @@ -1423,7 +1532,7 @@ network: EOL # Set correct permissions - sudo chmod 600 /etc/netplan/01-network-manager-all.yaml + sudo chmod 600 "$NETPLAN_CONFIG" # Apply the configuration echo "Applying DHCP configuration..." @@ -1440,7 +1549,10 @@ EOL fi } -# Main script +# ============================================================================== +# Main +# ============================================================================== + check_sudo_privileges while true; do From 7b63157edf9455bcf1fcf0dde35a77380e504d21 Mon Sep 17 00:00:00 2001 From: Decaded Date: Thu, 18 Jun 2026 13:19:38 +0200 Subject: [PATCH 2/4] Refactor the script and add essential app alternatives This refactor also Closes #7 --- .github/workflows/shellcheck.yml | 23 ++ .vscode/settings.json | 8 +- README.md | 21 +- install.sh | 408 ++++++++++--------------------- 4 files changed, 165 insertions(+), 295 deletions(-) create mode 100644 .github/workflows/shellcheck.yml diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml new file mode 100644 index 0000000..262a05f --- /dev/null +++ b/.github/workflows/shellcheck.yml @@ -0,0 +1,23 @@ +name: Shell checks + +on: + push: + pull_request: + +jobs: + shellcheck: + name: Bash syntax and ShellCheck + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Install ShellCheck + run: sudo apt-get update && sudo apt-get install -y shellcheck + + - name: Check Bash syntax + run: bash -n install.sh + + - name: Run ShellCheck + run: shellcheck install.sh diff --git a/.vscode/settings.json b/.vscode/settings.json index 7f7771f..c240b0a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,13 +1,19 @@ { "cSpell.words": [ + "Armbian", + "btop", "decoscript", "ethernets", "fastcgi", + "journalctl", "LEMP", "mbstring", + "netfilter", "Netplan", "networkd", + "nftables", "NOPASSWD", - "phpinfo" + "phpinfo", + "pihole" ] } diff --git a/README.md b/README.md index 656487c..3ec7718 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ### Overview -This script is a modular server utility tool for Debian-based and Ubuntu-based systems. It helps automate common setup tasks without forcing a specific stack. You choose exactly +This script is a menu-driven server utility tool for Debian-based and Ubuntu-based systems. It helps automate common setup tasks without forcing a specific stack. You choose exactly what gets installed: essential tools, Nginx, PHP, NVM, static IP profiles, and more. Ubuntu is the primary target environment. Other Debian derivatives should work, but if you encounter issues, feel free to open an @@ -45,6 +45,8 @@ Ubuntu is the primary target environment. Other Debian derivatives should work, ./install.sh ``` +The script requires sudo privileges. If your sudo session is not already active, run `sudo -v` first. + A menu will appear with all available options.
@@ -66,7 +68,7 @@ Install a curated pack of common system utilities: 5. **[firewalld](https://firewalld.org/)** – firewall management - Automatically opens SSH - - Migrates from UFW if needed + - Checks for working netfilter support before starting firewalld 6. **[fail2ban](https://github.com/fail2ban/fail2ban)** – intrusion prevention @@ -77,6 +79,7 @@ Install a curated pack of common system utilities: - Optional first-time setup 8. **[unattended-upgrades](https://wiki.debian.org/UnattendedUpgrades)** – automatic security updates +9. **[Pi-hole](https://pi-hole.net/)** – ad blocker and optional DHCP server ### SSH Configuration @@ -89,10 +92,10 @@ Switch to secure, key-only SSH authentication. The script: Backup file location: ``` bash -/etc/ssh/sshd_config_decoscript.backup +/etc/ssh/sshd_config_decoscript.backup.* ``` -Re-running the script replaces the old backup, so rename it if you want to keep multiple versions. +The script keeps the five most recent SSH config backups and offers a restore option from the main menu when a backup exists. ### Passwordless Sudo @@ -106,7 +109,7 @@ Enables password-free sudo access if desired. If your system already uses this c Installs the full **LEMP** stack: - **[Nginx](https://nginx.org/)** installation and configuration -- **[MySQL](https://www.mysql.com/)** installation and secure setup +- **[MySQL](https://www.mysql.com/)** installation with optional `mysql_secure_installation` - **[PHP](https://www.php.net/)** installation with commonly used modules - Configures **php-fpm** to work with Nginx @@ -125,10 +128,10 @@ Installs the full **LEMP** stack: ### Node.js via NVM -Installs the latest **[NVM](https://github.com/nvm-sh/nvm)** version and lets you manage Node.js installations cleanly: +Installs the latest **[NVM](https://github.com/nvm-sh/nvm)** version and lets you choose a Node.js version to install: -- Install or remove Node.js versions -- Switch between versions +- Lists recent remote Node.js versions +- Installs the version you enter ### Static IP Configuration @@ -141,7 +144,7 @@ Supports: - Gateway - DNS servers -If Netplan isn’t present, the script chooses the best available method. +If Netplan isn’t present, the script installs `netplan.io` before writing the configuration. A revert option is available from the main menu. --- diff --git a/install.sh b/install.sh index ab70f23..f29f774 100644 --- a/install.sh +++ b/install.sh @@ -32,7 +32,6 @@ SCRIPT_URL="https://raw.githubusercontent.com/Decaded/install-script/refs/heads/ SSH_CONFIG="/etc/ssh/sshd_config" SSH_BACKUP_PATTERN="/etc/ssh/sshd_config_decoscript.backup.*" -FIREWALLD_CONF="/etc/firewalld/firewalld.conf" NETPLAN_CONFIG="/etc/netplan/01-network-manager-all.yaml" NGINX_DEFAULT_SITE="/etc/nginx/sites-available/default" NGINX_CERT_DIR="/etc/nginx/cert" @@ -59,8 +58,7 @@ apt_install() { } check_sudo_privileges() { - sudo -n true - if [ $? -ne 0 ]; then + if ! sudo -n true; then echo "You need sudo privilege to run this script." exit 1 fi @@ -80,7 +78,10 @@ has_ssh_config_backup() { } latest_ssh_config_backup() { - ls -1t $SSH_BACKUP_PATTERN 2>/dev/null | head -n1 + find /etc/ssh -maxdepth 1 -name "sshd_config_decoscript.backup.*" -printf "%T@ %p\n" 2>/dev/null | + sort -rn | + head -n1 | + cut -d' ' -f2- } # ============================================================================== @@ -286,9 +287,7 @@ install_essential_apps() { local configure_unattended_upgrades=false local configure_git_after_install=false - choices=$(dialog --clear --title "Essential Apps Installer" --checklist "Choose which apps to install:" 0 0 0 "${app_options[@]}" 2>&1 >/dev/tty) - - if [ $? -ne 0 ]; then + if ! choices=$(dialog --clear --title "Essential Apps Installer" --checklist "Choose which apps to install:" 0 0 0 "${app_options[@]}" 2>&1 >/dev/tty); then clear echo "Canceled. Returning to the main menu." return @@ -330,8 +329,7 @@ install_essential_apps() { if $install_pihole; then echo "Installing Pi-hole..." - curl -sSL https://install.pi-hole.net | bash - if [ $? -ne 0 ]; then + if ! curl -sSL https://install.pi-hole.net | bash; then echo "Error: Failed to install Pi-hole. Please check your internet connection and try again." return fi @@ -376,115 +374,53 @@ is_firewalld_running() { sudo firewall-cmd --state >/dev/null 2>&1 } -is_armbian_system() { - if [ -f "/etc/armbian-release" ]; then +# Best-effort: ensure the nf_tables kernel module is loaded before starting +# firewalld. On some minimal SBC/Armbian images the module exists but is not +# auto-loaded, which makes firewalld fail at startup with +# "cache initialization failed: Invalid argument". Harmless no-op when the +# module is already loaded; does nothing when the kernel lacks it entirely. +ensure_netfilter_modules() { + if lsmod 2>/dev/null | grep -q '^nf_tables'; then return 0 fi - if [ -f "/etc/os-release" ]; then - grep -qiE '^(ID|ID_LIKE|NAME|PRETTY_NAME)=.*armbian' /etc/os-release - return $? - fi - - return 1 + sudo modprobe nf_tables >/dev/null 2>&1 } -ensure_firewalld_iptables_dependencies() { - local missing_packages=() +# Probe whether the running kernel can actually service firewalld's backend. +# firewalld uses the nftables backend by default on Debian/Armbian, so if the +# 'nft' tool cannot even read the ruleset the kernel lacks working netfilter +# support and firewalld will never start. Returns 0 if usable, 1 otherwise. +check_netfilter_support() { + ensure_netfilter_modules - if ! package_installed iptables; then - missing_packages+=("iptables") - fi - - if ! package_installed ipset; then - missing_packages+=("ipset") - fi - - if [ ${#missing_packages[@]} -eq 0 ]; then - return 0 + if command_exists nft; then + sudo nft list ruleset >/dev/null 2>&1 + return $? fi - echo "Installing firewalld iptables backend dependencies: ${missing_packages[*]}" - apt_install "${missing_packages[@]}" + # nft not present (unusual): let the firewalld start attempt be the judge. + return 0 } -set_firewalld_iptables_backend() { - local backup_name="${FIREWALLD_CONF}.decoscript.backup.$(date +%Y%m%d%H%M%S)" - local vendor_conf="" - - echo "Trying firewalld iptables backend fallback..." - - if ! ensure_firewalld_iptables_dependencies; then - echo "Error: Failed to install firewalld iptables backend dependencies." - return 1 - fi - - if [ -f "/usr/lib/firewalld/firewalld.conf" ]; then - vendor_conf="/usr/lib/firewalld/firewalld.conf" - elif [ -f "/lib/firewalld/firewalld.conf" ]; then - vendor_conf="/lib/firewalld/firewalld.conf" - fi - - if [ ! -f "$FIREWALLD_CONF" ]; then - if ! sudo mkdir -p "$(dirname "$FIREWALLD_CONF")"; then - echo "Error: Failed to create /etc/firewalld." - return 1 - fi - - if [ -n "$vendor_conf" ]; then - if ! sudo cp "$vendor_conf" "$FIREWALLD_CONF"; then - echo "Error: Failed to copy default firewalld config from $vendor_conf." - return 1 - fi - else - if ! echo "FirewallBackend=nftables" | sudo tee "$FIREWALLD_CONF" >/dev/null; then - echo "Error: Failed to create $FIREWALLD_CONF." - return 1 - fi - fi - elif [ -n "$vendor_conf" ] && ! sudo grep -q "^DefaultZone=" "$FIREWALLD_CONF"; then - if ! sudo cp "$FIREWALLD_CONF" "$backup_name"; then - echo "Error: Failed to back up incomplete $FIREWALLD_CONF." - return 1 - fi - - if ! sudo cp "$vendor_conf" "$FIREWALLD_CONF"; then - echo "Error: Failed to restore default firewalld config from $vendor_conf." - return 1 - fi - - echo "Replaced incomplete $FIREWALLD_CONF. Backup saved as: $backup_name" - fi - - if sudo grep -q "^FirewallBackend=iptables$" "$FIREWALLD_CONF"; then - echo "Firewalld already uses the iptables backend." - return 0 - fi - - if [ ! -f "$backup_name" ]; then - if ! sudo cp "$FIREWALLD_CONF" "$backup_name"; then - echo "Error: Failed to back up $FIREWALLD_CONF." - return 1 - fi - fi - - if sudo grep -q "^#\\?FirewallBackend=" "$FIREWALLD_CONF"; then - sudo sed -i "s/^#\\?FirewallBackend=.*/FirewallBackend=iptables/" "$FIREWALLD_CONF" - else - echo "FirewallBackend=iptables" | sudo tee -a "$FIREWALLD_CONF" >/dev/null - fi - - if [ $? -ne 0 ]; then - echo "Error: Failed to set FirewallBackend=iptables." - return 1 - fi - - echo "Updated $FIREWALLD_CONF. Backup saved as: $backup_name" +# Explain that the running kernel cannot run firewalld and how to fix it. +print_unsupported_kernel_recommendation() { + echo + echo "Error: this kernel does not provide working netfilter (nftables) support," + echo "so firewalld cannot be started." + echo + echo " Running kernel: $(uname -r)" + echo + echo "Recommended fix: update the kernel and reboot, then re-run this script." + echo " sudo apt update && sudo apt full-upgrade" + echo " sudo reboot" + echo + echo "On Armbian you can also use 'sudo armbian-update' to move to a kernel" + echo "build that ships nf_tables support." + echo } start_firewalld() { - local backend_fallback_applied=false - echo "Enabling and starting firewalld..." if ! sudo systemctl enable firewalld >/dev/null 2>&1; then @@ -492,42 +428,24 @@ start_firewalld() { return 1 fi - if is_armbian_system; then - echo "Armbian detected. Using firewalld iptables backend for compatibility." - if ! set_firewalld_iptables_backend; then - echo "Error: Failed to configure firewalld backend fallback." - return 1 - fi - backend_fallback_applied=true + # Verify the kernel can actually run a netfilter backend before we try, so we + # can give a clear recommendation instead of a cryptic startup failure. + if ! check_netfilter_support; then + print_unsupported_kernel_recommendation + return 1 fi - if ! sudo systemctl start firewalld >/dev/null 2>&1; then - if $backend_fallback_applied; then - echo "Error: Failed to start firewalld with the iptables backend." - echo "Please check details with: sudo systemctl status firewalld" - return 1 - fi - - echo "Warning: Failed to start firewalld with the default backend." - - if ! set_firewalld_iptables_backend; then - echo "Error: Failed to configure firewalld backend fallback." - return 1 - fi - - sudo systemctl reset-failed firewalld >/dev/null 2>&1 || true - - if ! sudo systemctl start firewalld >/dev/null 2>&1; then - echo "Error: Failed to start firewalld with both nftables and iptables backends." - echo "Please check details with: sudo systemctl status firewalld" - return 1 - fi + # 'restart' (not 'start') re-executes a previously failed unit; confirm the + # daemon is actually responsive via firewall-cmd --state. + if sudo systemctl restart firewalld >/dev/null 2>&1 && is_firewalld_running; then + return 0 fi - if ! sudo firewall-cmd --state >/dev/null 2>&1; then - echo "Error: Firewalld is not running." - return 1 - fi + echo "Error: Failed to start firewalld." + echo "Inspect the failure with:" + echo " sudo systemctl status firewalld" + echo " sudo journalctl -xeu firewalld" + return 1 } add_firewalld_tcp_port() { @@ -604,8 +522,7 @@ configure_firewall() { read -rp "Please provide your current SSH port (default is 22): " sshPort sshPort=${sshPort:-22} - validate_port "$sshPort" - if [ $? -ne 0 ]; then + if ! validate_port "$sshPort"; then echo "Invalid port input. Exiting." exit 1 fi @@ -615,22 +532,19 @@ configure_firewall() { firewalld_was_running=true fi - add_firewalld_tcp_port "$sshPort" - if [ $? -ne 0 ]; then + if ! add_firewalld_tcp_port "$sshPort"; then echo "Error: Failed to open firewall port." exit 1 fi if $firewalld_was_running; then echo "Reload configuration..." - sudo firewall-cmd --reload - if [ $? -ne 0 ]; then + if ! sudo firewall-cmd --reload; then echo "Error: Failed to reload firewall configuration." exit 1 fi else - start_firewalld - if [ $? -ne 0 ]; then + if ! start_firewalld; then exit 1 fi fi @@ -667,17 +581,23 @@ setup_ssh_key_authentication() { # Backup with max 5 kept local max_backups=5 - local backup_name="${SSH_CONFIG}_decoscript.backup.$(date +%Y%m%d%H%M%S)" - + local backup_name + backup_name="${SSH_CONFIG}_decoscript.backup.$(date +%Y%m%d%H%M%S)" + # Create a backup of the sshd_config file sudo cp "$SSH_CONFIG" "$backup_name" echo "Backup created: $backup_name" - + # Clean old backups, keep only the most recent $max_backups - local backup_count=$(ls -1t $SSH_BACKUP_PATTERN 2>/dev/null | wc -l) + local backup_count + backup_count=$(find /etc/ssh -maxdepth 1 -name "sshd_config_decoscript.backup.*" 2>/dev/null | wc -l) if [ "$backup_count" -gt "$max_backups" ]; then echo "Cleaning old backups (keeping $max_backups most recent)..." - ls -1t $SSH_BACKUP_PATTERN 2>/dev/null | tail -n +$((max_backups + 1)) | xargs -r sudo rm -f + find /etc/ssh -maxdepth 1 -name "sshd_config_decoscript.backup.*" -printf "%T@ %p\n" 2>/dev/null | + sort -rn | + tail -n +$((max_backups + 1)) | + cut -d' ' -f2- | + xargs -r sudo rm -f fi echo "#######################################################" @@ -697,9 +617,7 @@ setup_ssh_key_authentication() { # Check if the authorized_keys file exists and the key is not already present if [ -f "$authorized_keys_file" ] && ! grep -q "$ssh_public_key" "$authorized_keys_file"; then - # Save the public key to the authorized_keys file - echo "$ssh_public_key" >>"$authorized_keys_file" - if [ $? -ne 0 ]; then + if ! echo "$ssh_public_key" >>"$authorized_keys_file"; then echo "Error: Failed to save the public key to authorized_keys file." exit 1 fi @@ -707,8 +625,7 @@ setup_ssh_key_authentication() { echo "Public key added to authorized_keys." elif [ ! -f "$authorized_keys_file" ]; then echo "Creating authorized_keys file..." - echo "$ssh_public_key" >"$authorized_keys_file" - if [ $? -ne 0 ]; then + if ! echo "$ssh_public_key" >"$authorized_keys_file"; then echo "Error: Failed to create authorized_keys file." exit 1 fi @@ -727,8 +644,7 @@ setup_ssh_key_authentication() { sudo sed -i 's/^PubkeyAuthentication no/PubkeyAuthentication yes/' "$SSH_CONFIG" # Restart the SSH service for changes to take effect - sudo service ssh restart - if [ $? -ne 0 ]; then + if ! sudo service ssh restart; then echo "Error: Failed to restart the SSH service." exit 1 fi @@ -759,14 +675,12 @@ restore_ssh_config() { fi echo "Restoring SSH configuration..." - sudo cp "$backup" "$SSH_CONFIG" - if [ $? -ne 0 ]; then + if ! sudo cp "$backup" "$SSH_CONFIG"; then echo "Error: Failed to restore SSH configuration." exit 1 fi - sudo service ssh restart - if [ $? -ne 0 ]; then + if ! sudo service ssh restart; then echo "Error: Failed to restart the SSH service." exit 1 fi @@ -798,8 +712,7 @@ enable_passwordless_sudo() { if [[ "$enable_sudo_option" =~ ^[Yy]$ ]]; then # Append to /etc/sudoers using echo and sudo - echo "$username ALL=(ALL) NOPASSWD:ALL" | sudo EDITOR='tee -a' visudo - if [ $? -ne 0 ]; then + if ! echo "$username ALL=(ALL) NOPASSWD:ALL" | sudo EDITOR='tee -a' visudo; then echo "Error: Failed to enable passwordless sudo access." exit 1 fi @@ -816,78 +729,6 @@ enable_passwordless_sudo() { # Web Server # ============================================================================== -install_nginx_and_php() { - clear - - local nginx_installed=false - - # Check if NGINX is already installed - if dpkg -l | grep -q "nginx"; then - echo "NGINX is already installed. Skipping NGINX installation." - nginx_installed=true - else - # Install NGINX - sudo apt install nginx -y - nginx_installed=true - fi - - # Detect latest available PHP version - echo "Detecting latest available PHP version..." - local php_versions=(8.4 8.3 8.2 8.1 8.0) - local php_version="" - - for ver in "${php_versions[@]}"; do - if apt-cache policy "php$ver" 2>/dev/null | grep -q 'Candidate:'; then - php_version="$ver" - break - fi - done - - if [ -z "$php_version" ]; then - echo "No specific PHP version found, using default 'php' package." - if $nginx_installed; then - sudo apt install php php-fpm -y - else - sudo apt install php -y - fi - else - echo "Found PHP $php_version available." - if $nginx_installed; then - sudo apt install "php$php_version" "php$php_version-fpm" -y - else - sudo apt install "php$php_version" -y - fi - fi - - # Remove apache2 if it exists - if dpkg -l | awk '/apache2/ {print }' | grep -q .; then - echo "Apache2 is installed. Removing." - sudo service apache2 stop - sudo apt remove apache2 -y - sudo apt purge apache2 -y - sudo apt autoremove -y - sudo systemctl start nginx || true - fi - - echo "#######################################################" - echo "Firewall configuration" - echo "#######################################################" - configure_firewalld_ports 80 443 - echo - - # Create a directory for SSL certs if it doesn't exist - if [ ! -d "$NGINX_CERT_DIR" ]; then - echo "Creating directory $NGINX_CERT_DIR" - sudo mkdir -p "$NGINX_CERT_DIR" - sudo chmod 700 "$NGINX_CERT_DIR" - fi - - echo - echo "Finished setting up NGINX and PHP." - echo "You can upload SSL certificates into $NGINX_CERT_DIR" - echo -} - # Function to show web server installation menu install_web_server_menu() { clear @@ -1074,19 +915,24 @@ install_php_with_extensions() { sudo systemctl start php*-fpm } +backup_nginx_default_site() { + if [ -f "$NGINX_DEFAULT_SITE" ]; then + sudo cp "$NGINX_DEFAULT_SITE" "${NGINX_DEFAULT_SITE}.backup" + fi +} + # Helper: Configure NGINX for PHP configure_nginx_php() { echo echo "Configuring NGINX for PHP..." # Backup default config - if [ -f "$NGINX_DEFAULT_SITE" ]; then - sudo cp "$NGINX_DEFAULT_SITE" "${NGINX_DEFAULT_SITE}.backup" - fi + backup_nginx_default_site # Detect PHP-FPM socket - local php_socket=$(ls /run/php/php*-fpm.sock 2>/dev/null | head -1) - + local php_socket + php_socket=$(find /run/php -maxdepth 1 -name "php*-fpm.sock" -print -quit 2>/dev/null) + if [ -z "$php_socket" ]; then echo "Warning: Could not detect PHP-FPM socket. Using default." php_socket="/run/php/php-fpm.sock" @@ -1139,9 +985,7 @@ configure_nginx_static() { echo "Configuring NGINX for static content..." # Backup default config - if [ -f "$NGINX_DEFAULT_SITE" ]; then - sudo cp "$NGINX_DEFAULT_SITE" "${NGINX_DEFAULT_SITE}.backup" - fi + backup_nginx_default_site # Create a clean static config sudo tee "$NGINX_DEFAULT_SITE" >/dev/null </dev/null; then - # Install Fail2ban if not already installed - sudo apt install fail2ban -y sudo wget -O /etc/fail2ban/jail.local "$fail2ban_custom_config_url" echo "User custom Fail2ban configuration applied." else echo "Warning: Invalid URL or unable to reach the URL. Using the default configuration." - # Install Fail2ban with the default configuration - sudo apt install fail2ban -y fi ;; *) @@ -1360,14 +1203,25 @@ configure_fail2ban() { # Static IP / Netplan # ============================================================================== -cidr_to_netmask() { - local bits=$1 - local mask=$((0xffffffff ^ ((1 << (32 - bits)) - 1))) - printf "%d.%d.%d.%d\n" \ - $(((mask >> 24) & 0xff)) \ - $(((mask >> 16) & 0xff)) \ - $(((mask >> 8) & 0xff)) \ - $((mask & 0xff)) +create_netplan_backup() { + local backup_dir="/etc/netplan/backups_decoscript" + local backup_timestamp + + if [ ! -d "$backup_dir" ]; then + sudo mkdir -p "$backup_dir" + fi + + backup_timestamp=$(date +%Y%m%d%H%M%S) + sudo cp "$NETPLAN_CONFIG" "$backup_dir/01-network-manager-all.yaml.$backup_timestamp" + echo "Backup created: $backup_dir/01-network-manager-all.yaml.$backup_timestamp" +} + +detect_netplan_renderer() { + if systemctl is-active --quiet NetworkManager 2>/dev/null; then + echo "NetworkManager" + else + echo "networkd" + fi } configure_static_ip() { @@ -1416,22 +1270,15 @@ configure_static_ip() { fi # Backup existing netplan configs before making changes - local backup_dir="/etc/netplan/backups_decoscript" - if [ ! -d "$backup_dir" ]; then - sudo mkdir -p "$backup_dir" - fi - - local backup_timestamp=$(date +%Y%m%d%H%M%S) if [ -f "$NETPLAN_CONFIG" ]; then - sudo cp "$NETPLAN_CONFIG" "$backup_dir/01-network-manager-all.yaml.$backup_timestamp" - echo "Backup created: $backup_dir/01-network-manager-all.yaml.$backup_timestamp" + create_netplan_backup fi # Determine renderer based on what's actually being used - local renderer="networkd" - if systemctl is-active --quiet NetworkManager 2>/dev/null; then + local renderer + renderer=$(detect_netplan_renderer) + if [ "$renderer" = "NetworkManager" ]; then echo "NetworkManager is active, using NetworkManager renderer." - renderer="NetworkManager" else echo "Using networkd renderer (systemd-networkd)." fi @@ -1501,21 +1348,13 @@ revert_static_ip() { return fi - # Create backup before reverting - local backup_dir="/etc/netplan/backups_decoscript" - if [ ! -d "$backup_dir" ]; then - sudo mkdir -p "$backup_dir" - fi - - local backup_timestamp=$(date +%Y%m%d%H%M%S) - sudo cp "$NETPLAN_CONFIG" "$backup_dir/01-network-manager-all.yaml.$backup_timestamp" - echo "Backup created: $backup_dir/01-network-manager-all.yaml.$backup_timestamp" + create_netplan_backup # Determine renderer - local renderer="networkd" - if systemctl is-active --quiet NetworkManager 2>/dev/null; then + local renderer + renderer=$(detect_netplan_renderer) + if [ "$renderer" = "NetworkManager" ]; then echo "NetworkManager is active, using NetworkManager renderer." - renderer="NetworkManager" else echo "Using networkd renderer (systemd-networkd)." fi @@ -1536,16 +1375,15 @@ EOL # Apply the configuration echo "Applying DHCP configuration..." - sudo netplan apply - - if [ $? -eq 0 ]; then + + if sudo netplan apply; then echo echo "Successfully reverted to DHCP configuration." echo "Your network device '$network_device' will now obtain IP address automatically." else echo echo "Error: Failed to apply netplan configuration." - echo "You can restore from backup: $backup_dir/01-network-manager-all.yaml.$backup_timestamp" + echo "You can restore from the backup shown above." fi } From aa120225caafaeb4d60ba536c048edebd0fbe152 Mon Sep 17 00:00:00 2001 From: Decaded Date: Thu, 18 Jun 2026 13:32:06 +0200 Subject: [PATCH 3/4] Refactor README and install script for clarity --- README.md | 113 +++++++++++++++++++++++++++++------------------------ install.sh | 25 ------------ 2 files changed, 63 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index d4fa425..29df445 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ ## Overview -This script is a menu-driven server utility tool for Debian-based and Ubuntu-based systems. It helps automate common setup tasks without forcing a specific stack. You choose exactly -what gets installed: essential tools, Nginx, PHP, NVM, static IP profiles, and more. +This is a menu-driven server utility script for Ubuntu and other Debian-based systems. It helps automate common setup tasks without forcing one fixed stack. You choose what to install or configure: +essential tools, Nginx, PHP, MySQL, NVM, SSH hardening, Fail2ban, static IP profiles, and more. -Ubuntu is the primary target environment. Other Debian derivatives should work, but if you encounter issues, feel free to open an -[issue](https://github.com/Decaded/install-script/issues) and describe your setup. +Ubuntu is the primary target environment. Other Debian derivatives should work, but some minimal images, SBC images, and custom kernels may need extra care. If you encounter an issue, +open an [issue](https://github.com/Decaded/install-script/issues) and include your OS, kernel version, and the menu option that failed.
@@ -45,9 +45,7 @@ Ubuntu is the primary target environment. Other Debian derivatives should work, ./install.sh ``` -The script requires sudo privileges. If your sudo session is not already active, run `sudo -v` first. - -A menu will appear with all available options. +The script requires sudo privileges and checks for them on startup. A menu will appear with the available actions.
Script Menu Preview @@ -57,45 +55,53 @@ A menu will appear with all available options. ## Features -### Essential Tools - -Install a curated pack of common system utilities: - -1. **[htop](https://htop.dev/)** or **[btop](https://github.com/aristocratos/btop)** – process viewer -2. **[screen](https://www.gnu.org/software/screen/)** or **[tmux](https://github.com/tmux/tmux/wiki)** – terminal multiplexer -3. **[nload](https://github.com/rolandriegel/nload)** – network traffic monitor -4. **[nano](https://www.nano-editor.org/)** or **[Neovim](https://neovim.io/)** – text editor -5. **[firewalld](https://firewalld.org/)** – firewall management +### Main Menu - - Automatically opens SSH - - Checks for working netfilter support before starting firewalld +- Install selected essential apps +- Install a web server stack +- Install Node Version Manager (NVM) +- Enable passwordless sudo +- Configure SSH key-only authentication +- Configure a static IP address +- Configure Fail2ban +- Revert static IP configuration to DHCP +- Check for script updates +- Restore SSH configuration when a backup exists -6. **[fail2ban](https://github.com/fail2ban/fail2ban)** – intrusion prevention - - - Default configuration or custom rules +### Essential Tools -7. **[git](https://git-scm.com/)** – version control +Install any combination of common utilities from a checklist: - - Optional first-time setup +- **[htop](https://htop.dev/)** and **[btop](https://github.com/aristocratos/btop)** - process viewers +- **[screen](https://www.gnu.org/software/screen/)** and **[tmux](https://github.com/tmux/tmux/wiki)** - terminal multiplexers +- **[nload](https://github.com/rolandriegel/nload)** - network traffic monitor +- **[nano](https://www.nano-editor.org/)** and **[Neovim](https://neovim.io/)** - text editors +- **[firewalld](https://firewalld.org/)** - firewall management +- **[fail2ban](https://github.com/fail2ban/fail2ban)** - intrusion prevention +- **[git](https://git-scm.com/)** - version control +- **[unattended-upgrades](https://wiki.debian.org/UnattendedUpgrades)** - automatic security updates +- **[Pi-hole](https://pi-hole.net/)** - ad blocker and optional DHCP server -8. **[unattended-upgrades](https://wiki.debian.org/UnattendedUpgrades)** – automatic security updates -9. **[Pi-hole](https://pi-hole.net/)** – ad blocker and optional DHCP server +When selected, some tools offer follow-up configuration. Firewalld asks for the current SSH port, opens that port, and checks for working netfilter/nftables support before trying to start +the service. Fail2ban can use the default setup or a custom `jail.local` URL. Git can configure global name, email, and default branch. ### SSH Configuration -Switch to secure, key-only SSH authentication. The script: +Switch to key-only SSH authentication. The script: - Disables password-based logins +- Enables public key authentication +- Adds the provided public key to `~/.ssh/authorized_keys` - Creates a backup of your SSH config -- Provides a restore option +- Provides a restore option when a backup exists Backup file location: -``` bash +```bash /etc/ssh/sshd_config_decoscript.backup.* ``` -The script keeps the five most recent SSH config backups and offers a restore option from the main menu when a backup exists. +The script keeps the five most recent SSH config backups. ### Passwordless Sudo @@ -103,28 +109,20 @@ Enables password-free sudo access if desired. If your system already uses this c ### Web Server Setup -- Automatic cleanup of Apache2 if present -- Firewall rules for HTTP(S) when using firewalld +Choose one of three web server paths: -Installs the full **LEMP** stack: +- Full **LEMP** stack: **[Nginx](https://nginx.org/)**, **[MySQL](https://www.mysql.com/)**, and **[PHP](https://www.php.net/)** +- **Nginx + PHP** +- **Nginx only** -- **[Nginx](https://nginx.org/)** installation and configuration -- **[MySQL](https://www.mysql.com/)** installation with optional `mysql_secure_installation` -- **[PHP](https://www.php.net/)** installation with commonly used modules +The web server setup can: - - Configures **php-fpm** to work with Nginx - - Installs modules: - - **php-cli** - - **php-fpm** - - **php-mbstring** - - **php-curl** - - **php-xml** - - **php-zip** - - **php-gd** - - **php-mysql** - -- **OR** install Nginx and PHP only, -- **OR** install only Nginx. +- Remove Apache2 if it is installed, to avoid port conflicts +- Enable and start Nginx +- Install common PHP/FPM packages and configure Nginx for PHP +- Create `/etc/nginx/cert` for SSL certificates +- Create a simple default page if the web root is empty +- Open HTTP and HTTPS ports when firewalld is available ### Node.js via NVM @@ -135,7 +133,7 @@ Installs the latest **[NVM](https://github.com/nvm-sh/nvm)** version and lets yo ### Static IP Configuration -Configure a static IP address using **Netplan** when available. +Configure a static IP address using **Netplan**. Supports: @@ -144,7 +142,22 @@ Supports: - Gateway - DNS servers -If Netplan isn’t present, the script installs `netplan.io` before writing the configuration. A revert option is available from the main menu. +If Netplan is not present, the script installs `netplan.io` before writing the configuration. It also installs `net-tools` when needed to list network interfaces. + +Before changing an existing Netplan file, the script creates a backup under: + +```bash +/etc/netplan/backups_decoscript/ +``` + +A revert option is available from the main menu to switch an interface back to DHCP. + +--- + +## Notes + +- Firewalld depends on working kernel netfilter/nftables support. On some minimal Armbian or SBC images, firewalld may not be usable until the kernel is updated and the system is rebooted. +- SSH and network changes can disconnect you from a remote server if incorrect values are entered. Keep another access path available when possible. --- diff --git a/install.sh b/install.sh index 8247162..f29f774 100644 --- a/install.sh +++ b/install.sh @@ -374,13 +374,6 @@ is_firewalld_running() { sudo firewall-cmd --state >/dev/null 2>&1 } -# Best-effort: ensure the nf_tables kernel module is loaded before starting -# firewalld. On some minimal SBC/Armbian images the module exists but is not -# auto-loaded, which makes firewalld fail at startup with -# "cache initialization failed: Invalid argument". Harmless no-op when the -# module is already loaded; does nothing when the kernel lacks it entirely. -ensure_netfilter_modules() { - if lsmod 2>/dev/null | grep -q '^nf_tables'; then # Best-effort: ensure the nf_tables kernel module is loaded before starting # firewalld. On some minimal SBC/Armbian images the module exists but is not # auto-loaded, which makes firewalld fail at startup with @@ -448,24 +441,6 @@ start_firewalld() { return 0 fi - echo "Error: Failed to start firewalld." - echo "Inspect the failure with:" - echo " sudo systemctl status firewalld" - echo " sudo journalctl -xeu firewalld" - return 1 - # Verify the kernel can actually run a netfilter backend before we try, so we - # can give a clear recommendation instead of a cryptic startup failure. - if ! check_netfilter_support; then - print_unsupported_kernel_recommendation - return 1 - fi - - # 'restart' (not 'start') re-executes a previously failed unit; confirm the - # daemon is actually responsive via firewall-cmd --state. - if sudo systemctl restart firewalld >/dev/null 2>&1 && is_firewalld_running; then - return 0 - fi - echo "Error: Failed to start firewalld." echo "Inspect the failure with:" echo " sudo systemctl status firewalld" From 245310a56fce75227ee4eb2eb3e7205e5405e50f Mon Sep 17 00:00:00 2001 From: Decaded Date: Thu, 18 Jun 2026 13:35:09 +0200 Subject: [PATCH 4/4] Fix update_script function to correctly handle temporary script naming and backup permissions --- install.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index f29f774..82cb436 100644 --- a/install.sh +++ b/install.sh @@ -210,7 +210,7 @@ check_for_updates() { update_script() { echo echo "Downloading latest version..." - local temp_script="/tmp/install_sh_update_$" + local temp_script="/tmp/install_sh_update_$$" if curl -fsSL --max-time 10 -o "$temp_script" "$SCRIPT_URL" 2>/dev/null; then # Verify the download @@ -218,7 +218,9 @@ update_script() { chmod +x "$temp_script" # Backup current script and remove execute permissions from backup - cp "$0" "${0}.backup" 2>/dev/null && chmod -x "${0}.backup" 2>/dev/null || true + if cp "$0" "${0}.backup" 2>/dev/null; then + chmod -x "${0}.backup" 2>/dev/null || true + fi # Replace with new version mv "$temp_script" "$0"