diff --git a/.github/workflows/per-commit_publish-image.yaml b/.github/workflows/per-commit_publish-image.yaml index 90c4c99..c321f3e 100644 --- a/.github/workflows/per-commit_publish-image.yaml +++ b/.github/workflows/per-commit_publish-image.yaml @@ -39,6 +39,23 @@ jobs: push: true tags: ${{ vars.CI_REGISTRY_IMAGE }}:${{ github.sha }}_v2-alpine_cloudflare # Build a custom Caddy docker image with the modules listed below & push it to Quay.io + # * http.handlers.rate_limit: https://github.com/mholt/caddy-ratelimit + v2-alpine_rate-limit: + runs-on: ubuntu-latest + steps: + - name: "Login to ${{ vars.CI_REGISTRY }}" + uses: docker/login-action@v3 + with: + registry: ${{ vars.CI_REGISTRY }} + username: ${{ secrets.CI_REGISTRY_USER }} + password: ${{ secrets.CI_REGISTRY_PASSWORD }} + - name: Build and push + uses: docker/build-push-action@v5 + with: + file: images/v2-alpine_rate-limit/Dockerfile + push: true + tags: ${{ vars.CI_REGISTRY_IMAGE }}:${{ github.sha }}_v2-alpine_rate-limit + # Build a custom Caddy docker image with the modules listed below & push it to Quay.io # * dns.providers.cloudflare: https://github.com/caddy-dns/cloudflare # * http.handlers.rate_limit: https://github.com/mholt/caddy-ratelimit v2-alpine_cloudflare_rate-limit: diff --git a/.github/workflows/production_publish-image.yaml b/.github/workflows/production_publish-image.yaml index f15bbcd..6fe36d0 100644 --- a/.github/workflows/production_publish-image.yaml +++ b/.github/workflows/production_publish-image.yaml @@ -43,6 +43,24 @@ jobs: push: true tags: ${{ vars.CI_REGISTRY_IMAGE }}:v2-alpine_cloudflare # Build a custom Caddy docker image with the modules listed below & push it to Quay.io + # * http.handlers.rate_limit: https://github.com/mholt/caddy-ratelimit + v2-alpine_rate-limit: + runs-on: ubuntu-latest + environment: Production + steps: + - name: "Login to ${{ vars.CI_REGISTRY }}" + uses: docker/login-action@v3 + with: + registry: ${{ vars.CI_REGISTRY }} + username: ${{ secrets.CI_REGISTRY_USER }} + password: ${{ secrets.CI_REGISTRY_PASSWORD }} + - name: Build and push + uses: docker/build-push-action@v5 + with: + file: images/v2-alpine_rate-limit/Dockerfile + push: true + tags: ${{ vars.CI_REGISTRY_IMAGE }}:v2-alpine_rate-limit + # Build a custom Caddy docker image with the modules listed below & push it to Quay.io # * dns.providers.cloudflare: https://github.com/caddy-dns/cloudflare # * http.handlers.rate_limit: https://github.com/mholt/caddy-ratelimit v2-alpine_cloudflare_rate-limit: diff --git a/examples/v2-alpine_rate-limit/Caddyfile b/examples/v2-alpine_rate-limit/Caddyfile new file mode 100644 index 0000000..ca4079f --- /dev/null +++ b/examples/v2-alpine_rate-limit/Caddyfile @@ -0,0 +1,35 @@ +# +# This example adds a rate limit for all IPs in "abuseipdb/s100-7d.ipv4.caddyfile" and "127.18.0.1" +# Check https://github.com/mholt/caddy-ratelimit for more details about the rate_limit module +# +:80 { + log { + output stdout + level DEBUG + } + + # Respond with a custom error message in case we hit a rate limit + handle_errors 429 { + respond "Too many requests!" + } + + @rateLimitedIPs { + # Add your own IP here to check if the rate limit works as expected + remote_ip 172.18.0.1 + import abuseipdb/s100-7d.ipv4.caddyfile + import microsoft-public-ip-space/current.caddyfile + } + + rate_limit @rateLimitedIPs { + zone default { + # If you're behind a reverse proxy, you should probably use client_ip instead of remote_ip: + # https://github.com/mholt/caddy-ratelimit/issues/19 + key {remote_ip} + events 10 + window 60s + } + } + + # No rate limit hit + respond "It works!" +} diff --git a/examples/v2-alpine_rate-limit/docker-compose.yml b/examples/v2-alpine_rate-limit/docker-compose.yml new file mode 100644 index 0000000..8393da2 --- /dev/null +++ b/examples/v2-alpine_rate-limit/docker-compose.yml @@ -0,0 +1,10 @@ +services: + caddy: + build: + context: ../../ + dockerfile: ./images/v2-alpine_rate-limit/Dockerfile + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile + ports: + - "31080:80" + - "31443:443" diff --git a/images/v2-alpine_rate-limit/Dockerfile b/images/v2-alpine_rate-limit/Dockerfile new file mode 100644 index 0000000..ef75200 --- /dev/null +++ b/images/v2-alpine_rate-limit/Dockerfile @@ -0,0 +1,42 @@ +FROM caddy:2-builder-alpine AS builder + +# Build Caddy with required plugins +RUN xcaddy build \ + --with github.com/mholt/caddy-ratelimit + +FROM caddy:2-alpine + +# Defines in which directory the blocklist should be stored. A subdirectory of /etc/caddy is recommended so the blocklist can be used in a Caddyfile (import abuseipdb/...) +ENV ABUSE_IP_DB_LOCAL_BASE_DIRECTORY="/etc/caddy/abuseipdb" +# The filename where the blocklist is stored inside of ABUSE_IP_DB_LOCAL_BASE_DIRECTORY +ENV ABUSE_IP_DB_LOCAL_FILENAME="s100-7d.ipv4.caddyfile" +# We use the 7d blocklist, because it's a good mix of "up to date" and "too shortlived" +# Check https://github.com/borestad/blocklist-abuseipdb for all available options +ENV ABUSE_IP_DB_REMOTE_FILENAME="abuseipdb-s100-7d.ipv4" +# As the minimum expected IPs/rows we use 20000, on 2024-10-04 the blocklist had 47703 rows so this should be safe +ENV ABUSE_IP_DB_MINIMUM_ENTRY_COUNT=20000 + +# Defines in which directory the Microsoft Public IP space should be stored. +# A subdirectory of /etc/caddy is recommended so the blocklist can be used in a Caddyfile (import microsoft-public-ip-space/...) +ENV MICROSOFT_PUBLIC_IP_SPACE_LOCAL_BASE_DIRECTORY="/etc/caddy/microsoft-public-ip-space" +# The filename where the Microsoft Public IP Space is stored inside of MICROSOFT_PUBLIC_IP_SPACE_LOCAL_BASE_DIRECTORY +ENV MICROSOFT_PUBLIC_IP_SPACE_LOCAL_FILENAME="current.caddyfile" + +# Copy the caddy binary from the builder image +COPY --from=builder /usr/bin/caddy /usr/bin/caddy + +# Add AbuseIPDB scripts +COPY /images/v2-alpine_rate-limit/bin/abuseipdb_cron.sh /usr/local/bin/ +COPY /images/v2-alpine_rate-limit/bin/abuseipdb_update.sh /usr/local/bin/ +# Ensure the AbuseIPDB base directory exists +RUN mkdir "${ABUSE_IP_DB_LOCAL_BASE_DIRECTORY}" +# Download & process the selected AbuseIPDB blocklist +RUN /usr/local/bin/abuseipdb_update.sh + +# Add Microsoft Public IP Space scripts +COPY /images/v2-alpine_rate-limit/bin/microsoft-public-ip-space_cron.sh /usr/local/bin/ +COPY /images/v2-alpine_rate-limit/bin/microsoft-public-ip-space_update.sh /usr/local/bin/ +# Ensure the Microsoft Public IP Space base directory exists +RUN mkdir "${MICROSOFT_PUBLIC_IP_SPACE_LOCAL_BASE_DIRECTORY}" +# Download & process the Microsoft Public IP space +RUN /usr/local/bin/microsoft-public-ip-space_update.sh diff --git a/images/v2-alpine_rate-limit/bin/abuseipdb_cron.sh b/images/v2-alpine_rate-limit/bin/abuseipdb_cron.sh new file mode 100755 index 0000000..196e4e2 --- /dev/null +++ b/images/v2-alpine_rate-limit/bin/abuseipdb_cron.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +# +# Execute this script with a cron job on the host system to keep the AbuseIPDB list up to date without pulling the latest image +# + +# Update the AbuseIPDB blocklist +/usr/local/bin/abuseipdb_update.sh +# Reload caddy to apply the new updated AbuseIPDB list +caddy reload --config /etc/caddy/Caddyfile diff --git a/images/v2-alpine_rate-limit/bin/abuseipdb_update.sh b/images/v2-alpine_rate-limit/bin/abuseipdb_update.sh new file mode 100755 index 0000000..e2adf29 --- /dev/null +++ b/images/v2-alpine_rate-limit/bin/abuseipdb_update.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +# +# This script expects the following environment variables to exist: +# ABUSE_IP_DB_LOCAL_BASE_DIRECTORY +# ABUSE_IP_DB_LOCAL_FILENAME +# ABUSE_IP_DB_REMOTE_FILENAME +# ABUSE_IP_DB_MINIMUM_ENTRY_COUNT +# +# Check the Dockerfile for a detailed explanation of these environment variables +# + +DOWNLOAD_URL="https://raw.githubusercontent.com/borestad/blocklist-abuseipdb/refs/heads/main/${ABUSE_IP_DB_REMOTE_FILENAME}" +OUTPUT_FILE="${ABUSE_IP_DB_LOCAL_BASE_DIRECTORY}/${ABUSE_IP_DB_LOCAL_FILENAME}" + +# Download the AbuseIPDB blocklist +TEMP_DOWNLOAD_FILE=$(mktemp) +if ! wget "${DOWNLOAD_URL}" -O "${TEMP_DOWNLOAD_FILE}" +then + echo "Failed to download the AbuseIPDB blocklist" + exit 1 +fi + +LINE_COUNT=$(wc -l < "${TEMP_DOWNLOAD_FILE}") +if [ "${LINE_COUNT}" -lt "${ABUSE_IP_DB_MINIMUM_ENTRY_COUNT}" ] +then + echo "Too few IPs in the list (${TEMP_DOWNLOAD_FILE}). Expected: ${ABUSE_IP_DB_MINIMUM_ENTRY_COUNT} Actual: ${LINE_COUNT}" + exit 1 +fi + +echo "Successfully downloaded the AbuseIPDB blocklist to ${TEMP_DOWNLOAD_FILE}" + +# Truncate the output file, otherwise running this script multiple times would append the result every time +true > "${OUTPUT_FILE}" + +# Loop through each IP in the AbuseIPDB file, excluding comments, and add remote_ip before each IP so it can be imported in a Caddyfile +for IP in $(grep -hv '^#' "${TEMP_DOWNLOAD_FILE}"); do + echo "remote_ip $IP" >> "${OUTPUT_FILE}" +done diff --git a/images/v2-alpine_rate-limit/bin/microsoft-public-ip-space_cron.sh b/images/v2-alpine_rate-limit/bin/microsoft-public-ip-space_cron.sh new file mode 100755 index 0000000..3349311 --- /dev/null +++ b/images/v2-alpine_rate-limit/bin/microsoft-public-ip-space_cron.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +# +# Execute this script with a cron job on the host system to keep the Microsoft Public IP Space up to date without pulling the latest image +# + +# Update the AbuseIPDB blocklist +/usr/local/bin/microsoft-public-ip-space_update.sh +# Reload caddy to apply the new updated AbuseIPDB list +caddy reload --config /etc/caddy/Caddyfile diff --git a/images/v2-alpine_rate-limit/bin/microsoft-public-ip-space_update.sh b/images/v2-alpine_rate-limit/bin/microsoft-public-ip-space_update.sh new file mode 100755 index 0000000..bafedb3 --- /dev/null +++ b/images/v2-alpine_rate-limit/bin/microsoft-public-ip-space_update.sh @@ -0,0 +1,47 @@ +#!/bin/sh + +# Microsoft landing page which contains the URL to the current public IP CSV +PAGE_URL="https://www.microsoft.com/en-us/download/confirmation.aspx?id=53602" + +# Microsoft blocks requests from wget without a valid user agent, so we fake one +USER_AGENT="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.60 Safari/537.36" + +# Output file name +OUTPUT_FILE="${MICROSOFT_PUBLIC_IP_SPACE_LOCAL_BASE_DIRECTORY}/${MICROSOFT_PUBLIC_IP_SPACE_LOCAL_FILENAME}" + +# Fetch the confirmation page content +PAGE_CONTENT=$(wget --user-agent="$USER_AGENT" -q -O - "${PAGE_URL}") + +# Determine the current CSV URL and make sure it's the right download link +LATEST_CSV_URL=$(echo "${PAGE_CONTENT}" | grep -i 'data-bi-containername="download retry"' | grep -oE 'https://download\.microsoft\.com/download/[^"]+\.csv') + +if [ -z "${LATEST_CSV_URL}" ]; then + echo "Failed to determine the latest CSV URL" + exit 1 +fi + +LATEST_CSV_DATA=$(wget --user-agent="$USER_AGENT" -q -O - "${LATEST_CSV_URL}") + +LINE_COUNT=$(echo "${LATEST_CSV_DATA}" | wc -l) +if [ "${LINE_COUNT}" -lt 100 ] +then + echo "Too few IPs in the list. Expected: 100 Actual: ${LINE_COUNT}" + exit 1 +fi + +# Truncate the output file, otherwise running this script multiple times would append the result every time +true > "${OUTPUT_FILE}" + +# Remove header row from the CSV +LATEST_CSV_DATA=$(echo "${LATEST_CSV_DATA}" | tail -n +2) + +# Get first column (IP range) +LATEST_CSV_DATA=$(echo "${LATEST_CSV_DATA}" | cut -d',' -f1) + +# Filter IPv6 addresses +LATEST_CSV_DATA=$(echo "${LATEST_CSV_DATA}" | grep -v ':') + +echo "${LATEST_CSV_DATA}" | while read -r IP_RANGE +do + echo "remote_ip ${IP_RANGE}" >> "${OUTPUT_FILE}" +done