보안

[보안] HSTS와 보안 헤더 - HTTPS를 정말 강제하는 방법

jmineekim 2026. 5. 22. 10:00

지금까지 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는 반드시 세트. 인증서 만료 = 사이트 다운이라는 점만 잊지 말 것.
반응형