Expondo serviços do homelab na internet com Cloudflare Tunnel

Neste post
No post anterior, mostrei como o SSH-J.com resolve um problema específico: acessar via SSH uma máquina que está atrás de NAT, sem abrir portas no roteador e sem depender de IP público. O túnel reverso funciona bem para sessões interativas e transferência de arquivos, e o SSH-J.com como jump host torna tudo trivial de configurar. Para SSH, continua sendo a solução mais simples que conheço.
Mas SSH é só uma peça do quebra-cabeça. Quem mantém um homelab — mesmo que seja só um mini PC embaixo da mesa ou um Raspberry Pi no canto da sala — inevitavelmente acaba rodando serviços web: um leitor de RSS, um dashboard de monitoramento, um Gitea, um Jellyfin, um Immich. Esses serviços escutam em portas HTTP locais e funcionam perfeitamente enquanto você está na mesma rede. O problema aparece quando você quer acessá-los de fora — do escritório, do celular no ônibus, de qualquer lugar que não seja a sua rede local.
As opções tradicionais são as mesmas de sempre: port forwarding no roteador (que esbarra no CGNAT e expõe portas para a internet), DDNS para lidar com IP dinâmico (que resolve apenas metade do problema), ou uma VPN como WireGuard ou Tailscale (que funciona mas exige um cliente instalado em cada dispositivo). O Cloudflare Tunnel oferece uma alternativa que não requer nenhuma dessas coisas: o serviço local faz uma conexão de saída para a rede da Cloudflare, que por sua vez publica o serviço em um subdomínio do seu domínio, com HTTPS configurado automaticamente. Nenhuma porta aberta, nenhum IP público, nenhum certificado para gerenciar. De fora, o acesso é um URL normal no browser.
Neste post, mostro como configurei o meu próprio setup: um leitor de RSS rodando em um container LXC no Proxmox, publicado em rss.sarmento.org através de um Cloudflare Tunnel gerenciado localmente via CLI e mantido por systemd.
Como o Cloudflare Tunnel funciona#
O princípio é o mesmo do túnel reverso do SSH-J.com, só que aplicado a tráfego HTTP em vez de sessões SSH. Um daemon chamado cloudflared roda na sua máquina local e abre conexões de saída para a rede de edge da Cloudflare. Como a conexão parte de dentro da sua rede, o NAT não é problema — o roteador trata como qualquer outra conexão de saída. A Cloudflare recebe as requisições HTTP destinadas ao seu subdomínio e as encaminha pelo túnel até o cloudflared, que por sua vez as repassa para o serviço local.
A diferença em relação ao SSH-J.com é que aqui não existe relay de terceiro operando com infraestrutura mínima — é a rede da Cloudflare, com mais de 300 pontos de presença no mundo, servindo de proxy reverso para o seu serviço. O HTTPS é provido pela Cloudflare usando certificados gerados automaticamente para o seu domínio. O tráfego entre o browser do visitante e a Cloudflare é criptografado normalmente com TLS. O tráfego entre a Cloudflare e o cloudflared na sua máquina viaja pela conexão de túnel, que por sua vez usa QUIC ou HTTP/2 com TLS. E entre o cloudflared e o serviço local, como ambos estão na mesma máquina, a comunicação acontece sobre localhost sem criptografia — o que é perfeitamente aceitável quando tudo roda no mesmo host.
Existem duas formas de gerenciar um Cloudflare Tunnel: pelo dashboard web (Zero Trust → Tunnels) ou pela CLI. O dashboard é mais visual e permite configurar tudo pelo browser, mas o resultado é um túnel “remotamente gerenciado” — a configuração fica na Cloudflare e o cloudflared local só precisa de um token para conectar. A CLI cria um túnel “localmente gerenciado” onde a configuração fica em um arquivo YAML na máquina, o que dá mais controle e visibilidade sobre o que está rodando. Neste post uso a CLI porque é o caminho que faz mais sentido para quem já está confortável com terminal e quer entender o que cada peça faz.
Pré-requisitos#
Antes de começar, você precisa de três coisas.
Um domínio próprio com o DNS gerenciado pela Cloudflare. Se o seu domínio já está na Cloudflare (por exemplo, porque você usa Cloudflare Pages para hospedar um site estático), esse requisito já está atendido. Se não está, o processo é adicionar o domínio na Cloudflare e apontar os nameservers do seu registrador para os que ela te fornece. A documentação oficial cobre isso em detalhes.
Uma conta gratuita na Cloudflare. O Cloudflare Tunnel faz parte do plano gratuito — não é necessário plano pago para uso pessoal.
Uma máquina Linux rodando o serviço que você quer expor. No meu caso é um container LXC com Debian 13 no Proxmox, mas pode ser qualquer distribuição com systemd. O serviço precisa estar escutando em uma porta local — no meu exemplo, um web app em http://127.0.0.1:8080.
Instalando o cloudflared#
O cloudflared é o daemon que estabelece e mantém o túnel. No Debian, a instalação pode ser feita pelo repositório oficial da Cloudflare ou baixando o .deb diretamente. Optei por baixar o pacote:
wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared-linux-amd64.deb
Confirme que a instalação funcionou:
cloudflared --version
A saída deve mostrar a versão instalada. Se você estiver em ARM (Raspberry Pi, por exemplo), troque amd64 por arm64 no URL do download.
Autenticando na Cloudflare#
O próximo passo é vincular o cloudflared à sua conta Cloudflare. Execute:
cloudflared tunnel login
O comando vai gerar um URL e pedir que você o abra no browser. Na página que abrir, faça login na sua conta Cloudflare e selecione o domínio que será usado para o túnel. Após a autorização, o cloudflared salva um certificado em ~/.cloudflared/cert.pem que será usado para criar e gerenciar túneis.
Se a máquina onde você está instalando o cloudflared não tem browser (que é o caso comum em servidores), copie o URL exibido no terminal e abra-o em qualquer outro computador onde você esteja logado na Cloudflare. O fluxo de autorização acontece no browser, não na máquina local.
Criando o túnel#
Com a autenticação feita, crie o túnel:
cloudflared tunnel create homelab
O nome homelab é um identificador que você escolhe — pode ser qualquer coisa descritiva. O comando cria o túnel na sua conta Cloudflare e gera um arquivo de credenciais em ~/.cloudflared/<UUID>.json, onde <UUID> é o identificador único do túnel. Anote esse UUID — você vai precisar dele na configuração.
Para confirmar que o túnel foi criado:
cloudflared tunnel list
A saída mostra o nome, o UUID e o status de cada túnel associado à sua conta.
Configurando o roteamento#
O túnel existe, mas ainda não sabe para onde encaminhar o tráfego. Essa configuração é feita em um arquivo YAML. Crie ~/.cloudflared/config.yml:
tunnel: <UUID>
credentials-file: /root/.cloudflared/<UUID>.json
ingress:
- hostname: rss.sarmento.org
service: http://127.0.0.1:8080
- service: http_status:404
Troque <UUID> pelo UUID real do seu túnel e ajuste o hostname e a porta para o seu caso. O bloco ingress define as regras de roteamento: requisições para rss.sarmento.org são encaminhadas para o serviço local na porta 8080, e qualquer outra requisição recebe um 404. A última regra com service: http_status:404 é obrigatória — o cloudflared exige uma regra catch-all no final do ingress.
Se você quiser expor mais de um serviço pelo mesmo túnel, basta adicionar entradas ao ingress antes da regra catch-all:
ingress:
- hostname: rss.sarmento.org
service: http://127.0.0.1:8080
- hostname: git.sarmento.org
service: http://127.0.0.1:3000
- service: http_status:404
Cada hostname precisa de um registro DNS na Cloudflare, que é o próximo passo.
Criando o registro DNS#
O cloudflared tem um comando que cria automaticamente o registro CNAME apontando o subdomínio para o túnel:
cloudflared tunnel route dns homelab rss.sarmento.org
Esse comando cria um registro CNAME no DNS da Cloudflare apontando rss.sarmento.org para <UUID>.cfargotunnel.com. A partir desse momento, qualquer requisição para rss.sarmento.org vai bater na rede da Cloudflare, que vai procurar um túnel ativo com aquele UUID para encaminhá-la.
Você pode verificar o registro no dashboard da Cloudflare, em DNS → Records do seu domínio. O registro CNAME deve aparecer lá com o status de proxy ativo (nuvem laranja).
Testando manualmente#
Antes de criar o serviço systemd, teste o túnel manualmente para confirmar que tudo funciona:
cloudflared tunnel run homelab
O cloudflared deve conectar à rede da Cloudflare e começar a mostrar logs no terminal. Abra https://rss.sarmento.org no browser — o serviço local deve aparecer, servido com HTTPS, sem nenhuma configuração adicional de certificado. O terminal vai mostrar as requisições sendo encaminhadas. Quando estiver satisfeito que tudo funciona, interrompa com Ctrl+C.
Rodando como serviço com systemd#
O cloudflared tem um comando integrado que cria e configura o serviço systemd automaticamente:
sudo cloudflared service install
Esse comando faz três coisas: copia a configuração de ~/.cloudflared/config.yml para /etc/cloudflared/config.yml, copia o arquivo de credenciais para /etc/cloudflared/, e cria a unit file em /etc/systemd/system/cloudflared.service com o conteúdo adequado. A unit file gerada se parece com isto:
[Unit]
Description=cloudflared
After=network.target
[Service]
TimeoutStartSec=0
Type=notify
ExecStart=/usr/bin/cloudflared --no-autoupdate --config /etc/cloudflared/config.yml tunnel run
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
O Type=notify indica que o cloudflared avisa o systemd quando está pronto para receber tráfego — uma integração mais sofisticada do que o Restart=always que usamos no post do SSH-J.com. O --no-autoupdate desabilita o mecanismo de atualização automática do cloudflared, que é o comportamento correto quando você gerencia pacotes pelo sistema operacional.
Ative e inicie o serviço:
sudo systemctl enable --now cloudflared
Confirme que está rodando:
sudo systemctl status cloudflared
A saída deve mostrar active (running) e os logs de conexão. A partir de agora o túnel inicia automaticamente no boot e reconecta em caso de falha.
Diferenças em relação ao SSH-J.com#
Vale colocar as duas soluções lado a lado para entender quando usar cada uma.
O SSH-J.com é ideal para acesso SSH a máquinas atrás de NAT. Não precisa de conta, não precisa de domínio, e a configuração inteira é um comando SSH. A contrapartida é que o tráfego passa por um servidor de terceiros (embora criptografado ponta a ponta) e o serviço se limita a encaminhar conexões TCP — não serve para expor aplicações web com HTTPS.
O Cloudflare Tunnel serve para expor serviços HTTP e HTTPS na internet com um hostname próprio. O HTTPS vem de graça, o DNS é gerenciado pela Cloudflare, e o tráfego passa pela rede de edge deles. Em compensação, exige um domínio com DNS na Cloudflare, instalação do cloudflared na máquina, e uma configuração um pouco mais envolvida. É possível usar o Cloudflare Tunnel para SSH também (a Cloudflare tem documentação específica para isso), mas a complexidade é consideravelmente maior do que simplesmente usar o SSH-J.com.
Na prática, as duas soluções coexistem sem conflito. Uso o SSH-J.com para acessar máquinas via SSH quando estou fora de casa, e o Cloudflare Tunnel para publicar web apps do homelab com domínio e HTTPS. Cada ferramenta resolve o problema para o qual foi desenhada.
Quando algo dá errado#
O túnel conecta mas o site não carrega#
O problema mais comum é um descompasso entre o hostname configurado no ingress e o registro DNS criado na Cloudflare. Se o CNAME aponta para um UUID diferente do que está no config.yml, o tráfego não chega ao túnel certo. Verifique com:
cloudflared tunnel info homelab
Compare o UUID exibido com o que está no config.yml e no registro CNAME do dashboard.
Outro cenário frequente é o serviço local não estar rodando ou estar escutando em um endereço ou porta diferente do configurado. Se o ingress aponta para http://127.0.0.1:8080 mas o serviço está na porta 3000, o cloudflared vai receber um connection refused e retornar um erro 502 para o browser. Teste o serviço localmente antes de culpar o túnel:
curl http://127.0.0.1:8080
Se o curl funciona mas o browser não, o problema está entre o cloudflared e a Cloudflare, não entre o cloudflared e o serviço local.
O serviço systemd fica reiniciando#
Se o systemctl status cloudflared mostra ciclos de start → failed → auto-restart, o problema quase certamente está na configuração. Olhe os logs:
journalctl -u cloudflared -n 50 --no-pager
Os erros mais comuns são: arquivo de credenciais não encontrado (o cloudflared service install não copiou corretamente, ou o path no config.yml está errado), YAML inválido no config.yml (indentação errada, campo faltando), ou falta da regra catch-all no final do ingress.
Certificado SSL inválido no browser#
Se o browser mostra erro de certificado ao acessar o subdomínio, verifique no dashboard da Cloudflare se o modo SSL/TLS está configurado como “Full” ou “Flexible” — não “Off”. A Cloudflare gera o certificado automaticamente para o seu domínio, mas o modo SSL precisa estar ativo para que ele seja servido. Isso normalmente já é o padrão para domínios com proxy ativo.
Sysadmin, 53 anos, brasileiro trabalhando de casa para o mundo todo. Cuida de servidores Linux, containers LXC, e de gatos que não saem de cima do teclado.