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>
This commit is contained in:
DongHyeonka
2026-09-04 22:51:59 +09:00
co-authored by Claude Opus 5
parent 43bccd08a8
commit b2963105a8
5017 changed files with 372751 additions and 4943 deletions
@@ -0,0 +1,199 @@
---
kind: CASE
slug: a06-f020-tls-stable
title: 프로파일이 선언한 여섯 값이 드라이버에 하나도 도달하지 않는다
topic: security-and-trust-boundaries
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:a06-f020-tls-stable
evidenceCapturedOn: 2026-09-02
body: case-a06-f020-tls-stable.body.md
assets:
- key: a06-f020-tls-stable
file: ../../../final/evidence/rendered/a06-f020-tls-stable.svg
- key: a06-f020-tls-stable-probe
file: ../../../final/evidence/rendered/a06-f020-tls-stable-probe.svg
evidence:
- ../../../final/evidence/raw/a06-f020-tls-stable.txt
- ../../../final/evidence/raw/a06-f020-tls-stable-probe.txt
source:
- 원본 분석 절은 analysis/06-adapter-outbound-persistence-mongo.md#L1161 이다. 등급은 P1 이다. 팩토리의 javadoc, 호출자가 없다는 계수, 자격증명 해석기의 미도달, 실제 클라이언트를 Boot 가 만든다는 사실, 탐침이 잰 값들, 이미 고쳐진 형제, 그리고 기존 시험 둘이 이 경계를 보지 못한다는 판정이 그 절에 있다.
- 팩토리가 만들었을 설정의 여섯 값, 커스터마이저를 적용한 뒤에도 실제값이 그대로라는 것, ssl 속성이 두 번째 경로라는 것, README 의 속성 키가 이 Boot 버전에서 바인딩되지 않는다는 것, 그리고 프로파일을 선언하려면 토폴로지 프로브가 함께 있어야 한다는 것은 이 기록에서 확인했다.
---
# 프로파일이 선언한 여섯 값이 드라이버에 하나도 도달하지 않는다
프로파일의 값을 드라이버 설정으로 옮기는 클래스가 있고, 저장소 어디에도 그것을 부르는 코드가 없다. Boot 가 클라이언트를 만들 때 쓰는 설정은 TLS 가 꺼져 있고 타임아웃과 풀과 서버 API 와 UUID 표현이 전부 드라이버 기본값이다.
## 관계
- **검증기가 운영에 TLS를 요구하고, 실제로 조립되는 생산자에는 그 설정이 없다**
다른 리프에서 같은 형태가 난 사례다.
- **@Bean이 있다는 것은 조립 증거가 아니다**
수리 코드가 배선되지 않은 형태다.
- **조립 결함을 판정하려면 조립하는 쪽을 먼저 읽어야 한다**
이 사례를 찾는 방법이다.
## 문제
값을 옮기는 클래스는 존재하고, 자기 javadoc 에 왜 만들어졌는지 적어 두었다. 프로파일과 자격증명 해석기와 TLS 와 Stable API 플래그와 풀과 타임아웃 정책이 전부 있었고 전부 단위 시험돼 있었는데 그중 어느 것도 설정에 도달하지 않았다는 것이다.
같은 형태가 한 단계 뒤에서 반복된다. 그 클래스는 작성됐고, 그것을 부르는 배선은 없다.
## 결론
참조가 자기 파일과 자기 시험 밖으로 나가지 않는다. 저장소 전체에서 남는 언급 넷은 안내 문서, 옛 리뷰, 생성된 API 표면 기준선이며 호출은 하나도 없다. 유일하게 그 클래스만 쓰는 자격증명 해석기도 같다.
Boot 가 클라이언트를 만들 때 쓰는 설정을 재었다. 기본 빈에 등록된 커스터마이저 둘을 적용한 결과가 그 설정이다. 프로덕션 프로파일은 TLS 요구, 엄격한 서버 API 고정, 연결 2초, 서버 선택 3초, 풀 상한 40, 고정 UUID 표현을 말한다. 실제 설정은 TLS 꺼짐, 연결 10초, 서버 선택 30초, 풀 상한 100, 서버 API 없음, UUID 표현 미지정이다. 여섯이 전부 다르다. 같은 프로파일로 팩토리를 부르면 여섯이 전부 선언대로 나온다.
첫 값만 성질이 다르다. 타임아웃과 풀 상한이 성능 문제라면 TLS 는 노출 문제다.
TLS 를 켜는 경로는 둘이다. 배포가 URI 에 그 옵션을 넣거나, spring.mongodb.ssl 속성을 켜는 것이다. 어느 쪽이든 프로파일이 선언한 값과는 무관하게 배포가 한 번 더 말해야 한다.
이 선언이 실제로 검증되는 배포는 아직 없다. 이 모듈 아래 출하 설정이 여는 키는 둘 — 활성화 여부와 활성 프로파일 이름이다. 프로파일 본체를 채워 주는 자리는 없다. 넣으려면 포크가 토폴로지 프로브 빈까지 함께 공급해야 하고, 넣지 않으면 기동이 그 사실을 말하며 거부한다. 지금 도는 배포에서 터지는 결함이 아니다. 프로파일이 선언되는 순간부터 성립한다.
이 경계를 보는 시험은 없다. 팩토리 시험은 팩토리를 직접 만들어 그 출력이 프로파일대로인지 확인한다. 그 시험의 javadoc 은 타이핑된 프로파일이 드라이버가 실제로 만들어지는 설정에 도달한다고 적는데, 확인하는 것은 팩토리의 출력이고 그 팩토리는 아무도 부르지 않는다. TLS 레인 시험은 ssl.enabled(true) 를 손수 붙인 클라이언트로 서버가 TLS 를 강제하는지 확인한다.
수리 수단은 이 리프에 이미 있다. 관측 쪽에도 설정 빌더 적용 지점에 호출자가 없던 시기가 있었다. 관측 쪽은 자동 구성이 Boot 커스터마이저를 빈으로 올려 해결했다. 프로덕션에서 그 수단이 등록되는 자리는 하나다.
## 검증 환경
OpenJDK : 21.0.12
Spring Boot : 4.0.8
확인 방식 : 참조 전수 계수, 실제 자동 구성을 올린 컨텍스트에서 커스터마이저 적용 후 설정 값 비교
소스 수정 : x
## 재현 조건
1. 클라이언트 설정 팩토리의 javadoc 을 읽는다.
2. 그 타입과 자격증명 해석기를 언급하는 곳을 저장소 전체에서 센다.
3. 프로파일 검증이 production 에 TLS 를 요구하는 자리와, 그 검증이 켜지려면 무엇이 있어야 하는지를 읽는다.
4. 출하 설정이 이 모듈 아래 노출하는 키와 프로파일을 넣는 곳의 수를 센다.
5. 이 리프의 자동 구성과 Boot 의 Mongo 자동 구성을 올린 컨텍스트를 띄운다.
6. 기본 설정 빈에 등록된 커스터마이저를 전부 적용하고 값을 읽는다.
7. URI 를 건드리지 않고 spring.mongodb.ssl.enabled 만 켜서 같은 값을 읽는다.
8. 같은 프로파일로 팩토리를 불러 만들어졌을 설정의 값을 읽는다.
9. 이 경계를 보는 시험이 있는지 확인한다.
## 본문
<!-- body:start -->
값을 옮기는 클래스는 있다. 자기 javadoc 에 왜 만들어졌는지도 적혀 있다.
## 설정을 만드는 팩토리
:::evidence key="a06-f020-tls-stable" alt="클라이언트 설정 팩토리의 javadoc, 저장소 전체에서 그 타입과 자격증명 해석기를 언급하는 곳 전수, 프로파일 검증이 production 에 TLS 를 요구하는 자리, 그 검증이 켜지려면 토폴로지 프로브가 필요하다는 기동 가드, 출하 설정이 이 모듈 아래 노출하는 키와 프로파일을 넣는 곳의 수, 이 리프가 관측 쪽에 쓴 Boot 빌더 커스터마이저 등록과 그 수단의 프로덕션 등록 수, 그리고 팩토리 시험의 javadoc 과 TLS 레인 시험이 쓰는 손수 만든 클라이언트를 출력한 터미널 기록." caption="팩토리와 해석기를 언급하는 곳은 문서 넷뿐이고 호출은 없음 · 검증은 프로파일이 선언됐을 때만 켜지고 토폴로지 프로브가 없으면 기동이 거부 · 출하 키는 enabled 와 active-profile 둘, 프로파일을 넣는 곳 0 · 빌더 커스터마이저의 프로덕션 등록은 1 · 두 시험 다 팩토리 출력이나 손수 만든 설정을 본다 — 69줄 · exit 0" zoom="true"
:::
```java
* <p>The profile, the credential resolver, the TLS and Stable-API flags and the pool and timeout
* policy all existed and were all unit-tested. None of them reached a {@link MongoClientSettings}:
* the values were checked as intermediate objects and whatever the driver ended up configured with
* was decided elsewhere, by defaults nobody had chosen. A policy that nothing applies reads exactly
* like a policy that is applied the tests pass, the record is populated, and the client connects
* with a three-second timeout it inherited from the driver rather than the two the profile states.
```
이 클래스를 참조하는 곳은 자기 파일과 자기 시험뿐이다. 저장소 전체를 훑으면 안내 문서와 옛 리뷰 문서와 생성된 API 표면 기준선의 언급 넷이 남고, 어느 것도 호출이 아니다.
## 선언값과 실제값
:::evidence key="a06-f020-tls-stable-probe" alt="같은 URI 를 두 속성 키로 각각 넣어 얻은 접속 호스트, 프로덕션 프로파일이 선언하는 TLS 요구와 서버 API 고정과 두 타임아웃과 풀 상한과 UUID 표현, 그 선언을 설정 검증이 통과시키는 결과, 이 리프와 Boot 의 자동 구성을 올린 컨텍스트에서 커스터마이저를 전부 적용한 뒤 읽은 같은 값들, URI 를 건드리지 않고 ssl 속성만 켰을 때의 결과, 그리고 같은 프로파일로 팩토리를 불러 만들어졌을 설정의 같은 값들을 출력한 터미널 기록." caption="README 가 보여 주는 키는 호스트를 바꾸지 못하고 다른 키만 바꾼다 · 선언은 TLS 요구·엄격 서버 API·2초·3초·40·STANDARD · 커스터마이저 둘을 적용한 실제 설정은 TLS 꺼짐·10초·30초·100·없음·UNSPECIFIED · ssl 속성만 켜도 TLS 는 켜짐 · 팩토리가 만들었을 설정은 여섯이 전부 선언대로 — 21줄 · exit 0" zoom="true"
:::
이 리프의 자동 구성과 Boot 의 Mongo 자동 구성을 올리고, 기본 설정 빈에 등록된 커스터마이저를 Boot 가 하는 대로 전부 적용한 뒤 값을 읽었다.
```text
[선언] production 프로파일이 말하는 값
tlsRequired=true stableApiStrict=true connectTimeout=2000ms serverSelection=3000ms poolMax=40 uuid=STANDARD
이 선언을 설정 검증이 통과시킨다
[실제] Boot 가 클라이언트를 만들 때 쓰는 설정 (커스터마이저 적용 후)
커스터마이저 2개 적용
sslEnabled=false
connectTimeoutMs=10000 serverSelectionTimeoutMs=30000
poolMaxSize=100 serverApi=null uuidRepresentation=UNSPECIFIED
[미도달] 팩토리가 만들었을 설정
sslEnabled=true
connectTimeoutMs=2000 serverSelectionTimeoutMs=3000
poolMaxSize=40 serverApi=ServerApi{version=V1, deprecationErrors=true, strict=true} uuidRepresentation=STANDARD
```
여섯이 전부 다르다. 관측 커스터마이저가 등록된 상태에서도 같다 — 그 커스터마이저는 리스너만 붙이고 나머지에 손대지 않는다. `serverApi=null` 은 엄격한 서버 API 고정이 없다는 뜻이고, `uuidRepresentation=UNSPECIFIED` 는 팩토리가 저장된 문서 아래서 값이 움직이는 것을 아무도 쓰지 않은 마이그레이션이라 부르며 고정하려던 값이다.
첫 값은 나머지와 성질이 다르다. 타임아웃과 풀 상한은 성능이고, TLS 는 노출이다.
## TLS 를 켜는 경로는 프로파일이 아니다
```text
spring.mongodb.ssl.enabled=true -> sslEnabled=true
```
URI 를 건드리지 않고 속성만 켜도 TLS 가 켜진다. URI 에 옵션을 직접 넣는 경로도 있다. 어느 쪽이든 배포가 프로파일과 별개로 한 번 더 말해야 한다.
덧붙여, 모듈 README 가 보여 주는 속성 키는 이 Boot 버전에서 바인딩되지 않는다. 같은 URI 를 두 키로 넣어 접속 호스트를 읽으면 갈린다.
```text
spring.mongodb.uri -> [canary-host:31337]
spring.data.mongodb.uri -> [localhost:27017]
```
README 의 키를 그대로 넣은 배포는 기본 호스트로 뜬다.
## 이 선언이 검증되는 배포는 아직 없다
프로파일 검증은 프로덕션 프로파일이 TLS 를 요구하지 않으면 거부하고, 그 메시지가 같은 결함의 이전 판을 적고 있다.
```java
"a production MongoDB profile requires TLS; the flag existed and nothing checked it, so a"
+ " deployment could carry tls-required=false and still be called production"
```
다만 그 검증은 프로파일이 선언됐을 때만 켜진다. 출하 설정이 이 모듈 아래 노출하는 키는 활성화 여부와 활성 프로파일 이름 둘뿐이고, 프로파일 자체를 넣는 곳은 저장소에 없다. 넣으려면 토폴로지 프로브 빈이 함께 있어야 한다.
```java
if (probe.getIfAvailable() == null) {
throw new IllegalStateException(
"the Mongo platform has configured profiles but no MongoTopologyProbe bean, so the "
+ "startup validator has nothing to ask about the server: …
```
그래서 이것은 지금 도는 배포의 결함이 아니라 조립하는 쪽이 프로파일을 선언하는 순간 성립하는 결함이다.
## 이 경계를 보는 시험이 없다
```java
/**
* The typed profile reaches the settings the driver is actually built from (MNG-INT-002).
```
팩토리 시험의 javadoc 이다. 확인하는 것은 팩토리의 출력이고, 그 팩토리는 아무도 부르지 않는다. TLS 레인 시험은 반대쪽에서 같은 자리를 비껴간다.
```java
MongoClientSettings.builder()
.applyConnectionString(new ConnectionString(connectionString))
.applyToSslSettings(ssl -> ssl.enabled(true).context(trustOnly(authorityPem)))
```
손수 붙인 설정으로 서버가 TLS 를 강제하는지 확인한다. 어느 쪽도 프로파일의 선언이 실제 연결을 TLS 로 만드는지 묻지 않는다.
## 이 리프가 이미 쓰는 배선 수단
```java
/** The customizer Boot applies when it builds the client. */
@Bean
@ConditionalOnBean(MeterRegistry.class)
@ConditionalOnMissingBean(name = "mongoDriverObservabilityCustomizer")
public MongoClientSettingsBuilderCustomizer mongoDriverObservabilityCustomizer(
```
관측 쪽도 설정 빌더에 적용하는 자리에 호출자가 없는 같은 형태였고, 자동 구성이 Boot 의 빌더 커스터마이저를 등록해 고쳤다. 프로덕션에서 그 수단을 등록하는 곳은 이 한 자리뿐이다. 설정 쪽에는 쓰이지 않았다.
## 확인하지 못한 것
실제 서버에 평문으로 연결되는 것을 관측하지 않았다. 확인한 것은 Boot 가 클라이언트를 만들 때 넘기는 설정 객체의 값이다.
<!-- body:end -->