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>
79 lines
4.1 KiB
Plaintext
79 lines
4.1 KiB
Plaintext
# 능력 선언 — 제약을 평범한 문자열로만 담는 이유
|
|
* <p>Constraints are plain bounded strings on purpose. A capability record must stay serialisable
|
|
* into a report and safe to publish through an actuator endpoint, so it never stores a provider
|
|
* object — no {@code DataSource}, no {@code EntityManagerFactory}, no Hibernate {@code
|
|
* SessionFactory}. Holding one would drag a live resource into a value type and let a report leak a
|
|
* JDBC URL or credentials.
|
|
*/
|
|
public record CapabilitySupport(
|
|
JpaCapability capability, SupportLevel level, List<String> constraints) {
|
|
# 컴팩트 생성자가 강제하는 것
|
|
public CapabilitySupport {
|
|
Objects.requireNonNull(capability, "capability");
|
|
Objects.requireNonNull(level, "level");
|
|
constraints = List.copyOf(Objects.requireNonNull(constraints, "constraints"));
|
|
for (String constraint : constraints) {
|
|
if (constraint == null || constraint.isBlank()) {
|
|
throw new IllegalArgumentException("capability constraint must not be blank");
|
|
}
|
|
}
|
|
}
|
|
|
|
# 특권 리포트 — 담지 않는 것이 정의다
|
|
* <p>Deliberately narrow: the connected role, the schema search path, and two privilege answers.
|
|
* There is no JDBC URL, no password, and no host — this record is designed to be safe to surface in
|
|
* an actuator endpoint, and anything that would have to be masked before publishing does not belong
|
|
* in it at all.
|
|
*/
|
|
public record DatabasePrivilegeReport(
|
|
String currentUser, String searchPath, boolean canCreateInSchema, boolean canCreateInDatabase) {
|
|
|
|
# 밖으로 나가는 리포트 — 담을 수 없는 것
|
|
/**
|
|
* What the JPA platform is willing to say about itself over HTTP (design §37).
|
|
*
|
|
* <p>The interesting part of this type is what it cannot hold. There is no JDBC URL, no username,
|
|
* no password, no SQL, and no entity catalog — an actuator endpoint is reachable by anyone who
|
|
* reaches the management port, and every one of those would be a free reconnaissance answer.
|
|
*
|
|
* <p>What it does report is what an operator actually needs during an incident: which database
|
|
* major version is behind this instance, which provider, which schema version, and whether the
|
|
* safety properties this platform depends on are holding.
|
|
# 그 리포트가 특권 리포트를 담는 방식과, 그 불리언이 실제로 계산하는 것
|
|
/**
|
|
* Builds the report, deriving only bounded values from the privilege report.
|
|
*
|
|
* <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.
|
|
openInViewDisabled,
|
|
privileges != null && !privileges.holdsCreatePrivilege(),
|
|
capabilities);
|
|
# 역할 이름과 search_path 를 검사하는 정책의 main 호출자: 0
|
|
|
|
# 두 리포트의 컴포넌트 선언부에 있는 provider/자격증명 식별자 수
|
|
JpaPlatformReport.java: 0
|
|
DatabasePrivilegeReport.java: 0
|
|
|
|
# 그 리포트를 내보내는 엔드포인트
|
|
20:@Endpoint(id = "jpaplatform")
|
|
30: @ReadOperation
|
|
@WriteOperation / @DeleteOperation / @Selector 매치: 0
|
|
*
|
|
* <p>Read-only by construction: there is no write operation and no parameter. An endpoint that
|
|
* could trigger a migration, a repair, or a cache eviction would be an admin capability reachable
|
|
* over HTTP by whoever reaches the management port, which is exactly the boundary design §8.4 puts
|
|
* those operations behind.
|
|
|
|
# 다만 그 엔드포인트는 지금 노출되지 않는다
|
|
저장소에서 'jpaplatform' 이 나오는 곳: 1
|
|
JpaPlatformEndpoint.java:20:@Endpoint(id = "jpaplatform")
|
|
출하 설정의 노출 목록
|
|
258: include: health,prometheus,info,loggers,adapteractivation
|
|
259- # D2/D4/D5: explicitly excluded dangerous endpoints (env leaks secrets,
|
|
260- # heapdump/threaddump = memory forensics, shutdown = remote kill, configprops = secret leak).
|
|
등록 지점이 그것을 적어 둔다
|
|
*
|
|
* <p>{@code @Endpoint} alone registers nothing; exposure is still governed by the application's
|
|
* existing {@code management.endpoints} policy, which this deliberately does not widen.
|