HSTS max-age=0의 함정 — 보안 헤더가 있어도 없는 셈인 경우
보안 헤더를 점검할 때 가장 흔한 실수는 "헤더가 존재하는지"만 확인하고 끝내는 것입니다. 대부분의 헤더는 존재 여부만으로 충분하지만, Strict-Transport-Security(HSTS)는 예외입니다. 이 헤더는 값에 따라 정반대의 의미를 가질 수 있어서, "있으니까 안전하다"는 직관이 틀리는 대표적인 케이스입니다. 이 가이드는 왜 그런지, 그리고 이걸 자동으로 걸러내려면 무엇을 확인해야 하는지 정리합니다.
1. HSTS 헤더가 하는 일
HSTS는 Strict-Transport-Security: max-age=31536000; includeSubDomains처럼 응답에 포함시키는 헤더로, 브라우저에게 "앞으로 max-age초 동안 이 도메인은 무조건 HTTPS로만 접속하라"고 지시합니다. 사용자가 실수로 http://로 접속을 시도해도 브라우저가 서버에 요청을 보내기 전에 자체적으로 https://로 바꿔버리기 때문에, 중간자 공격(다운그레이드 공격)을 원천 차단하는 효과가 있습니다. max-age 값은 초 단위이며 1년은 31536000초입니다.
2. max-age=0의 진짜 의미
여기서 함정이 시작됩니다. HSTS 스펙인 RFC 6797은 max-age=0을 단순히 "짧은 유효기간"이 아니라 HSTS 정책을 즉시 해제하라는 명시적 신호로 정의합니다. 즉 서버가 Strict-Transport-Security: max-age=0을 보내면, 브라우저는 그 순간부터 해당 도메인을 더 이상 HTTPS 전용으로 취급하지 않습니다. 헤더 자체는 응답에 분명히 포함되어 있지만, 실질적인 보호 효과는 0입니다. 이 값은 실무에서 실수로 나오는 게 아니라, HSTS를 의도적으로 해제하고 싶을 때(예: 서브도메인 중 하나가 아직 HTTPS 인증서를 준비 못 했을 때) 공식적으로 쓰라고 만들어진 값입니다.
3. "헤더가 있다"는 착각이 위험한 이유
보안 점검 스크립트나 체크리스트가 헤더의 존재 여부만 grep으로 확인하는 방식이라면, max-age=0인 응답도 "HSTS 적용됨: 통과"로 오판합니다. 실제로는 그 사이트가 HTTPS 강제라는 보호를 전혀 받지 못하고 있는데도 말입니다. 이런 오판은 특히 다음 상황에서 실제 사고로 이어질 수 있습니다.
- 인증서 문제 대응 중 남은 설정: SSL 인증서 만료나 갱신 실패로 임시로 HSTS를 max-age=0으로 낮춰 HTTP 폴백을 허용해뒀다가, 문제 해결 후 원래 값으로 되돌리는 걸 잊는 경우.
- 서브도메인 전환 도중: includeSubDomains를 쓰던 상위 도메인에서 특정 서브도메인만 HTTPS 준비가 안 돼 전체를 max-age=0으로 내렸다가, 그대로 방치되는 경우.
- 자동화 점검 도구의 오탐: "헤더 존재 = 통과"로만 판정하는 스캐너를 쓰면 이런 상태를 계속 놓치게 됩니다.
4. 값까지 검사하는 방법
HTTP 헤더 검사기는 이 문제를 피하기 위해 HSTS를 단순 존재 여부가 아니라 max-age 값까지 파싱해서 판정합니다. 정규식으로 max-age=숫자 부분을 추출한 뒤 그 숫자가 0보다 큰 경우에만 "존재(적용됨)"로 표시하고, max-age=0이거나 max-age 자체가 없으면 값이 아무리 존재해도 "누락"으로 분류합니다. 다른 보안 헤더(CSP, X-Frame-Options 등)는 값의 세부 내용과 무관하게 존재 자체만으로 통과 처리되지만, HSTS만큼은 이 예외 로직이 적용되는 것도 그래서입니다.
| 실제 응답 헤더 | 헤더 존재? | 실질적 보호 | 올바른 판정 |
|---|---|---|---|
| Strict-Transport-Security: max-age=31536000 | 예 | 1년간 HTTPS 강제 | 적용됨 |
| Strict-Transport-Security: max-age=0 | 예 | 없음(즉시 해제) | 누락과 동일 |
| (헤더 자체가 없음) | 아니오 | 없음 | 누락 |
5. 점검 체크리스트
- curl -I 또는 브라우저 개발자도구에서 Strict-Transport-Security 값을 직접 확인하고, max-age 뒤 숫자를 반드시 읽습니다.
- 운영 환경이라면 max-age는 최소 6개월(15768000) 이상, 권장은 1년(31536000)입니다.
- 임시로 HSTS를 내려야 하는 상황이 끝나면 반드시 원래 max-age 값으로 복원하는 절차를 배포 체크리스트에 포함시킵니다.
- Apache/Nginx 설정에서 헤더를 하드코딩했다면 Apache 설정 생성기나 Nginx 설정 생성기로 표준 값(1년)을 다시 생성해 대조합니다.
- CSP 등 다른 보안 헤더도 함께 놓치지 않았는지 CSP 생성기로 교차 확인합니다.
자주 묻는 질문
Q. max-age=0을 일부러 써야 하는 경우도 있나요?
네. HTTPS 마이그레이션을 되돌려야 하거나 서브도메인의 인증서 문제로 일시적으로 HTTP 접속을 허용해야 할 때 RFC 6797이 공식적으로 정의한 해제 방법입니다. 다만 임시 조치이므로 원인이 해결되면 즉시 원래 값으로 되돌려야 합니다.
Q. max-age 값이 아예 없이 헤더만 있는 경우는요?
Strict-Transport-Security 헤더에 max-age 지시어가 없으면 스펙상 유효하지 않은 헤더로 취급되어 브라우저가 무시합니다. 이 경우도 실질적으로는 HSTS 미적용과 같습니다.
Q. 다른 보안 헤더도 값까지 확인해야 하나요?
X-Frame-Options나 X-Content-Type-Options처럼 대부분 헤더는 존재 자체가 곧 보호를 의미하지만, HSTS는 max-age라는 숫자 하나로 "켜짐/꺼짐"이 완전히 바뀌는 특수한 구조라 별도로 값을 확인해야 합니다.
Q. HSTS를 처음 적용할 때 주의할 점은?
includeSubDomains를 함께 켜면 모든 서브도메인이 HTTPS 준비가 되어 있어야 합니다. 준비 안 된 서브도메인이 있으면 그 서브도메인이 완전히 접속 불가 상태가 될 수 있으므로, 먼저 짧은 max-age로 테스트한 뒤 점진적으로 늘리는 것이 안전합니다.