Nginx à frente, backend privado atrás da VPN
O navegador fala com o Nginx, e o Nginx abre uma segunda conexão para o túnel. Encaminhe ambas, verifique os dois certificados e mantenha públicos os redirecionamentos públicos.
Siga uma requisição de um telefone em um trem até um backend que só existe dentro de uma rede privada. O telefone resolve um nome público, conecta-se a um endereço público na porta 443 e conclui um handshake TLS normal. O Nginx aceita essa conexão e depois abre uma segunda, totalmente separada, para um endereço que só alcança por uma interface VPN. Nada no telefone muda, e o telefone nunca fica sabendo que houve um túnel.
Acompanhar uma requisição por duas conexões
O trecho de entrada é uma transação HTTP pública. Ele tem um nome público, um certificado público e um caminho de resposta que precisa funcionar para qualquer endereço que o visitante tenha naquele momento. Qualquer mudança que perturbe esse caminho quebra todos os visitantes de uma vez. É por isso que o trecho público é o que você menos deve querer experimentar.
O trecho de saída é privado. O Nginx vira cliente, resolve um nome interno ou usa um endereço fixo, e alcança um serviço que recusaria uma conexão da internet.
Public client -> frontend public address:443 (TLS, public name)
Frontend -> VPN private backend 10.40.0.20:8443 (TLS, private name)
Mantenha os dois separados na cabeça e a configuração fica óbvia. Junte-os e você acaba tentando fazer um cliente público participar de um túnel, que é de onde vem a maior parte da confusão neste desenho. O trecho privado pode ser frágil. O público não.
Antes de construir qualquer parte disso, confirme que publicar a aplicação é intencional. Um proxy que funciona também é uma exposição que funciona, e o modelo de autenticação precisa ser uma decisão, não um efeito colateral.
O nome do gateway precisa sobreviver ao túnel
Há uma armadilha sutil na resolução de nomes. Se o DNS privado sobrescrever o mesmo nome que o perfil de VPN usa para o próprio gateway, uma reconexão pode acabar tentando alcançar esse gateway através do túnel que ela deveria estabelecer. O cliente precisa que esse nome resolva antes de o túnel existir. Assim que ele resolve dentro do túnel, a conexão nunca pode ser construída.
Use nomes aprovados distintos para o gateway e o backend, e limite a política do resolver para que cada nome resolva para o endereço de que seu consumidor precisa. Alterar o cabeçalho Host de uma aplicação não resolve isso, porque quem faz a consulta é o cliente de VPN.
Depois pense no escopo. O frontend precisa de uma rota até o endereço do backend e também precisa continuar atendendo clientes públicos. Um túnel completo nesse host satisfaz o primeiro requisito e quebra o segundo.
Onde a política permite, um túnel para um destino específico é o desenho mais limpo. Onde a política exige um túnel completo, o frontend pertence a um contexto de rede separado em vez de ser remendado com rotas ad hoc para visitantes conhecidos.
Escrever o bloco de servidor para que os dois trechos verifiquem
Suponha que o backend apresente um certificado para backend.corp.example e responda em um endereço privado. Conectar-se a um endereço enquanto se verifica um nome é exatamente para isso que servem as opções de SNI e de nome do módulo de proxy.
upstream private_app {
server 10.40.0.20:8443;
}
server {
listen 443 ssl;
server_name app.example.com;
ssl_certificate /etc/nginx/tls/app-fullchain.pem;
ssl_certificate_key /etc/nginx/tls/app-key.pem;
location / {
proxy_pass https://private_app;
proxy_ssl_server_name on;
proxy_ssl_name backend.corp.example;
proxy_ssl_trusted_certificate /etc/nginx/trust/backend-ca.pem;
proxy_ssl_verify on;
proxy_ssl_verify_depth 3;
proxy_set_header Host app.example.com;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $remote_addr;
}
}
Cada caminho e endereço acima é um espaço reservado. As partes importantes são as que mantêm a verificação ligada: um nome para conferir, um pacote de confiança com que conferi-lo e uma profundidade que corresponda à cadeia aprovada.
proxy_set_header Host app.example.com é proposital. O backend deve ser configurado para aceitar essa origem pública, e deve confiar em cabeçalhos encaminhados apenas deste frontend. Sobrescrever X-Forwarded-For com o endereço do cliente está certo para um único proxy e errado para uma cadeia, então uma implantação com vários proxies precisa de seu próprio arranjo de endereços confiáveis.
Fazer a origem pública se comportar como tal
A aplicação deve emitir URLs absolutas, redirecionamentos e cookies para a origem pública. Se ela redirecionar um visitante para backend.corp.example, esse visitante é enviado para um nome que não consegue resolver.
Corrija isso pela configuração de URL externa suportada pela aplicação e por uma política de redirecionamento revisada, não reescrevendo nomes de host nos corpos das respostas. Substituição de strings em conteúdo arbitrário quebra cargas assinadas e cria bugs difíceis de rastrear.
Cookies merecem o mesmo cuidado. Um cookie limitado ao nome privado nunca será enviado por um navegador que só conhece o público.
Confirme também que o Nginx pode realmente ser dono do socket público. Um serviço web existente na 443 precisa mudar de lugar ou o frontend precisa viver em outro ponto, porque dois processos não podem se ligar ao mesmo endereço e à mesma porta.
Verificar cada trecho onde ele realmente roda
Valide a configuração com o analisador instalado antes de qualquer reload, para que um erro de digitação não derrube o listener que está servindo naquele momento.
Depois teste o trecho do backend a partir do próprio host do frontend, no mesmo contexto de rede que o Nginx usa. Um ping pelo túnel prova alcançabilidade e nada sobre TLS, autorização ou a aplicação.
Por fim faça uma requisição pública nova ao nome de host real com verificação normal de certificado, e observe o caminho de resposta em vez de apenas o corpo.
Guarde as duas observações junto com a configuração implantada e registre o que acontece quando o túnel está indisponível. O frontend deve devolver um erro de upstream controlado, não cair silenciosamente para outro backend que ninguém documentou. Um 502 com uma linha de log clara é um bom resultado. Uma página em cache de uma origem não pretendida não é.