Files
document-haness/docs/clean-architecture-backend-template/tech-log-studio/http-client-and-resilience/case/case-a11-f007-dns.md
T
DongHyeonkaandClaude Opus 5 b2963105a8 docs(keycloak-session-store): import the session-storage lab as a new project
The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.

Follows the import procedure in README.md.

  source/     the originating repository verbatim — 78 documents, 28 SVGs,
              8 manifests, plus .source-revision recording the commit
  final/      the SSOT
    document.md   729 lines written from the 29 experiment documents, not
                  concatenated: what was predicted, what was measured, and
                  where the measurement itself was wrong
    evidence/raw    125 outputs, flattened to <experiment>__<file> because
                    the originals collided (01-baseline.txt appeared three
                    times) and the audit only globs the top level
    evidence/meta   one per raw file; command and exitCode are null and the
                    README says why rather than inventing them
    evidence/browser  22 captures
    assets/       three diagrams through techviz
    .techviz/     their VizSpecs

A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.

Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.

verify-pipeline.py passes. audit-records.py reports no issues.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 22:51:59 +09:00

224 lines
14 KiB
Markdown

---
kind: CASE
slug: a11-f007-dns
title: 혼동이라 적어 둔 이름이 아직 검사 문구에 있다
topic: http-client-and-resilience
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:a11-f007-dns
evidenceCapturedOn: 2026-09-02
body: case-a11-f007-dns.body.md
assets:
- key: a11-f007-dns
file: ../../../final/evidence/rendered/a11-f007-dns.svg
- key: a11-f007-dns-overloads
file: ../../../final/evidence/rendered/a11-f007-dns-overloads.svg
evidence:
- ../../../final/evidence/raw/a11-f007-dns.txt
- ../../../final/evidence/raw/a11-f007-dns-overloads.txt
source:
- 원본 분석 절은 analysis/11-adapter-outbound-httpclient.md#L583 이다. 등급은 P3 이다. 블로킹 오버로드에만 그 검사와 주석이 있다는 대조, 반응형 오버로드는 동적 대상 안정만 본다는 지적, 그래서 주석이 혼동이라 적은 상태가 반응형 경로에 남아 있다는 판정, 출하된 두 반응형 전송의 두 플래그가 같은 값이라 지금 노출이 없다는 확인, 프로파일 검증기가 동적 모드와 실험적 전송 조합을 이미 거부한다는 사실, 위험이 열리는 조합과 수정 방향이 그 절에 있다.
- 이 기록이 더한 것은 넷이다. 36 의 문구가 자기가 읽지 않는 플래그의 이름을 쓴다는 것 — 혼동은 과거형이 아니다. 그래서 이 두 검사에 걸린 유일한 test 가 36 의 문구만으로 통과하므로 43 을 지워도 빨개지지 않는다는 것. 두 플래그 조합 넷을 실제로 넣어, 갈리는 것이 한 조합뿐이고 나머지 거부에서 반응형이 내놓는 문구가 검사하지 않은 능력의 이름이라는 것. 그리고 오늘 막고 있는 것이 프로파일 검증기가 아니라 반응형 제공자 맵에 전송이 하나뿐이라는 사실이고, 그 맵은 빈 부재 조건이라 배포가 갈아 끼울 수 있다는 것이다.
---
# 혼동이라 적어 둔 이름이 아직 검사 문구에 있다
전송 능력 검증기의 블로킹 오버로드에 동적 모드 검사가 둘 있다. 앞엣것은 안정 플래그를 읽으면서 뒤엣것이 지키는 능력의 이름을 문구에 쓴다. 반응형 오버로드에는 앞엣것만 있고, 그래서 그 문구가 반응형 거부에도 그대로 나온다.
## 관계
- **두 오버로드가 있었고 호출되는 쪽이 틀린 쪽이었다**
두 사례 모두 오버로드가 둘인데 검사가 한쪽에만 있다.
- **재생 가능으로 인증된 본문이 다른 바이트를 낸다**
같은 리프의 다른 검사 사례다.
- **가드의 존재가 곧 테넌트 격리 보장은 아니다**
지금 노출이 없는 것과 검사가 있는 것은 다르다.
## 문제
능력 검증기는 블로킹용과 반응형용 오버로드를 따로 갖는다.
두 오버로드가 같은 검사를 하는지 대조했다.
## 결론
블로킹 쪽에만 있는 검사가 하나 있다.
동적 모드인데 호출 범위 검증된 DNS 고정 능력이 없으면 누락 목록에 넣는 검사다. 그 앞에는 같은 조건에서 동적 대상 안정 능력을 보는 검사가 따로 있다.
뒤엣것의 주석이 이유를 적는다. 그 능력 플래그가 모든 능력 레코드에 선언되어 있었지만 아무도 읽지 않았다는 것이다. 서버 측 요청 위조 방어의 주소 검증이 소켓까지 살아남는지를 결정하는 것이 그 능력이므로, 그것이 없는 전송은 동적 대상 안정 플래그가 무엇을 말하든 동적 대상을 서비스할 수 없다는 것이다. 둘이 혼동되고 있었다는 것이다.
그런데 앞엣것의 문구가 자기가 읽지 않는 플래그의 이름을 쓴다. 그 검사는 안정 플래그를 보면서 검증된 DNS 고정이 없다고 적고, 고정 플래그를 실제로 읽는 것은 뒤엣것이다. 주석이 지나간 일로 적은 혼동이 이 문구에는 아직 남아 있다.
그래서 하나뿐인 그 test 가 둘째 줄을 놓친다. 그 test 는 두 플래그가 모두 거짓인 능력을 넣고 메시지에 검증된 DNS 고정이 들어 있는지만 본다. 앞엣것의 문구만으로 맞는 조건이다. 뒤엣것을 통째로 지워도 초록이다.
반응형 오버로드에는 앞엣것만 있다.
실행으로 확인했다. 동적 프로파일 하나에 두 플래그 조합 넷을 넣었다. 둘 다 참이면 양쪽 통과다. 안정이 거짓인 두 조합은 양쪽 다 거부하는데, 그때 반응형이 내놓는 문구가 검증된 DNS 고정이 없다는 말이다. 고정이 거짓이고 안정이 참인 조합에서만 갈린다. 블로킹은 호출 범위 항목으로 거부하고 반응형은 통과한다.
지금 그 조합은 만들어지지 않는다. 다섯 능력 모두 두 플래그에 같은 값을 넣어 둔다. 셋은 둘 다 참이고 둘은 둘 다 거짓이다.
막고 있는 것은 프로파일 검증기가 아니다. 프로파일 검증기는 동적 모드와 두 전송 상수의 조합을 이름으로 막는데, 전송이 하나 늘면 그 목록에 없다. 오늘 이 오버로드에 닿는 능력이 하나뿐인 이유는 기본 자동 구성이 반응형 제공자 맵에 전송 하나만 넣기 때문이다.
그 맵은 빈 부재 조건이 붙어 있다. 자기 맵을 올리는 배포는 소스를 건드리지 않고 그 자리의 능력을 갈아 끼울 수 있다.
판정은 P3 다.
위험은 그렇게 올린 능력이 동적 대상 안정을 참으로, 호출 범위 검증된 DNS 고정을 거짓으로 선언하는 경우다. 주석이 못박은 정확히 그 조합이 반응형 쪽에서는 통과한다.
수정은 38~44 를 반응형 오버로드에 복사하는 것이다. 다만 출하된 다섯이 두 플래그를 같은 값으로 두고 있으므로 오늘 바뀌는 판정은 없다.
반응형 레코드에는 어느 검사도 읽지 않는 플래그가 둘 더 있다. 주석이 적은 상태가 그 둘에는 그대로다.
## 검증 환경
OpenJDK : 21.0.12
확인 방식 : 두 오버로드 대조, 플래그 참조 전수 확인, 실행 탐침
소스 수정 : x
## 재현 조건
1. 전송 능력 검증기의 두 오버로드를 나란히 읽는다.
2. 블로킹 쪽 마지막 두 검사가 각각 어떤 플래그를 읽고 어떤 문구를 넣는지 대조한다.
3. 그 두 검사에 걸린 test 를 찾아 무엇을 단언하는지 읽는다.
4. 세 능력 플래그를 읽는 곳을 저장소 전체에서 센다.
5. 출하된 능력 다섯이 두 플래그에 넣는 값을 뽑는다.
6. 반응형 제공자 맵에 어떤 전송이 들어가는지, 그 빈에 어떤 조건이 붙어 있는지 읽는다.
7. 프로파일 검증기가 동적 모드에 대해 이름으로 막는 전송을 확인한다.
8. 동적 프로파일 하나에 두 플래그 조합 넷을 넣어 양쪽 오버로드에 통과시킨다.
## 본문
<!-- body:start -->
전송 능력 검증기에 오버로드가 둘 있다.
## 한쪽에만 있는 검사
:::evidence key="a11-f007-dns" alt="전송 능력 검증기의 블로킹 오버로드에 있는 마지막 두 검사와 그 사이의 주석, 반응형 오버로드의 마지막 검사, 그 두 검사에 걸린 유일한 test 가 단언하는 것, 세 능력 플래그를 읽는 곳 전부, 출하된 능력 다섯이 두 플래그에 넣는 값, 반응형 제공자 맵에 들어가는 전송과 그 빈에 붙은 조건, 그리고 프로파일 검증기가 동적 모드에 대해 이름으로 막는 전송을 출력한 터미널 기록." caption="36 은 안정 플래그를 읽으면서 검증된 DNS 고정이라는 문구를 넣고, 고정 플래그를 읽는 것은 43 이다 · test 는 검증된 DNS 고정이라는 문자열만 확인한다 · 출하된 능력 다섯이 두 플래그를 같은 값으로 선언한다 · 반응형 제공자 맵에는 REACTOR_NETTY 하나가 들어가고 그 빈은 빈 부재 조건이다 — 68줄 · exit 0" zoom="true"
:::
블로킹 쪽의 마지막 두 검사다.
```java
35: if (profile.mode() == ClientMode.DYNAMIC && !capabilities.dynamicTargetStable()) {
36: missing.add("validated DNS pinning for dynamic targets");
37: }
38: if (profile.mode() == ClientMode.DYNAMIC && !capabilities.validatedDnsPinning()) {
39: // `validatedDnsPinning` was declared on every capability record and read by nothing. It is
40: // the capability that decides whether the SSRF address validation survives to the socket, so
41: // a transport that does not have it cannot serve a dynamic target no matter what its
42: // `dynamicTargetStable` flag says — the two were being conflated.
43: missing.add("call-scoped validated DNS pinning");
44: }
```
반응형 쪽은 여기서 끝난다.
```java
64: if (profile.mode() == ClientMode.DYNAMIC && !capabilities.dynamicTargetStable()) {
65: missing.add("validated DNS pinning for dynamic targets");
66: }
67: reject(profile, missing);
68: }
```
## 36 의 문구가 가리키는 플래그
36 은 자기가 읽는 플래그를 가리키지 않는다. 그 검사가 보는 것은 `dynamicTargetStable` 인데 적히는 말은 검증된 DNS 고정이고, `validatedDnsPinning` 을 실제로 읽는 검사는 43 이다. 주석이 과거형으로 적은 혼동이 이 문구에 남아 있다.
그래서 이 두 검사에 걸린 유일한 test 가 43 을 붙들지 못한다.
```java
39: void rejectsDynamicModeWithoutValidatedPinning() {
40- ClientProfile dynamic = ClientProfiles.builder("webhook").mode(ClientMode.DYNAMIC).build();
41- assertThatThrownBy(
42- () ->
43- validator.validate(
44- dynamic, BlockingTransportCapabilities.lightweightHttp11AndHttp2()))
45- .isInstanceOf(HttpConfigurationException.class)
46- .hasMessageContaining("validated DNS pinning");
```
넣는 능력은 두 플래그가 모두 거짓이고, 단언은 메시지에 그 문자열이 있는지다. 36 의 문구만으로 맞는다. 38~44 를 통째로 지워도 이 test 는 초록이다.
## 넣어 보면
:::evidence key="a11-f007-dns-overloads" alt="동적 대상 프로파일 하나에 두 플래그 조합 넷을 넣어, 블로킹 오버로드와 반응형 오버로드가 각각 통과시키는지 거부하는지와 거부할 때 적히는 누락 항목을 출력한 터미널 기록. 출하된 두 반응형 전송의 두 플래그 값도 함께 적는다. 픽스처는 고정 리비전 소스에서 직접 컴파일한다." caption="안정이 거짓인 두 조합은 양쪽 다 거부하고 반응형 문구는 검증된 DNS 고정이 없다는 말이다 · 고정이 거짓이고 안정이 참인 조합에서만 블로킹이 거부하고 반응형이 통과한다 — 20줄 · exit 0" zoom="true"
:::
```text
pinning=true stable=true
블로킹 : 통과
반응형 : 통과
pinning=false stable=false
블로킹 : 거부 — validated DNS pinning for dynamic targets, call-scoped validated DNS pinning
반응형 : 거부 — validated DNS pinning for dynamic targets
pinning=true stable=false
블로킹 : 거부 — validated DNS pinning for dynamic targets
반응형 : 거부 — validated DNS pinning for dynamic targets
pinning=false stable=true
블로킹 : 거부 — call-scoped validated DNS pinning
반응형 : 통과
```
안정이 거짓인 두 조합은 양쪽 다 거부한다. 그때 반응형이 운영자에게 내놓는 말이 검증된 DNS 고정이 없다는 문구다. 검사한 것은 다른 플래그다.
갈리는 것은 마지막 줄뿐이다.
## 지금 그 조합은 만들어지지 않는다
```text
apacheClassic validatedDnsPinning=true dynamicTargetStable=true
http11AndHttp2 validatedDnsPinning=true dynamicTargetStable=true
lightweightHttp11AndHttp2 validatedDnsPinning=false dynamicTargetStable=false
reactorNetty validatedDnsPinning=true dynamicTargetStable=true
jettyHttp3Experimental validatedDnsPinning=false dynamicTargetStable=false
```
출하된 능력 다섯이 전부 두 플래그를 같은 값으로 선언한다. 38~44 를 복사해도 오늘 바뀌는 판정은 없다.
## 막고 있는 것은 프로파일 검증기가 아니다
```text
173- if (profile.mode() == ClientMode.DYNAMIC
174- && (profile.transport() == TransportType.JDK
175- || profile.transport() == TransportType.JETTY)) {
176: out.add(violation("DYNAMIC_TARGET_TRANSPORT_UNSUPPORTED", profile, "transport"));
```
이 검사가 이름으로 막는 것은 두 상수다. 전송이 하나 늘면 여기에는 걸리지 않는다.
오늘 이 오버로드에 닿는 능력이 하나뿐인 이유는 다른 데 있다.
```java
@Bean
@ConditionalOnMissingBean(name = "httpClientReactiveTransportProviders")
Map<TransportType, ReactiveTransportProvider> httpClientReactiveTransportProviders(
ObjectProvider<MeterRegistry> meterRegistry, TlsMaterialProvider tlsMaterialProvider) {
Map<TransportType, ReactiveTransportProvider> providers = new EnumMap<>(TransportType.class);
providers.put(
TransportType.REACTOR_NETTY,
```
맵에 들어가는 전송이 하나다. 그리고 그 빈에는 빈 부재 조건이 붙어 있다. 배포가 자기 맵을 올리면 소스를 갈라내지 않고 다른 능력을 그 자리에 넣을 수 있다.
## 아직 아무도 읽지 않는 플래그
```text
TransportCapabilityValidator.java:38: if (profile.mode() == ClientMode.DYNAMIC && !capabilities.validatedDnsPinning()) {
ReactiveTransportCapabilities.java:14: boolean validatedDnsPinning,
ReactiveTransportCapabilities.java:16: boolean serverSentEvents,
ReactiveTransportCapabilities.java:17: boolean cancellationReleasesConnection) {
BlockingTransportCapabilities.java:19: boolean validatedDnsPinning,
```
`serverSentEvents``cancellationReleasesConnection` 은 선언 줄 말고 나오는 곳이 없다. 39 의 주석이 `validatedDnsPinning` 에 대해 적은 상태가 이 둘에는 손대지 않은 채로 있다.
## 확인하지 못한 것
배포가 제공자 맵을 실제로 갈아 끼워 그 조합을 기동시키지 않았다. 확인한 것은 기동 검증기의 판정까지다.
<!-- body:end -->