개요
사내 웹 솔루션은 HTTPS 요청 처리 시 SSL 인증서를 기반으로 동작합니다. 따라서 인증서의 유효기간이 만료되면, 각 사이트 담당 엔지니어가 직접 인증서를 갱신하는 절차를 수행해야 합니다. 이 과정에서 인증서가 잘못 적용되거나 구성 오류가 발생할 경우, 시스템 전체 장애로 이어질 수 있는 위험이 존재합니다.
이를 예방하기 위해 인증서를 자동으로 갱신하고, 애플리케이션 재시작 없이 즉시 반영(hot-swap)할 수 있는 기능을 도입했습니다. 일반적으로 hot-swap은 시스템의 전원을 끄지 않고 구성 요소를 교체하여 계속 사용할 수 있는 기능을 의미합니다. 웹 애플리케이션 관점에서는 애플리케이션을 중단하지 않고 구성 요소를 변경하고 즉시 반영할 수 있는 기능을 의미합니다.
Spring Boot 3.2 버전부터는 SSL Bundle 기반의 SSL hot reload 기능을 공식적으로 제공합니다.
(공식 문서 링크: https://spring.io/blog/2023/11/07/ssl-hot-reload-in-spring-boot-3-2-0)
하지만 사내 웹 애플리케이션은 해당 버전보다 낮은 Spring Boot를 사용하고 있어, 공식 기능을 활용할 수 없었습니다. 따라서 이를 대체하기 위해 자체 구현한 SSL hot-swap 방식을 도입하였습니다. 간략한 아키텍처에 대한 설명 이후, ssl reload 과정에 대해서도 알아보겠습니다.
Architecture

운영 환경(Linux)에서는 systemd timer가 주기적으로 실행되며, 인증서의 만료일을 확인해 30일 이하로 남았을 경우 자동으로 SSL 인증서를 갱신합니다. 인증서 갱신이 완료되면 시스템은 웹 애플리케이션의 내부 REST API(/api/ssl-reload)를 호출하여 새로운 인증서를 반영하도록 요청합니다.
웹 애플리케이션은 이 요청을 SslReloadRestController에서 받아 SslReloader.reloadSslContext()를 실행합니다. 해당 메서드의 실행 과정은 3단계로 나눠집니다.
- ServletWebServerApplicationContext를 통해 현재 Embedded Tomcat 인스턴스를 가져옵니다.
- 가져온 Embedded Tomcat의 모든 HTTPS Connector를 찾아 갱신된 keystore 정보를 반영합니다.
- reloadSslHostConfigs()를 호출해 Tomcat의 SSL 설정과 SSLContext를 새 인증서 기준으로 다시 로딩합니다.
이 과정을 통해 JVM이나 애플리케이션 재기동 없이 수행되며, JSSE(Tomcat의 SSL 엔진)가 즉시 새로운 인증서로 TLS 통신을 이어갈 수 있습니다.
해당 내용을 쉽게 파악하기 위해, Java 기반 웹 애플리케이션에서 SSL/TLS 설정을 위한 기술인 JSSE와 현재 실행 중인 Embedded Tomcat의 컴포넌트들을 조회하기 위한 클래스인 ServletWebServerApplicationContext에 대해 간단히 설명드리겠습니다.
ServletWebServerApplicationContext
- WebServer를 생성하는 Factory 객체를 통해 내장 톰캣을 생성하는 클래스
JSSE
https://www.ibm.com/docs/ko/i/7.6.0?topic=security-java-secure-socket-extension
- JSSE (Java™ Secure Socket Extension) 는 TLS (Transport Layer Security)의 기본 메커니즘을 추상화하는 프레임워크
- 기본 프로토콜의 복잡도와 특수성을 요약하여 JSSE는 프로그래머가 보안 암호화된 통신을 사용할 수 있도록 하는 동시에 가능한 보안 취약성을 최소화할 수 있게 합니다.
- JSSE (Java Secure Socket Extension) 는 TLS 프로토콜을 사용하여 클라이언트와 서버 간에 보안 암호화된 통신을 제공합니다.
TLS는 개인정보 보호 및 데이터 무결성을 제공하기 위해 서버 및 클라이언트를 인증하는 방법을 제공합니다. 모든 TLS 통신은 서버와 클라이언트 사이의 "핸드셰이크"로 시작합니다. 핸드셰이크 중에 TLS는 클라이언트와 서버가 서로 통신하기 위해 사용하는 암호 스위트를 협상합니다. 이 암호 스위트는 TLS를 통해 사용 가능한 다양한 보안 기능의 조합입니다.
JSSE는 애플리케이션의 보안을 개선하기 위해 다음을 수행합니다.
- 암호화를 통해 통신 자료를 보호합니다.
- 리모트 사용자 ID를 인증합니다.
- 리모트 시스템명을 인증합니다.
이제 SslReloader 클래스가 어떻게 런타임중에 SSL/TLS 인증서를 hot-reload 하는지 알아보겠습니다.
SslReloadRestController
@RequestMapping("/api")
@RestController
public class SslReloadRestController {
private static final org.slf4j.Logger logger = getLogger(SslReloadRestController.class);
@Autowired private SslReloader sslReloader;
@PostMapping("/ssl-reload")
public ResponseEntity<Void> reloadSsl() {
logger.warn("ssl-reload triggered");
try {
sslReloader.reloadSslContext();
logger.warn("successfully reloaded ssl");
return ResponseEntity.ok().build();
} catch (Exception e) {
logger.error("failed to renew ssl", e);
throw new SslReloadFailException();
}
}
}
reloadSslContext()
public void reloadSslContext() {
Tomcat tomcat = ((TomcatWebServer) webServerAppCtx.getWebServer()).getTomcat();
String newPassword = sslPasswordLoader.loadSslPassword();
log.info("New SSL password: {}", newPassword);
/**
* Hot-reload SSL without restarting JVM.
* Requires the keystore to be already updated externally.
*/
for (Connector connector : tomcat.getService().findConnectors()) {
if (HTTPS.equalsIgnoreCase(connector.getScheme()) || connector.getSecure()) {
try {
AbstractHttp11JsseProtocol<?> protocol = (AbstractHttp11JsseProtocol<?>) connector.getProtocolHandler();
protocol.setKeystorePass(newPassword);
protocol.setKeyPass(newPassword);
protocol.reloadSslHostConfigs();
log.info("reloadSslHostConfigs() called on connector port {}", connector.getPort());
} catch (Exception e) {
log.error("Failed to reload SSL on port {}", connector.getPort(), e);
}
}
}
}
- AbstractHttp11JsseProtocol
- Apache Tomcat(coyote) HTTP/1.1 프로토콜의 추상화 클래스
- JSSE(Java Secure Socket Extension)을 사용하여 HTTPS 연결을 처리하는데 필요한 기능을 제공합니다.
- Tomcat 웹 서버의 SSL/TLS 설정 / 키 설정 / 인증서 관련 설정을 담당합니다.
- setKeyStorePass, setKeyPass
- 새로 읽어온 패스워드를 Tomcat 프로토콜에 반영합니다.
- Tomcat 내부적으로 SSL 설정 객체(SSLHostConfig)가 이것을 사용해 keystore를 다시 엽니다.
- reloadSslHostConfigs()
- Tomcat이 기존 SSL 구성을 날리고, keystore 파일/패스워드를 기반으로 SSLContext를 다시 초기화합니다. (https://tomcat.apache.org/tomcat-8.5-doc/api/org/apache/tomcat/util/net/AbstractEndpoint.html)
- 이때, keystore 안에 들어있는 key / crt도 새로 읽습니다.
- 해당 커넥터를 통해 처리되는 모든 HTTPS 트래픽(일반 HTTP 요청 + WebSocket 업그레이드 포함)에 새 인증서가 적용됩니다.
- SSLContext
- SSL/TLS 연결에 필요한 암호화 알고리즘, 프로토콜 버전, 인증서 및 키 관리를 설정하는 데 사용되는 모듈
- 클라이언트/서버 간의 통신이 보안상 안전하게 이루어질 수 있습니다.
- SSL/TLS 웹사이트와 클라이언트 간의 전송되는 데이터를 암호화하여 인터넷 연결을 보호하기 위한 기술
- NioEndpoint
- Http11NioProtocol에서 소켓을 수신하며 처리하는 모듈
테스트 결과
11-14 12:59:39.310 INFO 35784 [https-jsse-nio-8443-exec-6] o.a.t.util.net.NioEndpoint.certificate :173 - Connector [https-jsse-nio-8443], TLS virtual host [_default_], certificate type [UNDEFINED] configured from keystore [file:/C:/project/target/classes/keystore.jks] using alias [alias] with trust store [null]
11-14 12:59:39.310 DEBUG 35784 [https-jsse-nio-8443-exec-6] o.a.t.util.net.NioEndpoint.certificate :173 -
[
SHA-256 fingerprint: 77e072575fe08b627981403c0f1e989f6e70194f3d4cb42764f3a8ecd4be6066
SHA-1 fingerprint: 8dd80833a6378e6e953974cae949e55235a221ad
[
[
Version: V3
Subject: CN=none, OU=none, O=none, L=Seoul, ST=Seoul, C=KR
Signature Algorithm: SHA256withRSA, OID = 1.2.840.113549.1.1.11
Key: Sun RSA public key, 2048 bits
params: null
modulus: 16497042240796911050787389462675891976338213680411636554357277049075603725957713430218630685336331322564281850705541147745095982431713258030959228068969244979329396317672001436874969102725909310805561870656985888524666236532166710887135714960155276145008500711287423375994968419509997404831166207249793381355923645558646542348374671349136649340942795648129440335665044993665872339251797524752406644009875844524425352334388955404223435203488186054051184558111689223174965649296938627311713724432037018754133295408906813767104384992988306996355012088144295065031729124852078738080237612755179825426930564403567186567947
public exponent: 65537
Validity: [From: Tue Nov 26 10:23:00 KST 2024,
To: Fri Nov 24 10:23:00 KST 2034]
Issuer: CN=none, OU=none, O=none, L=Seoul, ST=Seoul, C=KR
SerialNumber: [ 01a897b6]
Certificate Extensions: 1
[1]: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: C6 0F B6 7B A8 E2 4B 28 6F B3 50 D3 B7 54 21 F0 ......K(o.P..T!.
0010: 21 0B 75 63 !.uc
]
]
]
Algorithm: [SHA256withRSA]
Signature:
0000: 71 AE D2 1C 55 48 47 36 BC D7 4A 76 65 95 51 7F q...UHG6..Jve.Q.
0010: 26 2E B3 DA 1F 61 31 31 6C 5D D4 08 6D 0C C2 AD &....a11l]..m...
0020: 61 62 37 81 BF 86 01 62 54 FE 5D 72 F9 BC 21 36 ab7....bT.]r..!6
0030: D0 A6 7F D9 72 11 8D CC 27 C5 0E 2E FA 4D 15 6C ....r...'....M.l
0040: B4 B6 BB 3C 54 94 9C 70 0A 82 0D D8 1E C4 5B 87 ...<T..p......[.
0050: 13 59 5B BA 2E 02 88 F9 BF A8 69 3A 41 DB 3A 18 .Y[.......i:A.:.
0060: 35 57 B0 57 E1 08 F0 C8 3B 98 D4 5E 47 7B E4 F0 5W.W....;..^G...
0070: 7B 75 CC 41 0B 75 64 00 38 F4 6D 94 64 6B 2C A2 .u.A.ud.8.m.dk,.
0080: F4 EC 1E 21 D0 A7 D1 7E 09 DE D0 FD B4 AC A5 E2 ...!............
0090: 29 0A 22 36 91 4F D7 F2 C1 43 06 60 66 63 98 86 )."6.O...C.`fc..
00A0: 1B B0 ED 9A 2E 63 A9 BC BA B8 D3 01 BB 97 56 2C .....c........V,
00B0: DA 8C 29 2C 1D 9A 2E 16 87 47 04 CA E2 A8 CF 9E ..),.....G......
00C0: 2B 8D 73 D0 1D CF 2A 59 62 5A AD 29 1D 14 93 2D +.s...*YbZ.)...-
00D0: F2 DD E9 3F 85 14 D4 BA C3 70 C8 53 F3 E6 37 71 ...?.....p.S..7q
00E0: 9D B3 F1 4E CC 06 36 CB 19 CE 6B 88 CC 97 B9 0B ...N..6...k.....
00F0: A6 F2 DE C3 C2 14 11 98 8B C1 C6 8C A1 86 8F 6D ...............m
]
]
11-14 12:59:39.311 INFO 35784 [https-jsse-nio-8443-exec-6] com.example.api.ssl.SslReloader :49 - reloadSslHostConfigs() ca0lled on connector port 8443
systemd timer에 의해 서버 시스템에서 SSL 인증서가 갱신된 후, 내부 REST API(/api/ssl-reload, POST)를 호출하면 위와 같이 Embedded Tomcat 로그에서 새로운 keystore 기반으로 인증서가 재로딩되는 것을 확인할 수 있습니다.
'Java > Spring' 카테고리의 다른 글
| [Spring] Mybatis Framework에 대해서 알아보자 (0) | 2026.02.09 |
|---|---|
| [Spring] csv 파일의 데이터 파싱 및 저장 성능 개선기 (JPA save() vs JdbcTemplate batchUpdate() vs MariaDB LOAD DATA INFILE) (2) | 2025.05.22 |
| [DB] Spring boot 2.x에서 기본으로 지원하는 HikariCP에 대해 알아보자 (2) | 2025.05.15 |
| [JMeter] Apache JMeter를 활용한 성능 테스트 (4) | 2025.04.18 |
| [Spring] java 메모리에 존재하는 list 데이터 페이징 처리 (2) | 2025.02.12 |