Tutorial prático cobrindo WireGuard “vanilla” instalado direto no host e a alternativa via container wg-easy, com comparação entre as duas abordagens e exemplos alinhados a um servidor de referência (VPS Debian 13, IP 203.0.113.10, docker network proxy 172.28.0.0/16).
1. Visão geral
WireGuard é uma VPN moderna que roda em userspace ou (preferencialmente) como módulo do kernel Linux a partir da versão 5.6. O design é deliberadamente minimalista: apenas ~4 mil linhas de código no kernel, criptografia fixa (Curve25519, ChaCha20, Poly1305, BLAKE2s, SipHash24, HKDF), sem negociação de algoritmos.
Vantagens contra OpenVPN e IPsec
- Configuração declarativa em arquivo
.confcurto (uma seção[Interface], uma[Peer]por par). - Performance: throughput próximo do link físico em hardware moderno, latência menor que OpenVPN (que roda em TLS sobre TCP/UDP em userspace).
- Roaming nativo: o cliente pode mudar de Wi-Fi para 4G sem reabrir o túnel; basta o handshake estar válido (a cada 2 minutos).
- Sem CA, sem cipher suite negotiation, sem PKI complexa. Cada peer é identificado por uma chave pública Curve25519.
- Stealth razoável: sem resposta a pacotes inválidos, o daemon não revela presença para scanners não autenticados.
Quando NÃO usar
- Cenários que precisam de autenticação por usuário/senha integrada com AD/LDAP. WireGuard só conhece chaves; autenticação humana fica por conta de camadas externas (ou de soluções como wg-easy, NetBird, Tailscale).
- Quando o requisito é TCP-only (proxies corporativos que bloqueiam UDP). WireGuard é UDP puro; tunelar via TCP exige workarounds (udp2raw, wstunnel).
- IPs dinâmicos sem endpoint discovery: WireGuard estático precisa de pelo menos um lado com endpoint conhecido. Em redes peer-to-peer dinâmicas, usar Tailscale/Headscale por cima.
2. Arquitetura de referência
flowchart LR
laptop["Laptop<br/>peer 10.8.0.2"]
phone["Celular<br/>peer 10.8.0.3"]
internet((Internet))
vps["VPS Debian 13<br/>203.0.113.10"]
wgeasy["Container wg-easy<br/>172.28.0.12<br/>UDP 51820 / TCP 51821"]
bridge["docker bridge proxy<br/>172.28.0.0/16"]
app1["app-a<br/>172.28.0.x:3000"]
app2["app-b<br/>172.28.0.x:8080"]
db["postgres<br/>172.28.0.x:5432"]
storage["object-storage<br/>172.28.0.x:9000"]
laptop -->|UDP 51820| internet
phone -->|UDP 51820| internet
internet -->|porta 51820/udp publicada| vps
vps --> wgeasy
wgeasy -->|MASQUERADE: src 10.8.0.x reescrito p/ 172.28.0.12| bridge
bridge --> app1
bridge --> app2
bridge --> db
bridge --> storagePonto chave: o tráfego que entra com origem 10.8.0.0/24 (subnet dos peers WireGuard) sai do container com SNAT/MASQUERADE aplicado pela própria iptables que o wg-easy configura via cap_add: NET_ADMIN. Para os outros containers da proxy, todo cliente VPN aparece como 172.28.0.12. Isso simplifica allowlists do nginx (allow 172.28.0.1; deny all;) mas impede granularidade por peer no nível dos serviços downstream.
3. Conceitos
| Termo | Significado |
|---|---|
| Peer | Cada ponta do túnel. Servidor é um peer; cada cliente é outro peer. WireGuard não tem hierarquia client/server, é simétrico — o que distingue é quem tem Endpoint configurado. |
| PrivateKey | Chave privada Curve25519 do peer local. Fica em /etc/wireguard/privatekey ou inline em [Interface]. Nunca sai da máquina. |
| PublicKey | Derivada da PrivateKey, é o que se compartilha. Identifica o peer no [Peer] da outra ponta. |
| AllowedIPs | Dupla função: (1) crypto-routing — define quais IPs de destino são roteados para esse peer pelo wg-quick; (2) ACL de entrada — pacotes recebidos desse peer com source IP fora dessa lista são descartados. |
| Endpoint | IP:porta UDP onde o peer remoto escuta. Opcional num lado (o que está atrás de NAT/IP dinâmico). |
| PersistentKeepalive | Intervalo em segundos para enviar pacote vazio e manter o mapping NAT vivo. 25 é o valor recomendado quando o peer está atrás de NAT. |
| Handshake | Troca Noise_IK a cada 2 minutos (ou na primeira mensagem). Sem handshake recente, o túnel está morto mesmo que wg show liste o peer. |
| MTU | Default 1420 (IPv4 em link Ethernet). Cai para 1380 quando o caminho passa por PPPoE (fibra residencial brasileira). Se vir Destination Unreachable: Fragmentation Needed, é MTU. |
Handshake — sequência simplificada
sequenceDiagram
participant C as Cliente (initiator)
participant S as Servidor (responder)
C->>S: Handshake Initiation (ephemeral pub, encrypted static pub, timestamp, MAC)
S->>C: Handshake Response (ephemeral pub, encrypted empty, MAC)
Note over C,S: Sessao simetrica derivada (ChaCha20-Poly1305)
C->>S: Data packet (n=0)
S->>C: Data packet (n=0)
Note over C,S: A cada 2 minutos ou apos REKEY_AFTER_MESSAGES, novo handshake4. Pré-requisitos
- Debian 13 (Trixie) ou Ubuntu 24.04+. Kernel
>= 5.6traz o módulowireguardnativo — nenhum DKMS necessário. - Verificar:
uname -r # kernel >= 5.6
modinfo wireguard # deve listar o modulo- Porta
UDP 51820aberta na firewall do provedor (no caso da VPS) e no roteador doméstico se for site-to-site reverso. - IPv4 público estático (ou IPv6 público — WireGuard suporta ambos sem distinção).
iptablesounftablesinstalado para MASQUERADE (vanilla). Container já traz tudo embarcado.
5. Opção A — WireGuard vanilla no host
5.1 Instalação
sudo apt update
sudo apt install -y wireguard wireguard-tools5.2 Habilitar IP forwarding
sudo tee /etc/sysctl.d/99-wireguard.conf <<'EOF'
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
EOF
sudo sysctl --systemConferir:
sysctl net.ipv4.ip_forward # deve retornar 15.3 Gerar chaves do servidor
sudo mkdir -p /etc/wireguard
sudo chmod 700 /etc/wireguard
cd /etc/wireguard
umask 077
wg genkey | sudo tee privatekey | wg pubkey | sudo tee publickeyResultado: dois arquivos com permissão 600. A privatekey será referenciada no wg0.conf.
5.4 Servidor — /etc/wireguard/wg0.conf
Identifique a interface externa (geralmente eth0 numa VPS):
ip route get 1.1.1.1 | awk '{print $5; exit}'[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = <CONTEUDO_DE_/etc/wireguard/privatekey>
SaveConfig = false
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -s 10.0.0.0/24 -o eth0 -j MASQUERADE
[Peer]
# laptop
PublicKey = <PUBLICKEY_DO_CLIENTE_LAPTOP>
AllowedIPs = 10.0.0.2/32
[Peer]
# celular
PublicKey = <PUBLICKEY_DO_CLIENTE_CELULAR>
AllowedIPs = 10.0.0.3/32SaveConfig = false é importante: com true, qualquer wg set em runtime sobrescreve o arquivo na hora do wg-quick down, atrapalhando versionamento.
5.5 Subir o túnel
sudo wg-quick up wg0
sudo wg showSaída esperada de wg show:
interface: wg0
public key: <server-pub>
private key: (hidden)
listening port: 51820
peer: <client-pub>
allowed ips: 10.0.0.2/325.6 Persistir via systemd
sudo systemctl enable --now wg-quick@wg0
sudo systemctl status wg-quick@wg0A unit wg-quick@.service já vem com o pacote wireguard-tools. O @wg0 casa com /etc/wireguard/wg0.conf.
5.7 Cliente — wg0.conf
Gerar chaves no cliente:
umask 077
wg genkey | tee client-priv | wg pubkey > client-pub[Interface]
Address = 10.0.0.2/24
PrivateKey = <CLIENT_PRIVATE_KEY>
DNS = 1.1.1.1
[Peer]
PublicKey = <SERVER_PUBLIC_KEY>
Endpoint = 203.0.113.10:51820
AllowedIPs = 10.0.0.0/24, 172.28.0.0/16
PersistentKeepalive = 25AllowedIPs define o split tunnel — ver seção 9. Adicionar o client-pub no [Peer] do wg0.conf do servidor e recarregar:
sudo wg syncconf wg0 <(wg-quick strip wg0)(syncconf não derruba o túnel; wg-quick down/up derrubaria.)
6. Opção B — wg-easy via docker compose
wg-easy empacota WireGuard + UI web em Node.js + iptables auto-configurado num único container. Útil quando o objetivo é gerenciar peers sem editar .conf na mão.
6.1 docker-compose.yml
Alinhado ao setup descrito (container fixo em 172.28.0.12, network proxy já existente):
services:
wg-easy:
image: ghcr.io/wg-easy/wg-easy:14
container_name: wg-easy
restart: unless-stopped
environment:
- WG_HOST=203.0.113.10
- PASSWORD_HASH=$$2a$$12$$abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123
- WG_PORT=51820
- WG_DEFAULT_ADDRESS=10.8.0.x
- WG_DEFAULT_DNS=1.1.1.1
- WG_ALLOWED_IPS=10.8.0.0/24,172.28.0.0/16
- WG_PERSISTENT_KEEPALIVE=25
- WG_MTU=1420
- UI_TRAFFIC_STATS=true
- LANG=pt
# Opcional: rotear DNS interno do ambiente
# - WG_POST_UP=iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
# - WG_POST_DOWN=iptables -t nat -D POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
volumes:
- ./data:/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
networks:
proxy:
ipv4_address: 172.28.0.12
networks:
proxy:
external: trueNotas importantes:
- PASSWORD_HASH é bcrypt. Cada
$precisa ser escapado como$$dentro do compose (ou o Compose tentará interpolar variáveis). - Gerar hash:
docker run --rm ghcr.io/wg-easy/wg-easy wgpw 'minha-senha'(versão >=14) ou usarhtpasswd -bnBC 12 "" 'senha' | tr -d ':\n'. WG_DEFAULT_ADDRESS=10.8.0.x— oxé literal; o wg-easy substitui pelo último octeto quando cria cada peer.WG_ALLOWED_IPScontrola o que vai no.confgerado para os clientes — não confundir com oAllowedIPsdo peer no servidor (que continua10.8.0.<n>/32).- A imagem
:14ainda usa esse layout legado. A:15mudou a UI e o esquema (auth interna,PASSWORD_HASHfoi renomeado). Se for migrar, ler o changelog antes — a 15 introduz database SQLite em/etc/wireguard/db.sqlite.
6.2 Subir
docker compose up -d
docker compose logs -f wg-easyAcessar a UI em http://<host>:51821 (ou via reverse proxy nginx atrás do allowlist 172.28.0.1).
6.3 Vanilla vs wg-easy — comparação
| Aspecto | Vanilla | wg-easy |
|---|---|---|
| Adicionar peer | Editar wg0.conf + wg syncconf | Botão “New Client” na UI |
Distribuir .conf | scp / pendrive | QR code direto da UI |
| Topologias complexas (site-to-site, múltiplas subnets) | Total controle, Table, rotas custom | Limitado — UI assume hub-and-spoke |
| Métricas | wg show, scripts custom | UI mostra rx/tx por peer |
| Backup | Copiar /etc/wireguard/ | Volume ./data/ (inclui DB) |
| Footprint | Pacote do sistema (~1 MB) | Container Node.js (~150 MB) |
| Multi-tenant / autenticação humana | Não tem | UI com senha (e 2FA na v15) |
Roteamento custom (Table = off, PreUp/PostUp arbitrário) | Sim | Limitado a WG_PRE_UP / WG_POST_UP env |
Resumo: wg-easy ganha em ergonomia para o caso 90% (hub-and-spoke, alguns peers humanos). Vanilla ganha quando precisa de topologia em malha, múltiplas interfaces wgN, ou integração com frr/BGP.
7. Criando peers via UI wg-easy
- Abrir
http://<host>:51821(ou domínio atrás de nginx). - Login com a senha definida em
PASSWORD_HASH. - Botão ”+ New Client” no canto superior direito.
- Informar nome do peer (ex.:
laptop-user). Opcional: definir IP fixo, expiração, AllowedIPs custom. - A UI mostra dois botões:
- QR code: escanear direto no app do celular.
- Download: baixa um
.confpronto parawg-quick.
- Para revogar: ícone de lixeira ao lado do peer. O servidor recarrega a config automaticamente.
Toggle de pausa (ícone de pause) suspende o peer sem deletar — útil para testes.
8. Importando no cliente
Linux
sudo cp ~/Downloads/laptop-user.conf /etc/wireguard/wg0.conf
sudo chmod 600 /etc/wireguard/wg0.conf
sudo wg-quick up wg0
sudo systemctl enable --now wg-quick@wg0 # persistirPara derrubar: sudo wg-quick down wg0.
Windows / macOS
App oficial WireGuard (Microsoft Store / App Store / wireguard.com/install).
- Importar túnel → escolher o
.conf. - Ativar/desativar com toggle.
- macOS pede permissão de System Extension na primeira vez.
Android / iOS
App WireGuard (Play Store / App Store, oficial do projeto).
- + → Create from QR code → apontar câmera para o QR da UI wg-easy.
- Toggle de ativação.
- iOS exige permissão “Adicionar configurações de VPN”.
9. Split tunnel vs full tunnel
O lado cliente controla isso via AllowedIPs:
# Split tunnel — só tráfego para a infraestrutura interna passa pela VPN
AllowedIPs = 10.8.0.0/24, 172.28.0.0/16
# Full tunnel — TODO tráfego do dispositivo sai pela VPS
AllowedIPs = 0.0.0.0/0, ::/0Quando usar full tunnel:
- Wi-Fi público (café, aeroporto). Toda navegação sai pelo IP da VPS.
- Bypass de geoblocking baseado em IP de origem.
- Esconder DNS da operadora.
Quando usar split:
- Acesso pontual a serviços internos sem sacrificar latência de Netflix/Spotify.
- Economia de banda da VPS (full tunnel faz dobrar o tráfego).
Observação: no wg-easy, WG_ALLOWED_IPS define o default do que vai no .conf gerado. Para diferenciar por peer, a UI da v14+ permite override no momento da criação.
10. DNS dentro do túnel
Sem DNS no [Interface] do cliente, a resolução continua usando o DNS do sistema — leak de DNS para a operadora local. Soluções:
# Cliente — usar DNS público
DNS = 1.1.1.1, 1.0.0.1Ou apontar para um resolver interno da infraestrutura (AdGuard, Pi-hole, Unbound):
DNS = 172.28.0.xNo wg-easy, WG_DEFAULT_DNS=1.1.1.1 (ou múltiplos separados por vírgula) injeta isso em todo peer novo. Trocar o DNS de peers já criados exige editar via UI ou regenerar.
Verificar leak: rodar https://dnsleaktest.com com o túnel ativo.
11. Firewall e iptables
Servidor vanilla — INPUT mínimo
# Aceitar handshake WireGuard
sudo iptables -A INPUT -p udp --dport 51820 -j ACCEPT
# Manter conexões já estabelecidas
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# loopback
sudo iptables -A INPUT -i lo -j ACCEPT
# SSH só por dentro do túnel (depois que validar acesso)
sudo iptables -A INPUT -p tcp --dport 22 -s 10.0.0.0/24 -j ACCEPT
# Default DROP
sudo iptables -P INPUT DROP
sudo iptables -P FORWARD ACCEPT # ou regras mais finasPersistir com iptables-persistent:
sudo apt install -y iptables-persistent
sudo netfilter-persistent saveMASQUERADE
Já está nas PostUp/PostDown da seção 5.4. Sem MASQUERADE, os pacotes chegam aos serviços com source 10.0.0.x, mas a resposta tenta voltar pela rota default — sem rota reversa, drop.
wg-easy
O container faz tudo isso automaticamente quando recebe cap_add: NET_ADMIN. Inspeção:
docker exec wg-easy iptables -t nat -L POSTROUTING -n -v
docker exec wg-easy iptables -L FORWARD -n -v12. Hardening
- SSH só dentro do túnel. Após validar acesso pela VPN:
# /etc/ssh/sshd_config
ListenAddress 10.0.0.1
PermitRootLogin no
PasswordAuthentication nosudo systemctl reload sshdFail2ban para a porta 51820 — pouco efetivo porque WireGuard não responde a pacotes inválidos. Útil mesmo é para SSH e nginx.
Rotação de chaves: gerar novas a cada 6-12 meses ou imediatamente se um cliente foi roubado. Procedimento:
- Revogar peer antigo (deletar
[Peer]do servidor ou clicar lixeira no wg-easy). - Gerar novo par no cliente.
- Adicionar novo peer com nova
PublicKey.
- Revogar peer antigo (deletar
PresharedKey opcional, mas recomendado para defesa quântica futura:
wg genpsk > preshared[Peer]
PublicKey = ...
PresharedKey = <PSK>
AllowedIPs = 10.0.0.2/32Adiciona uma camada de XOR pós-quântica ao handshake.
- Não expor 51821/tcp publicamente. A UI do wg-easy fica atrás do nginx com allowlist
172.28.0.1— só acessível depois que a VPN já está montada.
13. Múltiplos sites (site-to-site)
Cenário: VPS (servidor A, 10.0.0.1/24) interligada a um servidor doméstico (B, 10.0.1.1/24), com redes LAN distintas atrás.
Servidor A — /etc/wireguard/wg0.conf:
[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = <A_PRIV>
[Peer]
# Servidor B
PublicKey = <B_PUB>
AllowedIPs = 10.0.1.0/24, 192.168.1.0/24
Endpoint = b.dyndns.example.com:51820
PersistentKeepalive = 25Servidor B (atrás de NAT residencial):
[Interface]
Address = 10.0.1.1/24
ListenPort = 51820
PrivateKey = <B_PRIV>
[Peer]
# VPS
PublicKey = <A_PUB>
AllowedIPs = 10.0.0.0/24, 172.28.0.0/16
Endpoint = 203.0.113.10:51820
PersistentKeepalive = 25Ambos os lados precisam de ip_forward=1 e MASQUERADE/route apropriados para que as LANs internas (192.168.1.0/24 e 172.28.0.0/16) se enxerguem. No lado residencial, ainda é necessário aceitar o tráfego na firewall do roteador ou usar PersistentKeepalive para furar o NAT a partir de dentro.
14. Troubleshooting
Handshake nunca acontece
sudo wg show
# Procurar "latest handshake: ..." — se "0 seconds ago" nunca aparece, o handshake nunca completouChecklist:
- UDP 51820 realmente aberto?
sudo ss -ulnp | grep 51820no servidor. - Firewall do provedor (Oracle Cloud, AWS Security Group, Hetzner) bloqueando?
- Endpoint do cliente bate com IP/porta reais do servidor?
- PublicKey do servidor no cliente igual à de
wg show interfaceno servidor? - Hora dos dois lados sincronizada? WireGuard rejeita handshake com timestamp >2min de skew.
- NAT residencial assassinando o fluxo? Adicionar
PersistentKeepalive = 25no lado interno.
Capturar pacotes
sudo tcpdump -i any -n udp port 51820
sudo tcpdump -i wg0 -nNo primeiro deve aparecer fluxo UDP. No segundo, tráfego já decifrado.
MTU
Sintoma: ping funciona, mas downloads/SSH travam. Especialmente com fibra PPPoE.
[Interface]
MTU = 1380Ou medir com ping -M do -s 1372 <peer> e ajustar.
Rotas
ip route show table all | grep wg0wg-quick up adiciona uma rota por AllowedIPs. Se conflitar com rota existente, sai erro RTNETLINK answers: File exists — usar Table = off em [Interface] para gerenciar manualmente.
Logs
# Vanilla
sudo journalctl -u wg-quick@wg0 -f
sudo dmesg | grep -i wireguard
# wg-easy
docker compose logs -f wg-easyContainer wg-easy não consegue criar interface
Verificar cap_add: NET_ADMIN e que /lib/modules está montado (somente leitura é o suficiente). Em kernels que não carregam wireguard automaticamente:
sudo modprobe wireguardEm hosts onde o módulo não existe (containers LXC sem kernel próprio), wg-easy cai para implementação userspace wireguard-go — funciona mas com latência maior.
Conflito de subnet
WG_DEFAULT_ADDRESS=10.8.0.x colide com 10.8.0.0/24 da LAN do cliente? O cliente vai mandar tráfego pelo túnel quando deveria ir local. Reservar uma subnet dedicada para o WG (10.66.66.0/24 é comum).
15. Backup
Itens críticos:
- Chaves privadas (
/etc/wireguard/privatekey, todos os.confcomPrivateKeyinline). Sem elas, é necessário regenerar e redistribuir para cada peer. - Lista de PublicKeys dos peers (recuperável dos próprios clientes se eles tiverem backup, mas chato).
- DB do wg-easy: volume
./data/mapeado em/etc/wireguarddentro do container. Para v14 contémwg0.conf+ arquivos de peer. Para v15 contémdb.sqlite.
Script simples de backup diário:
#!/bin/bash
set -e
DEST=/var/backups/wireguard
mkdir -p "$DEST"
tar czf "$DEST/wg-$(date +%F).tar.gz" /etc/wireguard /opt/docker/wg-easy/data
find "$DEST" -name 'wg-*.tar.gz' -mtime +30 -deleteCifrar antes de mandar para storage externo (chave privada em backup não cifrado é receita para desastre):
gpg --symmetric --cipher-algo AES256 "$DEST/wg-$(date +%F).tar.gz"