본문 바로가기
빌드노트 검색

빌드노트

DNS 전환 전 HTTPS 확인은 curl --resolve로 한다

DNS 전환 전 새 IP를 HTTPS로 검사할 때는 URL의 도메인을 유지하고 curl --resolve로 연결 주소만 바꾼다. 전용 CA와 loopback TLS 서버에서 Host·SNI·인증서 이름 검증을 비교하고 Windows Schannel의 미완료 조건도 구분한다.

새 서버의 IP로 HTTPS 요청을 보내면서 Host 헤더에 서비스 도메인을 넣었는데 인증서 오류가 난다면, URL부터 확인한다. URL에는 서비스 도메인을 그대로 두고, --resolve로 연결할 IP만 지정한다. HTTP의 Host를 바꿔도 TLS에서 사용할 이름까지 바뀌지는 않는다.

HTTPS에서는 HTTP 요청을 보내기 전에 TLS 연결을 준비하고 서버 인증서를 검증한다. curl은 URL의 도메인에서 SNI(Server Name Indication)를 정한다. 서버는 이 이름으로 제공할 인증서를 고를 수 있고, curl도 인증서가 URL의 이름에 맞는지 검사한다. -H "Host: ..."는 그 뒤에 보낼 HTTP 헤더를 지정한다. everything curl의 이름 지정 설명 (새 탭에서 열림)

URL은 도메인 그대로, 연결 주소만 새 IP로

먼저 새 서버에 확인할 도메인의 인증서와 가상 호스트가 준비돼 있어야 한다. 다음은 Bash에서 실행한 명령이다. preview.test, 127.0.0.1, 43771은 아래 합성 실험의 도메인·IP·포트다. 자신의 서버에서는 각각 실제 서비스 도메인, 새 서버 IP, HTTPS 포트로 바꾸고, /도 확인하려는 읽기 전용 경로로 바꾼다.

여기서 ./lab-ca.pem은 실험용 인증서를 발급한 CA의 PEM 인증서 파일이다. 사설 CA를 쓰는 서버라면 관리자가 제공한 신뢰할 CA 파일을 지정한다. 기본 신뢰 설정으로 검증되는 공개 CA 인증서라면 --cacert 지정은 생략할 수 있다. CA 파일을 지정하는 것과 서버 이름 검증을 끄는 것은 다르다. curl의 인증서 검증 설명 (새 탭에서 열림)

이 명령으로 URL의 이름을 유지한 채 지정 IP에 직접 연결하고, 인증서 검증과 응답을 확인한다.

curl -q --noproxy "*" --http1.1 --connect-timeout 2 --max-time 5 --cacert ./lab-ca.pem --verbose --silent --show-error --resolve "preview.test:43771:127.0.0.1" "https://preview.test:43771/"

--resolve의 순서는 호스트:포트:IP다. URL의 호스트와 포트가 지정한 쌍과 일치해야 적용된다. 기본 HTTPS 포트를 쓰면 URL은 https://서비스도메인/, 대응 항목은 서비스도메인:443:새IP다. 이 주소 지정은 해당 curl 실행에 적용되며 시스템의 DNS나 hosts 파일을 고치지 않는다. curl --resolve 문서 (새 탭에서 열림)

-q는 첫 옵션에서 기본 curl 설정 파일을 제외한다. --noproxy "*"는 프록시 없이 새 IP에 직접 접속하기 위한 조건이다. 프록시가 필수인 환경을 이 명령으로 그대로 검사할 수 있다고 가정하지 않는다. 연결 제한 2초와 전송 전체 제한 5초는 이 작은 비교에서 사용한 값이다. 실제 서버의 정상 응답 시간에 맞춰 정한다. curl 옵션 설명 (새 탭에서 열림)

실제 상세 출력에서 다음 줄들을 확인했다. 중간 TLS 메시지는 생략했다.

* Connected to preview.test (127.0.0.1) port 43771
*  subjectAltName: host "preview.test" matched cert's "preview.test"
*  SSL certificate verify ok.
> Host: preview.test:43771
< HTTP/1.1 200 OK

서버가 돌려준 본문은 다음과 같았고 curl의 종료 코드는 0이었다. 이 본문의 필드들은 확인을 위해 실험 서버가 넣은 값이며 일반 웹 서버의 표준 응답 형식은 아니다.

site=preview.test
sni=preview.test
host=preview.test:43771

서버의 TLS 콜백에서도 SNI가 preview.test였고, 해당 이름의 인증서를 선택한 사실을 따로 기록했다. 연결 IP는 127.0.0.1이지만 URL의 이름, SNI, 인증서의 DNS 이름과 HTTP Host는 preview.test로 맞춰진 것이다. 포트가 기본값이 아니어서 HTTP Host에는 :43771도 붙었다. Host 헤더를 수동으로 추가할 필요가 없었다.

IP URL에 Host만 붙인 요청은 HTTP 전에 멈췄다

비교에는 새 CA 하나와 그 CA가 발급한 default.test, preview.test 인증서를 사용했다. 두 인증서의 SAN(인증서가 유효한 대상 이름 목록)에는 각각의 DNS 이름만 넣었고 IP 주소는 넣지 않았다. 서버는 SNI가 preview.test일 때 그 인증서를 선택하고, 나머지는 기본 인증서를 제공하도록 구성했다. 실제 장애 기록이 아닌 별도 재현이다.

다음 명령으로 같은 IP와 포트에 접속하되 URL만 IP로 바꾸고, Host를 수동 지정한 결과를 확인했다.

curl -q --noproxy "*" --http1.1 --connect-timeout 2 --max-time 5 --cacert ./lab-ca.pem --verbose --silent --show-error -H "Host: preview.test:43771" "https://127.0.0.1:43771/"

curl은 종료 코드 60과 아래 오류를 남겼다. CA 파일은 앞의 성공 요청과 같았다.

curl: (60) SSL: no alternative certificate subject name matches target host name '127.0.0.1'

이 요청에서 서버가 받은 SNI는 없었고, 선택한 인증서는 default.test였다. curl은 URL의 IP에 맞는 인증서를 요구했지만 그 인증서에 해당 IP가 없었다. 서버에 HTTP 요청 자체가 도착하지 않았으므로 Host도 수신되지 않았다. 명령행에 Host를 적었다는 사실을 서버가 그 헤더를 읽었다는 증거로 삼으면 안 된다.

이름의 역할을 한 번 더 분리해 봤다. URL을 default.test로 두고 그 이름을 같은 IP로 --resolve하면서 Host만 preview.test:43771로 지정하자, 인증서 검증은 통과했다. 서버가 기록한 SNI는 default.test, HTTP Host는 preview.test:43771이었다. 이 합성 서버는 TLS에서 선택한 가상 호스트를 본문에 표시하므로 site=default.test를 반환했다. 실제 서버가 HTTP Host로 콘텐츠를 다시 선택하는지는 서버 설정에 달려 있다. 여기서 확인한 것은 Host를 바꿔도 URL에서 정한 TLS 이름이 바뀌지 않는다는 점이다.

오류가 남으면 메시지와 응답 대상을 함께 본다

--resolve는 잘못된 이름의 인증서를 통과시키지 않는다. 같은 실험에서 인증서를 마련하지 않은 missing.test를 URL과 --resolve에 넣자, 서버는 SNI로 missing.test를 받았지만 기본 인증서를 제공했고 curl은 다시 이름 불일치 오류와 종료 코드 60으로 끝났다. --cacert로 발급 CA를 알려 줘도 이름 검사는 남아 있었다. 두 비교 모두 -k나 --insecure를 사용하지 않았다.

자기 서버에서 인증서 오류가 계속되면 상세 출력의 대상 이름과 서버가 제공한 인증서 이름부터 비교한다. 이름은 맞는데 발급 CA를 신뢰하지 못한다는 오류라면 CA 파일과 서버의 인증서 체인을 확인한다. 연결 자체가 안 된다면 먼저 --resolve의 도메인·포트와 새 IP에 직접 접속할 수 있는지를 본다. TLS 검증 뒤 200을 받아도 예상한 사이트인지 본문이나 서버 식별 정보까지 확인한다.

성공·실패 비교를 마친 환경은 WSL2의 curl 8.5.0(OpenSSL 3.0.13), Bash 5.2.21, Python 3.12.3이었다. HTTP/1.1과 TLS 1.3으로 통신했으며 서버는 loopback에만 열고 종료했다. 시스템 신뢰 저장소·hosts·DNS와 운영 인증서는 변경하지 않았다.

Windows의 curl 8.21.0(Schannel)에서도 먼저 시도했지만, 네 조건 모두 the revocation status is unknown으로 중단됐다. 이 결과로 Windows에서 이름 검증 비교까지 완료했다고 말할 수는 없다. 종료 코드 60만으로 원인을 확정하지 않고 오류 문구를 함께 읽어야 하는 이유다. 이 글은 새 IP로의 직접 접속과 TLS 이름 보존을 확인한 범위이며, 실제 DNS 전파·프록시·CDN·리다이렉트 이후의 접속은 검증하지 않았다.

← 전체 글 보기