Este blog roda numa VM dentro do meu homelab. Ninguém na internet consegue abrir uma conexão com a minha casa, e a VM, por sua vez, não enxerga nada da minha rede além do roteador. Esta série mostra como montar isso do zero, para qualquer serviço HTTP: um blog, um Nextcloud, um painel, uma API.
A peça mais conhecida é o Cloudflare Tunnel, que publica o serviço sem abrir portas no roteador e funciona até atrás de CGNAT. Mas ele só cuida do caminho de entrada. Se o serviço for invadido, o atacante continua na mesma rede que o seu NAS, as câmeras e o seu notebook. Por isso a série começa pelo isolamento, antes de expor qualquer coisa.
A série tem três partes:
- Isolamento (este post): a VM exposta não alcança o resto da casa.
- Túnel: publicar o serviço com Cloudflare Tunnel.
- Pipeline: deploy automático com GitHub Actions e um runner dentro da VM isolada.
Você não precisa de equipamento especial para acompanhar. Uma VM Linux no hypervisor que você já usa basta, e se o seu roteador suporta VLAN, melhor ainda.
O diagrama abaixo mostra o sistema inteiro, do git push até o visitante. O trecho destacado é o deste post.

O que significa isolar uma VM exposta#
Antes de escolher a ferramenta, vale definir o comportamento esperado. A VM que vai para a internet deve:
- não abrir conexão com nenhum outro dispositivo da rede (NAS, roteador, outras VMs);
- não aceitar conexão de ninguém, a não ser da máquina de onde você a administra;
- conseguir sair para a internet, porque o túnel da Parte 2 é uma conexão que a própria VM abre para fora;
- usar o DNS do roteador.
flowchart LR
internet((Internet))
subgraph casa[Sua rede]
direction LR
nas[NAS, notebook,
roteador, outras VMs]
admin[Sua máquina
de administração]
vm[VM exposta]
end
vm -- "saída: liberada" --> internet
vm -. "bloqueado pelo firewall" .-> nas
admin -- "SSH: liberado" --> vm
internet -. "nenhuma porta aberta" .-> vm
Firewall no roteador ou ufw na VM?#
Há dois lugares para colocar essa barreira, e a diferença entre eles aparece justamente no pior cenário.
| Firewall no roteador (com VLAN) | ufw na própria VM | |
|---|---|---|
| Onde a regra fica | no roteador, fora da VM | dentro da VM |
| Precisa de | roteador com VLAN e firewall (UniFi, OPNsense, pfSense, OpenWrt) e um switch gerenciável | nada além da VM |
| Se a VM for invadida | a regra continua valendo | quem virar root pode desligar o ufw |
| Containers Docker | também ficam isolados | escapam das regras de saída (veja abaixo) |
Se o seu roteador permite, fique com a primeira opção. Se não permite, o ufw ainda é muito melhor do que deixar a VM solta na rede.
Opção 1: VLAN com firewall no roteador (o que eu uso)#
Aqui a VM sai da rede de casa e ganha uma rede só dela. Quem decide o que entra e o que sai é o roteador.
No meu caso, o roteador é um gateway UniFi (Network 10.6) com firewall por zonas, mas o mesmo desenho funciona em OPNsense, pfSense ou OpenWrt. Muda só a tela onde você clica.
Criei uma rede na VLAN 101 e coloquei essa rede na zona DMZ, que já vem pronta no UniFi. Não precisei escrever nenhuma regra de bloqueio, porque as políticas padrão da zona já fazem o isolamento inteiro. Esta é a matriz que o meu gateway aplica hoje, conferida direto na API dele:
| De → Para | Resultado |
|---|---|
| DMZ → rede interna, lab, monitoração, VPN, visitantes | bloqueado |
| DMZ → DMZ (entre redes da zona) | bloqueado |
| DMZ → gateway | bloqueado, exceto DNS, DHCP e ICMP |
| DMZ → internet | liberado |
| internet → DMZ | bloqueado (só passa o tráfego de retorno) |
| rede interna → DMZ | liberado (é por onde eu administro) |
Em outro roteador, o equivalente é um conjunto de regras na interface da VLAN, nesta ordem: liberar DNS para o gateway, bloquear todas as redes privadas e liberar o resto.
Do lado do hypervisor, a VM precisa estar na VLAN. No Proxmox, isso pede uma bridge que entenda VLANs, ligada a uma porta do switch que transporte a VLAN 101 com tag. A minha /etc/network/interfaces está assim:
auto vmbr0
iface vmbr0 inet static
bridge-ports bond0
bridge-stp off
bridge-fd 0
bridge-vlan-aware yes
bridge-vids 2-4094Na VM, basta definir a tag. Como eu crio as VMs a partir de um template com cloud-init, o IP vem junto:
qm set 101 --net0 virtio=BC:24:11:AA:BB:CC,bridge=vmbr0,tag=101
qm set 101 --ipconfig0 ip=10.10.101.10/24,gw=10.10.101.1
qm set 101 --nameserver 10.10.101.1Em outros hypervisors, procure o campo de VLAN ID na placa de rede da VM.
No meu lab, o isolamento para por aí. A VM não tem ufw ativo, e tudo vem da rede.
Opção 2: ufw na própria VM (quando o roteador não ajuda)#
Se a sua rede é a do roteador da operadora, sem VLAN, quem precisa se proteger é a própria VM. Nos exemplos abaixo, a rede de casa é 192.168.1.0/24, o roteador é 192.168.1.1, a sua máquina de administração é 192.168.1.50 e a VM exposta é 192.168.1.200. Troque pelos seus endereços.
Na VM (Ubuntu ou Debian):
sudo ufw default deny incoming
sudo ufw default allow outgoing
# entrada: só SSH, e só da sua máquina
sudo ufw allow from 192.168.1.50 to any port 22 proto tcp
# saída: DNS pelo roteador primeiro, depois bloqueia as redes privadas
sudo ufw allow out to 192.168.1.1 port 53
sudo ufw deny out to 10.0.0.0/8
sudo ufw deny out to 192.168.0.0/16
sudo ufw enable
sudo ufw status numberedO que cada parte faz:
- Nada entra, exceto o SSH vindo da sua máquina. Essa regra vem antes do
enablepara você não derrubar a própria sessão. - O ufw avalia as regras em ordem. O DNS para o roteador passa primeiro, e depois disso qualquer destino em
10.0.0.0/8ou192.168.0.0/16é bloqueado, incluindo a interface do roteador, o NAS e as outras VMs. - O resto, ou seja, a internet, fica liberado. Respostas de conexões já estabelecidas também passam, então o SSH funciona normalmente.
- Se a VM pega IP por DHCP, adicione
sudo ufw allow out to 192.168.1.1 port 67 proto udpantes dosdeny.
Talvez você tenha estranhado a falta de 172.16.0.0/12 na lista. Essa é a faixa que o Docker usa para as redes dos containers, e bloqueá-la faz o host perder o acesso aos próprios containers. Se a sua rede de casa estiver nessa faixa, libere antes as redes do Docker (docker network inspect mostra quais são) e só depois bloqueie.
Docker ignora o ufw (e outros limites)#
O ufw tem quatro limites que vale conhecer antes de confiar nele.
- Quem virar root na VM desliga o ufw com um comando. A barreira segura erros de configuração e um invasor sem privilégios, mas não um invasor com root.
- As regras de saída não valem para containers Docker. O tráfego deles passa pela chain
FORWARD, que o Docker controla antes do ufw, então um container comprometido alcança a sua rede mesmo com o ufw ativo. Para filtrar containers, use a chainDOCKER-USER(documentação do Docker sobre firewall). - Portas publicadas pelo Docker também passam por cima do ufw na entrada. Publique os serviços só em
127.0.0.1(-p 127.0.0.1:8080:8080), já que o túnel da Parte 2 só precisa alcançarlocalhost. - A VM continua na mesma rede (camada 2) que o resto da casa e vê o tráfego de broadcast dela.
Como testar o isolamento#
Qualquer que seja a opção, teste de dentro da VM antes de confiar na configuração. Os comandos abaixo usam os endereços da opção 2; na opção 1, troque o roteador pelo gateway da VLAN.
# deve funcionar: DNS pelo roteador e saída para a internet
getent hosts github.com
curl -sI https://github.com | head -1
# deve FALHAR (timeout): outro dispositivo da rede, como o NAS
nc -vz -w 3 192.168.1.10 445
# deve FALHAR: a interface web do roteador
curl -m 5 -sI http://192.168.1.1 | head -1Se o nc ou o último curl responderem, alguma regra está liberando esse tráfego ou a ordem das regras está errada. No ufw, sudo ufw status numbered mostra a ordem.
Quem usa Docker deve repetir o último teste de dentro de um container: docker run --rm curlimages/curl -m 5 -sI http://192.168.1.1. Na opção 2, é provável que ele responda, pelo motivo explicado acima.
O que este isolamento não resolve#
Nenhuma das duas opções resolve tudo. Três pontos continuam em aberto.
- A saída para a internet está toda liberada, então um serviço comprometido pode baixar ferramentas ou mandar dados para fora. Dá para apertar isso. O túnel da Parte 2 precisa de saída para a Cloudflare nas portas 7844 (TCP e UDP) e 443, e a VM precisa de 80 e 443 para atualizações e de 123/UDP para NTP. Liberar só isso fecha bastante o cerco; o preço é lembrar dessa lista toda vez que instalar algo novo.
- O hypervisor é compartilhado. Isolamento de rede não protege contra uma falha no próprio hypervisor, e agentes como o
qemu-guest-agentabrem um canal de administração que não passa pela rede. Quem controla o host controla a VM, com ou sem firewall. - A monitoração também fica do lado de fora. Se você usa Zabbix, Prometheus ou algo parecido, o servidor de monitoração também é bloqueado. Libere só a porta do agente, de forma explícita.
Próximo passo: o túnel#
Com isso, temos uma VM que não fala com nada da sua rede e que ninguém na internet alcança. Na Parte 2, o serviço dela vai para o ar com um Cloudflare Tunnel: a VM abre a conexão para fora, e os visitantes chegam por ela, sem nenhuma porta aberta no roteador.
Se você isolou o seu serviço de outro jeito, com OPNsense, OpenWrt ou outra ferramenta, conta nos comentários.
Referências:



