Setup a Secure WireGuard VPN With wg-easy on AlmaLinux 10

Set up a secure WireGuard VPN on AlmaLinux 10 with the wg-easy v15 web UI: Docker install, firewalld hardening, and split tunnel or full tunnel modes.

WireGuard is a modern, fast, and secure VPN protocol that’s significantly simpler than traditional VPN solutions like OpenVPN or IPSec. In this comprehensive guide, you’ll learn how to set up a production-ready WireGuard VPN server on AlmaLinux 10, complete with the wg-easy web interface for easy peer management, proper firewall hardening, and configuration for both split tunnel and full tunnel modes.

What You’ll Learn

  • Installing and configuring WireGuard on AlmaLinux 10
  • Setting up firewalld for secure VPN operations
  • Deploying wg-easy web UI for graphical peer management
  • Understanding split tunnel vs full tunnel VPN configurations
  • Hardening your VPN server for production use
  • Managing VPN clients across different platforms

Prerequisites

Before we begin, ensure you have:

  • A server running AlmaLinux 10.1 or later
  • Root access to the server
  • A public IP address (e.g., 100.23.15.226)
  • Basic knowledge of Linux command line
  • Firewall port 51820/UDP accessible from the internet

This guide targets wg-easy v15, which is a ground-up rewrite. If you’re following an older tutorial that has you generating a bcrypt PASSWORD_HASH or setting WG_HOST and friends as environment variables, that’s v14 — those variables no longer do anything. Step 8 covers the difference and the migration path.

Understanding VPN Tunnel Modes

Before diving into installation, it’s crucial to understand the two main VPN tunnel modes and choose the right one for your use case.

Full Tunnel VPN

What it does: Routes ALL your internet traffic through the VPN server.

Benefits:

  • Your public IP appears as the VPN server’s IP address
  • All internet traffic is encrypted end-to-end
  • Ideal for privacy and security on public WiFi
  • Bypass geo-restrictions and censorship
  • Complete traffic anonymization

Use cases:

  • Working from coffee shops or airports
  • Accessing region-locked content
  • Maximum privacy protection
  • Protecting all device traffic

Configuration: Client uses AllowedIPs = 0.0.0.0/0, ::/0

Drawbacks:

  • Slightly slower internet speeds (all traffic routes through VPN)
  • Increased data usage on VPN server
  • May interfere with local network access

Split Tunnel VPN

What it does: Only routes specific network traffic through the VPN.

Benefits:

  • Faster general internet browsing (direct connection)
  • Lower data usage on VPN server
  • Access local network resources simultaneously
  • Better for accessing internal company resources

Use cases:

  • Accessing internal network services remotely
  • Connecting to private databases or applications
  • Maintaining local network printer/scanner access
  • Optimized performance for specific resources

Configuration: Client uses AllowedIPs = 10.99.0.0/24 (VPN network only)

Drawbacks:

  • Your public IP remains unchanged for non-VPN traffic
  • Less privacy protection for general internet usage
  • Requires careful routing configuration

Step 1: Update System and Install WireGuard

First, ensure your AlmaLinux 10 system is up to date and install WireGuard tools.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Update system packages
sudo dnf update -y

# Install EPEL repository (if not already installed)
sudo dnf install -y epel-release

# Install WireGuard and related tools
sudo dnf install -y wireguard-tools qrencode

# Verify installation
wg --version

Expected output (the exact version depends on what your repos currently ship — anything from the v1.0.x series is fine):

1
wireguard-tools v1.0.20250521

WireGuard itself is in the kernel on AlmaLinux 10, so wireguard-tools is the only userspace piece you need.

Step 2: Enable IP Forwarding

For VPN traffic routing to work, enable IP forwarding at the kernel level.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Enable IPv4 forwarding
echo "net.ipv4.ip_forward=1" | sudo tee -a /etc/sysctl.d/99-wireguard.conf

# Enable IPv6 forwarding (optional)
echo "net.ipv6.conf.all.forwarding=1" | sudo tee -a /etc/sysctl.d/99-wireguard.conf

# Enable source address validation (required for WireGuard)
echo "net.ipv4.conf.all.src_valid_mark=1" | sudo tee -a /etc/sysctl.d/99-wireguard.conf

# Apply changes
sudo sysctl -p /etc/sysctl.d/99-wireguard.conf

Verify settings:

1
2
sysctl net.ipv4.ip_forward
# Should return: net.ipv4.ip_forward = 1

Step 3: Configure Firewalld Security

AlmaLinux 10 uses firewalld for firewall management. Let’s configure it securely for WireGuard.

Install and Enable Firewalld

1
2
3
4
5
6
7
8
9
# Install firewalld (usually pre-installed)
sudo dnf install -y firewalld

# Start and enable firewalld
sudo systemctl enable --now firewalld

# Check status
sudo firewall-cmd --state
# Should return: running

Configure Firewall Rules

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Allow SSH (if not already allowed)
sudo firewall-cmd --permanent --add-service=ssh

# Allow WireGuard UDP port
sudo firewall-cmd --permanent --add-port=51820/udp

# Enable masquerading for NAT (required for VPN routing)
sudo firewall-cmd --permanent --zone=public --add-masquerade

# Give the tunnel its own zone rather than lumping it in with your LAN
sudo firewall-cmd --permanent --new-zone=wireguard
sudo firewall-cmd --permanent --zone=wireguard --add-interface=wg0

# Allow the tunnel to forward OUT to the public zone. This crosses zones, so it
# needs a policy object — see the note below.
sudo firewall-cmd --permanent --new-policy=wg-to-public
sudo firewall-cmd --permanent --policy=wg-to-public --add-ingress-zone=wireguard
sudo firewall-cmd --permanent --policy=wg-to-public --add-egress-zone=public
sudo firewall-cmd --permanent --policy=wg-to-public --set-target=ACCEPT

# Reload firewall to apply changes
sudo firewall-cmd --reload

# Verify rules
sudo firewall-cmd --list-all
sudo firewall-cmd --info-policy=wg-to-public

Why a policy object, and not --add-forward? This is the single easiest thing to get wrong. --add-forward enables intra-zone forwarding only — traffic between interfaces that are in the same zone. A full-tunnel VPN is the other case: packets arrive on wg0 (the wireguard zone) and leave on your NIC (the public zone), which is inter-zone. --add-forward does not cover that, and a zone whose target is default will not forward across zones — so a setup that puts wg0 in internal and calls --add-forward looks right, reloads cleanly, and still leaves your clients with a tunnel but no internet. Inter-zone forwarding is exactly what policy objects are for.

The older way to force this through was firewall-cmd --direct --add-rule ipv4 filter FORWARD .... Those rules still work, but the direct interface has been deprecated since firewalld 1.0 and is superseded by exactly the zones-and-policies model above. Only fall back to --direct on firewalld releases too old to have policy objects (pre-0.9).

A blunter alternative, if you don’t want a policy: put wg0 in the built-in trusted zone, whose target is ACCEPT — and per the firewalld man page, ACCEPT “allows traffic to be forwarded from the zone, either to the same zone or other zones.” It works, but it also means VPN clients can reach every port on the VPN server itself, which the policy above does not.

Expected output:

1
2
3
4
5
public (active)
  target: default
  services: ssh
  ports: 51820/udp
  masquerade: yes

Step 4: Generate WireGuard Server Keys

WireGuard uses asymmetric cryptography. Generate server keys securely.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Create WireGuard configuration directory
sudo mkdir -p /etc/wireguard
sudo chmod 700 /etc/wireguard

# Generate server private key
wg genkey | sudo tee /etc/wireguard/server_private.key
sudo chmod 600 /etc/wireguard/server_private.key

# Generate server public key from private key
sudo cat /etc/wireguard/server_private.key | wg pubkey | sudo tee /etc/wireguard/server_public.key

# Display public key (save this - clients will need it)
cat /etc/wireguard/server_public.key

Step 5: Create WireGuard Server Configuration

Now create the main server configuration file. We’ll configure it for full tunnel mode by default.

1
2
# Create base configuration
sudo vim /etc/wireguard/wg0.conf

For Full Tunnel VPN (routes all client traffic):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
[Interface]
# Server VPN IP address
Address = 10.99.0.1/24

# UDP port WireGuard listens on
ListenPort = 51820

# Server private key
PrivateKey = <paste your server private key here>

# PostUp/PostDown rules for firewalld integration (runtime, zone-based)
PostUp = firewall-cmd --zone=public --add-masquerade
PostUp = firewall-cmd --zone=internal --add-interface=wg0
PostUp = firewall-cmd --zone=internal --add-forward
PostDown = firewall-cmd --zone=internal --remove-forward
PostDown = firewall-cmd --zone=internal --remove-interface=wg0
PostDown = firewall-cmd --zone=public --remove-masquerade

# Peer configurations will be added below
# Each client is a peer

Because Step 3 already wrote the equivalent rules with --permanent, these hooks are belt-and-braces — they keep the runtime firewall correct if someone reloads firewalld while the tunnel is down. Drop them if you prefer to manage the firewall purely declaratively.

Replace <paste your server private key here> with the contents of /etc/wireguard/server_private.key.

Set proper permissions:

1
sudo chmod 600 /etc/wireguard/wg0.conf

Step 6: Enable and Start WireGuard Service

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Enable WireGuard service
sudo systemctl enable wg-quick@wg0

# Start WireGuard service
sudo systemctl start wg-quick@wg0

# Check status
sudo systemctl status wg-quick@wg0

# Verify WireGuard interface
sudo wg show

Expected output:

1
2
3
4
interface: wg0
  public key: <your server public key>
  private key: (hidden)
  listening port: 51820

Step 7: Install Docker for wg-easy Web UI

wg-easy provides a modern web interface for managing WireGuard peers. We’ll install Docker to run it.

Important — pick one WireGuard server, not two. wg-easy runs its own WireGuard interface inside the container and binds UDP/51820 itself. If you completed Steps 4–6, the host’s wg-quick@wg0 already owns that port, and the container will fail to start with an “address already in use” error. Before continuing, stop and disable the manual tunnel:

1
sudo systemctl disable --now wg-quick@wg0

Steps 4–6 are still worth reading — they show what wg-easy automates for you, and they’re the path to take if you’d rather manage peers by hand. Just don’t run both at once. (If you genuinely need both, give each a different ListenPort.)

Install Docker CE on AlmaLinux 10

AlmaLinux 10 requires Docker CE from the official Docker repository.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Install the config-manager plugin (AlmaLinux 10 ships dnf5)
sudo dnf install -y dnf5-plugins

# Add Docker CE repository
sudo dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/centos/docker-ce.repo

# Install Docker packages
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# Start and enable Docker
sudo systemctl enable --now docker

# Verify Docker installation
sudo docker --version

--add-repo is gone on AlmaLinux 10. AlmaLinux 10 ships dnf5, where config-manager grew a proper addrepo subcommand and the old dnf config-manager --add-repo <url> flag was removed — it now fails with Unknown argument "--add-repo". Older guides (and Docker’s own EL instructions) still show the DNF 4 form. If you’d rather skip the plugin entirely, dropping the repo file in place works on any release:

1
2
sudo curl -fsSL https://download.docker.com/linux/centos/docker-ce.repo \
  -o /etc/yum.repos.d/docker-ce.repo

A word on Docker and nftables

AlmaLinux 10 uses nftables, and you’ll find plenty of guides telling you to set "iptables": false in /etc/docker/daemon.json on that basis. Don’t — not for this setup. Docker’s iptables and ip6tables commands are symlinked to iptables-nft, so its rules are translated into nftables for you and everything works out of the box. Turning Docker’s firewall management off only makes sense when every container runs with network_mode: host; it breaks NAT and published ports for containers on a bridge network, which is exactly what the wg-easy compose file below uses. Leave the default alone and you don’t need a daemon.json at all.

1
2
# Verify Docker is running
sudo docker ps

If you’re on Docker Engine 29 or newer and want to skip the iptables compatibility layer, there is now an experimental native nftables backend ("firewall-backend": "nftables" in daemon.json). It’s optional, and not required for anything in this guide — see the Docker nftables documentation.

Step 8: Deploy wg-easy Web UI

Now deploy the wg-easy container for graphical peer management.

First, know which wg-easy you’re running

This matters more than anything else in this step. wg-easy v15 is a complete rewrite, and it threw out the configuration model that every older tutorial (including the first version of this guide) depends on:

v14 and earlierv15 and later
PASSWORD_HASH bcrypt hash, generated with docker run ... wgpwAdmin account created in a web setup wizard on first run
WG_HOST, WG_PORT, WG_DEFAULT_ADDRESS, WG_DEFAULT_DNS, WG_ALLOWED_IPS, WG_MTU, WG_PERSISTENT_KEEPALIVEConfigured in the Admin Panel, stored in a SQLite database
Config in wg0.jsonConfig in a SQLite DB under /etc/wireguard
network_mode: host typicalBridge network with published ports

The wgpw subcommand and the WG_* environment variables are simply gone in v15 — they are silently ignored, which is a confusing way to fail.

Watch out for the latest tag here. At the time of writing it still resolves to v14, not v15 — upstream deliberately held it back because v15 breaks existing installs. So image: ghcr.io/wg-easy/wg-easy:latest does not mean “v15 today”, and it does not mean “v14 forever” either: the day that tag is moved, an unpinned docker compose pull silently swaps your VPN’s entire configuration model. That is the real argument for pinning. The instructions below pin 15 explicitly, so you get patch and minor updates within v15 and a future v16 can’t surprise you.

Create wg-easy Docker Compose File

1
2
3
4
5
6
# Create directory for wg-easy
sudo mkdir -p /opt/wg-easy
cd /opt/wg-easy

# Create docker-compose.yml
sudo vim docker-compose.yml

This is upstream’s recommended v15 compose file, with one addition (INSECURE, explained below):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
volumes:
  etc_wireguard:

services:
  wg-easy:
    image: ghcr.io/wg-easy/wg-easy:15
    container_name: wg-easy
    environment:
      # Allow logging in over plain HTTP. See the warning below.
      - INSECURE=true
    networks:
      wg:
        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"
      - "51821:51821/tcp"
    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

networks:
  wg:
    driver: bridge
    enable_ipv6: true
    ipam:
      driver: default
      config:
        - subnet: 10.42.42.0/24
        - subnet: fdcc:ad94:bacf:61a3::/64

Important notes:

  • There is no version: key. Compose v2 ignores it and warns that it’s obsolete.
  • There is no server IP, VPN subnet, or DNS setting here any more — you’ll set those in the setup wizard. The 10.42.42.0/24 block above is the container’s Docker network, not your VPN subnet.
  • The sysctls: block is what makes forwarding work inside the container. It does not replace the host-level forwarding you enabled in Step 2.

About INSECURE=true

v15 refuses to let you log in over plain HTTP — you’ll get “You can’t log in with an insecure connection. Use HTTPS.” Setting INSECURE=true re-enables HTTP so you can reach the UI at http://<server-ip>:51821 and finish the wizard.

That is fine for a first run on a trusted network, but your admin password crosses the internet in the clear. For anything long-lived, drop INSECURE and put the UI behind a reverse proxy with a real TLS certificate, or restrict port 51821 to your own IP (Step 11) and reach it over an SSH tunnel.

Allow wg-easy Web UI Port

1
2
3
# Allow web UI port through firewall
sudo firewall-cmd --permanent --add-port=51821/tcp
sudo firewall-cmd --reload

Start wg-easy

1
2
3
4
5
6
7
8
# Start wg-easy container
sudo docker compose up -d

# Check container status
sudo docker ps

# View logs
sudo docker logs wg-easy

Expected output:

1
2
CONTAINER ID   IMAGE                        STATUS
abc123def456   ghcr.io/wg-easy/wg-easy:15   Up 30 seconds (healthy)

Use docker compose up / docker compose down to manage the container — upstream specifically warns against docker compose start / stop on v15, which can leave its state inconsistent.

Create Systemd Service for wg-easy

For automatic startup on boot:

1
2
# Create systemd service
sudo vim /etc/systemd/system/wg-easy.service

Add the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[Unit]
Description=WireGuard Easy Web UI
Requires=docker.service
After=docker.service

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/wg-easy
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=0

[Install]
WantedBy=multi-user.target

Enable the service:

1
2
3
4
5
6
7
8
# Reload systemd
sudo systemctl daemon-reload

# Enable wg-easy service
sudo systemctl enable wg-easy

# Check status
sudo systemctl status wg-easy

Step 9: Access wg-easy Web Interface

Open your browser and navigate to:

1
http://100.23.15.226:51821

(Replace with your server’s IP)

Complete the Setup Wizard

On a fresh v15 install you are met by a setup wizard, not a login prompt. There is no pre-seeded password — you create the admin account here. Walk through it and set:

  • Admin username and password — this replaces the old PASSWORD_HASH variable
  • Host / public endpoint — your server’s public IP or DNS name (e.g. 100.23.15.226), which is what clients will connect to. This replaces WG_HOST.
  • VPN subnet10.99.0.0/24 to match the rest of this guide. This replaces WG_DEFAULT_ADDRESS.
  • Client DNS — e.g. 1.1.1.1. This replaces WG_DEFAULT_DNS.

Everything else (MTU, keepalive, allowed IPs) lives in Admin Panel → Settings once you’re in, and can be changed later without touching the compose file.

Migrating an existing v14 install? Back up its wg0.json first, then upload it when the wizard offers to import an existing configuration. v14 environment variables are not migrated automatically.

Add Your First VPN Client

  1. Click "+ New" button

  2. Enter a client name (e.g., “john-laptop”)

  3. The web UI automatically:

    • Generates client keys
    • Assigns an IP address (10.99.0.10, 10.99.0.11, etc.)
    • Creates the configuration
    • Generates a QR code
  4. For mobile devices: Scan the QR code with the WireGuard app

  5. For desktop/laptop: Click “Download” to get the configuration file

Step 10: Configure VPN Clients

Mobile Clients (iOS/Android)

iOS:

  1. Install WireGuard from App Store
  2. Tap “+” → “Create from QR code”
  3. Scan the QR code from wg-easy
  4. Toggle connection on

Android:

  1. Install WireGuard from Play Store
  2. Tap “+” → “Scan from QR code”
  3. Scan the QR code from wg-easy
  4. Toggle connection on

Desktop/Laptop Clients

Linux:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# Install WireGuard
sudo dnf install wireguard-tools  # RHEL/AlmaLinux
sudo apt install wireguard         # Ubuntu/Debian

# Copy configuration from wg-easy
sudo vim /etc/wireguard/wg0.conf
# Paste the downloaded configuration

# Set permissions
sudo chmod 600 /etc/wireguard/wg0.conf

# Start VPN
sudo wg-quick up wg0

# Enable on boot (optional)
sudo systemctl enable wg-quick@wg0

# Check status
sudo wg show

macOS:

  1. Install WireGuard from App Store
  2. Click “Import Tunnel(s) from File”
  3. Select downloaded .conf file
  4. Toggle connection on

Windows:

  1. Download WireGuard for Windows
  2. Click “Import Tunnel(s) from File”
  3. Select downloaded .conf file
  4. Click “Activate”

Verify VPN Connection

Once connected, verify your setup:

1
2
3
4
5
6
7
8
9
# Check your public IP (should show VPN server IP for full tunnel)
curl ifconfig.me

# Ping VPN server
ping 10.99.0.1

# Check VPN interface
ip addr show wg0    # Linux
ifconfig utun3      # macOS

For full tunnel: curl ifconfig.me should return your VPN server’s IP (100.23.15.226)

For split tunnel: curl ifconfig.me returns your original IP, but you can access VPN network (10.99.0.0/24)

Step 11: Security Hardening Best Practices

1. Use Strong Passwords

1
2
# Generate strong password for wg-easy
openssl rand -base64 32

2. Implement Fail2ban for SSH Protection

1
2
3
4
5
6
7
8
# Install fail2ban
sudo dnf install -y fail2ban

# Enable and start
sudo systemctl enable --now fail2ban

# Check status
sudo fail2ban-client status sshd

3. Regular Security Updates

1
2
# Create update script
sudo vim /usr/local/bin/security-updates.sh

Add:

1
2
3
4
5
#!/bin/bash
set -euo pipefail
dnf update -y
cd /opt/wg-easy && docker compose pull && docker compose up -d
docker image prune -f

Because the compose file pins wg-easy:15, this pulls patch and minor updates within v15 but will never jump you across a breaking major release. Read the release notes before bumping that tag.

1
2
3
4
5
# Make executable
sudo chmod +x /usr/local/bin/security-updates.sh

# Add to crontab (weekly updates)
sudo crontab -e

Add line:

1
0 3 * * 0 /usr/local/bin/security-updates.sh >> /var/log/security-updates.log 2>&1

4. Monitor VPN Activity

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Check active connections
sudo wg show

# View WireGuard logs
sudo journalctl -u wg-quick@wg0 -f

# View wg-easy logs
sudo docker logs -f wg-easy

# Check firewall logs
sudo journalctl -u firewalld -f

5. Backup WireGuard Configuration

1
2
3
4
5
# Create backup
sudo tar -czf wireguard-backup-$(date +%Y%m%d).tar.gz /etc/wireguard/ /opt/wg-easy/

# Copy to safe location
scp wireguard-backup-*.tar.gz user@backup-server:/backups/

6. Limit wg-easy Web UI Access

Only allow access from specific IPs:

1
2
3
4
5
6
7
8
# Allow only from specific IP
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="YOUR_IP/32" port protocol="tcp" port="51821" accept'

# Remove general port rule
sudo firewall-cmd --permanent --remove-port=51821/tcp

# Reload
sudo firewall-cmd --reload

Step 12: Switching Between Tunnel Modes

To switch from full tunnel to split tunnel (or vice versa):

Update wg-easy Configuration

On v15 this is a web-UI change, not a compose-file change — there is no WG_ALLOWED_IPS variable any more. Go to Admin Panel → Settings and edit the default Allowed IPs:

  • Full tunnel: 0.0.0.0/0, ::/0
  • Split tunnel: 10.99.0.0/24

Save, and no container restart is needed. Note that this sets the default for newly created clients; existing clients keep the value they were issued, so update them using one of the two options below.

Update Existing Clients

Option 1: Re-generate client configs in wg-easy

  1. Delete old client from web UI
  2. Re-add client (new config will use updated tunnel mode)
  3. Re-deploy configuration to client

Option 2: Manually edit client configuration

Edit the AllowedIPs line in client config:

1
2
3
4
5
6
7
[Peer]
PublicKey = <server public key>
Endpoint = 100.23.15.226:51820
AllowedIPs = 0.0.0.0/0, ::/0      # Full tunnel
# OR
AllowedIPs = 10.99.0.0/24         # Split tunnel
PersistentKeepalive = 25

Reconnect to apply changes.

Troubleshooting Common Issues

Issue 1: VPN Connects but No Internet Access

Symptoms: Connected to VPN but can’t browse internet.

Solution:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Check IP forwarding
sysctl net.ipv4.ip_forward
# Should return: net.ipv4.ip_forward = 1

# Check masquerading
sudo firewall-cmd --zone=public --query-masquerade
# Should return: yes

# Check forwarding is allowed in the zone holding wg0
sudo firewall-cmd --zone=internal --query-forward
# Should return: yes

# Restart WireGuard
sudo systemctl restart wg-quick@wg0

If you’re running wg-easy rather than the manual tunnel, the container handles its own forwarding via the sysctls: block — check sudo docker logs wg-easy instead, and confirm host forwarding is on (Step 2).

Issue 2: wg-easy Container Keeps Restarting

Symptoms: docker ps shows “Restarting” status.

Solution:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Check container logs
sudo docker logs wg-easy

# Common issues on v15:
# - UDP/51820 already in use by a host wg-quick@wg0 (see Step 7)
# - Missing cap_add: NET_ADMIN / SYS_MODULE
# - /lib/modules not mounted read-only into the container
# - Following a v14 tutorial: PASSWORD_HASH and WG_* vars do nothing on v15

# Rebuild container
cd /opt/wg-easy
sudo docker compose down
sudo docker compose up -d

If the port is the problem, confirm what already holds it:

1
sudo ss -ulnp | grep 51820

Issue 3: Can’t Connect to wg-easy Web UI

Symptoms: Browser shows “connection refused” on port 51821.

Solution:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Check if port is open
sudo firewall-cmd --list-ports | grep 51821

# If not, add it
sudo firewall-cmd --permanent --add-port=51821/tcp
sudo firewall-cmd --reload

# Check Docker is running
sudo docker ps | grep wg-easy

# Check if port is listening
sudo ss -tulnp | grep 51821

Issue 4: DNS Not Working on VPN

Symptoms: Can ping IP addresses but not domain names.

Solution:

Update the client DNS servers in Admin Panel → Settings (on v14 this was the WG_DEFAULT_DNS variable), then re-issue the client config — for example:

1
1.1.1.1, 8.8.8.8, 9.9.9.9

Or fix it directly in an existing client config:

1
2
[Interface]
DNS = 1.1.1.1, 8.8.8.8

Issue 5: Firewall Blocks VPN Traffic

Symptoms: VPN connects but traffic is blocked.

Solution:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Check firewall rules
sudo firewall-cmd --list-all

# Re-apply WireGuard rules
sudo firewall-cmd --permanent --zone=public --add-masquerade
sudo firewall-cmd --permanent --zone=internal --add-interface=wg0
sudo firewall-cmd --permanent --zone=internal --add-forward
sudo firewall-cmd --reload

# Check SELinux (if enabled)
sudo getenforce
# If "Enforcing", check audit logs:
sudo ausearch -m avc -ts recent | grep wireguard

Performance Optimization

Optimize MTU Settings

MTU lives in Admin Panel → Settings on v15 (it was the WG_MTU variable on v14). The default is 1420. If you see packet loss or stalled TLS handshakes over the tunnel — the classic symptom of MTU trouble — try 1400, then 1380.

Enable BBR Congestion Control

1
2
3
4
# Enable BBR for better throughput
echo "net.core.default_qdisc=fq" | sudo tee -a /etc/sysctl.d/99-wireguard.conf
echo "net.ipv4.tcp_congestion_control=bbr" | sudo tee -a /etc/sysctl.d/99-wireguard.conf
sudo sysctl -p /etc/sysctl.d/99-wireguard.conf

Monitor Performance

1
2
3
4
5
6
7
8
# Check WireGuard statistics
sudo wg show wg0

# Monitor bandwidth
sudo iftop -i wg0

# Check connection quality
sudo wg show wg0 latest-handshakes

Maintenance Tasks

Daily/Weekly Checks

1
2
3
4
5
6
7
8
9
# Check service status
sudo systemctl status wg-quick@wg0
sudo systemctl status wg-easy

# View recent logs
sudo journalctl -u wg-quick@wg0 --since "1 hour ago"

# Check active peers
sudo wg show

Monthly Tasks

1
2
3
4
5
6
7
8
9
# Update system and Docker images
sudo dnf update -y
cd /opt/wg-easy && sudo docker compose pull && sudo docker compose up -d

# Review and remove unused peers
# (Via wg-easy web UI)

# Rotate logs (if needed)
sudo journalctl --vacuum-time=30d

Backup Configuration

Create automated backups:

1
2
# Create backup script
sudo vim /usr/local/bin/backup-wireguard.sh

Add:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/bin/bash
BACKUP_DIR="/root/wireguard-backups"
DATE=$(date +%Y%m%d-%H%M%S)

mkdir -p $BACKUP_DIR
tar -czf $BACKUP_DIR/wireguard-$DATE.tar.gz \
  /etc/wireguard/ \
  /opt/wg-easy/ \
  /etc/docker/daemon.json

# Keep only last 7 backups
cd $BACKUP_DIR && ls -t | tail -n +8 | xargs -r rm
1
2
3
4
5
# Make executable
sudo chmod +x /usr/local/bin/backup-wireguard.sh

# Add to cron (daily at 2 AM)
echo "0 2 * * * /usr/local/bin/backup-wireguard.sh" | sudo crontab -

Conclusion

You now have a fully functional, secure WireGuard VPN server running on AlmaLinux 10 with:

Modern WireGuard protocol for fast, secure VPN connections ✅ wg-easy web UI for easy peer management ✅ Firewalld hardening for production-grade security ✅ Flexible tunnel modes (full or split tunnel) ✅ Cross-platform support for all devices ✅ Automated management with systemd services

Key Takeaways

  1. Full Tunnel: Best for privacy and security - routes all traffic through VPN
  2. Split Tunnel: Best for performance - routes only specific traffic through VPN
  3. Firewalld Integration: Essential for AlmaLinux 10 with nftables kernel
  4. wg-easy Simplifies Management: Web UI makes peer management effortless
  5. Regular Maintenance: Keep your VPN secure with updates and monitoring

Next Steps

  • Configure additional security measures (fail2ban, intrusion detection)
  • Set up automated monitoring and alerting
  • Implement log aggregation for better visibility
  • Consider setting up WireGuard on multiple servers for redundancy
  • Explore advanced routing configurations for specific use cases

Resources

Have questions or run into issues? Drop a comment below, and I’ll be happy to help troubleshoot your WireGuard setup!


This guide was tested on AlmaLinux 10.1 (kernel 6.12) with wg-easy v15. Commands and configurations may vary slightly for other versions — in particular, wg-easy v14 and earlier use a completely different, environment-variable-based configuration model.

comments powered by Disqus
Citizix Ltd
Built with Hugo
Theme Stack designed by Jimmy