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>
15 lines
5.0 KiB
JSON
15 lines
5.0 KiB
JSON
{
|
|
"assetKey": "a-red-test-misread-as-a-product-defect-host-control",
|
|
"kind": "terminal",
|
|
"command": "d=$(mktemp -d)\ncat > $d/cp.gradle <<'GRADLE_EOF'\ngradle.projectsEvaluated {\n def p = rootProject.findProject(':adapter:outbound:httpclient')\n if (p != null) {\n p.tasks.register('printTestCp') {\n doLast { println 'TESTCP=' + p.sourceSets.test.runtimeClasspath.files.findAll { it.exists() }.join(':') }\n }\n }\n}\nGRADLE_EOF\ncat > $d/HostControl.java <<'JAVA_EOF'\nimport dev.caskeleton.adapter.outbound.httpclient.apache.ApacheBlockingTransportProvider;\nimport dev.caskeleton.adapter.outbound.httpclient.apache.ApacheFailureClassifier;\nimport dev.caskeleton.adapter.outbound.httpclient.api.operation.AttemptStage;\nimport dev.caskeleton.adapter.outbound.httpclient.profile.ClientProfile;\nimport dev.caskeleton.adapter.outbound.httpclient.profile.RuntimeGeneration;\nimport dev.caskeleton.adapter.outbound.httpclient.testkit.ClientProfiles;\nimport dev.caskeleton.adapter.outbound.httpclient.testkit.MockHttpServer;\nimport dev.caskeleton.adapter.outbound.httpclient.testkit.NoopLifecycleListener;\nimport dev.caskeleton.adapter.outbound.httpclient.testkit.TlsFixture;\nimport dev.caskeleton.adapter.outbound.httpclient.testkit.TlsMaterials;\nimport dev.caskeleton.adapter.outbound.httpclient.transport.TransportFailure;\nimport java.net.URI;\nimport java.util.Optional;\nimport org.springframework.web.client.RestClient;\n\npublic class HostControl {\n\n static String cut(String s) {\n if (s == null) return \"null\";\n s = s.replace('\\n', ' ');\n return s.length() > 96 ? s.substring(0, 96) + \"...\" : s;\n }\n\n static void run(String label, TlsFixture serverFixture, TlsFixture clientTrust, String host)\n throws Exception {\n try (MockHttpServer server = MockHttpServer.startTls(serverFixture.serverSocketFactory(), false)) {\n server.enqueueJson(200, \"{\\\"id\\\":1}\");\n URI base = server.uri(\"/\");\n URI root = new URI(base.getScheme(), null, host, base.getPort(), \"/\", null, null);\n URI target = new URI(base.getScheme(), null, host, base.getPort(), \"/users/1\", null, null);\n ClientProfile profile = ClientProfiles.builder(\"partner\").baseUrl(root).build();\n ApacheBlockingTransportProvider provider =\n new ApacheBlockingTransportProvider(\n Optional.empty(),\n ignored -> Optional.of(TlsMaterials.trustOnly(clientTrust)),\n ignored -> Optional.empty());\n System.out.println(\"=== \" + label + \" @\" + host + \" ===\");\n try {\n RestClient client =\n RestClient.builder()\n .requestFactory(\n provider.create(profile, new RuntimeGeneration(1), NoopLifecycleListener.INSTANCE))\n .build();\n try {\n client.get().uri(target).retrieve().body(String.class);\n System.out.println(\" 요청이 성공했다\");\n } catch (RuntimeException failure) {\n int i = 0;\n for (Throwable t = failure; t != null; t = t.getCause()) {\n System.out.println(\" [\" + (i++) + \"] \" + t.getClass().getSimpleName() + \" :: \" + cut(t.getMessage()));\n }\n TransportFailure c = new ApacheFailureClassifier().classify(failure, AttemptStage.TLS_HANDSHAKE);\n System.out.println(\" -> stage=\" + c.stage() + \" category=\" + c.category()\n + \" permanent=\" + c.category().permanent());\n }\n } finally {\n provider.close(profile, new RuntimeGeneration(1));\n }\n }\n }\n\n public static void main(String[] args) throws Exception {\n for (String host : new String[] {\"localhost\", \"127.0.0.1\"}) {\n // 테스트와 같은 픽스처 구성: 신뢰불가 CA 만 서버와 클라이언트가 서로 다른 신뢰재료를 쓴다.\n run(\"untrusted-authority\", TlsFixture.trusted(), TlsFixture.trusted(), host);\n TlsFixture expired = TlsFixture.expired();\n run(\"expired-certificate\", expired, expired, host);\n TlsFixture mismatch = TlsFixture.hostnameMismatch();\n run(\"hostname-mismatch\", mismatch, mismatch, host);\n }\n }\n}\nJAVA_EOF\nCP=$(./gradlew -I $d/cp.gradle :adapter:outbound:httpclient:printTestCp --offline --console=plain -q 2>/dev/null | grep '^TESTCP=' | head -1 | cut -d= -f2-)\njavac -nowarn -cp \"$CP\" -d $d $d/HostControl.java 2>&1 | head -5\necho '# 같은 서버·같은 신뢰재료로 접속 호스트만 바꾼다'\njava -cp \"$CP:$d\" HostControl 2>/dev/null\n",
|
|
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
|
|
"exitCode": 0,
|
|
"executedAt": "2026-09-02T01:51:16+00:00",
|
|
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
|
|
"raw": "evidence/raw/a-red-test-misread-as-a-product-defect-host-control.txt",
|
|
"svg": "evidence/rendered/a-red-test-misread-as-a-product-defect-host-control.svg",
|
|
"rawSha256": "b3c6814158e193cd01f98d39fd3c7229c673f7a42088236fd37da6d5c9c43b6a",
|
|
"lines": 32,
|
|
"redaction": "none — 코드베이스 정적 검색"
|
|
}
|