TLS 연결 디버깅: 인증서 오류를 찾아서

몇 주 전이 아니라 거의 석달 아니면 넉달 전 일인데, TCP 기반 서비스의 SSL/TLS 를 사용한 연결이 제대로 동작하지 않는 문제를 확인할 일이 생겼다. 문제 자체는 굉장히 단순했다. 나는 TLS 연결 문제를 확인할 때 주로 OpenSSL 의 opeenssl s_client명령을 이용하는데, 첫 문제는 이 명령어 한 번으로 찾을 수 있었다.

잘못된 TLS 설정을 사용하는 nginx 서버에 openssl s_client -connect 10.0.12.34:443 -servername hub.prgmr.net 명령을 실행하면 아래와 같은 로그를 출력한다. 테스트를 위해서 실제 서버가 아니라 10.0.12.34 에 떠 있는 서버가 실제로 해당 도메인 주소를 가지고 있다고 믿게(?)하고, 이 서버와 연결해서 테스트했다.

CONNECTED(00000003)
depth=0 CN = hub.prgmr.net
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 CN = hub.prgmr.net
verify error:num=21:unable to verify the first certificate
verify return:1
---
Certificate chain
 0 s:/CN=hub.prgmr.net
   i:/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
---
Server certificate
-----BEGIN CERTIFICATE-----
MIIGUDCCBTigAwIBAgISA0kdStyJNTUMMLjMLXKmzZvOMA0GCSqGSIb3DQEBCwUA
... (중략)
IhVTEXbM5GE5ECs2VflW6w+zBY3wHOlMnn3lA+Dn83iWHg+J
-----END CERTIFICATE-----
subject=/CN=hub.prgmr.net
issuer=/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
---
No client certificate CA names sent
Server Temp Key: ECDH, P-256, 256 bits
---
SSL handshake has read 2562 bytes and written 348 bytes
---
New, TLSv1/SSLv3, Cipher is ECDHE-RSA-AES256-GCM-SHA384
Server public key is 4096 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : ECDHE-RSA-AES256-GCM-SHA384
    Session-ID: 02085AA959197358B866CA34CF177224FD47454B18A2C49D59940B03E251472E
    Session-ID-ctx:
    Master-Key: 58465AA5FB96B22EA782C60B1A2E591D4C1F430EE0FCAF0B942E0374937C9FAA9ED8543925DFC6AFFA69A43B8142A857
    TLS session ticket lifetime hint: 300 (seconds)
    TLS session ticket:
    0000 - c5 3c 2a bb f3 20 53 e0-96 2a 13 6d 5b c8 13 54   .<*.. S..*.m[..T
    0010 - 70 ad a4 ed e3 5a e1 c6-d9 1a 54 70 e5 c6 2c ce   p....Z....Tp..,.
    0020 - 1c c1 39 10 aa 6d 79 07-6a b0 d1 dc 8f f7 a8 b7   ..9..my.j.......
    0030 - 58 cc 3a 38 ab 31 75 01-f6 c0 21 14 f0 29 b4 44   X.:8.1u...!..).D
    0040 - cc 87 d2 43 f3 63 73 63-7f 12 81 fb d4 d1 03 8b   ...C.csc........
    0050 - 6f 8d f7 1e c0 e6 bb ce-1c 78 e3 bd b9 e7 87 42   o........x.....B
    0060 - c2 7a 0e 57 00 a6 8b c3-bc 9b 6a f8 68 ff 6a 93   .z.W......j.h.j.
    0070 - c4 25 6a 27 8b b6 69 b8-85 8e 9d f0 16 d3 de 90   .%j'..i.........
    0080 - d3 7e 26 4b 90 bd a8 74-ab 38 c5 d3 8d 32 1f 28   .~&K...t.8...2.(
    0090 - 76 fd 74 d7 ea 18 0b ae-67 33 7f c0 d1 59 89 a2   v.t.....g3...Y..
    00a0 - b3 23 4c f2 12 c0 4f 40-ac fa 13 80 21 de 64 81   .#L...O@....!.d.
    00b0 - a1 8d 6d 4e f1 ad cf b4-58 3b 7e 26 92 23 42 81   ..mN....X;~&.#B.

    Start Time: 1546124091
    Timeout   : 7200 (sec)
    Verify return code: 21 (unable to verify the first certificate)
---

여기서 문제는 “unable to verify the first certificate” 이다. SSL/TLS는 인증서를 인증하기 위한 인증 기관 (Certificate Authority; CA) 인증서부터, 실제로 서버가 보내는 인증서까지 일련의 서명이 필요하다.

  • 각 브라우저 혹은 OS 혹은 사용 중인 TLS 라이브러리가 인식하는 루트 인증서로 서명한 인증서 (intermediate certificate)
  • 위 인증서로 서명한 또 다른 인증서 (또다른 intermediate certificate) …를 여러 단계 혹은 0 단계.
  • …마지막 중간 단계 인증서로 서명한 서버의 인증서

예를 들어 이 블로그를 구동하는 서버의 인증서는 DST Root CA X3 가 루트 인증서고, 그 인증서로 서명한 Let’s Encrypt 의 인증서, 마지막으로 해당 인증서로 서명한 이 블로그 도메인의 인증서로 이어진다.

TLS 인증서의 체인 예시

이런 형태로 연결된 일련의 인증서 (certificate chain) 가 필요하다. 그래서 “서버 인증서” 와 “서버 인증서까지 이르는 중간 단계 인증서” 를 모두 보내야하는데, 위에선 그렇지 않아서 이런 오류를 출력한다. TLS를 처음 설정할 때 이런 실수를 많이 하는데 (정식으로 서명한 인증서가 있으니 잘 돌아야하는데 왜 에러인가 같은 질문을 스택오버플로우에서 많이 찾아볼 수 있다), 잘못된 nginx 설정은 아래와 같다. 인증서 디렉터리는 Ubuntu 16.04 에서 Let’s Encrypt 를 사용하는 경우처럼 설정했다.

server {
  listen 443 ssl;
  server_name hub.prgmr.net;
  root /var/www/html;

  ssl on;
  ssl_certificate /etc/nginx/hub.prgmr.net/cert.pem;
  ssl_certificate_key /etc/nginx/hub.prgmr.net/privkey.pem;

  ssl_protocols TLSv1.2;
  ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECD
HE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA
384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256;
  ssl_prefer_server_ciphers on;
}

여기서, ssl_certificate 를 서버 인증서 파일인 으로 지정했는데, 실제로는 “서버 인증서\n중간 단계인증서k\n중간단계인증서k-1 … \n중간단계인증서0” 처럼 중간단계 인증서를 모두 포함한 파일로 지정해야 한다. Let’s Encrypt 인증서를 쓴다면 이미 해당 파일을 fullchain.pem 이란 이름으로 제공한다. 이 파일을 cert.pem 대신 지정해주면 정상적으로 TLS 연결을 맺는다.

이 설정 문제는 문제를 덮어놓은 다른 문제였고, 실제 문제는 다른 글로 정리하겠다(…).

새 모니터와 맥북 그리고 HDMI 문제

얼마 전에 집에 사용 중인 모니터 하나를 교체했다. 내 모니터는 아니어서 쓸일이 없었는데, 11월 말 – 12월 초에 독감으로 집에서 일하다가 잠시 이 4k지원 모니터를 맥북에 연결해서 써봄.

… 잘 나오더라? 이전에 회사에서 2560 × 1600 모니터 (아마도 Dell U2713HM) 에서 레터박스가 나와서 쓰는걸 포기하고 그 이후에는 Dell 24″ 만 연결해서 사용했는데 이건 왜 문제 없지? 라고 생각.

며칠 후에 애플 홈페이지를 확인해보니 사용 중인 맥북 (2015년 버전) 에서 4K 출력이 가능하다는 것.

연말에 시간 여유가 생긴 김에 Dell U2718Q 를 구매하고 설정. 설치하고나서 Windows 데스크톱과는 모니터의 miniDP / 본체의 DP를 연결하고, 이전부터 쓰던모니터는 HDMI 로 양쪽을 연결함. 대략 이런 셋업?

구입한 가장 큰 이유가 집에서 모니터에 맥북 물려서 쓸때도 눈 좀 편하게 써보자를 시험하기 시작. 근데 HDMI 케이블을 연결하니 화면 출력이 안된다. 맥북 화면도 계속 깜빡이고 — 외부 모니터에 연결되는 순간엔 항상 이런 식임 — Dell 모니터도 화면이 껌뻑이고 전원 대기 상태 진입하다 화면 출력하려 시도하다 반복하기만. 그러다가 대략 “이 모니터와 연결하려면 3840 × 2160 @ 60 Hz 를 지원하는 기기를 써라” 같은 메시지가 모니터 쪽에 표시.

여기까지 하고 지인 분 결혼식에 갔다옴. 거기서 만난 동문분들과 대화하는데, “케이블 문제인 것 같다” 라고 해서 집에 돌아와서 정리 후 시험해 봤다. 이전에 아내의 LG 모니터에 연결할 때 사용한 닌텐도 스위치의 HDMI 케이블을 새 모니터에 연결했다. 잘 나오더라.

이게 뭐가 원인일까 하고 구글 검색 시작. 이런 글이 나오더라. 요약하자면:

  • Standard (1.4보다 이전 버전): 4K 나 HDR 지원 안함
  • High-Speed (v 1.4): 4K 해상도 지원 + ARC, Ethernet
  • High-Speed (v 2.0): 4K @ 60fps / 2.0a, b는 HDR 지원 포함

사용 중인 케이블을 보니 “High-Speed HDMI cable with Ethernet” 이라고 적혀있더라. 근처 마트가서 이 케이블을 다시 확인해 보니 “1.4 버전” 케이블이라고 표기되어 있더라. 그래서 2.0 버전의 HDMI 케이블을 새로 구입해서 맥북과 연결하니 잘 된다.

애플 공식 페이지의 각주를 잘 읽었다면 좀 더 빨리 문제 원인을 파악하고 해결했을지도 모르겠다.

HDMI 2.0 케이블을 USB-C to HDMI 어댑터로 연결하고 아래와 같이 해상도 설정해서 쓰기 시작. 이젠 굳이 랩탑 모니터에서 캡쳐하거나, 코드를 읽지 않아도 좀 더 눈 덜 아프게 작업하게 될듯하다