feat: geo-restriction via Cloudflare WAF custom rules
Add country-based access control managed through the Settings page.
Global allow-list with per-app overrides, searchable country selector,
automatic sync to Cloudflare WAF on settings change / deploy / remove,
plus periodic 6-hour verification.
New package: internal/cloudflare/ (client, zone, waf, countries, geosync)
New API: /api/geo/* (6 endpoints) + /api/stacks/{name}/geo/override
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+110
-3
@@ -4,7 +4,7 @@
|
||||
|
||||
A single, lightweight Go container that replaces Portainer + scattered systemd scripts with a unified, Hungarian-language web dashboard for managing Docker Compose stacks, backups, storage, monitoring, and notifications on customer hardware.
|
||||
|
||||
**Current version: v0.29.2**
|
||||
**Current version: v0.30.0**
|
||||
|
||||
---
|
||||
|
||||
@@ -24,6 +24,7 @@ A single, lightweight Go container that replaces Portainer + scattered systemd s
|
||||
- [Disaster Recovery](#10-disaster-recovery)
|
||||
- [Asset Sync](#11-asset-sync)
|
||||
- [Debug Mode](#12-debug-mode)
|
||||
- [Geo-Restriction](#13-geo-restriction)
|
||||
- [Repository Layout](#repository-layout)
|
||||
- [Configuration](#configuration)
|
||||
- [REST API](#rest-api)
|
||||
@@ -1096,11 +1097,109 @@ When `logging.level: "debug"` is set in `controller.yaml`, the controller expose
|
||||
|
||||
---
|
||||
|
||||
### 13. Geo-Restriction
|
||||
|
||||
Country-based access control via **Cloudflare WAF Custom Rules**. The controller manages WAF rules in the `http_request_firewall_custom` phase to block requests from non-allowed countries. Rules are identified by a `[felhom-geo]` description prefix — other WAF rules are never touched.
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
The existing `cf_api_token` (used for DNS-01 ACME) needs **Zone WAF:Edit** permission added. No new token is needed — just expanded permissions on the same token. The settings UI only appears when a CF API token is configured.
|
||||
|
||||
#### Architecture
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌──────────────────┐ ┌──────────────────────┐
|
||||
│ Settings UI │────▶│ GeoSyncManager │────▶│ Cloudflare WAF API │
|
||||
│ (settings. │ │ (geosync.go) │ │ /zones/{id}/ │
|
||||
│ html) │ │ diff & apply │ │ rulesets/{id}/rules │
|
||||
└─────────────┘ └──────────────────┘ └──────────────────────┘
|
||||
│ ▲
|
||||
│ POST /api/geo/* │ Scheduler (6h)
|
||||
▼ │ + deploy/remove hooks
|
||||
┌─────────────┐ │
|
||||
│ API layer │──────────────┘
|
||||
│ (geo.go) │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
**Rule structure:**
|
||||
- **Global rule**: `(not ip.src.country in {"HU"})` → block (with `http.host ne` exclusions for apps that have per-app overrides)
|
||||
- **Per-app rule**: `(http.host eq "app.example.com" and not ip.src.country in {"HU" "US"})` → block
|
||||
- **Block response**: HTTP 403 with Hungarian message
|
||||
|
||||
**Local network access** is inherently unaffected — traffic from the LAN goes directly to the server, bypassing Cloudflare entirely.
|
||||
|
||||
#### Cloudflare API Client (`internal/cloudflare/`)
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `client.go` | HTTP client with Bearer token auth, 15s timeout, generic `do()` helper |
|
||||
| `zone.go` | Zone ID resolution — tries exact domain, then parent domains progressively |
|
||||
| `waf.go` | WAF rule CRUD, expression builders (`BuildGlobalExpression`, `BuildAppExpression`) |
|
||||
| `countries.go` | ~250 ISO 3166-1 alpha-2 codes with Hungarian names |
|
||||
| `geosync.go` | Sync orchestrator — diffs desired vs existing rules, creates/updates/deletes |
|
||||
|
||||
**GeoSyncManager** uses a `StackLister` interface (implemented by `geoStackAdapter` in main.go) to get deployed app hostnames without circular imports.
|
||||
|
||||
#### Settings Model
|
||||
|
||||
Stored in `settings.json` (runtime-modifiable):
|
||||
|
||||
```go
|
||||
type GeoRestriction struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
AllowedCountries []string `json:"allowed_countries"`
|
||||
AppOverrides map[string]AppGeoOverride `json:"app_overrides,omitempty"`
|
||||
LastSync string `json:"last_sync,omitempty"`
|
||||
LastSyncError string `json:"last_sync_error,omitempty"`
|
||||
ZoneID string `json:"zone_id,omitempty"`
|
||||
RulesetID string `json:"ruleset_id,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
Thread-safe access via `GetGeoRestriction()`, `SetGeoRestriction()`, `SetGeoAppOverride()`, `RemoveGeoAppOverride()`, `SetGeoSyncState()`.
|
||||
|
||||
#### API Endpoints
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/geo/status` | Current geo settings + sync state |
|
||||
| POST | `/api/geo/settings` | Update global settings (enable/disable, countries) |
|
||||
| POST | `/api/geo/sync` | Trigger manual sync |
|
||||
| GET | `/api/geo/countries` | Full country list for search UI |
|
||||
| POST | `/api/stacks/{name}/geo/override` | Set per-app country override |
|
||||
| DELETE | `/api/stacks/{name}/geo/override` | Remove per-app override |
|
||||
|
||||
All mutating endpoints trigger an async Cloudflare sync.
|
||||
|
||||
#### Sync Triggers
|
||||
|
||||
1. **Settings change** — user saves geo settings or per-app override
|
||||
2. **Deploy/remove** — app deployment or removal changes the hostname list
|
||||
3. **Scheduler** — periodic verification every 6 hours
|
||||
4. **Startup** — delayed initial sync 15s after boot
|
||||
5. **Manual** — "Szinkronizálás" button on settings page
|
||||
|
||||
#### UI
|
||||
|
||||
**Settings page** ("Beállítások" → "Földrajzi korlátozás"):
|
||||
- Enable/disable toggle
|
||||
- Searchable country autocomplete with tag-based selection
|
||||
- Hungary pinned with `confirm()` warning on removal
|
||||
- Per-app overrides summary with add/edit/remove
|
||||
- Sync status display (last sync time, errors)
|
||||
|
||||
**App detail page** (per-app override, shown when geo is globally enabled):
|
||||
- Toggle for custom country restriction
|
||||
- Independent country selector
|
||||
|
||||
---
|
||||
|
||||
## Repository Layout
|
||||
|
||||
```
|
||||
controller/
|
||||
├── cmd/controller/main.go # Entry point, wires all 15 modules (setup mode branch + normal startup)
|
||||
├── cmd/controller/main.go # Entry point, wires all 16 modules (setup mode branch + normal startup)
|
||||
├── internal/
|
||||
│ ├── config/config.go # YAML loader, validation, env overrides
|
||||
│ ├── crypto/crypto.go # AES-256-GCM encryption for app.yaml secrets, key management
|
||||
@@ -1130,8 +1229,16 @@ controller/
|
||||
│ │ ├── restore_scan.go # DR: scan drives for backup data, build restore plan
|
||||
│ │ ├── restore_app_linux.go # DR: per-app restore (rsync config/data + docker compose up)
|
||||
│ │ └── restore_drives_linux.go # DR: auto-mount drives by UUID from Hub infra backup
|
||||
│ ├── cloudflare/
|
||||
│ │ ├── client.go # CF API client (Bearer auth, generic JSON helper)
|
||||
│ │ ├── zone.go # Zone ID resolution (domain → zone)
|
||||
│ │ ├── waf.go # WAF rule CRUD + expression builders
|
||||
│ │ ├── countries.go # ISO 3166-1 country codes + Hungarian names
|
||||
│ │ └── geosync.go # Geo sync orchestrator (diff & apply rules)
|
||||
│ ├── assets/syncer.go # Hub asset sync (download, SHA-256 compare, resolve)
|
||||
│ ├── api/router.go # REST API endpoints (~30 routes)
|
||||
│ ├── api/
|
||||
│ │ ├── router.go # REST API endpoints (~36 routes)
|
||||
│ │ └── geo.go # Geo-restriction API handlers
|
||||
│ ├── scheduler/scheduler.go # Central job scheduler (Every, Daily)
|
||||
│ ├── system/
|
||||
│ │ ├── info.go, info_linux.go # RAM, disk, CPU, temperature, load average
|
||||
|
||||
Reference in New Issue
Block a user