diff --git a/.github/.dependabot.yml b/.github/.dependabot.yml index 2f27b1c..5c943b0 100644 --- a/.github/.dependabot.yml +++ b/.github/.dependabot.yml @@ -1,9 +1,35 @@ version: 2 updates: + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "weekly" + commit-message: + prefix: "chore:" + include: "scope" + labels: + - "dependencies" + - "docker" + - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "daily" + interval: "weekly" + commit-message: + prefix: "chore:" + include: "scope" + labels: + - "dependencies" + - "github-actions" + + - package-ecosystem: "github-actions" + directory: "/.github/workflows" + schedule: + interval: "weekly" commit-message: prefix: "chore:" include: "scope" + labels: + - "dependencies" + - "github-actions" + - "workflows" \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f1f0b80..6d13c01 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,15 +13,19 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: actions/checkout@v3 + - name: Checkout repository + uses: actions/checkout@v3 - - name: Set up BATS - run: | - git clone https://github.com/bats-core/bats-core.git - cd bats-core - sudo ./install.sh /usr/local + - name: Set up BATS + run: | + sudo apt-get update + sudo apt-get install -y bats - - name: Run tests - run: | - bats test_cf_ufw.bats + - name: Set up test environment + run: | + sudo apt-get install -y ufw curl + + - name: Run tests + run: | + cd tests + sudo bats cloudflare_ufw_updater.bats \ No newline at end of file diff --git a/.gitignore b/.gitignore index b6e4761..d77b176 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ __pycache__/ # C extensions *.so +.idea +.vscode # Distribution / packaging .Python diff --git a/Dockerfile b/Dockerfile index a1c9530..12c55eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,17 @@ -# Use a lightweight base image with shell capabilities -FROM alpine:latest +FROM ubuntu:latest -# Install curl and ufw (Uncomplicated Firewall) -RUN apk add --no-cache curl ufw +# Install dependencies +RUN apt-get update && \ + apt-get install -y ufw curl -# Copy the script into the container -COPY cf_ufw.sh /cf_ufw.sh +# Create a directory for the script +RUN mkdir /app -# Set the script as executable -RUN chmod +x /cf_ufw.sh +# Copy the script to the container +COPY cloudflare-ufw-updater.sh /app/ -# Set the entrypoint to run the script -ENTRYPOINT ["/cf_ufw.sh"] +# Make the script executable +RUN chmod +x /app/cloudflare-ufw-updater.sh + +# Set the entrypoint to the script +ENTRYPOINT ["/app/cloudflare-ufw-updater.sh"] \ No newline at end of file diff --git a/Dockerfile.test b/Dockerfile.test deleted file mode 100644 index 8087924..0000000 --- a/Dockerfile.test +++ /dev/null @@ -1,18 +0,0 @@ -# Use a base image with shell capabilities -FROM ubuntu:latest - -# Install curl, ufw, git, and other dependencies -RUN apt-get update && \ - apt-get install -y curl ufw git - -# Install BATS -RUN git clone https://github.com/bats-core/bats-core.git && \ - cd bats-core && \ - ./install.sh /usr/local - -# Copy the script and the test file into the container -COPY cf_ufw.sh /cf_ufw.sh -COPY test_cf_ufw.bats /test_cf_ufw.bats - -# Set the entrypoint to run BATS with the test file -ENTRYPOINT ["bats", "/test_cf_ufw.bats"] diff --git a/README.md b/README.md index 13a8662..cb2e7e8 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,105 @@ # Cloudflare UFW Updater -This repository contains a script `cf_ufw.sh` that automatically updates UFW rules to allow only HTTP and HTTPS traffic from Cloudflare IP addresses, ensuring a secure and up-to-date firewall. +A Bash script to automatically update UFW (Uncomplicated Firewall) rules to allow incoming HTTP and HTTPS traffic only from Cloudflare IP addresses. + +## Features + +- Fetches the latest Cloudflare IP addresses (IPv4 and IPv6) from the official Cloudflare API. +- Updates UFW rules to allow incoming traffic on specified ports only from Cloudflare IP addresses. +- Supports customization through a configuration file or environment variables. +- Provides logging functionality to track script execution and any errors encountered. +- Includes error handling and dependency checks to ensure smooth operation. +- Supports backup and restore of UFW rules for easy rollback if needed. ## Prerequisites -- UFW (Uncomplicated Firewall) installed and enabled -- `curl` command-line tool installed +- UFW (Uncomplicated Firewall) installed and enabled on your system. +- Bash shell environment. +- `curl` command-line tool for fetching Cloudflare IP addresses. ## Installation -1. Clone the repository: - -`git clone https://github.com/yourusername/cloudflare-ufw-updater.git` +1. Clone the repository or download the script file: -2. Change to the repository directory: + ```bash + git clone https://github.com/yourusername/cloudflare-ufw-updater.git + ``` -`cd cloudflare-ufw-updater` +2. Make the script executable: -3. Make the script executable: + ```bash + chmod +x cloudflare-ufw-updater.sh + ``` -`chmod +x cf_ufw.sh` +3. (Optional) Create a configuration file at `/etc/cloudflare-ufw-updater.conf` to customize the script's behavior. See the "Configuration" section for more details. ## Usage -You can run the script manually: +Run the script with root privileges: -`./cf_ufw.sh` +```bash +sudo ./cloudflare-ufw-updater.sh +``` -To schedule the script to run automatically every day, follow these steps: +The script will fetch the latest Cloudflare IP addresses, update the UFW rules, and reload UFW to apply the changes. -1. Open the root user's crontab: +## Configuration -`sudo crontab -e` +The script can be customized through a configuration file or environment variables. -2. Add the following line to the end of the file, replacing `/path/to/script` with the actual path to the `cf_ufw.sh` script: +### Configuration File -`@daily /path/to/script/cf_ufw.sh &> /dev/null` +Create a configuration file at `/etc/cloudflare-ufw-updater.conf` with the following variables: -3. Save and exit the editor. The script will now run once a day, updating your UFW rules to the latest Cloudflare IP ranges. +```bash +# Cloudflare IP address URLs +CLOUDFLARE_IPV4_URL="https://www.cloudflare.com/ips-v4" +CLOUDFLARE_IPV6_URL="https://www.cloudflare.com/ips-v6" -## Inspired by -[https://github.com/Paul-Reed/cloudflare-ufw/blob/master/cloudflare-ufw.sh](https://github.com/Paul-Reed/cloudflare-ufw/blob/master/cloudflare-ufw.sh) +# Allowed HTTP/HTTPS ports +ALLOWED_HTTP_PORTS="80,443,8080" -[https://github.com/jakejarvis/cloudflare-ufw-updater/](https://github.com/jakejarvis/cloudflare-ufw-updater/) +# Cloudflare UFW rule label +CLOUDFLARE_RULE_LABEL="Cloudflare" -## Contributing +# Log file path +LOG_FILE="/var/log/cloudflare-ufw-updater.log" + +# Backup file path +BACKUP_FILE="/etc/ufw/cloudflare-ufw-updater.backup" +``` + +Adjust the values according to your requirements. + +### Environment Variables + +You can also set configuration values using environment variables. The script will prioritize environment variables over the values in the configuration file. + +Example: -If you'd like to contribute to this project, please submit a pull request with your changes or open an issue to discuss your ideas. +```bash +LOG_FILE="/path/to/custom/log.txt" ./cloudflare-ufw-updater.sh +``` + +## Logging + +The script logs its execution details and any errors encountered to the specified log file (default: `/var/log/cloudflare-ufw-updater.log`). You can customize the log file path in the configuration file or through the `LOG_FILE` environment variable. + +## Backup and Restore + +The script automatically creates a backup of the current UFW rules before making any changes. The backup file is stored at the path specified by the `BACKUP_FILE` variable (default: `/etc/ufw/cloudflare-ufw-updater.backup`). + +To restore the UFW rules from the backup file, run the script with the `--restore` flag: + +```bash +sudo ./cloudflare-ufw-updater.sh --restore +``` ## License -This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for more information. +This script is released under the [MIT License](LICENSE). + +## Contributing + +Contributions are welcome! If you find any issues or have suggestions for improvements, please open an issue or submit a pull request on the [GitHub repository](https://github.com/yourusername/cloudflare-ufw-updater). +```` \ No newline at end of file diff --git a/cf_ufw.sh b/cf_ufw.sh deleted file mode 100644 index 58e32b6..0000000 --- a/cf_ufw.sh +++ /dev/null @@ -1,72 +0,0 @@ -#!/bin/sh - -# This script updates the UFW rules to permit only HTTP and HTTPS traffic originating from Cloudflare IP addresses. -# For further information and documentation, visit: -# https://github.com/thomasvincent/cloudflare-ufw-updater/blob/master/README.md - -set -eu - -# Define variables -CLOUDFLARE_IP_FILE=$(mktemp) -CLOUDFLARE_IPV4_URL="https://www.cloudflare.com/ips-v4" -CLOUDFLARE_IPV6_URL="https://www.cloudflare.com/ips-v6" -ALLOWED_HTTP_PORTS="80,443" -CLOUDFLARE_RULE_LABEL="Cloudflare" - -# Check dependencies -check_dependencies() { - for cmd in ufw curl; do - if ! command -v "$cmd" > /dev/null; then - printf "Command not found in PATH: %s\n" "$cmd" - exit 1 - fi - done -} - -# Check permissions -check_permissions() { - if [ "$(id -u)" -ne 0 ]; then - printf "This script must be run as root. Aborting.\n" - exit 1 - fi -} - -# Fetch latest IPv4 addresses -fetch_ipv4_addresses() { - if ! curl -s --retry 3 --retry-delay 5 "${CLOUDFLARE_IPV4_URL}" > "$CLOUDFLARE_IP_FILE"; then - printf "Failed to fetch IPv4 addresses: %s\n" "$(curl -s -o /dev/null -w %{http_code})" - exit 1 - fi -} - -# Fetch latest IPv6 addresses -fetch_ipv6_addresses() { - if ! curl -s --retry 3 --retry-delay 5 "${CLOUDFLARE_IPV6_URL}" >> "$CLOUDFLARE_IP_FILE"; then - printf "Failed to fetch IPv6 addresses: %s\n" "$(curl -s -o /dev/null -w %{http_code})" - exit 1 - fi -} - -# Update UFW rules -update_ufw_rules() { - while IFS= read -r ip; do - ufw allow from "$ip" to any port "$ALLOWED_HTTP_PORTS" proto tcp comment "$CLOUDFLARE_RULE_LABEL" - done < "$CLOUDFLARE_IP_FILE" -} - -# Main function -main() { - check_dependencies - check_permissions - fetch_ipv4_addresses - fetch_ipv6_addresses - - # Optional: Close temporary file explicitly - # cat "$CLOUDFLARE_IP_FILE" | update_ufw_rules - - update_ufw_rules - rm -f "$CLOUDFLARE_IP_FILE" # Ensure removal even on errors - ufw reload -} - -main diff --git a/cloudflare-ufw-updater.conf b/cloudflare-ufw-updater.conf new file mode 100644 index 0000000..dced00a --- /dev/null +++ b/cloudflare-ufw-updater.conf @@ -0,0 +1,20 @@ +# Cloudflare UFW Updater Configuration File + +# URLs to fetch the Cloudflare IP addresses +CLOUDFLARE_IPV4_URL="https://www.cloudflare.com/ips-v4" +CLOUDFLARE_IPV6_URL="https://www.cloudflare.com/ips-v6" + +# Ports to allow HTTP and HTTPS traffic +ALLOWED_HTTP_PORTS="80,443" + +# Label used for UFW rules +CLOUDFLARE_RULE_LABEL="Cloudflare" + +# Path to the log file +LOG_FILE="/var/log/cloudflare-ufw-updater.log" + +# Path to the backup file for UFW rules +BACKUP_FILE="/etc/ufw/cloudflare-ufw-updater.backup" + +# Optional: Define the minimum required version of UFW +MIN_UFW_VERSION="0.36" diff --git a/cloudflare-ufw-updater.sh b/cloudflare-ufw-updater.sh new file mode 100644 index 0000000..2f63b8d --- /dev/null +++ b/cloudflare-ufw-updater.sh @@ -0,0 +1,174 @@ +#!/bin/bash + +# MIT License +# +# Copyright (c) 2022-2024 Thomas Vincent +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +# This script updates the UFW rules to permit only HTTP and HTTPS traffic +# originating from Cloudflare IP addresses. For further information and +# documentation, visit: +# https://github.com/thomasvincent/cloudflare-ufw-updater/blob/master/README.md + +set -euo pipefail + +# Check Bash version compatibility - Mac OS X ships with 3.2.* for several releases. Use brew bash. =( +if [[ "${BASH_VERSINFO[0]}" -lt 4 ]]; then + echo "This script requires Bash version 4.0 or higher. You are using Bash version ${BASH_VERSION}. Please upgrade your Bash." >&2 + exit 1 +fi + +readonly CLOUDFLARE_IP_FILE="$(mktemp)" +readonly CLOUDFLARE_IPV4_URL="https://www.cloudflare.com/ips-v4" +readonly CLOUDFLARE_IPV6_URL="https://www.cloudflare.com/ips-v6" +readonly ALLOWED_HTTP_PORTS="80,443" +readonly CLOUDFLARE_RULE_LABEL="Cloudflare" +readonly LOG_FILE="/var/log/cloudflare-ufw-updater.log" +readonly CONFIG_FILE="/etc/cloudflare-ufw-updater.conf" +readonly BACKUP_FILE="/etc/ufw/cloudflare-ufw-updater.backup" +readonly MIN_UFW_VERSION="0.36" + +# Clean up temporary files on exit +trap 'rm -f "$CLOUDFLARE_IP_FILE"' EXIT + +check_dependencies() { + local missing=0 + for cmd in ufw curl; do + if ! command -v "$cmd" &>/dev/null; then + log_error "Command not found in PATH: $cmd" + missing=1 + fi + done + if (( missing )); then + exit 1 + fi +} + +check_permissions() { + if (( EUID != 0 )); then + log_error "This script must be run as root. Aborting." + exit 1 + fi +} + +check_ufw_version() { + local ufw_version + ufw_version="$(ufw --version | awk '{print $2}')" + if ! version_greater_equal "$ufw_version" "$MIN_UFW_VERSION"; then + log_error "UFW version $ufw_version is not compatible. Minimum required version is $MIN_UFW_VERSION." + exit 1 + fi +} + +version_greater_equal() { + printf '%s\n%s' "$1" "$2" | sort -C -V +} + +fetch_addresses() { + local url="$1" + if ! curl -s --retry 3 --retry-delay 5 "$url" >> "$CLOUDFLARE_IP_FILE"; then + log_error "Failed to fetch addresses from $url" + exit 1 + fi +} + +update_ufw_rules() { + # Delete existing Cloudflare rules + ufw delete allow from any to any port "$ALLOWED_HTTP_PORTS" proto tcp comment "$CLOUDFLARE_RULE_LABEL" + + while IFS= read -r ip; do + ufw allow from "$ip" to any port "$ALLOWED_HTTP_PORTS" proto tcp comment "$CLOUDFLARE_RULE_LABEL" + log_message "Allowing traffic from $ip to ports $ALLOWED_HTTP_PORTS" + done < "$CLOUDFLARE_IP_FILE" +} + +log_message() { + printf "%s - %s\n" "$(date +"%Y-%m-%d %H:%M:%S")" "$1" | tee -a "$LOG_FILE" +} + +log_error() { + printf "%s - [ERROR] %s\n" "$(date +"%Y-%m-%d %H:%M:%S")" "$1" | tee -a "$LOG_FILE" >&2 +} + +load_config() { + if [[ -f "$CONFIG_FILE" ]]; then + # shellcheck source=/etc/cloudflare-ufw-updater.conf + source "$CONFIG_FILE" + fi + + # Override config values with environment variables if set + CLOUDFLARE_IPV4_URL="${CLOUDFLARE_IPV4_URL:-$CLOUDFLARE_IPV4_URL}" + CLOUDFLARE_IPV6_URL="${CLOUDFLARE_IPV6_URL:-$CLOUDFLARE_IPV6_URL}" + ALLOWED_HTTP_PORTS="${ALLOWED_HTTP_PORTS:-$ALLOWED_HTTP_PORTS}" + CLOUDFLARE_RULE_LABEL="${CLOUDFLARE_RULE_LABEL:-$CLOUDFLARE_RULE_LABEL}" + LOG_FILE="${LOG_FILE:-$LOG_FILE}" + BACKUP_FILE="${BACKUP_FILE:-$BACKUP_FILE}" +} + +backup_ufw_rules() { + ufw status numbered | tee "$BACKUP_FILE" + log_message "Backed up UFW rules to $BACKUP_FILE" +} + +restore_ufw_rules() { + if [[ -f "$BACKUP_FILE" ]]; then + ufw reset 1>/dev/null + while read -r rule; do + # Skip comment lines + if [[ $rule =~ ^\s*# ]]; then + continue + fi + ufw "$rule" + done < "$BACKUP_FILE" + log_message "Restored UFW rules from $BACKUP_FILE" + else + log_error "Backup file not found: $BACKUP_FILE" + fi +} + +main() { + check_dependencies + check_permissions + check_ufw_version + load_config + + if [[ "$1" == "--restore" ]]; then + restore_ufw_rules + exit 0 + fi + + log_message "Starting Cloudflare UFW Updater" + + log_message "Fetching Cloudflare IP addresses..." + fetch_addresses "$CLOUDFLARE_IPV4_URL" + fetch_addresses "$CLOUDFLARE_IPV6_URL" + + backup_ufw_rules + + log_message "Updating UFW rules..." + update_ufw_rules + + log_message "Reloading UFW..." + ufw reload + + log_message "UFW rules updated successfully." +} + +main "$@" \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index c4a825f..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,7 +0,0 @@ -version: "3.9" - -services: - bats_test: - build: - context: . - dockerfile: Dockerfile.test diff --git a/test_cf_ufw.bats b/test_cf_ufw.bats deleted file mode 100644 index 82cb291..0000000 --- a/test_cf_ufw.bats +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bats - -@test "Check if the script fetches IP ranges and updates UFW rules" { - # Set up a temporary UFW rules file for testing - export UFW_RULES_FILE="$(mktemp)" - echo "# temporary UFW rules file for testing" > "$UFW_RULES_FILE" - - # Replace the `ufw` command with a mock that appends the rule to the temporary file - function ufw() { - echo "$@" >> "$UFW_RULES_FILE" - } - - # Run the script - ./cf_ufw.sh - - # Check if the temporary UFW rules file contains the expected rules - grep -q "allow from" "$UFW_RULES_FILE" - [ "$?" -eq 0 ] - - # Clean up - unset UFW_RULES_FILE - unalias ufw -} diff --git a/tests/cloudflare_ufw_updater.bats b/tests/cloudflare_ufw_updater.bats new file mode 100755 index 0000000..79014d3 --- /dev/null +++ b/tests/cloudflare_ufw_updater.bats @@ -0,0 +1,32 @@ +#!/usr/bin/env bats + +setup() { + # Set up the test environment + docker-compose up -d + docker cp ../cloudflare-ufw-updater.sh cloudflare-ufw-updater:/app/ + docker exec cloudflare-ufw-updater chmod +x /app/cloudflare-ufw-updater.sh +} + +teardown() { + # Clean up the test environment + docker-compose down +} + +@test "Cloudflare UFW Updater script updates UFW rules" { + run docker exec cloudflare-ufw-updater /app/cloudflare-ufw-updater.sh + assert_success + assert_output --partial "UFW rules updated successfully" +} + +@test "Cloudflare UFW Updater script backs up UFW rules" { + run docker exec cloudflare-ufw-updater /app/cloudflare-ufw-updater.sh + assert_success + run docker exec cloudflare-ufw-updater ls /etc/ufw/cloudflare-ufw-updater.backup + assert_success +} + +@test "Cloudflare UFW Updater script restores UFW rules from backup" { + run docker exec cloudflare-ufw-updater /app/cloudflare-ufw-updater.sh --restore + assert_success + assert_output --partial "Restored UFW rules from backup" +} \ No newline at end of file diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml new file mode 100644 index 0000000..9b84c26 --- /dev/null +++ b/tests/docker-compose.yml @@ -0,0 +1,9 @@ +version: '3' +services: + cloudflare-ufw-updater: + image: ubuntu:latest + container_name: cloudflare-ufw-updater + tty: true + volumes: + - ./:/app + command: tail -f /dev/null \ No newline at end of file