OpenVPN no Docker: o contrato de execução de que um cliente mínimo precisa
Uma imagem revisada, um perfil protegido, acesso a TUN e NET_ADMIN. O que o contêiner recebe, o que não deve receber e como provar que o túnel funciona.
Coloque um cliente VPN em um contêiner e você obtém um espaço de nomes com um túnel dentro dele, não um host que encaminha todo o seu tráfego por uma VPN. São projetos separados, e tratá-los como um só é justamente como os contêineres acabam privilegiados por acidente. Este guia cobre apenas o primeiro: uma imagem revisada, um perfil protegido e um processo de cliente confinado ao seu próprio espaço de nomes de rede.
Comece por um artefato que alguém já aprovou. As etapas abaixo pressupõem uma imagem que contém o executável openvpn e um perfil de cliente completo com certificado e material de chave já resolvidos dentro da montagem. Elas não compilam software, não emitem credenciais e não cobrem ambientes sem root nem contêineres nativos do Windows, cada um dos quais muda a resposta.
O que o contêiner recebe e o que não recebe
Mantenha o perfil e tudo a que ele se refere em um diretório local com permissões restritas, montado como somente leitura. Caminhos relativos no perfil precisam ser resolvidos dentro dessa montagem; esse é um detalhe que as pessoas perdem quando um certificado fica um diretório acima do perfil e o contêiner nunca o vê.
services:
vpn:
image: ${VPN_CLIENT_IMAGE:?set the reviewed image reference}
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
volumes:
- ./vpn:/vpn:ro
working_dir: /vpn
entrypoint: ["openvpn"]
command: ["--config", "/vpn/client.ovpn"]
Esse arquivo concede três coisas e não pede mais nada. A referência da imagem deve ser fixada por digest se a sua política de implantação disser isso, e o perfil deve ser o revisado, não uma cópia que alguém editou no host.
Somente leitura aqui significa que o contêiner não pode modificar seus arquivos no host; não significa que não possa ler a chave privada que você montou. Esse é todo o sentido da montagem, e é por isso que permissões de arquivo, propriedade e como o segredo chegou ao host ainda importam. Nunca passe uma senha na linha de comando nem a embuta em uma camada de imagem, porque ambas sobrevivem em lugares que você não poderá limpar depois.
Capacidades, entrypoint e a armadilha entre as duas
São necessários dois ajustes e eles fazem trabalhos não relacionados. NET_ADMIN permite a manipulação de interfaces e rotas dentro do espaço de nomes do contêiner. A entrada de dispositivo /dev/net/tun permite ao processo abrir o dispositivo de túnel. Conceder um sem o outro produz uma falha clara, o que é conveniente: um dispositivo TUN ausente falha na abertura, enquanto uma capacidade ausente falha quando o cliente tenta configurar a interface.
Nenhuma das duas justifica privileged: true. Um contêiner privilegiado recebe todas as capacidades mais acesso a dispositivos, e isso é uma concessão muito maior do que esta carga de trabalho precisa. A referência de privilégios e capacidades do ambiente de execução explica até onde essa concessão chega. Se um chart ou um post de blog pedir isso, verifique primeiro se o requisito era na verdade um caminho de dispositivo ausente ou uma capacidade ausente.
Substituir o entrypoint é a parte que morde. Invocar o openvpn diretamente é correto para uma imagem comum e errado para qualquer imagem de produto cujo script de inicialização prepara regras de firewall, arquivos de resolver ou credenciais antes de executar o daemon. Substituir esse script pula a preparação e deixa você depurando um túnel que subiu em um espaço de nomes semiconfigurado.
Execute o cliente em primeiro plano e deixe que ele seja o processo principal do contêiner. Um processo que se torna daemon sai imediatamente, o contêiner reinicia em loop, e os logs de que você precisa ficam espalhados por reinícios que parecem todos iguais.
Verifique de dentro do espaço de nomes, não do host
Suba a pilha, confirme no log que o cliente alcançou um estado conectado e então inspecione o espaço de nomes que realmente mantém o túnel.
docker compose up -d
docker compose logs --tail 40 vpn
docker compose exec vpn ip -br addr
docker compose exec vpn ip route
A lista de endereços deve mostrar uma interface de túnel com um endereço do pool, e a tabela de rotas deve mostrar os destinos enviados apontando para ela. Se a interface faltar, o cliente nunca chegou ao ponto de criá-la. Se existir mas nenhuma rota a usar, o lugar de olhar são as opções de push ou de rota.
Depois teste o destino que lhe importa, a partir do espaço de nomes que deveria usar o túnel, com a aplicação e não apenas com um ping. A resolução de nomes é uma falha à parte que vale testar explicitamente: um endereço DNS enviado não atualiza por si só o resolver de um contêiner, e um cliente autônomo que ignora o push resolverá nomes pelo que a imagem tiver configurado.
O estado desconectado faz parte do contrato
O comportamento de reconexão precisa da sua própria verificação, porque uma política de reinício prova que o processo reinicia e não diz nada sobre o que o tráfego faz nesse meio-tempo. Pare o túnel, tente o destino pretendido e observe se a conexão é recusada, fica pendurada ou obtém sucesso silenciosamente pela rota comum do contêiner. O último resultado é o que importa, e ele é invisível em um log saudável.
Se a carga de trabalho nunca deve usar o caminho comum, projete isso explicitamente com regras de firewall e um sinal de prontidão dentro do espaço de nomes. Um kill switch é uma política que você escreve e testa, não uma propriedade herdada por executar um cliente em um contêiner.
Por fim, lembre-se da fronteira: essas rotas vivem no espaço de nomes do contêiner e não substituem a tabela de roteamento do host. As regras de encaminhamento, tradução e firewall do Docker continuam sendo parte do caminho externo, e uma porta publicada não é uma interface VPN.