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.
| |
Expected output (the exact version depends on what your repos currently ship — anything from the v1.0.x series is fine):
| |
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.
| |
Verify settings:
| |
Step 3: Configure Firewalld Security
AlmaLinux 10 uses firewalld for firewall management. Let’s configure it securely for WireGuard.
Install and Enable Firewalld
| |
Configure Firewall Rules
| |
Why a policy object, and not
--add-forward? This is the single easiest thing to get wrong.--add-forwardenables intra-zone forwarding only — traffic between interfaces that are in the same zone. A full-tunnel VPN is the other case: packets arrive onwg0(thewireguardzone) and leave on your NIC (thepubliczone), which is inter-zone.--add-forwarddoes not cover that, and a zone whose target isdefaultwill not forward across zones — so a setup that putswg0ininternaland calls--add-forwardlooks 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--directon firewalld releases too old to have policy objects (pre-0.9).A blunter alternative, if you don’t want a policy: put
wg0in the built-intrustedzone, whose target isACCEPT— 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:
| |
Step 4: Generate WireGuard Server Keys
WireGuard uses asymmetric cryptography. Generate server keys securely.
| |
Step 5: Create WireGuard Server Configuration
Now create the main server configuration file. We’ll configure it for full tunnel mode by default.
| |
For Full Tunnel VPN (routes all client traffic):
| |
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:
| |
Step 6: Enable and Start WireGuard Service
| |
Expected output:
| |
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@wg0already 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:
1sudo systemctl disable --now wg-quick@wg0Steps 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.
| |
--add-repois gone on AlmaLinux 10. AlmaLinux 10 ships dnf5, whereconfig-managergrew a properaddreposubcommand and the olddnf config-manager --add-repo <url>flag was removed — it now fails withUnknown 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 2sudo 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.
| |
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 earlier | v15 and later |
|---|---|
PASSWORD_HASH bcrypt hash, generated with docker run ... wgpw | Admin 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_KEEPALIVE | Configured in the Admin Panel, stored in a SQLite database |
Config in wg0.json | Config in a SQLite DB under /etc/wireguard |
network_mode: host typical | Bridge 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
| |
This is upstream’s recommended v15 compose file, with one addition (INSECURE,
explained below):
| |
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/24block 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
| |
Start wg-easy
| |
Expected output:
| |
Use
docker compose up/docker compose downto manage the container — upstream specifically warns againstdocker compose start/stopon v15, which can leave its state inconsistent.
Create Systemd Service for wg-easy
For automatic startup on boot:
| |
Add the following:
| |
Enable the service:
| |
Step 9: Access wg-easy Web Interface
Open your browser and navigate to:
| |
(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_HASHvariable - 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 replacesWG_HOST. - VPN subnet —
10.99.0.0/24to match the rest of this guide. This replacesWG_DEFAULT_ADDRESS. - Client DNS — e.g.
1.1.1.1. This replacesWG_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.jsonfirst, then upload it when the wizard offers to import an existing configuration. v14 environment variables are not migrated automatically.
Add Your First VPN Client
Click "+ New" button
Enter a client name (e.g., “john-laptop”)
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
For mobile devices: Scan the QR code with the WireGuard app
For desktop/laptop: Click “Download” to get the configuration file
Step 10: Configure VPN Clients
Mobile Clients (iOS/Android)
iOS:
- Install WireGuard from App Store
- Tap “+” → “Create from QR code”
- Scan the QR code from wg-easy
- Toggle connection on
Android:
- Install WireGuard from Play Store
- Tap “+” → “Scan from QR code”
- Scan the QR code from wg-easy
- Toggle connection on
Desktop/Laptop Clients
Linux:
| |
macOS:
- Install WireGuard from App Store
- Click “Import Tunnel(s) from File”
- Select downloaded .conf file
- Toggle connection on
Windows:
- Download WireGuard for Windows
- Click “Import Tunnel(s) from File”
- Select downloaded .conf file
- Click “Activate”
Verify VPN Connection
Once connected, verify your setup:
| |
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
| |
2. Implement Fail2ban for SSH Protection
| |
3. Regular Security Updates
| |
Add:
| |
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.
| |
Add line:
| |
4. Monitor VPN Activity
| |
5. Backup WireGuard Configuration
| |
6. Limit wg-easy Web UI Access
Only allow access from specific IPs:
| |
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
- Delete old client from web UI
- Re-add client (new config will use updated tunnel mode)
- Re-deploy configuration to client
Option 2: Manually edit client configuration
Edit the AllowedIPs line in client config:
| |
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:
| |
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:
| |
If the port is the problem, confirm what already holds it:
| |
Issue 3: Can’t Connect to wg-easy Web UI
Symptoms: Browser shows “connection refused” on port 51821.
Solution:
| |
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:
| |
Or fix it directly in an existing client config:
| |
Issue 5: Firewall Blocks VPN Traffic
Symptoms: VPN connects but traffic is blocked.
Solution:
| |
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
| |
Monitor Performance
| |
Maintenance Tasks
Daily/Weekly Checks
| |
Monthly Tasks
| |
Backup Configuration
Create automated backups:
| |
Add:
| |
| |
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
- Full Tunnel: Best for privacy and security - routes all traffic through VPN
- Split Tunnel: Best for performance - routes only specific traffic through VPN
- Firewalld Integration: Essential for AlmaLinux 10 with nftables kernel
- wg-easy Simplifies Management: Web UI makes peer management effortless
- 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
Related Reading
- Automate WireGuard VPN Setup on AlmaLinux 10 with SaltStack — once you’ve done this by hand, codify it
- How to Self-Host NetBird VPN on AlmaLinux 10 With Caddy — a mesh-VPN alternative with SSO and proper TLS
- How to Set Up a WireGuard VPN Server on Ubuntu 22.04 — the same tunnel on a Debian-family host
Resources
- WireGuard Official Documentation
- wg-easy Documentation
- wg-easy GitHub Repository
- AlmaLinux Wiki
- Firewalld Documentation
- Docker with nftables
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.