Files
document-haness/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/case/case-a-validator-that-demands-tls-and-an-assembly-that-omits-it.md
T
DongHyeonkaandClaude Fable 5.1 b25357c48a docs(clean-architecture-backend-template): fold analysis into final and re-select one topic
- analysis/·source-index·state.json 을 final/document.md 제2부·제3부로 접었다. SSOT 는 하나다
- 파일럿 — commit-ambiguity-as-a-result 를 새 기준으로 재선별. 후보 14 → 글감 5
  (PROMOTE 5 · MERGE_INTO 3 · KEEP_IN_SSOT 4 · 보류 2). 기록 5건을 다시 썼고 그림 1개를
  techviz 로 만들었다
- 재선별이 잡은 것: 제1부 §6.2·§11.1 이 자기 §13.2 와 어긋나 있었다(레인을 안 돌렸다 vs
  돌렸다) — 정정. 이미 답이 나와 있던 Question 을 HEAD 재실행 질문으로 다시 세웠다.
  Concept 이 인용한 코드가 SSOT 에 없어 뺐다
- candidateScope·sourceRepository 기록. 나머지 43개 주제는 재선별 대기(PENDING 905)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 12:39:20 +09:00

16 KiB

kind, slug, title, topic, project, status, sourceRevision, rootTreeNode, evidenceCapturedOn, assets, evidence, source
kind slug title topic project status sourceRevision rootTreeNode evidenceCapturedOn assets evidence source
CASE a-validator-that-demands-tls-and-an-assembly-that-omits-it 검증기가 운영에 TLS를 요구하고, 실제로 조립되는 생산자에는 그 설정이 없다 assembly-ownership clean-architecture-backend-template 게시 전 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 case:a-validator-that-demands-tls-and-an-assembly-that-omits-it 2026-09-03
key file
a-validator-that-demands-tls-and-an-assembly-that-omits-it ../../../final/evidence/rendered/a-validator-that-demands-tls-and-an-assembly-that-omits-it.svg
key file
a-validator-that-demands-tls-and-an-assembly-that-omits-it-run ../../../final/evidence/rendered/a-validator-that-demands-tls-and-an-assembly-that-omits-it-run.svg
../../../final/evidence/raw/a-validator-that-demands-tls-and-an-assembly-that-omits-it.txt
../../../final/evidence/raw/a-validator-that-demands-tls-and-an-assembly-that-omits-it-run.txt
원본 분석 절은 final/document.md#a19-messaging-spring-boot-starter 의 §17.1 이다.

검증기가 운영에 TLS를 요구하고, 실제로 조립되는 생산자에는 그 설정이 없다

KafkaProfileValidator 는 운영으로 선언된 브로커에 TLS 와 브로커 인증이 켜져 있기를 요구하고, 기동 시점에 실제로 실행된다. 그런데 프로덕션에서 만들어지는 KafkaProducer 둘 어느 쪽 설정 맵에도 security.protocol 이 없어서, 두 맵 모두 PLAINTEXT 로 해석된다.

관계

  • 조립 결함을 판정하려면 조립하는 쪽을 먼저 읽어야 한다 KafkaProfileValidator 만 읽으면 운영 프로파일에 전송 보안이 강제된 것으로 보인다. KafkaProducer 를 만드는 두 코드를 열어야 그 설정 맵에 security.protocol 이 없다는 것이 보인다.
  • 검증기는 발행이 아니라 주입이 강제다 KafkaSecurityConfigurer 는 빈 팩토리까지 있지만 그것을 주입받는 프로덕션 코드가 0 건이라 아무 설정 맵에도 닿지 않는다.
  • "꺼짐"은 조건의 반복이 아니라 구조여야 한다 기동에서 설정값을 한 번 더 검사해도, 두 조립부가 security.protocol 을 설정 맵에 넣지 않으면 프로듀서는 평문 설정으로 만들어진다. 검사 조건을 늘리는 쪽이 아니라 조립 코드가 그 키를 넣어야 막힌다.

문제

브로커 프로파일이 값으로 표현되고 기동 때 검증된다. 운영 프로파일이면 전송 보안과 브로커 인증이 있어야 한다.

그 요구가 실제로 걸리는지, 그리고 통과한 뒤 만들어지는 프로듀서가 그 선언대로 붙는지 확인했다.

결론

두 규칙은 기동 시점에 실제로 실행된다. 그 검증을 통과한 뒤 만들어지는 KafkaProducer 두 곳의 설정 맵에 security.protocol 이 없다.

규칙은 KafkaProfileValidator:51 과 :55 에 있다. KafkaMessagingAutoConfiguration:55 의 kafkaProfileStartupValidation 이 StartupProfileValidation 을 통해 설정에서 컴파일된 프로파일에 이 검증기를 실행하고, MessagingConfigurationBindingTest:235 가 그 거부를 고정한다.

같은 애플리케이션이 만드는 KafkaProducer 는 둘이다. KafkaMessagingAutoConfiguration:153 이 키 다섯 개짜리를, KafkaSenderConfig:81 이 아홉 개짜리를 내놓는데 보안 키는 어느 쪽에도 없다. 두 맵을 ProducerConfig 에 그대로 넘겨 읽으면 security.protocol 이 PLAINTEXT 로, sasl.jaas.config 가 널로 나온다.

보안 값을 설정 맵에 실을 수 있는 코드 자체는 KafkaSecurityConfigurer:80 에 있다. 그 클래스의 빈 팩토리는 KafkaMessagingAutoConfiguration:109 에 있지만 조건이 두 단이고, 그 끝에 있는 CredentialProvider 를 구현하는 main 클래스가 0 건이다. 출하되는 애플리케이션에서는 이 빈이 만들어지지 않고, 만들어졌더라도 주입받는 코드가 없다.

그래서 배포가 겪는 것은 이렇다. app.messaging.enabled=true 와 app.messaging.broker=kafka 를 켜고 운영 프로파일에 전송 보안과 인증을 선언하면 기동은 통과한다. 만들어진 프로듀서의 설정 맵은 security.protocol 이 PLAINTEXT 로 해석되므로 클라이언트는 평문으로 접속을 시도한다. 설정에 선언한 보안과 프로듀서가 실제로 쓰는 값이 다른데도, 기동에서 예외나 경고가 나오지 않는다.

판정은 P1 로 원본 분석과 같다. 근거는 셋이다. 이 경로는 미배선 블록이 아니라 환경변수 둘로 켜지는 출하 조립이다. 기동에서 통과하는 검증은 실제 연결이 아니라 설정에 적힌 선언을 본다. 그리고 조립을 확인하는 테스트가 빈이 있는지만 단언하고 설정 맵의 키는 보지 않아서, 이 상태가 테스트 레인에 걸리지 않는다.

수정은 KafkaMessagingAutoConfiguration.messagingKafkaProducer 와 KafkaSenderConfig.kafkaSeamProducer 가 KafkaSecurityConfigurer.configure 의 결과를 각자의 설정 맵에 넣는 것이다. 그러려면 그 빈이 실제로 만들어져야 하므로 CredentialProvider 구현을 함께 정해야 한다.

검증 환경

OpenJDK : 21.0.12 kafka-clients : 4.1.2 확인 방식 : 검증기의 두 규칙과 그 실행 경로 확인, 프로덕션 KafkaProducer 생성 지점 전수와 각 설정 맵의 원문 확인, KafkaSecurityConfigurer 의 빈 조건 사슬과 주입처 계수, 두 설정 맵을 ProducerConfig 에 넘겨 해석된 보안 값 다섯 읽기 소스 수정 : x

재현 조건

  1. KafkaProfileValidator 에서 운영 프로파일에 거는 규칙 둘을 읽는다.
  2. 그 검증기를 설정에서 컴파일된 프로파일에 돌리는 자리와, 그것이 기동의 어느 단계인지 확인한다.
  3. 그 거부를 고정하는 테스트를 찾는다.
  4. 두 조립부를 켜는 프로퍼티를 전부 찾고 출하 기본값을 읽는다.
  5. 프로덕션에서 KafkaProducer 를 만드는 자리를 전부 세고, 각 설정 맵의 원문을 그대로 읽는다.
  6. security.protocol 을 상수명과 리터럴 양쪽으로 저장소 전체에서 찾는다.
  7. KafkaSecurityConfigurer.configure 가 자격 종류마다 무엇을 넣고 어디서 던지는지 읽는다.
  8. 그 클래스의 빈 팩토리에 붙은 조건을 따라가고, 사슬 끝의 인터페이스를 구현하는 main 클래스를 센다.
  9. 그 빈을 주입받는 코드와 getBean·ObjectProvider·빈 이름 조회를 각각 센다.
  10. 5에서 읽은 두 맵을 그대로 만들어 ProducerConfig 에 넘기고 보안 값 다섯을 읽는다.
  11. 조립을 확인하는 테스트가 무엇을 단언하는지, 실 브로커 시험이 어떤 컨테이너에 붙는지 읽는다.

본문

이 저장소는 브로커 프로파일을 값으로 표현하고 기동 시점에 검증한다. 그중 운영 프로파일에는 전송 보안과 브로커 인증을 요구하는 규칙 둘이 있다.

KafkaProfileValidator 가 운영 프로파일에 요구하는 둘

:::evidence key="a-validator-that-demands-tls-and-an-assembly-that-omits-it" alt="저장소 루트에서 돌린 정적 검색 출력 102줄. KafkaProfileValidator 51행부터 58행까지의 두 요구가 원문 그대로 나오고, 그 검증기가 InitializingBean 의 afterPropertiesSet 에서 도는 경로와 그 거부를 지키는 테스트가 이어진다. 그다음 두 조립부를 켜는 프로퍼티가 나오는데 플랫폼 루트와 브리지 루트가 각각 messaging enabled 를 조건으로 달고 심 쪽은 broker 프로퍼티를 달며 출하 기본값이 false 다. 이어서 프로덕션에서 KafkaProducer 를 만드는 두 자리의 설정 맵이 주석까지 원문 그대로 실려 있고, security.protocol 을 상수명과 리터럴 양쪽으로 검색한 결과가 KafkaSecurityConfigurer 의 상수 선언과 그 한 줄뿐이다. 그다음 configure 가 자격 종류마다 넣는 값과 던지는 두 분기, 그 클래스가 빈이 되기까지의 조건 사슬과 CredentialProvider 를 구현하는 main 클래스가 0 건이라는 계수, 그 빈을 받는 코드와 getBean·ObjectProvider·빈 이름 조회가 모두 0 건이라는 확인, 그리고 조립 테스트 둘이 단언하는 것과 실 브로커 시험이 붙는 컨테이너가 보인다." caption="두 요구와 그 실행 경로 · 두 프로듀서의 설정 맵 원문 · security.protocol 을 넣는 코드 한 줄 · 조건 사슬과 CredentialProvider 0 건 · 조립 테스트의 단언 — 102줄 · exit 0" zoom="true" :::

KafkaProfileValidator:51 은 운영 프로파일에 전송 보안이 꺼져 있으면 IllegalArgumentException 을 던지고 메시지에 브로커 이름을 붙인다. :55 가 브로커 인증에 같은 것을 한다.

이 검증기는 선언만 있는 것이 아니라 실제로 돈다. KafkaMessagingAutoConfiguration:55kafkaProfileStartupValidationStartupProfileValidation 을 만들고, 그 클래스가 InitializingBean 을 구현해 afterPropertiesSet(:43)에서 설정에서 컴파일된 프로파일에 검증기를 건다. MessagingConfigurationBindingTest:235aProductionKafkaBrokerWithoutTransportSecurityFailsStartup 이 그 거부를 고정한다.

여기까지만 보면 운영 프로파일로 뜬 배포는 평문으로 브로커에 붙을 수 없다.

조립되는 KafkaProducer 두 곳에 security.protocol 이 없다

프로덕션에서 KafkaProducer 를 만드는 자리는 둘이다.

KafkaMessagingAutoConfiguration.messagingKafkaProducer:153 에서 만든다. :139 에서 빈 HashMap 을 열고 :140~:152 에 넣는 것은 BOOTSTRAP_SERVERS_CONFIG, 직렬화기 둘, ACKS_CONFIG, ENABLE_IDEMPOTENCE_CONFIG 다섯이다.

KafkaSenderConfig.kafkaSeamProducer:81 에서 만든다. 위 다섯에 MAX_BLOCK_MS_CONFIG, LINGER_MS_CONFIG, REQUEST_TIMEOUT_MS_CONFIG, DELIVERY_TIMEOUT_MS_CONFIG 를 더해 아홉이다.

두 메서드 모두 @ConditionalOnMissingBean 이 붙어 있어 채택자가 자기 빈을 내놓지 않으면 이것이 쓰인다. 켜는 프로퍼티는 둘이다. MessagingPlatformRootAutoConfiguration:28MessagingBridgeRootAutoConfiguration:21 이 각각 app.messaging.enabled=true 를 조건으로 달고, 그 안에서 MessagingProviderSelection:48app.messaging.broker=kafkaKafkaMessagingAutoConfiguration 을 물리며 KafkaSenderConfig:38 이 같은 값을 조건으로 단다. application.yml:949 의 출하 기본값은 enabled: ${APP_MESSAGING_ENABLED:false} 다.

두 설정 맵의 원문 어디에도 security.protocol 이 없다.

두 설정 맵을 ProducerConfig 에 넘겼을 때 해석되는 보안 값

:::evidence key="a-validator-that-demands-tls-and-an-assembly-that-omits-it-run" alt="JVM 프로브 출력 19줄. OpenJDK 판이 먼저 찍히고 kafka-clients 판본이 나온다. 두 조립부가 넣는 설정 맵을 그대로 ProducerConfig 에 넘긴 결과가 프로듀서마다 나오는데, 넣은 키 목록과 함께 security.protocol 이 PLAINTEXT 로, ssl.endpoint.identification.algorithm 이 https 로, sasl.mechanism 이 GSSAPI 로, sasl.jaas.config 가 null 로, ssl.enabled.protocols 가 TLSv1.2 와 TLSv1.3 으로 해석된 것이 보인다. 두 프로듀서의 보안 값 다섯이 모두 같다." caption="두 조립 맵을 ProducerConfig 에 넘겨 읽은 보안 값 다섯 — 19줄 · exit 0" zoom="true" :::

두 맵 모두 security.protocolPLAINTEXT 로 해석된다. 자격 쪽도 비어 있어 sasl.jaas.config 가 널이다. 브로커에 붙기 전에 클라이언트가 무엇으로 붙을지는 이 시점에 이미 정해져 있다.

KafkaSecurityConfigurer 가 넣는 값 중 둘은 클라이언트 기본값과 같다. ssl.endpoint.identification.algorithm 은 넣지 않아도 https 이고, ssl.enabled.protocols 의 기본값도 [TLSv1.2, TLSv1.3] 이다. 실제로 차이가 나는 것은 security.protocol 과 SASL 두 값이다.

보안 값을 만드는 코드는 있고, 그 빈이 만들어지지 않는다

security.protocol 을 넣는 프로덕션 코드는 저장소 전체에서 KafkaSecurityConfigurer:80 한 줄이다. 상수명과 리터럴 양쪽으로 찾아도 :32 의 상수 선언과 그 한 줄뿐이다.

같은 메서드가 :83 에서 활성 프로토콜 목록을, :86 에서 종단 식별 알고리즘을 넣고, 그다음은 자격 종류에 따라 갈린다. SASL/SCRAM 이면 :92SCRAM-SHA-512 를, 사용자·암호면 :109PLAIN 을, 상호 TLS 면 :106NONE 을 넣는다. OAuth2 와 Nkey 는 값을 넣지 않고 :99:113 에서 던진다.

이 클래스를 만드는 팩토리는 KafkaMessagingAutoConfiguration:109 에 있는데, 조건이 두 단이다. :107@ConditionalOnBean(CredentialRuntimeRegistry.class) 이고, 그 레지스트리를 내놓는 MessagingCoreAutoConfiguration:317:315@ConditionalOnBean(CredentialProvider.class) 뒤에 있다. 그런데 CredentialProvider 를 구현하는 main 클래스가 0 건이다. 유일한 구현은 CredentialRuntimeRegistryTest:21 의 시험용 클래스다.

그래서 출하되는 애플리케이션에서 이 빈은 만들어지지도 않는다. 만들어졌다 해도 받을 곳이 없다 — 그것을 파라미터나 필드로 받는 프로덕션 코드가 0 건이고, getBeanObjectProvider 와 빈 이름 문자열로 가져가는 자리도 0 건이다.

조립 테스트가 설정 맵의 키를 단언하지 않는다

MessagingStarterOffContractTest:118selectingKafkaAssemblesOnlyKafka 는 컨텍스트가 실패하지 않았는지, KafkaMessagingAutoConfiguration 빈이 하나인지, Rabbit 쪽 빈이 없는지를 단언한다(:127, :130). :172aSelectedTransportAssemblesAPublisherMessagePublisher 빈이 하나인지를 본다(:189). 프로듀서가 어떤 키를 들고 있는지는 어느 쪽도 보지 않는다.

실 브로커에 붙는 MessagingLiveRoundTripQualificationTest:66new KafkaContainer("apache/kafka:4.1.0") 를 쓴다. 보안 설정이 하나도 없는 컨테이너다. 그래서 이 테스트가 확인한 왕복은 보안 설정이 없는 브로커와의 왕복이다.

원문과 갈리는 자리

원문 §17.1 은 조립되는 생산자를 messagingKafkaProducer 하나로 적었다. 프로덕션에서 KafkaProducer 를 만드는 자리는 둘이고, KafkaSenderConfig.kafkaSeamProducer 도 같은 프로퍼티 조건에서 조립되며 그쪽에도 보안 키가 없다.

원문이 KafkaSecurityConfigurer 가 만드는 성분을 다섯으로 센 것도 자격 종류를 하나로 놓았을 때다. configure 는 자격 종류마다 다른 SASL 메커니즘을 넣고, 상호 TLS 에서는 sasl.jaas.config 없이 NONE 만 넣으며, OAuth2 와 Nkey 에서는 아무것도 넣지 않고 던진다.

원문의 시나리오는 운영자가 CredentialProvider 빈을 공급하는 데까지 간다. 그 단계 없이도 이 상태는 성립한다. 검증기가 보는 tlsEnabledapp.messaging.brokers.* 소속이고, 자격을 요구하는 MessagingCredentialRequirementValidator 가 보는 것은 app.messaging.security.* 소속이라 두 네임스페이스가 다르다. 보안 프로파일을 적지 않으면 자격 공급 없이 통과한다.

판정과 근거는 원문과 같다.

확인하지 못한 것

브로커를 세워 핸드셰이크를 보지 않았다. 읽은 것은 클라이언트가 그 맵에서 어떤 프로토콜로 붙기로 정하는가까지다.

브로커를 띄워 실제 핸드셰이크를 관측하지 않았다. 확인한 것은 조립부가 만드는 설정 맵을 ProducerConfig 에 넘겼을 때 security.protocolPLAINTEXT 로 해석되는 데까지다.