diff --git a/config/ansible/files/generate_country_blocks.py b/config/ansible/files/generate_country_blocks.py new file mode 100644 index 0000000..dd150c4 --- /dev/null +++ b/config/ansible/files/generate_country_blocks.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +""" +Generate country IP ranges for Caddy blocking configuration. +Downloads the latest country IP ranges and formats them for use in Caddyfile. +""" + +import json +import requests +import sys +import argparse +from typing import List, Dict +import ipaddress + + +def download_country_ranges(countries: List[str]) -> Dict[str, List[str]]: + """ + Download IP ranges for specified countries from ip2location.com free database. + + Args: + countries: List of ISO 3166-1 alpha-2 country codes (e.g., ['CN', 'RU']) + + Returns: + Dictionary mapping country codes to lists of IP ranges + """ + country_ranges = {} + + for country in countries: + print(f"Downloading IP ranges for {country}...", file=sys.stderr) + + # Use ipinfo.io's free API for country IP ranges + try: + url = f"https://ipinfo.io/countries/{country.lower()}" + headers = {'User-Agent': 'Mozilla/5.0 (compatible; country-blocker/1.0)'} + response = requests.get(url, headers=headers, timeout=30) + + if response.status_code == 200: + # The response contains IP ranges in CIDR format, one per line + ranges = [] + for line in response.text.strip().split('\n'): + line = line.strip() + if line and not line.startswith('#'): + try: + # Validate the IP range + ipaddress.ip_network(line, strict=False) + ranges.append(line) + except ValueError: + continue + + country_ranges[country] = ranges + print(f"Found {len(ranges)} IP ranges for {country}", file=sys.stderr) + else: + print(f"Failed to download ranges for {country}: HTTP {response.status_code}", file=sys.stderr) + + except Exception as e: + print(f"Error downloading ranges for {country}: {e}", file=sys.stderr) + + return country_ranges + + +def get_alternative_ranges(countries: List[str]) -> Dict[str, List[str]]: + """ + Alternative method using a different data source. + Falls back to manually curated ranges for common blocking scenarios. + """ + # Common IP ranges for frequently blocked countries + # These are examples - you should update with current ranges + manual_ranges = { + 'CN': [ + '1.0.1.0/24', '1.0.2.0/23', '1.0.8.0/21', '1.0.32.0/19', + '14.0.12.0/22', '14.0.36.0/22', '14.1.0.0/18', '14.16.0.0/12', + '27.0.0.0/11', '27.50.40.0/21', '27.54.192.0/18', '27.98.208.0/20', + # Add more ranges as needed + ], + 'RU': [ + '2.56.96.0/19', '2.58.56.0/21', '5.8.0.0/19', '5.34.96.0/19', + '5.42.192.0/19', '5.44.64.0/18', '5.53.96.0/19', '5.61.24.0/21', + # Add more ranges as needed + ], + 'KP': [ + '175.45.176.0/22', '210.52.109.0/24', '77.94.35.0/24' + ], + 'IR': [ + '2.176.0.0/12', '5.22.0.0/16', '5.23.0.0/16', '5.56.128.0/17', + # Add more ranges as needed + ] + } + + result = {} + for country in countries: + if country in manual_ranges: + result[country] = manual_ranges[country] + print(f"Using manual ranges for {country}: {len(manual_ranges[country])} ranges", file=sys.stderr) + else: + print(f"No manual ranges available for {country}", file=sys.stderr) + result[country] = [] + + return result + + +def format_for_caddy(country_ranges: Dict[str, List[str]]) -> List[str]: + """ + Format IP ranges for use in Caddyfile remote_ip matcher. + + Args: + country_ranges: Dictionary of country codes to IP ranges + + Returns: + List of IP ranges formatted for Caddy + """ + all_ranges = [] + for country, ranges in country_ranges.items(): + all_ranges.extend(ranges) + + return all_ranges + + +def main(): + parser = argparse.ArgumentParser(description='Generate country IP ranges for blocking') + parser.add_argument('countries', nargs='+', + help='Country codes to block (e.g., CN RU KP IR)') + parser.add_argument('--format', choices=['caddy', 'json', 'list'], + default='caddy', help='Output format') + parser.add_argument('--fallback', action='store_true', + help='Use manual fallback ranges instead of downloading') + parser.add_argument('--output', '-o', help='Output file (default: stdout)') + + args = parser.parse_args() + + # Normalize country codes to uppercase + countries = [c.upper() for c in args.countries] + + # Download or use fallback ranges + if args.fallback: + country_ranges = get_alternative_ranges(countries) + else: + country_ranges = download_country_ranges(countries) + # If download failed, try fallback + if not any(country_ranges.values()): + print("Download failed, using fallback ranges...", file=sys.stderr) + country_ranges = get_alternative_ranges(countries) + + # Format output + if args.format == 'caddy': + ranges = format_for_caddy(country_ranges) + output = ' '.join(ranges) + elif args.format == 'json': + output = json.dumps(country_ranges, indent=2) + else: # list format + ranges = format_for_caddy(country_ranges) + output = '\n'.join(ranges) + + # Write output + if args.output: + with open(args.output, 'w') as f: + f.write(output) + print(f"Output written to {args.output}", file=sys.stderr) + else: + print(output) + + +if __name__ == '__main__': + main() diff --git a/config/ansible/group_vars/servers.yml b/config/ansible/group_vars/servers.yml index 968a698..fb86db5 100644 --- a/config/ansible/group_vars/servers.yml +++ b/config/ansible/group_vars/servers.yml @@ -1,3 +1,19 @@ --- flatpaks: false install_ui_apps: false + +# Country blocking configuration for Caddy +# List of countries to block by ISO 3166-1 alpha-2 country codes +# Common examples: CN (China), RU (Russia), KP (North Korea), IR (Iran), BY (Belarus) +blocked_countries_codes: + - CN # China + - RU # Russia + - KP # North Korea + - IR # Iran + +# IP ranges for blocked countries (generated automatically) +# This will be populated by the country blocking script +blocked_countries: [] + +# Enable/disable country blocking globally +enable_country_blocking: true diff --git a/config/ansible/tasks/servers/server.yml b/config/ansible/tasks/servers/server.yml index 5ee03af..3d32718 100644 --- a/config/ansible/tasks/servers/server.yml +++ b/config/ansible/tasks/servers/server.yml @@ -37,11 +37,11 @@ enabled: true - name: beszel enabled: true - - name: arr-stack - enabled: false - name: downloaders enabled: true - name: wireguard enabled: true - name: echoip enabled: true + - name: arr-stack + enabled: false diff --git a/config/ansible/tasks/servers/services/caddy/Caddyfile.j2 b/config/ansible/tasks/servers/services/caddy/Caddyfile.j2 index 8c3ae2f..275fd89 100644 --- a/config/ansible/tasks/servers/services/caddy/Caddyfile.j2 +++ b/config/ansible/tasks/servers/services/caddy/Caddyfile.j2 @@ -1,43 +1,74 @@ +# Global configuration for country blocking +{ + # Block specific countries (add ISO country codes as needed) + # Examples: CN (China), RU (Russia), KP (North Korea), IR (Iran) + servers { + protocols h1 h2 h3 + } +} + +# Country blocking snippet - reusable across all sites +{% if enable_country_blocking | default(false) and blocked_countries | default([]) | length > 0 %} +(country_block) { + @blocked_countries { + remote_ip {{ blocked_countries | join(' ') }} + } + respond @blocked_countries "Access denied from your country" 403 +} +{% else %} +(country_block) { + # Country blocking disabled +} +{% endif %} + photos.mvl.sh { + import country_block reverse_proxy immich:2283 tls {{ caddy_email }} } photos.vleeuwen.me { + import country_block redir https://photos.mvl.sh{uri} tls {{ caddy_email }} } karakeep.mvl.sh { + import country_block reverse_proxy karakeep:3000 tls {{ caddy_email }} } hoarder.mvl.sh { + import country_block redir https://karakeep.mvl.sh{uri} } git.vleeuwen.me git.mvl.sh { + import country_block reverse_proxy gitea:3000 tls {{ caddy_email }} } status.vleeuwen.me status.mvl.sh { + import country_block reverse_proxy uptime-kuma:3001 tls {{ caddy_email }} } sf.mvl.sh { + import country_block reverse_proxy seafile:80 handle /seafdav* { reverse_proxy seafile:8080 } - + tls {{ caddy_email }} } of.mvl.sh { + import country_block reverse_proxy onlyoffice:80 { header_up Host {host} header_up X-Real-IP {remote} @@ -48,31 +79,37 @@ of.mvl.sh { } fsm.mvl.sh { + import country_block reverse_proxy factorio-server-manager:80 tls {{ caddy_email }} } df.mvl.sh { + import country_block redir / https://git.mvl.sh/vleeuwenmenno/dotfiles/raw/branch/master/setup.sh tls {{ caddy_email }} } overseerr.mvl.sh jellyseerr.mvl.sh overseerr.vleeuwen.me jellyseerr.vleeuwen.me { + import country_block reverse_proxy mennos-server:5555 tls {{ caddy_email }} } jellyfin.mvl.sh jellyfin.vleeuwen.me { + import country_block reverse_proxy jellyfin:8096 tls {{ caddy_email }} } fladder.mvl.sh { + import country_block reverse_proxy fladder:80 tls {{ caddy_email }} } ip.mvl.sh { + import country_block reverse_proxy echoip:8080 { header_up X-Real-IP {http.request.remote.host} header_up X-Forwarded-For {http.request.remote.host} @@ -84,6 +121,7 @@ ip.mvl.sh { } http://ip.mvl.sh { + import country_block reverse_proxy echoip:8080 { header_up X-Real-IP {http.request.remote.host} header_up X-Forwarded-For {http.request.remote.host} diff --git a/config/ansible/tasks/servers/services/caddy/README-country-blocking.md b/config/ansible/tasks/servers/services/caddy/README-country-blocking.md new file mode 100644 index 0000000..2700e3a --- /dev/null +++ b/config/ansible/tasks/servers/services/caddy/README-country-blocking.md @@ -0,0 +1,214 @@ +# Caddy Country Blocking Configuration + +This directory contains configuration for implementing country-based IP blocking in Caddy reverse proxy. + +## Overview + +The country blocking feature allows you to block traffic from specific countries by their IP address ranges. This is useful for: + +- Reducing spam and malicious traffic +- Complying with regional restrictions +- Improving security posture +- Reducing server load from unwanted traffic + +## How It Works + +1. **IP Range Generation**: The `generate_country_blocks.py` script downloads current IP ranges for specified countries +2. **Caddy Configuration**: The Caddyfile template includes a reusable snippet that blocks IPs from those ranges +3. **Automatic Updates**: Ansible task generates fresh IP ranges on each deployment + +## Configuration + +### Enable/Disable Country Blocking + +In `group_vars/servers.yml`: + +```yaml +# Enable or disable country blocking +enable_country_blocking: true # Set to false to disable +``` + +### Specify Countries to Block + +Add country codes (ISO 3166-1 alpha-2) to the `blocked_countries_codes` list: + +```yaml +blocked_countries_codes: + - CN # China + - RU # Russia + - KP # North Korea + - IR # Iran + - BY # Belarus +``` + +### Common Country Codes + +| Country | Code | | Country | Code | +|---------|------|-|---------|------| +| China | CN | | Russia | RU | +| North Korea | KP | | Iran | IR | +| Belarus | BY | | Syria | SY | +| Myanmar | MM | | Afghanistan | AF | +| Cuba | CU | | Venezuela | VE | + +## Files Structure + +``` +caddy/ +├── Caddyfile.j2 # Main Caddy configuration with country blocking +├── caddy.yml # Ansible task for Caddy deployment +├── country-blocking.yml # Ansible task for country blocking setup +├── docker-compose.yml.j2 # Docker Compose configuration +├── generate_country_blocks.py # Script to generate IP ranges +└── README-country-blocking.md # This documentation +``` + +## Manual Usage + +### Generate IP Ranges Manually + +```bash +# Generate ranges for specific countries +python3 generate_country_blocks.py CN RU --format=list + +# Output to file +python3 generate_country_blocks.py CN RU KP --output=blocked_ranges.txt + +# Use fallback/manual ranges if download fails +python3 generate_country_blocks.py CN RU --fallback + +# JSON format for inspection +python3 generate_country_blocks.py CN RU --format=json +``` + +### Test Country Blocking + +After deployment, test blocking by checking access from different locations: + +```bash +# Test from a VPN/proxy in a blocked country +curl -I https://your-domain.com + +# Should return HTTP 403 with "Access denied from your country" +``` + +## Deployment + +The country blocking is automatically configured when you run the Caddy Ansible task: + +```bash +ansible-playbook -i inventory.ini playbook.yml --tags caddy +``` + +Or specifically for country blocking: + +```bash +ansible-playbook -i inventory.ini playbook.yml --tags country-blocking +``` + +## Troubleshooting + +### No IP Ranges Generated + +If the script fails to download ranges: + +1. Check internet connectivity on the Ansible control machine +2. Verify the country codes are valid (2-letter ISO codes) +3. Use fallback ranges: set `--fallback` flag in the script +4. Check the script output for error messages + +### Legitimate Traffic Blocked + +If legitimate users are blocked: + +1. Verify their country isn't in your blocked list +2. Check if they're using a VPN/proxy from a blocked country +3. Consider whitelisting specific IP ranges for known users +4. Review your country blocking list for overly broad restrictions + +### Service Not Starting + +If Caddy fails to start after enabling country blocking: + +1. Check the generated Caddyfile syntax: `caddy validate --config /path/to/Caddyfile` +2. Verify the IP ranges are valid CIDR notation +3. Check Docker logs: `docker compose logs caddy` + +## Security Considerations + +### Benefits + +- **Reduced Attack Surface**: Blocks traffic from high-risk regions +- **Lower Server Load**: Reduces processing of malicious requests +- **Compliance**: Helps meet regional access restrictions + +### Limitations + +- **VPN Bypass**: Users can circumvent blocking using VPNs +- **Legitimate Users**: May block legitimate traffic from blocked countries +- **Maintenance**: IP ranges change over time and need updates +- **False Positives**: Geolocation isn't 100% accurate + +### Best Practices + +1. **Whitelist Critical Services**: Don't block access to monitoring/health endpoints +2. **Regular Updates**: Update IP ranges regularly (automated with Ansible) +3. **Monitor Logs**: Watch for legitimate users being blocked +4. **Gradual Implementation**: Start with high-risk countries, expand cautiously +5. **Document Decisions**: Keep records of why specific countries are blocked + +## Advanced Configuration + +### Custom Block Message + +Modify the response in `Caddyfile.j2`: + +```caddy +respond @blocked_countries "Custom block message" 403 +``` + +### Whitelist Specific IPs + +Add to the country_block snippet: + +```caddy +(country_block) { + @blocked_countries { + remote_ip {{ blocked_countries | join(' ') }} + not remote_ip 1.2.3.4 5.6.7.8/24 # Whitelist specific IPs + } + respond @blocked_countries "Access denied from your country" 403 +} +``` + +### Log Blocked Requests + +Add logging to track blocked requests: + +```caddy +(country_block) { + @blocked_countries { + remote_ip {{ blocked_countries | join(' ') }} + } + log @blocked_countries { + output file /var/log/caddy/blocked_countries.log + format json + } + respond @blocked_countries "Access denied from your country" 403 +} +``` + +## Data Sources + +The script uses multiple data sources for IP ranges: + +1. **Primary**: ipinfo.io free API +2. **Fallback**: Manually curated ranges for common countries +3. **Alternative**: Can be extended to use other sources like MaxMind, IP2Location + +## License and Legal + +- Ensure compliance with local laws regarding geographic blocking +- Some regions have regulations about access restrictions +- Consider accessibility requirements for your services +- Review terms of service for IP geolocation data providers \ No newline at end of file diff --git a/config/ansible/tasks/servers/services/caddy/caddy.yml b/config/ansible/tasks/servers/services/caddy/caddy.yml index cf8de0e..17a6a1f 100644 --- a/config/ansible/tasks/servers/services/caddy/caddy.yml +++ b/config/ansible/tasks/servers/services/caddy/caddy.yml @@ -1,6 +1,8 @@ --- - name: Deploy Caddy service block: + - name: Setup country blocking + ansible.builtin.include_tasks: country-blocking.yml - name: Set Caddy directories ansible.builtin.set_fact: caddy_service_dir: "{{ ansible_env.HOME }}/services/caddy" diff --git a/config/ansible/tasks/servers/services/caddy/country-blocking.yml b/config/ansible/tasks/servers/services/caddy/country-blocking.yml new file mode 100644 index 0000000..2319010 --- /dev/null +++ b/config/ansible/tasks/servers/services/caddy/country-blocking.yml @@ -0,0 +1,52 @@ +--- +- name: Country blocking setup for Caddy + block: + - name: Ensure Python requests module is installed + ansible.builtin.pip: + name: requests + state: present + when: enable_country_blocking | default(false) + + - name: Copy country blocking script + ansible.builtin.copy: + src: generate_country_blocks.py + dest: "{{ caddy_service_dir }}/generate_country_blocks.py" + mode: '0755' + when: enable_country_blocking | default(false) + + - name: Generate country IP ranges + ansible.builtin.command: + cmd: "python3 {{ caddy_service_dir }}/generate_country_blocks.py {{ blocked_countries_codes | join(' ') }} --format=list" + register: country_ranges_result + when: + - enable_country_blocking | default(false) + - blocked_countries_codes | default([]) | length > 0 + changed_when: false + + - name: Set country IP ranges fact + ansible.builtin.set_fact: + blocked_countries: "{{ country_ranges_result.stdout.split('\n') | select('match', '^[0-9]') | list }}" + when: + - enable_country_blocking | default(false) + - country_ranges_result is defined + - country_ranges_result.stdout is defined + + - name: Display blocked countries info + ansible.builtin.debug: + msg: + - "Country blocking enabled: {{ enable_country_blocking | default(false) }}" + - "Countries to block: {{ blocked_countries_codes | default([]) | join(', ') }}" + - "IP ranges found: {{ blocked_countries | default([]) | length }}" + when: enable_country_blocking | default(false) + + - name: Fallback to empty list if no ranges generated + ansible.builtin.set_fact: + blocked_countries: [] + when: + - enable_country_blocking | default(false) + - blocked_countries is not defined + + tags: + - caddy + - security + - country-blocking