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:
co-authored by
Claude Opus 5
parent
43bccd08a8
commit
b2963105a8
@@ -0,0 +1,265 @@
|
||||
# grpc-advanced-diagnostics 완전 해부
|
||||
|
||||
> 상태: COMPLETE
|
||||
> 재오픈 게이트: cycle 2 — `src/main` production 4파일 277줄, test 1파일 229줄과 픽스처 1개 축자 통독 완료. `STRUCTURAL_ONLY` 잔여 없음.
|
||||
> 기준 revision: `21234e38cdb9a926cbc92bb97a2aee2e4a7d2916`
|
||||
> 분석 범위: `src/grpc-advanced/grpc-advanced-diagnostics`
|
||||
> SSOT owner: `grpc-advanced-diagnostics`
|
||||
> integration/family document: `analysis/20-grpc-platform.md` (secondary, INTEGRATION_ONLY)
|
||||
|
||||
---
|
||||
|
||||
## 0. SSOT identity / 커버리지
|
||||
|
||||
- `allowed_dependencies`: `["grpc-core-api", "grpc-client", "grpc-advanced-bootstrap"]`
|
||||
- `runtime_memberships`: **`[]`** — build-only (Advanced 가족 전체가 그렇다)
|
||||
|
||||
| 파일 | LOC |
|
||||
|---|---:|
|
||||
| `GrpcAdvancedInfrastructureTestkit` | 87 |
|
||||
| `GrpcDiagnosticsRedactor` | 72 |
|
||||
| `GrpcChannelDiagnosticsSnapshot` | 63 |
|
||||
| `GrpcChannelDiagnosticsPolicy` | 55 |
|
||||
| **main 합계** | **277** |
|
||||
| `GrpcChannelDiagnosticsPolicyTest` | 229 |
|
||||
| `xds/control-plane-snapshot.json` | 24 |
|
||||
|
||||
### Coverage ledger
|
||||
|
||||
| scope | count | disposition | reason |
|
||||
|---|---:|---|---|
|
||||
| `main/java/**` | 4 | `FULL_READ` | 전 본문 축자 확인 |
|
||||
| `test/java/**` | 1 | `FULL_READ` | 229줄 · 테스트 11개 |
|
||||
| `test/resources/xds/*.json` | 1 | `FULL_READ` | 24줄 픽스처 |
|
||||
| `build.gradle` | 1 | `FULL_READ` | 10줄 |
|
||||
| `gradle.lockfile` | 1 | `STRUCTURAL_ONLY` | 잠금 파일 |
|
||||
|
||||
`UNCLASSIFIED` 0.
|
||||
|
||||
---
|
||||
|
||||
## 1. 모듈의 정체
|
||||
|
||||
진단 표면(Channelz·CSDS)과 그것을 게시 가능하게 만드는 편집기, 그리고 고급 능력이 무엇을 상대로 검증되어야 하는지를 이름 짓는 테스트킷 계약을 담는다.
|
||||
|
||||
편집기 javadoc 이 왜 이것이 필요한지 적는다.
|
||||
|
||||
> "Channelz is unusually dangerous to expose because it is genuinely useful: it holds every socket's
|
||||
> local and remote address, the security details of each connection, and per-call state… once the
|
||||
> endpoint exists the whole of it is one authorization mistake away from being readable."
|
||||
|
||||
## 2. 두 겹의 게이트
|
||||
|
||||
`GrpcChannelDiagnosticsPolicy` 는 네트워크와 역할 두 게이트를 모두 요구하고, 하나라도 비면 생성자가 거부한다.
|
||||
|
||||
> "diagnostics need both a network and a role gate; Channelz holds every socket's peer and security
|
||||
> detail, so either gate alone is the whole surface"
|
||||
|
||||
그리고 등록 판정이 능력 깃발에 걸려 있다. CSDS 는 Channelz 가 켜져 있고 xDS 도 켜져 있을 때만 등록된다.
|
||||
|
||||
> "A CSDS service on a deployment that does not use xDS answers every query with nothing, which is
|
||||
> harmless, and advertises a control-plane surface that does not exist, which is not."
|
||||
|
||||
## 3. 스냅숏이 스스로를 검사한다
|
||||
|
||||
`GrpcChannelDiagnosticsSnapshot` 정규 생성자가 두 가지를 거부한다.
|
||||
|
||||
```java
|
||||
maskedSocketAddresses.stream()
|
||||
.filter(address -> !address.equals(GrpcDiagnosticsRedactor.maskAddress(address)))
|
||||
… // 마스킹되지 않은 주소
|
||||
xdsResourceVersions.keySet().stream()
|
||||
.filter(GrpcDiagnosticsRedactor::forbiddenField)
|
||||
… // 금지된 필드 이름
|
||||
```
|
||||
|
||||
즉 편집을 거치지 않은 값으로는 스냅숏을 만들 수 없다. §17.1 이 그 검사의 범위를 다룬다.
|
||||
|
||||
## 4. 마스킹의 형태
|
||||
|
||||
주소는 버리지 않고 가린다.
|
||||
|
||||
> "An operator has to be able to tell two subchannels apart, and a stable mask does that without
|
||||
> publishing where they point… The last two octets go; the first two stay, because 'which subnet'
|
||||
> is a real diagnostic question and 'which host' is not one the diagnostics endpoint should answer."
|
||||
|
||||
## 5. 인프라 없는 증거를 거부하는 계약
|
||||
|
||||
`GrpcAdvancedInfrastructureTestkit` 이 능력별로 필요한 실제 인프라를 이름 짓는다.
|
||||
|
||||
| 능력 | 필요 인프라 |
|
||||
|---|---|
|
||||
| `GRPC_WEB` | gRPC-Web 프록시 |
|
||||
| `SERVLET_COMPAT` | 서블릿 컨테이너 |
|
||||
| `XDS` | 멈출 수 있는 xDS 통제 평면 |
|
||||
| `KOTLIN` | 코틀린 툴체인 |
|
||||
| 나머지 11종 | 없음 |
|
||||
|
||||
근거가 javadoc 에 있다.
|
||||
|
||||
> "gRPC-Web without a proxy tests a code path no browser will take; a Servlet profile without a
|
||||
> container tests the profile object; xDS without a control plane cannot exercise the case that
|
||||
> matters, which is the control plane going away. In all three, a suite that runs without the
|
||||
> infrastructure passes and establishes nothing, which is worse than not having one."
|
||||
|
||||
## 10. 테스트 레인
|
||||
|
||||
11개 테스트 229줄. 두 게이트, CSDS 조건부 등록, 금지 필드 제거, 마스킹, 스냅숏 거부와 수용, 커밋된 xDS 픽스처의 편집, 능력별 인프라 목록을 확인한다.
|
||||
|
||||
## 12. negative-space probes
|
||||
|
||||
**12.1 도달성.** Advanced 가족이므로 배선 경로가 없다. 리프 밖 참조도 없고, 이 리프를 의존 선언한 모듈도 없다.
|
||||
|
||||
```
|
||||
$ grep -rn "advanced.diagnostics" --include=*.java src/ | grep -v grpc-advanced-diagnostics/
|
||||
grpc-core-api/…/GrpcStableModuleCatalog.java:42: "grpc-advanced-diagnostics"); ← 목록 안의 문자열
|
||||
$ grep -rn "grpc-advanced-diagnostics" --include=*.gradle src/
|
||||
(매치 없음)
|
||||
```
|
||||
|
||||
방향을 뒤집으면 이 리프는 `grpc-advanced-bootstrap` 의 실제 소비자 둘 중 하나다 — `GrpcChannelDiagnosticsPolicy` 가 `GrpcAdvancedModuleGuard.available` 을 두 번 부른다(그쪽 §12.1). 이 가족에서 리프끼리 실제로 코드가 닿는 몇 안 되는 자리다.
|
||||
|
||||
**12.2 선언된 의존 셋 중 둘이 쓰이지 않는다.**
|
||||
|
||||
```groovy
|
||||
api project(':grpc:grpc-core-api') // import 0
|
||||
api project(':grpc:grpc-client') // import 0
|
||||
api project(':grpc-advanced:grpc-advanced-bootstrap') // import 4줄
|
||||
```
|
||||
|
||||
리프의 자바 4파일이 갖는 `dev.caskeleton` import 는 넷뿐이고 전부 bootstrap 것이다.
|
||||
|
||||
```
|
||||
GrpcAdvancedInfrastructureTestkit.java:3 GrpcAdvancedCapability
|
||||
GrpcChannelDiagnosticsPolicy.java:3,4,5 GrpcAdvancedCapability · GrpcAdvancedFeatureFlags · GrpcAdvancedModuleGuard
|
||||
```
|
||||
|
||||
`grpc-client` 는 특히 눈에 띈다 — Channelz 진단이 채널을 다루는 주제이므로 의존 선언은 자연스럽게 읽히는데, 이 리프의 스냅숏은 채널 타입을 쓰지 않고 `String channelProfile` 과 `String connectivityState` 로 받는다. 진단 값 객체가 채널 타입에서 독립적인 것 자체는 설계로 읽히고, 그렇다면 남은 것은 쓰이지 않는 의존 선언이다.
|
||||
|
||||
같은 형태를 세 리프에서 기록했다 — `grpc-advanced-edition` §12.2(셋 다 미사용), `grpc-spring-boot-starter` §12.3(셋 미사용), `grpc-observability` §12.1(두 모듈이 이 리프를 `api` 로 노출하면서 쓰지 않음).
|
||||
|
||||
**12.2 대조군.** 이 저장소의 다른 편집기와 비교하면 방향이 같다 — `grpc-observability` 의 태그 정책은 허용 목록으로, 이쪽은 금지 패턴 + 마스킹으로 같은 문제(내용이 관측 표면으로 새는 것)를 푼다.
|
||||
|
||||
**12.3 대조군 — 같은 두 리터럴이 두 리프에 있다.**
|
||||
|
||||
```java
|
||||
// grpc-advanced-diagnostics: GrpcChannelDiagnosticsPolicy.standard()
|
||||
new GrpcChannelDiagnosticsPolicy(Set.of("admin"), Set.of("ROLE_PLATFORM_ADMIN"));
|
||||
|
||||
// grpc-spring-boot-starter: GrpcPlatformAutoConfiguration.grpcReflectionPolicy(...)
|
||||
new GrpcReflectionPolicy(properties.getReflectionMode(),
|
||||
java.util.Set.of("admin"), java.util.Set.of("ROLE_PLATFORM_ADMIN"));
|
||||
```
|
||||
|
||||
관리 네트워크 이름과 관리 역할 이름이 같은 값으로 두 곳에 손으로 적혀 있고, 둘을 묶는 상수가 없다. 하나를 바꾸면 다른 하나가 남는다. 스타터 쪽은 그 리터럴이 설정 표면에 노출되지 않는다는 별도 문제도 있다(그쪽 §17.4).
|
||||
|
||||
**12.4 드리프트.** build.gradle 이 서술한 세 요소(Channelz/CSDS 진단, 편집기, 인프라 테스트킷 계약)가 전부 존재한다. 드리프트 없음.
|
||||
|
||||
## 16. 확인하지 못한 것
|
||||
|
||||
- 실제 Channelz 서비스를 띄워 스냅숏을 만들지 않았다. 배선 경로가 없다.
|
||||
- IPv6 주소로 스냅숏을 만들어 §17.1 을 실행으로 재현하지 않았다. 정규식과 생성자 검사로 판정했다.
|
||||
- 테스트를 실행하지 않았다. 11개 전부 본문으로만 확인했다.
|
||||
- 두 의존이 쓰이지 않는다는 것(§12.2)은 `^import dev.caskeleton` grep 으로 판정했다.
|
||||
|
||||
## 17. 손볼 것
|
||||
|
||||
### 17.1 P2 — 마스킹이 IPv4 만 알고, 그 결과 "마스킹되지 않은 주소" 검사가 나머지 형태를 전부 통과시킨다
|
||||
|
||||
```java
|
||||
private static final Pattern IPV4_WITH_PORT =
|
||||
Pattern.compile("\\b(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})(:\\d{1,5})?\\b");
|
||||
|
||||
public static String maskAddress(String address) {
|
||||
if (address == null || address.isBlank()) { return "unknown"; }
|
||||
return IPV4_WITH_PORT.matcher(address)
|
||||
.replaceAll(m -> m.group(1) + "." + m.group(2) + ".x.x");
|
||||
}
|
||||
```
|
||||
|
||||
IPv4 가 아닌 주소는 패턴에 맞지 않아 **입력 그대로 반환된다.**
|
||||
|
||||
그리고 스냅숏 생성자의 검사는 이렇게 되어 있다.
|
||||
|
||||
```java
|
||||
.filter(address -> !address.equals(GrpcDiagnosticsRedactor.maskAddress(address)))
|
||||
```
|
||||
|
||||
마스킹 결과가 입력과 같으면 이미 마스킹된 것으로 판정한다. 그러므로 IPv4 가 아닌 주소는 전부 이 검사를 통과한다.
|
||||
|
||||
| 입력 | `maskAddress` 결과 | 생성자 판정 |
|
||||
|---|---|---|
|
||||
| `10.4.13.201:9090` | `10.4.x.x` | 거부(마스킹 필요) |
|
||||
| `10.4.x.x` | `10.4.x.x` | 수용 |
|
||||
| `[2001:db8::4:13:201]:9090` | 입력 그대로 | **수용** |
|
||||
| `pod-3.svc.cluster.local:8080` | 입력 그대로 | **수용** |
|
||||
| `unix:/var/run/grpc.sock` | 입력 그대로 | **수용** |
|
||||
|
||||
세 번째와 네 번째가 문제다. 이 플랫폼이 겨냥하는 배포 형태가 쿠버네티스이고(`grpc-discovery` 전체가 그 주제다), 헤드리스 레코드의 엔드포인트는 파드 DNS 이름이며 이중 스택 클러스터에서는 IPv6 주소다. 편집기가 막으려 한 것이 정확히 그것이다 — "a diagnostics endpoint that publishes peer addresses publishes every tenant's connection."
|
||||
|
||||
`unix` 소켓 경로도 통과한다. 그것은 호스트 파일 시스템 경로다.
|
||||
|
||||
**테스트가 이것을 볼 수 없다.** 테스트의 주소 리터럴이 전부 IPv4 다 — `10.4.13.201:9090` · `10.9.13.201` · `10.4.x.x` · `10.5.x.x`. IPv6 도 호스트 이름도 없다.
|
||||
|
||||
**수정.** 마스킹을 형태별로 나눈다. IPv6 는 앞 두 그룹만 남기고 나머지를 `:x:x` 로, 호스트 이름은 최상위 라벨 몇 개만 남기고, 그 밖의 형태는 `unknown` 으로 접는다. 그리고 검사를 "결과가 입력과 같으면 통과" 가 아니라 "알려진 마스킹 형태와 일치해야 통과" 로 뒤집는다. 지금 형태는 마스킹이 모르는 입력을 전부 안전하다고 판정한다.
|
||||
|
||||
### 17.2 P3 — 금지 필드 검사가 키에만 적용되고 값에는 적용되지 않는다
|
||||
|
||||
```java
|
||||
xdsResourceVersions.keySet().stream().filter(GrpcDiagnosticsRedactor::forbiddenField)…
|
||||
```
|
||||
|
||||
`redact(...)` 도 같다 — 금지 이름의 키를 버리고, 남은 값은 주소 필드일 때만 마스킹한다. 값 자체가 자격증명 형태인지는 보지 않는다.
|
||||
|
||||
`grpc-observability` 의 태그 정책은 값도 본다(UUID·`sha256:`·`bearer ` 패턴). 같은 저장소의 두 관측 편집기가 값 검사에서 갈린다.
|
||||
|
||||
xDS 자원 버전은 보통 짧은 숫자나 해시라 도달성이 낮다. 기록하는 이유는 두 편집기의 규율이 다르다는 점이다.
|
||||
|
||||
### 17.3 P3 — "실환경 증거" 가 두 리프에 반씩 있고 서로 만나지 않는다
|
||||
|
||||
이 리프가 능력별로 무엇이 실환경인지 정의한다.
|
||||
|
||||
```java
|
||||
public static Set<Infrastructure> requiredFor(GrpcAdvancedCapability capability) { … }
|
||||
public static List<String> missingInfrastructure(GrpcAdvancedCapability capability, Set<Infrastructure> available) { … }
|
||||
```
|
||||
|
||||
그리고 `grpc-advanced-bootstrap` 이 승격 증거로 그것을 요구한다.
|
||||
|
||||
```java
|
||||
public record GrpcAdvancedPromotionEvidence(
|
||||
GrpcAdvancedCapability capability, …, boolean realEnvironmentTest) { … }
|
||||
// ^^^^^^^^^^^^^^^^^^^^^^^^^^ 불리언 하나
|
||||
```
|
||||
|
||||
`GrpcAdvancedPromotionGate.evaluate` 는 그 불리언이 거짓이면 "xds has no real environment test" 를 차단 사유로 낸다. 그 불리언을 무엇으로 채워야 하는지는 그쪽에서 답하지 않고, 답하는 코드가 이 리프에 있는데 두 쪽이 서로를 부르지 않는다.
|
||||
|
||||
결과: `GrpcAdvancedPromotionEvidence.complete(XDS, 7일)` 은 `realEnvironmentTest = true` 를 그냥 넣는다. xDS 통제 평면이 실제로 있었는지와 무관하다. 이 리프의 javadoc 이 경계한 상태 — "a suite that runs without the infrastructure passes and establishes nothing" — 를 승격 게이트가 그대로 통과시킬 수 있다.
|
||||
|
||||
**왜 P3 인가.** 두 리프 모두 배선되지 않았고 승격은 사람이 수행한다. 다만 이 두 조각이 존재하는 이유가 "그 판단을 코드로 적어 두는 것" 이므로, 판단의 절반이 다른 절반을 부르지 않는 것은 그 목적에 어긋난다. `grpc-advanced-edition` §17.2 가 같은 가족에서 같은 모양을 기록했다 — 두 승격 게이트가 서로를 부르지 않는다.
|
||||
|
||||
**수정.** `GrpcAdvancedPromotionEvidence.realEnvironmentTest` 를 불리언 대신 `Set<Infrastructure> availableInfrastructure` 로 바꾸고, 게이트가 `missingInfrastructure(capability, available)` 를 불러 그 결과를 차단 사유에 합친다. 그러면 "실환경 테스트를 했다" 가 선언이 아니라 능력별 목록에 대한 대조가 된다. 의존 방향도 맞는다 — 이 리프가 이미 bootstrap 을 의존하므로, 게이트가 이쪽을 부르려면 방향을 뒤집거나 `Infrastructure` 열거형을 bootstrap 으로 옮겨야 한다는 점은 함께 정해야 한다.
|
||||
|
||||
### 확인된 설계(문제 아님)
|
||||
|
||||
- **두 게이트를 모두 요구하고 하나만 있으면 생성자가 거부하는 것.**
|
||||
- **CSDS 를 xDS 사용 시에만 등록하는 것과 그 근거** — 존재하지 않는 통제 평면 표면을 광고하지 않는다.
|
||||
- **주소를 버리지 않고 가리는 판단** — 두 서브채널을 구별할 수 있어야 한다.
|
||||
- **스냅숏이 스스로 편집 여부를 검사하는 것** — 편집을 우회한 값으로는 만들 수 없다(형태 범위는 §17.1).
|
||||
- **능력별로 필요한 실제 인프라를 이름 지은 것** — 인프라 없이 통과하는 묶음은 없는 것보다 나쁘다. (승격 게이트와의 연결 없음은 §17.3.)
|
||||
- **`requiredFor` 의 switch 가 15개 능력을 전부 나열하고 `default` 를 두지 않은 것** — 능력이 하나 늘면 이 파일이 컴파일되지 않는다. 새 능력이 조용히 "인프라 불필요" 로 분류되지 않는다.
|
||||
- **픽스처가 금지 대상 셋을 일부러 담고 있는 것** — 통제 평면 토큰·피어 인증서·원시 소켓 주소. 파일 안 주석이 그 의도를 적고("so the redactor is tested against data shaped like the real thing rather than against a string somebody invented for the assertion"), 테스트가 편집 전에 그 셋이 실제로 들어 있는지부터 단언한 뒤 편집 결과를 본다.
|
||||
|
||||
---
|
||||
|
||||
## Source anchors
|
||||
|
||||
```
|
||||
src/grpc-advanced/grpc-advanced-diagnostics/build.gradle:1-10
|
||||
main/java/…/diagnostics/GrpcAdvancedInfrastructureTestkit.java:1-87
|
||||
main/java/…/diagnostics/GrpcDiagnosticsRedactor.java:1-72
|
||||
main/java/…/diagnostics/GrpcChannelDiagnosticsSnapshot.java:1-63
|
||||
main/java/…/diagnostics/GrpcChannelDiagnosticsPolicy.java:1-55
|
||||
test/java/…/diagnostics/GrpcChannelDiagnosticsPolicyTest.java:1-229
|
||||
test/resources/xds/control-plane-snapshot.json:1-24
|
||||
```
|
||||
Reference in New Issue
Block a user