
사이트가 1초 만에 털렸다: HTTPS 강제와 HSTS
HTTP로 접속하는 사용자를 단순히 HTTPS로 리다이렉트하는 것만으로는 부족합니다. 중간자 공격(MITM)에 취약하기 때문이죠. 브라우저에게 '이 사이트는 무조건 HTTPS로만 접속해!'라고 각인시키는 HSTS(HTTP Strict Transport Security)의 원리와 적용 방법을 정리합니다.

HTTP로 접속하는 사용자를 단순히 HTTPS로 리다이렉트하는 것만으로는 부족합니다. 중간자 공격(MITM)에 취약하기 때문이죠. 브라우저에게 '이 사이트는 무조건 HTTPS로만 접속해!'라고 각인시키는 HSTS(HTTP Strict Transport Security)의 원리와 적용 방법을 정리합니다.
프론트엔드 개발자가 알아야 할 4가지 저장소의 차이점과 보안 이슈(XSS, CSRF), 그리고 언제 무엇을 써야 하는지에 대한 명확한 기준.

매번 3-Way Handshake 하느라 지쳤나요? 한 번 맺은 인연(TCP 연결)을 소중히 유지하는 법. HTTP 최적화의 기본.

클래스 이름 짓기 지치셨나요? HTML 안에 CSS를 직접 쓰는 기괴한 방식이 왜 전 세계 프론트엔드 표준이 되었는지 파헤쳐봤습니다.

HTTP는 무전기(오버) 방식이지만, 웹소켓은 전화기(여보세요)입니다. 채팅과 주식 차트가 실시간으로 움직이는 기술적 비밀.

HTTPS 인증서(SSL)를 샀습니다. Nginx 설정 파일에 Listen 443을 적었습니다.
브라우저 주소창에 초록색 자물쇠가 떴습니다.
"아, 이제 우리 사이트는 안전하구나! 고객님들의 소중한 정보를 훌륭하게 지켜냈어."
저는 발을 뻗고 잤습니다. 하지만 해커들은 잠들지 않더군요. 어느 보안 세미나에서 충격적인 시연을 봤습니다. 공용 와이파이(Starbucks WiFi)에서 해커가 제 사이트의 로그인 정보를 가로채는 데 걸린 시간은 단 1초였습니다. 분명히 HTTPS를 적용했는데 말이죠.
범인은 바로 "http://로 접속하는 첫 순간"이었습니다.
사용자들은 주소창에 https://naver.com이라고 꼼꼼하게 치지 않습니다.
그냥 naver.com이라고 치죠. 귀찮으니까요.
그러면 브라우저는 기본적으로 http://naver.com으로 접속을 시도합니다. (요즘 크롬은 https부터 시도하기도 하지만, 아직 완벽하지 않습니다.)
서버는 당연히 이렇게 응답합니다.
"어? HTTP로 오셨네요? 보안 때문에 HTTPS로 다시 오세요." (301 Moved Permanently)
사용자의 브라우저는 "아, 넵!" 하고 https://naver.com으로 다시 접속합니다.
이 과정은 0.1초 만에 일어나서 우리는 눈치채지 못합니다.
하지만 해커는 이 0.1초의 틈을 노립니다. 이를 SSL Stripping 공격이라고 합니다.
사용자가 처음 http://로 문을 두드리는 순간, 해커가 공유기(Router) 레벨에서 중간에 끼어들어 "내가 네이버 서버야" 하고 가짜 응답을 보내버리는 거죠.
사용자는 여전히 http:// 통신을 하고 있는데, 화면은 네이버 로그인 창과 똑같습니다. 아이디와 비번을 입력하면? 해커에게 고스란히 전송됩니다.
이 문제를 해결하려면 브라우저가 처음부터 아예 http로 접속 시도조차 하지 않게 만들어야 합니다. 그게 바로 HSTS (HTTP Strict Transport Security)입니다.
HSTS는 서버가 브라우저에게 보내는 강력한 경고장입니다.
"야, 잘 들어. 앞으로 내 사이트에 올 때는 무조건, 절대적으로 HTTPS로만 와. 혹시라도 사용자가 실수로
http://라고 쳐도, 네가 알아서https://로 바꿔서 와. 알겠지?"
이 경고장을 받은 브라우저는 내부 메모장(HSTS Cache)에 이렇게 적어둡니다.
naver.com = 무조건 HTTPS
이제 사용자가 주소창에 http://naver.com을 쳐도, 브라우저는 서버에 요청을 보내기도 전에 자체적으로(Internal Redirect) https://로 바꿔버립니다.
네트워크를 타기 전에 바꿔버리니 해커가 끼어들 틈조차 없는 것이죠.
HSTS를 적용하는 건 생각보다 쉽습니다. 응답 헤더에 한 줄만 추가하면 됩니다.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
하나씩 뜯어볼까요? 의미를 모르면 대형 사고를 칠 수 있습니다.
max-age=31536000"이 규칙을 1년(31,536,000초) 동안 기억해."
브라우저는 이 기간 동안 절대 HTTP로 접속하지 않습니다.
보통 1년(31536000)이나 2년(63072000)으로 설정합니다.
includeSubDomains"내 자식들(서브도메인)도 다 포함이야."
api.naver.com, blog.naver.com, test.naver.com 등 모든 서브도메인에 적용됩니다.
⚠️ 주의할 점: 혹시 사내 테스트 서버(dev.internal.naver.com)가 HTTPS 설정이 안 되어 있나요?
이 옵션을 켜는 순간, 그 테스트 서버도 접속이 불가능해집니다. 브라우저가 강제로 HTTPS로 연결하려고 하는데, 서버는 HTTPS를 안 받으니까 "연결 거부"가 뜨죠.
사내의 모든 서브도메인이 HTTPS를 지원하는지 반드시 확인하고 켜야 합니다.
preload"전 세계 브라우저의 소스코드에 내 이름을 박제해 줘." 이건 좀 무서운 옵션입니다. 아래에서 자세히 설명하죠.
HSTS에도 약점이 하나 있습니다. 바로 "최초의 접속(First Contact)"입니다. 브라우저가 경고장(HSTS 헤더)을 받으려면, 적어도 한 번은 접속을 해야 하잖아요? 그 첫 만남은 여전히 HTTP일 수 있고, 그때 공격당할 수 있습니다.
그래서 구글(Chrome) 팀이 "HSTS Preload List"라는 걸 만들었습니다. 이건 크롬 브라우저 설치 파일 안에 아예 하드코딩된 명단입니다.
"여기에 적힌 사이트들(facebook.com, naver.com, toss.im 등)은 태어날 때부터 HTTPS임. 토 달지 마."
여기에 내 사이트를 등록하면, 사용자가 태어나서 처음 내 사이트에 접속하더라도 브라우저는 HTTPS로 연결합니다. "맨 처음의 위험"까지 원천 봉쇄하는 거죠.
preload 옵션을 추가합니다.Preload 등록은 신중, 또 신중해야 합니다. 만약 실수로 등록했는데, 나중에 사정이 생겨서 HTTP를 다시 써야 한다면? (예: 레거시 시스템 연동) 못 씁니다. 절대 못 씁니다.
Preload 명단에서 삭제 신청을 해도, 전 세계 사용자의 브라우저가 업데이트될 때까지 수개월이 걸립니다. 그동안 사용자들은 "접속 불가" 화면만 보게 됩니다. 회사가 망할 수도 있는 문제입니다.
가장 일반적인 설정입니다. ssl_certificate 설정 아래에 추가하세요.
server {
listen 443 ssl http2;
server_name mysite.com;
ssl_certificate /etc/letsencrypt/live/mysite.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mysite.com/privkey.pem;
# 여기가 핵심! 항상(always) 헤더를 내려보냅니다.
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
}
Node.js 백엔드라면 helmet 미들웨어를 쓰는 게 가장 편하고 안전합니다.
const express = require('express');
const helmet = require('helmet');
const app = express();
// HSTS만 따로 설정하거나, helmet() 기본값 사용
app.use(helmet.hsts({
maxAge: 31536000, // 1년
includeSubDomains: true, // 서브도메인 포함
preload: true // 프리로드 리스트 신청 준비 완료
}));
"로컬 개발할 때도 HTTPS 써야 하나요?"
네, 쓰는 게 좋습니다. httpOnly 쿠키나 SameSite 설정 때문에 로컬에서도 HTTPS가 필요할 때가 많거든요.
하지만 localhost는 HSTS가 적용되지 않는 경우가 많습니다. 브라우저가 개발 편의를 위해 봐주거든요.
대신 mkcert를 써서 로컬 인증서를 발급받고, .dev 도메인(예: myapp.dev)을 /etc/hosts에 등록해서 쓰면 HSTS가 작동합니다. (.dev 도메인은 전체가 Preload List에 등록되어 있습니다!)
무턱대고 max-age=1년을 때려 박았다가 사이트가 먹통이 되면 곤란합니다.
저는 이렇게 점진적으로 적용했습니다.
max-age=300 (5분). 문제가 생기면 5분만 욕먹으면 됩니다. "아 잠시 서버 점검 중입니다"라고 둘러대세요.max-age=86400 (1일). 하루 동안 트래픽과 로그를 지켜봅니다. 서브도메인에서 에러가 안 나는지 체크합니다.max-age=31536000 (1년). 이제 뒤로 갈 수 없습니다.preload 옵션 추가 후 구글에 등록 신청. 이제 당신의 사이트는 전 세계적으로 "HTTPS 전용"으로 공인받았습니다.지피지기면 백전백승. 해커가 어떻게 여러분의 사이트를 터는지 알아야 막을 수 있습니다. 툴(Tool) 이름은 언급하지 않겠습니다만, 과정은 이렇습니다. (절대 따라하지 마세요, 철창갑니다.)
http://bank.com을 요청하는 순간을 딱 잡습니다.Release 버전으로 HTTPS 통신을 맺습니다. 하지만 피해자에게는 HTTPS를 벗겨내고(Strip) 그냥 HTTP로 응답을 줍니다.HSTS는 이 과정 중 2번을 원천 차단합니다. 브라우저가 아예 HTTP 요청을 안 보내니까요.
이거 확인 안 하고 적용하면 장애 보고서 쓰게 됩니다.
dev, stage, admin, legacy, jira 등 모든 서브도메인이 HTTPS 인증서를 가지고 있는가?example.com과 www.example.com 둘 다 커버가 되는가?preload 옵션을 켜세요)HTTPS가 느리다는 편견은 옛말입니다. TLS 1.3이 등장하면서 혁명이 일어났거든요.
보안은 더 이상 속도의 족쇄가 아닙니다.
HSTS보다 더 깊은 심연에는 Certificate Transparency (인증서 투명성)가 있습니다. 예전에는 해커가 몰래 "가짜 네이버 인증서"를 발급받으면, 브라우저는 속수무책이었습니다. CA(인증 기관)가 발급했으니까 믿을 수밖에요.
이제는 CT 로그라는 전 세계 공용 장부에 "나 인증서 발급했어!"라고 기록하지 않으면, 크롬이 "이 인증서는 장부에 없으므로 가짜다!"라고 차단해 버립니다.
우리가 브라우저를 켤 때마다, 크롬은 뒤에서 이 CT 로그를 확인하고 있습니다. 웹 보안은 이렇게 보이지 않는 곳에서 치열하게 발전하고 있습니다.
Q: HSTS를 설정하면 SEO(검색 엔진 최적화)에 문제없나요? A: 오히려 좋습니다. 구글은 HTTPS 사이트에 가산점을 줍니다. 그리고 301 리다이렉트를 통해 "이 사이트는 이제 HTTPS가 본진이야"라고 알려주므로, 검색 랭킹(Link Juice)도 안전하게 이전됩니다.
Q: 인증서가 만료되면 HSTS 사이트는 어떻게 되나요? A: 접속 불가 상태가 됩니다. 일반 사이트는 "안전하지 않음" 경고를 무시하고 들어갈 수 있지만, HSTS가 걸린 사이트는 "뒤로 가기" 버튼밖에 안 뜹니다. 그러니 인증서 자동 갱신(Certbot)은 생명줄입니다.
Q: 서브도메인 하나만 HTTP로 쓰고 싶은데...
A: includeSubDomains 옵션을 끄셔야 합니다. 하지만 보안상 권장하지 않습니다. 해커가 그 HTTP 서브도메인의 쿠키를 탈취해서 메인 도메인을 공격할 수 있거든요.
보안의 제1원칙은 "사용자를 믿지 마라"입니다.
"사용자가 알아서 HTTPS로 들어오겠지"라고 믿는 건 직무유기입니다.
사용자는 귀찮아서 http://도 안 치고 그냥 naver.com만 칩니다.
HSTS는 서버가 할 수 있는 가장 강력한 안전벨트입니다.
지금 바로 여러분의 사이트 헤더를 개발자 도구의 Network 탭에서 확인해 보세요.
Strict-Transport-Security가 없다면, 여러분의 문은 반쯤 열려있는 겁니다.
해커가 그 틈을 비집고 들어오기 전에, 단단히 잠그시길 바랍니다.