Commercial VPN providers make big promises. No logs. Military-grade encryption. Complete anonymity. Then a court order lands and suddenly logs appear that were never supposed to exist. PureVPN handed connection logs to the FBI in 2017. IPVanish did the same. HideMyAss gave up a LulzSec member. The list keeps growing.
The problem is not the VPN protocol. The problem is trust. When you route your traffic through someone else's infrastructure, you are replacing your ISP's ability to monitor you with a VPN company's ability to monitor you. You have shifted the surveillance point, not eliminated it.
The only VPN you can trust is the one you operate yourself, on infrastructure you control, in a jurisdiction that respects privacy by law — not by marketing copy.
This guide covers three things: deploying WireGuard manually on a Swiss VPS, simplifying management with wg-easy's Docker-based web UI, and then doing what commercial VPNs claim but rarely deliver — eliminating every log on the system so there is genuinely nothing to hand over.
WireGuard is roughly 4,000 lines of code. OpenVPN is over 100,000. IPsec implementations in the Linux kernel span hundreds of thousands of lines across multiple subsystems. This is not a trivia point — it is a security argument.
Every line of code is a potential vulnerability. Smaller codebases are easier to audit, easier to reason about, and have fewer places for bugs to hide. WireGuard's cryptographic choices are fixed — ChaCha20 for symmetric encryption, Poly1305 for authentication, Curve25519 for key exchange, BLAKE2s for hashing. No cipher negotiation, no configuration matrix, no downgrade attacks. It either uses modern cryptography or it does not connect.
Performance is the other advantage. WireGuard runs inside the Linux kernel as a network interface. There is no userspace daemon shuffling packets through TUN/TAP devices. On identical hardware, WireGuard consistently delivers 2-4x the throughput of OpenVPN and lower latency than most IPsec configurations. On a Swiss VPS with a 1Gbps port, you will actually use that bandwidth instead of bottlenecking at 300-400Mbps on OpenVPN.
The simplicity extends to configuration. A complete WireGuard setup is a single config file per peer. No certificate authority, no PKI infrastructure, no renewal scripts. Generate a key pair, exchange public keys, define allowed IPs. That is it.
Start with a clean Ubuntu 22.04 or Debian 12 installation on your Swiss VPS. WireGuard is included in the default kernel since Linux 5.6, so installation is straightforward.
Install WireGuard tools:
apt update && apt install -y wireguard wireguard-tools
Generate server keys:
# Generate private and public keys
wg genkey | tee /etc/wireguard/server_private.key | wg pubkey > /etc/wireguard/server_public.key
chmod 600 /etc/wireguard/server_private.key
Create the server configuration:
# /etc/wireguard/wg0.conf
[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = <contents of server_private.key>
# NAT and forwarding rules
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT
PostUp = iptables -A FORWARD -o wg0 -j ACCEPT
PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT
PostDown = iptables -D FORWARD -o wg0 -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
# Client 1
[Peer]
PublicKey = <client1_public_key>
AllowedIPs = 10.0.0.2/32
# Client 2
[Peer]
PublicKey = <client2_public_key>
AllowedIPs = 10.0.0.3/32
Replace eth0 with your actual network interface name. Check with ip route show default — it is often ens3, ens18, or enp0s3 on virtual machines.
Enable IP forwarding:
# Enable immediately
sysctl -w net.ipv4.ip_forward=1
# Make persistent across reboots
echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.d/99-wireguard.conf
sysctl -p /etc/sysctl.d/99-wireguard.conf
Start WireGuard and enable on boot:
systemctl enable --now wg-quick@wg0
# Verify it is running
wg show
You should see the interface with its public key, listening port, and any connected peers.
On each client device (laptop, phone, router), generate a key pair and create a config file:
# Generate client keys (on the client machine)
wg genkey | tee client_private.key | wg pubkey > client_public.key
Create the client configuration file:
# /etc/wireguard/wg0.conf (on client)
[Interface]
PrivateKey = <client_private_key>
Address = 10.0.0.2/24
DNS = 1.1.1.1, 9.9.9.9
[Peer]
PublicKey = <server_public_key>
Endpoint = your-swiss-vps-ip:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
Setting AllowedIPs = 0.0.0.0/0, ::/0 routes all traffic through the VPN. For split tunneling — where only specific subnets go through the VPN — replace this with the specific CIDR ranges you want to route.
Adding the client's public key to the server can be done live without restarting WireGuard:
# On the server — add a peer without restarting
wg set wg0 peer <client_public_key> allowed-ips 10.0.0.2/32
A VPN is useless if DNS queries leak outside the tunnel. Two things to lock down:
DNS leak prevention: The DNS = 1.1.1.1, 9.9.9.9 line in the client config tells the WireGuard client to use those DNS servers through the tunnel. On Linux clients, verify with:
# Check for DNS leaks
resolvectl status
# Should show wg0 using 1.1.1.1 / 9.9.9.9, not your ISP's DNS
Kill switch via iptables: If the VPN tunnel drops, traffic should stop entirely — not fall back to your unprotected connection. Add these rules on the client:
# Kill switch — block all traffic unless it goes through WireGuard
iptables -I OUTPUT ! -o wg0 -m mark ! --mark $(wg show wg0 fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
ip6tables -I OUTPUT ! -o wg0 -m mark ! --mark $(wg show wg0 fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
# Allow traffic to the WireGuard endpoint itself (so the tunnel can reconnect)
iptables -I OUTPUT -d your-swiss-vps-ip -p udp --dport 51820 -j ACCEPT
This ensures that if wg0 goes down, no traffic leaves the machine unencrypted. The connection simply stops until the tunnel comes back up.
Manual WireGuard configuration works, but managing multiple clients — generating keys, distributing configs, tracking who is connected — gets tedious fast. This is where wg-easy changes the game.
wg-easy is an open-source project that packages WireGuard with a clean, modern web-based administration interface inside a single Docker container. It handles everything: key generation, client management, QR codes for mobile devices, traffic statistics, enable/disable toggles per client, and more.
Features that make it worth using:
• QR code generation — scan with the WireGuard mobile app, connected in seconds
• Client management — add, remove, enable, disable clients from the web UI
• Traffic statistics — per-client Tx/Rx counters and bandwidth charts
• Two-factor authentication (2FA) — TOTP-based login protection for the admin panel
• OIDC support — integrate with your existing identity provider
• One-time links — share client configs that expire after first use
• Client expiration — set automatic expiry dates for temporary access
• Per-client firewall filtering — restrict what each client can access through the VPN
Prerequisites: Install Docker and Docker Compose on your Swiss VPS if not already present:
curl -fsSL https://get.docker.com | sh
systemctl enable --now docker
Generate a password hash for the web UI (bcrypt):
# Install apache2-utils for htpasswd, or use Docker
docker run --rm -it ghcr.io/wg-easy/wg-easy wgpw 'YOUR_SECURE_PASSWORD'
# Output will look like: $2a$12$abc...xyz
# When pasting into docker-compose.yml, escape each $ as $$
Create the docker-compose.yml:
# /opt/wg-easy/docker-compose.yml
services:
wg-easy:
image: ghcr.io/wg-easy/wg-easy
container_name: wg-easy
environment:
- LANGUAGE=en
- WG_HOST=your-server-ip-or-domain
- PASSWORD_HASH=$$2a$$12$$hashed_password_here
- WG_DEFAULT_DNS=1.1.1.1,9.9.9.9
- WG_ALLOWED_IPS=0.0.0.0/0,::/0
- UI_TRAFFIC_STATS=true
- UI_CHART_TYPE=2
volumes:
- wg-easy:/etc/wireguard
ports:
- "51820:51820/udp"
- "51821:51821/tcp"
cap_add:
- NET_ADMIN
- SYS_MODULE
sysctls:
- net.ipv4.ip_forward=1
- net.ipv4.conf.all.src_valid_mark=1
restart: unless-stopped
volumes:
wg-easy:
Key environment variables explained:
• WG_HOST — your server's public IP or domain name. Clients use this to connect
• PASSWORD_HASH — bcrypt hash of your admin password. Double-escape dollar signs with $$ in YAML
• WG_DEFAULT_DNS — DNS servers pushed to clients. Use privacy-respecting resolvers
• WG_ALLOWED_IPS — CIDR ranges clients can access. 0.0.0.0/0,::/0 means route everything
• UI_TRAFFIC_STATS — show per-client bandwidth usage in the web interface
• UI_CHART_TYPE — bandwidth chart style (1 = line, 2 = area, 3 = bar)
Launch it:
cd /opt/wg-easy
docker compose up -d
Access the web UI: Open http://your-server-ip:51821 in a browser. Log in with the password you hashed. You will see a clean dashboard where you can add clients with one click.
Adding a client: Click "New Client," give it a name (e.g., "Phone," "Laptop," "Router"). wg-easy generates the key pair, creates the config, and displays a QR code. On your phone, open the WireGuard app, tap the + icon, select "Scan from QR code," point at the screen. Connected. The entire process takes about ten seconds.
The web UI runs on HTTP by default. For production, put it behind an HTTPS reverse proxy. Here is a minimal Nginx configuration:
# /etc/nginx/sites-available/wg-easy
server {
listen 443 ssl http2;
server_name vpn.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/vpn.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/vpn.yourdomain.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:51821;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support (for real-time stats)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
server {
listen 80;
server_name vpn.yourdomain.com;
return 301 https://$server_name$request_uri;
}
With the reverse proxy in place, change the wg-easy port binding to only listen on localhost: "127.0.0.1:51821:51821/tcp" in the docker-compose.yml. This prevents direct HTTP access to the admin panel from the internet.
Keeping wg-easy updated:
# Pull the latest image and restart
cd /opt/wg-easy
docker compose up --detach --pull always
Run this periodically or automate it with a cron job. wg-easy is actively maintained and receives regular security patches.
Here is where most "privacy VPN" guides stop. They show you how to set up WireGuard, maybe mention Docker, and call it done. But your VPN server is still running a full Linux operating system that logs everything by default — every SSH login, every service start, every kernel message, every Docker container output. If someone gains access to your server — through a legal order, a hosting provider's cooperation, or a compromise — those logs tell the entire story of who connected, when, and what they did.
If your VPS logs connections, your privacy is theater.
This section covers disabling every logging mechanism on a Linux VPS. This is an aggressive approach. It makes debugging significantly harder. The trade-off is intentional: you are choosing privacy over convenience.
Recommended approach: Keep logging enabled during initial setup and testing. Once everything is stable and verified working, come back to this section and disable it all.
systemd-journald is the default logging daemon on modern Linux distributions. It captures output from every service, kernel messages, and system events. Disable persistent storage:
# Edit journald configuration
cat > /etc/systemd/journald.conf << 'EOF'
[Journal]
Storage=none
Compress=no
Seal=no
ForwardToSyslog=no
ForwardToKMsg=no
ForwardToConsole=no
ForwardToWall=no
EOF
# Restart journald to apply
systemctl restart systemd-journald
# Clear any existing journal data
journalctl --rotate
journalctl --vacuum-time=1s
With Storage=none, journald keeps logs only in a volatile ring buffer in memory. They disappear on reboot and cannot grow beyond a small memory allocation. Combined with the ForwardTo*=no directives, nothing is forwarded to other logging systems either.
rsyslog is the traditional syslog daemon. Even with journald configured to not forward, rsyslog may still capture messages directly from the kernel or from applications that write to /dev/log:
# Stop and disable rsyslog
systemctl stop rsyslog
systemctl disable rsyslog
# Verify it is dead
systemctl status rsyslog
If your system uses syslog-ng instead (common on some Debian configurations):
systemctl stop syslog-ng
systemctl disable syslog-ng
The kernel ring buffer (accessible via dmesg) logs hardware events, driver messages, and network interface changes — including WireGuard interface bring-up events. Restrict access and reduce verbosity:
# Restrict dmesg access to root only
echo "kernel.dmesg_restrict = 1" >> /etc/sysctl.d/99-no-logs.conf
# Reduce kernel log verbosity to emergencies only
echo "kernel.printk = 0 0 0 0" >> /etc/sysctl.d/99-no-logs.conf
# Apply immediately
sysctl -p /etc/sysctl.d/99-no-logs.conf
This does not fully eliminate kernel messages but restricts who can read them and reduces what gets generated. The kernel ring buffer is volatile — it exists only in memory and is lost on reboot.
Linux tracks every login, logout, and failed authentication attempt in binary log files. These are the records that commands like last, lastb, and lastlog read from. On a privacy VPN server, these need to go:
# Truncate existing records
> /var/log/wtmp
> /var/log/btmp
> /var/log/lastlog
# Make them immutable — even root cannot write to them
# (without first removing the immutable flag)
chattr +i /var/log/wtmp /var/log/btmp /var/log/lastlog
# Verify
lsattr /var/log/wtmp /var/log/btmp /var/log/lastlog
# Should show: ----i---------- for each file
The chattr +i flag sets the immutable attribute at the filesystem level. The system will continue trying to write login records to these files but will silently fail. No errors, no crashes — just no records.
By default, Docker captures everything that containers write to stdout and stderr and stores it as JSON files on disk. For a wg-easy container, this could include peer connection events and handshake logs. Disable Docker logging globally:
# Create or edit Docker daemon configuration
cat > /etc/docker/daemon.json << 'EOF'
{
"log-driver": "none"
}
EOF
# Restart Docker to apply
systemctl restart docker
# Restart wg-easy so it picks up the new logging driver
cd /opt/wg-easy
docker compose up -d --force-recreate
With "log-driver": "none", Docker does not write any container output to disk. docker logs wg-easy will return nothing. This is exactly what you want.
If you want per-container control instead of a global setting, add the logging directive to your docker-compose.yml:
services:
wg-easy:
image: ghcr.io/wg-easy/wg-easy
logging:
driver: none
# ... rest of config
WireGuard itself does not log by default — this is one of its design principles. There is no logging configuration option in the WireGuard kernel module. However, verify that no debug or verbose logging has been enabled:
# Check for any Log directives in WireGuard configs
grep -ri "log" /etc/wireguard/ 2>/dev/null
# For wg-easy, check the mounted volume
docker exec wg-easy grep -ri "log" /etc/wireguard/ 2>/dev/null
# Neither should return anything related to logging
WireGuard's lack of logging is not a missing feature — it is an intentional cryptographic design decision. The protocol does not even have a concept of "connection" or "session" in the traditional sense. Peers exchange encrypted UDP packets. There is no handshake logging, no connection state logging, no traffic logging. It is privacy by architecture.
The cron daemon logs every job execution, including timestamps. If you are using cron for any maintenance tasks, these logs can reveal server activity patterns:
# For cron, edit /etc/default/cron (Debian/Ubuntu)
# Add EXTRA_OPTS to suppress logging
echo 'EXTRA_OPTS="-L 0"' >> /etc/default/cron
systemctl restart cron
# Disable at daemon if not needed
systemctl stop atd
systemctl disable atd
After disabling all logging daemons, clean up whatever logs already exist on the system:
# Remove all existing log files
find /var/log -type f -name "*.log" -delete
find /var/log -type f -name "*.gz" -delete
find /var/log -type f -name "*.old" -delete
find /var/log -type f -name "*.1" -delete
# Truncate log files that are held open by running processes
find /var/log -type f -exec truncate -s 0 {} \;
# Remove log rotation configs (no logs = no need to rotate)
rm -f /etc/logrotate.d/*
After all the above changes, verify that the system is genuinely not writing logs anywhere:
# Create a timestamp marker
touch /tmp/log-check-marker
# Wait a few minutes, then check for any new files in /var/log
sleep 180
find /var/log -type f -newer /tmp/log-check-marker -ls
# Check journald (should show nothing or minimal entries)
journalctl --no-pager 2>&1 | wc -l
# Check Docker logs (should return error or empty)
docker logs wg-easy 2>&1 | wc -l
# Check for any syslog sockets still active
ss -xlnp | grep -i log
# Check what is writing to /var/log
lsof +D /var/log 2>/dev/null
If find returns new files, investigate what is creating them. Common culprits include fail2ban (if installed), unattended-upgrades, or custom scripts. Disable or reconfigure each one individually.
Let us be direct about what you are giving up. With all logging disabled:
• Debugging is painful. When something breaks, you have no logs to diagnose it. You are flying blind. You will need to re-enable logging temporarily to troubleshoot, then disable it again once the issue is resolved.
• Security monitoring is gone. You cannot detect brute-force SSH attempts, unusual process behavior, or unauthorized access without logs. Compensate with external monitoring and strict firewall rules.
• Compliance frameworks may object. Some regulatory frameworks require logging. If you are subject to such requirements, this approach conflicts with them. Know your legal obligations.
• You need to trust your setup. Without logs, you will not know if something quietly failed at 3 AM. Automated health checks that report externally (not to local logs) are essential.
This is a real trade-off, not a free win. You are optimizing for one thing — genuine privacy — at the cost of operational visibility. For a personal VPN server where privacy is the entire point, this trade-off makes sense. For production infrastructure serving customers, it probably does not.
With logging disabled, your firewall becomes even more critical. Lock down the server to only the ports it needs:
# Reset iptables to a clean state
iptables -F
iptables -X
# Default policies: drop everything
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
# Allow established connections
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# Allow loopback
iptables -A INPUT -i lo -j ACCEPT
# Allow SSH (restrict to your management IP if possible)
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
# Better: iptables -A INPUT -p tcp --dport 22 -s YOUR_MGMT_IP -j ACCEPT
# Allow WireGuard
iptables -A INPUT -p udp --dport 51820 -j ACCEPT
# Allow wg-easy web UI (only if NOT behind reverse proxy)
# iptables -A INPUT -p tcp --dport 51821 -j ACCEPT
# Allow forwarding through the VPN
iptables -A FORWARD -i wg0 -j ACCEPT
iptables -A FORWARD -o wg0 -m state --state ESTABLISHED,RELATED -j ACCEPT
# NAT for VPN clients
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
# Save rules
apt install -y iptables-persistent
netfilter-persistent save
If you restricted SSH to a specific management IP, make absolutely sure that IP is correct and static. With logging disabled, you will not see failed login attempts — but with a proper firewall, there should not be any to see.
Jurisdiction matters. Switzerland is not in the EU and not subject to EU data retention directives. The Swiss Federal Act on Data Protection (FADP) provides strong protections that are enforced domestically, not watered down by multinational agreements. Swiss authorities cannot compel a hosting provider to install monitoring equipment the way US National Security Letters or UK Investigatory Powers Act orders can.
This does not mean Switzerland is a lawless haven — it is not. Swiss courts can and do issue lawful orders for data access. But those orders require Swiss judicial oversight, probable cause for serious crimes, and cannot be issued secretly or extraterritorially. The bar is higher, the process is transparent, and the scope is narrower than most comparable jurisdictions.
For a self-hosted VPN, the jurisdiction of your server determines whose legal system governs access to that server. A VPN running on a Swiss VPS benefits from Swiss privacy law. A VPN running on an AWS instance in Frankfurt does not, regardless of what country you personally are in.
Infrastructure quality matters too. Swiss data centers operate on redundant power grids, maintain strict physical access controls, and connect to major European internet exchanges with low-latency peering. Your VPN is only useful if it is fast and reliable. A server that drops offline or routes traffic through congested paths defeats the purpose.
SwissLayer's dedicated servers provide bare-metal hardware — no shared hypervisor, no noisy neighbors, no cloud provider middleware between you and the network stack. For a WireGuard deployment, this means kernel-level performance with direct NIC access. Combined with unmetered bandwidth on 1Gbps+ ports, your VPN will not be the bottleneck.
Before you call this deployment complete, verify each item:
• WireGuard connectivity: Connect from a client, verify IP shows as your Swiss VPS IP at ifconfig.me or ip.me
• DNS leak test: Visit dnsleaktest.com and run the extended test. Only your configured DNS servers should appear
• WebRTC leak test: Visit browserleaks.com/webrtc. Your real IP should not be visible
• Kill switch verification: Disconnect WireGuard. Try to access any website. Nothing should load
• wg-easy dashboard: Confirm all clients appear, traffic stats are updating, QR codes generate correctly
• Logging verification: Run the verification commands from Step 9. Confirm zero log output
• Firewall verification: nmap your-server-ip from an external host. Only ports 22 (SSH), 51820/udp (WireGuard), and optionally 443 (if using reverse proxy) should be open
• Reboot persistence: Reboot the server. Verify WireGuard, Docker, wg-easy, and firewall rules all come back automatically
Run through this checklist after every change. A VPN that leaks once is a VPN that was not worth building.
A self-hosted WireGuard VPN on Swiss infrastructure is not the easiest path. A commercial VPN subscription is simpler — download an app, click connect, done. But simplicity is not the point. Control is the point. Verifiability is the point.
With a commercial VPN, you trust a company's no-logs claim. With your own server, you can verify it. You can read the WireGuard source code, inspect the kernel module, audit the Docker container, check every log directory, and confirm with your own eyes that nothing is being recorded. That verification is something no commercial VPN can offer.
Swiss jurisdiction adds legal backing to what the technical setup provides. The combination of WireGuard's minimal, auditable codebase running on infrastructure protected by Swiss privacy law, with logging explicitly eliminated at every layer of the operating system, is as close to a truly private VPN as current technology allows.
It is not perfect — nothing is. But it is honest. And in the VPN space, honesty is rare.