diff --git a/README.md b/README.md index a5a8770..6d44c78 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,118 @@ # Linux Security Scripts -[![Project Tracker](https://img.shields.io/badge/repo%20status-Project%20Tracker-lightgrey)](https://wiki.hthompson.dev/en/project-tracker) -[![Style Guide](https://img.shields.io/badge/code%20style-Style%20Guide-blueviolet)](https://github.com/StrangeRanger/bash-style-guide) +[![Project Tracker](https://img.shields.io/badge/repo%20status-Project%20Tracker-lightgrey)](https://hthompson.dev/project-tracker#project-293920085) +[![Style Guide](https://img.shields.io/badge/code%20style-Style%20Guide-blueviolet)](https://bsg.hthompson.dev/) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/598c2083cd6f432a910a315fd10aaa66)](https://www.codacy.com/gh/StrangeRanger/linux-security-scripts/dashboard?utm_source=github.com&utm_medium=referral&utm_content=StrangeRanger/linux-security-scripts&utm_campaign=Badge_Grade) -This repository is a collection of scripts designed to secure/harden Linux based Distributions. +This repository is a collection of scripts designed to secure/harden Linux-based distributions. + +
+Table of Contents + +- [Linux Security Scripts](#linux-security-scripts) + - [Tools and Scripts](#tools-and-scripts) + - [Getting Started](#getting-started) + - [Prerequisites](#prerequisites) + - [Download and Setup](#download-and-setup) + - [Usage](#usage) + - [Quick Start](#quick-start) + - [Individual Script Usage](#individual-script-usage) + - [Post-Installation](#post-installation) + - [Tested On](#tested-on) + - [Other Resources](#other-resources) + - [Security Auditing Tools](#security-auditing-tools) + - [Additional Hardening Resources](#additional-hardening-resources) + - [System Monitoring](#system-monitoring) + - [Support and Issues](#support-and-issues) + - [License](#license) + +
+ +## Tools and Scripts + +Below is a list of tools included in this repository. + +| Tool Name | Description | Category | Requirements | Notes | +|-----------|-------------|----------|--------------|-------| +| **[Lynis Installer](auditing/Lynis%20Installer/lynis-installer.bash)** | Download (clone) Lynis, a security auditing tool for Unix-like systems. | Auditing | Git, Internet connection | No root required | +| **[Root Locker](hardening/Root%20Locker/root-locker.bash)** | Locks the root account to prevent direct logins. | Hardening | Root privileges | Preserves sudo access | +| **[SSHD Hardening](hardening/SSHD%20Hardening/harden-sshd.bash)** | Harden OpenSSH server (sshd) per Lynis recommendations. | Hardening | Root privileges | Creates backups | +| **[UFW Cloudflare](hardening/UFW%20Cloudflare/ufw-cloudflare.bash)** | Configure UFW to only allow HTTP/HTTPS from Cloudflare IP ranges. | Hardening | Root privileges, UFW, Internet connection | Creates backups | - +> [!NOTE] +> All scripts include version information in their headers. Check individual CHANGELOG.md files in each tool's directory for version history and updates. ## Getting Started -### Downloading +### Prerequisites + +The following requirements extend to every tool in this repository: + +- **Bash**: Version 4.0 or higher +- **Operating System**: Linux-based distribution + +> [!NOTE] +> Individual scripts may have additional requirements listed in the table above. + +### Download and Setup All you need to do is download this repository to your local machine: -`git clone https://github.com/StrangeRanger/linux-security-scripts` +```bash +git clone https://github.com/StrangeRanger/linux-security-scripts +cd linux-security-scripts +``` ## Usage -> [!NOTE] -> Some of the scripts in this repository require root privileges to run. You can run the scripts with the `sudo` command to give them the necessary permissions. +### Quick Start + +For users who want to get started immediately: + +1. **Audit your system first**: Run the Lynis installer to download the auditing tool. + ```bash + ./auditing/Lynis\ Installer/lynis-installer.bash + ``` + +2. **Run a security audit**: Use Lynis to identify security issues. + ```bash + cd ~/lynis && sudo ./lynis audit system + ``` + +3. **Apply hardening**: Based on the audit results, run the appropriate hardening scripts with root privileges. + +> [!CAUTION] +> **Production Environment Warning**: Always test scripts in a non-production environment first. Some scripts modify critical system configurations and may affect system accessibility. + +### Individual Script Usage + +You can run any script individually using one of the following methods: -You can run the scripts in this repository by using the following command: +```bash +./[script-name] +``` -`./[script name]` OR `bash [script name]` +**or** + +```bash +bash [script-name] +``` + +## Post-Installation + +After running the hardening scripts: + +1. **Verify SSH access**: Before logging out, test SSH connectivity in a new terminal session. +2. **Review firewall rules**: Check UFW status with `sudo ufw status verbose` if you used the UFW Cloudflare script. +3. **Run Lynis again**: Re-audit your system to see security improvements. +4. **Backup configurations**: Keep copies of any modified configuration files. + +> [!WARNING] +> The SSHD hardening script modifies SSH configurations. Ensure you have alternative access to your system before applying changes in production environments. ## Tested On -All of the scripts should work on most, if not all Linux Distributions. With that said, below is a list of Linux Distributions that the scripts have been officially tested and are confirmed to work on. +All of the scripts should work on most, if not all, Linux distributions with Bash v4.0+ installed. With that said, below is a list of Linux distributions that the scripts have been officially tested and are confirmed to work on. | Distributions | Distro Versions | | ------------- | ---------------------- | @@ -36,6 +121,28 @@ All of the scripts should work on most, if not all Linux Distributions. With tha ## Other Resources -While this repository has scripts that can help secure Linux, it's not nearly enough to secure the system as much as it needs to be. Below is a list of other resources that you can/should use to help make your system as secure as possible. +Below is a list of additional resources that you can/should use to help make your system as secure as possible. + +### Security Auditing Tools + +- [SSH Audit](https://github.com/jtesta/ssh-audit) - SSH server & client auditing (banner, key exchange, encryption, mac, compression, compatibility, security, etc) + +### Additional Hardening Resources + +- [CIS Benchmarks](https://www.cisecurity.org/cis-benchmarks) - Industry-standard security configuration guidelines +- [NIST Cybersecurity Framework](https://www.nist.gov/cyberframework) - Comprehensive cybersecurity guidance +- [OpenSCAP](https://www.open-scap.org/) - Security compliance and vulnerability management + +### System Monitoring + +- [AIDE](https://aide.github.io/) - Advanced Intrusion Detection Environment +- [Fail2Ban](https://github.com/fail2ban/fail2ban) - Intrusion prevention software +- [rkhunter](http://rkhunter.sourceforge.net/) - Rootkit detection tool + +## Support and Issues + +Please use [GitHub Issues](https://github.com/StrangeRanger/linux-security-scripts/issues) for bug reports and feature requests. + +## License -- [SSH Audit](https://github.com/jtesta/ssh-audit) - SSH server & client auditing (banner, key exchange, encryption, mac, compression, compatibility, security, etc). +Licensing may vary by tool; see individual file headers. diff --git a/auditing/Lynis Installer/CHANGELOG.md b/auditing/Lynis Installer/CHANGELOG.md index 4e023ef..5342886 100644 --- a/auditing/Lynis Installer/CHANGELOG.md +++ b/auditing/Lynis Installer/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## v1.0.9 - 2025-08-10 + +### Removed + +- Remove pointless `-e` flag in `echo`. + ## v1.0.8 - 2024-12-20 ### Changed @@ -17,7 +23,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - No longer requires root permission to run the script. - Won't download lynis if is already present on the system. - Improved syntax of the script. -- Rename script to `lynis-installer.bash`. +- Rename script to `lynis-installer.bash`. ## v1.0.6 - 2024-04-13 diff --git a/auditing/Lynis Installer/lynis-installer.bash b/auditing/Lynis Installer/lynis-installer.bash index 75fe317..a5c1c5a 100755 --- a/auditing/Lynis Installer/lynis-installer.bash +++ b/auditing/Lynis Installer/lynis-installer.bash @@ -5,9 +5,9 @@ # it. Unless an error is encountered, Lynis will always be downloaded to the current # user's root directory (`/home/USERNAME/`). # -# Version: v1.0.8 +# Version: v1.0.9 # License: MIT License -# Copyright (c) 2020-2024 Hunter T. (StrangeRanger) +# Copyright (c) 2020-2025 Hunter T. (StrangeRanger) # ######################################################################################## @@ -48,5 +48,5 @@ git clone https://github.com/CISOfy/lynis || { } echo -e "\n${C_SUCCESS}Lynis has been downloaded to your system" -echo -e "${C_NOTE}To perform a system scan with lynis, execute the following command" \ +echo "${C_NOTE}To perform a system scan with lynis, execute the following command" \ "in the lynis root directory: sudo ./lynis audit system" diff --git a/hardening/Root Locker/CHANGELOG.md b/hardening/Root Locker/CHANGELOG.md index a5d5905..42d36bc 100644 --- a/hardening/Root Locker/CHANGELOG.md +++ b/hardening/Root Locker/CHANGELOG.md @@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## v1.0.10 - 2025-08-10 + +### Changed + +- Replace `[[ ]]` with `(( ))`. +- Remove redundant comments. + +## v1.0.9 - 2025-08-09 + +### Changed + +- Removed "Exiting..." message from output. + ## v1.0.8 - 2024-12-20 ### Changed diff --git a/hardening/Root Locker/root-locker.bash b/hardening/Root Locker/root-locker.bash index 02e0583..bd57608 100755 --- a/hardening/Root Locker/root-locker.bash +++ b/hardening/Root Locker/root-locker.bash @@ -6,9 +6,9 @@ # Locking the root account doesn't prevent users from using something like `sudo su` # to gain root access. # -# Version: v1.0.8 +# Version: v1.0.10 # License: MIT License -# Copyright (c) 2020-2024 Hunter T. (StrangeRanger) +# Copyright (c) 2020-2025 Hunter T. (StrangeRanger) # ######################################################################################## @@ -24,10 +24,8 @@ C_INFO="${C_BLUE}==>${C_NC} " C_NOTE="${C_CYAN}==>${C_NC} " -## Check if this script was executed with root privilege. -if [[ $EUID != 0 ]]; then +if (( EUID != 0 )); then echo "${C_ERROR}Please run this script as or with root privilege" >&2 - echo -e "\n${C_INFO}Exiting..." exit 1 fi @@ -37,7 +35,6 @@ read -rp "${C_NOTE}We will now disable the root account. Press [Enter] to contin echo "${C_INFO}Disabling root account..." usermod -L root || { echo -e "${C_ERROR}Failed to lock the root account" >&2 - echo -e "\n${C_INFO}Exiting..." exit 1 } diff --git a/hardening/SSHD Hardening/CHANGELOG.md b/hardening/SSHD Hardening/CHANGELOG.md index 9ec38e4..6e5f5e7 100644 --- a/hardening/SSHD Hardening/CHANGELOG.md +++ b/hardening/SSHD Hardening/CHANGELOG.md @@ -2,7 +2,22 @@ All notable changes to this project will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## v2.1.0 - 2025-08-09 + +### Added + +- **Session backup system**: Automatic restoration during script interruptions with temporary backup preservation for manual recovery +- **Cross-platform SSH service restart**: Automatically detects and restarts either `sshd` or `ssh` service based on distribution +- **Enhanced signal handling**: Proper restoration and cleanup on script interruption (SIGHUP, SIGINT, SIGTERM) + +### Changed + +- **Backup strategy**: Dual backup system with permanent `.bak` file for user reference and session backup for auto-restoration +- **Exit handling**: Strategic use of `clean_exit` function only when cleanup or restoration is needed +- **User messaging**: Enhanced feedback throughout backup, restoration, and cleanup processes +- **Output colors**: "Already set" messages now use note (cyan) instead of success (green) for better semantic clarity ## v2.0.2 - 2024-12-20 diff --git a/hardening/SSHD Hardening/harden-sshd.bash b/hardening/SSHD Hardening/harden-sshd.bash index 52fcdbb..dd7e65d 100755 --- a/hardening/SSHD Hardening/harden-sshd.bash +++ b/hardening/SSHD Hardening/harden-sshd.bash @@ -1,37 +1,37 @@ #!/bin/bash # -# This script hardens the ssh server by modifying its configuration file, 'sshd_config'. +# Harden the ssh server by modifying its configuration file with the recommended settings +# outlined by the security auditing tool known as Lynis (https://github.com/CISOfy/lynis). # # NOTE: -# These configurations align with the recommendations of the security auditing tool -# known as Lynis (https://github.com/CISOfy/lynis). +# - Two types of backups are created: +# - Permanent backup (.bak): For manual user restoration and reference. +# - Session backup (.session_backup): For automatic script restoration during +# interruptions. # -# TODO: -# - Impliment functionality to revert changes if the script fails. -# -# Version: v2.0.2 +# Version: v2.1.0 # License: MIT License -# Copyright (c) 2020-2024 Hunter T. (StrangeRanger) +# Copyright (c) 2020-2025 Hunter T. (StrangeRanger) # -######################################################################################## -####[ Global Variables ]################################################################ +############################################################################################ +####[ Global Variables ]#################################################################### +C_TMP_DIR=$(mktemp -d); readonly C_TMP_DIR +readonly C_SESSION_BACKUP="$C_TMP_DIR/sshd_config.session_backup" readonly C_CONFIG_FILE_BAK="/etc/ssh/sshd_config.bak" readonly C_CONFIG_FILE="/etc/ssh/sshd_config" -## Used to colorize output. C_YELLOW="$(printf '\033[1;33m')" C_GREEN="$(printf '\033[0;32m')" C_BLUE="$(printf '\033[0;34m')" C_CYAN="$(printf '\033[0;36m')" C_RED="$(printf '\033[1;31m')" C_NC="$(printf '\033[0m')" -readonly C_GREEN C_CYAN C_RED C_NC +readonly C_YELLOW C_GREEN C_BLUE C_CYAN C_RED C_NC -## Short-hand colorized messages. -readonly C_SUCCESS="${C_GREEN}==>${C_NC} " readonly C_WARNING="${C_YELLOW}==>${C_NC} " +readonly C_SUCCESS="${C_GREEN}==>${C_NC} " readonly C_ERROR="${C_RED}ERROR:${C_NC} " readonly C_INFO="${C_BLUE}==>${C_NC} " readonly C_NOTE="${C_CYAN}==>${C_NC} " @@ -77,49 +77,66 @@ declare -A C_SSHD_CONFIG=( ) readonly C_SSHD_CONFIG +modifications_in_progress=false -####[ Functions ]####################################################################### + +####[ Functions ]########################################################################### #### -# Exit the script and display a message based on the exit code. +# Cleanly exit the script by removing temporary files, restoring backups if needed, and +# displaying a message based on the exit code. # # PARAMETERS: # - $1: exit_code (Required) clean_exit() { local exit_code="$1" - # Unset the EXIT trap to prevent re-entry. - trap - EXIT - case "$exit_code" in - 0) ;; - 1) echo "" ;; - 129) echo -e "\n${C_WARNING}Hangup signal detected (SIGHUP)" ;; - 130) echo -e "\n${C_WARNING}User interrupt detected (SIGINT)" ;; - 143) echo -e "\n${C_WARNING}Termination signal detected (SIGTERM)" ;; - *) echo -e "\n${C_WARNING}Exiting with code: $exit_code" ;; + 0|1) echo "" ;; + 129) echo -e "\n\n${C_WARNING}Hangup signal detected (SIGHUP)" ;; + 130) echo -e "\n\n${C_WARNING}User interrupt detected (SIGINT)" ;; + 143) echo -e "\n\n${C_WARNING}Termination signal detected (SIGTERM)" ;; + *) echo -e "\n\n${C_WARNING}Exiting with code: $exit_code" ;; esac - echo "Exiting..." + # Check if we need to restore the original configurations. + if [[ $modifications_in_progress == true ]] && [[ -f "$C_SESSION_BACKUP" ]]; then + echo "${C_WARNING}Script was interrupted during configuration changes" + echo "${C_INFO}Restoring original 'sshd_config'..." + if cp "$C_SESSION_BACKUP" "$C_CONFIG_FILE"; then + echo "${C_SUCCESS}Successfully restored original configurations" + echo "${C_INFO}Cleaning up..." + [[ -d "$C_TMP_DIR" ]] && rm -rf "$C_TMP_DIR" + else + echo "${C_ERROR}Failed to restore 'sshd_config'" >&2 + echo "${C_NOTE}Session backup is available at: $C_SESSION_BACKUP" + echo "${C_NOTE}Permanent backup is available at: $C_CONFIG_FILE_BAK" + echo "${C_NOTE}Temp directory preserved for manual recovery: $C_TMP_DIR" + fi + else + echo "${C_INFO}Cleaning up..." + [[ -d "$C_TMP_DIR" ]] && rm -rf "$C_TMP_DIR" + fi + + echo "${C_INFO}Exiting..." exit "$exit_code" } -####[ Trapping Logic ]################################################################## +####[ Trapping Logic ]###################################################################### trap 'clean_exit 129' SIGHUP trap 'clean_exit 130' SIGINT trap 'clean_exit 143' SIGTERM -trap 'clean_exit $?' EXIT -####[ Prepping ]######################################################################## +####[ Prepping ]############################################################################ ## Check if the script was executed with root privilege. -if [[ $EUID != 0 ]]; then +if (( EUID != 0 )); then echo "${C_ERROR}This script requires root privilege" >&2 exit 1 fi @@ -132,7 +149,7 @@ if [[ ! -f $C_CONFIG_FILE ]]; then fi -####[ Main ]############################################################################ +####[ Main ]################################################################################ read -rp "${C_NOTE}We will now harden sshd. Press [Enter] to continue." @@ -144,14 +161,12 @@ read -rp "${C_NOTE}We will now harden sshd. Press [Enter] to continue." if [[ -f $C_CONFIG_FILE_BAK ]]; then printf "%sA backup of 'sshd_config' already exists. " "$C_NOTE" read -rp "Do you want to overwrite it? [y/N] " choice - choice="${choice,,}" + choice="${choice,,}" case "$choice" in y*) echo "${C_INFO}Overwriting backup of 'sshd_config'..." - { - rm $C_CONFIG_FILE_BAK && cp $C_CONFIG_FILE $C_CONFIG_FILE_BAK - } || { + cp $C_CONFIG_FILE $C_CONFIG_FILE_BAK || { echo "${C_ERROR}Failed to overwrite backup of 'sshd_config'" >&2 exit 1 } @@ -164,18 +179,25 @@ if [[ -f $C_CONFIG_FILE_BAK ]]; then unset choice else echo "${C_INFO}Backing up 'sshd_config'..." - cp $C_CONFIG_FILE $C_CONFIG_FILE_BAK || { + cp "$C_CONFIG_FILE" "$C_CONFIG_FILE_BAK" || { echo "${C_ERROR}Failed to back up sshd_config" >&2 - echo "${C_NOTE}Please create a backup of the original 'sshd_config' before" \ - "continuing" + echo "${C_NOTE}Create a backup of the original 'sshd_config' file before continuing" exit 1 } fi +echo "${C_INFO}Creating session backup for safe restoration..." +cp "$C_CONFIG_FILE" "$C_SESSION_BACKUP" || { + echo "${C_ERROR}Failed to create session backup" >&2 + clean_exit 1 +} + ### ### [ Harden 'sshd_config' ] ### +modifications_in_progress=true + for key in "${!C_SSHD_CONFIG[@]}"; do # Skip processing Regex keys directly. if [[ "$key" =~ Regex$ ]]; then @@ -188,7 +210,7 @@ for key in "${!C_SSHD_CONFIG[@]}"; do ## Check if the key is already set to the desired value. if grep -Eq "^${key} ${C_SSHD_CONFIG[$key]}$" "$C_CONFIG_FILE"; then - echo "${C_SUCCESS}${key} already set to '${C_SSHD_CONFIG[$key]}'" + echo "${C_NOTE}${key} already set to '${C_SSHD_CONFIG[$key]}'" ## Check if the configurations are present in the file. elif grep -Eq "${C_SSHD_CONFIG[$regex_key]}" "$C_CONFIG_FILE"; then echo "${C_INFO}Setting '${key} ${C_SSHD_CONFIG[$key]}'..." @@ -204,15 +226,23 @@ done ### [ Finalizing ] ### -echo -e "\n${C_INFO}Restarting sshd..." -systemctl restart sshd || { - echo "${C_ERROR}Failed to restart sshd" >&2 - exit 1 -} +modifications_in_progress=false + +echo -e "\n${C_INFO}Restarting SSH service..." +if systemctl restart sshd 2>/dev/null; then + echo "${C_SUCCESS}SSH service (sshd) restarted successfully" +elif systemctl restart ssh 2>/dev/null; then + echo "${C_SUCCESS}SSH service (ssh) restarted successfully" +else + echo "${C_ERROR}Failed to restart SSH service (tried both 'sshd' and 'ssh')" >&2 + echo "${C_NOTE}You may need to restart the SSH service manually" +fi -echo -e "\n${C_SUCCESS}Finished hardening sshd" -echo -e "${C_NOTE}It is highly recommended to manually:" -echo -e "${C_NOTE} 1) Change the default sshd port (22)" -echo -e "${C_NOTE} 2) Disable PasswordAuthentication in favor of PubkeyAuthentication" -echo -e "${C_NOTE} 3) Add 'AllowUsers [your username]' to the bottom of 'sshd_config'" +echo "" +echo "${C_SUCCESS}Finished hardening sshd" +echo "${C_NOTE}It is highly recommended to manually:" +echo "${C_NOTE} 1) Change the default sshd port (22)" +echo "${C_NOTE} 2) Disable PasswordAuthentication in favor of PubkeyAuthentication" +echo "${C_NOTE} 3) Add 'AllowUsers [your username]' to the bottom of 'sshd_config'" +clean_exit 0 diff --git a/hardening/UFW Cloudflare/CHANGELOG.md b/hardening/UFW Cloudflare/CHANGELOG.md index e69de29..d5aa6c4 100644 --- a/hardening/UFW Cloudflare/CHANGELOG.md +++ b/hardening/UFW Cloudflare/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## v1.0.1 - 2025-08-10 + +### Fixed + +- Remove temp files on exit to avoid leftovers + +## v1.0.0 - 2025-08-09 + +- Full production-ready release diff --git a/hardening/UFW Cloudflare/ufw-cloudflare.bash b/hardening/UFW Cloudflare/ufw-cloudflare.bash index f63248a..cb3d3c2 100755 --- a/hardening/UFW Cloudflare/ufw-cloudflare.bash +++ b/hardening/UFW Cloudflare/ufw-cloudflare.bash @@ -1,155 +1,108 @@ #!/bin/bash # -# Sets up UFW to only allow HTTP and HTTPS traffic from Cloudflare's IP ranges. +# Set up UFW to only allow HTTP and HTTPS traffic from Cloudflare's IP ranges. # -# Version: v1.0.0-beta.2 +# Version: v1.0.1 # License: MIT License -# Copyright (c) 2024 Hunter T. (StrangeRanger) +# Copyright (c) 2024-2025 Hunter T. (StrangeRanger) # -######################################################################################## -####[ Global Variables ]################################################################ +############################################################################################ +####[ Global Variables ]#################################################################### -## URL for retrieving the current Cloudflare IP ranges. +C_TMP_DIR=$(mktemp -d) +C_UFW_BACKUP_ARCHIVE="$C_TMP_DIR/ufw-backup-$(date +%F).tar.gz" +readonly C_TMP_DIR C_UFW_BACKUP_ARCHIVE +readonly C_CLOUDFLARE_UFW_COMMENT="Cloudflare IP" +readonly C_SLEEP_TIME=1 + readonly C_CLOUDFLARE_IPV4_RANGES_URL="https://www.cloudflare.com/ips-v4/" readonly C_CLOUDFLARE_IPV6_RANGES_URL="https://www.cloudflare.com/ips-v6/" +C_YELLOW="$(printf '\033[1;33m')" +C_GREEN="$(printf '\033[0;32m')" +C_BLUE="$(printf '\033[0;34m')" +C_CYAN="$(printf '\033[0;36m')" +C_RED="$(printf '\033[1;31m')" +C_NC="$(printf '\033[0m')" +readonly C_YELLOW C_GREEN C_BLUE C_CYAN C_RED C_NC + +readonly C_SUCCESS="${C_GREEN}==>${C_NC} " +readonly C_WARNING="${C_YELLOW}==>${C_NC} " +readonly C_ERROR="${C_RED}ERROR:${C_NC} " +readonly C_INFO="${C_BLUE}==>${C_NC} " +readonly C_NOTE="${C_CYAN}==>${C_NC} " + current_cloudflare_rule_numbers=() current_cloudflare_ip_ranges=() new_cloudflare_ip_ranges=() stage=0 -####[ Function ]######################################################################## - +####[ Function ]############################################################################ -#### -# Check if a UFW rule exists for a specific IP address and port. -# -# PARAMETERS: -# - $1: ip (Required) -# - The IP address to check. -# - $2: port (Required) -# - The port to check. -# -# RETURN: -# - 0: The rule exists. -# - ?: The rule does not exist. -ufw_rule_exists() { - local ip="$1" - local port="$2" - - ufw status | grep -qE "^${port}.*ALLOW.*${ip}.*$" -} #### -# Retrieves the rule number of all Cloudflare IP rules currently set in UFW, then -# stores them in an array. +# Cleanly exit the script by removing temporary files, restoring backups if needed, and +# displaying a message based on the exit code. # # PARAMETERS: -# - $1: string_to_grep (Required) -# - The string to grep for in the UFW status output. -# - Acceptable values: -# - 0: "Cloudflare IP" -# - 1: "Temporary rule" -get_set_cloudflare_rule_numbers() { - if (( $1 == 0 )); then - local string_to_grep="Cloudflare IP" - elif (( $1 == 1 )); then - local string_to_grep="Temporary rule" - else - echo "Invalid argument: $1" - exit 1 - fi +# - $1: exit_code (Required) +clean_exit() { + local exit_code="$1" + + case "$exit_code" in + 0|1) echo "" ;; + 129) echo -e "\n\n${C_WARNING}Hangup signal detected (SIGHUP)" ;; + 130) echo -e "\n\n${C_WARNING}User interrupt detected (SIGINT)" ;; + 143) echo -e "\n\n${C_WARNING}Termination signal detected (SIGTERM)" ;; + *) echo -e "\n\n${C_WARNING}Exiting with code: $exit_code" ;; + esac - mapfile -t current_cloudflare_rule_numbers < <( - ufw status numbered \ - | grep "$string_to_grep" \ - | awk -F'[][]' '{print $2}' \ - | sort -rn - ) -} + case $stage in + 2|3|4) + echo "${C_WARNING}Interrupt occurred during stage '$stage'; incomplete changes" + echo "${C_INFO}Temporarily disabling UFW..." + ufw disable + echo "${C_INFO}Restoring previous UFW rules..." + sudo tar -C /etc -xf "$C_UFW_BACKUP_ARCHIVE" + echo "${C_INFO}Re-enabling UFW..." + ufw enable + echo "${C_INFO}Displaying current UFW status..." + echo "---" + ufw status verbose + echo "---" + ;; + esac -#### -# Retrieves the IP addresses of all Cloudflare IP rules currently set in UFW, then -# stores them in an array. -get_set_cloudflare_ip_ranges() { - while IFS= read -r line; do - ip=$(echo "$line" | awk '{print $3}') # Extract the IP address. - current_cloudflare_ip_ranges+=("$ip") - done < <(sudo ufw status | grep "Cloudflare IP") -} + if [[ -d "$C_TMP_DIR" ]]; then + echo "${C_INFO}Cleaning up..." + rm -rf "$C_TMP_DIR" + fi -#### -# Set the new Cloudflare IP ranges in UFW, retrieved from the Cloudflare website. -set_new_cloudflare_ip_ranges() { - for ip in "${new_cloudflare_ip_ranges[@]}"; do - ufw_rule_exists "$ip" "80,443" \ - || ufw allow from "$ip" to any port 80,443 proto tcp comment "Cloudflare IP" - done + echo "${C_INFO}Exiting..." + exit "$exit_code" } -#### -# Restores the previous (non-new) Cloudflare IP ranges in UFW. -restore_current_cloudflare_ip_ranges() { - for ip in "${current_cloudflare_ip_ranges[@]}"; do - ufw_rule_exists "$ip" "80,443" \ - || ufw allow from "$ip" to any port 80,443 proto tcp comment "Cloudflare IP" - done -} -#### -# Deletes all Cloudflare IP rules currently set in UFW. -delete_set_cloudflare_rules() { - get_set_cloudflare_rule_numbers "0" +####[ Trapping Logic ]###################################################################### - for rule_num in "${current_cloudflare_rule_numbers[@]}"; do - # TODO: Add configuration option to confirm deletion. - yes | ufw delete "$rule_num" - done -} -#### -# Cleanup function to close ports 80 and 443 from any IP address. -cleanup() { - case $stage in - 2) - delete_set_cloudflare_rules "1" - ;; - 3) - echo "Potential error or interruption detected." - echo "Restoring the previous Cloudflare IP ranges..." - restore_current_cloudflare_ip_ranges - delete_set_cloudflare_rules "1" - ;; - 4) - echo "Potential error or interruption detected." - echo "Restoring the previous Cloudflare IP ranges..." - delete_new_cloudflare_ip_ranges - restore_current_cloudflare_ip_ranges - delete_set_cloudflare_rules "1" - ;; - 5) - # Continue, as we are too far along to realistically undo anything - ;; - *) - echo "Invalid stage: $stage" - ;; - esac -} +trap 'clean_exit 129' SIGHUP +trap 'clean_exit 130' SIGINT +trap 'clean_exit 143' SIGTERM -####[ Trapping Logic ]################################################################## +####[ Prepping ]############################################################################ -trap 'clean_exit 130' SIGINT -trap 'clean_exit 143' SIGTERM -trap 'clean_exit 129' SIGHUP -trap 'clean_exit 131' SIGQUIT -trap 'clean_exit $?' EXIT +if (( EUID != 0 )); then + echo "${C_ERROR}This script requires root privilege" >&2 + exit 1 +fi -####[ Main ]############################################################################ +####[ Main ]################################################################################ ### @@ -158,55 +111,101 @@ trap 'clean_exit $?' EXIT stage=1 -get_set_cloudflare_ip_ranges -mapfile -t new_cloudflare_ip_ranges < <(curl -s "$C_CLOUDFLARE_IPV4_RANGES_URL") -mapfile -t new_cloudflare_ipv6_ranges < <(curl -s "$C_CLOUDFLARE_IPV6_RANGES_URL") +echo "${C_INFO}Retrieving current Cloudflare IP rules from UFW..." +while IFS= read -r line; do + read -ra fields <<< "$line" + current_cloudflare_ip_ranges+=("${fields[2]}") +done < <(sudo ufw status | grep "Cloudflare IP") +unset fields -new_cloudflare_ip_ranges+=("${new_cloudflare_ipv6_ranges[@]}") -unset new_cloudflare_ipv6_ranges +echo "${C_INFO}Retrieving new Cloudflare IP ranges..." +mapfile -t new_cloudflare_ip_ranges < <( + curl -s "$C_CLOUDFLARE_IPV4_RANGES_URL" + echo "" # Will prevent the last IPv4 and first IPv6 address from being merged. + curl -s "$C_CLOUDFLARE_IPV6_RANGES_URL" +) + +echo "${C_INFO}Creating UFW backup archive at: $C_UFW_BACKUP_ARCHIVE" +tar -C /etc -cf "$C_UFW_BACKUP_ARCHIVE" ufw ### -### [ Opening ports 80 and 443 from any IP address ] +### Add temporary rule to prevent traffic disruption. ### stage=2 -echo "Temporarily opening ports 80 and 443 from any IP address..." -ufw allow from any to any port 80,443 proto tcp comment "Temporary rule" -sleep 1 # Wait for the rule to take effect. +echo "${C_INFO}Temporarily opening ports 80 and 443 from any IP address..." +if ! ufw allow from any to any port 80,443 proto tcp comment "Temporary rule"; then + echo "${C_ERROR}Failed to add temporary rule" >&2 + clean_exit 1 +fi ### -### [ Removing the existing Cloudflare IP ranges ] +### Remove the existing Cloudflare IP ranges to allow new ones. ### stage=3 +echo "${C_NOTE}Waiting '$C_SLEEP_TIME' second for changes to take effect..." +sleep "$C_SLEEP_TIME" if (( ${#current_cloudflare_ip_ranges[@]} != 0 )); then - echo "Removing the existing Cloudflare IP ranges..." - delete_set_cloudflare_rules + echo "${C_INFO}Removing the existing Cloudflare IP ranges..." + + mapfile -t current_cloudflare_rule_numbers < <( + ufw status numbered \ + | grep -E "^\[ *[0-9]+\].*$C_CLOUDFLARE_UFW_COMMENT" \ + | while IFS= read -r line; do + ## Extract the number between brackets (handles both [1] and [ 1] formats). + temp="${line#*[}" + temp="${temp%%]*}" + ## Remove any leading/trailing whitespace. + temp="${temp#"${temp%%[![:space:]]*}"}" + temp="${temp%"${temp##*[![:space:]]}"}" + echo "$temp" + done \ + | sort -rn + ) + + for rule_num in "${current_cloudflare_rule_numbers[@]}"; do + yes | ufw delete "$rule_num" + done fi -sleep 1 # Wait for the rule to take effect. +unset current_cloudflare_rule_numbers ### -### [ Adding the new Cloudflare IP ranges ] +### Add the new Cloudflare IP ranges. ### stage=4 - -echo "Adding the new Cloudflare IPv4 and IPv6 ranges..." -set_new_cloudflare_ip_ranges -sleep 1 # Wait for the rule to take effect. +echo "${C_NOTE}Waiting '$C_SLEEP_TIME' second for changes to take effect..." +sleep "$C_SLEEP_TIME" + +echo "${C_INFO}Adding the new Cloudflare IPv4 and IPv6 ranges..." +for ip in "${new_cloudflare_ip_ranges[@]}"; do + echo "${C_INFO} Adding rule for '$ip'..." + if ! ufw allow from "$ip" to any port 80,443 proto tcp comment \ + "$C_CLOUDFLARE_UFW_COMMENT" >/dev/null + then + echo "${C_ERROR}Failed to add rule for '$ip'" >&2 + fi +done ### -### [ Finalizing ] +### Perform the last modifications to UFW. ### stage=5 +echo "${C_NOTE}Waiting '$C_SLEEP_TIME' second for changes to take effect..." +sleep "$C_SLEEP_TIME" -echo "Removing temporary rules..." -ufw delete allow from any to any port 80,443 proto tcp -sleep 1 # Wait for the rule to take effect. - -echo "Done." +echo "${C_INFO}Removing temporary rules..." +if ! ufw delete allow from any to any port 80,443 proto tcp comment "Temporary rule"; then + echo "${C_ERROR}Failed to remove temporary rule" >&2 + echo "${C_NOTE}Please check your UFW configuration and remove it manually" +fi +sleep "$C_SLEEP_TIME" +echo "${C_NOTE}Waiting '$C_SLEEP_TIME' second for changes to take effect..." +echo "${C_SUCCESS}Finished setting up UFW with Cloudflare IP ranges" +clean_exit 0