diff --git a/bin/actions/update.py b/bin/actions/update.py index f29b96d..1d13743 100755 --- a/bin/actions/update.py +++ b/bin/actions/update.py @@ -20,6 +20,10 @@ def help_message(): "green", " --ansible-verbose Upgrade Ansible packages with verbose output. (-vvv)", ) + printfe( + "green", + " --tags TAG Run only specific Ansible tags (e.g., --tags caddy).", + ) printfe( "green", " --full-speed, -F Upgrade packages and use all available cores for compilation. (Default: 8 cores)", @@ -222,6 +226,9 @@ def main(): action="store_true", help="Upgrade Ansible packages with verbose output", ) + parser.add_argument( + "--tags", type=str, help="Run only specific Ansible tags" + ) parser.add_argument( "--full-speed", "-F", action="store_true", help="Use all available cores" ) @@ -338,12 +345,19 @@ def main(): return 1 printfe("cyan", "Running Ansible playbook...") + # Determine which playbook to use based on tags + if args.tags and any(tag.strip() in ['caddy', 'country-blocking', 'caddyfile', 'config'] for tag in args.tags.split(',')): + playbook_path = f"{dotfiles_path}/config/ansible/caddy-playbook.yml" + printfe("cyan", f"Using dedicated Caddy playbook for tags: {args.tags}") + else: + playbook_path = f"{dotfiles_path}/config/ansible/playbook.yml" + ansible_cmd = [ "/usr/bin/env", "ansible-playbook", "-i", f"{dotfiles_path}/config/ansible/inventory.ini", - f"{dotfiles_path}/config/ansible/playbook.yml", + playbook_path, "--extra-vars", f"hostname={hostname}", "--extra-vars", @@ -353,9 +367,15 @@ def main(): "--ask-become-pass", ] + if args.tags: + ansible_cmd.extend(["--tags", args.tags]) + if args.ansible_verbose: ansible_cmd.append("-vvv") + # Debug: Show the command being executed + printfe("yellow", f"Debug: Executing command: {' '.join(ansible_cmd)}") + result = subprocess.run(ansible_cmd) if result.returncode != 0: printfe("red", "Failed to upgrade Ansible packages.") diff --git a/config/ansible/caddy-playbook.yml b/config/ansible/caddy-playbook.yml new file mode 100644 index 0000000..0c5bee3 --- /dev/null +++ b/config/ansible/caddy-playbook.yml @@ -0,0 +1,104 @@ +--- +- name: Configure Caddy service + hosts: all + handlers: + - name: Import handler tasks + ansible.builtin.import_tasks: handlers/main.yml + gather_facts: true + + tasks: + - name: Set Caddy directories (basic) + ansible.builtin.set_fact: + caddy_service_dir: "{{ ansible_env.HOME }}/services/caddy" + caddy_data_dir: "/mnt/object_storage/services/caddy" + tags: + - caddy + - setup + - country-blocking + - always + + - name: Get Caddy email from 1Password + ansible.builtin.set_fact: + caddy_email: "{{ lookup('community.general.onepassword', 'qwvcr4cuumhqh3mschv57xdqka', vault='j7nmhqlsjmp2r6umly5t75hzb4', field='email') }}" + ignore_errors: true + tags: + - caddy + - config + - caddyfile + - country-blocking + + - name: Set fallback email if 1Password failed + ansible.builtin.set_fact: + caddy_email: "admin@example.com" + when: caddy_email is not defined + tags: + - caddy + - config + - caddyfile + - country-blocking + + - name: Setup country blocking + ansible.builtin.include_tasks: tasks/servers/services/caddy/country-blocking.yml + tags: + - caddy + - country-blocking + - security + + - name: Create Caddy directory + ansible.builtin.file: + path: "{{ caddy_service_dir }}" + state: directory + mode: "0755" + tags: + - caddy + - setup + + - name: Create Caddy network + ansible.builtin.command: docker network create caddy_default + register: create_caddy_network + failed_when: + - create_caddy_network.rc != 0 + - "'already exists' not in create_caddy_network.stderr" + changed_when: create_caddy_network.rc == 0 + tags: + - caddy + - docker + - network + + - name: Deploy Caddy docker-compose.yml + ansible.builtin.template: + src: tasks/servers/services/caddy/docker-compose.yml.j2 + dest: "{{ caddy_service_dir }}/docker-compose.yml" + mode: "0644" + register: caddy_compose + tags: + - caddy + - docker + - config + + - name: Deploy Caddy Caddyfile + ansible.builtin.template: + src: tasks/servers/services/caddy/Caddyfile.j2 + dest: "{{ caddy_service_dir }}/Caddyfile" + mode: "0644" + register: caddy_file + tags: + - caddy + - config + - caddyfile + + - name: Stop Caddy service + ansible.builtin.command: docker compose -f "{{ caddy_service_dir }}/docker-compose.yml" down --remove-orphans + when: caddy_compose.changed or caddy_file.changed + tags: + - caddy + - docker + - service + + - name: Start Caddy service + ansible.builtin.command: docker compose -f "{{ caddy_service_dir }}/docker-compose.yml" up -d + when: caddy_compose.changed or caddy_file.changed + tags: + - caddy + - docker + - service diff --git a/config/ansible/files/generate_country_blocks.py b/config/ansible/files/generate_country_blocks.py deleted file mode 100644 index 320a134..0000000 --- a/config/ansible/files/generate_country_blocks.py +++ /dev/null @@ -1,220 +0,0 @@ -#!/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', - '36.0.0.0/10', '39.0.0.0/11', '42.0.0.0/8', '58.0.0.0/9', - '59.32.0.0/11', '60.0.0.0/8', '61.0.0.0/10', '110.0.0.0/7', - '112.0.0.0/5', '120.0.0.0/6', '124.0.0.0/7', '180.76.0.0/16' - ], - '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', - '31.31.192.0/19', '37.9.64.0/18', '37.18.0.0/16', '46.17.40.0/21', - '77.88.0.0/18', '78.24.216.0/21', '79.104.0.0/14', '80.66.176.0/20', - '81.68.0.0/14', '82.138.140.0/22', '84.201.128.0/18', '85.143.192.0/18', - '91.103.0.0/17', '91.208.165.0/24', '94.181.160.0/20', '95.165.0.0/16' - ], - 'IN': [ - '1.22.0.0/15', '14.96.0.0/11', '27.34.0.0/15', '36.255.0.0/16', - '39.32.0.0/11', '49.248.0.0/13', '58.84.0.0/15', '59.144.0.0/12', - '103.21.58.0/24', '106.51.0.0/16', '110.224.0.0/11', '117.192.0.0/10', - '122.160.0.0/12', '125.16.0.0/12', '150.129.0.0/16', '157.32.0.0/11', - '163.47.0.0/16', '180.179.0.0/16', '182.64.0.0/10', '183.82.0.0/15' - ], - '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', - '5.144.128.0/17', '31.2.128.0/17', '37.156.0.0/14', '37.191.0.0/16', - '46.32.0.0/11', '78.39.192.0/18', '79.175.128.0/18', '80.191.0.0/16', - '81.91.128.0/17', '82.99.192.0/18', '85.15.0.0/16', '91.98.0.0/15' - ], - 'VN': [ - '14.160.0.0/11', '27.64.0.0/10', '42.112.0.0/13', '45.117.0.0/16', - '103.21.148.0/22', '103.56.156.0/22', '113.160.0.0/11', '115.73.0.0/16', - '116.96.0.0/10', '118.69.0.0/16', '171.224.0.0/11', '203.113.128.0/18' - ], - 'BR': [ - '138.0.0.0/8', '177.0.0.0/8', '179.0.0.0/8', '186.192.0.0/12', - '189.0.0.0/8', '191.0.0.0/8', '200.128.0.0/9', '201.0.0.0/8' - ], - 'TR': [ - '31.145.0.0/16', '46.1.0.0/16', '78.160.0.0/11', '85.111.0.0/16', - '88.229.0.0/16', '94.54.0.0/15', '176.88.0.0/13', '185.2.0.0/16', - '188.119.0.0/16', '212.156.0.0/14' - ], - 'ID': [ - '36.64.0.0/10', '43.218.0.0/15', '103.10.0.0/15', '103.56.0.0/14', - '114.4.0.0/14', '118.96.0.0/11', '125.160.0.0/11', '139.228.0.0/15', - '182.253.0.0/16', '202.43.0.0/16' - ], - 'TH': [ - '27.130.0.0/15', '49.228.0.0/14', '58.8.0.0/14', '101.51.0.0/16', - '103.10.228.0/22', '110.164.0.0/14', '124.120.0.0/13', '171.96.0.0/13', - '182.232.0.0/14', '202.28.0.0/14' - ], - 'BD': [ - '27.147.0.0/16', '43.245.8.0/22', '103.4.92.0/22', '103.15.200.0/22', - '114.130.0.0/16', '118.179.0.0/16', '119.40.64.0/18', '180.211.192.0/18', - '202.40.32.0/19', '203.83.160.0/19' - ], - 'PK': [ - '39.32.0.0/11', '58.27.0.0/16', '103.11.60.0/22', '110.93.192.0/18', - '115.42.0.0/16', '119.63.128.0/20', '175.107.0.0/16', '182.176.0.0/12', - '202.47.96.0/19', '203.124.0.0/14' - ], - 'RO': [ - '31.13.224.0/19', '37.233.0.0/16', '46.19.0.0/16', '79.114.0.0/15', - '86.35.0.0/16', '89.136.0.0/13', '94.177.0.0/16', '109.166.0.0/15', - '188.24.0.0/13', '212.146.0.0/15' - ], - 'BY': [ - '31.130.176.0/20', '37.17.128.0/18', '46.16.104.0/21', '78.108.176.0/20', - '85.113.0.0/16', '86.57.0.0/17', '93.84.64.0/18', '178.120.0.0/13', - '178.172.160.0/19', '212.98.160.0/19' - ] - } - - 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/files/test_country_blocking.py b/config/ansible/files/test_country_blocking.py deleted file mode 100644 index b438b74..0000000 --- a/config/ansible/files/test_country_blocking.py +++ /dev/null @@ -1,251 +0,0 @@ -#!/usr/bin/env python3 -""" -Test script to verify country blocking functionality in Caddy. -This script tests access to your services from different country IP ranges. -""" - -import requests -import sys -import json -import argparse -from typing import List, Dict, Tuple -import time -import random - - -# Sample IP addresses from blocked countries for testing -TEST_IPS = { - 'CN': ['1.2.4.8', '14.17.22.35', '27.115.124.56', '36.248.15.72'], - 'RU': ['2.56.96.15', '5.8.15.32', '31.31.192.45', '77.88.8.8'], - 'IN': ['1.22.15.45', '14.96.32.78', '27.34.56.123', '117.192.45.89'], - 'KP': ['175.45.176.10', '210.52.109.15'], - 'IR': ['2.176.15.45', '5.22.78.123', '37.156.32.89'], - 'VN': ['14.160.32.45', '27.64.78.123', '113.160.45.89'], - 'BR': ['177.15.45.89', '179.32.78.123', '186.192.45.89'], - 'TR': ['31.145.15.45', '78.160.32.89', '94.54.78.123'], - 'ID': ['36.64.15.45', '114.4.32.89', '182.253.78.123'], - 'TH': ['27.130.15.45', '110.164.32.89', '202.28.78.123'], - 'BD': ['27.147.15.45', '114.130.32.89', '202.40.78.123'], - 'PK': ['39.32.15.45', '115.42.32.89', '202.47.123.45'], - 'RO': ['31.13.224.45', '89.136.32.89', '212.146.78.123'], - 'BY': ['31.130.176.45', '85.113.32.89', '212.98.160.123'] -} - -# Sample IPs from allowed countries for comparison -ALLOWED_IPS = { - 'US': ['8.8.8.8', '1.1.1.1', '208.67.222.222'], - 'DE': ['85.25.12.34', '193.99.144.85'], - 'NL': ['145.100.108.155', '194.109.6.66'], - 'GB': ['212.58.244.20', '151.101.65.140'] -} - - -def test_ip_blocking(domain: str, test_ip: str, timeout: int = 10) -> Tuple[int, str, float]: - """ - Test if an IP is blocked by making a request with X-Forwarded-For header. - - Args: - domain: Domain to test against - test_ip: IP address to simulate request from - timeout: Request timeout in seconds - - Returns: - Tuple of (status_code, response_text, response_time) - """ - headers = { - 'X-Forwarded-For': test_ip, - 'X-Real-IP': test_ip, - 'User-Agent': 'Country-Blocking-Test/1.0' - } - - try: - start_time = time.time() - response = requests.get(f'https://{domain}', - headers=headers, - timeout=timeout, - allow_redirects=False) - response_time = time.time() - start_time - - return response.status_code, response.text[:200], response_time - - except requests.exceptions.Timeout: - return -1, 'TIMEOUT', timeout - except requests.exceptions.RequestException as e: - return -2, f'ERROR: {str(e)}', 0.0 - - -def run_blocking_tests(domains: List[str], countries: List[str] = None) -> Dict: - """ - Run comprehensive blocking tests across multiple domains and countries. - - Args: - domains: List of domains to test - countries: List of country codes to test (defaults to all blocked countries) - - Returns: - Dictionary with test results - """ - if countries is None: - countries = list(TEST_IPS.keys()) - - results = { - 'blocked_tests': {}, - 'allowed_tests': {}, - 'summary': { - 'total_tests': 0, - 'blocked_correctly': 0, - 'allowed_correctly': 0, - 'errors': 0 - } - } - - print("šŸš€ Starting country blocking tests...\n") - - # Test blocked countries - for country in countries: - if country not in TEST_IPS: - print(f"āš ļø No test IPs available for {country}") - continue - - results['blocked_tests'][country] = {} - test_ips = random.sample(TEST_IPS[country], min(2, len(TEST_IPS[country]))) - - for domain in domains: - print(f"šŸ”’ Testing {country} -> {domain}") - results['blocked_tests'][country][domain] = [] - - for ip in test_ips: - status, response, resp_time = test_ip_blocking(domain, ip) - result = { - 'ip': ip, - 'status_code': status, - 'response': response, - 'response_time': resp_time, - 'blocked_correctly': status == 403 - } - - results['blocked_tests'][country][domain].append(result) - results['summary']['total_tests'] += 1 - - if result['blocked_correctly']: - results['summary']['blocked_correctly'] += 1 - print(f" āœ… {ip} -> {status} (blocked)") - elif status > 0: - print(f" āŒ {ip} -> {status} (should be blocked!)") - else: - results['summary']['errors'] += 1 - print(f" āš ļø {ip} -> ERROR: {response}") - - time.sleep(0.5) # Rate limiting - - # Test allowed countries for comparison - print(f"\nšŸŒ Testing allowed countries for comparison...\n") - - for country, ips in ALLOWED_IPS.items(): - results['allowed_tests'][country] = {} - test_ip = random.choice(ips) - - for domain in domains: - print(f"🌐 Testing {country} -> {domain}") - status, response, resp_time = test_ip_blocking(domain, test_ip) - - result = { - 'ip': test_ip, - 'status_code': status, - 'response': response, - 'response_time': resp_time, - 'allowed_correctly': status not in [403, -1, -2] - } - - results['allowed_tests'][country][domain] = result - results['summary']['total_tests'] += 1 - - if result['allowed_correctly']: - results['summary']['allowed_correctly'] += 1 - print(f" āœ… {test_ip} -> {status} (allowed)") - elif status == 403: - print(f" āŒ {test_ip} -> {status} (should be allowed!)") - else: - results['summary']['errors'] += 1 - print(f" āš ļø {test_ip} -> ERROR: {response}") - - time.sleep(0.5) - - return results - - -def print_summary(results: Dict): - """Print a summary of test results.""" - summary = results['summary'] - total = summary['total_tests'] - - print(f"\nšŸ“Š Test Summary") - print(f"{'='*50}") - print(f"Total tests run: {total}") - print(f"Blocked correctly: {summary['blocked_correctly']}") - print(f"Allowed correctly: {summary['allowed_correctly']}") - print(f"Errors: {summary['errors']}") - - if total > 0: - success_rate = ((summary['blocked_correctly'] + summary['allowed_correctly']) / total) * 100 - print(f"Success rate: {success_rate:.1f}%") - - if success_rate >= 95: - print("šŸŽ‰ Country blocking is working excellently!") - elif success_rate >= 85: - print("āœ… Country blocking is working well") - elif success_rate >= 70: - print("āš ļø Country blocking needs attention") - else: - print("āŒ Country blocking has serious issues") - - -def main(): - parser = argparse.ArgumentParser(description='Test country blocking functionality') - parser.add_argument('domains', nargs='+', help='Domains to test (e.g., photos.mvl.sh git.mvl.sh)') - parser.add_argument('--countries', nargs='*', - help='Specific countries to test (default: all blocked countries)') - parser.add_argument('--output', '-o', help='Save results to JSON file') - parser.add_argument('--verbose', '-v', action='store_true', - help='Show detailed output') - - args = parser.parse_args() - - print("šŸ›”ļø Country Blocking Test Suite") - print(f"Testing domains: {', '.join(args.domains)}") - - if args.countries: - countries = [c.upper() for c in args.countries] - print(f"Testing countries: {', '.join(countries)}") - else: - countries = None - print("Testing all blocked countries") - - print() - - # Run tests - results = run_blocking_tests(args.domains, countries) - - # Print summary - print_summary(results) - - # Save detailed results if requested - if args.output: - with open(args.output, 'w') as f: - json.dump(results, f, indent=2) - print(f"\nšŸ’¾ Detailed results saved to {args.output}") - - # Exit with appropriate code - if results['summary']['errors'] > 0: - sys.exit(2) - elif results['summary']['total_tests'] == 0: - sys.exit(1) - else: - success_rate = ((results['summary']['blocked_correctly'] + - results['summary']['allowed_correctly']) / - results['summary']['total_tests']) * 100 - sys.exit(0 if success_rate >= 85 else 1) - - -if __name__ == '__main__': - main() diff --git a/config/ansible/group_vars/servers.yml b/config/ansible/group_vars/servers.yml index d386078..e240151 100644 --- a/config/ansible/group_vars/servers.yml +++ b/config/ansible/group_vars/servers.yml @@ -2,27 +2,22 @@ flatpaks: false install_ui_apps: false -# Country blocking configuration for Caddy -# List of countries to block by ISO 3166-1 alpha-2 country codes -# Includes user-specified countries and top sources of malicious IP traffic -blocked_countries_codes: - # User-specified countries - - CN # China - - RU # Russia - - IN # India - - KP # North Korea - - # Top countries for malicious IP traffic and abuse - - IR # Iran - - VN # Vietnam - - BR # Brazil - - TR # Turkey - - ID # Indonesia - - TH # Thailand - - BD # Bangladesh - - PK # Pakistan - - RO # Romania - - BY # Belarus +# Countries that are allowed to access the server Caddy reverse proxy +allowed_countries_codes: + - US # United States + - CA # Canada + - GB # United Kingdom + - DE # Germany + - FR # France + - ES # Spain + - IT # Italy + - NL # Netherlands + - AU # Australia + - NZ # New Zealand + - JP # Japan + - KR # South Korea + - SK # Slovakia + - FL # Finland # IP ranges for blocked countries (generated automatically) # This will be populated by the country blocking script diff --git a/config/ansible/tasks/servers/services/caddy/Caddyfile.j2 b/config/ansible/tasks/servers/services/caddy/Caddyfile.j2 index 275fd89..83a62af 100644 --- a/config/ansible/tasks/servers/services/caddy/Caddyfile.j2 +++ b/config/ansible/tasks/servers/services/caddy/Caddyfile.j2 @@ -1,19 +1,22 @@ # 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 blocking snippet using MaxMind GeoLocation - reusable across all sites +{% if enable_country_blocking | default(false) and allowed_countries_codes | default([]) | length > 0 %} (country_block) { - @blocked_countries { - remote_ip {{ blocked_countries | join(' ') }} + @not_allowed_countries { + not { + maxmind_geolocation { + db_path "/etc/caddy/geoip/GeoLite2-Country.mmdb" + allow_countries {{ allowed_countries_codes | join(' ') }} + } + } } - respond @blocked_countries "Access denied from your country" 403 + respond @not_allowed_countries "Access denied" 403 } {% else %} (country_block) { @@ -116,7 +119,6 @@ ip.mvl.sh { header_up X-Forwarded-Proto {scheme} header_up X-Forwarded-Host {host} } - tls {{ caddy_email }} } diff --git a/config/ansible/tasks/servers/services/caddy/Dockerfile b/config/ansible/tasks/servers/services/caddy/Dockerfile new file mode 100644 index 0000000..7e9bed1 --- /dev/null +++ b/config/ansible/tasks/servers/services/caddy/Dockerfile @@ -0,0 +1,15 @@ +FROM caddy:2.9.1-builder AS builder + +RUN xcaddy build \ + --with github.com/porech/caddy-maxmind-geolocation + +FROM caddy:2.9.1-alpine + +COPY --from=builder /usr/bin/caddy /usr/bin/caddy + +# Create directory for MaxMind databases and logs +RUN mkdir -p /etc/caddy/geoip /var/log/caddy + +EXPOSE 80 443 + +CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"] diff --git a/config/ansible/tasks/servers/services/caddy/README-country-blocking.md b/config/ansible/tasks/servers/services/caddy/README-country-blocking.md deleted file mode 100644 index 396120d..0000000 --- a/config/ansible/tasks/servers/services/caddy/README-country-blocking.md +++ /dev/null @@ -1,261 +0,0 @@ -# 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: - # User-specified high-risk countries - - CN # China - - RU # Russia - - IN # India - - KP # North Korea - - # Top countries for malicious IP traffic and abuse - - IR # Iran - - VN # Vietnam - - BR # Brazil - - TR # Turkey - - ID # Indonesia - - TH # Thailand - - BD # Bangladesh - - PK # Pakistan - - RO # Romania - - BY # Belarus -``` - -### Currently Blocked Countries - -The default configuration blocks these countries based on high levels of malicious traffic: - -| Country | Code | Reason | -|---------|------|--------| -| China | CN | High volume of attacks, state-sponsored threats | -| Russia | RU | Cybercrime hub, state-sponsored threats | -| India | IN | Large botnet presence, spam sources | -| North Korea | KP | State-sponsored attacks | -| Iran | IR | State-sponsored threats | -| Vietnam | VN | High malware hosting, botnet activity | -| Brazil | BR | Large botnet networks | -| Turkey | TR | Hosting malicious infrastructure | -| Indonesia | ID | Compromised hosts, botnet activity | -| Thailand | TH | Hosting malicious services | -| Bangladesh | BD | Compromised infrastructure | -| Pakistan | PK | Botnet activity, compromised hosts | -| Romania | RO | Cybercrime activity | -| Belarus | BY | State-aligned threats | - -### Additional Country Codes Reference - -| Country | Code | | Country | Code | -|---------|------|-|---------|------| -| Syria | SY | | Myanmar | MM | -| Afghanistan | AF | | Cuba | CU | -| Venezuela | VE | | Ukraine | UA | -| Philippines | PH | | Nigeria | NG | - -## 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 (up to 70% reduction) -- **Compliance**: Helps meet regional access restrictions -- **Proactive Defense**: Stops attacks before they reach your applications -- **Cost Savings**: Reduces bandwidth and compute costs from malicious traffic - -### Limitations - -- **VPN Bypass**: Users can circumvent blocking using VPNs/proxies -- **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 -- **Business Impact**: May affect legitimate business relationships -- **Overblocking**: Current configuration blocks ~40% of global IP space - -### Risk Assessment - -The current blocking list includes countries that generate disproportionate amounts of: -- Botnet traffic (85% from blocked countries) -- Brute force attacks (78% from blocked countries) -- Malware hosting (72% from blocked countries) -- Spam campaigns (81% from blocked countries) - -### 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**: Current config is aggressive - monitor impact -5. **Document Decisions**: Keep records of why specific countries are blocked -6. **Business Review**: Ensure blocking doesn't conflict with business needs -7. **Whitelist Partners**: Add specific IP ranges for known business partners -8. **Regular Assessment**: Review blocked country list quarterly - -## 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 17a6a1f..6a7a56c 100644 --- a/config/ansible/tasks/servers/services/caddy/caddy.yml +++ b/config/ansible/tasks/servers/services/caddy/caddy.yml @@ -1,46 +1,58 @@ --- - 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" - caddy_data_dir: "/mnt/object_storage/services/caddy" - caddy_email: "{{ lookup('community.general.onepassword', 'qwvcr4cuumhqh3mschv57xdqka', vault='j7nmhqlsjmp2r6umly5t75hzb4', field='email') }}" + - name: Set Caddy directories + ansible.builtin.set_fact: + caddy_service_dir: "{{ ansible_env.HOME }}/services/caddy" + caddy_data_dir: "/mnt/object_storage/services/caddy" + caddy_email: "{{ lookup('community.general.onepassword', 'qwvcr4cuumhqh3mschv57xdqka', vault='j7nmhqlsjmp2r6umly5t75hzb4', field='email') }}" - - name: Create Caddy directory - ansible.builtin.file: - path: "{{ caddy_service_dir }}" - state: directory - mode: "0755" + - name: Setup country blocking + ansible.builtin.include_tasks: country-blocking.yml - - name: Create Caddy network - ansible.builtin.command: docker network create caddy_default - register: create_caddy_network - failed_when: - - create_caddy_network.rc != 0 - - "'already exists' not in create_caddy_network.stderr" - changed_when: create_caddy_network.rc == 0 + - name: Create Caddy directory + ansible.builtin.file: + path: "{{ caddy_service_dir }}" + state: directory + mode: "0755" - - name: Deploy Caddy docker-compose.yml - ansible.builtin.template: - src: docker-compose.yml.j2 - dest: "{{ caddy_service_dir }}/docker-compose.yml" - mode: "0644" - register: caddy_compose + - name: Copy Dockerfile for custom Caddy build + ansible.builtin.copy: + src: Dockerfile + dest: "{{ caddy_service_dir }}/Dockerfile" + mode: "0644" + register: caddy_dockerfile - - name: Deploy Caddy Caddyfile - ansible.builtin.template: - src: Caddyfile.j2 - dest: "{{ caddy_service_dir }}/Caddyfile" - mode: "0644" - register: caddy_file + - name: Create Caddy network + ansible.builtin.command: docker network create caddy_default + register: create_caddy_network + failed_when: + - create_caddy_network.rc != 0 + - "'already exists' not in create_caddy_network.stderr" + changed_when: create_caddy_network.rc == 0 - - name: Stop Caddy service - ansible.builtin.command: docker compose -f "{{ caddy_service_dir }}/docker-compose.yml" down --remove-orphans - when: caddy_compose.changed or caddy_file.changed + - name: Deploy Caddy docker-compose.yml + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ caddy_service_dir }}/docker-compose.yml" + mode: "0644" + register: caddy_compose - - name: Start Caddy service - ansible.builtin.command: docker compose -f "{{ caddy_service_dir }}/docker-compose.yml" up -d - when: caddy_compose.changed or caddy_file.changed + - name: Deploy Caddy Caddyfile + ansible.builtin.template: + src: Caddyfile.j2 + dest: "{{ caddy_service_dir }}/Caddyfile" + mode: "0644" + register: caddy_file + + - name: Stop Caddy service + ansible.builtin.command: docker compose -f "{{ caddy_service_dir }}/docker-compose.yml" down --remove-orphans + when: caddy_compose.changed or caddy_file.changed + + - name: Start Caddy service + ansible.builtin.command: docker compose -f "{{ caddy_service_dir }}/docker-compose.yml" up -d + when: caddy_compose.changed or caddy_file.changed + tags: + - caddy + - services + - reverse-proxy diff --git a/config/ansible/tasks/servers/services/caddy/country-blocking.yml b/config/ansible/tasks/servers/services/caddy/country-blocking.yml index 8ec4316..23b8174 100644 --- a/config/ansible/tasks/servers/services/caddy/country-blocking.yml +++ b/config/ansible/tasks/servers/services/caddy/country-blocking.yml @@ -1,53 +1,50 @@ --- -- name: Country blocking setup for Caddy +- name: Country blocking setup for Caddy with MaxMind GeoLocation block: - - name: Ensure Python requests module is installed - ansible.builtin.apt: - name: python3-requests - state: present - update_cache: yes - when: enable_country_blocking | default(false) + - name: Copy Dockerfile for custom Caddy build with GeoIP + ansible.builtin.copy: + src: Dockerfile + dest: "{{ caddy_service_dir }}/Dockerfile" + mode: "0644" + 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: Check if MaxMind Country database is available + ansible.builtin.stat: + path: "/mnt/object_storage/services/echoip/GeoLite2-Country.mmdb" + register: maxmind_country_db + 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: Ensure log directory exists for Caddy + ansible.builtin.file: + path: "{{ caddy_data_dir }}/logs" + state: directory + mode: "0755" + become: true + when: enable_country_blocking | default(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 country blocking configuration + ansible.builtin.debug: + msg: + - "āœ… Country blocking enabled: {{ enable_country_blocking | default(false) }}" + - "šŸ›”ļø Countries to allow: {{ allowed_countries_codes | default([]) | join(', ') }}" + - "šŸ“ Using MaxMind GeoLocation plugin" + - "šŸ’¾ Database path: /etc/caddy/geoip/GeoLite2-Country.mmdb" + - "šŸ“Š Database available: {{ maxmind_country_db.stat.exists | default(false) }}" + when: enable_country_blocking | default(false) - - 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 + - name: Warn if MaxMind database not found + ansible.builtin.debug: + msg: + - "āš ļø WARNING: MaxMind Country database not found!" + - "Expected location: /mnt/object_storage/services/echoip/GeoLite2-Country.mmdb" + - "Country blocking will not work until EchoIP service is deployed" + - "Run: dotf update --ansible --tags echoip" + when: + - enable_country_blocking | default(false) + - not maxmind_country_db.stat.exists | default(false) tags: - - caddy - - security - - country-blocking + - caddy + - security + - country-blocking + - geoip diff --git a/config/ansible/tasks/servers/services/caddy/docker-compose.yml.j2 b/config/ansible/tasks/servers/services/caddy/docker-compose.yml.j2 index 85d31df..ff5c402 100644 --- a/config/ansible/tasks/servers/services/caddy/docker-compose.yml.j2 +++ b/config/ansible/tasks/servers/services/caddy/docker-compose.yml.j2 @@ -1,6 +1,8 @@ services: caddy: - image: caddy:2.9.1-alpine + build: + context: . + dockerfile: Dockerfile restart: unless-stopped ports: - "80:80" @@ -9,6 +11,8 @@ services: - {{ caddy_data_dir }}/data:/data - {{ caddy_data_dir }}/config:/config - {{ caddy_service_dir }}/Caddyfile:/etc/caddy/Caddyfile + - /mnt/object_storage/services/echoip:/etc/caddy/geoip:ro + - {{ caddy_data_dir }}/logs:/var/log/caddy environment: - TZ=Europe/Amsterdam - PUID=1000 diff --git a/config/ansible/tasks/servers/services/services.yml b/config/ansible/tasks/servers/services/services.yml index 4e6a2b5..7033420 100644 --- a/config/ansible/tasks/servers/services/services.yml +++ b/config/ansible/tasks/servers/services/services.yml @@ -7,4 +7,7 @@ loop: "{{ services }}" when: item.enabled|bool loop_control: - label: "{{ item.name }}" + label: "{{ item.name }}" + tags: + - "{{ item.name }}" + - services diff --git a/config/home-manager/flake.lock b/config/home-manager/flake.lock index e79221f..ebac5f0 100644 --- a/config/home-manager/flake.lock +++ b/config/home-manager/flake.lock @@ -23,11 +23,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1748810746, - "narHash": "sha256-1na8blYvU1F6HLwx/aFjrhUqpqZ0SCsnqqW9n2vXvok=", + "lastModified": 1749668643, + "narHash": "sha256-gaWJEWGBW/g1u6o5IM4Un0vluv86cigLuBnjsKILffc=", "owner": "nixos", "repo": "nixpkgs", - "rev": "78d9f40fd6941a1543ffc3ed358e19c69961d3c1", + "rev": "1965fd20a39c8e441746bee66d550af78f0c0a7b", "type": "github" }, "original": { @@ -39,11 +39,11 @@ }, "nixpkgs-unstable": { "locked": { - "lastModified": 1748693115, - "narHash": "sha256-StSrWhklmDuXT93yc3GrTlb0cKSS0agTAxMGjLKAsY8=", + "lastModified": 1749794982, + "narHash": "sha256-Kh9K4taXbVuaLC0IL+9HcfvxsSUx8dPB5s5weJcc9pc=", "owner": "nixos", "repo": "nixpkgs", - "rev": "910796cabe436259a29a72e8d3f5e180fc6dfacc", + "rev": "ee930f9755f58096ac6e8ca94a1887e0534e2d81", "type": "github" }, "original": { diff --git a/config/ssh/authorized_keys/mennos-server b/config/ssh/authorized_keys/mennos-server index e053253..26b9fb2 100644 --- a/config/ssh/authorized_keys/mennos-server +++ b/config/ssh/authorized_keys/mennos-server @@ -6,3 +6,6 @@ ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMSJwfqOZQxGDbM07JziQeBNirvQxhFd6nEwWPjy1zCo # Menno's 2025 SSH Key ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE22Hfx8wgkc57TXX1TCMHcNrCdjbfog5QeHFJfl7IeD menno_fallback + +# Menno's Server (rsync mostly) +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMwyFxs+Zva2W2Viu9dzznFR9CfsiRWM1gxxvD5FUhfb menno@mennos-server