네트워크 학습 노트 목차

HTTPS·TLS — 엿보기·변조·사칭을 막다

지금까지 본 HTTP평문이다 — 요청과 응답이 케이블을 따라 그대로 흐른다. 중간의 라우터·공용 Wi-Fi·통신사 누구든 내용을 읽고, 몰래 바꾸고, 심지어 다른 서버인 척 가로챌 수 있다. 이걸 막는 게 HTTPS — 곧 HTTP를 TLS라는 보안 계층 위에 얹은 것이다. TLS가 보장하는 건 세 가지다 — 기밀성(엿봐도 못 읽음), 무결성(바뀌면 들킴), 인증(상대가 진짜인지 확인). 이 셋을 어떻게 동시에 이루는지를 본다.

빠른 자물쇠와 느린 자물쇠 — 대칭과 비대칭

암호에는 성격이 다른 두 종류가 있다. 대칭키같은 키로 잠그고 푼다 — 빠르지만 그 키를 어떻게 상대에게 안전하게 전달하느냐가 문제다(키를 보내다 도청당하면 끝). 비대칭키공개키로 잠그면 개인키로만 풀린다(반대도) — 키 전달 문제가 없지만 수십~수백 배 느리다. TLS는 둘을 영리하게 조합한다. 느린 비대칭으로 대칭키를 안전하게 합의하는 악수(핸드셰이크)만 하고, 그 뒤 실제 데이터는 빠른 대칭키로 암호화한다. (출처: Cloudflare — TLS Handshake.)

CS 연결 — "비대칭은 안전한데 왜 안 그걸로 다 안 하나?"의 답은 비용이다. 비대칭(RSA·ECC)은 큰 수의 모듈러 지수승 같은 무거운 연산이라 CPU를 많이 먹는다. 그래서 키 교환·인증이라는 한 번의 비싼 일에만 쓰고, *대량 데이터는 싼 대칭(AES-GCM·ChaCha20)*에 맡긴다 — 비싼 자원은 꼭 필요한 한 번만이라는, 이 노트 전체에 흐르는 발상이다.

진짜 그 서버가 맞나 — 인증서와 CA

암호화만으론 부족하다 — 공격자와 안전하게 암호화 통신해 봐야 소용없다. 그래서 상대가 진짜인지 확인하는 인증이 필요하다. 서버는 **인증서**를 내미는데, 거기엔 서버의 공개키신뢰할 수 있는 기관(CA)의 디지털 서명이 들어 있다.

다이어그램 로딩 중…

브라우저는 미리 신뢰하는 루트 CA 목록을 들고 있어, 서버 인증서가 그 루트까지 이어지는 체인인지, 만료되지 않았는지, 호스트명이 맞는지, 폐기되지 않았는지를 검증한다. 결정적으로 — 서버는 핸드셰이크를 자기 개인키로 서명해 보낸다. 브라우저가 인증서의 공개키로 그 서명을 확인하면, "이 서버가 인증서에 적힌 개인키를 진짜 갖고 있다"가 증명된다 — 남의 인증서를 훔쳐 흉내 내는 것을 막는 핵심이다. (출처: Certificate chain validation.)

더 빠르게 — TLS 1.3과 0-RTT

핸드셰이크는 *왕복(RTT)*을 잡아먹어 첫 응답을 늦춘다. TLS 1.3은 이를 1-RTT로 줄였고(클라이언트가 키 교환 파라미터를 첫 메시지에 바로 보내), *순방향 비밀성(forward secrecy)*을 필수로 만들었다 — 나중에 개인키가 털려도 과거 통신은 못 푼다. 더 나아가 재접속에는 **0-RTT**가 있다 — 이전에 합의한 *사전 공유 키(PSK)*로 첫 메시지부터 암호화된 요청을 바로 보낸다(왕복 0번).

실무에서는 — 0-RTT의 그늘과 인증서 운영. 0-RTT는 빠르지만 *순방향 비밀성이 없고, 같은 요청이 *재전송(replay)될 위험이 있다. 그래서 표준은 0-RTT를 멱등 요청(GET)에만 허용하라고 권한다 — 여기서도 멱등성(같은 요청을 두 번 처리해도 안전함)이 안전의 열쇠다(HTTP 편 연장). 그리고 운영에서 가장 흔한 보안 장애가 인증서 만료다 — 어느 날 갑자기 모든 사용자가 "안전하지 않음" 경고를 본다. 그래서 Let's Encrypt 같은 자동 갱신만료 모니터링이 필수다. HSTS로 브라우저가 항상 HTTPS만 쓰게 강제하고, 서비스 간 통신엔 양쪽이 서로 인증서를 검증하는 mTLS(서버만이 아니라 클라이언트도 인증)를 쓰기도 한다.

실제로 눈으로 보기

핸드셰이크와 인증서 사슬

openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | head -20
# CONNECTED(00000003)
# ---
# Certificate chain
#  0 s:CN = example.com                       ← 서버 인증서(리프)
#    i:C = US, O = DigiCert Inc, CN = DigiCert TLS RSA SHA256 2020 CA1
#  1 s:C = US, O = DigiCert Inc, CN = DigiCert TLS RSA SHA256 2020 CA1
#    i:C = US, O = DigiCert Inc, CN = DigiCert Global Root CA   ← 루트로 이어진다
# ---
# New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384

s:(subject)와 i:(issuer)가 사슬로 이어지는 것이 요점이다. 0번의 발급자가 1번의 주체이고, 1번의 발급자가 루트다. 브라우저는 이 사슬을 따라 올라가 자기가 이미 신뢰하는 루트에 닿는지 확인한다.

가장 흔한 실패는 사슬이 끊긴 것이다

openssl s_client -connect broken.example.com:443 </dev/null 2>&1 | grep -E 'Verify|verify error'
# verify error:num=20:unable to get local issuer certificate
# Verify return code: 21 (unable to verify the first certificate)

이 오류는 대개 서버가 중간 인증서를 안 보낸 것이다. 서버는 리프와 중간을 함께 보내야 하는데 리프만 보내면 사슬이 끊긴다.

고약한 점은 브라우저에서는 되는데 서버끼리는 안 되는 경우가 생긴다는 것이다. 브라우저는 중간 인증서를 캐시하거나 알아서 받아 오지만, 서버 측 클라이언트는 그러지 않는다. "웹에서는 잘 되는데 API 호출만 실패한다" 가 이 모양이다.

만료와 이름 불일치

echo | openssl s_client -connect example.com:443 2>/dev/null \
  | openssl x509 -noout -dates -subject -ext subjectAltName
# notBefore=Jan 15 00:00:00 2026 GMT
# notAfter=Apr 15 23:59:59 2026 GMT
# subject=CN = example.com
# X509v3 Subject Alternative Name:
#     DNS:example.com, DNS:www.example.com

요즘 브라우저는 CN 을 안 본다. subjectAltName에 그 이름이 있어야 통과한다. api.example.com으로 접속하는데 SAN 에 없으면 실패한다 — 인증서를 발급할 때 서브도메인을 빠뜨리면 여기서 걸린다.

어느 이름으로 물어보느냐가 응답을 바꾼다

# SNI 를 주면 그 사이트의 인증서가 온다
openssl s_client -connect 93.184.216.34:443 -servername example.com </dev/null 2>/dev/null | grep 'subject='

# SNI 없이 IP 로만 붙으면 기본 인증서가 온다
openssl s_client -connect 93.184.216.34:443 </dev/null 2>/dev/null | grep 'subject='

한 IP 에 사이트가 여러 개 있을 수 있으므로, 클라이언트가 "나는 이 이름으로 왔다" 고 먼저 알려 줘야(SNI) 서버가 맞는 인증서를 고른다. SNI 는 암호화 전에 평문으로 나가므로, 어느 사이트에 접속하는지는 중간에서 보인다.

1.3 이 무엇을 줄였나

openssl s_client -connect example.com:443 -tls1_2 </dev/null 2>/dev/null | grep -E 'Protocol|Cipher'
openssl s_client -connect example.com:443 -tls1_3 </dev/null 2>/dev/null | grep -E 'Protocol|Cipher'
TLS 1.2   ClientHello → ServerHello → 키교환 → Finished   = 왕복 2회
TLS 1.3   ClientHello(키 추측 포함) → ServerHello+Finished = 왕복 1회

1.3 은 클라이언트가 키 공유를 미리 얹어 보내 왕복을 하나 줄였다. RTT 100ms 면 100ms 를 그냥 번다. 그리고 안전하지 않은 알고리즘을 협상 목록에서 아예 없애 잘못 고를 여지를 제거했다 — 선택지를 줄이는 것이 보안이라는 접근이다.

정리하면, HTTPS는 평문 HTTP를 TLS 위에 얹어기밀성·무결성·인증을 더한다. 느린 비대칭으로 대칭키를 안전하게 합의하고 빠른 대칭으로 데이터를 암호화하며, CA가 서명한 인증서개인키 서명으로 진짜 서버임을 증명한다. TLS 1.3은 핸드셰이크를 1-RTT로 줄이고 순방향 비밀성을 보장하며, 0-RTT는 빠르지만 멱등 요청에만 안전하다 — 실무에선 인증서 자동 갱신·HSTS·mTLS가 그 위에서 보안을 떠받친다. 이제 이 모든 프로토콜을 코드에서 실제로 다루는 창구소켓을 볼 차례다.

HTTP — 요청/응답·메서드·1.1/2/3·HoL소켓 — bind/listen/accept·blocking·epoll