From c726ff18f1e17188c32061dd096c7b5797387951 Mon Sep 17 00:00:00 2001 From: rnsrk Date: Mon, 30 Mar 2026 11:33:59 +0200 Subject: [PATCH] Add WireGuard VPN and restrict SSH to VPN clients only --- README.md | 98 +++++++++++++++++++++++++++++++++ core/docker-compose.yml | 28 +++++++++- override/core.example.env | 9 +++ scripts/secure-ssh-vpn.sh | 112 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 scripts/secure-ssh-vpn.sh diff --git a/README.md b/README.md index 27fd516..66b3dca 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,22 @@ docker compose -f nextcloud/docker-compose.yml up -d ``` 3) Visit nextcloud domain and login with your .env credentials. +#### Nextcloud Office with Collabora +Collabora container is included for document editing. Configure via Nextcloud admin panel: +1) Install **Nextcloud Office** app from Apps menu. +2) Go to **Settings** → **Administration** → **Office**. +3) Select **"Use your own server"** and enter: `https://office.yourdomain.com`. +4) Configure WOPI allowlist with Collabora's IP: +```bash +# Get Collabora IP. +docker inspect nextcloud-collabora --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' +# Set allowlist. +docker exec -u www-data nextcloud php occ config:app:set richdocuments wopi_allowlist --value="COLLABORA_IP" +# Enable local servers. +docker exec -u www-data nextcloud php occ config:system:set allow_local_remote_servers --value=true --type=boolean +``` +See `/var/deploy/nextcloud/COLLABORA-QUICK-SETUP.md` for details. + ### Openproject 1) Start containers. ```bash @@ -162,6 +178,88 @@ docker compose -f hedgedoc/docker-compose.yml up -d ``` 2) Visit openproject domain and login with admin:admin and set new password. +## SSH over VPN (WireGuard) + +SSH access is secured behind a WireGuard VPN. Port 22 is only reachable from within the VPN subnet (`10.13.13.0/24`). + +### Server-side setup + +**1. Configure `core/.env`** + +```env +WG_SERVERURL=your.server.hostname.or.ip +WG_PEERS=laptop,phone # or a number, e.g. "3" +TZ=Europe/Berlin +``` + +**2. Start the WireGuard container** + +```bash +docker compose -f core/docker-compose.yml up -d wireguard +``` + +Peer configs and QR codes are generated automatically in: +``` +core/volumes/wireguard/config/peer_/ + peer_.conf ← import this on the client + peer_.png ← scan this QR code on mobile +``` + +**3. Apply firewall rules** + +Run *after* confirming the VPN works (see client setup below): + +```bash +sudo bash scripts/secure-ssh-vpn.sh [--dry-run] +``` + +This opens ports 80, 443, 25, 465, 587, 143, 993, 4190, 2424, 51820 and restricts SSH to VPN clients only. To also restrict mail client ports (IMAP, submission) to VPN: + +```bash +sudo bash scripts/secure-ssh-vpn.sh --mail-vpn-only +``` + +### Local client setup + +#### Linux + +```bash +sudo apt install wireguard +# Copy peer config from the server +scp user@your-server:/var/deploy/core/volumes/wireguard/config/peer_laptop/peer_laptop.conf \ + ~/.config/wireguard/wg0.conf +sudo wg-quick up wg0 +# Connect on boot: +sudo systemctl enable wg-quick@wg0 +``` + +#### macOS + +```bash +brew install wireguard-tools +# Or use the App Store app: "WireGuard" +# Import peer_laptop.conf via File → Import Tunnel(s) from File +``` + +#### Windows + +Download [WireGuard for Windows](https://www.wireguard.com/install/), then: +*Add Tunnel → Import tunnel(s) from file* → select `peer_laptop.conf`. + +#### Android / iOS + +Scan the QR code at `core/volumes/wireguard/config/peer_/peer_.png` with the WireGuard app. + +### Verify the tunnel + +```bash +# On the server — check connected peers +docker exec wireguard wg show + +# From the client — SSH should work only after connecting to VPN +ssh user@10.13.13.1 +``` + ## Roadmap - Tweak the core components and subservices for petter performance. - More automatisation when installing the environment. diff --git a/core/docker-compose.yml b/core/docker-compose.yml index 56db985..adb6086 100644 --- a/core/docker-compose.yml +++ b/core/docker-compose.yml @@ -1,4 +1,30 @@ services: + # VPN — WireGuard server. Clients must connect before SSH is reachable. + # network_mode: host is required so wg0 is created on the host network stack, + # making 10.13.13.1 reachable by sshd and other host services. + wireguard: + image: linuxserver/wireguard:latest + container_name: wireguard + network_mode: host + cap_add: + - NET_ADMIN + - SYS_MODULE + environment: + - PUID=1000 + - PGID=1000 + - TZ=${TZ:-Europe/Berlin} + - SERVERURL=${WG_SERVERURL} + - SERVERPORT=51820 + - PEERS=${WG_PEERS} + - PEERDNS=auto + - INTERNAL_SUBNET=10.13.13.0 + - ALLOWEDIPS=10.13.13.0/24 + - LOG_CONFS=false + volumes: + - ./volumes/wireguard/config:/config + - /lib/modules:/lib/modules:ro + restart: unless-stopped + # Database-Stack adminer: image: adminer:5 @@ -93,7 +119,7 @@ services: # Redirect HTTP requests to HTTPS - --entrypoints.web.http.redirections.entryPoint.to=websecure - --entrypoints.web.http.redirections.entryPoint.scheme=https - - --entrypoints.web.http.redirections.entrypoint.permanent=true + - --entrypoints.web.http.redirections.entryPoint.permanent=true # Use the specified email address for Let's Encrypt certificate requests - --certificatesresolvers.le.acme.email=${TRAEFIK_EMAIL} # Use the HTTP challenge for Let's Encrypt certificate requests diff --git a/override/core.example.env b/override/core.example.env index 8030ac3..dd03476 100644 --- a/override/core.example.env +++ b/override/core.example.env @@ -19,3 +19,12 @@ TRAEFIK_USERNAME= TRAEFIK_PASSWORD= TRAEFIK_EMAIL= TRAEFIK_HASHED_PASSWORD= + +# WireGuard VPN (SSH gateway) +# Public IP or DNS hostname of this server +WG_SERVERURL= +# Comma-separated peer names or a number (e.g. "laptop,phone" or "3") +# Each peer gets a config file + QR code in ./core/volumes/wireguard/config/ +WG_PEERS= +# Timezone for container (default: Europe/Berlin) +TZ=Europe/Berlin diff --git a/scripts/secure-ssh-vpn.sh b/scripts/secure-ssh-vpn.sh new file mode 100644 index 0000000..66e059f --- /dev/null +++ b/scripts/secure-ssh-vpn.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# Configure ufw firewall for this server stack: +# - SSH (22) : VPN clients only (10.13.13.0/24) +# - SMTP (25) : public (external mail servers must reach this) +# - HTTP/HTTPS : public (Traefik) +# - WireGuard UDP : public +# - Mail client ports (465, 587, 143, 993, 4190): public by default +# Set MAIL_VPN_ONLY=true to restrict these to VPN clients as well. +# +# Run this AFTER the WireGuard container is up and you have verified you can +# connect to the server via VPN (10.13.13.x). Once applied, SSH on port 22 is +# only reachable from within the WireGuard subnet. +# +# Usage: +# sudo bash scripts/secure-ssh-vpn.sh [--dry-run] [--mail-vpn-only] +# +# To undo SSH restriction: +# sudo ufw delete deny 22 +# sudo ufw delete allow from 10.13.13.0/24 to any port 22 + +set -euo pipefail + +WG_SUBNET="10.13.13.0/24" +SSH_PORT="22" +WG_UDP_PORT="51820" +DRY_RUN=false +MAIL_VPN_ONLY=false + +for arg in "$@"; do + [[ "$arg" == "--dry-run" ]] && DRY_RUN=true + [[ "$arg" == "--mail-vpn-only" ]] && MAIL_VPN_ONLY=true +done + +run() { + if $DRY_RUN; then + echo "[dry-run] $*" + else + "$@" + fi +} + +if [[ $EUID -ne 0 ]]; then + echo "Error: run this script as root (sudo)." >&2 + exit 1 +fi + +echo "=== WireGuard SSH firewall setup ===" +echo "VPN subnet : $WG_SUBNET" +echo "SSH port : $SSH_PORT (VPN only)" +echo "WG UDP port : $WG_UDP_PORT/udp" +echo "Mail VPN only : $MAIL_VPN_ONLY" +$DRY_RUN && echo "(dry-run mode — no changes will be made)" +echo + +# Ensure ufw is installed +if ! command -v ufw &>/dev/null; then + echo "Installing ufw..." + run apt-get install -y ufw +fi + +# ── Web & VPN ────────────────────────────────────────────────────────────── +run ufw allow 80/tcp comment 'HTTP (Traefik)' +run ufw allow 443/tcp comment 'HTTPS (Traefik)' +run ufw allow 2424/tcp comment 'GitLab SSH (Traefik)' +run ufw allow "$WG_UDP_PORT/udp" comment 'WireGuard VPN' + +# ── SSH — VPN clients only ──────────────────────────────────────────────── +# Deny rule is added first (gets higher rule number), then the VPN allow is +# inserted at position 1 so it always takes priority over the deny. +run ufw deny "$SSH_PORT/tcp" comment 'Block public SSH' +run ufw insert 1 allow from "$WG_SUBNET" to any port "$SSH_PORT" comment 'SSH via WireGuard VPN' + +# ── Mail (Mailcow) ──────────────────────────────────────────────────────── +# Port 25 must always be public so other mail servers can deliver to you. +run ufw allow 25/tcp comment 'SMTP (public — required for mail delivery)' + +if $MAIL_VPN_ONLY; then + # Restrict mail client access to VPN subnet only + for port in 465 587 143 993 4190; do + run ufw deny "$port/tcp" comment "Block public mail client port $port" + run ufw insert 1 allow from "$WG_SUBNET" to any port "$port" comment "Mail client port $port via VPN" + done + echo "Mail client ports (465, 587, 143, 993, 4190) restricted to VPN." +else + run ufw allow 465/tcp comment 'SMTPS (Mailcow)' + run ufw allow 587/tcp comment 'SMTP Submission (Mailcow)' + run ufw allow 143/tcp comment 'IMAP (Mailcow)' + run ufw allow 993/tcp comment 'IMAPS (Mailcow)' + run ufw allow 4190/tcp comment 'Sieve (Mailcow)' +fi + +# ── Enable / reload ufw ─────────────────────────────────────────────────── +if ! ufw status | grep -q "Status: active"; then + echo + echo "WARNING: enabling ufw. Make sure you are connected via WireGuard" + echo " or you will lose your current SSH session." + echo "Press Ctrl-C within 10 seconds to abort..." + sleep 10 + run ufw --force enable +else + run ufw reload +fi + +echo +echo "Done. Current ufw rules:" +ufw status numbered +echo +echo "To verify WireGuard is running:" +echo " docker compose -f /var/deploy/core/docker-compose.yml ps wireguard" +echo " docker exec wireguard wg show" +echo +echo "Client configs are in: ./core/volumes/wireguard/config/peer_/"