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>
162 lines
12 KiB
Plaintext
162 lines
12 KiB
Plaintext
Sub-scope 08 (inbound-web) observability + proxy + filter + mvc/webflux remainder
|
|
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
|
generatedAt=2026-08-30T07:37:35+00:00
|
|
|
|
=== OWNED FILES ===
|
|
ad29fda6cf666e83673267bf4a5e9f8e1398f9c1 main/java/dev/caskeleton/adapter/inbound/web/advanced/mvc/MvcDisconnectDetector.java 86
|
|
479a4cb2cd2cd52a9c4209a55ced6c892f2f6b50 main/java/dev/caskeleton/adapter/inbound/web/advanced/mvc/MvcStreamWriter.java 117
|
|
5e11dd088b32542688596629d9ba2bda45d266f5 main/java/dev/caskeleton/adapter/inbound/web/advanced/mvc/MvcStreamingExecutorConfiguration.java 86
|
|
9bec0eae7bdf9aed96fdbe07eb34b0c60fb4209a main/java/dev/caskeleton/adapter/inbound/web/advanced/mvc/VirtualThreadMvcConfiguration.java 77
|
|
e53c1995fe4995c04af32faafd868735c2f9f8f2 main/java/dev/caskeleton/adapter/inbound/web/advanced/mvc/VirtualThreadSettings.java 103
|
|
ee140492d372bea76d992f1058c6b244f6da6346 main/java/dev/caskeleton/adapter/inbound/web/advanced/webflux/ControlledBlockingBridge.java 70
|
|
857177965a147a7debc03b0e60fb4828b6520631 main/java/dev/caskeleton/adapter/inbound/web/advanced/webflux/SlowConsumerClosedException.java 32
|
|
31fa7e1b220c27fcff7709efc086a83dbe304e91 main/java/dev/caskeleton/adapter/inbound/web/advanced/webflux/WebFluxDisconnectDetector.java 72
|
|
64cb4db2c9e741b3f780a63b0e3e9e9d25466398 main/java/dev/caskeleton/adapter/inbound/web/advanced/webflux/WebFluxSseAdapter.java 117
|
|
77d744ec0b05c69a27f3f30e6fab7e6d428d045e main/java/dev/caskeleton/adapter/inbound/web/advanced/webflux/WebFluxSseHeartbeat.java 40
|
|
be63236a6dfaaee182f394c2edadce3a03427b34 main/java/dev/caskeleton/adapter/inbound/web/advanced/webflux/WebFluxStreamAdmission.java 124
|
|
f88258c1b19569a1765ed563e0866e062772b0e5 main/java/dev/caskeleton/adapter/inbound/web/advanced/webflux/WebFluxStreamWriter.java 70
|
|
841c398a94920699fde61451184f61c243a4a7e1 main/java/dev/caskeleton/adapter/inbound/web/filter/CacheControlFilter.java 35
|
|
0032f474ad554cd7276b3da819ad4d091d9c8d11 main/java/dev/caskeleton/adapter/inbound/web/filter/RequestLoggingFilter.java 147
|
|
4a2ddb15de9ee9d6d7ecb6c9d3d5386fc36686f0 main/java/dev/caskeleton/adapter/inbound/web/mvc/context/WebMvcRequestContextArgumentResolver.java 42
|
|
2a32453e80f6539a5427be10dbcf6634df561586 main/java/dev/caskeleton/adapter/inbound/web/mvc/context/WebMvcRequestContextHolder.java 66
|
|
250ebee6de1c046ea069d6b19e8927d77506fe49 main/java/dev/caskeleton/adapter/inbound/web/mvc/filter/WebMvcEvidenceFilter.java 114
|
|
13aff34a24f8c0224de66bb6bbe528411bfa495d main/java/dev/caskeleton/adapter/inbound/web/mvc/filter/WebMvcRequestIdFilter.java 146
|
|
dd75b418a205c76415f14ee546493e6b077739de main/java/dev/caskeleton/adapter/inbound/web/observability/HeaderSanitizer.java 25
|
|
ef9f1d3afe0b5e3f9cf29492c01ea0fdfbbb1695 main/java/dev/caskeleton/adapter/inbound/web/observability/MdcCorrelationIdPortAdapter.java 16
|
|
5e4cfcea7b360e37b132f51c9ddbb532511d753b main/java/dev/caskeleton/adapter/inbound/web/observability/MdcKeys.java 13
|
|
a790a5ee999056de8a0238ada60a319a61b3b45b main/java/dev/caskeleton/adapter/inbound/web/observability/ResponseMetaFactory.java 18
|
|
e18414ead91ed2945a8d32cd221b09754b2942a0 main/java/dev/caskeleton/adapter/inbound/web/observability/RetryAfterAdvisor.java 43
|
|
7a8ab4e5f306fe176e4fe503bc9ae25e59b97b5e main/java/dev/caskeleton/adapter/inbound/web/observability/WebAccessLogEvent.java 65
|
|
fd84d9ed27536784f87a92a247b7845fa524b4f0 main/java/dev/caskeleton/adapter/inbound/web/observability/WebAccessLogger.java 19
|
|
9a3056466bd7efac7ebf88e459a51c4bddbdc050 main/java/dev/caskeleton/adapter/inbound/web/observability/WebAuditAction.java 41
|
|
7118a82494a8b306838c92da8a181482a512cb7c main/java/dev/caskeleton/adapter/inbound/web/observability/WebAuditEvent.java 55
|
|
43cca5eb139a9ce6ccd417b9dbbab1b624f4b57b main/java/dev/caskeleton/adapter/inbound/web/observability/WebAuditPublisher.java 17
|
|
3c8f55205e241c8b9f6a95e76f9046dd78612080 main/java/dev/caskeleton/adapter/inbound/web/observability/WebMetricCardinalityPolicy.java 112
|
|
de2b95f45330d7c180edd647222741f8cc5103c5 main/java/dev/caskeleton/adapter/inbound/web/observability/WebObservationConvention.java 72
|
|
b9f52ddfb6d07054753d320b4bc5888a7011390a main/java/dev/caskeleton/adapter/inbound/web/proxy/ForwardedHeaderSanitizer.java 72
|
|
086d1b881e91c24e5e043fd0a7a7d7e263bb4f35 main/java/dev/caskeleton/adapter/inbound/web/proxy/NormalizedForwardedHeaders.java 158
|
|
f39d3d998e2851c95a2a1cd0bcda239ee751e4f1 main/java/dev/caskeleton/adapter/inbound/web/proxy/TrustedProxyPolicy.java 161
|
|
e1b3b88b46aecb7ccb502b9202623bbde9507549 main/java/dev/caskeleton/adapter/inbound/web/proxy/UntrustedForwardedHeaderException.java 30
|
|
d2ffecb95fce15ef375f368f706a8ff834e6368b main/java/dev/caskeleton/adapter/inbound/web/webflux/context/WebFluxRequestContextAccessor.java 73
|
|
88a2668dc8cd7323a80ef1cccedc6467c0df56c5 main/java/dev/caskeleton/adapter/inbound/web/webflux/context/WebFluxRequestContextFilter.java 147
|
|
1afebbb62bb9d3a468fe7dbc325873d889b5eeda main/java/dev/caskeleton/adapter/inbound/web/webflux/guard/BlockingCallDetectedException.java 26
|
|
b467ec7a0458c29b875e6eee1559b835e80223b5 main/java/dev/caskeleton/adapter/inbound/web/webflux/guard/BlockingDependencyGuard.java 79
|
|
cba639f6f927be1139777ac2295ed5228fc95f58 test/java/dev/caskeleton/adapter/inbound/web/advanced/mvc/MvcAdvancedConfigurationTest.java 145
|
|
a3bd77e8e99c29eb47ee968fa02f4d71ae373148 test/java/dev/caskeleton/adapter/inbound/web/advanced/mvc/MvcStreamingWiringTest.java 175
|
|
f200ed3eb0fe0db3b5c3fdc7f905f826d8e51b61 test/java/dev/caskeleton/adapter/inbound/web/advanced/webflux/WebFluxStreamingTest.java 309
|
|
c9a604d350f8de1b7884aa2558bc0f02cbb4bfc3 test/java/dev/caskeleton/adapter/inbound/web/filter/CacheControlFilterTest.java 52
|
|
dd6e28c879899589a97a0fe2227935b6a25c4615 test/java/dev/caskeleton/adapter/inbound/web/filter/RequestLoggingFilterTest.java 296
|
|
0be7101fe31a3fba785072a2a84aa39bdd5bfb8a test/java/dev/caskeleton/adapter/inbound/web/mvc/filter/WebMvcEvidenceFilterTest.java 78
|
|
4810257df962a1b5d914e13cbb51a1e774f4003e test/java/dev/caskeleton/adapter/inbound/web/mvc/filter/WebMvcRequestIdFilterTest.java 71
|
|
793dbbbb7030d97017fa2ceb879b94b4622cda5e test/java/dev/caskeleton/adapter/inbound/web/observability/HeaderSanitizerTest.java 39
|
|
bdfb21f0cc2ff54ef55b1616067c5ce72131e1fc test/java/dev/caskeleton/adapter/inbound/web/observability/MdcCorrelationIdPortAdapterTest.java 36
|
|
4e01d57fc524f6ce9e529e3c67a24ab303c33185 test/java/dev/caskeleton/adapter/inbound/web/observability/ResponseMetaFactoryTest.java 34
|
|
9d8bfb0ccfdd2510d49e6b044b9bc690fdd6029e test/java/dev/caskeleton/adapter/inbound/web/observability/RetryAfterAdvisorTest.java 85
|
|
b6d0c36a1538ddd23daa94b47eb6af86a4acdbb5 test/java/dev/caskeleton/adapter/inbound/web/observability/WebObservabilityPolicyTest.java 188
|
|
bc4a4a26a90697c783240dcf4f70d2cd3a3fa4bc test/java/dev/caskeleton/adapter/inbound/web/proxy/ForwardedHeaderSanitizerTest.java 129
|
|
a9023edbbe9bae94c9539ec423a354af82467382 test/java/dev/caskeleton/adapter/inbound/web/webflux/context/WebFluxRequestContextAccessorTest.java 93
|
|
6dd0ac4a3cddcf083a07d1132211e209933d169d test/java/dev/caskeleton/adapter/inbound/web/webflux/guard/BlockingDependencyGuardTest.java 92
|
|
file_count=53
|
|
|
|
=== 8.2 X-Request-Id 를 쓰는 두 배선 필터 ===
|
|
$ grep -n 'trustInboundRequestId\|REQUEST_ID_HEADER\|setHeader' adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/mvc/filter/WebMvcRequestIdFilter.java
|
|
42: public static final String REQUEST_ID_HEADER = "X-Request-Id";
|
|
54: private final boolean trustInboundRequestId;
|
|
65: * @param trustInboundRequestId whether a client may choose the request id, which only a trusted
|
|
69: public WebMvcRequestIdFilter(boolean trustInboundRequestId, int order) {
|
|
70: this.trustInboundRequestId = trustInboundRequestId;
|
|
85: response.setHeader(REQUEST_ID_HEADER, requestId.value());
|
|
106: if (!trustInboundRequestId) {
|
|
109: return sanitized(request.getHeader(REQUEST_ID_HEADER))
|
|
exit=0
|
|
|
|
$ grep -n 'HEADER_REQUEST_ID\|resolveOrGenerate\|setHeader\|MDC.put' adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/filter/RequestLoggingFilter.java | head -12
|
|
40: private static final String HEADER_REQUEST_ID = "X-Request-Id";
|
|
56: String requestId = resolveOrGenerate(req.getHeader(HEADER_REQUEST_ID));
|
|
57: String correlationId = resolveOrGenerate(req.getHeader(HEADER_CORRELATION_ID));
|
|
58: res.setHeader(HEADER_REQUEST_ID, requestId);
|
|
59: res.setHeader(HEADER_CORRELATION_ID, correlationId);
|
|
61: MDC.put(MdcKeys.REQUEST_ID, requestId);
|
|
62: MDC.put(MdcKeys.CORRELATION_ID, correlationId);
|
|
65: TraceParent traceParent = resolveOrGenerateTraceParent(req.getHeader(HEADER_TRACEPARENT));
|
|
66: MDC.put(MdcKeys.TRACE_ID, traceParent.traceId());
|
|
67: MDC.put(MdcKeys.SPAN_ID, traceParent.spanId());
|
|
68: res.setHeader(HEADER_TRACEPARENT, traceParent.toHeader());
|
|
91: private static String resolveOrGenerate(String inbound) {
|
|
exit=0
|
|
|
|
$ grep -n 'class RequestLoggingFilter' -B 3 adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/filter/RequestLoggingFilter.java
|
|
33- * trace_id} when a real tracer is later wired in.
|
|
34- */
|
|
35-@Component
|
|
36:public class RequestLoggingFilter extends OncePerRequestFilter {
|
|
exit=0
|
|
|
|
$ cat adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/observability/HeaderSanitizer.java | grep -vE '^import|^package'
|
|
|
|
/**
|
|
* Neutralizes inbound header values before they reach MDC / logs by stripping every {@code \r},
|
|
* {@code \n} and ASCII control char ({@code < 0x20}) and length-capping the result. See README for
|
|
* the design rationale.
|
|
*/
|
|
public final class HeaderSanitizer {
|
|
|
|
private HeaderSanitizer() {}
|
|
|
|
public static String sanitize(String raw, int maxLength) {
|
|
if (raw == null) {
|
|
return null;
|
|
}
|
|
StringBuilder sb = new StringBuilder(Math.min(raw.length(), maxLength));
|
|
for (int i = 0; i < raw.length() && sb.length() < maxLength; i++) {
|
|
char c = raw.charAt(i);
|
|
if (c >= 0x20) {
|
|
sb.append(c);
|
|
}
|
|
}
|
|
return sb.toString();
|
|
}
|
|
}
|
|
exit=0
|
|
|
|
=== 8.1 proxy 패키지 도달성 ===
|
|
TrustedProxyPolicy 1 main 1 test 1 testkit
|
|
NormalizedForwardedHeaders 2 main 2 test 1 testkit
|
|
ForwardedHeaderSanitizer 1 test 1 testkit
|
|
WebAuditPublisher
|
|
WebAccessLogger 1 main
|
|
$ grep -rn 'forward-headers-strategy' app-bootstrap/src/main/resources/application.yml
|
|
321: forward-headers-strategy: ${APP_SERVER_FORWARD_HEADERS_STRATEGY:framework}
|
|
exit=0
|
|
|
|
$ grep -n 'void ' adapter/inbound/web/src/nginxProxyTest/java/dev/caskeleton/adapter/inbound/web/testkit/proxy/NginxProxyContractIT.java | head -12
|
|
37: static void requireDocker() {
|
|
47: static void stopProxy() {
|
|
63: void attackerCannotOverrideForwardedHost() {
|
|
79: void attackerCannotDowngradeTheForwardedScheme() {
|
|
93: void attackerCannotForgeTheClientAddress() {
|
|
107: void standardForwardedHeaderIsCleared() {
|
|
123: void prefixIsAppliedExactlyOnce() {
|
|
134: void publishedUriCarriesTheIncomingPrefix() {
|
|
143: void clientCannotInjectAPrefix() {
|
|
157: void bodyWithinTheProxyLimitArrives() {
|
|
167: void oversizedBodyIsRefusedAtTheProxy() {
|
|
180: void unknownPrefixIsNotProxied() {
|
|
exit=0
|
|
|
|
$ grep -rn 'TrustedProxyPolicy.of' adapter/inbound/web/src/testkit
|
|
adapter/inbound/web/src/testkit/java/dev/caskeleton/adapter/inbound/web/testkit/proxy/ProxyFixtureController.java:53: TrustedProxyPolicy.of("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8"));
|
|
exit=0
|
|
|
|
=== 8.4 ExternalRequestContext 생성 ===
|
|
$ sed -n '140,146p' adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/webflux/context/WebFluxRequestContextFilter.java
|
|
private ExternalRequestContext externalRequest(ServerHttpRequest request) {
|
|
var uri = request.getURI();
|
|
String scheme = uri.getScheme() == null ? "http" : uri.getScheme();
|
|
int port = uri.getPort() > 0 ? uri.getPort() : ("https".equalsIgnoreCase(scheme) ? 443 : 80);
|
|
String host = uri.getHost() == null ? "localhost" : uri.getHost();
|
|
return new ExternalRequestContext(scheme, host, port, "");
|
|
}
|
|
exit=0
|
|
|