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>
142 lines
17 KiB
Plaintext
142 lines
17 KiB
Plaintext
Sub-scope 04 (notification) platform/template + platform/security — manifest and probes
|
|
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
|
generatedAt=2026-08-30T06:49:18+00:00
|
|
|
|
=== OWNED FILES ===
|
|
9346bef6c5dd48f99d3844f3e11eef3ae25e466e main/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmCallbackPayloadProtection.java 144
|
|
71ca753a825184bcf3097323883f4d27919fa29c main/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmContactPointProtector.java 220
|
|
dba783d4abc8a1a3353de194c360656fe55ecdda main/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmNotificationPayloadProtection.java 159
|
|
5a214c569bbd6aef67ec7e9368cd5de6f4baf7ae main/java/dev/caskeleton/adapter/outbound/notification/platform/security/CredentialGeneration.java 69
|
|
e7354819fdffe7830c2c69e3819b6b6006335657 main/java/dev/caskeleton/adapter/outbound/notification/platform/security/HmacProviderRequestIdHasher.java 53
|
|
f2cc413558aa0fd9cc4f23f796ab21eb46db0d26 main/java/dev/caskeleton/adapter/outbound/notification/platform/security/ProviderCredentialManager.java 228
|
|
ab92af842fdbc57398d5fa304d483ef5ae9dc3e7 main/java/dev/caskeleton/adapter/outbound/notification/platform/security/SettingsSecretMaterialProvider.java 49
|
|
f0cb06f7f5e16b65aea7e2ede4a5128c3224cddd main/java/dev/caskeleton/adapter/outbound/notification/platform/template/CanonicalNotificationRenderer.java 185
|
|
de60cca7689ab2744368b1ec01b6e1602d7529a6 main/java/dev/caskeleton/adapter/outbound/notification/platform/template/JacksonInboxContentCodec.java 78
|
|
2ee51493e045e65b15bac6e613d8eb5fd0effefc main/java/dev/caskeleton/adapter/outbound/notification/platform/template/JacksonNotificationVariablesCodec.java 52
|
|
a3bac7bcbe260d9da1c700405c7e068cebefc603 main/java/dev/caskeleton/adapter/outbound/notification/platform/template/JacksonTemplateContentCodec.java 34
|
|
46b2a550a65f32b9189d66feee894acf963e7271 main/java/dev/caskeleton/adapter/outbound/notification/platform/template/JsonSchemaVariableValidator.java 63
|
|
df06a2611c45006150e5abab4c6f5dededb5c597 main/java/dev/caskeleton/adapter/outbound/notification/platform/template/NotificationDigest.java 31
|
|
84940d4cf820ac09a0ee2efffb69d33f462ce72f main/java/dev/caskeleton/adapter/outbound/notification/platform/template/NotificationJsonMapper.java 25
|
|
5c9fda8dad8c514638f54b0242aa33adc228cd9e main/java/dev/caskeleton/adapter/outbound/notification/platform/template/NotificationTemplateEngine.java 42
|
|
50cc859f260939c56110cdc16ad0a0e0028478c8 main/java/dev/caskeleton/adapter/outbound/notification/platform/template/PlaceholderTemplateEngine.java 68
|
|
15a9409b412feb1965b3b6e3980c5cc7a5b822e0 main/java/dev/caskeleton/adapter/outbound/notification/platform/template/Sha256MessageDigestAdapter.java 12
|
|
24351d1a05410d0c1a6a828c22ff945a5094d2e0 main/java/dev/caskeleton/adapter/outbound/notification/platform/template/TemplateSlotMode.java 29
|
|
aa4c6fa386f64e0cd26cfe2a510296e158a23618 main/java/dev/caskeleton/adapter/outbound/notification/platform/template/TemplateSlotPolicy.java 129
|
|
b7e6a77d01988109f8a52ca18321f106dcbd2b8c main/java/dev/caskeleton/adapter/outbound/notification/platform/template/ThymeleafNotificationRenderer.java 50
|
|
ef52533c065b81d09e74461628e5dc7ee3b52761 main/java/dev/caskeleton/adapter/outbound/notification/platform/template/ThymeleafStringTemplateEngine.java 162
|
|
ca195f7f8facaba4926929311a0519169a2cfce2 test/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmContactPointProtectorTest.java 89
|
|
57066ca6a3bd5cd4effe1ba9b7cbda14c88c4a96 test/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmNotificationPayloadProtectionTest.java 195
|
|
ed5627bb3158dc87ce83b3c10bc05289e1ba5ca7 test/java/dev/caskeleton/adapter/outbound/notification/platform/security/CallbackPayloadBoundTest.java 108
|
|
3bb01206fc668913558bfe3b14a1d93495f6c235 test/java/dev/caskeleton/adapter/outbound/notification/platform/security/CallbackPayloadRotationTest.java 99
|
|
520a7a954f32e01f17531ebf325e965987e95bb6 test/java/dev/caskeleton/adapter/outbound/notification/platform/security/CredentialDrainWindowTest.java 171
|
|
cc4491121483e729536258f302d7309cd1d287d4 test/java/dev/caskeleton/adapter/outbound/notification/platform/security/ProviderCredentialManagerConcurrencyTest.java 100
|
|
20aadd974a813ee2cbfe78600107d2b3fc94c515 test/java/dev/caskeleton/adapter/outbound/notification/platform/security/SecurityFixtures.java 117
|
|
3635d36869434d2cc757968cf908d309152f1407 test/java/dev/caskeleton/adapter/outbound/notification/platform/template/BothEnginesHonourSlotModeTest.java 100
|
|
6ba89f7fec2f42c6c5d4f3f7b19b4c3bfde5cfcb test/java/dev/caskeleton/adapter/outbound/notification/platform/template/CanonicalNotificationRendererTest.java 148
|
|
b75f5ab0227fa81adf6020f67fc6666acecf72ca test/java/dev/caskeleton/adapter/outbound/notification/platform/template/SlotAwareRenderingTest.java 127
|
|
de4160b12485e82ac23df8827de5dbd4306c4ecc test/java/dev/caskeleton/adapter/outbound/notification/platform/template/ThymeleafNotificationRendererTest.java 179
|
|
|
|
=== 8.1 AccessContext: 감사 컨텍스트를 읽는 코드가 있는가 ===
|
|
$ grep -rn '\.auditRequired()' --include=*.java . | wc -l
|
|
0
|
|
exit=0
|
|
|
|
$ grep -rn '\.purposeCode()' --include=*.java . | wc -l
|
|
0
|
|
exit=0
|
|
|
|
$ grep -rn 'AccessContext' --include=*.java . | grep -v import | grep -v 'AttachmentAccessContext'
|
|
./application-core/src/main/java/dev/caskeleton/application/notification/platform/security/AccessContext.java:6:public record AccessContext(String purposeCode, String actorRef, boolean auditRequired) {
|
|
./application-core/src/main/java/dev/caskeleton/application/notification/platform/security/AccessContext.java:8: public AccessContext {
|
|
./application-core/src/main/java/dev/caskeleton/application/notification/platform/security/AccessContext.java:17: public static AccessContext dispatch(String providerProfileId) {
|
|
./application-core/src/main/java/dev/caskeleton/application/notification/platform/security/AccessContext.java:18: return new AccessContext("DISPATCH", providerProfileId, false);
|
|
./application-core/src/main/java/dev/caskeleton/application/notification/platform/security/ContactPointProtector.java:12: ContactPointValue reveal(ProtectedContactPoint protectedValue, AccessContext context);
|
|
./adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/SmtpProviderRuntimeAssemblerTest.java:122: dev.caskeleton.application.notification.platform.security.AccessContext context) {
|
|
./adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmContactPointProtectorTest.java:27: assertThat(protector.reveal(first, AccessContext.dispatch("ses-primary"))).isEqualTo(value);
|
|
./adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmContactPointProtectorTest.java:65: assertThatThrownBy(() -> protector.reveal(moved, AccessContext.dispatch("ses-primary")))
|
|
./adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmContactPointProtectorTest.java:86: assertThatThrownBy(() -> protector.reveal(rotated, AccessContext.dispatch("ses-primary")))
|
|
./adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmContactPointProtector.java:86: public ContactPointValue reveal(ProtectedContactPoint protectedValue, AccessContext context) {
|
|
./adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/ses/SesNotificationProviderAdapter.java:115: AccessContext.dispatch(submission.profile().profileId().value()));
|
|
./adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webpush/WebPushNotificationProviderAdapter.java:90: AccessContext.dispatch(submission.profile().profileId().value()));
|
|
./adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/smtp/SmtpNotificationProviderAdapter.java:90: AccessContext.dispatch(submission.profile().profileId().value()));
|
|
./adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/twilio/TwilioSmsProviderAdapter.java:86: AccessContext.dispatch(submission.profile().profileId().value()));
|
|
./adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/fcm/FcmBatchCoordinator.java:70: AccessContext.dispatch(submission.profile().profileId().value()));
|
|
./adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/apns/ApnsNotificationProviderAdapter.java:84: AccessContext.dispatch(submission.profile().profileId().value()));
|
|
exit=0
|
|
|
|
$ grep -n 'context' adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmContactPointProtector.java
|
|
86: public ContactPointValue reveal(ProtectedContactPoint protectedValue, AccessContext context) {
|
|
88: Objects.requireNonNull(context, "context");
|
|
exit=0
|
|
|
|
=== 8.2 두 render 오버로드의 예외 처리 비교 ===
|
|
$ grep -n 'catch\|process(\|verifyRendered' adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/ThymeleafStringTemplateEngine.java
|
|
80: String rendered = engineFor(mode).process(source, contextFor(source, variables));
|
|
81: return TemplateSlotPolicy.verifyRendered(mode, rendered);
|
|
91: return engine.process(source, contextFor(source, variables));
|
|
92: } catch (RuntimeException failure) {
|
|
exit=0
|
|
|
|
$ grep -rn 'engine.render(' adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/CanonicalNotificationRenderer.java
|
|
125: return engine.render(modeOf(slot), template.content().requireSlot(slot), variables);
|
|
133: .map(source -> engine.render(modeOf(slot), source, variables));
|
|
exit=0
|
|
|
|
$ grep -rn 'render(source\|render(String source' --include=*.java adapter/outbound/notification/src/main
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/ThymeleafStringTemplateEngine.java:85: public String render(String source, Map<String, Object> variables) {
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/PlaceholderTemplateEngine.java:28: public String render(String source, Map<String, Object> variables) {
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/template/NotificationTemplateEngine.java:21: String render(String source, Map<String, Object> variables);
|
|
exit=0
|
|
|
|
=== 8.3 감사 싱크는 존재하는가 ===
|
|
$ grep -rn 'NotificationAuditPort' --include=*.java adapter/outbound/notification/src/main app-bootstrap/src/main | head
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ProviderRuntimeRotator.java:5:import dev.caskeleton.application.notification.platform.observation.NotificationAuditPort;
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ProviderRuntimeRotator.java:26: private final NotificationAuditPort audit;
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/ProviderRuntimeRotator.java:34: NotificationAuditPort audit,
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/observation/LoggingNotificationAudit.java:5:import dev.caskeleton.application.notification.platform.observation.NotificationAuditPort;
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/observation/LoggingNotificationAudit.java:23: implements NotificationAuditPort, NotificationSecurityAuditPort {
|
|
app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformDispatchConfig.java:34:import dev.caskeleton.application.notification.platform.observation.NotificationAuditPort;
|
|
app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformDispatchConfig.java:283: dev.caskeleton.application.notification.platform.observation.NotificationAuditPort audit,
|
|
app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformDispatchConfig.java:427: NotificationAuditPort audit,
|
|
app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformObservabilityConfig.java:14:import dev.caskeleton.application.notification.platform.observation.NotificationAuditPort;
|
|
app-bootstrap/src/main/java/dev/caskeleton/bootstrap/notification/NotificationPlatformObservabilityConfig.java:104: * <p>One bean. {@link LoggingNotificationAudit} implements both {@link NotificationAuditPort} and
|
|
exit=0
|
|
|
|
=== 8.4 키 분리 / 길이 검사 ===
|
|
$ grep -n 'REQUIRED_KEY_BITS\|requireDistinctKeys\|associatedData' adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmContactPointProtector.java
|
|
49: private static final int REQUIRED_KEY_BITS = 256;
|
|
68: requireDistinctKeys(encryption, lookup);
|
|
75: byte[] ciphertext = encrypt(encryption, nonce, associatedData(value.type()), plaintext);
|
|
94: associatedData(protectedValue.type()),
|
|
108: if (encryption.lengthBits() != REQUIRED_KEY_BITS) {
|
|
110: "contact encryption key must be " + REQUIRED_KEY_BITS + " bits");
|
|
115: private static void requireDistinctKeys(SecretKeyMaterial encryption, SecretKeyMaterial lookup) {
|
|
122: private static byte[] associatedData(ContactPointType type) {
|
|
129: SecretKeyMaterial key, byte[] nonce, byte[] associatedData, byte[] plaintext) {
|
|
136: cipher.updateAAD(associatedData);
|
|
145: SecretKeyMaterial key, byte[] nonce, byte[] associatedData, byte[] ciphertext) {
|
|
152: cipher.updateAAD(associatedData);
|
|
exit=0
|
|
|
|
$ grep -rn 'SecretPurpose\.' --include=*.java adapter/outbound/notification/src/main | sed 's/:.*SecretPurpose\./ -> /' | sort -u | head -20
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationSecretRequirements.java -> CALLBACK_FINGERPRINT_HMAC);
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationSecretRequirements.java -> CALLBACK_SIGNING);
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationSecretRequirements.java -> CONTACT_ENCRYPTION,
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationSecretRequirements.java -> CONTACT_LOOKUP_HMAC,
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationSecretRequirements.java -> PAYLOAD_ENCRYPTION,
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationSecretRequirements.java -> PROVIDER_CREDENTIAL);
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationSecretRequirements.java -> PROVIDER_REQUEST_LOOKUP_HMAC);
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/autoconfigure/NotificationSecretRequirements.java -> VAPID_SIGNING);
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/twilio/TwilioCallbackAdapter.java -> CALLBACK_SIGNING) {
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webhook/WebhookNotificationProviderAdapter.java -> CALLBACK_SIGNING) {
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webpush/VapidKeyRegistry.java -> VAPID_SIGNING) {
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmCallbackPayloadProtection.java -> CALLBACK_FINGERPRINT_HMAC).material(), "HmacSHA256"));
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmContactPointProtector.java -> CONTACT_ENCRYPTION);
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmContactPointProtector.java -> CONTACT_LOOKUP_HMAC), value.type(), value.normalized());
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmContactPointProtector.java -> CONTACT_LOOKUP_HMAC);
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmNotificationPayloadProtection.java -> PAYLOAD_ENCRYPTION);
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/HmacProviderRequestIdHasher.java -> PROVIDER_REQUEST_LOOKUP_HMAC).material(),
|
|
adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/ProviderCredentialManager.java -> PROVIDER_CREDENTIAL) {
|
|
exit=0
|
|
|