HSTS가 HTTP 접속을 HTTPS로 강제하는 원리와 max-age, includeSubDomains, preload의 의미를 알아보고 Spring Security에서 안전하게 설정하는 방법을 살펴봅니다.
웹사이트에 접속할 때 주소창에 도메인만 입력하면 브라우저가 먼저 HTTP로 연결한 뒤 HTTPS로 이동하는 경우가 있다. 서버가 HTTP 요청을 받아 301 또는 302 응답으로 HTTPS 주소를 안내하는 방식이다.
HTTPS로 최종 연결되므로 안전하다고 생각하기 쉽지만, 처음 발생한 HTTP 요청은 암호화되지 않은 상태다. 같은 네트워크에 있는 공격자가 중간에서 응답을 조작하면 HTTPS로 이동하지 못하게 하거나 가짜 사이트로 연결할 가능성이 생긴다.
이러한 문제를 줄이기 위해 사용하는 HTTP 보안 응답 헤더가 HSTS다.
HSTS란?
HSTS는 ‘HTTP Strict Transport Security’의 약자로, 브라우저에 특정 사이트는 HTTPS로만 접속해야 한다고 알려주는 보안 정책이다.
서버는 HTTPS 응답에 다음과 같은 헤더를 포함한다.
Strict-Transport-Security: max-age=31536000
이 헤더를 받은 브라우저는 해당 도메인을 HSTS 적용 대상으로 저장한다. 이후 사용자가 http://example.com으로 접속하거나 페이지 내부에 HTTP 링크가 포함돼 있어도 서버에 HTTP 요청을 보내기 전에 브라우저가 주소를 HTTPS로 변경한다.
일반적인 HTTP 리다이렉트는 서버가 요청을 받은 뒤 HTTPS 주소를 돌려준다. 반면 HSTS가 적용된 브라우저는 처음부터 HTTP 요청을 전송하지 않는다.
HSTS의 핵심은 서버에 접속한 뒤 HTTPS로 이동하는 것이 아니라, 브라우저가 접속 전에 HTTP 주소를 HTTPS로 바꾸는 것이다.
HSTS가 필요한 이유
HTTPS 사이트라도 사용자가 처음 입력한 주소나 오래된 즐겨찾기에 http://가 포함될 수 있다. 외부 게시물이나 이메일에 HTTP 링크가 남아 있을 가능성도 있다.
서버에서 HTTP 요청을 HTTPS로 리다이렉트하면 일반적인 상황에서는 정상적으로 암호화 통신으로 전환된다. 하지만 최초 HTTP 통신은 암호화되지 않았기 때문에 중간자 공격자가 리다이렉트 응답을 조작할 가능성이 있다.
공격자가 사용자의 연결을 계속 HTTP로 유지하면 사용자는 HTTPS 사이트에 접속했다고 생각하면서 실제로는 암호화되지 않은 통신을 사용할 수 있다. 이러한 방식을 SSL 스트리핑 또는 프로토콜 다운그레이드 공격이라고 부른다.
브라우저가 이미 HSTS 정책을 기억하고 있다면 HTTP 연결 자체를 시도하지 않는다. 따라서 공격자가 HTTP 구간에서 리다이렉트 응답을 가로채는 상황을 줄일 수 있다. HSTS Preload 공식 안내에서도 HSTS가 HTTP 주소를 HTTPS로 업그레이드해 프로토콜 다운그레이드와 쿠키 탈취 위험을 줄인다고 설명한다.
HSTS는 어떻게 작동할까?
HSTS가 적용되는 과정은 다음과 같다.
사용자가 HTTPS로 웹사이트에 접속하면 서버가 Strict-Transport-Security 헤더를 응답에 포함한다. 브라우저는 헤더의 max-age 값을 확인하고 해당 기간 동안 도메인을 HTTPS 전용 사이트로 기억한다.
이후 사용자가 같은 사이트의 HTTP 주소로 접속하면 브라우저는 네트워크 요청을 보내기 전에 HTTPS 주소로 변경한다. 서버에서는 HTTP 요청을 받은 뒤 리다이렉트할 필요가 없으며, 브라우저와 서버 사이의 첫 통신부터 TLS로 보호된다.
HSTS 적용 중 인증서가 만료됐거나 신뢰할 수 없는 인증서가 제공되면 브라우저는 접속을 차단한다. 일반적인 인증서 경고 화면처럼 사용자가 위험을 감수하고 접속을 계속할 수 없는 경우도 있다. 따라서 HSTS를 적용하기 전에는 인증서 발급과 자동 갱신 체계가 안정적으로 운영되는지 확인해야 한다.
HSTS 헤더는 HTTPS 응답에서만 유효하다
HSTS 헤더는 반드시 HTTPS 응답으로 전달해야 한다.
Strict-Transport-Security: max-age=31536000
HTTP 응답에 이 헤더를 추가해도 브라우저는 신뢰하지 않는다. 공격자가 암호화되지 않은 HTTP 응답에 임의의 HSTS 헤더를 삽입할 수 있기 때문이다.
따라서 서버는 80번 포트의 HTTP 요청을 HTTPS로 리다이렉트하고, 실제 HSTS 헤더는 443번 포트의 HTTPS 응답에 포함해야 한다. Spring Security 역시 기본적으로 요청이 안전한 HTTPS 요청으로 판단될 때 HSTS 헤더를 추가한다. Spring Security 공식 문서에서도 ServletRequest.isSecure()가 참인 경우에 HSTS 헤더를 적용한다고 설명한다.
max-age란?
max-age는 브라우저가 해당 도메인을 HSTS 사이트로 기억할 시간을 초 단위로 지정하는 필수 지시어다.
Strict-Transport-Security: max-age=31536000
31536000초는 약 1년이다. 브라우저는 이 헤더를 받은 시점부터 1년 동안 해당 도메인에 HTTP로 접속하지 않는다.
브라우저가 HSTS 헤더를 다시 받을 때마다 만료 시점도 갱신된다. 사용자가 사이트를 계속 방문해 정상적인 HTTPS 응답을 받으면 정책 유지 기간이 그 시점부터 다시 계산된다.
RFC 6797은 max-age를 브라우저가 해당 호스트를 HSTS 대상으로 취급하는 기간이라고 정의한다.
max-age=300은 어떤 의미일까?
다음 설정은 HSTS 정책을 300초 동안 유지한다.
Strict-Transport-Security: max-age=300
300초는 5분이다. 테스트 과정에서 HSTS가 서비스에 문제를 일으키는지 확인하기에는 유용하지만 운영 환경의 장기적인 보호 설정으로는 지나치게 짧다.
사용자가 HSTS 헤더를 받은 뒤 5분 동안 다시 방문하지 않으면 정책이 만료될 수 있다. 이후 HTTP 주소로 접속하면 다시 최초의 암호화되지 않은 연결 과정이 발생할 수 있다.
따라서 max-age=300은 초기 점검 단계에 사용하고, 서비스가 정상적으로 동작하는지 확인한 후 값을 점차 늘리는 방법이 좋다.
max-age를 단계적으로 설정하는 방법
HSTS는 한 번 적용되면 설정된 기간 동안 브라우저에 저장된다. 처음부터 1년 이상의 긴 시간을 지정했다가 HTTPS를 지원하지 않는 하위 도메인이 발견되면 사용자가 해당 서비스에 접속하지 못할 수 있다.
HSTS Preload 공식 사이트에서는 다음과 같이 단계적으로 적용하는 방법을 안내한다.
Strict-Transport-Security: max-age=300; includeSubDomains
먼저 5분으로 시작해 서비스와 하위 도메인에 문제가 없는지 확인한다. 이후 1주일인 604800초로 늘린다.
Strict-Transport-Security: max-age=604800; includeSubDomains
문제가 없다면 1개월인 2592000초로 늘려 충분한 기간 동안 점검한다.
Strict-Transport-Security: max-age=2592000; includeSubDomains
모든 HTTPS 연결과 인증서 갱신 체계가 안정적으로 운영된다면 최종적으로 1년인 31536000초를 적용할 수 있다.
Strict-Transport-Security: max-age=31536000; includeSubDomains
각 단계에서는 로그인, 결제, API, 정적 파일과 모든 하위 도메인이 HTTPS로 정상 작동하는지 확인해야 한다. 특히 외부 업체가 운영하는 하위 도메인과 내부 업무용 주소도 점검 대상에 포함해야 한다.
includeSubDomains란?
includeSubDomains는 현재 도메인뿐 아니라 모든 하위 도메인에도 HSTS 정책을 적용하는 선택 지시어다.
Strict-Transport-Security:
max-age=31536000; includeSubDomains
이 헤더를 example.com에서 제공하면 www.example.com, api.example.com, admin.example.com 같은 하위 도메인도 HTTPS로만 연결된다.
보호 범위가 넓어진다는 장점이 있지만 하위 도메인 중 하나라도 HTTP만 지원한다면 접속 장애가 발생할 수 있다. 현재 사용하지 않는 하위 도메인이라도 향후 생성되면 HSTS 정책의 영향을 받는다.
따라서 includeSubDomains를 추가하기 전에는 공개된 서비스뿐 아니라 내부 시스템과 오래된 하위 도메인까지 HTTPS 지원 여부를 확인해야 한다.
상위 도메인에 includeSubDomains를 적용했다고 해서 각 하위 도메인의 HSTS 만료 시간이 개별적으로 동일하게 관리되는 것은 아니다. 중요한 하위 도메인에서도 HSTS 헤더를 직접 제공하면 해당 호스트의 정책을 별도로 갱신할 수 있다.
Spring Security에서 HSTS 설정하기
Spring Security에서는 SecurityFilterChain 설정을 통해 HSTS 헤더를 구성할 수 있다.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(
HttpSecurity http) throws Exception {
http.headers(headers ->
headers.httpStrictTransportSecurity(hsts ->
hsts
.maxAgeInSeconds(31536000)
.includeSubDomains(true)
.preload(false)
)
);
return http.build();
}
}
maxAgeInSeconds(31536000)은 브라우저가 HSTS 정책을 1년 동안 기억하도록 설정한다. includeSubDomains(true)는 모든 하위 도메인으로 적용 범위를 넓힌다.
예시에서는 preload(false)로 설정했다. preload는 단순히 헤더에 단어 하나를 추가하는 설정이 아니라 전체 도메인을 장기간 HTTPS 전용으로 운영하겠다는 결정이기 때문이다.
Spring Security의 현재 API에서는 HSTS 기본 max-age가 1년이고 includeSubDomains는 기본적으로 활성화되며, preload는 기본적으로 비활성화된다. 구체적인 동작은 Spring Security HSTS 설정 문서에서 확인할 수 있다.
리버스 프록시 환경에서 확인할 부분
운영 환경에서는 로드밸런서나 Nginx에서 TLS 연결을 종료하고 Spring 애플리케이션에는 HTTP로 요청을 전달하는 경우가 있다.
이 구조에서는 외부 사용자가 HTTPS로 접속했더라도 Spring 애플리케이션이 요청을 HTTP로 판단할 수 있다. 그 결과 HSTS 헤더가 응답에 포함되지 않거나 HTTPS 리다이렉트가 반복되는 문제가 발생할 수 있다.
프록시가 원래 요청의 프로토콜을 Forwarded 또는 X-Forwarded-Proto 헤더로 전달하도록 설정하고, 애플리케이션이 신뢰된 프록시의 전달 헤더를 올바르게 처리하도록 구성해야 한다.
HSTS 적용 여부는 소스코드 설정만 보고 판단하지 말고 실제 외부 주소에 접속해 최종 HTTPS 응답 헤더를 확인해야 한다.
preload는 무엇일까?
일반적인 HSTS는 브라우저가 HTTPS 사이트에 한 번 접속해 헤더를 받은 다음부터 작동한다. 사용자가 처음 방문하는 순간에는 아직 해당 사이트의 HSTS 정책을 알지 못한다.
이러한 최초 접속의 빈틈을 줄이기 위해 브라우저에 HSTS 도메인 목록을 미리 포함하는 방식이 HSTS preload다.
Strict-Transport-Security:
max-age=31536000;
includeSubDomains;
preload
단순히 헤더에 preload를 추가했다고 브라우저 목록에 자동 등록되는 것은 아니다. 요구사항을 충족한 뒤 HSTS Preload 공식 사이트를 통해 별도로 신청해야 한다.
현재 등록 요건에는 유효한 인증서, 같은 호스트의 HTTP에서 HTTPS로의 리다이렉트, 모든 하위 도메인의 HTTPS 지원, 1년 이상의 max-age, includeSubDomains와 preload 지시어가 포함된다.
다만 preload 적용은 신중해야 한다. 공식 사이트 역시 일반적인 HSTS 사용은 권장하지만 preload는 기본적으로 권장하지 않으며, 포함된 도메인을 제거하더라도 브라우저 업데이트를 거쳐 사용자에게 반영되기까지 수개월이 걸릴 수 있다고 안내한다.
모든 하위 도메인을 장기간 HTTPS로 운영할 준비가 끝난 경우에만 검토해야 한다.
max-age=0은 어떤 의미일까?
HSTS 정책을 해제하려면 HTTPS 응답에 다음 헤더를 전송한다.
Strict-Transport-Security: max-age=0
이 헤더를 받은 브라우저는 해당 호스트에 저장된 HSTS 정책을 제거한다. includeSubDomains가 같이 있어도 max-age=0이면 정책을 중단한다.
하지만 이미 HSTS가 적용된 브라우저는 해당 사이트에 HTTPS로만 접속하려고 한다. 따라서 정책을 해제하는 응답 역시 정상적인 HTTPS 연결을 통해 제공해야 한다.
preload 목록에 등록된 도메인은 max-age=0만 전송한다고 즉시 해제되지 않는다. 별도의 제거 신청과 브라우저 목록 업데이트가 필요하다.
HSTS가 해결하지 못하는 문제
HSTS는 HTTP 연결을 HTTPS로 바꾸는 정책이지 웹사이트의 모든 취약점을 해결하는 기능은 아니다.
서버가 취약한 TLS 버전이나 암호화 방식을 사용한다면 별도의 보안 설정이 필요하다. XSS, SQL 인젝션과 접근통제 오류도 HSTS로 막을 수 없다.
HSTS는 인증서를 발급하거나 갱신해 주지도 않는다. 인증서가 만료되거나 도메인과 일치하지 않으면 오히려 사용자가 사이트에 접속하지 못할 수 있다.
HTTPS 페이지 안에서 HTTP 이미지나 스크립트를 불러오는 혼합 콘텐츠 문제도 별도로 점검해야 한다. CSP의 upgrade-insecure-requests를 보조적으로 적용할 수 있지만 모든 자원이 HTTPS에서 실제로 제공되는지 먼저 확인하는 것이 중요하다.
HSTS 보안 점검 항목
HSTS를 점검할 때는 먼저 HTTP 주소가 동일한 호스트의 HTTPS 주소로 리다이렉트되는지 확인한다. 이후 최종 HTTPS 응답에 Strict-Transport-Security 헤더가 포함되는지 살펴본다.
max-age가 지나치게 짧지 않은지 확인하고 운영 초기라면 단계적으로 값을 늘린다. 장기 운영 단계에서는 1년인 31536000초를 기준으로 서비스 정책에 맞는 기간을 정할 수 있다.
includeSubDomains를 사용한다면 모든 하위 도메인이 HTTPS와 유효한 인증서를 지원하는지 확인해야 한다. 존재하지 않거나 내부에서만 사용하는 하위 도메인도 정책의 영향을 받을 수 있다.
HSTS 헤더는 HTTP 응답이 아닌 HTTPS 응답에 설정돼 있어야 한다. 로그인 페이지뿐 아니라 오류 페이지와 리다이렉트 응답 등 모든 HTTPS 응답에서 누락되지 않는지도 점검한다.
로드밸런서나 CDN을 사용하는 환경에서는 최종 사용자가 받는 응답을 기준으로 확인해야 한다. 여러 장비가 서로 다른 HSTS 값을 추가해 중복 헤더가 생성되지 않는지도 살펴볼 필요가 있다.
자주 묻는 질문
HTTP를 HTTPS로 리다이렉트하면 HSTS는 필요 없나요?
필요할 수 있다. HTTP 리다이렉트는 최초 요청이 서버에 도착한 후에 작동한다. HSTS를 기억한 브라우저는 HTTP 요청을 보내기 전에 주소를 HTTPS로 변경하므로 다운그레이드 공격 위험을 줄일 수 있다.
max-age=300으로 설정해도 되나요?
테스트 단계에서는 사용할 수 있다. 하지만 300초는 5분에 불과하므로 장기적인 보호 효과는 제한적이다. HTTPS 운영에 문제가 없는지 확인한 후 1주일, 1개월과 1년 순서로 늘리는 방법이 좋다.
max-age=31536000은 무엇을 의미하나요?
브라우저가 약 1년 동안 해당 도메인을 HTTPS 전용 사이트로 기억한다는 의미다. 브라우저가 새로운 HSTS 헤더를 받을 때마다 만료 시점은 다시 계산된다.
includeSubDomains는 반드시 넣어야 하나요?
일반적인 HSTS 규격에서는 선택 사항이다. 하위 도메인까지 보호하려면 사용하는 것이 좋지만 모든 하위 도메인이 HTTPS를 지원하는지 먼저 확인해야 한다. preload 목록에 등록하려면 반드시 필요하다.
preload를 설정하면 바로 적용되나요?
아니다. 헤더에 preload를 추가한 뒤 공식 사이트를 통해 신청하고 브라우저 목록에 포함돼야 한다. 반영과 제거에는 긴 시간이 걸릴 수 있으므로 충분한 검토가 필요하다.
마무리
HSTS는 브라우저가 웹사이트에 HTTP로 연결하지 않고 처음부터 HTTPS를 사용하도록 만드는 보안 정책이다. 일반적인 HTTP 리다이렉트보다 앞 단계에서 작동하기 때문에 SSL 스트리핑과 프로토콜 다운그레이드 위험을 줄이는 데 도움이 된다.
max-age는 브라우저가 HSTS 정책을 기억할 시간을 초 단위로 지정한다. 초기에는 300처럼 짧은 값으로 점검한 뒤 문제가 없다면 단계적으로 늘리고, 안정적인 운영 환경에서는 31536000과 같은 장기 값을 적용할 수 있다.
HSTS 설정의 핵심은 긴 max-age 값을 바로 넣는 것이 아니라 모든 HTTPS 연결과 인증서 갱신 체계가 안정적인지 먼저 확인하는 것이다. 하위 도메인을 포함하거나 preload를 신청하기 전에는 장기간 HTTPS 전용으로 운영할 준비가 됐는지 반드시 점검해야 한다.
핵심 키워드: HSTS, HTTP Strict Transport Security, HTTPS 강제 연결, HSTS max-age, includeSubDomains, HSTS preload, Strict-Transport-Security, HSTS 설정 방법