목차
주소창에 자물쇠가 떠 있으면 안심하게 된다. 그런데 정확히 무엇이 안전해진 건지 물으면 답하기가 애매하다.
https면 다 안전한가
먼저 답부터 말하면 아니다. 그런데 “아니다”가 “HTTPS는 별거 아니다”라는 뜻도 아니다.
HTTPS는 아주 구체적인 일을 아주 잘한다. 문제는 그 일의 범위가 우리가 막연히 기대하는 “안전”보다 좁다는 것이다. 이 글은 암호 알고리즘 이야기를 하지 않는다. 경계선을 긋는 게 목적이다. 어디까지가 HTTPS의 일이고 어디부터가 아닌지.
평문은 지나는 모두가 읽는다
왜 감싸야 하는지부터 보자. 요청과 응답 글에서 HTTP 메시지가 그냥 글자라고 했다. 글자 그대로 선을 타고 간다.
그 선은 나와 서버 사이의 직통이 아니다. 카페 공유기, 통신사 장비, 중간 사업자의 라우터를 줄줄이 거친다.
평문 HTTP라면 이 길목 어디서든 메시지를 통째로 읽는다. 비밀번호도 쿠키도 그대로다. 그리고 읽는 것보다 무서운 게 있다. 고칠 수도 있다. 응답 HTML에 광고나 스크립트를 끼워 넣어도 받는 쪽은 원본이 그랬는지 아닌지 알 방법이 없다.
세 번째 위험도 있다. 내가 말하는 상대가 진짜 그 서버인지 확인할 방법이 없다. 누군가 중간에서 서버인 척 답해도 구분이 안 된다.
HTTPS가 하는 일은 셋이다
방금 나온 세 가지 위험에 하나씩 대응한다.
| 위험 | HTTPS가 하는 일 |
|---|---|
| 읽힌다 | 암호화 - 오가는 내용을 당사자만 읽게 |
| 고쳐진다 | 무결성 - 중간에 바뀌면 티가 나게 |
| 상대가 누군지 모른다 | 신원 확인 - 상대가 그 도메인의 주인임을 증명 |
HTTPS는 새 프로토콜이 아니다. HTTP를 TLS라는 층으로 감싼 것이다. 위층의 HTTP는 자기가 감싸였다는 것도 모른다. 메서드도 헤더도 상태 코드도 그대로다.
본격적인 대화 전에 이 감싸는 층을 세우는 절차를 핸드셰이크라고 부른다. 크게 세 가지를 한다. 어떤 방식으로 말할지 합의하고, 서버가 자기 신원을 증명하고, 이번 대화에만 쓸 열쇠를 함께 만든다.
여기에 연결 글에서 말한 비용이 붙는다. TCP 연결 준비 왕복 위에 이 핸드셰이크 왕복이 더 얹힌다. TLS 1.3은 1.2보다 이 왕복 횟수를 줄였고, HTTP/3의 QUIC은 연결 준비와 이 절차를 아예 합쳤다.
신원은 인증서가 증명한다
셋 중 가장 헷갈리는 게 신원 확인이다. 서버가 “나 example.com 맞다”고 말하는 건 누구나 할 수 있다. 그걸 어떻게 믿나.
제3자가 보증한다. 서버는 인증서라는 문서를 내미는데, 거기에는 도메인 이름과 서버의 공개키가 적혀 있고, 그 내용에 인증기관(CA)의 서명이 붙어 있다.
브라우저는 서버가 준 인증서부터 위로 거슬러 올라가며 서명을 검증한다. 그러다 자기가 이미 갖고 있는 루트 CA에 닿으면 통과다. 루트 CA 목록은 브라우저나 운영체제에 미리 들어 있다.
브라우저가 확인하는 건 대략 이렇다.
- 서명 사슬이 신뢰하는 루트까지 이어지는가
- 인증서에 적힌 도메인이 지금 접속한 도메인과 맞는가
- 유효기간이 지나지 않았는가
- 취소된 인증서는 아닌가
하나라도 어긋나면 경고 화면이 뜬다. 그 화면은 “위험한 사이트”라는 뜻이 아니라 **“이 상대가 주장하는 신원을 확인할 수 없다”**는 뜻이다.
인증서 만료는 흔한 장애 원인이다. 사람이 달력에 적어두는 방식은 언젠가 반드시 놓친다. 자동 갱신을 걸어두고, 갱신이 실패했을 때 알림이 오는지까지 확인해두는 편이 낫다. 갱신 자동화보다 갱신 실패 감지가 더 자주 빠진다.
자물쇠는 이 사이트가 착하다는 뜻이 아니다
여기가 이 글에서 가장 자주 오해받는 지점이다.
인증서가 증명하는 건 “이 서버가 이 도메인의 주인이다” 하나다. 그 도메인이 사기 사이트가 아니라는 보증이 아니다.
피싱 사이트도 자기 도메인을 사고 인증서를 발급받을 수 있다. 무료 인증서가 흔해진 뒤로는 더 쉬워졌다. examp1e.com 같은 도메인에도 정상적인 자물쇠가 뜬다. 자물쇠는 “당신이 보고 있는 그 주소로 제대로 연결됐다”까지만 말한다. 그 주소가 당신이 가려던 곳인지는 말해주지 않는다.
그래서 브라우저들이 주소창의 자물쇠 표시를 점점 눈에 안 띄게 바꿔왔다. 안전 신호처럼 읽히는 게 오히려 위험했기 때문이다.
인증서에는 검증 수준의 등급이 있다. 도메인 소유만 확인하는 것과 기관의 실체까지 확인하는 것이 다르다. 다만 오늘날 브라우저 UI에서 이 차이가 사용자에게 잘 드러나지 않는다. 인증서 등급으로 사이트의 신뢰도를 판단하는 습관은 기대만큼 작동하지 않는다.
그래서 감춰지는 것
경계선을 그어보자. 먼저 확실히 감춰지는 것들이다.
- 경로와 쿼리스트링.
/users/42?token=abc의/users/42?token=abc전체가 안 보인다. URL에서 감춰지는 건 이 부분이다. - 요청·응답 헤더 전부. 쿠키,
Authorization,User-Agent가 다 가려진다. - 바디. 보낸 폼 값도 받은 JSON도 안 보인다.
- 상태 코드와 메서드.
GET인지POST인지, 200인지 404인지도 안 보인다.
정리하면 HTTP 메시지 자체는 통째로 감춰진다. 앞의 글들에서 본 그 글자들이 전부 여기 들어간다.
감춰지지 않는 것
이제 이 글의 알맹이다. 감싸고도 그대로 드러나는 것들이 있다.
하나씩 보자.
어느 서버에 붙었는지(IP). 암호화는 내용을 가리지 봉투의 주소까지 지우지는 못한다. 어디로 가는 패킷인지는 길목의 모두가 본다.
어떤 도메인을 찾았는지(DNS). 접속하기 전에 도메인을 IP로 바꾸는 조회가 먼저 일어나는데, 이 조회는 전통적으로 평문이다. HTTPS는 그 뒤에 시작되므로 이 단계를 감싸지 못한다. 이걸 가리려는 별도 방식(DNS over HTTPS 등)이 나와 있지만, HTTPS를 쓴다고 자동으로 적용되는 게 아니다. 클라이언트와 네트워크 설정에 달렸다.
핸드셰이크에 적히는 도메인 이름(SNI). 이게 가장 덜 알려진 구멍이다. 서버 컴퓨터 하나에 사이트가 여러 개 있으면, 서버는 어느 사이트의 인증서를 꺼내야 할지 알아야 한다. 그런데 그 시점은 아직 암호화가 시작되기 전이다. 그래서 클라이언트가 첫 인사에 도메인 이름을 평문으로 적어 보낸다. 감시자는 example.com에 접속했다는 사실을 그대로 읽는다.
이 SNI까지 가리려는 확장(Encrypted Client Hello)이 있다. 다만 이건 별도 확장이고 클라이언트·서버·DNS 쪽이 모두 받쳐줘야 동작한다. “요즘은 SNI도 암호화된다”고 일반화할 수 있는 단계는 아니다. 지금 내 환경에서 되는지는 실제로 확인해야 아는 일이다.
주고받은 양과 시점. 내용은 못 읽어도 얼마나 큰 것이 언제 오갔는지는 보인다. 이것만으로도 짐작이 가능한 경우가 있다. 특정 페이지의 크기가 특징적이라면 어느 페이지를 봤는지 추측할 여지가 생긴다. 이런 추측을 어렵게 하려면 트래픽 자체를 섞어야 하는데, 그건 HTTPS의 일이 아니다.
정리하면 이렇다. HTTPS는 무엇을 말했는지를 감추지만, 누구와 얼마나 이야기했는지는 감추지 못한다.
회사 네트워크에서는 왜 다 보이나
여기까지 읽으면 이상한 게 하나 있다. 사내 보안 장비가 직원의 HTTPS 트래픽을 검사한다는 이야기를 들어봤을 것이다. 감춰진다면서 어떻게 검사하나.
중간에서 대신 끊고 다시 감싸기 때문이다.
이게 되려면 조건이 하나 필요하다. 직원 PC가 사내 CA를 신뢰해야 한다. 회사가 관리하는 기기에 사내 루트 인증서를 미리 설치해두면, 프록시가 발급한 가짜 인증서도 정상으로 보인다.
이 사실이 알려주는 게 있다. HTTPS의 신뢰는 결국 “내 기기가 어떤 루트 CA를 믿느냐”에 달려 있다. 누군가 내 기기에 자기 루트 인증서를 심을 수 있으면 그 지점부터 앞의 보장은 무너진다. 출처가 불분명한 인증서를 설치하라는 안내를 조심해야 하는 이유다.
같은 원리가 개발할 때도 쓰인다. 로컬에서 HTTPS 트래픽을 들여다보는 도구들이 정확히 이 방식으로 동작한다.
실무에서 챙기는 것
끝나는 지점이 어디인지 안다. 큰 서비스는 대개 로드밸런서나 CDN에서 TLS를 벗기고, 그 뒤 내부망에서는 평문 HTTP로 앱에 넘긴다. 앱이 보는 요청에는 자물쇠가 없다. 그러니 “우리 서비스는 HTTPS니까 다 암호화된다”는 말은 끝나는 지점까지만 참이다. 내부망도 감싸야 하는지는 별도로 정할 문제다.
앱이 프로토콜을 착각한다. 앞단이 TLS를 벗기고 넘기면 앱은 자기가 HTTP로 불렸다고 생각한다. 그래서 리다이렉트 주소를 http://로 만들어버리는 사고가 난다. 앞단이 붙여주는 X-Forwarded-Proto 같은 헤더를 앱이 읽도록 설정해야 한다.
쿠키에 Secure를 붙인다. 세션 식별자나 토큰이 담긴 쿠키에 이 속성이 없으면, 브라우저는 그 쿠키를 평문 HTTP 요청에도 실어 보낸다. 사용자가 주소창에 example.com만 쳤을 때 나가는 그 첫 요청이 바로 그 경우다. 서버가 곧바로 HTTPS로 돌려보내더라도 쿠키는 이미 평문으로 한 번 나간 뒤다. 앞에서 본 “감춰지지 않는 것”에 쿠키까지 얹히는 셈이라, HSTS와 같은 문제를 다른 쪽에서 막는 장치다.
HTTP로 온 요청은 HTTPS로 돌린다. 그리고 그것만으로는 부족하다. 사용자가 주소창에 example.com만 치면 첫 요청은 평문으로 나간다. 그 한 번이 가로채기의 창이 된다. 그래서 Strict-Transport-Security(HSTS) 헤더로 “다음부터는 이 도메인에 무조건 HTTPS로 붙어라”를 브라우저에 기억시킨다.
HSTS는 기억시키는 게 요점이라 되돌리기가 느리다. 유효기간을 길게 걸어두면 그 기간 동안 브라우저는 그 도메인에 평문 접속을 거부한다. 인증서 준비가 안 된 하위 도메인까지 범위에 넣었다가 접속이 막히는 사고가 생길 수 있다. 짧은 기간으로 시작해서 늘려가는 편이 안전하다.
섞이면 경고가 뜬다. HTTPS 페이지 안에서 이미지나 스크립트를 http://로 불러오면 브라우저가 막거나 경고한다. 이걸 혼합 콘텐츠라고 부른다. 감싼 페이지 안에 안 감싼 자원이 하나라도 있으면 그 자원은 여전히 읽히고 고쳐질 수 있기 때문이다.
민감한 값을 URL에 넣지 않는다. 경로와 쿼리는 전송 중에는 감춰진다. 하지만 브라우저 기록, 서버 액세스 로그, 리퍼러 헤더에는 평문으로 남는다. 전송 구간을 감싸는 것과 저장되는 곳을 감추는 것은 다른 문제다.
정리
| HTTPS란 | HTTP를 TLS로 감싼 것. 위층의 HTTP는 그대로다 |
| 하는 일 셋 | 암호화 · 무결성 · 서버 신원 확인 |
| 인증서가 증명하는 것 | ”이 서버가 이 도메인의 주인이다” 하나뿐 |
| 자물쇠 | 사이트가 정직하다는 뜻이 아니다. 피싱 사이트도 받는다 |
| 감춰지는 것 | 경로 · 쿼리 · 헤더 · 쿠키 · 바디 · 상태 코드 |
| 안 감춰지는 것 | IP · DNS 조회 · SNI의 도메인 · 데이터 크기 · 시점 |
| 신뢰의 뿌리 | 내 기기가 믿는 루트 CA 목록. 여기가 뚫리면 전부 무너진다 |
| 끝나는 지점 | 대개 앞단에서 벗겨진다. 그 뒤는 별도 문제 |
한 문장으로 줄이면 이렇다. 무엇을 말했는지는 감추고, 누구와 이야기했는지는 못 감춘다.
여기까지 오면서 한 왕복을 뜯어보고, 상태를 어디 둘지 정하고, 연결을 아끼고, 안 보내는 법을 배우고, 감싸는 법까지 봤다. 그런데 이 모든 게 클라이언트가 먼저 묻는다는 전제 위에 서 있다. 서버에 새 소식이 생겨도 아무도 안 물어보면 전할 방법이 없다. 다음 글은 그 전제를 뒤집는 이야기다.