12 KiB
title, source_type, status, confidence, tags, related_projects, last_reviewed, canonical_sources, audience, target_publish, status_label
| title | source_type | status | confidence | tags | related_projects | last_reviewed | canonical_sources | audience | target_publish | status_label | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Security Baseline을 JWT, Actuator, Secrets로 나누기 | blog | verified | high |
|
|
2026-07-03 |
|
backend-engineer | ready |
Security Baseline을 JWT, Actuator, Secrets로 나누기
Parent / 부모 (필수)
- 핵심 canonical: wiki/projects/ca-tmpl/security-baseline-jwt-actuator-secrets
- 관련 개념 문서: wiki/concepts/security-baseline-jwt-actuator-secrets - JWT Resource Server, actuator 노출, secret source/rotation의 일반 배경. 현재 이 글의 구현 사실 근거는 verified project canonical에 둔다.
타깃 독자 / Target reader
- 독자 profile: Spring Boot skeleton에 보안 baseline을 넣으려는 백엔드 엔지니어.
- 이미 안다고 가정하는 것: JWT, OAuth2 Resource Server, Spring Security filter chain, actuator, 환경 변수 기반 secret 주입.
- 처음 듣는다고 가정하는 것: security baseline을 인증 설정 하나가 아니라 데이터면 인증/인가, 제어면 actuator, secret lifecycle의 세 계약으로 나누는 방식.
도입 / Hook
Spring Boot 프로젝트에 보안을 붙인다고 하면 보통 SecurityFilterChain부터 떠올립니다. JWT를 검증하고, public path를 열고, 나머지는 인증을 요구하면 일단 그림은 그려집니다. 그런데 운영 관점에서 보면 그 정도로는 baseline이라고 부르기 어렵습니다. 인증 실패가 어떤 JSON shape으로 내려가는지, actuator endpoint가 앱 트래픽과 같은 경계에 놓이는지, secret rotation을 runtime reload로 볼지 restart-only로 볼지까지 같이 정해야 합니다.
ca-tmpl은 이 문제를 세 표면으로 나눴습니다. 데이터면은 JWT Resource Server와 AuthN/AuthZ 실패 envelope으로, 제어면은 management actuator chain으로, secret은 SecretSource와 restart-only guard로 다룹니다. 이 글은 ca-tmpl에 실제로 구현되고 로컬 검증된 범위와, 아직 IdP/secret manager 운영 경험처럼 말하면 안 되는 범위를 분리합니다.
본문 outline / Body outline
- security baseline은 인증 설정 하나가 아니다.
- 데이터면: JWT Resource Server와 filter-layer error envelope.
- 제어면: actuator를 별도 security chain으로 본다.
- secret: source abstraction과 restart-only rotation guard.
- 구현된 baseline과 운영 미검증 범위를 분리한다.
본문 / Body
보안 baseline을 좁게 잡으면 “JWT를 검증한다”가 전부가 됩니다. 하지만 skeleton/template에서는 다음 프로젝트가 무엇을 가져가야 하는지까지 보여줘야 합니다. ca-tmpl의 기준은 세 가지였습니다. 첫째, 사용자 요청이 들어오는 데이터면 인증/인가를 stateless JWT Resource Server로 고정합니다. 둘째, actuator 같은 제어면은 일반 API와 다른 노출 정책을 갖게 합니다. 셋째, secret은 문자열 설정값이 아니라 source와 reload 정책이 있는 runtime 계약으로 봅니다.
데이터면의 핵심은 adapter-web의 SecurityConfig와 JwtDecoderConfig입니다. SecurityConfig는 exceptionHandling과 oauth2ResourceServer 양쪽에 같은 entry point와 access denied handler를 연결합니다. Spring Security filter layer에서 발생한 401/403은 @ControllerAdvice까지 내려오지 않는 경우가 많습니다. 그래서 filter layer 자체가 ca-tmpl의 API error envelope을 쓰도록 entry point/denied handler를 맞춘 것입니다.
JWT decoder도 framework 기본값에만 맡기지 않습니다. JwtDecoderConfig는 SupplierJwtDecoder를 사용해 JWKS discovery를 기동 시점이 아니라 첫 decode 시점으로 미룹니다. validator chain에는 60초 clock skew, issuer validation, 선택적 audience validation이 명시됩니다. 여기서 구현된 것은 “JWT 검증 baseline”입니다. 외부 IdP 운영, JWKS rotation latency, unknown kid 상황의 실측값은 아직 없습니다.
제어면은 actuator입니다. ca-tmpl의 ManagementSecurityConfig는 actuator endpoint용 SecurityFilterChain을 @Order(0)으로 별도 구성합니다. health, info, prometheus는 allowlist로 열고, POST/DELETE /actuator/loggers/**는 deny합니다. 이 결정의 요지는 “actuator도 Spring Security가 보호한다”가 아니라, application API와 다른 security matcher, 다른 노출 정책, 다른 ingress/network boundary를 가져야 한다는 점입니다.
secret 쪽에서는 SecretSource abstraction과 restart-only 원칙이 중요합니다. ca-tmpl은 local .env와 prod secret source를 같은 소비자 코드가 보게 하되, runtime reload를 기본 경로로 만들지 않습니다. SecretReloadContractTest는 context refresh 이후 property source를 바꿔도 이미 바인딩된 configuration property 값이 바뀌지 않는다는 것을 확인합니다. 또한 Spring Cloud refresh scope machinery가 runtime classpath에 없다는 점도 검증합니다. 이것은 secret manager 연동을 구현했다는 뜻이 아니라, ca-tmpl의 secret 소비 계약이 restart-only라는 뜻입니다.
이 세 영역을 묶으면 security baseline의 의미가 달라집니다. JWT는 데이터면 인증을 담당하고, actuator는 제어면 노출을 담당하며, secret source는 runtime config lifecycle을 담당합니다. 세 영역은 모두 Spring Boot 설정처럼 보이지만 실패 형태, 네트워크 경계, lifecycle 위험이 다릅니다. ca-tmpl은 그 차이를 문서에만 남기지 않고 SecurityErrorClassifierTest, JwtDecoderConfigTest, ActuatorSecurityHttpTest, SecretReloadContractTest 같은 테스트로 일부 고정했습니다.
주의할 점도 분명합니다. 이 글에서 “구현됐다”고 말할 수 있는 것은 ca-tmpl repo 안의 filter chain, lazy decoder, actuator policy, secret source/reload guard, 로컬 ./gradlew check 통과 범위입니다. 실제 Keycloak이나 외부 IdP를 붙여 token lifecycle을 검증한 것이 아니고, Vault/AWS Secrets Manager/GCP Secret Manager 통합도 없습니다. actuator endpoint에 대한 침투 테스트나 production metric도 없습니다. security baseline을 설명할 때는 이 경계를 같이 말해야 합니다.
코드 예제 / Code samples (있다면)
// 출처: [[wiki/projects/ca-tmpl/security-baseline-jwt-actuator-secrets]]
// 실제 파일: adapter-web/.../auth/SecurityConfig.java, ca-tmpl @f6fbd4e196b4
.exceptionHandling(
ex ->
ex.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler))
.oauth2ResourceServer(
oauth ->
oauth
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler)
.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtConverter)));
// 출처: [[wiki/projects/ca-tmpl/security-baseline-jwt-actuator-secrets]]
// 실제 파일: adapter-web/.../auth/JwtDecoderConfig.java, ca-tmpl @f6fbd4e196b4
return new SupplierJwtDecoder(
() -> {
NimbusJwtDecoder decoder =
NimbusJwtDecoder.withIssuerLocation(settings.issuerUri()).build();
decoder.setJwtValidator(jwtValidator(settings.issuerUri(), settings.audience()));
return decoder;
});
validators.add(new JwtTimestampValidator(Duration.ofSeconds(60)));
validators.add(new JwtIssuerValidator(issuerUri));
// 출처: [[wiki/projects/ca-tmpl/security-baseline-jwt-actuator-secrets]]
// 실제 파일: app-bootstrap/.../management/security/ManagementSecurityConfig.java, ca-tmpl @f6fbd4e196b4
http.securityMatcher(EndpointRequest.toAnyEndpoint())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(
auth ->
auth
.requestMatchers(EndpointRequest.to("health", "info", "prometheus"))
.permitAll()
.requestMatchers(HttpMethod.POST, "/actuator/loggers/**")
.denyAll()
.requestMatchers(HttpMethod.DELETE, "/actuator/loggers/**")
.denyAll()
.anyRequest()
.authenticated());
// 출처: [[wiki/projects/ca-tmpl/security-baseline-jwt-actuator-secrets]]
// 실제 파일: app-bootstrap/.../contract/SecretReloadContractTest.java, ca-tmpl @f6fbd4e196b4
sources.addFirst(
new MapPropertySource(
"rotated-secret-source",
Map.of("secret-reload-probe.value", "rotated-secret")));
SecretHolder afterRotation = context.getBean(SecretHolder.class);
assertThat(afterRotation.value()).isEqualTo("initial-secret");
assertThatThrownBy(
() -> Class.forName("org.springframework.cloud.context.scope.refresh.RefreshScope"))
.isInstanceOf(ClassNotFoundException.class);
Sources / 근거 (canonical 인용 필수, derived layer 의무)
- wiki/projects/ca-tmpl/security-baseline-jwt-actuator-secrets - 이 글의 1차 canonical. JWT Resource Server, filter-layer envelope, actuator security chain, secret source/reload guard, local verification, IdP/secret manager/prod 미검증 경계를 따른다.
- wiki/concepts/security-baseline-jwt-actuator-secrets - 관련 개념 문서. JWT, actuator, secret rotation의 일반 배경으로만 둔다.
사실 vs 의견 / Fact vs opinion 구분
- 사실: ca-tmpl에는
SecurityConfig,JwtDecoderConfig,SecurityErrorClassifier, envelope entry point/denied handler,ManagementSecurityConfig,SecretSource*,SecretReloadContractTest가 존재한다. 근거: wiki/projects/ca-tmpl/security-baseline-jwt-actuator-secrets - 사실:
./gradlew check가 2026-07-02 기준 통과했고, security/error path, actuator policy, secret source/reload guard 관련 테스트가 canonical에 기록되어 있다. 근거: wiki/projects/ca-tmpl/security-baseline-jwt-actuator-secrets - 사실: 외부 IdP 운영, JWKS rotation latency, secret manager integration, real secret rotation automation, pentest, prod metric 검증은 없다. 근거: wiki/projects/ca-tmpl/security-baseline-jwt-actuator-secrets
- 의견: skeleton의 security baseline은 JWT 검증 코드보다 실패 계약, control plane 노출, secret lifecycle을 함께 묶을 때 설명력이 높아진다.
- 알지 못하는 것: 실제 IdP 장애 상황, secret manager rotation window, actuator 노출 사고 대응 경험.
답할 수 있는 범위 / Answer boundary
- 자신 있게 답할 수 있는 후속 질문:
- filter-layer 401/403을 error envelope으로 맞춘 이유는 무엇인가?
SupplierJwtDecoder와 60초 clock skew를 명시한 이유는 무엇인가?- actuator를 일반 API security chain과 분리해서 보는 이유는 무엇인가?
- secret runtime reload를 기본 경로로 두지 않은 이유는 무엇인가?
- 다음 글로 넘길 부분:
- real IdP integration과 token lifecycle.
- Vault/Secrets Manager/KMS 통합.
- actuator endpoint penetration test나 prod metric 기반 검증.
게시 체크리스트 / Publish checklist
- 모든 사실 주장에 canonical 링크 있음
- 사실 vs 의견 분리 명시됨
- 금지 마케팅 표현 없음
- 코드 예제 출처 명시
- 타깃 독자 가정과 톤 일치
/lint통과- 게시 URL 기록 (게시 후):