Prześledź jedno żądanie od telefonu w pociągu do backendu, który istnieje tylko w sieci prywatnej. Telefon rozwiązuje publiczną nazwę, łączy się z publicznym adresem na porcie 443 i wykonuje normalny uścisk dłoni TLS. Nginx przyjmuje to połączenie, a następnie otwiera drugie, całkowicie osobne, do adresu, do którego dociera tylko przez interfejs VPN. W telefonie nic się nie zmienia i telefon nigdy się nie dowiaduje, że brał w tym udział tunel.

Śledzenie jednego żądania przez dwa połączenia

Odcinek wejściowy to publiczna transakcja HTTP. Ma publiczną nazwę, publiczny certyfikat i ścieżkę odpowiedzi, która musi działać dla dowolnego adresu, jaki akurat ma odwiedzający. Każda zmiana, która zakłóca tę ścieżkę, psuje wszystkich odwiedzających naraz. Dlatego odcinek publiczny to ten, na którym najmniej chcesz eksperymentować.

Odcinek wyjściowy jest prywatny. Nginx staje się klientem, rozwiązuje wewnętrzną nazwę lub używa stałego adresu, i dociera do usługi, która odrzuciłaby połączenie z internetu.

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

Trzymaj je osobno w głowie, a konfiguracja staje się oczywista. Połącz je, a skończysz, próbując zmusić publicznego klienta do udziału w tunelu, i stąd bierze się większość zamieszania w tym projekcie. Odcinek prywatny może być kruchy. Publiczny nie.

Zanim cokolwiek z tego zbudujesz, potwierdź, że publikacja aplikacji jest zamierzona. Działający proxy to także działające wystawienie, a model uwierzytelniania musi być decyzją, a nie efektem ubocznym.

Nazwa bramy musi przetrwać tunel

W rozwiązywaniu nazw kryje się subtelna pułapka. Jeśli prywatny DNS nadpisuje tę samą nazwę, której profil VPN używa dla własnej bramy, ponowne połączenie może w końcu próbować dotrzeć do tej bramy przez tunel, który ma dopiero ustanowić. Klient potrzebuje tej nazwy rozwiązaną, zanim tunel istnieje. Gdy zostanie rozwiązana wewnątrz tunelu, połączenia nigdy nie da się zbudować.

Używaj odrębnych zatwierdzonych nazw dla bramy i backendu oraz ogranicz politykę resolvera tak, aby każda nazwa rozwiązywała się na adres potrzebny jej użytkownikowi. Zmiana nagłówka Host aplikacji nic tu nie daje, bo to klient VPN wykonuje zapytanie.

Potem pomyśl o zakresie. Frontend potrzebuje trasy do adresu backendu, a także musi nadal odpowiadać publicznym klientom. Pełny tunel na tym hoście może spełnić pierwszy wymóg i zepsuć drugi.

Tam, gdzie polityka na to pozwala, tunel do konkretnego celu jest czystszym rozwiązaniem. Tam, gdzie polityka wymaga pełnego tunelu, frontend należy do osobnego kontekstu sieciowego, zamiast być łatany doraźnymi trasami dla znanych odwiedzających.

Napisz blok serwera tak, aby oba odcinki weryfikowały

Załóż, że backend przedstawia certyfikat dla backend.corp.example i odpowiada na prywatnym adresie. Łączenie się z adresem przy weryfikacji nazwy to dokładnie to, do czego służą opcje SNI i nazwy w 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;
    }
}

Każda ścieżka i adres powyżej to placeholder. Ważne są te części, które utrzymują weryfikację włączoną: nazwa do sprawdzenia, pakiet zaufania, względem którego ją sprawdzić, oraz głębokość zgodna z zatwierdzonym łańcuchem.

proxy_set_header Host app.example.com jest celowe. Backend powinien być skonfigurowany tak, aby akceptował to publiczne pochodzenie, i powinien ufać przekazywanym nagłówkom tylko z tego frontendu. Nadpisywanie X-Forwarded-For adresem klienta jest właściwe dla jednego proxy i błędne dla łańcucha, więc wdrożenie z wieloma proxy potrzebuje własnego rozwiązania dla zaufanych adresów.

Sprawić, by publiczne pochodzenie zachowywało się jak publiczne

Aplikacja powinna emitować bezwzględne adresy URL, przekierowania i pliki cookie dla publicznego pochodzenia. Jeśli przekierowuje odwiedzającego do backend.corp.example, ten odwiedzający trafia do nazwy, której nie potrafi rozwiązać.

Napraw to przez obsługiwane ustawienie zewnętrznego URL aplikacji i przejrzaną politykę przekierowań, a nie przez przepisywanie nazw hostów w treści odpowiedzi. Zamiana ciągów w dowolnej treści psuje podpisane ładunki i tworzy błędy trudne do wytropienia.

Pliki cookie zasługują na taką samą troskę. Plik cookie powiązany z prywatną nazwą nigdy nie zostanie wysłany przez przeglądarkę, która zna tylko publiczną.

Potwierdź też, że Nginx naprawdę może być właścicielem publicznego gniazda. Istniejąca usługa WWW na porcie 443 musi się przenieść albo frontend musi mieszkać gdzie indziej, bo dwa procesy nie mogą związać tego samego adresu i portu.

Sprawdzać każdy odcinek tam, gdzie naprawdę działa

Zweryfikuj konfigurację zainstalowanym parserem przed każdym przeładowaniem, żeby literówka nie powaliła listenera, który właśnie obsługuje ruch.

Następnie przetestuj odcinek backendu z samego hosta frontendu, w tym samym kontekście sieciowym, którego używa Nginx. Ping przez tunel dowodzi osiągalności i niczego o TLS, autoryzacji ani aplikacji.

Na koniec wykonaj świeże publiczne żądanie do prawdziwej nazwy hosta z normalną weryfikacją certyfikatu i obserwuj ścieżkę odpowiedzi, a nie tylko treść.

Zachowaj obie obserwacje wraz z wdrożoną konfiguracją i zapisz, co się dzieje, gdy tunel jest niedostępny. Frontend powinien zwrócić kontrolowany błąd upstreamu, a nie po cichu przełączyć się na inny backend, którego nikt nie udokumentował. 502 z jasną linią logu to dobry wynik. Zbuforowana strona z niezamierzonego pochodzenia nie jest.

Powiązane artykuły