preload가 일반 HSTS 헤더에 더해 주는 것

Strict-Transport-Security 헤더는 브라우저에 “앞으로 max-age초 동안 이 호스트와는 HTTPS로만 통신하라”고 알려 줍니다. http://를 입력하거나 예전 링크를 눌러도, 브라우저가 요청이 기기를 떠나기 전에 스스로 업그레이드합니다. 인증서 오류도 ‘계속 진행’할 수 있는 경고가 아니라 넘어갈 수 없는 실패가 됩니다.

빈틈은 첫 방문입니다. 내 헤더를 한 번도 본 적 없는 브라우저는 사용자가 입력하거나 누른 대로 첫 요청을 평문 HTTP로 보내고, 네트워크상의 공격자는 그 한 번의 요청을 가로채 사용자를 HTTP에 묶어 둘 수 있습니다. 방문자가 max-age가 끝나기 전에 다시 오지 않으면 같은 빈틈이 다시 열립니다.

preload 목록이 그 빈틈을 막습니다. Chromium 소스 코드에 들어가는 도메인 목록입니다. 크롬과 다른 Chromium 기반 브라우저는 이 목록을 그대로 쓰고, 파이어폭스와 사파리는 이를 바탕으로 자체 목록을 관리합니다. 브라우저는 설치된 순간부터 목록에 있는 도메인을 HTTPS 전용으로 다루므로 첫 요청이 필요 없습니다.

이점은 그게 전부입니다. 첫 요청입니다. 나머지는 헤더가 이미 해 줍니다.

애초에 해야 하나?

제 의견은 이렇습니다. HTTPS 사이트라면 어디든 헤더는 보내세요. 가능하면 긴 max-age와 includeSubDomains까지 넣습니다. preload는 아래 세 가지가 모두 맞을 때만 하세요.

  • 모든 서브도메인을 포함해 도메인 전체가 지금 HTTPS 전용이고, 앞으로도 그렇게 유지됩니다.
  • 내부용을 포함해 모든 서브도메인을 관리하거나, 적어도 전부 파악할 수 있습니다.
  • 첫 요청부터 보호할 가치가 있는 공격 대상입니다. 은행, 메일, 관리 콘솔처럼 공격자가 가로챌 이유가 있는 로그인이 있는 곳입니다.

하나라도 흔들리면 preload의 이득은 작고, 손해는 빨리 되돌릴 수 없다는 것입니다.

요건, 정확하게

hstspreload.org가 제시하는 요건이고, 제출 폼이 자동으로 확인합니다.

  1. 유효한 인증서를 제공합니다.
  2. 80번 포트를 연다면 HTTP를 같은 호스트의 HTTPS로 리다이렉트합니다. http://example.com은 https://www.example.com으로 바로 가지 말고 먼저 https://example.com으로 가야 합니다. 그래야 브라우저가 apex에서 HSTS 헤더를 봅니다.
  3. 모든 서브도메인을 HTTPS로 제공합니다. 특히 www에 DNS 레코드가 있다면 www가 HTTPS로 동작해야 합니다.
  4. HTTPS 기본 도메인에서 HSTS 헤더를 보냅니다. 조건은 다음과 같습니다.
    • max-age가 31536000(1년) 이상
    • includeSubDomains
    • preload https://example.com이 다른 곳으로 리다이렉트한다면, 그 리다이렉트 응답 자체에 헤더가 있어야 합니다.

제출할 수 있는 것은 등록 가능한 도메인뿐이고(shop.example.com이 아니라 example.com), 그 항목은 아래의 모든 이름에 적용됩니다.

지금 상태 확인하기

# 1. HTTP는 같은 호스트의 HTTPS로 리다이렉트돼야 함
curl -s -o /dev/null -D - http://example.com/ | grep -iE '^(HTTP/|location:)'
# HTTP/1.1 301 Moved Permanently
# Location: https://example.com/

# 2. HTTPS apex는 (리다이렉트하더라도) 헤더를 보내야 함
curl -s -o /dev/null -D - https://example.com/ | grep -iE '^(HTTP/|location:|strict-transport-security:)'
# HTTP/2 301
# location: https://www.example.com/
# strict-transport-security: max-age=63072000; includeSubDomains; preload

# 3. www는 HTTPS로 동작해야 함
curl -sI https://www.example.com/ | grep -i '^HTTP/'

HTTP 점검은 리다이렉트 체인과 최종 페이지가 보내는 HSTS 헤더를 보여 줍니다. SSL 점검도 HSTS 헤더를 읽어 등급에 반영합니다(자세한 내용은 이 페이지 끝에 있습니다). 둘 다 도메인이 실제로 preload 목록에 있는지는 알려 주지 않습니다. 그건 hstspreload.org의 상태 조회로 확인하세요.

진짜 위험: 모든 서브도메인, 영원히

preload된 항목의 includeSubDomains는 지금과 앞으로의 모든 하위 이름이, 목록을 담은 모든 브라우저에서 HTTPS 전용이 된다는 뜻입니다. 잊고 있던 이름도 포함됩니다.

  • 사내 DNS에서 HTTP로 운영되는 위키를 가리키는 intranet.example.com
  • printer.office.example.com에 있는 프린터, NAS, 공유기, 카메라 관리 페이지
  • 개발자 PC에서 자체 서명 인증서로 도는 dev.example.com
  • 어딘가에서 HTTP로 리다이렉트하는, 외부 업체가 운영하는 status.example.com이나 mail.example.com
  • 이 글을 읽지 않은 누군가가 내년에 새로 만들 무언가

그런 이름에서 브라우저는 평문 HTTP를 거부합니다. 인증서가 자체 서명이거나 만료됐거나 이름이 다르면, HSTS가 금지하므로 ‘계속 진행’ 버튼이 없는 오류 페이지가 뜹니다. 해결책은 그 호스트에 유효한 인증서를 주는 것뿐이고, 그때까지는 브라우저로 접속할 수 없습니다.

제출 전에 목록을 만드세요.

  • 공개 DNS 존을 내보내서 이름마다 HTTPS로 확인합니다.
  • 공인 인증서를 받은 적 있는 이름은 인증서 투명성(CT) 로그에 남습니다. 서브도메인 조회가 CT 로그에서 그 목록을 보여 줍니다.
  • 그다음 사내 DNS를 확인합니다. CT 로그에는 공인 인증서를 받은 이름만 보이는데, preload가 망가뜨리는 HTTP 전용 내부 호스트가 바로 그런 인증서를 받은 적 없는 이름들입니다.

HTTPS 전용으로 만들 수 없는 서브도메인이 있다면 이 도메인은 preload하지 마세요. includeSubDomains 없이 헤더만 쓰거나, 그 서비스를 다른 도메인 아래로 옮기세요.

max-age는 한 번에 올리지 말고 단계적으로

실수가 빨리 만료되도록 짧게 시작해서, 아무것도 깨지지 않으면 늘립니다. hstspreload.org가 권하는 단계입니다.

max-age=300; includeSubDomains          # 5분
max-age=604800; includeSubDomains       # 1주
max-age=2592000; includeSubDomains      # 1개월
max-age=63072000; includeSubDomains; preload   # 2년, 그다음 제출

실제 사용자들이 모든 서브도메인을 한 번씩 거칠 만큼 각 단계에 머물고, 내부 서비스에 “접속이 안 된다”는 문의가 오는지 지켜보세요. 최종값으로는 흔히 2년을 권하고, preload의 최소 요건은 1년(31536000)입니다.

어느 단계에서 문제가 생기면 HTTPS로 max-age=0을 보내 브라우저가 그 호스트의 동적 HSTS 항목을 잊게 할 수 있습니다. 헤더로 배운 것에만 통하고, preload 항목에는 통하지 않습니다.

헤더 설정하기

nginx:

add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

always는 오류 응답에도 헤더를 붙입니다. nginx의 상속 규칙을 조심하세요. location 블록에 자기 add_header가 하나라도 있으면 server 블록의 add_header를 상속하지 않아서, 그 URL들에서는 HSTS 헤더가 조용히 사라집니다. nginx -T | grep -n add_header로 확인하고, 경로 몇 개를 curl로 시험해 보세요.

Apache(mod_headers):

Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"

HTTPS 가상 호스트에 넣으세요. 브라우저는 평문 HTTP로 받은 헤더를 무시하므로, 80번 포트에서 보내 봐야 소용없습니다.

Cloudflare에서는 SSL/TLS → Edge Certificates에 HSTS 설정이 있고, max-age, includeSubDomains, preload를 각각 켭니다. 존의 모든 호스트명에 적용되므로 주의할 점은 똑같습니다.

제출, 그리고 그다음

hstspreload.org에 기본 도메인을 제출합니다. 폼이 실제 사이트를 대상으로 모든 요건을 다시 확인하고, 무엇이 안 맞는지 정확히 알려 줍니다. 승인되면 도메인이 Chromium의 목록에 추가되고, 그 목록을 담은 브라우저 릴리스가 나와야 사용자에게 적용됩니다. 보통 몇 주에서 몇 달이 걸리고, 다른 브라우저는 각자 일정에 따라 반영합니다.

등록 뒤에도 preload가 든 헤더를 계속 보내세요. hstspreload.org는 요건을 계속 충족해야 한다고 안내합니다.

어떤 최상위 도메인은 통째로 preload돼 있습니다. 예를 들어 구글의 .app과 .dev가 그렇습니다. 그 아래의 이름은 제출하든 안 하든 이미 HTTPS 전용입니다.

제거는 느립니다. 영구적이라고 생각하세요

목록에서 빠져야 한다면 헤더에서 preload를 지우고 hstspreload.org/removal에 도메인을 제출합니다. 그다음 제거도 등록 때처럼 브라우저 릴리스에 실려야 하므로 몇 달이 걸립니다. 예전 버전 브라우저를 쓰는 사용자는 업데이트할 때까지 예전 목록을 그대로 쓰고, 끝내 업데이트하지 않는 사람도 있습니다. 그동안 HTTP 전용 서브도메인은 그 사용자들에게 계속 접속 불가입니다.

이 체크리스트가 있는 진짜 이유가 이것입니다. preload는 몇 분이면 걸지만, 되돌리는 일은 내 일정대로 되지 않습니다.

내 브라우저에서 확인하기

크롬의 chrome://net-internals/#hsts(엣지는 edge://net-internals/#hsts)에서 도메인을 조회할 수 있습니다. preload된 도메인은 static_upgrade_mode: FORCE_HTTPS로, 헤더로 배운 도메인은 dynamic_upgrade_mode 항목으로 나옵니다. 같은 페이지의 “Delete domain security policies”는 동적 항목만 지웁니다. preload 항목은 로컬에서 지울 수 없습니다.

DechoNet이 HSTS를 채점하는 방식

SSL 점검은 100점 중 20점을 HSTS에 배정하고, preload 목록이 아니라 HTTPS 사이트가 보내는 헤더로 채점합니다.

헤더점수
max-age ≥ 31536000, includeSubDomains와 preload 포함20
max-age ≥ 31536000, includeSubDomains 포함15
max-age ≥ 31536000만10
그보다 짧은 max-age5
헤더 없음0

그래서 일부러 preload를 하지 않기로 했어도 preload 지시어가 없으면 5점이 깎이고, preload된 도메인이라도 헤더를 보내지 않으면 20점을 모두 잃습니다. HTTP 점검은 HSTS를 따로, 채점하는 보안 헤더 6개 중 하나로 다루고 max-age가 1년 미만이면 약함으로 표시합니다. 나머지 헤더는 HTTP 보안 헤더 가이드에서 다룹니다.

내 도메인에서 바로 확인

무료, 가입 불필요. 이 가이드가 다루는 항목을 바로 검사하고 조치 방법을 알려드립니다.