Compare commits

..

8 Commits

Author SHA1 Message Date
davide 9016605f0d security: harden wg-init and wg-easy container isolation
- wg-init: isolate with network_mode:none, drop repo mount, use
  explicit PUID/PGID env vars instead of stat trick
- wg-easy: add read_only filesystem, /run tmpfs, no-new-privileges
- .env.example: simplify and document PUID/PGID

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 08:32:20 +02:00
davide fc5d6209c1 docs: add resource limits guide for low-RAM devices
- README: new section explaining WG_MEM_LIMIT / WG_MEMSWAP_LIMIT with
  per-RAM-tier values and host swap configuration for SBC boards
- CLAUDE.md: simplify resource limits table, drop device-specific
  measurements, reference README for per-board guidance
- .env.example: update comments with per-tier values and OOM warning

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 08:22:43 +02:00
davide 4d8eb01e4c fix: set wg-data ownership to repo user in wg-init
Mount the repo root read-only (/repo) in wg-init and use
`stat -c '%u:%g' /repo` to detect the host user automatically.
chown is applied before chmod 700, so the directory is accessible
without sudo on any machine regardless of UID.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 08:16:35 +02:00
davide 19d03ea624 docs(claude): document hardware compatibility and SBC constraints
Add a dedicated section covering:
- tested architectures: arm64 (RPi 3/4/5, Orange Pi, Rock Pi), armv7,
  amd64 (x86_64 servers, VMs, Intel NUC)
- known ip6tables boot issue on hosts with BSP/minimal kernels and how
  wg-init solves it automatically across all architectures
- resource limit variables (WG_MEM_LIMIT, WG_MEMSWAP_LIMIT, WG_CPUS)
  with guidance for boards under 1 GB RAM

Also corrects stale wg0.json reference to wg-easy.db in constraints list.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 08:10:43 +02:00
davide df78675198 docs: update README and .gitignore for automated setup
- .gitignore: remove .gitkeep exception, ignore wg-data/ entirely and
  fix stale comment (directory is generated by the container, not
  created manually)
- README: document init container behaviour in setup step 4, update
  project structure (wg0.json -> wg-easy.db), align security section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 08:07:57 +02:00
davide 8c56e2fc9f feat: add wg-init service and pin image to minor tag
Introduce a wg-init container (Alpine) that runs before wg-easy and:
- loads ip6_tables and ip6table_nat kernel modules (silently skipped if
  already built-in or unavailable), fixing startup on hosts that do not
  auto-load these modules (e.g. Raspberry Pi)
- sets chmod 700 on wg-data/ so private keys are protected from the
  moment the container writes them

wg-easy now depends on wg-init completing successfully, making the setup
portable across hardware without any manual host configuration.

Also pins the image tag from 15.2.2 to the minor tag (15) to receive
patch updates automatically while avoiding breaking changes across majors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 08:07:50 +02:00
davide e177300864 docs: aggiorna documentazione per wg-easy v15
Rimuove le istruzioni per generare PASSWORD_HASH, aggiorna la tabella
variabili, descrive il wizard di setup al primo avvio e aggiunge
la raccomandazione di aggiornamento mensile per la sicurezza.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 23:58:36 +02:00
davide 2b38127156 migrate: aggiorna configurazione a wg-easy v15
Rimuove le variabili d'ambiente obsolete (WG_HOST, PASSWORD_HASH,
WG_DEFAULT_DNS) ora gestite dalla web UI. Aggiunge INSECURE=true per
accesso HTTP locale. Pulisce i commenti superflui dal compose.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 23:58:30 +02:00
5 changed files with 130 additions and 104 deletions
+11 -32
View File
@@ -1,40 +1,19 @@
# ============================================================ # Fuso orario IANA — obbligatorio
# WireGuard VPN — Configurazione locale
# Copia .env.example in .env e compila i valori richiesti.
# ============================================================
# --- OBBLIGATORI ---
# IP pubblico o hostname DDNS del server (es. mio-host.duckdns.org)
WG_HOST=your-ddns-hostname.example.com
# Hash bcrypt della password per la web UI.
# Vedi README.md per come generarlo.
# ATTENZIONE: incolla il valore esatto, senza aggiungere $$ o escape.
PASSWORD_HASH=$2a$12$replacethiswithyourrealbcrypthash
# Fuso orario (formato IANA)
# Lista completa: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
TZ=Europe/Rome TZ=Europe/Rome
# --- OPZIONALI (valori di default mostrati) --- # UID/GID dell'utente host proprietario di wg-data/ (trova con: id -u && id -g)
# PUID=1000
# PGID=1000
# Porta UDP WireGuard # Porte esposte sull'host (opzionali, default mostrati)
WG_PORT=51820 # WG_PORT=51820
# WG_UI_PORT=51821
# Porta TCP interfaccia web # Limiti risorse container (opzionali — adatta alla RAM disponibile)
WG_UI_PORT=51821 # 512 MB → WG_MEM_LIMIT=96m
# 1 GB → WG_MEM_LIMIT=128m
# DNS inviati ai client # 2 GB+ → WG_MEM_LIMIT=256m ← default
WG_DEFAULT_DNS=1.1.1.1,8.8.8.8
# --- OPZIONALI — Limiti risorse container (SBC) ---
# Decommenta e adatta alla RAM disponibile:
# 512 MB RAM → WG_MEM_LIMIT=128m
# 1 GB RAM → WG_MEM_LIMIT=256m (default)
# 2 GB+ RAM → WG_MEM_LIMIT=384m
# Tenere WG_MEMSWAP_LIMIT = WG_MEM_LIMIT per disabilitare lo swap del container. # Tenere WG_MEMSWAP_LIMIT = WG_MEM_LIMIT per disabilitare lo swap del container.
# Su single-core (Pi Zero / Pi 1) impostare WG_CPUS=0.75.
# WG_MEM_LIMIT=256m # WG_MEM_LIMIT=256m
# WG_MEMSWAP_LIMIT=256m # WG_MEMSWAP_LIMIT=256m
# WG_CPUS=1.0 # WG_CPUS=1.0
+1 -1
View File
@@ -1,6 +1,6 @@
# Variabili locali — contiene segreti, non committare mai # Variabili locali — contiene segreti, non committare mai
.env .env
# Directory WireGuard — auto-generata dal container al primo avvio. # Directory WireGuard — generata dal container al primo avvio.
# Contiene chiavi crittografiche private. Non committare mai. # Contiene chiavi crittografiche private. Non committare mai.
wg-data/ wg-data/
+35 -11
View File
@@ -11,10 +11,7 @@ A Docker Compose setup for a self-hosted WireGuard VPN server using [wg-easy](ht
```bash ```bash
# First-time setup # First-time setup
cp .env.example .env cp .env.example .env
# Edit .env with WG_HOST, PASSWORD_HASH, TZ # Edit .env: set TZ, optionally WG_PORT / WG_UI_PORT
# Generate bcrypt password hash (no extra dependencies)
docker run --rm -it ghcr.io/wg-easy/wg-easy wgpw 'YourPassword'
# Start the VPN # Start the VPN
docker compose up -d docker compose up -d
@@ -28,20 +25,47 @@ docker compose logs -f wg-easy
## Configuration ## Configuration
All runtime configuration lives in `.env` (not committed). Required variables: `.env` (not committed) controls only infrastructure-level settings:
| Variable | Description | | Variable | Description |
|---|---| |---|---|
| `WG_HOST` | Public IP or DDNS hostname clients connect to |
| `PASSWORD_HASH` | bcrypt hash of the web UI password (prefix `$2a$`, `$2b$`, or `$2y$`) |
| `TZ` | IANA timezone (e.g. `Europe/Rome`) | | `TZ` | IANA timezone (e.g. `Europe/Rome`) |
| `WG_PORT` | UDP VPN port (default 51820) |
| `WG_UI_PORT` | Web UI port (default 51821) |
Optional: `WG_PORT` (default 51820/udp), `WG_UI_PORT` (default 51821/tcp), `WG_DEFAULT_DNS` (default `1.1.1.1,8.8.8.8`). **v15+:** Host, password, and DNS are configured through the web UI wizard on first launch — not via environment variables.
## Important constraints ## Important constraints
- `wg-data/` is auto-generated by the container on first start and holds live WireGuard keys (`wg0.conf`, `wg0.json`). Never commit it. - `wg-data/` is auto-generated by the container on first start and holds live WireGuard keys (`wg0.conf`, `wg-easy.db`). Never commit it. Permissions are set to `700` automatically by `wg-init`.
- `.env` is gitignored — it holds the password hash and hostname. - `.env` is gitignored.
- The container requires `NET_ADMIN` and `SYS_MODULE` capabilities plus `net.ipv4.ip_forward=1` sysctl — these are already set in `docker-compose.yml`. - The container requires `NET_ADMIN` and `SYS_MODULE` capabilities plus `net.ipv4.ip_forward=1` sysctl — these are already set in `docker-compose.yml`.
- The router must forward UDP port 51820 (or `WG_PORT`) to the server's local IP. - The router must forward UDP port 51820 (or `WG_PORT`) to the server's local IP.
- `openssl passwd` does **not** support bcrypt — use the Docker method, `htpasswd`, or Python's `bcrypt` library to generate `PASSWORD_HASH`. - `INSECURE=true` is set in `docker-compose.yml` to allow HTTP access on the local network.
## SBC / resource-constrained devices
Works on any Linux host with a 64-bit kernel and WireGuard support. Tested architectures:
| Hardware | Arch | Notes |
|---|---|---|
| Raspberry Pi 5 | arm64 | kernel `6.12.x+rpt-rpi-2712` |
| Raspberry Pi 4 / 3B+ | arm64 / armv7 | 32-bit kernels need `armv7` image |
| Orange Pi, Rock Pi, Banana Pi | arm64 | depends on board BSP kernel |
| Generic x86\_64 server / VM | amd64 | standard Debian/Ubuntu/Fedora |
| Intel NUC / mini-PC | amd64 | same as above |
**Known issue — ip6tables modules not loaded at boot.**
Affects mostly SBC boards with custom BSP kernels, but can occur on any host where `ip6_tables` and `ip6table_nat` are not auto-loaded. Without them `wg-quick up wg0` fails and rolls back, leaving no `wg0` interface. Symptom: every API call returns `Command failed: wg show wg0 dump / No such device`.
The `wg-init` service handles this automatically: it runs `modprobe ip6_tables ip6table_nat` (with `SYS_MODULE` cap and `/lib/modules` bind-mounted read-only) before wg-easy starts. Failures are silenced (`|| true`) so the setup works on kernels where these modules are built-in or unavailable.
**Resource limits** (override via `.env`):
| Variable | Default | Notes |
|---|---|---|
| `WG_MEM_LIMIT` | `256m` | Hard cap for the wg-easy container |
| `WG_MEMSWAP_LIMIT` | `256m` | Keep equal to `WG_MEM_LIMIT` to disable container swap |
| `WG_CPUS` | `1.0` | `0.75` on single-core boards (Pi Zero, Pi 1) |
The limit exists to prevent Node.js from slowly growing over long uptime and triggering the host OOM-killer (symptom: SSH becomes unreachable). Do not go below `96m` or the runtime OOM-kills on startup. See README §Dispositivi a risorse limitate for per-board guidance and swap configuration.
+45 -43
View File
@@ -27,60 +27,35 @@ cd vpn
cp .env.example .env cp .env.example .env
``` ```
Apri `.env` con un editor e compila i valori richiesti: L'unica variabile obbligatoria è `TZ` (già precompilata con `Europe/Rome`). Le porte possono essere lasciate ai valori di default.
| Variabile | Descrizione | Obbligatoria | | Variabile | Descrizione | Default |
|---|---|---| |---|---|---|
| `WG_HOST` | IP pubblico o hostname DDNS del server | Sì | | `TZ` | Fuso orario (formato IANA) | `Europe/Rome` |
| `PASSWORD_HASH` | Hash della password per la UI web (vedi sotto) | Sì | | `WG_PORT` | Porta UDP WireGuard | `51820` |
| `TZ` | Fuso orario (formato IANA, es. `Europe/Rome`) | Sì | | `WG_UI_PORT` | Porta TCP interfaccia web | `51821` |
| `WG_PORT` | Porta UDP WireGuard (default: `51820`) | No |
| `WG_UI_PORT` | Porta TCP interfaccia web (default: `51821`) | No |
| `WG_DEFAULT_DNS` | DNS inviati ai client (default: `1.1.1.1,8.8.8.8`) | No |
### 3. Genera e imposta la password > **Nota v15:** host VPN, password e DNS si configurano dalla web UI al primo avvio.
La password non si scrive in chiaro nel file `.env`: va convertita in un **hash** (una stringa cifrata che wg-easy usa per verificare il login senza conservare la password originale). ### 3. Apri la porta sul router
**Passo 1** — Scegli la password che vuoi usare per accedere all'interfaccia web, poi esegui questo comando sostituendo `LatuaPassword` con quella scelta:
```bash
docker run --rm -it ghcr.io/wg-easy/wg-easy wgpw 'LatuaPassword'
```
**Passo 2** — Il comando stampa una riga simile a questa:
```
PASSWORD_HASH='$2a$12$cDMCiMmFTuMOlT1E4BvxEO4CJfzMKSanRZSMqiE1234abcXYZ'
```
**Passo 3** — Copia solo la parte dopo `PASSWORD_HASH=`, **senza** le virgolette singole. Nel file `.env` deve apparire così:
```
PASSWORD_HASH=$2a$12$cDMCiMmFTuMOlT1E4BvxEO4CJfzMKSanRZSMqiE1234abcXYZ
```
> **Attenzione:** non aggiungere virgolette, spazi o altri caratteri intorno al valore. Copia e incolla esattamente quello che hai ottenuto.
### 4. Apri la porta sul router
Inoltra la porta **UDP 51820** (o quella scelta in `WG_PORT`) verso l'IP locale del server. Inoltra la porta **UDP 51820** (o quella scelta in `WG_PORT`) verso l'IP locale del server.
### 5. Avvia il container ### 4. Avvia il container
```bash ```bash
docker compose up -d docker compose up -d
``` ```
Al primo avvio la directory `wg-data/` viene creata automaticamente con tutte le chiavi crittografiche. Non è necessario modificarla manualmente. Al primo avvio, un init container crea automaticamente `wg-data/` con permessi `700` prima che wg-easy scriva le chiavi private.
### 6. Accedi all'interfaccia web ### 5. Completa il setup dalla web UI
``` ```
http://IP_DEL_TUO_SERVER:51821 http://IP_DEL_TUO_SERVER:51821
``` ```
Da qui puoi aggiungere client, scaricare i file di configurazione e generare QR code per dispositivi mobili. Al primo avvio appare un wizard che guida nella configurazione di password, hostname DDNS e DNS per i client. Dopo il setup puoi aggiungere client, scaricare configurazioni e generare QR code per dispositivi mobili.
--- ---
@@ -90,18 +65,46 @@ Da qui puoi aggiungere client, scaricare i file di configurazione e generare QR
vpn/ vpn/
├── docker-compose.yml # Definizione del servizio ├── docker-compose.yml # Definizione del servizio
├── .env.example # Template variabili (committato) ├── .env.example # Template variabili (committato)
├── .env # Variabili locali con segreti (NON committato) ├── .env # Variabili locali (NON committato)
├── .gitignore ├── .gitignore
├── README.md ├── README.md
└── wg-data/ # Generato automaticamente dal container (NON committato) └── wg-data/ # Generato dal container al primo avvio (ignorato da git)
├── wg0.conf # Configurazione WireGuard (chiavi reali) ├── wg0.conf # Configurazione WireGuard con chiavi private
└── wg0.json # Stato interno wg-easy (chiavi reali) └── wg-easy.db # Database interno wg-easy
```
---
## Dispositivi a risorse limitate (SBC)
Su board con poca RAM (≤ 1 GB) il processo Node.js di wg-easy può crescere nel tempo e far sì che il kernel OOM-killi altri processi di sistema — SSH compreso. Per evitarlo, decommenta e adatta questi valori nel tuo `.env`:
```ini
# Board con 512 MB RAM
WG_MEM_LIMIT=96m
WG_MEMSWAP_LIMIT=96m
# Board con 1 GB RAM
WG_MEM_LIMIT=128m
WG_MEMSWAP_LIMIT=128m
```
Tieni `WG_MEMSWAP_LIMIT` uguale a `WG_MEM_LIMIT` per disabilitare lo swap del container: quando il container raggiunge il limite viene riavviato da Docker invece di consumare swap di sistema.
Assicurati anche che lo swap dell'host sia almeno 512 MB. Su Raspberry Pi OS il default è 100 MB — per aumentarlo:
```bash
sudo dphys-swapfile swapoff
sudo sed -i 's/CONF_SWAPSIZE=100/CONF_SWAPSIZE=512/' /etc/dphys-swapfile
sudo dphys-swapfile setup && sudo dphys-swapfile swapon
``` ```
--- ---
## Aggiornare wg-easy ## Aggiornare wg-easy
Esegui mensilmente per ricevere patch di sicurezza:
```bash ```bash
docker compose pull docker compose pull
docker compose up -d docker compose up -d
@@ -111,6 +114,5 @@ docker compose up -d
## Sicurezza ## Sicurezza
- `wg-data/` contiene chiavi crittografiche private. Non committarla mai su Git. - `wg-data/` contiene chiavi crittografiche private ed è completamente ignorata da git. I permessi `700` vengono impostati automaticamente dall'init container al primo avvio.
- `.env` contiene la password (hash) e il tuo hostname. Non committarlo. - `.env` contiene variabili locali specifiche del server. Non committarlo.
- Entrambi sono esclusi da `.gitignore`.
+38 -17
View File
@@ -1,25 +1,45 @@
services: services:
wg-init:
image: alpine:3.21
network_mode: none
cap_add:
- SYS_MODULE
environment:
PUID: "${PUID:-1000}"
PGID: "${PGID:-1000}"
volumes:
- ./wg-data:/data
- /lib/modules:/lib/modules:ro
command:
- /bin/sh
- -c
- |
modprobe ip6_tables 2>/dev/null || true
modprobe ip6table_nat 2>/dev/null || true
chown "${PUID}:${PGID}" /data
chmod 700 /data
restart: "no"
wg-easy: wg-easy:
image: ghcr.io/wg-easy/wg-easy:15.2.2 # Pinnato — aggiornare deliberatamente con compose pull depends_on:
wg-init:
condition: service_completed_successfully
image: ghcr.io/wg-easy/wg-easy:15
container_name: wg-easy container_name: wg-easy
restart: unless-stopped restart: unless-stopped
# --- Limiti risorse (SBC con 1 GB RAM) ---
# mem_limit = memswap_limit disabilita lo swap per il container:
# se supera 256 MB Docker lo riavvia pulito invece di mandare in OOM l'host.
mem_limit: "${WG_MEM_LIMIT:-256m}" mem_limit: "${WG_MEM_LIMIT:-256m}"
memswap_limit: "${WG_MEMSWAP_LIMIT:-256m}" memswap_limit: "${WG_MEMSWAP_LIMIT:-256m}"
cpus: "${WG_CPUS:-1.0}" cpus: "${WG_CPUS:-1.0}"
# --- Rotazione log: evita che i log riempiano la SD card ---
logging: logging:
driver: json-file driver: json-file
options: options:
max-size: "10m" max-size: "10m"
max-file: "3" max-file: "3"
# --- Health check: riavvio automatico se Node.js si blocca ---
healthcheck: healthcheck:
# porta interna fissa; WG_UI_PORT controlla solo il mapping host
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:51821/"] test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:51821/"]
interval: 60s interval: 60s
timeout: 10s timeout: 10s
@@ -27,27 +47,28 @@ services:
start_period: 30s start_period: 30s
environment: environment:
PASSWORD_HASH: "${PASSWORD_HASH}"
TZ: "${TZ}" TZ: "${TZ}"
WG_HOST: "${WG_HOST}" INSECURE: "true"
WG_DEFAULT_DNS: "${WG_DEFAULT_DNS:-1.1.1.1,8.8.8.8}"
# Chiavi e configurazione WireGuard persistite fuori dal container
volumes: volumes:
- ./wg-data:/etc/wireguard - ./wg-data:/etc/wireguard
# File temporanei in RAM invece che sulla SD card read_only: true
tmpfs: tmpfs:
- /tmp:size=32m,mode=1777 - /tmp:size=32m,mode=1777
- /run:size=8m
ports: ports:
- "${WG_PORT:-51820}:51820/udp" # Traffico VPN WireGuard - "${WG_PORT:-51820}:51820/udp"
- "${WG_UI_PORT:-51821}:51821/tcp" # Interfaccia web di gestione - "${WG_UI_PORT:-51821}:51821/tcp"
cap_add: cap_add:
- NET_ADMIN # Necessario per gestire le interfacce di rete (wg0) e le route - NET_ADMIN
- SYS_MODULE # Necessario per caricare il modulo kernel wireguard - SYS_MODULE
security_opt:
- no-new-privileges:true
sysctls: sysctls:
- net.ipv4.ip_forward=1 # Abilita il routing dei pacchetti IPv4 tra i client VPN e internet - net.ipv4.ip_forward=1
- net.ipv6.conf.all.forwarding=1 # Abilita il routing IPv6 (richiesto anche se non si usa IPv6) - net.ipv6.conf.all.forwarding=1