Secure Your Homelab: Exposing Services Selectively with WireGuard VPN and Traefik
Disclaimer: This blog article has been generated with the assistance of AI. While the content is AI-generated, the software itself and the ideas behind it are the result of real development work and genuine user needs.
Running a homelab is exciting, but security should never be an afterthought. In this guide, I’ll show you how to create a sophisticated setup where:
- Public services remain accessible to everyone (like your blog or portfolio)
- Private services require VPN access (like admin panels, monitoring tools, or personal apps)
- Home network access works seamlessly when you’re at home
- VPN access is required when traveling
We’ll use WireGuard for VPN connectivity and Traefik as our reverse proxy to intelligently route traffic based on your location.
Table of Contents
- Architecture Overview
- Prerequisites
- Setting Up WireGuard with wg-easy
- Configuring Traefik for Selective Access
- Example Service Configurations
- Testing Your Setup
- Security Considerations
- Troubleshooting
Architecture Overview
Our setup creates three distinct access patterns:
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Internet │ │ Home Network │ │ VPN Clients │
│ (Public) │ │ (Trusted) │ │ (Secure) │
└─────────┬───────┘ └────────┬─────────┘ └─────────┬───────┘
│ │ │
│ │ │
┌─────▼─────────────────────▼────────────────────────▼─────┐
│ Traefik │
│ (Reverse Proxy + SSL Termination) │
└─────┬─────────────────────┬────────────────────────┬─────┘
│ │ │
┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐
│ Public │ │ Mixed │ │ Private │
│ Services │ │ Services │ │ Services │
│ (Blog, │ │(Admin + │ │(Database, │
│ API) │ │ Public) │ │ Monitoring│
└───────────┘ └───────────┘ └───────────┘
Prerequisites
Before we start, ensure you have:
- A server with Docker and Docker Compose installed
- A domain name with DNS pointing to your server
- Basic familiarity with Docker Compose
- Traefik already running (or follow the basic setup below)
Basic Traefik Setup
If you don’t have Traefik running yet, here’s a minimal configuration:
# traefik/docker-compose.yml
version: '3.8'
services:
traefik:
image: traefik:v3.0
container_name: traefik
command:
- "--api.dashboard=true"
- "--providers.docker=true"
- "--providers.docker.exposedByDefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
- "--certificatesresolvers.letsencrypt.acme.email=your-email@example.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./letsencrypt:/letsencrypt"
networks:
- proxy-network
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.rule=Host(`traefik.yourdomain.com`)"
- "traefik.http.routers.traefik.entrypoints=websecure"
- "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
networks:
proxy-network:
external: true
Create the network: docker network create proxy-network
Setting Up WireGuard with wg-easy
WireGuard Easy (wg-easy) v15 is a complete rewrite that simplifies VPN management significantly. Unlike previous versions, it uses a web-based setup wizard instead of environment variables.
WireGuard Configuration
# wireguard/docker-compose.yml
version: '3.8'
volumes:
etc_wireguard:
services:
wg-easy:
image: ghcr.io/wg-easy/wg-easy:15
container_name: wg-easy
environment:
# v15 requires minimal environment variables
- INSECURE=false # Set to true if accessing via HTTP instead of HTTPS
networks:
proxy-network: {} # For Traefik access to web UI
wg: # VPN network with static IPs
ipv4_address: 10.42.42.42
ipv6_address: fdcc:ad94:bacf:61a3::2a
volumes:
- etc_wireguard:/etc/wireguard
- /lib/modules:/lib/modules:ro
ports:
- "51820:51820/udp" # WireGuard VPN port (must be directly exposed)
# Port 51821 is handled internally by Traefik
restart: unless-stopped
cap_add:
- NET_ADMIN
- SYS_MODULE
sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.src_valid_mark=1
- net.ipv6.conf.all.disable_ipv6=0
- net.ipv6.conf.all.forwarding=1
- net.ipv6.conf.default.forwarding=1
labels:
# Traefik labels for HTTPS access to web management UI
- "traefik.enable=true"
- "traefik.http.routers.wg-easy.rule=Host(`wg.yourdomain.com`)"
- "traefik.http.routers.wg-easy.entrypoints=websecure"
- "traefik.http.routers.wg-easy.tls.certresolver=letsencrypt"
- "traefik.http.services.wg-easy.loadbalancer.server.port=51821"
- "traefik.docker.network=proxy-network"
networks:
wg:
name: wg # Force exact network name
driver: bridge
enable_ipv6: true
ipam:
driver: default
config:
- subnet: 10.42.42.0/24
- subnet: fdcc:ad94:bacf:61a3::/64
proxy-network:
external: true
Initial Setup
- Deploy WireGuard:
cd wireguard docker-compose up -d - Access the setup wizard:
- Visit
https://wg.yourdomain.com - Complete the setup wizard:
- Server IP: Your server’s public IP
- Admin password: Choose a strong password
- VPN subnet:
10.42.42.0/24 - DNS servers:
1.1.1.1, 8.8.8.8
- Visit
- Create your first VPN client:
- Click “Add Client” in the web UI
- Download the configuration or scan the QR code
- Test the connection
Configuring Traefik for Selective Access
Now comes the magic! We’ll create different access patterns using Traefik’s IP whitelisting middleware.
Access Pattern 1: Public Services
These services are accessible from anywhere on the internet:
# examples/public-service.yml
services:
blog:
image: nginx:alpine
container_name: blog
volumes:
- ./blog-content:/usr/share/nginx/html
networks:
- proxy-network
labels:
- "traefik.enable=true"
- "traefik.http.routers.blog.rule=Host(`blog.yourdomain.com`)"
- "traefik.http.routers.blog.entrypoints=websecure"
- "traefik.http.routers.blog.tls.certresolver=letsencrypt"
- "traefik.http.services.blog.loadbalancer.server.port=80"
# No IP restrictions - publicly accessible
networks:
proxy-network:
external: true
Access Pattern 2: VPN + Home Network Only
These services require either VPN access OR being on your home network:
# examples/mixed-access-service.yml
services:
admin-panel:
image: your-admin-app:latest
container_name: admin-panel
networks:
- proxy-network
labels:
- "traefik.enable=true"
- "traefik.http.routers.admin.rule=Host(`admin.yourdomain.com`)"
- "traefik.http.routers.admin.entrypoints=websecure"
- "traefik.http.routers.admin.tls.certresolver=letsencrypt"
- "traefik.http.services.admin.loadbalancer.server.port=3000"
# IP whitelist: VPN clients + home network + localhost
- "traefik.http.routers.admin.middlewares=home-and-vpn"
- "traefik.http.middlewares.home-and-vpn.ipwhitelist.sourcerange=10.42.42.0/24,192.168.1.0/24,127.0.0.1/32"
networks:
proxy-network:
external: true
Access Pattern 3: VPN-Only Services
These services are only accessible through the VPN:
# examples/vpn-only-service.yml
services:
monitoring:
image: grafana/grafana:latest
container_name: monitoring
environment:
- GF_SECURITY_ADMIN_PASSWORD=secure-password
networks:
- proxy-network
volumes:
- grafana_data:/var/lib/grafana
labels:
- "traefik.enable=true"
- "traefik.http.routers.monitoring.rule=Host(`monitoring.yourdomain.com`)"
- "traefik.http.routers.monitoring.entrypoints=websecure"
- "traefik.http.routers.monitoring.tls.certresolver=letsencrypt"
- "traefik.http.services.monitoring.loadbalancer.server.port=3000"
# Strict VPN-only access
- "traefik.http.routers.monitoring.middlewares=vpn-only"
- "traefik.http.middlewares.vpn-only.ipwhitelist.sourcerange=10.42.42.0/24"
volumes:
grafana_data:
networks:
proxy-network:
external: true
Access Pattern 4: Network Isolation
For maximum security, some services can be completely isolated from the internet:
# examples/isolated-service.yml
services:
database:
image: postgres:13
container_name: database
environment:
- POSTGRES_DB=homelab
- POSTGRES_USER=admin
- POSTGRES_PASSWORD=secure-password
networks:
- wg # Only connected to VPN network
volumes:
- postgres_data:/var/lib/postgresql/data
# No Traefik labels - only accessible via direct IP when connected to VPN
# Access via: postgresql://10.42.42.43:5432/homelab
internal-api:
image: your-internal-api:latest
container_name: internal-api
networks:
wg:
ipv4_address: 10.42.42.50
# Access via: http://10.42.42.50:8080 when connected to VPN
volumes:
postgres_data:
networks:
wg:
external: true
Example Service Configurations
Let’s look at some real-world examples:
Change Detection Service (Mixed Access)
services:
changedetection:
image: ghcr.io/dgtlmoon/changedetection.io
container_name: changedetection
volumes:
- ./data:/datastore
restart: unless-stopped
networks:
- proxy-network
labels:
- "traefik.enable=true"
- "traefik.http.routers.changedetection.rule=Host(`cd.yourdomain.com`)"
- "traefik.http.routers.changedetection.entrypoints=websecure"
- "traefik.http.routers.changedetection.tls.certresolver=letsencrypt"
- "traefik.http.services.changedetection.loadbalancer.server.port=5000"
# Allow home network and VPN access
- "traefik.http.routers.changedetection.middlewares=home-and-vpn"
- "traefik.http.middlewares.home-and-vpn.ipwhitelist.sourcerange=10.42.42.0/24,192.168.1.0/24,127.0.0.1/32"
networks:
proxy-network:
external: true
Portainer (VPN-Only)
services:
portainer:
image: portainer/portainer-ce:latest
container_name: portainer
command: -H unix:///var/run/docker.sock
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:/data
networks:
- proxy-network
labels:
- "traefik.enable=true"
- "traefik.http.routers.portainer.rule=Host(`portainer.yourdomain.com`)"
- "traefik.http.routers.portainer.entrypoints=websecure"
- "traefik.http.routers.portainer.tls.certresolver=letsencrypt"
- "traefik.http.services.portainer.loadbalancer.server.port=9000"
# VPN-only access for security
- "traefik.http.routers.portainer.middlewares=vpn-only"
- "traefik.http.middlewares.vpn-only.ipwhitelist.sourcerange=10.42.42.0/24"
volumes:
portainer_data:
networks:
proxy-network:
external: true
Testing Your Setup
1. Test Public Access
# Should work from anywhere
curl -I https://blog.yourdomain.com
# Expected: 200 OK
2. Test VPN-Only Access
# Without VPN - should be blocked
curl -I https://monitoring.yourdomain.com
# Expected: 403 Forbidden
# With VPN connected - should work
curl -I https://monitoring.yourdomain.com
# Expected: 200 OK
3. Test Mixed Access
# From home network - should work
curl -I https://admin.yourdomain.com
# Expected: 200 OK
# From mobile data (without VPN) - should be blocked
curl -I https://admin.yourdomain.com
# Expected: 403 Forbidden
# From mobile data (with VPN) - should work
curl -I https://admin.yourdomain.com
# Expected: 200 OK
4. Monitor Access Logs
# Monitor Traefik logs for IP restrictions
docker logs traefik --tail 50 | grep -E "(403|Forbidden|ipwhitelist)"
# Monitor successful VPN connections
docker logs wg-easy --tail 20
Security Considerations
Understanding IP Ranges
It’s crucial to understand how IP whitelisting works:
10.42.42.0/24: Your VPN clients (WireGuard users)192.168.1.0/24: Your home network (adjust to match your router’s range)127.0.0.1/32: Server localhost access
Why Private IPs Are Safe
When you’re at a friend’s house with WiFi using 192.168.1.x:
- Your device gets
192.168.1.50(friend’s network) - Friend’s router has public IP
203.45.67.89 - Traefik sees:
203.45.67.89(blocked!) - Not:
192.168.1.50(which would be allowed)
Private IP ranges only work for actual local network access, not internet requests from similar ranges elsewhere.
Firewall Configuration
# Allow WireGuard VPN port
sudo ufw allow 51820/udp
# Allow HTTPS (Traefik)
sudo ufw allow 443/tcp
# Allow HTTP (for redirects)
sudo ufw allow 80/tcp
# Block direct access to service ports
sudo ufw deny 5000 # Example: block direct access to change detection
sudo ufw deny 3000 # Example: block direct access to Grafana
Additional Security Measures
- Use strong passwords for all admin interfaces
- Enable 2FA where available (WireGuard Easy v15 supports TOTP)
- Regular updates of all containers
- Monitor access logs for suspicious activity
- Backup configurations regularly
# Backup WireGuard configs
docker exec wg-easy cat /etc/wireguard/wg0.json > backup-$(date +%Y%m%d).json
# Backup Traefik dynamic config
cp -r traefik/ backup-traefik-$(date +%Y%m%d)/
Troubleshooting
Common Issues and Solutions
Issue: 404 Not Found from Traefik
# Check if container is in proxy-network
docker network inspect proxy-network | grep your-service
# Verify Traefik can reach the service
docker exec traefik ping your-service-container
Issue: VPN Clients Can’t Access Services
# Check WireGuard client logs
# On client: wg show
# Verify routing
# On server:
docker exec wg-easy wg show
ip route | grep wg0
Issue: IP Whitelisting Not Working
# Check what IP Traefik sees
docker logs traefik --tail 50 | grep your-domain
# Test IP detection
curl -H "X-Real-IP: 10.42.42.5" https://your-service.com
Issue: Certificate Errors
# Check certificate resolver
docker logs traefik | grep -i acme
# Verify DNS propagation
dig your-domain.com
Network Debugging
# List all Docker networks
docker network ls
# Inspect specific network
docker network inspect wg
docker network inspect proxy-network
# Check container network connections
docker inspect your-container | grep -A 20 "Networks"
# Test connectivity between containers
docker exec container1 ping container2
Conclusion
This setup gives you the best of both worlds: convenience when you’re at home and security when you’re traveling. You can:
- Access everything normally when on your home network
- Use VPN for sensitive services when traveling
- Keep some services completely public for legitimate external access
- Isolate critical services that should never touch the internet
Happy homelabbing! 🏠🔧
What services are you planning to add to your homelab? Share your setup in the comments below!