지금까지 TLS 핸드셰이크부터 키 교환, 인증서 검증, Let's Encrypt 자동 갱신까지 HTTPS의 안쪽을 차근차근 파봤습니다. 그런데 한 가지 어이없는 함정이 남아 있어요. "HTTPS를 아무리 잘 깔아놔도, 사용자가 처음에 http://example.com으로 들어오면?" 그 순간 중간자가 가로채서 영원히 평문 사이트로 안내해 버릴 수 있습니다. 이걸 SSL Stripping 공격이라고 해요.
저도 예전에 "어차피 80 포트는 443으로 리다이렉트하니까 안전하지" 하고 넘겼다가, 보안 점검에서 HSTS 미설정으로 빨간불이 떴습니다. 오늘은 HTTPS를 정말 강제하는 HSTS와, 함께 챙겨야 할 핵심 보안 헤더들을 한 번에 정리해 보겠습니다.
개념 / 원리
HSTS, "이 도메인은 무조건 HTTPS"
HSTS(HTTP Strict Transport Security)는 서버가 응답 헤더로 "앞으로 N초 동안 이 도메인은 무조건 HTTPS로만 접속해"라고 브라우저에 선언하는 메커니즘입니다 (RFC 6797).
브라우저는 이 헤더를 받으면 다음 접속부터:
- 사용자가
http://로 입력하거나 평문 링크를 눌러도 → 브라우저가 즉시 자체적으로https://로 바꿔서 요청. - 인증서 오류가 있어도 "그래도 접속" 버튼이 사라지고 강제 차단.
헤더 모양은 이렇습니다.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
| 지시자 | 의미 |
|---|---|
max-age=N |
N초 동안 HTTPS 강제 (보통 1년 = 31536000) |
includeSubDomains |
모든 서브도메인까지 적용 |
preload |
브라우저에 미리 박힌 HSTS 목록 등재 의사 |
HSTS Preload, 최초 접속의 빈틈도 메우기
문제는 처음 접속할 때입니다. 그 한 번은 HSTS 헤더를 받기 전이라 평문일 수밖에 없어요. 이걸 메우려고 만든 게 HSTS Preload List. 주요 브라우저(크롬·파이어폭스·사파리·엣지)에 미리 박힌 채로 배포되는 도메인 목록입니다. 등록되면 "한 번도 접속한 적 없는 사용자조차" 처음부터 HTTPS로 강제됩니다.
등록은 https://hstspreload.org 에서 합니다. 조건이 까다롭고, 한 번 등록되면 빠지는 데 몇 달이 걸립니다. 신중하게.
같이 챙겨야 할 보안 헤더 4종
HSTS 하나로는 부족합니다. 보통 같이 세트로 가는 헤더들.
| 헤더 | 역할 |
|---|---|
Content-Security-Policy |
XSS 방어. 어떤 출처의 스크립트·이미지·iframe을 허용할지 |
X-Content-Type-Options: nosniff |
브라우저의 MIME 추측 차단 (스크립트로 오해받는 사고 방지) |
X-Frame-Options: SAMEORIGIN |
다른 사이트가 iframe으로 내 페이지 못 띄우게 (클릭재킹 방어) |
Referrer-Policy: strict-origin-when-cross-origin |
다른 사이트로 이동 시 Referer 노출 최소화 |
실전 예제
1) nginx에 HSTS + 보안 헤더 한 번에
server {
listen 443 ssl http2;
server_name example.com;
# HSTS - 1년, 서브도메인 포함, preload 신청 의사
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# 핵심 보안 헤더 세트
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; img-src 'self' data:; script-src 'self'" always;
# 나머지 ssl_ 설정...
}
# 80 포트는 무조건 301 리다이렉트
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
always 플래그를 빼먹으면 4xx·5xx 응답에는 헤더가 안 붙습니다. 항상 붙여 두세요.
2) 헤더가 잘 붙는지 curl로 확인
curl -sI https://example.com \
| grep -iE "strict-transport|content-security|x-frame|x-content-type|referrer"
기대 출력:
strict-transport-security: max-age=31536000; includeSubDomains; preload
content-security-policy: default-src 'self'; img-src 'self' data:; script-src 'self'
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
3) Preload 등록 전 단계적 적용
preload는 되돌리기 어렵기 때문에 단계적으로 가는 게 안전합니다.
# 1단계 (며칠): 짧게 가서 문제 없는지 확인
add_header Strict-Transport-Security "max-age=300" always;
# 2단계 (몇 주): 한 달짜리로
add_header Strict-Transport-Security "max-age=2592000; includeSubDomains" always;
# 3단계 (안정 후): 1년 + preload, 등록 신청
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
각 단계마다 모든 서브도메인이 HTTPS로 잘 뜨는지 확인. 한 번이라도 HTTPS 못 뜨는 서브도메인이 있으면 사용자가 그 사이트에 영영 못 들어가게 됩니다.
4) CSP는 Report-Only로 시작
CSP는 잘못 잡으면 멀쩡한 스크립트도 다 차단됩니다. 처음엔 차단 없이 보고만 받는 모드로 시작.
add_header Content-Security-Policy-Report-Only
"default-src 'self'; script-src 'self'; report-uri /csp-report" always;
며칠 로그를 보고 합법적인 외부 리소스만 허용 리스트에 추가한 뒤, -Report-Only를 떼고 진짜 차단 모드로 전환합니다.
자주 하는 실수
- HSTS 켜놓고 인증서 만료: 사용자가 한 번이라도 접속한 적이 있으면, 인증서 만료 후에는 "그래도 접속" 버튼조차 안 뜹니다. 사이트가 사실상 다운된 것과 같아요. HSTS와 인증서 자동 갱신은 반드시 세트.
- HSTS를 끄는 절차를 모름: 헤더만 빼면 이미 캐시된 브라우저는 여전히 강제됩니다. 정말 끄려면
max-age=0을 일정 기간 응답해서 캐시를 비워야 합니다. - includeSubDomains 영향 범위 미파악:
*.example.com안에 HTTPS 안 되는 내부 서비스가 하나라도 있으면, includeSubDomains를 켠 순간 그 서비스는 사용자 브라우저에서 못 열립니다. 전수조사 후 적용. - Preload를 가볍게 등록: 한 번 등록되면 빼는 데 몇 달. 등록 전 staging 도메인이나 짧은 max-age로 충분히 검증.
- CSP를 처음부터 strict하게: 멀쩡한 페이지가 다 깨집니다. Report-Only로 며칠 로그 본 다음 차단 모드로.
- X-XSS-Protection 헤더를 아직도 사용: 옛날 헤더라 요즘 브라우저는 무시하거나 오히려 권장하지 않습니다. CSP로 대체.
저는 예전에 한 사내 서비스에서 HSTS preload까지 신청해 놨다가, 내부망 전용 서브도메인이 HTTPS를 안 받쳐서 직원들이 그 사이트에 못 들어가는 사고를 본 적이 있습니다. preload는 정말 마지막 단계에서, 모든 서브도메인을 다 확인한 다음에.
마무리
정리하면 세 줄입니다.
- HSTS는 브라우저에 "이 도메인은 무조건 HTTPS"를 못 박는 헤더. preload는 최초 접속의 빈틈까지 메운다.
- 보안 헤더는 HSTS + CSP + nosniff + X-Frame-Options + Referrer-Policy 세트로 같이 챙긴다.
- 인증서 자동 갱신과 HSTS는 반드시 세트. 인증서 만료 = 사이트 다운이라는 점만 잊지 말 것.
'보안' 카테고리의 다른 글
| OAuth 2.0 Authorization Code Grant - 구글 로그인 뒤에서 일어나는 일 (0) | 2026.06.05 |
|---|---|
| CSRF와 SameSite 쿠키 - 토큰 없이도 막을 수 있을까 (0) | 2026.05.25 |
| [보안] Let's Encrypt와 ACME 프로토콜 - 인증서 자동 발급과 갱신의 동작 원리 (0) | 2026.05.21 |
| [보안] 인증서 검증의 전 과정 - 체인 추적부터 OCSP·CRL 폐기 확인까지 (0) | 2026.05.20 |
| [보안] TLS 키 교환의 비밀 - 대칭키와 비대칭키는 어떻게 협력할까 (0) | 2026.05.19 |