2-5. HTTPS와 TLS — 암호화의 지연 회계와 플랫폼의 강제
한 줄 요약: TLS 핸드셰이크가 첫 바이트까지의 지연에 얹는 비용을 왕복 단위로 계산하고, 인증서·HSTS·mixed content로 인한 장애를 진단하며, 플랫폼이 HTTPS를 강제하는 구조를 설명할 수 있다.
학습 목표
- TCP+TLS 1.2 / TLS 1.3 / 0-RTT / QUIC 각각의 연결 수립 비용을 RTT 단위로 계산하고 DevTools에서 실측할 수 있다.
- 0-RTT가 멱등 요청에만 안전한 이유를 재전송 공격 관점에서 설명할 수 있다.
- 인증서 체인 검증의 구조와, "인증서는 유효한데 특정 클라이언트만 실패하는" 사례를 진단할 수 있다.
- HSTS의 동작과 preload 목록의 비가역성을 설명하고 도입 절차를 설계할 수 있다.
- mixed content 차단 규칙과 Secure Context가 기능 동작에 미치는 영향을 판단할 수 있다.
배경: 왜 이것이 존재하는가
백엔드에서 TLS는 대개 로드밸런서가 종단(termination)해 주는 인프라의 일이었다 — 애플리케이션 서버에는 평문 HTTP가 도착하고, 인증서 갱신은 다른 팀 티켓이었다. 프론트엔드에서는 이 무관심이 성립하지 않는다. 두 가지 이유다.
첫째, TLS 핸드셰이크의 지연을 사용자가 직접 체감한다. 핸드셰이크는 사용자 단말과 서버 사이에서 일어나고, 그 왕복 시간은 첫 바이트가 오기 전에 고스란히 로딩 시간에 얹힌다. RTT 100ms의 모바일에서 왕복 두 번이면 200ms — 성능 예산에서 무시할 수 없는 항목이고, TLS 1.3과 QUIC의 개선이 정확히 이 왕복 수를 깎는 작업이었다.
둘째, 웹 플랫폼이 HTTPS를 기능의 전제 조건으로 만들었다. Service Worker, Geolocation, 클립보드 API는 HTTPS가 아니면 존재하지 않는 것처럼 동작하고, HTTPS 페이지 안의 HTTP 리소스는 조용히 차단된다. HTTPS는 "보안 옵션"이 아니라 플랫폼 참가 자격이 됐다 — 왜 그렇게 됐는지도 이 문서의 주제다.
TLS가 제공하는 것은 세 가지로 요약된다: 기밀성(도청 불가), 무결성(변조 감지), 인증(상대가 그 도메인이 맞음). 암호 알고리즘의 내부는 다루지 않는다 — 프론트엔드 엔지니어에게 필요한 것은 수학이 아니라 비용 구조와 실패 양상이다.
핵심 개념
핸드셰이크의 지연 회계 — 왕복 수 세기
연결 수립 비용은 왕복(RTT) 수로 센다. 대역폭이 아니라 왕복이 지배하는 이유는, 핸드셰이크 메시지가 작아서(수 KB) 전송 시간은 무시할 만하고 상대의 응답을 기다려야 다음 단계로 갈 수 있는 구조이기 때문이다.
| 구성 | 데이터 전송 전 왕복 수 | 내역 |
|---|---|---|
| TCP + TLS 1.2 | 3 RTT | TCP 수립 1 + TLS 협상 2 |
| TCP + TLS 1.3 | 2 RTT | TCP 수립 1 + TLS 협상 1 |
| TCP + TLS 1.3 재개(0-RTT) | 1 RTT | TCP 수립 1 + 첫 왕복에 요청 동승 |
| QUIC (HTTP/3) | 1 RTT | 전송 수립과 TLS가 한 왕복에 통합 |
| QUIC 재개(0-RTT) | 0 RTT | 첫 패킷에 요청 동승 |
TLS 1.2가 2왕복이었던 이유: 첫 왕복에서 버전·암호 스위트를 합의한 뒤에야 두 번째 왕복에서 키 교환을 할 수 있는 순차 구조였다. TLS 1.3(RFC 8446)의 1왕복은 이 순차를 병렬로 바꾼 결과다 — 클라이언트가 "아마 이 키 교환 방식을 쓰겠지"라고 추측하고 키 재료(key share)를 첫 메시지에 동봉한다. 추측이 맞으면(현실에서 거의 항상 맞는다 — 지원 조합이 정리됐기 때문) 서버는 즉시 자기 키 재료와 함께 응답하고 암호화가 성립한다. 낙관적 실행으로 왕복을 접은 것이다. TLS 1.3은 이 외에도 취약한 레거시 알고리즘 대량 제거, 핸드셰이크 자체의 암호화(인증서가 평문으로 노출되던 1.2와 달리), 다운그레이드 공격 방어를 포함한다 — "더 빠르고 더 단순해서 더 안전한" 드문 개정이었다.
실측 방법 두 가지:
# curl의 타이밍 변수로 단계별 소요를 분해한다
curl -so /dev/null -w 'DNS: %{time_namelookup}s\nTCP: %{time_connect}s\nTLS: %{time_appconnect}s\nTTFB: %{time_starttransfer}s\n' https://example.com/
# DNS: 0.012s
# TCP: 0.045s ← TCP 완료 시각
# TLS: 0.098s ← TLS까지 완료. (TLS − TCP)가 TLS 협상 비용
# TTFB: 0.152sDevTools에서는 Network 패널 → 문서 요청 → Timing 탭의 "Initial connection"과 "SSL" 구간이 이것이다. Phase 0-1의 타이밍 분해에서 본 그 구간의 정체가 이 왕복들이고, preconnect 리소스 힌트가 미리 지불해 두는 비용도 정확히 이것이다.
0-RTT — 왕복 제거의 대가
재개(resumption) 핸드셰이크에서 TLS 1.3은 한 발 더 나간다. 이전 연결에서 합의해 둔 비밀(PSK)이 양쪽에 있으므로, 클라이언트가 첫 패킷에 암호화된 HTTP 요청까지 실어 보내는 것이 0-RTT(early data)다. 왕복 0회 — 그런데 공짜가 아니다.
문제는 재전송 공격(replay attack)이다. 0-RTT 데이터는 서버가 "이 클라이언트와 지금 살아 있는 대화 중"임을 확인하기 전에 처리된다. 네트워크 중간에서 그 첫 패킷을 복사해 뒀다가 나중에 다시 보내면, 서버는 같은 요청을 한 번 더 처리한다 — 공격자는 내용을 읽을 수 없지만(암호화됨) 재생시킬 수는 있다. 완전한 중복 제거를 서버가 보장하기 어렵기 때문에, 스펙의 결론은 2-1의 언어로 표현된다: 0-RTT에는 재생돼도 무해한 요청, 즉 안전(safe)한 요청만 싣는다. 브라우저·CDN 구현도 GET 계열만 0-RTT에 태운다. RFC 8470은 이를 위한 서버 측 도구도 정의한다 — 0-RTT로 받기 곤란한 요청에 425 Too Early로 응답하면 클라이언트가 핸드셰이크 완료 후 재전송한다.
의미론 계층의 속성(멱등·안전)이 전송 계층 최적화의 허용 조건이 되는 이 지점은, 계층 분리가 관념적 구분이 아니라 실제로 맞물려 동작하는 구조임을 보여주는 좋은 사례다. GET에 부작용을 넣은 서비스는 0-RTT 시대에 새로운 방식으로 대가를 치르게 된다.
인증서 체인 — 신뢰는 어떻게 부트스트랩되는가
암호화 채널이 있어도 상대가 진짜 그 도메인인지는 별개 문제다(중간자가 자기 키로 암호화 채널을 열어 줄 수도 있다). 이것을 해결하는 것이 인증서와 신뢰 체인이다.
- 브라우저·OS에는 신뢰하는 루트 CA(인증 기관)의 공개키 목록(신뢰 저장소)이 내장되어 있다. 신뢰의 출발점이자, "왜 아무나 인증서를 못 만드는가"의 답이다.
- 서버가 제시하는 것은 체인이다: 도메인의 리프 인증서 ← 중간 CA의 서명 ← (필요하면 더) ← 루트 CA의 서명. 루트가 리프에 직접 서명하지 않는 이유는 루트 개인키가 너무 중요해서 오프라인 보관되기 때문이다.
- 브라우저의 검증: 서명 체인이 신뢰 저장소의 루트까지 이어지는가 + 리프의 SAN(Subject Alternative Name) 필드에 접속 도메인이 포함되는가 + 유효 기간 + 폐기 여부(요즘 브라우저는 주로 자체 배포하는 폐기 목록으로 확인). 하나라도 실패하면 그 유명한 전면 경고 화면이다.
실무에서 자주 만나는 미묘한 실패가 중간 인증서 누락이다. 서버 설정에 리프만 넣고 중간 CA 인증서를 빠뜨린 경우인데, 증상이 지저분하다: 주요 브라우저는 누락된 중간 인증서를 스스로 구해 오거나(AIA fetching) 캐시해 둔 것을 쓰기 때문에 멀쩡히 열리는데, 그런 기능이 없는 클라이언트(일부 모바일 앱의 HTTP 클라이언트, curl, 오래된 기기)만 실패한다. "브라우저에선 되는데 앱에서만 SSL 에러"라는 신고가 오면 이것부터 의심한다.
# 서버가 제시하는 체인 전체를 확인한다
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | grep -E '^ [0-9] s:'
# 0 s:CN=example.com ← 리프
# 1 s:C=US, O=Let's Encrypt, CN=R11 ← 중간 CA. 이 줄이 없으면 체인 누락-servername이 곧 SNI(Server Name Indication)다 — 한 IP에서 여러 도메인을 서빙하는 서버가 어떤 인증서를 내밀지 알려면, 클라이언트가 TLS 핸드셰이크 첫 메시지에 도메인명을 실어 줘야 한다. 이 필드는 암호화 성립 전이라 평문으로 노출되며(방문 도메인이 네트워크 관찰자에게 보이는 마지막 큰 구멍), 이를 암호화하는 ECH(Encrypted Client Hello)가 배포 진행 중이다. 참고로 발급 사고를 감시하는 장치로 Certificate Transparency가 있다 — 모든 인증서 발급이 공개 로그에 기록되어야 하고, Chrome은 CT 증거 없는 인증서를 거부한다. 오발급을 막지는 못하지만 숨길 수 없게 만드는 접근이다.
HSTS — 최초의 평문 요청이라는 구멍
사이트가 HTTPS로 완전 이전해도 구멍이 하나 남는다. 사용자가 주소창에 example.com만 치면 (브라우저 기본 동작에 따라) 첫 요청이 http://로 나갈 수 있고, 서버의 301 리다이렉트가 HTTPS로 보내 주기까지의 평문 왕복 한 번이 중간자에게 열려 있다 — 그 사이에서 응답을 위조하면 사용자를 계속 평문에 붙잡아 둘 수 있다(SSL 스트리핑).
HSTS(RFC 6797)는 이 구멍을 막는다. Strict-Transport-Security: max-age=31536000 헤더를 받은 브라우저는 그 도메인을 기억해 두고, 이후 모든 접근을 네트워크에 나가기 전에 HTTPS로 바꾼다(Phase 0-1에서 본, 주소창 입력 처리의 첫 단계). 서버가 리다이렉트할 기회조차 필요 없어진다.
그래도 "최초 1회"는 남는다 — HSTS를 아직 못 받아 본 브라우저의 첫 방문. 이를 위한 것이 preload 목록이다: hstspreload.org에 등록하면 도메인이 브라우저 바이너리에 하드코딩되어 첫 방문부터 HTTPS다. 단, 등록 요건에 includeSubDomains(모든 서브도메인에 적용)가 포함되고, 목록 제거는 브라우저 릴리스를 타고 퍼지는 데 수개월이 걸린다. 즉 사실상 비가역이다 — 사내 시스템 하나가 어느 서브도메인에서 평문 HTTP로 돌고 있었다면, preload 등록과 함께 그 시스템은 접근 불능이 된다. 2-2의 max-age, 2-1의 301과 같은 패턴의 세 번째 등장: 한번 내보낸 선언은 회수할 수 없으므로, 낮은 값부터 단계적으로 올린다(max-age 수 분 → 수 일 → 1년 → preload).
Mixed content — HTTPS 페이지 안의 HTTP
HTTPS 페이지가 HTTP 리소스를 로드하면 보안 보장이 깨진다 — 평문으로 온 스크립트가 변조됐다면 페이지 전체가 뚫린 것이다. 브라우저의 차단 규칙은 리소스의 위험도에 따라 두 단계다.
- 차단형(blockable): 스크립트, 스타일시트, iframe,
fetch()등 능동적 콘텐츠. HTTP로는 로드 자체가 차단된다. 콘솔에 에러가 남고, 해당 기능은 조용히 죽는다. - 승격형(upgradable): 이미지, 오디오, 비디오 같은 수동적 콘텐츠. 현행 브라우저는 차단 대신 URL을 자동으로
https://로 바꿔 시도하고, HTTPS로 제공되지 않으면 그때 실패한다(Mixed Content Level 2). 과거의 "자물쇠에 경고 표시" 단계는 사라지는 추세다.
디버깅 시나리오의 전형: HTTP 시절 DB에 저장된 이미지 절대 URL(http://cdn.example.com/...)이 HTTPS 이전 후에도 남아 있는 경우다. 해당 호스트가 HTTPS를 지원하면 자동 승격으로 아무 일 없지만, 지원하지 않으면 이미지가 깨진다 — "어떤 이미지만 안 보인다"면 콘솔의 mixed content 메시지부터 확인한다. 예방책으로 CSP의 upgrade-insecure-requests 지시어(페이지 내 모든 HTTP URL을 요청 전에 승격)가 있고, 근본 해법은 저장 단계에서 프로토콜 상대 경로가 아닌 https:// 고정이다. CSP 자체는 Phase 7-4에서 다룬다.
Secure Context — 기능으로 강제하는 HTTPS
플랫폼이 HTTPS를 밀어붙인 가장 강력한 수단은 경고 표시가 아니라 기능 게이팅이었다. Secure Contexts 스펙은 "충분히 신뢰할 수 있는 출처"(potentially trustworthy origin — HTTPS, 그리고 예외적으로 localhost)를 정의하고, 강력한 API들을 그 안에서만 노출한다.
Service Worker, Geolocation, navigator.clipboard, Web Crypto(crypto.subtle), WebUSB, 카메라·마이크(getUserMedia) 등이 해당한다. 중요한 것은 실패 양상이다 — 권한 거부 같은 명시적 에러가 아니라, API가 undefined거나 조용히 비활성인 경우가 많다. 새 기능이 표준화될 때 Secure Context 전용이 기본값이 된 지 오래라, 이 목록은 계속 늘어난다. "HTTPS가 사실상 필수"라는 말의 실체는 도덕적 권고가 아니라 이 기능 목록이다.
개발 환경에서의 함정이 여기서 나온다. localhost는 Secure Context로 인정되므로 로컬 개발은 HTTP로도 잘 돌아간다. 그런데 같은 개발 서버를 사설 IP로 접근하는 순간(http://192.168.0.5:3000 — 실기기 모바일 테스트의 전형) Secure Context가 아니게 되어, 잘 돌던 클립보드·카메라 기능이 실기기에서만 죽는다. 해법은 로컬 HTTPS다 — mkcert로 로컬 CA를 만들어 신뢰 저장소에 넣고 개발용 인증서를 발급하는 것이 표준적 방법이고, Vite 등 개발 서버들이 HTTPS 옵션을 지원한다.
실무 관점
TLS 지연의 진단과 처방
증상: Timing 탭에서 Initial connection + SSL 구간이 크다. 처방의 우선순위:
- TLS 1.3 확인 — DevTools Security 패널에서 프로토콜 버전 확인. 1.2라면 서버/CDN 설정 갱신이 왕복 하나를 그냥 없앤다(대부분의 CDN은 이미 1.3이 기본).
- 연결 재사용 확인 — 같은 오리진 요청들이 연결을 재사용하고 있는지(Timing에서 connection 구간이 0인지). 오리진이 흩어져 있으면 2-4의 통합 논리가 그대로 적용된다.
preconnect— 반드시 쓸 외부 오리진(API, 폰트)은<link rel="preconnect">로 핸드셰이크를 페이지 로드와 병렬화한다. 비용이 있으므로(연결 유지) 확실한 오리진에만.- H3/0-RTT — 재방문 트래픽이 많다면 재개·0-RTT의 효과가 크다. CDN 옵션 확인.
HSTS 도입 체크리스트
- 모든 서브도메인의 HTTPS 지원을 실사한다 — 잊힌 관리 도구, 레거시 시스템 포함.
includeSubDomains가 이들을 전부 물게 된다. max-age=300으로 시작해 문제 없음을 확인하며 단계적으로 올린다 (5분 → 1주 → 1년).- preload는 마지막이며, "제거에 수개월"을 조직이 감수할 수 있을 때만 등록한다.
인증서 운영이 프론트엔드에 닿는 지점
인증서 자동 갱신(Let's Encrypt + ACME)이 표준이 된 지금, 남은 사고 유형은 대부분 갱신 실패의 뒤늦은 발견과 중간 인증서 누락이다. 전자는 만료 모니터링(만료 2주 전 알림)으로, 후자는 배포 후 openssl s_client 체인 확인을 배포 절차에 넣는 것으로 막는다. 인증서 만료는 클라이언트 코드로 우회할 수 없는 전면 장애이므로, 프론트엔드 엔지니어도 이 두 확인 방법은 손에 익혀 둘 가치가 있다.
더 깊이
세션 재개의 계보 — 서버 상태에서 티켓으로
0-RTT의 전제인 "이전 연결의 비밀 공유"에는 역사가 있다. TLS 1.2까지는 두 방식이 있었다: 서버가 세션을 저장하고 ID로 찾아 주는 session ID 방식(서버 메모리 부담 + 서버 여러 대면 저장소 공유 문제 — 2-3의 세션 저장소와 정확히 같은 확장성 문제)과, 세션 상태를 암호화해 클라이언트에게 들려 보내는 session ticket 방식(서버 무상태 — JWT와 같은 구조). TLS 1.3은 이를 PSK(pre-shared key) 프레임워크로 통합·재설계했다. 상태를 어디에 둘 것인가라는 설계 문제가 HTTP 세션과 TLS 재개에서 같은 형태로 반복되고 같은 트레이드오프(서버 부담 vs 무효화·전방 보안)를 낳는다는 것은, 이런 패턴이 특정 기술이 아니라 분산 시스템의 구조적 문제임을 보여준다.
신뢰 저장소의 정치학
"어떤 CA를 신뢰할 것인가"는 기술이 아니라 거버넌스 문제다. 각 브라우저·OS 벤더는 루트 프로그램을 운영하며 CA에 감사·정책 준수를 요구하고, 위반 시 퇴출한다 — 2018년 Symantec CA 전체가 오발급 이력으로 Chrome/Firefox에서 단계적 불신임된 것이 대표 사례로, 당시 수십만 사이트가 인증서를 재발급받아야 했다. 인증서 유효기간이 수년에서 계속 단축되어 온 것(현재 최대 398일, 더 짧아지는 방향으로 진행 중)도 같은 맥락이다 — 폐기 메커니즘이 불완전하므로 수명 자체를 줄여 피해 창을 좁히는 것이고, 이것이 자동 갱신(ACME)을 사실상 의무로 만들었다.
ECH — 마지막 평문의 암호화
TLS 1.3이 핸드셰이크 대부분을 암호화했지만 SNI는 남았다 — 서버가 어떤 키를 쓸지 정하기 전에 도착해야 하는 정보라, 닭-달걀 문제였다. ECH(Encrypted Client Hello)는 DNS HTTPS 레코드(2-4에서 본 그 레코드)로 미리 배포된 공개키로 ClientHello 자체를 암호화해 이를 푼다 — DNS가 키 배포 채널이 되는 구조이고, 따라서 DNS 조회 자체의 암호화(DoH/DoT)와 한 세트로 완성된다. 배포는 진행 중이며(Chrome·Firefox·Cloudflare 지원), 방문 도메인을 네트워크 사업자가 볼 수 있는 마지막 창이 닫히는 중이다.
정리
- 연결 수립 비용은 왕복 수로 센다: TCP+TLS1.2는 3 RTT, TLS 1.3은 2, QUIC은 1, 0-RTT 재개는 0까지. TLS 1.3의 개선은 키 재료를 추측 동봉하는 낙관적 실행으로 왕복을 접은 것이고,
curl -w와 DevTools Timing으로 실측한다. - 0-RTT 데이터는 재전송 공격에 열려 있어 안전(safe)한 요청만 실을 수 있다 — 2-1의 메서드 속성이 전송 계층 최적화의 허용 조건이 되는 지점이다.
- 인증서 신뢰는 내장 루트 저장소에서 체인으로 부트스트랩된다. 중간 인증서 누락은 브라우저의 관용(AIA fetching) 때문에 "앱에서만 실패"라는 형태로 나타난다.
- HSTS는 평문 첫 요청의 창을 닫고, preload는 그 마지막 1회까지 없앤다 — 대신 사실상 비가역이므로 max-age를 단계적으로 올리는 것이 정석이다. 301, max-age에 이은 "회수 불가능한 선언" 패턴의 반복이다.
- 플랫폼은 HTTPS를 기능으로 강제한다: mixed content 차단(능동 콘텐츠)과 Secure Context 게이팅(Service Worker 등).
localhost는 예외라 개발 중엔 안 보이다가 사설 IP 실기기 테스트에서 드러난다 — 로컬 HTTPS(mkcert)가 해법이다.
확인 문제
1. RTT가 120ms인 모바일 환경에서, TLS 1.2 + HTTP/2인 사이트의 첫 요청이 서버 처리 0ms라고 가정할 때 첫 바이트까지 최소 몇 ms인가? 서버를 TLS 1.3으로 올리면? HTTP/3 + 0-RTT 재방문이면? (DNS는 캐시되어 있다고 가정)
정답과 해설
TLS 1.2: TCP 1 RTT + TLS 2 RTT + 요청/응답 1 RTT = 4 RTT = 480ms. TLS 1.3: TCP 1 + TLS 1 + 요청 1 = 3 RTT = 360ms — 서버 설정 변경만으로 120ms가 사라진다. HTTP/3 + 0-RTT: 첫 패킷에 요청이 동승하므로 요청/응답의 1 RTT만 남아 120ms. 이 계산이 보여주는 것: 저대역폭이 아니라 고지연 환경에서 프로토콜 세대의 차이가 지배적이 되며, 개선은 코드가 아니라 왕복 수에서 나온다. 실측은 curl -w의 time_connect/time_appconnect 분해나 DevTools Timing 탭으로 한다.
2. 결제 서비스가 CDN의 0-RTT 옵션을 켰다. 이후 보안 감사에서 "특정 조건에서 결제 요청이 중복 처리될 수 있다"는 지적을 받았다. 어떤 시나리오인가? 서비스 코드가 지켜야 했던 원칙과, 서버가 쓸 수 있는 방어 수단을 답하라.
정답과 해설
시나리오: 결제 요청이 GET(또는 0-RTT에 실리는 형태)으로 구현되어 있었다면, 네트워크 경로상의 공격자가 0-RTT early data 패킷을 복사해 뒀다가 재전송할 수 있다. 서버는 핸드셰이크 완료 전에 이 데이터를 처리하므로 같은 결제가 두 번 실행된다 — 공격자는 내용을 복호화하지 못해도 재생은 가능하다. 지켜야 했던 원칙: ① 부작용 있는 요청을 안전(safe) 메서드로 만들지 않는다(2-1의 계약 — 0-RTT 구현들은 safe 요청만 early data에 싣는 것을 전제한다). ② 비멱등 작업에는 Idempotency-Key 같은 애플리케이션 계층 중복 방지를 갖춘다. 서버 측 방어: 0-RTT로 도착한 요청 중 부작용이 있는 것에 425 Too Early를 응답하면(RFC 8470), 클라이언트가 핸드셰이크 완료 후 재전송한다 — 재생 창이 닫힌 뒤에만 처리하는 것이다.
3. HTTPS 이전을 마친 서비스에서 두 가지 잔여 증상이 보고됐다. (a) 웹은 정상인데 안드로이드 앱에서만 SSL 핸드셰이크 에러. (b) 일부 옛 게시글의 첨부 이미지만 깨져 보임. 각각의 원인 후보와 확인 명령·절차는?
정답과 해설
(a) 중간 인증서 누락이 1순위 후보다. 브라우저는 누락된 중간 CA 인증서를 AIA fetching이나 캐시로 스스로 보완하므로 정상 동작하지만, 그런 관용이 없는 앱의 HTTP 클라이언트는 체인을 완성하지 못해 실패한다. 확인: openssl s_client -connect host:443 -servername host로 서버가 제시하는 체인을 보고, 리프 아래에 중간 CA가 포함되어 있는지 확인한다(s: 줄이 리프 하나뿐이면 누락). 수정은 서버/LB 설정에 풀체인(fullchain) 배포. (b) mixed content. 옛 게시글 본문에 절대 URL http://...로 저장된 이미지가, 해당 호스트가 HTTPS 미지원이면 자동 승격(upgradable 콘텐츠의 https 재시도)에 실패해 깨진다. 확인: 해당 페이지 콘솔의 mixed content 메시지, Network 패널에서 이미지 요청의 URL·상태. 수정: 데이터의 URL을 https로 마이그레이션하는 것이 근본 해법이고, 과도기에는 CSP upgrade-insecure-requests로 페이지 차원 승격을 걸 수 있다.
참고 자료
- RFC 8446 — TLS 1.3 — 핸드셰이크 흐름(§2)과 0-RTT의 보안 고려(§8, Appendix E.5)가 이 문서의 1차 근거.
- RFC 8470 — Using Early Data in HTTP — 425 Too Early와 0-RTT 위 HTTP의 규칙.
- RFC 6797 — HTTP Strict Transport Security / hstspreload.org — HSTS 스펙과 preload 등록 요건·제거 절차.
- W3C — Mixed Content / Secure Contexts — 차단·승격 분류와 "potentially trustworthy origin"의 정의 원문.
- MDN — Features restricted to secure contexts — Secure Context 전용 API 목록.
- mkcert — 로컬 개발 HTTPS의 표준적 도구.
- Cloudflare Learning — What happens in a TLS handshake? — 핸드셰이크 단계의 시각적 개관(검증용 참고).