기차 안의 휴대폰에서 사설 네트워크 안에만 존재하는 백엔드까지 요청 하나를 따라가 봅시다. 휴대폰은 공개 이름을 해석하고, 443 포트의 공개 주소로 연결하며, 평범한 TLS 핸드셰이크를 마칩니다. Nginx는 그 연결을 받아들인 뒤 VPN 인터페이스를 통해서만 도달할 수 있는 주소로 완전히 별개의 두 번째 연결을 엽니다. 휴대폰에서는 아무것도 바뀌지 않고, 터널이 개입했다는 사실을 휴대폰은 결코 알지 못합니다.

요청 하나를 두 연결로 따라가기

들어오는 구간은 공개 HTTP 트랜잭션입니다. 공개 이름, 공개 인증서, 그리고 방문자가 그 시점에 가진 어떤 주소에도 동작해야 하는 응답 경로가 있습니다. 그 경로를 흔드는 어떤 변경이든 방문자 전원을 한꺼번에 망가뜨립니다. 그래서 공개 구간은 실험을 가장 덜 하고 싶은 구간입니다.

나가는 구간은 사설입니다. Nginx가 클라이언트가 되어 내부 이름을 해석하거나 고정 주소를 사용하고, 인터넷에서 오는 연결을 거부할 서비스에 도달합니다.

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

머릿속에서 둘을 분리해 두면 설정이 저절로 분명해집니다. 둘을 합치면 결국 공개 클라이언트를 터널에 참여시키려 하게 되고, 이 설계의 혼란 대부분이 거기서 나옵니다. 사설 구간은 취약해도 됩니다. 공개 구간은 안 됩니다.

무엇이든 만들기 전에 애플리케이션 게시가 의도된 일인지 확인하세요. 동작하는 프록시는 동작하는 노출이기도 하며, 인증 모델은 부작용이 아니라 결정이어야 합니다.

게이트웨이 이름은 터널을 견뎌야 합니다

이름 해석에는 미묘한 함정이 있습니다. 사설 DNS가 VPN 프로필이 자체 게이트웨이에 사용하는 같은 이름을 덮어쓰면, 재연결이 정작 자신이 세워야 할 터널을 통해 그 게이트웨이에 닿으려 할 수 있습니다. 클라이언트는 터널이 존재하기 전에 그 이름이 해석되어 있어야 합니다. 이름이 터널 안에서만 해석되면 연결은 애초에 만들어질 수 없습니다.

게이트웨이와 백엔드에 서로 다른 승인된 이름을 사용하고, 각 이름이 그 소비자에게 필요한 주소로 해석되도록 리졸버 정책을 좁히세요. 애플리케이션의 Host 헤더를 바꾸는 것은 조회를 하는 쪽이 VPN 클라이언트이기 때문에 여기에 아무 도움이 되지 않습니다.

다음으로 범위를 생각하세요. 프런트엔드는 백엔드 주소로 가는 경로가 필요하고, 동시에 공개 클라이언트에 계속 응답해야 합니다. 그 호스트에서 전체 터널을 쓰면 첫 번째 요구는 만족하지만 두 번째가 깨집니다.

정책이 허용하는 곳에서는 특정 목적지만 가는 터널이 더 깔끔한 설계입니다. 정책이 전체 터널을 강제하는 곳에서는 프런트엔드를 알려진 방문자를 위한 임시 경로로 덧대기보다 별도의 네트워크 컨텍스트에 두어야 합니다.

두 구간이 모두 검증하도록 서버 블록을 작성하기

백엔드가 이 이름에 대한 인증서를 제시한다고 가정합니다: backend.corp.example 그리고 사설 주소에서 응답합니다. 이름을 검증하면서 주소로 연결하는 것이 바로 프록시 모듈의 SNI와 이름 옵션이 존재하는 이유입니다.

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;
    }
}

위의 모든 경로와 주소는 자리 표시자입니다. 중요한 것은 검증을 켜 둔 채로 유지하는 부분들, 즉 검사할 이름, 그것을 검사할 신뢰 번들, 그리고 승인된 체인에 맞는 깊이입니다.

proxy_set_header Host app.example.com 는 의도된 것입니다. 백엔드는 그 공개 오리진을 받아들이도록 설정되어야 하고, 전달된 헤더는 이 프런트엔드에서 온 것만 신뢰해야 합니다. 다음을 덮어쓰는 것 X-Forwarded-For 을 클라이언트 주소로 하는 것은 단일 프록시에는 맞고 체인에는 틀리므로, 다중 프록시 배포에는 자체적인 신뢰 주소 체계가 필요합니다.

공개 오리진이 공개 오리진처럼 동작하게 만들기

애플리케이션은 공개 오리진에 대한 절대 URL, 리디렉션, 쿠키를 내보내야 합니다. 방문자를 다음으로 리디렉션하면: backend.corp.example 그 방문자는 해석할 수 없는 이름으로 보내집니다.

이것은 애플리케이션이 지원하는 외부 URL 설정과 검토된 리디렉션 정책으로 고쳐야 하며, 응답 본문의 호스트 이름을 다시 쓰는 방식이 아닙니다. 임의 콘텐츠에 대한 문자열 치환은 서명된 페이로드를 깨고 추적하기 어려운 버그를 만듭니다.

쿠키도 같은 주의가 필요합니다. 사설 이름에 묶인 쿠키는 공개 이름만 아는 브라우저가 결코 보내지 않습니다.

Nginx가 실제로 공개 소켓을 소유할 수 있는지도 확인하세요. 두 프로세스는 같은 주소와 포트를 바인딩할 수 없으므로, 443에서 돌고 있는 기존 웹 서비스를 옮기거나 프런트엔드를 다른 곳에 두어야 합니다.

각 구간을 실제로 도는 곳에서 확인하기

재로드 전에 설치된 파서로 설정을 검증하여, 오타 하나로 지금 서비스 중인 리스너를 떨어뜨리지 않도록 하세요.

그다음 프런트엔드 호스트 자체에서, Nginx가 쓰는 것과 같은 네트워크 컨텍스트로 백엔드 구간을 시험하세요. 터널을 통한 ping은 도달 가능성만 증명하고 TLS, 권한 부여, 애플리케이션에 대해서는 아무것도 증명하지 않습니다.

마지막으로 실제 호스트 이름으로 정상적인 인증서 검증을 포함한 새 공개 요청을 보내고, 응답 본문만이 아니라 응답 경로를 관찰하세요.

두 관찰을 배포된 설정과 함께 남기고, 터널을 쓸 수 없을 때 무슨 일이 벌어지는지 기록하세요. 프런트엔드는 통제된 upstream 오류를 반환해야 하며, 아무도 문서화하지 않은 다른 백엔드로 조용히 넘어가서는 안 됩니다. 명확한 로그 줄이 있는 502는 좋은 결과입니다. 의도하지 않은 오리진에서 캐시된 페이지 는 좋은 결과가 아닙니다.

관련 글