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>
154 lines
13 KiB
Plaintext
154 lines
13 KiB
Plaintext
Sub-scope 05 (fileserver) platform/local failure·probe·health·orphan + verification + security + audit
|
|
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
|
generatedAt=2026-08-30T01:27:59+00:00
|
|
|
|
=== OWNED FILES (24 main + 5 test) ===
|
|
9e61e441e81fd868a99c09b8afd1c4fa9a9e9172 platform/local/FilesystemFailureClassifier.java 51
|
|
67318269844b61a021bf3ed964ffb92b0abe7b30 platform/local/FilesystemOutcome.java 23
|
|
62e9aac34c4ae1696ac86c36b7caed19380ac075 platform/local/AmbiguousFilesystemOperationDetector.java 91
|
|
04b3b101d17e0b2d4148219c24466d1a1a27e797 platform/local/LocalStorageFailures.java 48
|
|
53e89c47a91b09940d297ea204b1a76fd70f9af7 platform/local/LocalStorageCapabilityProbe.java 302
|
|
7b803247f076a9bc79ec2fa084c6bac3b0e580f5 platform/local/LocalStorageProbeResult.java 53
|
|
ee5121e14abe0da88103aa3e713954c52a8c7b14 platform/local/LocalStorageHealthAdapter.java 70
|
|
636b22ca38c666258c5888a667234d20e4212cce platform/local/LocalStorageUsageProbe.java 46
|
|
527677720cbc2381fb41fd3b50c61285d25d5bfd platform/local/LocalReconciliationContentProbe.java 84
|
|
2bc58d8cd0dd9c54dbf756cbf832c05a1c04efff platform/local/LocalOrphanScanAdapter.java 239
|
|
cae4bf09a9d405aa927e4442007ca778aa1aa8a6 platform/verification/FilenamePolicyVerifier.java 38
|
|
30fc5c76d7c0f2ab45df7f6f4297566a5c4166f3 platform/verification/LengthVerifier.java 41
|
|
d24d5bc45303f4cd062266af4cf3d32b4eecc320 platform/verification/LocalVerificationContentReader.java 68
|
|
995e30a163712f4afc85e0f6ceb1a89470cf890a platform/verification/MediaTypeVerifier.java 108
|
|
3e2a1958457d413e71e581d2a2057fc53a004eae platform/verification/Sha256Verifier.java 32
|
|
ff6d5cdf0f6379464f4dc43c3c53d035fdab1bc6 platform/verification/ScriptableContentPolicy.java 57
|
|
6b592a50a5e46d8f110100248fef5e360295e692 platform/verification/VerificationContentReader.java 16
|
|
84602304e188c7942841d193650872e8b8c34246 platform/verification/VerificationCoordinator.java 67
|
|
0aec00fdd700990879833c03f48a653daad5a587 platform/verification/VerificationPolicyCombiner.java 43
|
|
3739210eadfbd16afede10a169a60bd1ffe5885d platform/verification/VerificationVerdictPrecedence.java 11
|
|
815473a43331d452ecc23ae7ed10b1a08bece58c platform/security/RoleBasedFileAccessPolicy.java 98
|
|
3f4c47032cea3783ec230e834d1ca385c71d74df platform/security/UnenforcedFileAccessPolicy.java 27
|
|
afec1144e343f5817ebb1e848770032ebf420c42 platform/audit/StructuredAdminAuditAdapter.java 35
|
|
4e9094c7a1a194345bae9f9208ed9f6aa05aa256 platform/audit/StructuredFileserverAuditAdapter.java 30
|
|
|
|
=== 8.1 resolving the sub-scope 01 forward reference: are these real implementations? ===
|
|
$ grep -n 'implements ' adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/audit/*.java adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/security/*.java adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalStorageHealthAdapter.java adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalStorageUsageProbe.java adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalOrphanScanAdapter.java adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalReconciliationContentProbe.java
|
|
adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/audit/StructuredAdminAuditAdapter.java:19:public final class StructuredAdminAuditAdapter implements AdminAuditPort {
|
|
adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/audit/StructuredFileserverAuditAdapter.java:15:public final class StructuredFileserverAuditAdapter implements FileserverAuditPort {
|
|
adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/security/RoleBasedFileAccessPolicy.java:27:public final class RoleBasedFileAccessPolicy implements FileAccessPolicy {
|
|
adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/security/UnenforcedFileAccessPolicy.java:20:public final class UnenforcedFileAccessPolicy implements FileAccessPolicy {
|
|
adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalStorageHealthAdapter.java:24:public final class LocalStorageHealthAdapter implements StorageHealthPort {
|
|
adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalStorageUsageProbe.java:21:public final class LocalStorageUsageProbe implements StorageUsageProbe {
|
|
adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalOrphanScanAdapter.java:45:public final class LocalOrphanScanAdapter implements OrphanScanPort {
|
|
adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalReconciliationContentProbe.java:22:public final class LocalReconciliationContentProbe implements ReconciliationContentProbe {
|
|
exit=0
|
|
|
|
$ git grep -rn 'new StructuredAdminAuditAdapter\|new StructuredFileserverAuditAdapter\|new LocalStorageHealthAdapter\|new LocalStorageUsageProbe\|new LocalOrphanScanAdapter\|new LocalReconciliationContentProbe\|new RoleBasedFileAccessPolicy\|new UnenforcedFileAccessPolicy' -- app-bootstrap/src/main
|
|
app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverSecurityConfiguration.java:51: new RoleBasedFileAccessPolicy(
|
|
app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverSecurityConfiguration.java:71: return new StructuredAdminAuditAdapter();
|
|
app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverSecurityConfiguration.java:77: return new StructuredFileserverAuditAdapter();
|
|
app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverSecurityConfiguration.java:90: return new UnenforcedFileAccessPolicy();
|
|
app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverStorageConfiguration.java:179: return new LocalStorageHealthAdapter(properties, probeResult, store);
|
|
app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverStorageConfiguration.java:189: return new LocalOrphanScanAdapter(
|
|
app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverStorageConfiguration.java:196: return new LocalStorageUsageProbe(properties.root());
|
|
app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverStorageConfiguration.java:203: return new LocalReconciliationContentProbe(store, stagingLocator);
|
|
exit=0
|
|
|
|
$ grep -rn 'not implemented' adapter/outbound/fileserver/README.md
|
|
103:`shared-mounted`/NFS and SFTP providers are not implemented. Cross-node producer fencing,
|
|
105:tracing, and audit are also not implemented. No setting or bean for those capabilities is exposed.
|
|
exit=0
|
|
|
|
=== 8.2 the scriptable-content marker: anchored at the prefix start ===
|
|
$ grep -n 'SCRIPTABLE_MARKERS\|stripLeading\|startsWith' -B 2 -A 2 adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/verification/ScriptableContentPolicy.java
|
|
21- private static final int PREFIX_BYTES = 1024;
|
|
22-
|
|
23: private static final List<String> SCRIPTABLE_MARKERS =
|
|
24- List.of("<!doctype html", "<html", "<script", "<svg", "<?xml", "<!entity");
|
|
25-
|
|
--
|
|
42- byte[] prefix = contentReader.readPrefix(request, PREFIX_BYTES);
|
|
43- String text =
|
|
44: new String(prefix, StandardCharsets.UTF_8).toLowerCase(Locale.ROOT).stripLeading();
|
|
45: boolean scriptable = SCRIPTABLE_MARKERS.stream().anyMatch(text::startsWith);
|
|
46-
|
|
47- if (!scriptable) {
|
|
exit=0
|
|
|
|
$ grep -n 'Detection is on content' -A 3 adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/verification/ScriptableContentPolicy.java
|
|
15: * <p>Detection is on content, not on the claimed type or the extension, because both are attacker
|
|
16- * controlled. Unless an explicit safe profile is enabled, scriptable content is quarantined rather
|
|
17- * than published.
|
|
18- */
|
|
exit=0
|
|
|
|
=== EXECUTION PROBE: what ScriptableContentPolicy actually quarantines ===
|
|
One temporary test class was added, run, and removed:
|
|
src/test/.../platform/verification/Ss05ScriptableProbe.java
|
|
inlineSafeProfile=false (the enforcing profile); the reader returns each candidate verbatim.
|
|
|
|
PROBE plain <script> -> QUARANTINE / SCRIPTABLE_CONTENT
|
|
PROBE plain <html> -> QUARANTINE / SCRIPTABLE_CONTENT
|
|
PROBE leading whitespace + <html> -> QUARANTINE / SCRIPTABLE_CONTENT
|
|
PROBE UTF-8 BOM + <html> -> ACCEPT / NO_SCRIPTABLE_CONTENT
|
|
PROBE HTML comment then <script> -> ACCEPT / NO_SCRIPTABLE_CONTENT
|
|
PROBE uppercase <SCRIPT> -> QUARANTINE / SCRIPTABLE_CONTENT
|
|
PROBE <svg onload> -> QUARANTINE / SCRIPTABLE_CONTENT
|
|
PROBE NUL byte then <html> -> ACCEPT / NO_SCRIPTABLE_CONTENT
|
|
PROBE plain text -> ACCEPT / NO_SCRIPTABLE_CONTENT
|
|
|
|
Reading: the check is text.stripLeading().startsWith(marker). String.stripLeading removes
|
|
only Character.isWhitespace, so a UTF-8 BOM (U+FEFF) or a NUL byte before the markup
|
|
defeats it, and any leading HTML comment does too. A browser renders all three as HTML.
|
|
|
|
=== 8.2b the sibling verifier decides on content the same way; does it anchor? ===
|
|
$ grep -n 'signature.matches(prefix)\|prefix\[index\]' -B 2 -A 2 adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/verification/MediaTypeVerifier.java
|
|
70- private static Optional<String> detect(byte[] prefix) {
|
|
71- for (Signature signature : SIGNATURES) {
|
|
72: if (signature.matches(prefix)) {
|
|
73- return Optional.of(signature.mediaType());
|
|
74- }
|
|
--
|
|
99- }
|
|
100- for (int index = 0; index < magic.size(); index++) {
|
|
101: if ((prefix[index] & 0xFF) != magic.get(index)) {
|
|
102- return false;
|
|
103- }
|
|
exit=0
|
|
|
|
=== 8.3 fail-closed composition around the verifiers ===
|
|
$ grep -n 'VERIFIER_TIMEOUT\|VERIFIER_UNAVAILABLE\|VERIFIER_INTERRUPTED\|NO_VERIFIER_ANSWERED' adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/verification/VerificationCoordinator.java adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/verification/VerificationPolicyCombiner.java
|
|
adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/verification/VerificationCoordinator.java:59: return VerificationResult.retry("VERIFIER_TIMEOUT");
|
|
adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/verification/VerificationCoordinator.java:62: return VerificationResult.retry("VERIFIER_INTERRUPTED");
|
|
adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/verification/VerificationCoordinator.java:64: return VerificationResult.retry("VERIFIER_UNAVAILABLE");
|
|
adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/verification/VerificationPolicyCombiner.java:21: return VerificationResult.retry("NO_VERIFIER_ANSWERED");
|
|
exit=0
|
|
|
|
$ grep -n 'REJECT > QUARANTINE > RETRY > ACCEPT' -B 2 -A 2 adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/verification/VerificationPolicyCombiner.java
|
|
9- * Reduces per-verifier answers to one verdict.
|
|
10- *
|
|
11: * <p>Precedence is {@code REJECT > QUARANTINE > RETRY > ACCEPT}. Because {@code RETRY} outranks
|
|
12- * {@code ACCEPT}, a scanner that timed out can never be silently overridden by the verifiers that
|
|
13- * did answer.
|
|
exit=0
|
|
|
|
=== 8.4 message hygiene: does any failure or audit line carry a path? ===
|
|
$ grep -rn 'exception.getMessage()\|root\b' adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalStorageFailures.java
|
|
16: * <p>No message carries a path, a mount, or a storage root: transports map on the error code alone.
|
|
exit=0
|
|
|
|
$ grep -rn 'record.subjectId()\|actorFingerprint' adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/audit/*.java
|
|
adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/audit/StructuredAdminAuditAdapter.java:30: record.actorFingerprint(),
|
|
adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/audit/StructuredAdminAuditAdapter.java:31: record.subjectId(),
|
|
adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/audit/StructuredFileserverAuditAdapter.java:26: event.actorFingerprint(),
|
|
exit=0
|
|
|
|
$ grep -n 'names neither the required role' -A 3 adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/security/RoleBasedFileAccessPolicy.java
|
|
86: * <p>The message names neither the required role nor the subject's roles: a denial that reported
|
|
87- * what was missing would turn every 403 into a readable description of the role model.
|
|
88- */
|
|
89- private static FileAccessDeniedException denied(FileserverErrorCode code, String message) {
|
|
exit=0
|
|
|
|
$ git status --short | wc -l
|
|
0
|
|
exit=0
|
|
|