스스로를 잠가 버리지 않고 HSTS 설정하기
하나의 잘못된 인증서가 장애로 이어지지 않도록 하면서 개인정보 보호를 강화하는 Strict-Transport-Security의 단계적이고 되돌릴 수 있는 배포 계획.
목차
- HSTS는 단순하지만, 늘 그런 것은 아닙니다
- HSTS 헤더가 실제로 하는 일
- 피해야 할 잠금 시나리오
- 1. 잊힌 하위 도메인이 HTTPS를 준비하지 못한 경우
- 2. 인증서가 만료되는 경우
- 3. 스테이징 또는 내부 도구가 프로덕션 도메인 아래에 있는 경우
- 4. Preload를 일상적인 체크박스로 취급하는 경우
- 안전한 배포 계획
- Step 1: 관리하는 모든 호스트 이름을 감사하세요
- Step 2: HSTS를 추가하기 전에 HTTPS를 고치세요
- Step 3: 매우 짧은 max-age로 시작하세요
- Step 4: 점진적으로 늘리세요
- Step 5: 감사가 실제로 끝난 뒤에만 includeSubDomains를 추가하세요
- Step 6: preload는 별도 프로젝트로 다루세요
- 구성 예시
- Nginx
- Apache
- CDN 또는 edge platform
- HSTS를 안전하게 되돌리는 방법
- 배포 전 테스트 체크리스트
- HSTS의 개인정보 보호 측면
HSTS는 단순하지만, 늘 그런 것은 아닙니다
보통 HSTS로 줄여 부르는 HTTP Strict Transport Security는 브라우저에 “이 사이트에서는 항상 HTTPS를 사용하라”고 알려줍니다. 브라우저가 유효한 HTTPS 연결을 통해 이 헤더를 받으면, 지정한 기간 동안 그 규칙을 기억합니다.
이는 유용합니다. 프로토콜 다운그레이드 공격을 막고, 우발적인 비보안 요청을 줄이며, 사용자가 example.com을 입력했을 때 리디렉션되기 전에 잠깐 일반 HTTP를 거치는 어색한 순간을 피하게 해줍니다.
하지만 이 규칙은 끈질기게 남습니다. 잘못된 HSTS 정책을 게시하면 서버에서 헤더를 제거한 뒤에도 브라우저가 오랫동안 계속 강제할 수 있습니다. 팀이 스스로를 잠가 버리는 방식이 바로 이것입니다. 정확히 말하면 자체 관리자 패널에서 쫓겨나는 것이 아니라, 사용자의 브라우저, 하위 도메인, 스테이징 시스템, 레거시 엔드포인트, 강제 HTTPS를 아직 준비하지 못한 잊힌 서비스에서 문제가 생깁니다.
목표는 HSTS를 피하는 것이 아닙니다. 목표는 토글처럼이 아니라 마이그레이션처럼 배포하는 것입니다.
HSTS 헤더가 실제로 하는 일
일반적인 HSTS 헤더는 다음과 같습니다.
Strict-Transport-Security: max-age=31536000; includeSubDomains
여기에는 중요한 부분이 세 가지 있습니다.
max-age: 브라우저가 이 호스트에 대해 HTTPS를 강제해야 하는 시간(초)입니다.includeSubDomains: 이 규칙을 모든 하위 도메인에도 적용할지 여부입니다.preload: 해당 도메인을 브라우저 preload 목록에 포함하고 싶다는 신호입니다.
브라우저는 이 헤더를 유효한 HTTPS를 통해 받았을 때만 신뢰합니다. 인증서가 유효하지 않거나, 만료되었거나, 일치하지 않으면 브라우저는 그 응답에서 새 HSTS 정책을 받아들이지 않아야 합니다.
정책이 저장되면 이후 http://example.com 방문 시도는 요청이 전송되기 전에 브라우저가 https://example.com으로 업그레이드합니다. 이것이 개인정보 보호상의 이점입니다. 비보안 요청이 기기를 떠나지 않습니다.
피해야 할 잠금 시나리오
대부분의 HSTS 실패는 메인 웹사이트 때문에 발생하지 않습니다. 경계에서 발생합니다.
1. 잊힌 하위 도메인이 HTTPS를 준비하지 못한 경우
includeSubDomains는 깔끔해 보이지만 절대적입니다. example.com에 설정하면 다음에 모두 적용됩니다.
www.example.comapi.example.comold-crm.example.comprinter-setup.example.comstaging.example.com- 그 도메인 아래의 그 밖의 모든 것
이 호스트 중 하나라도 유효한 HTTPS를 제공할 수 없다면, HSTS 정책이 캐시된 사용자는 HTTP로 접근할 수 없게 됩니다.
2. 인증서가 만료되는 경우
HSTS가 없으면 사용자가 인증서 경고를 클릭해 넘어가는 일이 때때로 있습니다. 좋은 보안 관행은 아니지만 실제로 일어납니다.
HSTS가 있으면 최신 브라우저는 해당 호스트의 인증서 오류를 쉽게 우회하도록 허용하지 않습니다. 이것이 핵심입니다. 동시에 인증서 갱신이 지루할 만큼 안정적이고, 모니터링되며, 테스트되어야 한다는 뜻이기도 합니다.
3. 스테이징 또는 내부 도구가 프로덕션 도메인 아래에 있는 경우
내부 도구를 *.example.com 아래에 두면 부모 도메인이 includeSubDomains를 사용할 때 고통스러워질 수 있습니다. 해당 도구가 자체 서명 인증서, 사설 인증 기관, 오래된 TLS 구성 또는 HTTPS 없음에 의존한다면 HSTS가 그 지름길을 드러냅니다.
많은 팀이 내부 및 실험 시스템을 자체 보안 정책을 가진 별도 도메인 아래에 두는 이유 중 하나가 이것입니다.
4. Preload를 일상적인 체크박스로 취급하는 경우
HSTS preload는 단순한 또 하나의 지시자가 아닙니다. 사용자가 사이트를 방문하기 전부터 도메인이 브라우저 안에 HTTPS 전용으로 포함될 수 있다는 뜻입니다.
이는 “첫 방문”의 공백을 닫아 주지만, 되돌리기는 훨씬 어렵습니다. preload 목록에서 제거해도 브라우저 릴리스 주기에 따라 사용자에게 도달하기까지 몇 주 또는 몇 달이 걸릴 수 있습니다. Preload는 안정적이고 성숙한 도메인에 적합합니다. 아직 하위 도메인 인벤토리를 파악하는 중인 사이트에는 적합하지 않습니다.
안전한 배포 계획
Step 1: 관리하는 모든 호스트 이름을 감사하세요
includeSubDomains를 설정하기 전에 해당 도메인 아래의 모든 호스트 이름을 나열하세요. DNS 레코드는 출발점이지만 전부는 아닙니다. CDN 구성, 호스팅 대시보드, 이메일 관련 호스트 이름, 오래된 마케팅 도구, 스토리지 버킷, 내부 문서를 확인하세요.
각 호스트 이름에 대해 다음에 답하세요.
- HTTP, HTTPS 또는 둘 다 제공하나요?
- HTTPS 인증서가 유효하고 자동으로 갱신되나요?
- HTTP가 HTTPS로 깔끔하게 리디렉션되나요?
- 공개용인가요?
- 아직 필요한가요?
팀에 이미 프로덕션 헤더 디버깅 습관이 있다면, 이는 리디렉션 및 헤더 점검과 자연스럽게 맞물립니다. 이 워크플로는 프로덕션에서 리디렉션과 HTTP 헤더를 디버깅하기 위한 작은 툴킷에서 다룬 바 있습니다.
Step 2: HSTS를 추가하기 전에 HTTPS를 고치세요
HSTS는 망가진 HTTPS 설정을 안전하게 만들지 않습니다. HTTPS를 필수로 만들 뿐입니다.
활성화하기 전에 다음을 확인하세요.
- TLS 인증서가 올바른 호스트 이름을 포함합니다.
- 인증서가 자동으로 갱신됩니다.
- 가능하면 HTTP가 HTTPS로 한 번의 깔끔한 홉으로 리디렉션됩니다.
- 표준 호스트 리디렉션이 일관됩니다. 예를 들어 non-
www에서www로, 또는 그 반대입니다. - 애플리케이션 자산이 비보안
http://URL에 의존하지 않습니다.
혼합 콘텐츠는 예전보다 덜 흔하지만, 오래된 CMS 테마, 분석 스니펫, 임베드된 미디어, 하드코딩된 이미지 경로에는 여전히 나타납니다.
Step 3: 매우 짧은 max-age로 시작하세요
1년으로 시작하지 마세요. 5분으로 시작하세요.
Strict-Transport-Security: max-age=300
이를 테스트 중인 호스트 이름에만 배포하세요. 보통 표준 프로덕션 웹사이트입니다. 지금은 includeSubDomains를 제외하세요.
그런 다음 실제 브라우저와 명령줄 요청으로 테스트하세요.
curl -I https://example.com
정확히 하나의 Strict-Transport-Security 헤더가 보여야 합니다. 앱 서버와 CDN에서 중복 HSTS 헤더가 나오는 것은 흔한 혼란의 원인입니다. 브라우저는 일반적으로 유효 정책을 적용하지만, 사고를 디버깅하는 사람에게 모호함은 필요하지 않습니다.
Step 4: 점진적으로 늘리세요
아무것도 깨지지 않는다면 기간을 단계적으로 늘리세요.
Strict-Transport-Security: max-age=86400
그다음:
Strict-Transport-Security: max-age=604800
그다음은 아마도:
Strict-Transport-Security: max-age=2592000
실용적인 일정은 다음과 같습니다.
- 5분
- 1일
- 1주
- 1개월
- 6개월 또는 1년
서두른다고 상이 있는 것은 아닙니다. 단계적 배포의 핵심은 모니터링, 지원 받은 편지함, 엣지 케이스가 체크리스트에서 놓친 것을 알려줄 시간을 주는 것입니다.
Step 5: 감사가 실제로 끝난 뒤에만 includeSubDomains를 추가하세요
모든 공개 하위 도메인이 HTTPS를 준비하면 다음을 고려할 수 있습니다.
Strict-Transport-Security: max-age=31536000; includeSubDomains
이때는 보수적으로 접근해야 합니다. 하나의 레거시 서비스가 아직 HTTP를 필요로 한다면 부모 도메인에 includeSubDomains를 추가하지 마세요. 해당 서비스를 마이그레이션하거나, 다른 도메인으로 옮기거나, 당분간 HSTS 정책을 더 좁게 유지해야 한다는 점을 받아들이세요.
보안 헤더는 현실을 반영해야 합니다. 나중에 갖추고 싶은 인프라를 위한 동기 부여 포스터처럼 사용해서는 안 됩니다.
Step 6: preload는 별도 프로젝트로 다루세요
다음이 모두 참일 때만 preload를 고려하세요.
- 도메인과 모든 하위 도메인이 유효한 HTTPS를 지원합니다.
- HTTP가 HTTPS로 리디렉션됩니다.
- HSTS 헤더가 최소 31536000초의
max-age를 사용합니다. - 헤더에
includeSubDomains가 포함됩니다. - 헤더에
preload가 포함됩니다. - 해당 도메인 아래 어디에서도 일반 HTTP가 필요하지 않을 것이라고 확신합니다.
preload 준비가 된 헤더는 다음과 같습니다.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
preload 목록 제출은 장기적인 약속입니다. 사이트가 캠페인 마이크로사이트, 임시 제품 도메인 또는 소유권 경계가 불분명한 도메인이라면 건너뛰세요.
구성 예시
Nginx
오류 응답에도 헤더가 전송되도록 always를 사용하세요.
add_header Strict-Transport-Security "max-age=300" always;
배포가 안정화된 뒤에는:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
Apache
mod_headers가 활성화된 상태에서:
Header always set Strict-Transport-Security "max-age=300"
나중에는:
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
CDN 또는 edge platform
CDN이 응답 헤더를 설정한다면 HSTS는 한 곳에서 관리하는 편이 좋습니다. 아주 명확한 이유가 없다면 origin과 edge에서 서로 다른 정책을 설정하지 마세요.
또한 CDN이 리디렉션, 캐시된 오류, 사용자 지정 오류 페이지에 헤더를 적용하는지도 확인하세요. 프로덕션 사이트는 200 OK 응답만으로 이루어지지 않습니다.
HSTS를 안전하게 되돌리는 방법
HSTS를 비활성화해야 한다면 다음을 보내세요.
Strict-Transport-Security: max-age=0
하지만 함정이 있습니다. 브라우저가 그 헤더를 받으려면 유효한 HTTPS를 통해 사이트에 성공적으로 도달해야 합니다. HTTPS 자체가 망가졌다면 HSTS 정책이 캐시된 사용자는 이를 지우라는 지시를 가져올 수 없습니다.
따라서 일반적인 복구 순서는 다음과 같습니다.
- 유효한 HTTPS를 복구합니다.
Strict-Transport-Security: max-age=0을 제공합니다.- 재방문 사용자가 이를 받을 수 있을 만큼 충분히 오래 유지합니다.
- 사고가 해결된 뒤 헤더를 제거하거나 교체합니다.
도메인이 preloaded 상태라면 max-age=0을 제공하는 것만으로는 새 브라우저 프로필에 충분하지 않습니다. preload 목록에서 제거를 요청하고, 그 변경이 브라우저 업데이트를 통해 배포될 때까지 기다려야 합니다.
배포 전 테스트 체크리스트
max-age를 늘리거나 includeSubDomains를 추가하기 전에 이 체크리스트를 사용하세요.
- 표준 HTTPS URL이 유효한 인증서를 반환합니다.
- HTTP가 HTTPS로 리디렉션됩니다.
- HSTS 헤더가 하나만 있습니다.
- 적절한 경우 리디렉션과 오류 응답에도 헤더가 나타납니다.
- 모든 공개 하위 도메인이 유효한 HTTPS를 갖고 있습니다.
- 인증서 갱신이 모니터링됩니다.
- 중요한 내부 시스템이 같은 부모 도메인 아래에서 HTTP에 의존하지 않습니다.
- Preload가 습관적으로 추가된 것이 아니라 명시적으로 논의되었습니다.
Lighthouse도 일부 맥락에서 누락되었거나 약한 보안 헤더를 표시할 수 있지만, 유일한 검증 방법이 되어서는 안 됩니다. 더 넓은 검토의 일부로 사용한다면 결과를 판정이 아니라 신호로 읽으세요. 당황하지 않고 Lighthouse 보고서를 읽는 방법에도 같은 태도가 적용됩니다.
<!-- tool-cta:start -->
💡 이것을 시도해 보세요: 각 HSTS 변경 전후에 Get Headers로 Strict-Transport-Security response를 검사하여 max-age, includeSubDomains 및 preload가 예상한 대로인지 확인하세요.
<!-- tool-cta:end -->
HSTS의 개인정보 보호 측면
HSTS는 흔히 보안 헤더로 설명되며, 실제로 그렇습니다. 동시에 개인정보 보호상의 이점도 있습니다. 신뢰할 수 없는 네트워크에서 사용자의 첫 요청이 일반 HTTP로 유출될 가능성을 줄여 줍니다.
이는 공항 Wi-Fi, 호텔 네트워크, 기업 게스트 네트워크, 그리고 사용자의 트래픽이 관찰되거나 수정될 수 있는 모든 곳에서 중요합니다. 일반 HTTP 요청은 호스트 이름, 경로, Secure 플래그가 없는 쿠키, 기타 요청 세부 정보를 노출할 수 있습니다. HTTPS가 마법은 아니지만, 이를 일관되게 강제하면 피할 수 있는 유출 범주 전체를 제거할 수 있습니다.
가장 좋은 HSTS 배포는 눈에 띄지 않습니다. 천천히 배포되고, 신뢰할 수 있는 인증서가 뒷받침하며, 아무도 알아차리지 못할 만큼 지루합니다. 실패 모드가 극적일 수 있는 헤더에 정확히 기대해야 하는 모습입니다.