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>
139 lines
8.5 KiB
Plaintext
139 lines
8.5 KiB
Plaintext
# 등록 순서 — 이 순서가 결과를 정한다
|
|
.authorizeHttpRequests(
|
|
auth -> {
|
|
if (publicPaths.length > 0) {
|
|
auth.requestMatchers(publicPaths).permitAll();
|
|
}
|
|
// Ordered before the authenticated catch-all: a management path must be refused at
|
|
// the transport, not by an application policy the request has already passed.
|
|
for (RestrictedPathRule rule : restricted) {
|
|
auth.requestMatchers(rule.pathPattern()).hasAnyAuthority(rule.authorities());
|
|
}
|
|
auth.anyRequest().authenticated();
|
|
})
|
|
|
|
# 규칙 자신은 약해지는 것을 막는다
|
|
30: if (requiredAuthorities.isEmpty()) {
|
|
31- throw new IllegalArgumentException(
|
|
32- "requiredAuthorities must not be empty: a rule that requires nothing is weaker than the "
|
|
33- + "authenticated default it replaces");
|
|
34- }
|
|
|
|
# 규칙을 내놓는 자리
|
|
main · FileserverAdminPlaneConfiguration.java:36: return new RestrictedPathRule(ADMIN_PATH_PATTERN, settings.security().adminRoles());
|
|
|
|
# 프로덕션이 등록하는 패턴과 그것을 켜야 하는 두 스위치
|
|
@ConditionalOnProperty(
|
|
prefix = "app.fileserver-platform.admin",
|
|
name = "enabled",
|
|
havingValue = "true")
|
|
static final String ADMIN_PATH_PATTERN = "/internal/fileserver/**";
|
|
-- 그 설정 클래스를 이름으로 부르는 자리 전부
|
|
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverAdminManagementContextConfiguration.java:20: * role gate contributed by {@link FileserverAdminPlaneConfiguration} stays: the two guards cover
|
|
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverAdminPlaneConfiguration.java:29:public class FileserverAdminPlaneConfiguration {
|
|
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverPlatformAutoConfiguration.java:71: FileserverAdminPlaneConfiguration.class,
|
|
-- 그 유일한 @Import 가 든 자동설정과 그 조건
|
|
@AutoConfiguration
|
|
@ConditionalOnProperty(
|
|
prefix = FileserverPlatformSettings.PREFIX,
|
|
name = "enabled",
|
|
havingValue = "true")
|
|
@Import({
|
|
FileserverStorageConfiguration.class,
|
|
FileserverSecurityConfiguration.class,
|
|
FileserverServiceConfiguration.class,
|
|
FileserverCleanupConfiguration.class,
|
|
FileserverAdminPlaneConfiguration.class,
|
|
FileserverStartupConfiguration.class
|
|
})
|
|
49: public static final String PREFIX = "app.fileserver-platform";
|
|
-- 스캔이 그 패키지를 제외하므로 다른 길이 없다
|
|
excludeFilters = {
|
|
@ComponentScan.Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
|
|
@ComponentScan.Filter(
|
|
type = FilterType.CUSTOM,
|
|
classes = AutoConfigurationExcludeFilter.class),
|
|
@ComponentScan.Filter(
|
|
type = FilterType.REGEX,
|
|
pattern = CaSkeletonApplication.AUTO_CONFIGURED_PACKAGES)
|
|
})
|
|
static final String AUTO_CONFIGURED_PACKAGES =
|
|
"dev\\.caskeleton\\.bootstrap\\.autoconfigure\\..*"
|
|
+ "|dev\\.caskeleton\\.bootstrap\\.notification\\..*"
|
|
+ "|dev\\.caskeleton\\.adapter\\.inbound\\.web\\.fileserver\\.admin\\..*"
|
|
+ "|dev\\.caskeleton\\.adapter\\.outbound\\.mongo\\..*"
|
|
+ "|dev\\.caskeleton\\.adapter\\.outbound\\.messaging\\..*"
|
|
+ "|dev\\.caskeleton\\.adapter\\.outbound\\.notification\\..*"
|
|
+ "|dev\\.caskeleton\\.adapter\\.outbound\\.persistence\\..*"
|
|
+ "|dev\\.caskeleton\\.adapter\\.inbound\\.graphql\\..*"
|
|
+ "|dev\\.caskeleton\\.adapter\\.inbound\\.web\\.mvc\\.error\\..*"
|
|
+ "|dev\\.caskeleton\\.adapter\\.inbound\\.web\\.mvc\\.budget\\..*"
|
|
+ "|dev\\.caskeleton\\.adapter\\.inbound\\.web\\.mvc\\.operation\\..*"
|
|
+ "|dev\\.caskeleton\\.adapter\\.inbound\\.web\\.webflux\\.error\\..*"
|
|
+ "|dev\\.caskeleton\\.adapter\\.inbound\\.web\\.webflux\\.operation\\..*"
|
|
+ "|dev\\.caskeleton\\.messaging\\..*";
|
|
-- 관리 라우트 자체는 관리 컨텍스트에 등록되고 포트는 따로 출하된다
|
|
@ManagementContextConfiguration(value = ManagementContextType.ANY, proxyBeanMethods = false)
|
|
@ConditionalOnProperty(
|
|
prefix = "app.fileserver-platform",
|
|
name = {"enabled", "admin.enabled"},
|
|
havingValue = "true")
|
|
@Import(FileserverAdminController.class)
|
|
249: port: ${MANAGEMENT_SERVER_PORT:9001}
|
|
317: port: ${APP_SERVER_PORT:8080}
|
|
694: public-paths: ${SECURITY_PUBLIC_PATHS:${PRESENTATION_API_BASE_PATH:/v1}/healthcheck}
|
|
854: enabled: ${APP_FILESERVER_PLATFORM_ENABLED:false}
|
|
899: admin-roles: ${APP_FILESERVER_PLATFORM_SECURITY_ADMIN_ROLES:ROLE_FILE_ADMIN}
|
|
911: enabled: ${APP_FILESERVER_PLATFORM_ADMIN_ENABLED:false}
|
|
|
|
# 공개 경로를 읽는 자리 — 접근자로 읽는 쪽과 원시 키로 읽는 쪽
|
|
test · SecuritySettingsTest.java:54: assertThat(context.getBean(SecuritySettings.class).publicPaths())
|
|
test · SecuritySettingsTest.java:67: assertThat(settings.publicPaths()).containsExactly("/healthcheck");
|
|
test · SecuritySettingsTest.java:68: assertThat(settings.publicPaths()).isUnmodifiable();
|
|
main · SecurityConfig.java:75: String[] publicPaths = securitySettings.publicPaths().toArray(new String[0]);
|
|
-- 저장소 전체에서 그 키를 쥔 파일. 점 표기와 환경 변수와 중첩 YAML 세 형태로 찾는다
|
|
~ .superpowers/sdd/2026-08-02-warning-zero-build-refactoring/task-8-gradle-review.diff
|
|
docs/security/public-paths-snapshot.txt
|
|
docs/superpowers/plans/2026-07-25-ci-control-plane-recovery.md
|
|
docs/superpowers/plans/2026-07-28-notification-production-capability.md
|
|
~ src/.env
|
|
src/README.md
|
|
src/adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/auth/CorsSecurityFilterIntegrationTest.java
|
|
src/adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/auth/JwtJwksSecurityFilterIntegrationTest.java
|
|
src/adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/auth/SecurityModeWebContractTest.java
|
|
src/adapter/inbound/web/src/test/java/dev/caskeleton/adapter/inbound/web/settings/SecuritySettingsTest.java
|
|
src/app-bootstrap/src/functionalTest/java/dev/caskeleton/bootstrap/contract/BuildVerificationPurityContractTest.java
|
|
src/app-bootstrap/src/main/resources/application-local.yml
|
|
src/app-bootstrap/src/main/resources/application.yml
|
|
src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/contract/ProfileSeparationContractTest.java
|
|
src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/security/PublicHealthPathAgreementTest.java
|
|
src/app-bootstrap/src/test/resources/application-test.yml
|
|
src/gradle/public-path-snapshot.gradle
|
|
src/sample-portfolio/src/main/resources/application.yml
|
|
src/sample-portfolio/src/test/resources/application-test.yml
|
|
전부 / 그중 추적되지 않는 것(~) : 19 / 2
|
|
그중 제한 패턴을 함께 언급하는 파일 : (0)
|
|
|
|
# 겹침을 보는 것이 있는가 — 규칙 이름을 쓰는 자리 전부와 공개 경로 스냅숏 게이트
|
|
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/auth/RestrictedPathRule.java
|
|
src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/auth/SecurityConfig.java
|
|
src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverAdminPlaneConfiguration.java
|
|
공개 경로 스냅숏 게이트가 있는 파일 : src/gradle/public-path-snapshot.gradle
|
|
그 게이트가 읽는 두 입력 :
|
|
File publicPathEnvironmentFile = rootProject.file('.env')
|
|
rootProject.file('../docs/security/public-paths-snapshot.txt')
|
|
값 쪽 입력이 추적되는가 : .gitignore:7:src/.env* src/.env
|
|
그 입력이 없으면 :
|
|
if (!environmentFile.isFile()) {
|
|
throw new GradleException(
|
|
"missing public-path environment file ${environmentFile}")
|
|
그런데도 그 태스크를 부르는 자리 :
|
|
.github/workflows/ci-quality-gates.yml:50: run: ./gradlew check verifyPublicPathSnapshot verifyDependencyLocks --warning-mode=fail --no-daemon --stacktrace
|
|
.github/workflows/ci-quality-gates.yml:117: verifyPublicPathSnapshot
|
|
.github/workflows/notification-platform.yml:97: ./gradlew verifyEnvKeys verifyPublicPathSnapshot --console=plain
|
|
.github/workflows/web-release.yml:48: verifyPublicPathSnapshot
|
|
워크플로 전체에서 .env 를 입에 올리는 자리 전부 :
|
|
.github/workflows/fileserver-pr.yml:22: - 'src/.env'
|
|
.github/workflows/notification-platform.yml:30: - 'src/.env'
|
|
그 게이트가 제한 패턴을 언급하는가 : 0
|