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>
10 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 | a05-f022-stable | 검증기는 도는데 정책을 넘기는 한 번의 호출이 없다 | capability-declaration-vs-proof | clean-architecture-backend-template | 게시 전 | 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 | case:a05-f022-stable | 2026-09-02 | case-a05-f022-stable.body.md |
|
|
|
검증기는 도는데 정책을 넘기는 한 번의 호출이 없다
런타임 롤 검증기가 빈으로 등록되고 프로덕션에서 실제로 권한을 읽는다. 다만 기동 시점이 아니라 액추에이터 리포트를 만들 때이고, 읽은 결과를 정책에 넘기지 않는다. 검증기 자신의 javadoc 은 기동 시점에 돌고 닫힌 방식으로 실패한다고 적는다.
관계
- 시작 검증기가 도는지는 그 능력에 자동설정 루트가 있는지와 일치한다 그 대응이 깨지는 경우다. 루트가 있고 검증기가 빈으로 등록되고 호출까지 되는데, 정책을 넘기는 호출만 빠져 있다.
- 지원 등급은 추론이 아니라 선언이고 증거 없이는 올라가지 않는다 등급이 뜻하는 것과 문서가 약속한 것이 다른 경우다.
- RLS가 성립하기 위한 세 전제 런타임 롤 속성이 격리 판정에 관여하는 다른 국면이다.
문제
보안 문서가 기동 실패 조건을 적는다. 런타임 롤이 허용 목록에 없거나 스키마나 데이터베이스에 생성 권한을 가지면 기동이 실패한다는 것이다.
검증기의 클래스 javadoc 은 더 직접적이다. 검증이 기동 시점에 돌고 닫힌 방식으로 실패한다고 적는다.
결론
배선 자체는 되어 있다. 자동설정이 검증기를 빈으로 만들고, 액추에이터 리포트를 만들 때마다 roleVerifier.verify(dataSource) 를 부른다.
기동 시점이 아니다. 기동 검사 빈에 걸린 것은 위험 설정 가드 하나이고, 그 가드의 인자에 롤 정책이 없다.
닫히지도 않는다. 리포트를 만드는 쪽이 검증기의 예외를 잡아 널을 돌려준다. 실패는 기동을 막는 대신 미검증 표시가 된다.
정책이 보는 항목은 넷이다. 허용 목록, 스키마 생성 권한, 데이터베이스 생성 권한, 검색 경로다. 그 정책에 검증 결과를 넘기는 두 인자짜리 메서드가 검증기에 있고, 그것을 부르는 곳은 코드베이스 전체에 하나다. PostgreSQL 보안 계약 시험이다.
정책 객체를 만드는 main 코드는 0 이다. 언급하는 파일을 세면 자기 자신, 검증기, 시험 둘이다.
액추에이터의 검증 완료 표시는 별개의 문제다. 권한 보고서가 널이 아닌지와 생성 권한을 갖지 않는지 둘로 계산한다. 그 넷 중 가운데 둘만 들어간다.
그 위 javadoc 은 현재 사용자와 검색 경로를 하나의 불리언으로 일부러 줄였다고 적고, 운영자는 그 롤이 검증을 통과했는지를 알면 된다고 덧붙인다. 이 축소가 하는 일은 둘이다. 리포트에서 어느 롤인지가 빠지고, 그 롤이 허용 목록과 검색 경로 정책을 통과했는지도 같이 빠진다.
그 불리언을 고정하는 시험 셋은 입력의 롤이 전부 허용된 이름이고 검색 경로도 전부 안전하다. 허용 목록 밖 롤을 넣은 입력이 없다. 정책 쪽 단위 시험은 바로 그 두 조합을 넣지만 액추에이터 불리언은 보지 않는다.
능력 등급 자체는 다른 이야기다. 안정 등급이란 계약 시험 스위트가 매트릭스 전체를 검증했다는 뜻이고, 그 시험 자체는 존재한다. 어긋난 것은 등급이 아니라 문서와 javadoc 이 약속한 기동 실패, 그리고 액추에이터 불리언의 의미다.
검증 환경
OpenJDK : 해당 없음. 정적 검색이다. 확인 방식 : 검증기 호출 경로 추적, 정책의 검사 항목 열람, 액추에이터 계산식과 그 시험 입력 대조 소스 수정 : x
재현 조건
- 검증기의 클래스 javadoc 과 보안 문서의 기동 실패 조건을 읽는다.
- 검증기의 verify 를 부르는 프로덕션 코드를 찾고, 그 호출이 언제 일어나는지 본다.
- 그 호출을 감싼 코드가 예외를 어떻게 다루는지 읽는다.
- 정책이 검사하는 항목을 열거하고, 정책을 넘기는 두 인자짜리 메서드의 호출처를 레포 전체에서 센다.
- 정책 객체를 만드는 main 코드와 그 타입을 언급하는 파일을 센다.
- 액추에이터의 검증 완료 계산식과 그것을 고정하는 시험의 입력을 나란히 본다.
- 안정 등급의 정의를 읽는다.
본문
검증기의 클래스 javadoc 이 이렇게 적는다.
* <p>The verification runs at startup and fails closed. Discovering after an incident that the
* application's own credential could drop tables is discovering it too late.
보안 문서도 같은 방향으로 적는다. 롤이 허용 목록에 없거나 생성 권한을 가지면 기동이 실패한다는 것이다.
검증기는 돈다. 기동 시점이 아닐 뿐이다
:::evidence key="a05-f022-stable" alt="검증기 클래스의 javadoc 주장, 정책이 검사하는 네 항목, 검증기를 프로덕션에서 부르는 코드와 그 예외 처리, 정책을 넘기는 두 인자짜리 메서드와 그 호출처와 정책 객체를 만드는 코드 수, 기동 검사 빈이 실행하는 것, 액추에이터의 검증 완료 계산식과 그 위 javadoc, 그 표시를 고정하는 시험의 입력과 정책 쪽 단위 시험의 입력, 그리고 안정 등급의 정의를 출력한 터미널 기록." caption="javadoc 은 기동 시점·닫힌 실패를 주장 · 정책은 네 항목 검사 · verify 는 리포트 요청 때 불리고 예외는 널로 삼켜짐 · 두 인자짜리 호출처는 계약 시험 하나, 정책 생성 0 · 표시는 네 항목 중 둘만 · 시험 입력에 허용 목록 밖 롤 없음 — 63줄 · exit 0" zoom="true" :::
private DatabasePrivilegeReport readPrivileges(DataSource dataSource) {
try {
return roleVerifier.verify(dataSource);
} catch (IllegalStateException unverified) {
return null;
}
}
액추에이터 리포트를 만들 때 불린다. 기동 검사 빈이 실행하는 것은 위험 설정 가드 하나이고, 그 가드는 롤 정책을 인자로 받지 않는다.
닫히지도 않는다. 검증기의 예외는 널이 되고, 널은 미검증 표시가 된다.
정책을 넘기는 호출이 없다
정책은 네 항목을 검사한다.
if (!allowedRoles.contains(currentUser)) { ... }
if (report.canCreateInSchema()) { ... }
if (report.canCreateInDatabase()) { ... }
searchPathPolicy.requireSafe(report.searchPath());
검증 결과를 그 정책에 넘기는 메서드는 검증기에 있다.
public void requireSafe(DataSource dataSource, DatabaseRolePolicy policy) {
Objects.requireNonNull(policy, "policy");
policy.requireSafe(verify(dataSource));
}
그것을 부르는 곳은 코드베이스 전체에 하나이고, PostgreSQL 보안 계약 시험이다. 정책 객체를 만드는 main 코드는 0 이므로 프로덕션에서는 그 메서드를 부를 수도 없다. 정책 타입을 언급하는 파일은 자기 자신과 검증기, 그리고 시험 둘뿐이다.
액추에이터 불리언의 계산식
privileges != null && !privileges.holdsCreatePrivilege(),
정책이 검사하는 네 항목 중 가운데 둘만 들어간다. 허용 목록도 검색 경로도 계산에 없다.
그 위 javadoc 이 이렇게 적는다.
* <p>The privilege report's {@code currentUser} and {@code searchPath} are deliberately reduced
* to a single boolean here: an operator needs to know the runtime role passed verification, not
* which role it is.
축소는 두 가지를 동시에 한다. 어느 롤인지를 리포트에서 지우고, 그 롤이 허용 목록과 검색 경로 정책을 통과했는지도 함께 지운다. 문서가 기동 실패 조건으로 지목한 값이 그 둘이다.
같은 불리언이 플랫폼 안전 판정에도 그대로 들어간다.
return openInViewDisabled && runtimeRoleVerified;
두 시험이 각각 절반만 덮는다
이 불리언을 고정하는 시험은 셋인데, 입력의 롤이 전부 app_runtime 이고 검색 경로도 전부 안전하다.
new DatabasePrivilegeReport("app_runtime", "app, pg_catalog", false, false)
new DatabasePrivilegeReport("app_runtime", "app", true, false)
허용 목록 밖 롤을 넣은 입력이 없으니 시험은 통과한다.
정책 쪽 단위 시험은 바로 그 조합을 넣는다.
new DatabasePrivilegeReport("postgres", "app", false, false)
new DatabasePrivilegeReport("app_runtime", "app, public", false, false)
다만 그 시험은 정책 객체만 보고 액추에이터 불리언은 보지 않는다. 둘 사이의 틈을 아무도 보지 않는다.
등급은 어긋나지 않았다
안정 등급의 정의는 계약 시험 스위트가 전체 PostgreSQL 매트릭스에서 검증했다는 것이고, 그 시험은 실제로 있다. 두 인자짜리 호출이 있는 유일한 자리가 바로 그 시험이다.
어긋난 것은 등급이 아니다. 문서와 javadoc 이 약속한 기동 실패가 어디서도 일어나지 않고, 액추에이터가 내는 판정이 정책의 네 항목 중 둘만 반영한다.
확인하지 못한 것
허용 목록 밖 롤로 기동해 실패하지 않는 것을 재현하지 않았다. 정적 도달성과 계산식까지만 확인했다.