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>
103 lines
5.5 KiB
Plaintext
103 lines
5.5 KiB
Plaintext
# 값 타입의 컴팩트 생성자가 하는 검사 전부
|
|
22: public WebPushSubscriptionValue {
|
|
23: Objects.requireNonNull(endpoint, "endpoint");
|
|
24: Objects.requireNonNull(p256dh, "p256dh");
|
|
25: Objects.requireNonNull(authSecret, "authSecret");
|
|
26: Objects.requireNonNull(vapidKeyId, "vapidKeyId");
|
|
27: if (!isSecureOrLoopback(endpoint)) {
|
|
28: throw new IllegalArgumentException("endpoint must be https outside the loopback interface");
|
|
29: }
|
|
30: if (p256dh.length != P256DH_LENGTH) {
|
|
31: throw new IllegalArgumentException("p256dh must be an uncompressed P-256 point");
|
|
32: }
|
|
33: if (authSecret.length != AUTH_SECRET_LENGTH) {
|
|
34: throw new IllegalArgumentException("authSecret must be 16 bytes");
|
|
35: }
|
|
36: if (vapidKeyId.isBlank()) {
|
|
37: throw new IllegalArgumentException("vapidKeyId");
|
|
38: }
|
|
# 다섯째 검사의 구현 — 같은 파일 안의 private 메서드
|
|
50: private static boolean isSecureOrLoopback(java.net.URI endpoint) {
|
|
51: String scheme = endpoint.getScheme() == null ? "" : endpoint.getScheme();
|
|
52: if ("https".equalsIgnoreCase(scheme)) {
|
|
53: return true;
|
|
54: }
|
|
55: String host =
|
|
56: endpoint.getHost() == null ? "" : endpoint.getHost().toLowerCase(java.util.Locale.ROOT);
|
|
57: return "http".equalsIgnoreCase(scheme)
|
|
58: && ("127.0.0.1".equals(host) || "::1".equals(host) || "localhost".equals(host));
|
|
59: }
|
|
|
|
# 그 private 사본이 베낀 원본. 같은 모듈의 공개 함수다.
|
|
24: public static URI requireSecureOrLoopback(URI endpoint, String name) {
|
|
25: Objects.requireNonNull(endpoint, name);
|
|
26: String scheme =
|
|
27: endpoint.getScheme() == null ? "" : endpoint.getScheme().toLowerCase(Locale.ROOT);
|
|
28: if ("https".equals(scheme)) {
|
|
29: return endpoint;
|
|
30: }
|
|
31: String host = endpoint.getHost() == null ? "" : endpoint.getHost().toLowerCase(Locale.ROOT);
|
|
32: if ("http".equals(scheme) && LOOPBACK_HOSTS.contains(host)) {
|
|
33: return endpoint;
|
|
34: }
|
|
35: throw new IllegalArgumentException(name + " must use https outside the loopback interface");
|
|
|
|
# 같은 파일의 다른 공개 함수가 자기가 누구를 위한 것인지 적는다
|
|
47: * <p>{@link #requireSecureOrLoopback} checks the scheme and nothing else, so any HTTPS URL was
|
|
48: * accepted — including {@code https://169.254.169.254/}, the cloud metadata service, and any RFC
|
|
49: * 1918 address. Web Push endpoints and webhook targets are supplied by clients, which makes this
|
|
50: * a server-side request forgery primitive: the platform will happily fetch an internal address
|
|
51: * and, for a webhook, deliver the message body there.
|
|
# 그 함수가 실제로 하는 검사
|
|
65: public static URI requireExternallyRoutable(URI endpoint, String name, boolean allowLoopback) {
|
|
66: Objects.requireNonNull(endpoint, name);
|
|
67: requireSecureOrLoopback(endpoint, name);
|
|
68: if (endpoint.getUserInfo() != null) {
|
|
71: throw new IllegalArgumentException(name + " must not carry userinfo");
|
|
72: }
|
|
73: String host = endpoint.getHost();
|
|
74: if (host == null || host.isBlank()) {
|
|
75: throw new IllegalArgumentException(name + " has no host");
|
|
76: }
|
|
77: if (allowLoopback && isLoopback(endpoint)) {
|
|
78: return endpoint;
|
|
79: }
|
|
81: java.net.InetAddress[] resolved;
|
|
82: try {
|
|
83: resolved = java.net.InetAddress.getAllByName(host);
|
|
84: } catch (java.net.UnknownHostException unresolvable) {
|
|
85: throw new IllegalArgumentException(name + " does not resolve", unresolvable);
|
|
86: }
|
|
87: if (resolved.length == 0) {
|
|
88: throw new IllegalArgumentException(name + " does not resolve");
|
|
89: }
|
|
90: for (java.net.InetAddress address : resolved) {
|
|
94: if (isInternal(address)) {
|
|
95: throw new IllegalArgumentException(
|
|
96: name + " resolves to an address inside the deployment's own network");
|
|
97: }
|
|
98: }
|
|
99: return endpoint;
|
|
|
|
# 강한 쪽의 프로덕션 호출처
|
|
SesProviderProperties.java:27: NotificationEndpoints.requireExternallyRoutable(endpoint, "SES endpoint", true);
|
|
WebhookSubscription.java:49: NotificationEndpoints.requireExternallyRoutable(target, "webhook target", trusted);
|
|
# 그중 웹푸시: 0
|
|
# 값 타입이 두 공개 함수 중 하나라도 부르는 줄: 0
|
|
|
|
# 호출처 도달을 고정하려고 만든 테스트가, 무엇을 목록으로 들고 있는가
|
|
17: * <p>{@code EndpointRoutabilityTest} already proves {@code requireExternallyRoutable} rejects the
|
|
18: * metadata service, RFC 1918, link-local and the rest. It proved that for months while the function
|
|
19: * had no caller: both sites it was written for — a webhook target and an SES endpoint — kept
|
|
20: * calling {@code requireSecureOrLoopback}, which reads the scheme and nothing else. A green test on
|
|
21: * a control nothing invokes is the shape this repository keeps finding, and testing the helper
|
|
22: * again would not have caught it.
|
|
37: @DisplayName("a webhook target on the cloud metadata service is refused")
|
|
45: @DisplayName("a webhook target inside the deployment's own network is refused")
|
|
54: @DisplayName("a webhook target carrying userinfo is refused")
|
|
68: @DisplayName("an SES endpoint inside the deployment's own network is refused")
|
|
82: @DisplayName("a client-supplied webhook target on the loopback interface is refused")
|
|
95: @DisplayName("a client-supplied webhook target on 127.0.0.1 is refused")
|
|
106: @DisplayName("loopback stays available, because local and contract profiles address it")
|
|
# 그 파일에서 WebPush 를 언급하는 줄: 0
|