Add country-based IP blocking for Caddy via Ansible
Some checks failed
Ansible Lint Check / check-ansible (push) Failing after 37s
Nix Format Check / check-format (push) Failing after 1m39s
Python Lint Check / check-python (push) Failing after 23s

- Introduce generate_country_blocks.py to fetch IP ranges by country
- Update group_vars/servers.yml with country blocking settings
- Add country_block snippet to Caddyfile and apply to all sites
- Create Ansible tasks for automated IP range generation and integration
- Add documentation for configuring and managing country blocking
This commit is contained in:
2025-06-15 01:30:42 +02:00
parent 020c32e8fe
commit 0f35a7b9e2
7 changed files with 487 additions and 3 deletions

View File

@@ -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

View File

@@ -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}

View File

@@ -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

View File

@@ -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"

View File

@@ -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