発生した問題
ALBにECMのSSL証明書を付けてSSL終端している環境で、spring-bootのアプリケーションにhttpsでアクセスしてもStrict-Transport-Securityヘッダーが付与されませんでした。
環境
- spring-boot 3.0.6
- spring-security 6.0.3
SpringSecurityの仕様を見るとStrict-Transport-Securityはデフォルトで付与されるようです。
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-Options: nosniff
Strict-Transport-Security: max-age=31536000 ; includeSubDomains
X-Frame-Options: DENY
X-XSS-Protection: 0
但し、メモとして以下の記載があります。
Strict-Transport-Security は HTTPS リクエストにのみ追加されます
今回の事象はhttpsでアクセスしてもStrict-Transport-Security
が付与されないというものです。
解決方法
結論、ALBでSSL終端する際にはapplication.yaml
でtomcatがx-forwarded-proto
を参照する様に設定する事でSSLでアクセスされていると認識してくれます。
server:
tomcat:
remoteip:
protocol-header: x-forwarded-proto
この設定によりStrict-Transport-Security
が付与されるようになりました。
補足
SpringSecurityがどの様にStrict-Transport-Securityの付与を判断しているのか気になったので調べました。
HstsHeaderWriterクラスのwriteHeadersがHttpServletResponseのsetHeaderで付与しています。
@Override
public void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
if (!this.requestMatcher.matches(request)) { // matchesがfalseならヘッダー付与せずにreturn
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Not injecting HSTS header since it did not match request to [%s]",
this.requestMatcher));
}
return;
}
if (!response.containsHeader(HSTS_HEADER_NAME)) {
response.setHeader(HSTS_HEADER_NAME, this.hstsHeaderValue); // ここで付与している
}
}
matchesはHstsHeaderWriterの内部クラスのSecureRequestMatcherでHttpServletRequestのisSecureの結果を返しています。
private static final class SecureRequestMatcher implements RequestMatcher {
@Override
public boolean matches(HttpServletRequest request) {
return request.isSecure(); // 判定はここでしている
}
}
どの様にHttpServletRequestのSecureが設定されるのかを確認するためにTomcatのソースも見てみました。
TomcatではRemoteIpValve
クラスでX-Forwarded-Proto
ヘッダーがあるか判定し、値がhttpsであればSecureがtrueとなるよう実装されていました。
private String protocolHeader = "X-Forwarded-Proto";
// 判定処理
if (protocolHeader != null) {
// 1. X-Forwarded-Protoヘッダーを取得
String protocolHeaderValue = request.getHeader(protocolHeader);
if (protocolHeaderValue == null) {
// 2. ヘッダーの値がnullでなければisForwardedProtoHeaderValueSecureメソッドに渡す
// メソッド内部ではprotocolHeaderValueの値がhttpsかを判定している
} else if (isForwardedProtoHeaderValueSecure(protocolHeaderValue)) {
// 3. isForwardedProtoHeaderValueSecureがtrueならSecureがtrueになる
request.setSecure(true);
request.getCoyoteRequest().scheme().setString("https");
setPorts(request, httpsServerPort);
} else {
request.setSecure(false);
request.getCoyoteRequest().scheme().setString("http");
setPorts(request, httpServerPort);
}
}
これで、spring-bootのアプリが無事isSecure=trueだと認識して動作するんですね。
ALBにACMの証明書を付けてSSL終端し、ターゲットグループのport 80に流すのはよくある構成だと思います。同じようにはまる人の助けになればと思います。
(正直自分が良く忘れるので自分のためなんですが)