wg-easy
wg-easy
is the easiest way to install & manage WireGuard on any Linux hostthe easiest way to install & manage WireGuard on any Linux host
Features:
- All-in-one: WireGuard + Web UI.
- Easy installation, simple to use.
- List, create, edit, delete, enable & disable clients.
- Show a client's QR code.
- Download a client's configuration file.
- Statistics for which clients are connected.
- Tx/Rx charts for each connected client.
- Gravatar support.
- Automatic Light / Dark Mode
- Multilanguage Support
- One Time Links
- Client Expiration
- Prometheus metrics support
- IPv6 support
- CIDR support
Installation⚑
With docker⚑
If you want to use the prometheus metrics you need to use a version greater than 14, as 15
is not yet released (as of 2025-03-20) I'm using nightly
.
Tweak the next docker compose to your liking:
---
services:
wg-easy:
environment:
- WG_HOST=<the-url-or-public-ip-of-your-server>
- WG_PORT=<select the wireguard port>
# Until the 15 tag exists (after the release of 15, then you can change it to 15)
# https://github.com/wg-easy/wg-easy/pkgs/container/wg-easy/versions
image: ghcr.io/wg-easy/wg-easy:nightly
container_name: wg-easy
networks:
wg:
ipv4_address: 10.42.42.42
wg-easy:
volumes:
- wireguard:/etc/wireguard
- /lib/modules:/lib/modules:ro
ports:
- "<select the wireguard port>:<select the wireguard port/udp"
restart: unless-stopped
healthcheck:
test: /usr/bin/timeout 5s /bin/sh -c "/usr/bin/wg show | /bin/grep -q interface || exit 1"
interval: 1m
timeout: 5s
retries: 3
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=1
networks:
wg:
driver: bridge
enable_ipv6: false
ipam:
driver: default
config:
- subnet: 10.42.42.0/24
wg-easy:
external: true
volumes:
wireguard:
driver: local
driver_opts:
type: none
o: bind
device: /data/apps/wg-easy/wireguard
Where:
- I usually save the compose file at
/data/apps/wg-easy
- I've disabled ipv6, go to the official docker compose if you want to enable it
- I'm not exposing the admin web interface directly, if you want to, use the port 51821. Instead I'm going to use authentik to protect the service. That's why I'm not using the
PASSWORD_HASH
. To even protect it further, only the authentik and prometheus dockers will have network access to thewg-easy
one. So in theory no unauthorised access should occur. - The
wg-easy
is the external network I'm creating to connect this docker to authentik and prometheus](docker.md#limit-the-access-of-a-docker-on-a-server-to-the-access-on-the-docker-of-another-server) - You'll need to add the
wg-easy
network to theauthentik
docker-compose.
The systemd service to start wg-easy
is:
[Unit]
Description=wg-easy
Requires=docker.service
After=docker.service
[Service]
Restart=always
User=root
Group=docker
WorkingDirectory=/data/apps/wg-easy
# Shutdown container (if running) when unit is started
TimeoutStartSec=100
RestartSec=2s
# Start container when unit is started
ExecStart=/usr/bin/docker compose -f docker-compose.yaml up
# Stop container when unit is stopped
ExecStop=/usr/bin/docker compose -f docker-compose.yaml down
[Install]
WantedBy=multi-user.target
To forward the traffic from nginx to authentik use this site config:
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name vpn.*;
include /config/nginx/ssl.conf;
client_max_body_size 0;
location / {
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app authentik;
set $upstream_port 9000;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
}
}
To configure authentik to forward the traffic to wg-easy
use this terraform code:
# ---------------
# -- Variables --
# ---------------
variable "wg_easy_url" {
type = string
description = "The url to access the service."
}
variable "wg_easy_internal_url" {
type = string
description = "The url authentik proxies the traffic to reach wg_easy."
default = "http://wg-easy:51821"
}
variable "wg_easy_icon" {
type = string
description = "The icon shown in the application"
default = "/application-icons/wireguard.png"
}
# --------------------
# -- Provider --
# --------------------
resource "authentik_provider_proxy" "wg_easy" {
name = "wg_easy"
internal_host = var.wg_easy_internal_url
external_host = var.wg_easy_url
authorization_flow = data.authentik_flow.default-authorization-flow.id
invalidation_flow = data.authentik_flow.default-provider-invalidation-flow.id
internal_host_ssl_validation = false
access_token_validity = "minutes=120"
}
# -----------------------
# -- Application --
# -----------------------
resource "authentik_application" "wg_easy" {
name = "Wireguard"
slug = "wireguard"
meta_icon = var.wg_easy_icon
protocol_provider = authentik_provider_proxy.wg_easy.id
lifecycle {
ignore_changes = [
# The terraform provider is continuously changing the attribute even though it's set
meta_icon,
]
}
}
resource "authentik_policy_binding" "wg_easy_admin" {
target = authentik_application.wg_easy.uuid
group = authentik_group.admins.id
order = 0
}
resource "authentik_outpost" "default" {
name = "authentik Embedded Outpost"
service_connection = authentik_service_connection_docker.local.id
protocol_providers = [
authentik_provider_proxy.wg_easy.id,
]
}
resource "authentik_service_connection_docker" "local" {
name = "Local Docker connection"
local = true
}
data "authentik_flow" "default_invalidation_flow" {
slug = "default-invalidation-flow"
}
data "authentik_flow" "default-authorization-flow" {
slug = "default-provider-authorization-implicit-consent"
}
data "authentik_flow" "default-authentication-flow" {
slug = "default-authentication-flow"
}
data "authentik_flow" "default-provider-invalidation-flow" {
slug = "default-provider-invalidation-flow"
}
With ansible⚑
Configuration⚑
Split tunneling⚑
If you only want to route certain ips through the vpn you can use the AllowedIPs wireguard configuration. You can set them in the WG_ALLOWED_IPS
docker compose environment variable
WG_ALLOWED_IPS=1.1.1.1,172.27.1.0/16
It's important to keep the DNS inside the allowed ips.
Keep in mind though that the WG_ALLOWED_IPS
only sets the routes on the client, it does not limit the traffic at server level. For example, if you set 172.30.1.0/24
as the allowed ips, but the client changes it to 172.30.0.0/16
it will be able to access for example 172.30.2.1
. The suggested way to prevent this behavior is to add the kill switch in the Pre and Post hooks (WG_POST_UP
and WG_POST_DOWN
)
Restrict Access to Networks with iptables⚑
If you need to restrict many networks you can use this allowed ips calculator
Kill switch⚑
Monitorization⚑
If you want to use the prometheus metrics you need to use a version greater than 14, as 15
is not yet released (as of 2025-03-20) I'm using nightly
.
You can enable them with the environment variable ENABLE_PROMETHEUS_METRICS=true
Scrape the metrics⚑
Add to your scrape config the required information
- job_name: vpn-admin
metrics_path: /metrics
static_configs:
- targets:
- {your vpn private ip}:{your vpn exporter port}
Create the monitor client⚑
To make sure that the vpn is working we'll add a client that is always connected. To do so we'll use linuxserver's wireguard docker