diff --git a/dynamic.yml b/dynamic.yml index f286f80..534497a 100644 --- a/dynamic.yml +++ b/dynamic.yml @@ -9,7 +9,7 @@ http: cloudflare: trustedCIDRs: [] overwriteRequestHeader: true - debug: true + debug: false crowdsec: plugin: bouncer: @@ -34,7 +34,7 @@ http: block-direct-access: rule: "HostRegexp(`{host:.+}`)" # Matches any host service: noop@internal - priority: 1 # Low priority to catch unmatched requests + priority: -1 # Low priority to catch unmatched requests entryPoints: - web - websecure diff --git a/scripts/update_cloudflare_ips.py b/scripts/update_cloudflare_ips.py new file mode 100755 index 0000000..b70fa13 --- /dev/null +++ b/scripts/update_cloudflare_ips.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +"""Refresh Cloudflare IP ranges in Traefik config and restart Traefik if needed.""" +from __future__ import annotations + +import hashlib +import subprocess +import sys +from pathlib import Path +from typing import Iterable, List +from urllib.request import Request, urlopen + +CLOUDFLARE_IPV4_URL = "https://www.cloudflare.com/ips-v4" +CLOUDFLARE_IPV6_URL = "https://www.cloudflare.com/ips-v6" +REPO_ROOT = Path(__file__).resolve().parents[1] +TRAEFIK_CONFIG_PATH = REPO_ROOT / "traefik.yml" +ENTRYPOINTS = [" web:", " websecure:"] +USER_AGENT = "TraefikCloudflareUpdater/1.0 (+https://gbanyan.net)" + + +def fetch_ip_ranges() -> List[str]: + ips: List[str] = [] + for url in (CLOUDFLARE_IPV4_URL, CLOUDFLARE_IPV6_URL): + req = Request(url, headers={"User-Agent": USER_AGENT}) + with urlopen(req, timeout=10) as resp: # noqa: S310 (trusted upstream) + for raw_line in resp.read().decode().splitlines(): + line = raw_line.strip() + if line and not line.startswith("#"): + ips.append(line) + # Remove duplicates while preserving order + seen = set() + deduped: List[str] = [] + for ip in ips: + if ip not in seen: + deduped.append(ip) + seen.add(ip) + return deduped + + +def update_trusted_ips(original: str, ips: Iterable[str], entrypoint_label: str) -> str: + try: + entry_idx = original.index(entrypoint_label) + except ValueError: + raise RuntimeError(f"EntryPoint {entrypoint_label.strip()} not found in traefik.yml") from None + + forwarded_token = "forwardedHeaders:\n" + fwd_idx = original.index(forwarded_token, entry_idx) + line_start = original.rfind("\n", 0, fwd_idx) + 1 + indent = original[line_start:fwd_idx] + inner_indent = indent + " " + list_indent = inner_indent + " " + + block_start = fwd_idx + len(forwarded_token) + cursor = block_start + while cursor < len(original): + next_newline = original.find("\n", cursor) + if next_newline == -1: + next_newline = len(original) + line = original[cursor:next_newline] + stripped = line.lstrip() + current_indent = len(line) - len(stripped) + if not stripped.startswith("-") and current_indent <= len(indent): + break + cursor = next_newline + 1 + block_end = cursor + + new_block = forwarded_token + new_block += f"{inner_indent}trustedIPs:\n" + for ip in ips: + new_block += f"{list_indent}- \"{ip}\"\n" + + return original[:fwd_idx] + new_block + original[block_end:] + + +def main() -> None: + ips = fetch_ip_ranges() + config_text = TRAEFIK_CONFIG_PATH.read_text() + before_hash = hashlib.sha256(config_text.encode()).hexdigest() + + for entry_label in ENTRYPOINTS: + config_text = update_trusted_ips(config_text, ips, entry_label) + + after_hash = hashlib.sha256(config_text.encode()).hexdigest() + if after_hash == before_hash: + print("Traefik trusted IP ranges already up-to-date.") + return + + TRAEFIK_CONFIG_PATH.write_text(config_text) + print(f"Updated {TRAEFIK_CONFIG_PATH} with {len(ips)} Cloudflare IP ranges.") + + try: + subprocess.run( + ["docker", "compose", "up", "-d", "traefik"], + check=True, + cwd=REPO_ROOT, + ) + print("Traefik container restarted via docker compose.") + except subprocess.CalledProcessError as exc: # pragma: no cover + print("Failed to restart Traefik via docker compose:", exc, file=sys.stderr) + sys.exit(exc.returncode) + + +if __name__ == "__main__": + try: + main() + except Exception as exc: # noqa: BLE001 + print(f"Error: {exc}", file=sys.stderr) + sys.exit(1) diff --git a/traefik.yml b/traefik.yml index f1769d7..673fcba 100644 --- a/traefik.yml +++ b/traefik.yml @@ -1,7 +1,7 @@ ## STATIC CONFIGURATION log: - level: "DEBUG" + level: "INFO" filePath: "/var/log/traefik/traefik.log" accessLog: filePath: "/var/log/traefik/access.log" @@ -16,10 +16,32 @@ api: entryPoints: web: - address: "10.0.0.225:80" - forwardedHeaders: - insecure: true #traefik-plugin-cloudflare already handle the real-ip from cloudflare to X-Forwarded-For - http: + address: "10.0.0.225:80" + forwardedHeaders: + trustedIPs: + - "173.245.48.0/20" + - "103.21.244.0/22" + - "103.22.200.0/22" + - "103.31.4.0/22" + - "141.101.64.0/18" + - "108.162.192.0/18" + - "190.93.240.0/20" + - "188.114.96.0/20" + - "197.234.240.0/22" + - "198.41.128.0/17" + - "162.158.0.0/15" + - "104.16.0.0/13" + - "104.24.0.0/14" + - "172.64.0.0/13" + - "131.0.72.0/22" + - "2400:cb00::/32" + - "2606:4700::/32" + - "2803:f800::/32" + - "2405:b500::/32" + - "2405:8100::/32" + - "2a06:98c0::/29" + - "2c0f:f248::/32" + http: redirections: # HTTPS redirection (80 to 443) entryPoint: to: "websecure" # The target element @@ -27,7 +49,29 @@ entryPoints: websecure: address: "10.0.0.225:443" forwardedHeaders: - insecure: true + trustedIPs: + - "173.245.48.0/20" + - "103.21.244.0/22" + - "103.22.200.0/22" + - "103.31.4.0/22" + - "141.101.64.0/18" + - "108.162.192.0/18" + - "190.93.240.0/20" + - "188.114.96.0/20" + - "197.234.240.0/22" + - "198.41.128.0/17" + - "162.158.0.0/15" + - "104.16.0.0/13" + - "104.24.0.0/14" + - "172.64.0.0/13" + - "131.0.72.0/22" + - "2400:cb00::/32" + - "2606:4700::/32" + - "2803:f800::/32" + - "2405:b500::/32" + - "2405:8100::/32" + - "2a06:98c0::/29" + - "2c0f:f248::/32" http3: {} internal_web: address: "192.168.50.4:80"