fahrengit-451/README.md
Albert Armea cf99cd50f2 Initial commit
Change written by Claude Sonnet 4.6:

I need to set up a Git hosting service for personal projects where certain repositories need to be blocked using the locale corresponding to the user’s IP address.

Here are my constraints:
*   The entire system will be running on a single VPS instance. I should be able to `docker compose up -d` the whole thing.
*   For each repository that this feature is enabled, I need to be able to set the blocked locales down to the state level, along with a custom HTTP status code and response body.
*   You may assume that the IP address of the request is where it actually came from — for this exercise, if the user uses a VPN to bypass the restriction, that is on them.
*   To simplify a reverse proxy setup, you may assume that all Git operations will happen over HTTPS. I will firewall off SSH access.
*    I will be using Let's Encrypt for HTTPS.

Some suggestions from prior research:
*   nginx seems like a reasonable reverse proxy that supports all of the requirements, but you may use a different one if it is simpler to implement or maintain.
*   I can obtain a MaxMind API key to get a geo-IP lookup table. If you use this, you will need to add a service that automatically retrieves the table at a reasonable frequency.
*   Forgejo seems like a reasonable, lightweight Git service, but you may use a different one if you’re aware of one that actually supports these requirements out of the box.

Write me a production-ready `docker-compose.yml` and supporting scripts or configuration scaffolding for me to implement this.
2026-03-21 18:34:50 +00:00

247 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Self-hosted Git (Forgejo) with State-Level Geo-Blocking
A single-VPS Docker Compose stack providing:
- **Forgejo** — lightweight, Gitea-compatible Git hosting
- **nginx** — reverse proxy with TLS termination and GeoIP2 blocking
- **MaxMind GeoLite2** — IP → country + state/province database (auto-updated)
- **geoblock_watcher** — watches `geo_rules.yml` and hot-reloads nginx when rules change
- **Certbot** — automatic Let's Encrypt certificate renewal
---
## Directory Layout
```
.
├── docker-compose.yml
├── .env.example ← copy to .env and fill in
├── geo_rules.yml ← ✏️ edit this to configure geo-blocking
├── bootstrap_certs.sh ← run once before first `docker compose up`
├── nginx/
│ ├── Dockerfile ← builds nginx + GeoIP2 dynamic module
│ ├── nginx.conf ← main nginx config (loads GeoIP2 module)
│ ├── conf.d/
│ │ └── git.conf ← virtual host (HTTP→HTTPS redirect + proxy)
│ └── geoblock/ ← rendered by geoblock_watcher at runtime
│ ├── repo_maps.conf
│ ├── repo_vars.conf
│ └── repo_locations.conf
└── geoblock_watcher/
├── Dockerfile
└── watcher.py
```
---
## Prerequisites
| Requirement | Notes |
|---|---|
| Docker Engine ≥ 26 + Compose v2 | `docker compose version` |
| A public domain name | DNS A record → your VPS IP |
| Ports 80 and 443 open | Firewall / security group |
| MaxMind account | Free — [sign up here](https://www.maxmind.com/en/geolite2/signup) |
| `openssl` on the host | Used by `bootstrap_certs.sh` for the dummy cert |
---
## Quick Start
### 1. Configure environment
```bash
cp .env.example .env
$EDITOR .env # fill in DOMAIN, MAXMIND_*, LETSENCRYPT_EMAIL
```
`.env` variables:
| Variable | Description |
|---|---|
| `DOMAIN` | Your public domain, e.g. `git.example.com` |
| `LETSENCRYPT_EMAIL` | Email for Let's Encrypt expiry notices |
| `MAXMIND_ACCOUNT_ID` | From your MaxMind account portal |
| `MAXMIND_LICENSE_KEY` | From your MaxMind account portal |
| `DISABLE_REGISTRATION` | Set `true` after creating your admin account |
### 2. Bootstrap TLS certificates (first run only)
```bash
chmod +x bootstrap_certs.sh
./bootstrap_certs.sh
```
This will:
1. Create a temporary self-signed cert so nginx can start
2. Bring up the stack
3. Obtain a real Let's Encrypt cert via the ACME webroot challenge
4. Reload nginx with the real cert
5. Print next steps
### 3. Complete Forgejo setup
Visit `https://your-domain/` and complete the web installer. Create your
admin account. Then set `DISABLE_REGISTRATION=true` in `.env` and run:
```bash
docker compose up -d forgejo
```
### 4. Configure geo-blocking
Edit `geo_rules.yml` — the watcher will detect the change within seconds and
hot-reload nginx automatically. No restart needed.
---
## Geo-Blocking Configuration
`geo_rules.yml` is the single source of truth. Example:
```yaml
repos:
- path: /alice/secret-project
rules:
# Block California and Texas with HTTP 451
- locales: ["US-CA", "US-TX"]
status: 451
body: "This repository is unavailable in your jurisdiction."
# Block all of Germany and France with HTTP 403
- locales: ["DE", "FR"]
status: 403
body: "Access to this repository is restricted in your country."
- path: /alice/another-repo
rules:
- locales: ["CN", "RU"]
status: 403
body: "Access denied."
```
### Locale format
| Format | Example | Matches |
|---|---|---|
| Country (ISO 3166-1 α-2) | `"US"` | All IPs in the United States |
| Country + State (ISO 3166-2) | `"US-CA"` | IPs in California |
State-level rules take precedence over country-level rules for the same repo.
**Common US state codes:** `US-AL` `US-AK` `US-AZ` `US-AR` `US-CA` `US-CO`
`US-CT` `US-DE` `US-FL` `US-GA` `US-HI` `US-ID` `US-IL` `US-IN` `US-IA`
`US-KS` `US-KY` `US-LA` `US-ME` `US-MD` `US-MA` `US-MI` `US-MN` `US-MS`
`US-MO` `US-MT` `US-NE` `US-NV` `US-NH` `US-NJ` `US-NM` `US-NY` `US-NC`
`US-ND` `US-OH` `US-OK` `US-OR` `US-PA` `US-RI` `US-SC` `US-SD` `US-TN`
`US-TX` `US-UT` `US-VT` `US-VA` `US-WA` `US-WV` `US-WI` `US-WY`
For other countries, find subdivision codes at:
https://www.iso.org/obp/ui/#search (search for the country, then see "Subdivision")
### HTTP status codes
| Code | Meaning | When to use |
|---|---|---|
| `403` | Forbidden | General access restriction |
| `451` | Unavailable For Legal Reasons | Legal / jurisdictional block (RFC 7725) |
### Hot reload
The watcher polls every 60 seconds and also reacts to inotify events
immediately. After saving `geo_rules.yml`, nginx will reload within seconds.
No traffic is dropped — nginx does a graceful configuration reload (SIGHUP).
---
## GeoIP Database Updates
The `geoipupdate` container fetches a fresh **GeoLite2-City** database every
72 hours (MaxMind publishes updates twice a week). The database is stored in
the `geoip_db` Docker volume and mounted read-only into nginx.
nginx reads the database file at request time (not cached in memory), so a
fresh database takes effect for the next request after the file is replaced —
no nginx reload required.
---
## Certificate Renewal
The `certbot` container runs `certbot renew` every 12 hours. When a
certificate is renewed, run:
```bash
docker compose exec nginx nginx -s reload
```
Or add this as a cron job on the host:
```cron
0 */12 * * * docker compose -f /path/to/docker-compose.yml exec nginx nginx -s reload
```
---
## Operations
### View logs
```bash
docker compose logs -f nginx # access + error logs
docker compose logs -f geoblock_watcher
docker compose logs -f forgejo
docker compose logs -f geoipupdate
```
### Test geo-blocking (from a blocked region)
Use a proxy or VPN to simulate a request from a blocked locale, or test
directly with curl overriding your IP (only works if you control nginx):
```bash
# Verify nginx config is valid after a rules change
docker compose exec nginx nginx -t
# Force a manual nginx reload
docker compose exec nginx nginx -s reload
```
### Verify the GeoIP database is loaded
```bash
docker compose exec nginx nginx -T | grep geoip2
```
### Check which database version is in use
```bash
docker compose exec geoipupdate cat /usr/share/GeoIP/GeoLite2-City_*/COPYRIGHT_AND_LICENSE
```
---
## Security Notes
- **SSH is disabled** in Forgejo; all Git operations use HTTPS.
- **Registration is disabled** by default after initial setup — only the admin
can create accounts.
- nginx **does not forward** `X-Forwarded-For` from downstream; it sets it
from `$remote_addr` (the actual connected IP). This is intentional — we
explicitly trust the direct connection IP as stated in the requirements.
- The `docker.sock` mount on `geoblock_watcher` is the minimum necessary
to send SIGHUP to the nginx container. If this is a concern, you can
replace it with a small privileged sidecar that only accepts a reload signal.
---
## Troubleshooting
| Symptom | Check |
|---|---|
| nginx won't start | `docker compose logs nginx` — likely a config syntax error |
| GeoIP variables always empty | Is the `geoip_db` volume populated? Check `docker compose logs geoipupdate` |
| Rules not applied | Check `docker compose logs geoblock_watcher` — look for YAML parse errors |
| Certificate errors | Ensure port 80 is open and DNS resolves before running `bootstrap_certs.sh` |
| 502 Bad Gateway | Forgejo not healthy yet — check `docker compose logs forgejo` |