feat(httpclient): close the platform review's P0/P1/P2 findings
The review found one defect shape repeated across the platform: surfaces that were declared, bound, and documented, but that nothing read. An operator configuring fullUrlRecording, bodyLogging, retry.policy, validatedDnsPinning, timeout.dns, or any of ten declared metric names got a guarantee the code never delivered. Every such surface is now in exactly one of three states -- wired for real, rejected at startup, or registered in a test-enforced gap list with its reason. No silent no-ops remain. P0: - Activate the platform from bootstrap behind app.httpclient.enabled, with a single auto-configuration importing the nine child configurations. - Give the platform a strict, repository-level ENV contract: 74 leaf fields derived from the settings record tree, unknown APP_HTTPCLIENT_* rejected. - Route typed HTTP service clients through the call kernel via KernelHttpExchangeAdapter, so they stop bypassing platform policy. - Pin dynamic-target DNS resolution to the socket for the life of a call, closing the resolve-then-connect TOCTOU / rebinding window. - Actually transmit the idempotency key, and make retry eligibility depend on transmission rather than on merely holding one. - Reject reactive authentication and reactive redirect at startup instead of declaring support that does not function. - Fix the Reactor-only Stable contract row so the lane stops failing. - Stop advertising HTTP/3 on a transport that negotiates HTTP/1. P1 covers execution and retry accounting, redirect security (per-hop target guarding, sensitive-header stripping, 303 body handling), runtime rotation and transport resource ownership keyed by generation, dynamic-target hardening (subdomain matching, global-unicast classification, strict CIDR parsing), protocol intent, pool and timeout wiring, streaming and body limits, observability parity, and OAuth single-flight refresh on a bounded pool with a bounded wait. P2 covers configuration and documentation drift, the Gradle check wiring for the four hermetic lanes, and the CI gate matrix. Two test-quality defects surfaced while closing these: the HTTP/2 stream saturation test ran against cleartext HTTP/1.1 while asserting nothing about the protocol, and an OAuth contention test slept on a latch that could fire before the callers it meant to observe. Both now assert what their names claim. Verification run: :adapter:outbound:httpclient:check and :app-bootstrap:check (checkstyle, spotless, spotbugs, and the four hermetic lanes), verifyCleanArchitectureDependencies, verifyEnvKeys, verifyOneTypePerFile, verifyDependencyLocks, the documentation and gate-matrix verifiers, and the performance lane against a real TLS+ALPN HTTP/2 server. Not executed, and tracked rather than claimed: Docker/Toxiproxy fault injection, JMH, a real QUIC/HTTP3 server, a real Spring Framework 6.2 distribution (now a delegated-pending gate), live OAuth/TLS/proxy/DNS integration, and a whole-repository check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
5f10b791d3
commit
0cd959a494
+31
-8
@@ -17,6 +17,7 @@
|
||||
| 게이트 | 하는 일 |
|
||||
| --- | --- |
|
||||
| `verifyCleanArchitectureDependencies` | 모듈 간 의존 방향이 허용된 범위 안에 있는지 검사 |
|
||||
| `verifyRuntimeModuleMembership` | registry의 두 composition root membership과 실제 main project dependency가 정확히 일치하는지 검사 |
|
||||
| `verifyEnvKeys` | `env-keys.yaml` ↔ `application.yml` ↔ `src/.env` 가 어긋나지 않는지 검사 |
|
||||
| `verifyOneTypePerFile` | 파일당 public 최상위 타입 1개, 파일명 == 타입명인지 검사 |
|
||||
| `verifyTrivyignore` | `.trivyignore.yaml` 의 Trivy suppression 이 사유·만료일을 갖추고 만료/기한초과가 아닌지 검사 |
|
||||
@@ -84,6 +85,24 @@ vendor/build나 container base image까지 byte-for-byte 같음을 주장하지
|
||||
registry와 ArchUnit 규칙(`CleanArchitectureTest`)을 함께 갱신해야 합니다. settings와 gate는
|
||||
같은 registry를 읽고, 등록되지 않은 leaf나 허용되지 않은 edge를 fail-closed로 거부합니다.
|
||||
|
||||
### `verifyRuntimeModuleMembership`
|
||||
|
||||
- **하는 일.** 같은 registry의 `runtime_compositions`와 각 leaf의 `runtime_memberships`를 읽어
|
||||
`app-bootstrap`/`sample-portfolio`의 실제 `api`/`implementation`/`compileOnly`/`runtimeOnly`
|
||||
project dependency와 정확히 대조합니다.
|
||||
- **opt-in의 의미.** membership이 빈 GraphQL/gRPC/WebSocket/Mongo leaf는 독립 빌드 대상이지만 두
|
||||
shipped runtime에는 없습니다. app-bootstrap의 `conditionalTransportTest` test-only classpath는
|
||||
실제 채택 전에 세 inbound transport를 함께 qualification하기 위한 evidence composition입니다.
|
||||
- **변경 규칙.** production edge를 추가하거나 제거할 때 `allowed_dependencies`,
|
||||
`runtime_memberships`, 실제 Gradle dependency를 같은 변경에서 갱신하지 않으면 `check`가 실패합니다.
|
||||
|
||||
세 opt-in inbound transport의 test-only composition, 실제 wire 경계, positive-count/zero-skip 증거는
|
||||
다음 release-blocking aggregate로 실행합니다.
|
||||
|
||||
```bash
|
||||
./gradlew conditionalTransportQualification
|
||||
```
|
||||
|
||||
### `verifyOneTypePerFile` (code-conventions I6)
|
||||
|
||||
- **하는 일.** `src/main/java` 의 모든 `.java` 파일이 public 최상위 타입을 1개만 갖고, 그 타입 이름이
|
||||
@@ -119,9 +138,9 @@ vendor/build나 container base image까지 byte-for-byte 같음을 주장하지
|
||||
경로를 **제외한** 모든 요청은 인증을 요구합니다(`src/.env` → `SecuritySettings.publicPaths()` →
|
||||
`SecurityConfig`). 이 public 표면이 바뀌는 순간이 곧 보호되던 엔드포인트가 조용히 공개로 노출되는
|
||||
지점입니다. 그래서 그 표면을 snapshot 으로 떠 두고, 미승인 변경에 빌드를 실패시킵니다.
|
||||
- **승인 방법.** reviewer 가 `./gradlew verifyPublicPathSnapshot -PapprovePublicPathChange` 로
|
||||
snapshot 을 의도적으로 다시 생성합니다. 공개 경로 변경은 보안 리뷰 대상으로 보고 수동 승인 후
|
||||
재생성된 snapshot 을 함께 커밋합니다.
|
||||
- **승인 방법.** `verifyPublicPathSnapshot` 은 항상 읽기 전용입니다. reviewer 가 변경을 승인한 뒤
|
||||
`./gradlew updatePublicPathSnapshot -PapprovePublicPathChange` 로 snapshot 을 명시적으로 다시
|
||||
생성합니다. 공개 경로 변경은 보안 리뷰 대상으로 보고 재생성된 snapshot 을 함께 커밋합니다.
|
||||
- **결정 — 무엇을 snapshot 했나 (프로젝트 선택).** 초기안은 기동 시
|
||||
`SecurityFilterChain.getFilters()` 를 introspection 하는 방식이었습니다. 하지만 그 reflection
|
||||
은 Spring 버전마다 깨지기 쉽습니다(`permitAll` matcher 가
|
||||
@@ -130,8 +149,8 @@ vendor/build나 container base image까지 byte-for-byte 같음을 주장하지
|
||||
변경은 무조건 게이트를 실패시킨다)는 같고, 메커니즘은 더 견고합니다.
|
||||
- **snapshot 위치.** `docs/security/public-paths-snapshot.txt`. 이 파일은 커밋된 필수 보안
|
||||
baseline 입니다. CI 는 Gradle 실행 전에 파일이 비어 있지 않고 Git에 추적되는지 검사하므로 fresh
|
||||
checkout 에서 누락되거나 untracked 상태면 즉시 실패합니다. 승인된 변경만 위 명령으로 재생성한 뒤
|
||||
보안 리뷰와 함께 커밋합니다.
|
||||
checkout 에서 누락되거나 untracked 상태면 즉시 실패합니다. 승인된 변경만 update task로 재생성한
|
||||
뒤 보안 리뷰와 함께 커밋합니다.
|
||||
|
||||
### `verifyTrivyignore`
|
||||
|
||||
@@ -260,9 +279,13 @@ ca-skeleton:
|
||||
- `ACTIVE`는 exact destination/provider/operation-catalog binding을 요구합니다. 현재 유일한
|
||||
buffered-classic readiness card가 `NOT_IMPLEMENTED`이므로 provider resource 생성 전에
|
||||
fail-closed합니다. 아직 운영 HTTP provider를 활성화할 수 있다는 뜻이 아닙니다.
|
||||
- 기존 `APP_OUTBOUND_HTTP_*`와 `app.outbound.http.*`는 canonical 설정이 아닙니다. `.env`,
|
||||
application YAML과 env-key registry에서 제거됐으며 canonical composition에 입력하면 상태와
|
||||
무관하게 기동을 거부합니다.
|
||||
- 기존 `APP_OUTBOUND_HTTP_*`와 `app.outbound.http.*`는 canonical 설정이 아닙니다. 루트 `src/.env`,
|
||||
`app-bootstrap`의 application YAML, env-key registry에서 제거됐으며 canonical composition에
|
||||
입력하면 상태와 무관하게 기동을 거부합니다.
|
||||
- 다만 `sample-portfolio`의 application YAML에는 legacy facade를 시연하기 위해 15개 키가 남아
|
||||
있습니다. 이 모듈은 fixture/reference consumer이고 production 의존성이 아니며, 그 YAML은
|
||||
`verifyEnvKeys`가 검사하는 세 파일에 포함되지 않습니다. "제거됐다"는 문장이 저장소 전체를
|
||||
가리킨다고 읽히지 않도록 범위를 명시합니다.
|
||||
- legacy JDK facade가 필요한 fork만 canonical composition 밖에서
|
||||
`OutboundHttpSettings.bindLegacy(Binder)`와 legacy configuration을 명시적으로 import합니다.
|
||||
timeout/retry/CB/response-size 설정은 그 migration API 내부 계약일 뿐 canonical provider
|
||||
|
||||
Reference in New Issue
Block a user