내부 도메인으로는 정상 접속되는데 외부 도메인으로는 404 가 나온다. NPM 로그에는 오류가 없다.
$ curl -I https://chat.example.com
HTTP/2 404
server: openresty
$ curl https://chat.example.com
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx</center>
응답 본문이 위와 같은 Nginx 기본 404 페이지이고 server 헤더가 openresty · nginx 라면, 요청은 프록시까지 왔지만 백엔드로 전달되지 않았다는 뜻이다. 애플리케이션이 낸 404 라면 그 애플리케이션의 화면이나 JSON 이 나온다.
Nginx 기본 404 는 곧 server_name 매칭 실패다. 요청의 Host 헤더에 해당하는 프록시 호스트가 없어 기본(default) 서버 블록으로 떨어진 것이다.
# 라우팅만 분리해서 시험한다. TLS · DNS 를 배제할 수 있다
curl -I -H "Host: chat.example.com" http://<NPM_IP>/
# NPM 이 실제로 어떤 server_name 을 들고 있는지
docker exec -it npm sh -c 'grep -r "server_name" /data/nginx/proxy_host/'
Host 헤더를 바꿔 가며 시험하면 어떤 이름이 등록돼 있고 어떤 이름이 없는지 바로 드러난다.
가장 흔하다. 내부 도메인만 프록시 호스트에 넣고 외부 도메인을 빠뜨린 경우다. 같은 백엔드로 보낼 것이라면 기존 프록시 호스트의 Domain Names 에 한 줄 더 추가하면 된다. 인증서를 따로 관리하고 싶으면 프록시 호스트를 별도로 만든다.
와일드카드(*.example.com)를 등록해 두었다면 그쪽이 먼저 잡을 수 있다. 등록 목록을 확인하고 더 구체적인 이름을 별도로 등록한다.
프록시 호스트는 등록됐는데도 404 가 계속되면 백엔드 애플리케이션이 Host 를 검사하는 경우다. Django ALLOWED_HOSTS, Nextcloud trusted_domains, Open WebUI 계열의 도메인 검사가 여기 해당한다.
# 백엔드에 직접 물어 확인한다
curl -I http://<backend_ip>:<port>/
curl -I -H "Host: chat.example.com" http://<backend_ip>:<port>/
두 번째만 404 라면 백엔드 설정을 고쳐야 한다. 애플리케이션을 고칠 수 없으면 프록시에서 Host 를 바꿔 보낸다. NPM 의 Advanced 탭에 넣는다.
proxy_set_header Host <백엔드가 기대하는 이름>;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
다만 Host 를 바꾸면 애플리케이션이 만들어 내는 절대 URL 이 그 이름으로 나올 수 있다. 가능하면 애플리케이션 쪽에 도메인을 등록하는 것이 정석이다.
외부 → NPM 은 HTTPS, NPM → 백엔드도 HTTPS 로 두면 인증서 검증과 SNI 문제가 겹친다. 내부 구간이라면 Scheme 을 http 로 두는 편이 단순하다. 굳이 암호화해야 한다면 백엔드 인증서의 이름과 Forward Hostname 을 일치시킨다.
ERR_CERT_COMMON_NAME_INVALID 가 나오면 그 도메인용 인증서가 없어 다른 인증서(다른 프록시 호스트의 것 또는 NPM 기본 인증서)가 응답한 것이다. 라우팅 문제와 뿌리가 같다. 프록시 호스트를 만들고 그 도메인으로 인증서를 발급하면 함께 해결된다.
openssl s_client -connect chat.example.com:443 -servername chat.example.com </dev/null 2>/dev/null \
| openssl x509 -noout -subject -ext subjectAltName
HSTS 가 걸린 도메인이라면 브라우저가 경고를 우회할 방법을 주지 않으므로 인증서를 먼저 고쳐야 한다. 이 동작은 인증서 체인이 끊길 때 의 HSTS 절에 정리했다.
프록시는 통과하는데 실시간 기능만 동작하지 않으면 WebSocket 업그레이드가 빠진 것이다. NPM 은 Websockets Support 스위치로 처리한다. 직접 넣는다면 다음과 같다.
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;