Skip to content

Commit

Permalink
Add Caddy image v2-alpine_rate-limit (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
christoph-bessei authored Oct 8, 2024
1 parent 50091e6 commit 484b66f
Show file tree
Hide file tree
Showing 9 changed files with 228 additions and 0 deletions.
17 changes: 17 additions & 0 deletions .github/workflows/per-commit_publish-image.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
18 changes: 18 additions & 0 deletions .github/workflows/production_publish-image.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
35 changes: 35 additions & 0 deletions examples/v2-alpine_rate-limit/Caddyfile
Original file line number Diff line number Diff line change
@@ -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!"
}
10 changes: 10 additions & 0 deletions examples/v2-alpine_rate-limit/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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"
42 changes: 42 additions & 0 deletions images/v2-alpine_rate-limit/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions images/v2-alpine_rate-limit/bin/abuseipdb_cron.sh
Original file line number Diff line number Diff line change
@@ -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
39 changes: 39 additions & 0 deletions images/v2-alpine_rate-limit/bin/abuseipdb_update.sh
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions images/v2-alpine_rate-limit/bin/microsoft-public-ip-space_cron.sh
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 484b66f

Please sign in to comment.