Suivez une requête depuis un téléphone dans un train jusqu’à un backend qui n’existe qu’à l’intérieur d’un réseau privé. Le téléphone résout un nom public, se connecte à une adresse publique sur le port 443 et effectue une poignée de main TLS normale. Nginx accepte cette connexion puis en ouvre une seconde, entièrement séparée, vers une adresse qu’il n’atteint que par une interface VPN. Rien ne change sur le téléphone, et le téléphone n’apprend jamais qu’un tunnel est intervenu.

Suivre une requête à travers deux connexions

Le segment entrant est une transaction HTTP publique. Il a un nom public, un certificat public et un chemin de retour qui doit fonctionner pour l’adresse que le visiteur possède au moment donné. Toute modification qui perturbe ce chemin casse tous les visiteurs d’un coup. C’est pourquoi le segment public est celui sur lequel vous voulez le moins expérimenter.

Le segment sortant est privé. Nginx devient client, résout un nom interne ou utilise une adresse fixe, et atteint un service qui refuserait une connexion venant d’Internet.

Public client  -> frontend public address:443   (TLS, public name)
Frontend       -> VPN private backend 10.40.0.20:8443  (TLS, private name)

Gardez-les séparés dans votre tête et la configuration devient évidente. Mélangez-les, et vous finissez par vouloir faire participer un client public à un tunnel, d’où vient l’essentiel de la confusion dans cette conception. Le segment privé a le droit d’être fragile. Le segment public non.

Avant de construire quoi que ce soit, confirmez que publier l’application est voulu. Un proxy qui fonctionne est aussi une exposition qui fonctionne, et le modèle d’authentification doit être une décision et non un effet de bord.

Le nom de la passerelle doit survivre au tunnel

Un piège discret se cache dans la résolution de noms. Si le DNS privé écrase le même nom que le profil VPN utilise pour sa propre passerelle, une reconnexion peut finir par tenter de joindre cette passerelle à travers le tunnel qu’elle est censée établir. Le client a besoin que ce nom soit résolu avant que le tunnel existe. Une fois qu’il se résout à l’intérieur du tunnel, la connexion ne peut jamais être construite.

Utilisez des noms approuvés distincts pour la passerelle et le backend, et cadrez la politique du résolveur pour que chaque nom se résolve vers l’adresse dont son consommateur a besoin. Changer l’en-tête Host d’une application ne règle rien ici, car c’est le client VPN qui fait la résolution.

Pensez ensuite à la portée. Le frontend a besoin d’une route vers l’adresse du backend, et il doit aussi continuer à répondre aux clients publics. Un tunnel complet sur cet hôte satisfait la première exigence et casse la seconde.

Là où la politique le permet, un tunnel vers une destination précise est la conception la plus propre. Là où la politique impose un tunnel complet, le frontend appartient à un contexte réseau séparé plutôt que d’être rapiécé avec des routes ad hoc pour des visiteurs connus.

Écrire le bloc serveur pour que les deux segments vérifient

Supposons que le backend présente un certificat pour backend.corp.example et réponde sur une adresse privée. Se connecter à une adresse tout en vérifiant un nom, c’est exactement l’usage des options SNI et de nom du module 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;
    }
}

Chaque chemin et adresse ci-dessus est un espace réservé. Les éléments importants sont ceux qui maintiennent la vérification active : un nom à contrôler, un paquet de confiance pour le contrôler et une profondeur qui correspond à la chaîne approuvée.

proxy_set_header Host app.example.com est volontaire. Le backend doit être configuré pour accepter cette origine publique, et il ne doit faire confiance aux en-têtes transférés que depuis ce frontend. Écraser X-Forwarded-For avec l’adresse du client est correct pour un proxy unique et faux pour une chaîne, donc un déploiement multi-proxy a besoin de sa propre organisation d’adresses de confiance.

Faire se comporter l’origine publique comme telle

L’application doit émettre des URL absolues, des redirections et des cookies pour l’origine publique. Si elle redirige un visiteur vers backend.corp.example, ce visiteur est envoyé vers un nom qu’il ne peut pas résoudre.

Corrigez cela via le réglage d’URL externe pris en charge par l’application et une politique de redirection revue, pas en réécrivant les noms d’hôte dans les corps de réponse. Le remplacement de chaînes dans un contenu arbitraire casse les charges signées et crée des bogues difficiles à tracer.

Les cookies méritent le même soin. Un cookie limité au nom privé ne sera jamais envoyé par un navigateur qui ne connaît que le nom public.

Confirmez aussi que Nginx peut réellement posséder le socket public. Un service web existant sur 443 doit bouger, sinon le frontend doit vivre ailleurs, car deux processus ne peuvent pas se lier à la même adresse et au même port.

Vérifier chaque segment là où il s’exécute réellement

Validez la configuration avec l’analyseur installé avant tout rechargement, afin qu’une faute de frappe ne mette pas à terre l’écouteur qui sert en ce moment.

Testez ensuite le segment backend depuis l’hôte frontend lui-même, dans le même contexte réseau que celui utilisé par Nginx. Un ping à travers le tunnel prouve l’accessibilité et rien sur TLS, l’autorisation ou l’application.

Enfin, faites une nouvelle requête publique vers le vrai nom d’hôte avec une vérification de certificat normale, et observez le chemin de retour plutôt que seulement le corps de la réponse.

Conservez les deux observations avec la configuration déployée, et notez ce qui se passe quand le tunnel est indisponible. Le frontend doit renvoyer une erreur d’upstream maîtrisée, et non basculer discrètement vers un autre backend que personne n’a documenté. Un 502 avec une ligne de journal claire est un bon résultat. Une page en cache provenant d’une origine non voulue ne l’est pas.

Articles similaires