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>
12 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 | a19-f008-acl | 브로커 ACL 매니페스트의 자기 점검이 존재하지 않는다 | messaging-and-outbox | clean-architecture-backend-template | 게시 전 | 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 | case:a19-f008-acl | 2026-09-04 | case-a19-f008-acl.body.md |
|
|
|
브로커 ACL 매니페스트의 자기 점검이 존재하지 않는다
messaging-security 의 BrokerAclManifest 자바독은 기동 대조와 파괴적 권한 거부를 적어 두었다. 두 검사 모두 이 record 안에 메서드로 있고 시험도 단언한다. 그 메서드를 부르는 main 코드가 BrokerAclManifest.java 밖에 없다.
관계
- ACL 매니페스트 전체가 쓰이지 않는다 저쪽은 이 타입의 소비자가 없다는 미해결 질문이고, 여기서는 그중 자바독이 적어 둔 두 검사가 어느 main 경로에서도 불리지 않는 것을 확인했다.
- 클래스 javadoc 이 강제되지 않는 규칙을 강제된다고 말한다 저쪽은 규칙이 코드로 없고 여기는 코드로 있는데 부르는 자리가 없다. 자바독만 읽는 사람에게 보이는 결과는 같다.
- 타입이 문서화한 불변식은 타입이 강제한다 이 record 는 그 규칙을 지킨 형태다. 거부가 조건부라 생성자가 아니라 가드 메서드로 두었고, 남은 문제는 그 가드를 부르는 자리다.
문제
브로커 권한 매니페스트가 출하 리프에 record 로 있다.
자바독이 적어 둔 두 검사가 어디까지 존재하고 어디서 불리는지 확인했다.
결론
두 검사 모두 이 record 안에 있다. 부르는 main 코드가 없다.
파괴적 권한 거부는 requireApplicationRuntime()(:103)이 한다. 선언된 파괴 권한이 비어 있지 않으면 APPLICATION_HOLDS_DESTRUCTIVE_GRANT 로 던진다. 기동 대조에 필요한 뺄셈은 undeclared(:122) 와 missing(:135) 에 양방향으로 있다.
CredentialRuntimeRegistryTest 한 파일이 이 타입을 17 줄에서 쓰는데, 그 시험은 파괴적 grant 를 넣은 매니페스트가 requireApplicationRuntime() 에서 예외를 내는 것과 undeclared() 가 선언에 없는 grant 를 돌려주는 것을 단언한다. 두 메서드를 부르는 자리가 그 시험뿐이다.
StartupProfileValidation 으로 감싸인 대상 셋에도 이 타입은 들어 있지 않다. 감싸인 것은 KafkaBrokerProfile 과 RabbitBrokerProfile 과 BrokerSecurityProfile 셋이다. 마지막 것은 이 매니페스트와 같은 패키지인데 매니페스트만 빠져 있다. 브로커에 권한을 질의하는 이름 여덟도 0 파일인데, Kafka AdminClient 자체가 main 에 없어서 그 0 이 말해 주는 범위는 좁다.
간결 생성자가 파괴 여부를 보지 않는 것은 결함이 아니다. 자바독이 거부한다고 적은 것은 애플리케이션 런타임이 파괴적 권한을 선언한 경우이고, 운영 평면의 principal 은 그것을 정당하게 선언한다. 조건을 아는 쪽이 부르는 가드가 맞고 그 가드가 이미 있다.
원본 분석의 등급은 P2 이고 이 기록은 새로 매기지 않는다. 자바독이 단정한 것이 코드로 없다는 P2 의 지렛대는 이 리비전에서 성립하지 않는다. 낮출 근거도 있는데, 검사 대상이 런타임에 만들어지지 않고 파괴 연산 인터페이스에도 main 구현이 없다는 것이다.
검증 환경
확인 방식 : 자바독 원문 확인, 대응 메서드 넷의 본문과 그것을 단언하는 시험 확인, 자기 파일 안팎을 나눈 호출자 계수, 이 타입의 생성·주입 지점 계수, Operation 열거값의 파괴 표시 확인, 기동 검증으로 감싸인 프로파일 전수, 브로커 ACL 질의 API 이름 여덟 검색과 AdminClient 의 소스 세트 분포, 파괴 연산 인터페이스의 구현 계수 소스 수정 : x
재현 조건
- BrokerAclManifest 의 클래스 자바독에서 적어 둔 두 검사를 읽는다.
- 그 두 검사에 대응하는 메서드가 있는지 파일 끝까지 읽는다.
- 그 메서드들을 부르는 자리를 자기 파일 안과 밖으로 나눠 세고 소스 세트도 가른다.
- 그 동작을 단언하는 시험의 이름과 단언 줄을 읽는다.
- 이 타입의 생성 지점과 주입 지점을 main 에서 찾는다.
- 간결 생성자가 검사하는 것을 나열하고, 거부가 무조건인지 조건부인지 자바독에서 확인한다.
- Operation 열거값의 파괴 표시를 읽고 자바독이 적어 둔 이름과 대조한다.
- StartupProfileValidation 으로 감싸인 대상을 전부 찾고 각각 무엇을 감싸는지 읽는다.
- 브로커 권한 질의 API 이름을 검색하고, 그 이름들이 속한 클라이언트가 main 에 있는지도 함께 센다.
- 파괴 연산을 담은 인터페이스의 main 구현을 센다.
본문
BrokerAclManifest 는 messaging-security 의 141줄짜리 record 다. 클래스 자바독에는 플랫폼이 기동 시점에 자신을 이 매니페스트와 대조한다고 적혀 있고, 선언한 것보다 많은 권한을 들고 있는 런타임은 finding 이라고 적혀 있다. 파괴적 권한을 선언한 애플리케이션 런타임은 그대로 거부된다고도 적혀 있다.
자바독이 적어 둔 두 검사
:::evidence key="a19-f008-acl" alt="저장소 루트에서 돌린 정적 검색 출력 125줄. BrokerAclManifest 의 줄 수와 자바독의 두 단정이 원문 그대로 먼저 나오고, 이어서 그 두 단정에 대응하는 메서드 넷의 본문이 실린다 — destructiveGrants, 파괴적 권한이 있으면 APPLICATION_HOLDS_DESTRUCTIVE_GRANT 로 던지는 requireApplicationRuntime, 그리고 관측된 권한과 선언을 양방향으로 비교하는 undeclared 와 missing 이다. 간결 생성자는 널 검사와 공백 검사와 리스트 복사 셋만 한다. 그 메서드들을 부르는 자리 전부가 나오는데 BrokerAclManifest.java 밖의 main 코드는 0 건이고, 이 타입을 만들거나 받는 main 코드도 0 건이며, 자기 파일 밖에서 이름이 나오는 파일은 시험 하나다. 그 시험이 파괴적 grant 에 예외가 나는 것과 선언에 없는 grant 를 골라내는 것을 각각 단언한다. 이어서 Operation 열거값 일곱과 파괴 표시 셋, 기동 검증으로 감싸인 프로파일 셋, DestructiveMessagingAdmin 을 구현하는 main 클래스 0, 브로커 권한 질의 API 이름 여덟이 모두 0 파일이고 Kafka AdminClient 자체가 test 에만 있다는 것이 보인다." caption="자바독의 두 검사 · 대응 메서드 넷의 본문 · 파일 밖 main 호출자 0 과 시험의 단언 · 기동 검증 대상 셋 · 파괴 연산 구현 0 · ACL 질의 0 과 그 0 의 의미 — 125줄 · exit 0" zoom="true" :::
두 번째 단정에 대응하는 것은 requireApplicationRuntime()(:103)이다. destructiveGrants() 로 선언된 파괴적 권한을 모아 비어 있지 않으면 MessagingConfigurationException 을 APPLICATION_HOLDS_DESTRUCTIVE_GRANT 코드로 던진다. 자바독 :99 에는 애플리케이션 런타임에 파괴적 권한을 주는 매니페스트를 거부한다고 적혀 있다.
첫 번째 단정에 대응하는 것은 undeclared(Set<Grant>)(:122)와 missing(Set<Grant>)(:135)다. 관측된 권한과 선언을 양방향으로 뺀다. :116~:117 에는 초과가 finding 이고 부족이 아니라고 적혀 있다 — 빠진 권한은 첫 사용에서 시끄럽게 실패하지만 선언되지 않은 여분은 남용될 때까지 눈에 띄지 않기 때문이다.
시험도 그 둘을 단언한다. CredentialRuntimeRegistryTest:167 의 anApplicationRuntimeMayNotHoldADestructiveGrant 가 :181 에서 requireApplicationRuntime 에 예외가 나는 것을, :187 의 aGrantTheBrokerHoldsButNobodyDeclaredIsTheFinding 가 :200 에서 undeclared 가 선언에 없는 grant 를 돌려주는 것을 확인한다.
부르는 프로덕션 코드가 없다
BrokerAclManifest.java 밖에서 이 네 메서드를 부르는 main 코드는 0 건이다. 이 타입을 생성하거나 파라미터로 받는 main 코드도 0 건이고, 자기 파일 밖에서 이름이 나오는 파일은 그 시험 하나다.
기동 시점 검증에도 이 타입이 없다. StartupProfileValidation 이 감싸는 것은 KafkaMessagingAutoConfiguration:59 의 Kafka 브로커 프로파일, RabbitMessagingAutoConfiguration:55 의 Rabbit 브로커 프로파일, MessagingCoreAutoConfiguration:221 의 compiled.security() 셋이다. 세 번째는 같은 messaging.security 계열인데 매니페스트는 그 목록에 없다.
브로커에 권한을 질의하는 코드도 없다. describeAcls·AclBinding·Authorizer 를 포함한 이름 여덟이 저장소 전체에서 0 파일이다. 다만 그 0 의 의미는 좁다 — Kafka AdminClient 자체가 main 에 없고 test 5 파일에만 있으므로, 애초에 맞을 수 있는 코드가 main 에 없었다.
간결 생성자에 넣을 검사는 아니다
:81~:87 의 간결 생성자는 grants 널 검사와 principal 공백 검사와 리스트 복사만 한다. 파괴 여부를 보지 않는다.
여기에 검사를 넣는 것이 수정 방향은 아니다. 자바독이 거부한다고 적은 것은 파괴적 권한 일반이 아니라 애플리케이션 런타임이 그것을 선언한 경우다. 운영 평면의 principal 은 DELETE 와 PURGE 를 정당하게 선언한다. 생성자에서 막으면 그 매니페스트를 이 타입으로 표현할 수 없다. 조건을 아는 쪽이 부르는 명시적 가드가 맞는 형태이고, 그 형태가 이미 :103 에 있다.
원문과 갈리는 자리
원문은 두 단정이 모두 실행되는 코드가 아니라고 적었고, 파괴적 권한을 선언한 매니페스트를 막는 코드가 record 자신에도 없다고 했다. 이 자리가 원문이 틀린 자리다. requireApplicationRuntime() 이 그 거부를 구현하고 시험이 그것을 고정한다. undeclared 와 missing 도 기동 대조에 필요한 비교를 갖고 있다. 없는 것은 구현이 아니라 그것을 부르는 자리다.
원문은 간결 생성자가 principal 과 pattern 공백을 검사한다고 적었다. pattern 공백 검사는 중첩된 Grant 의 생성자에 있다.
원문은 기동 검증으로 감싼 것을 브로커 프로파일 둘로 적었다. 셋이고, 세 번째가 이 매니페스트와 같은 패키지의 보안 프로파일이다.
원문이 인용한 자바독은 파괴적 권한으로 DELETE_TOPIC 과 PURGE 를 적는다. Operation 에 DELETE_TOPIC 이라는 값은 없고 DELETE 가 있으며, 파괴로 표시된 값은 ALTER·DELETE·PURGE 셋이다.
원문이 적은 "테스트 1건" 은 파일 수로는 맞다. 그 한 파일 안에서 이 타입이 나오는 줄은 17 이다.
등급에 대해
원본 분석의 등급은 P2 다. 이 리비전에서 그 등급을 떠받치던 근거 하나는 성립하지 않는다. 자바독이 단정한 것이 코드로 존재하지 않는다는 것이 P2 의 지렛대였는데, 코드는 있고 부르는 자리만 없다.
낮출 근거도 함께 있다. 프로덕션에서 이 매니페스트를 만드는 코드가 0 이라 검사할 대상이 런타임에 없고, 파괴 연산을 담은 DestructiveMessagingAdmin 은 main 구현이 0 이라 매니페스트가 거짓이어도 애플리케이션이 파괴 연산을 부를 수 없다.
이 기록은 등급을 새로 매기지 않는다. 상류가 P2 로 둔 근거와 여기서 확인한 반대 근거를 함께 남긴다.
확인하지 못한 것
그런 매니페스트를 실제로 만들어 아무 곳에서도 걸리지 않는 것을 실행으로 보이지는 않았다. requireApplicationRuntime() 을 부르는 main 코드가 없다는 데까지다.
브로커에 붙어 권한을 조회하지 않았다. 그런 조회를 하는 코드가 없다는 것까지 확인했다.
채택자가 DestructiveMessagingAdmin 을 구현해 넣는 배포는 보지 않았다. 이 저장소 main 에 구현이 0 이라는 것까지 확인했다.
채택자가 DestructiveMessagingAdmin 을 구현해 넣는 배포는 보지 않았다. 이 저장소 main 에 구현이 0 이라는 것까지 확인했다.