Spring Boot SSL Bundle이란?
Spring Boot 3.1에서 도입된 SSL Bundle은 TLS/SSL 인증서를 중앙에서 관리하는 기능입니다. 기존에는 RestTemplate, WebClient, 내장 서버, 데이터소스 등 각각 별도로 SSL을 설정해야 했지만, SSL Bundle을 사용하면 한 곳에서 인증서를 정의하고 여러 곳에서 참조할 수 있습니다. mTLS(상호 인증), PEM/JKS/PKCS12 형식 모두 지원하며, 런타임 인증서 갱신(Reload)까지 가능합니다.
기본 SSL Bundle 설정
# application.yml — PEM 형식 인증서
spring:
ssl:
bundle:
pem:
# 번들 이름: server-cert
server-cert:
keystore:
certificate: classpath:certs/server.crt
private-key: classpath:certs/server.key
truststore:
certificate: classpath:certs/ca.crt
# 번들 이름: client-cert (외부 API 호출용)
client-cert:
keystore:
certificate: classpath:certs/client.crt
private-key: classpath:certs/client.key
private-key-password: ${CLIENT_KEY_PASSWORD}
truststore:
certificate: classpath:certs/external-ca.crt
# JKS/PKCS12 형식
jks:
legacy-cert:
keystore:
location: classpath:certs/keystore.p12
password: ${KEYSTORE_PASSWORD}
type: PKCS12
truststore:
location: classpath:certs/truststore.jks
password: ${TRUSTSTORE_PASSWORD}
내장 서버 HTTPS 적용
# 내장 Tomcat/Netty에 SSL Bundle 적용
server:
port: 8443
ssl:
bundle: server-cert # 위에서 정의한 번들 이름 참조
// 기존 방식 (Spring Boot 3.0 이전):
server:
ssl:
key-store: classpath:keystore.p12
key-store-password: secret
key-store-type: PKCS12
trust-store: classpath:truststore.jks
trust-store-password: secret
// SSL Bundle 방식 (Spring Boot 3.1+):
// → 한 줄로 끝. 같은 인증서를 다른 곳에서도 재사용 가능
RestClient + SSL Bundle: 외부 API mTLS 호출
@Configuration
public class HttpClientConfig {
// RestClient에 SSL Bundle 적용
@Bean
public RestClient secureRestClient(
RestClient.Builder builder,
SslBundles sslBundles) {
return builder
.baseUrl("https://external-api.example.com")
.apply(b -> b.requestFactory(
ClientHttpRequestFactories.get(
ClientHttpRequestFactorySettings.DEFAULTS
.withSslBundle(sslBundles.getBundle("client-cert"))
.withConnectTimeout(Duration.ofSeconds(5))
.withReadTimeout(Duration.ofSeconds(30))
)
))
.build();
}
// WebClient에 SSL Bundle 적용 (WebFlux)
@Bean
public WebClient secureWebClient(
WebClient.Builder builder,
SslBundles sslBundles) {
SslBundle bundle = sslBundles.getBundle("client-cert");
HttpClient httpClient = HttpClient.create()
.secure(spec -> spec.sslContext(bundle.createSslContext()));
return builder
.baseUrl("https://external-api.example.com")
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
}
}
DataSource SSL: DB 커넥션 암호화
# PostgreSQL SSL 커넥션
spring:
datasource:
url: jdbc:postgresql://db.example.com:5432/mydb?sslmode=verify-full
username: app_user
password: ${DB_PASSWORD}
ssl:
bundle: db-cert # DB 인증서 번들 참조
ssl:
bundle:
pem:
db-cert:
truststore:
certificate: classpath:certs/db-ca.crt
keystore:
certificate: classpath:certs/db-client.crt
private-key: classpath:certs/db-client.key
// HikariCP에 직접 SSL Bundle 적용
@Configuration
public class DataSourceSslConfig {
@Bean
public DataSource dataSource(
DataSourceProperties properties,
SslBundles sslBundles) {
SslBundle bundle = sslBundles.getBundle("db-cert");
SSLContext sslContext = bundle.createSslContext();
HikariConfig config = new HikariConfig();
config.setJdbcUrl(properties.getUrl());
config.setUsername(properties.getUsername());
config.setPassword(properties.getPassword());
// PostgreSQL JDBC SSL 설정
config.addDataSourceProperty("ssl", "true");
config.addDataSourceProperty("sslmode", "verify-full");
config.addDataSourceProperty("sslfactory",
"org.postgresql.ssl.DefaultJavaSSLFactory");
return new HikariDataSource(config);
}
}
Redis SSL 커넥션
# Redis TLS 커넥션에 SSL Bundle 적용
spring:
data:
redis:
host: redis.example.com
port: 6380
ssl:
enabled: true
bundle: redis-cert
ssl:
bundle:
pem:
redis-cert:
truststore:
certificate: classpath:certs/redis-ca.crt
keystore:
certificate: classpath:certs/redis-client.crt
private-key: classpath:certs/redis-client.key
런타임 인증서 갱신 (Reload)
Spring Boot 3.2+에서는 인증서 파일이 변경되면 자동으로 Reload할 수 있습니다. Let’s Encrypt 등 자동 갱신 인증서에 필수적입니다:
# 파일 시스템 인증서 + 자동 Reload
spring:
ssl:
bundle:
pem:
server-cert:
reload-on-update: true # 파일 변경 감지 → 자동 Reload
keystore:
certificate: /etc/tls/server.crt # 파일 경로 (classpath 아님)
private-key: /etc/tls/server.key
truststore:
certificate: /etc/tls/ca.crt
server:
ssl:
bundle: server-cert
// 프로그래밍 방식으로 Reload 감지
@Component
@Slf4j
public class SslBundleReloadListener {
public SslBundleReloadListener(SslBundles sslBundles) {
// 번들 업데이트 이벤트 리스너
sslBundles.addBundleRegisteredHandler("server-cert", bundle -> {
log.info("SSL bundle 'server-cert' registered");
});
}
}
// K8s cert-manager와 연동:
// cert-manager가 /etc/tls/ 시크릿을 갱신
// → Spring Boot가 파일 변경 감지
// → SSL 컨텍스트 자동 Reload
// → 무중단 인증서 갱신!
K8s cert-manager + SSL Bundle 연동
# K8s Certificate 리소스
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: app-tls
spec:
secretName: app-tls-secret
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- api.example.com
duration: 2160h # 90일
renewBefore: 720h # 만료 30일 전 갱신
---
# Deployment: 시크릿을 파일로 마운트
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
spec:
template:
spec:
containers:
- name: app
image: my-app:latest
volumeMounts:
- name: tls-certs
mountPath: /etc/tls
readOnly: true
env:
- name: SERVER_SSL_BUNDLE
value: server-cert
volumes:
- name: tls-certs
secret:
secretName: app-tls-secret
items:
- key: tls.crt
path: server.crt
- key: tls.key
path: server.key
- key: ca.crt
path: ca.crt
mTLS 상호 인증 구현
# 서버 측: 클라이언트 인증서 요구
spring:
ssl:
bundle:
pem:
mtls-server:
keystore:
certificate: /etc/tls/server.crt
private-key: /etc/tls/server.key
truststore:
certificate: /etc/tls/client-ca.crt # 클라이언트 CA
server:
ssl:
bundle: mtls-server
client-auth: need # need: 필수, want: 선택적
// 클라이언트 인증서에서 사용자 정보 추출
@RestController
public class SecureController {
@GetMapping("/api/secure")
public Map<String, String> secureEndpoint(
HttpServletRequest request) {
X509Certificate[] certs = (X509Certificate[])
request.getAttribute("jakarta.servlet.request.X509Certificate");
if (certs != null && certs.length > 0) {
X509Certificate clientCert = certs[0];
String subject = clientCert.getSubjectX500Principal().getName();
String issuer = clientCert.getIssuerX500Principal().getName();
return Map.of(
"subject", subject,
"issuer", issuer,
"serialNumber", clientCert.getSerialNumber().toString(),
"validUntil", clientCert.getNotAfter().toString()
);
}
throw new RuntimeException("No client certificate");
}
}
// Spring Security와 연동: X.509 인증
@Configuration
@EnableWebSecurity
public class MtlsSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.x509(x509 -> x509
.subjectPrincipalRegex("CN=(.*?)(?:,|$)")
.userDetailsService(username ->
User.withUsername(username)
.password("")
.roles("CLIENT")
.build()
)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/secure/**").hasRole("CLIENT")
.anyRequest().permitAll()
)
.build();
}
}
커스텀 SSL Bundle 프로그래밍
// Vault에서 동적으로 인증서를 가져와 SSL Bundle 등록
@Component
public class VaultSslBundleRegistrar implements ApplicationRunner {
private final SslBundleRegistry registry;
private final VaultTemplate vault;
@Override
public void run(ApplicationArguments args) {
// Vault PKI에서 인증서 발급
VaultCertificateResponse cert = vault.opsForPki()
.issueCertificate("my-role",
VaultCertificateRequest.create("api.example.com"));
// PEM 문자열로 SSL Bundle 생성
PemSslStoreBundle storeBundle = PemSslStoreBundle.of(
new PemSslStoreDetails(
null,
cert.getCertificate(),
cert.getPrivateKey()
),
new PemSslStoreDetails(
null,
cert.getCaCertificate(),
null
)
);
SslBundle bundle = SslBundle.of(
storeBundle,
SslBundleKey.NONE,
SslOptions.DEFAULTS,
"TLS",
null
);
// 런타임에 번들 등록
registry.registerBundle("vault-cert", bundle);
}
}
테스트에서 SSL Bundle 사용
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class MtlsIntegrationTest {
@Autowired
private SslBundles sslBundles;
@LocalServerPort
private int port;
@Test
void shouldConnectWithClientCert() {
SslBundle clientBundle = sslBundles.getBundle("client-cert");
RestClient client = RestClient.builder()
.baseUrl("https://localhost:" + port)
.requestFactory(ClientHttpRequestFactories.get(
ClientHttpRequestFactorySettings.DEFAULTS
.withSslBundle(clientBundle)
))
.build();
String result = client.get()
.uri("/api/secure")
.retrieve()
.body(String.class);
assertThat(result).contains("CN=test-client");
}
}
// 테스트용 자체 서명 인증서 생성
// keytool -genkeypair -alias test -keyalg RSA -keysize 2048
// -validity 365 -keystore test.p12 -storetype PKCS12
// -storepass changeit -dname "CN=localhost"
핵심 정리
| 기능 | 설정 |
|---|---|
| PEM 인증서 번들 | spring.ssl.bundle.pem.{name} |
| JKS/PKCS12 번들 | spring.ssl.bundle.jks.{name} |
| 서버 HTTPS | server.ssl.bundle: {name} |
| 자동 갱신 | reload-on-update: true |
| mTLS | server.ssl.client-auth: need |
SSL Bundle은 Spring Boot의 TLS 설정을 한 곳에서 관리하고 여러 곳에서 재사용하는 핵심 추상화입니다. cert-manager와 reload-on-update를 결합하면 무중단 인증서 갱신이 가능합니다. K8s cert-manager TLS 자동화와 Spring OAuth2 Resource Server도 함께 참고하세요.