Files
document-haness/docs/clean-architecture-backend-template/tech-log-studio/security-and-trust-boundaries/case/case-a06-f020-tls-stable.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

14 KiB

kind, slug, title, topic, project, status, sourceRevision, rootTreeNode, evidenceCapturedOn, body, assets, evidence, source
kind slug title topic project status sourceRevision rootTreeNode evidenceCapturedOn body assets evidence source
CASE a06-f020-tls-stable 프로파일이 선언한 여섯 값이 드라이버에 하나도 도달하지 않는다 security-and-trust-boundaries clean-architecture-backend-template 게시 전 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 case:a06-f020-tls-stable 2026-09-02 case-a06-f020-tls-stable.body.md
key file
a06-f020-tls-stable ../../../final/evidence/rendered/a06-f020-tls-stable.svg
key file
a06-f020-tls-stable-probe ../../../final/evidence/rendered/a06-f020-tls-stable-probe.svg
../../../final/evidence/raw/a06-f020-tls-stable.txt
../../../final/evidence/raw/a06-f020-tls-stable-probe.txt
원본 분석 절은 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. 이 경계를 보는 시험이 있는지 확인한다.

본문

값을 옮기는 클래스는 있다. 자기 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" :::

 * <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 가 하는 대로 전부 적용한 뒤 값을 읽었다.

[선언] 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 를 켜는 경로는 프로파일이 아니다

  spring.mongodb.ssl.enabled=true -> sslEnabled=true

URI 를 건드리지 않고 속성만 켜도 TLS 가 켜진다. URI 에 옵션을 직접 넣는 경로도 있다. 어느 쪽이든 배포가 프로파일과 별개로 한 번 더 말해야 한다.

덧붙여, 모듈 README 가 보여 주는 속성 키는 이 Boot 버전에서 바인딩되지 않는다. 같은 URI 를 두 키로 넣어 접속 호스트를 읽으면 갈린다.

  spring.mongodb.uri      -> [canary-host:31337]
  spring.data.mongodb.uri -> [localhost:27017]

README 의 키를 그대로 넣은 배포는 기본 호스트로 뜬다.

이 선언이 검증되는 배포는 아직 없다

프로파일 검증은 프로덕션 프로파일이 TLS 를 요구하지 않으면 거부하고, 그 메시지가 같은 결함의 이전 판을 적고 있다.

"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"

다만 그 검증은 프로파일이 선언됐을 때만 켜진다. 출하 설정이 이 모듈 아래 노출하는 키는 활성화 여부와 활성 프로파일 이름 둘뿐이고, 프로파일 자체를 넣는 곳은 저장소에 없다. 넣으려면 토폴로지 프로브 빈이 함께 있어야 한다.

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: …

그래서 이것은 지금 도는 배포의 결함이 아니라 조립하는 쪽이 프로파일을 선언하는 순간 성립하는 결함이다.

이 경계를 보는 시험이 없다

/**
 * The typed profile reaches the settings the driver is actually built from (MNG-INT-002).

팩토리 시험의 javadoc 이다. 확인하는 것은 팩토리의 출력이고, 그 팩토리는 아무도 부르지 않는다. TLS 레인 시험은 반대쪽에서 같은 자리를 비껴간다.

MongoClientSettings.builder()
    .applyConnectionString(new ConnectionString(connectionString))
    .applyToSslSettings(ssl -> ssl.enabled(true).context(trustOnly(authorityPem)))

손수 붙인 설정으로 서버가 TLS 를 강제하는지 확인한다. 어느 쪽도 프로파일의 선언이 실제 연결을 TLS 로 만드는지 묻지 않는다.

이 리프가 이미 쓰는 배선 수단

/** The customizer Boot applies when it builds the client. */
@Bean
@ConditionalOnBean(MeterRegistry.class)
@ConditionalOnMissingBean(name = "mongoDriverObservabilityCustomizer")
public MongoClientSettingsBuilderCustomizer mongoDriverObservabilityCustomizer(

관측 쪽도 설정 빌더에 적용하는 자리에 호출자가 없는 같은 형태였고, 자동 구성이 Boot 의 빌더 커스터마이저를 등록해 고쳤다. 프로덕션에서 그 수단을 등록하는 곳은 이 한 자리뿐이다. 설정 쪽에는 쓰이지 않았다.

확인하지 못한 것

실제 서버에 평문으로 연결되는 것을 관측하지 않았다. 확인한 것은 Boot 가 클라이언트를 만들 때 넘기는 설정 객체의 값이다.