docs(keycloak-session-store): import the session-storage lab as a new project
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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
43bccd08a8
commit
b2963105a8
+169
@@ -0,0 +1,169 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: a06-f011-mongoregexpolicy-forbidden
|
||||
title: MongoRegexPolicy.forbidden()은 금지하지 않는다
|
||||
topic: security-policy-enforcement
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:a06-f011-mongoregexpolicy-forbidden
|
||||
evidenceCapturedOn: 2026-09-02
|
||||
assets:
|
||||
- key: a06-f011-mongoregexpolicy-forbidden-probe
|
||||
file: ../../../final/evidence/rendered/a06-f011-mongoregexpolicy-forbidden-probe.svg
|
||||
- key: a06-f011-mongoregexpolicy-forbidden
|
||||
file: ../../../final/evidence/rendered/a06-f011-mongoregexpolicy-forbidden.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/a06-f011-mongoregexpolicy-forbidden-probe.txt
|
||||
- ../../../final/evidence/raw/a06-f011-mongoregexpolicy-forbidden.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/06-adapter-outbound-persistence-mongo.md#L740 이다. 등급은 P3 이다. 금지 팩토리가 길이 1 정책이라는 관찰, 앵커 문자가 네 검사를 통과한다는 판정, 두 도우미만 막힌다는 대비, 그리고 필드가 정규식 연산자를 등록해야 도달한다는 조건이 그 절에 있다.
|
||||
- 그 절은 두 도우미의 이스케이프 결과가 항상 다섯 자 이상이라고 적는데, 포함 쪽 최단은 네 자다. 결론은 같다.
|
||||
- 통과하는 조합이 정확히 하나라는 것, 최대 길이 0 이 생성자에서 막힌다는 것, 매치되는 것이 그 필드의 문자열 값이라는 것, 그리고 이 팩토리의 호출 지점이 없다는 것은 이 기록에서 확인했다.
|
||||
---
|
||||
|
||||
# MongoRegexPolicy.forbidden()은 금지하지 않는다
|
||||
|
||||
금지가 별도 상태가 아니라 최대 길이 1 로 표현되어 있다. 그 정책을 통과하는 패턴과 플래그의 조합은 하나뿐이고, 그 하나는 대상 필드에 든 문자열 값을 전부 매치한다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **위험한 조합은 정책이 아니라 생성자가 거부하게 만든다**
|
||||
금지를 별도 상태로 두어야 하는 이유다.
|
||||
- **sanitize가 아니라 reject가 기본이다**
|
||||
정화가 아니라 거절이 기본이어야 한다는 규칙이다.
|
||||
- **접두사 시작 매칭은 시그니처에는 맞고 스니핑 패턴에는 맞지 않는다**
|
||||
두 사례 모두 검사 방식이 막으려는 대상과 맞지 않는다.
|
||||
|
||||
## 문제
|
||||
|
||||
정규식 정책은 최대 길이와 허용 플래그와 앵커 요구 셋으로 이루어진 레코드다. 금지를 뜻하는 자리는 그 셋에 없다.
|
||||
|
||||
컴팩트 생성자는 최대 길이가 0 이하면 거부한다. 음수 길이는 관대한 정책이 아니라 오타라는 판단이고 그 판단은 옳다. 다만 그 때문에 정규식 없음을 뜻할 수 있는 값이 표현 불가능해지고, 금지 팩토리는 표현 가능한 가장 작은 값을 쓴다.
|
||||
|
||||
## 결론
|
||||
|
||||
길이 1 이하이면서 앵커로 시작하는 문자열은 앵커 문자 하나뿐이다. 허용 플래그 집합이 비어 있으므로 플래그도 빈 문자열이어야 한다. 통과하는 조합은 그 둘의 짝 하나다.
|
||||
|
||||
그 짝은 대상 필드에 문자열이 든 문서를 전부 매치한다. 값이 문자열이 아니거나 필드가 없으면 매치되지 않는다. 무력화되는 것은 그 컬렉션에서 정규식 검색의 대상이 되는 값 전부다.
|
||||
|
||||
이스케이프 도우미 둘은 어떤 입력으로도 이 정책을 통과할 수 없다. 입력이 길수록 출력도 길어지는데, 가장 짧은 것이 다섯 자와 네 자다. 금지 정책이 실제로 막는 것은 호출자가 문법을 기여할 수 없는 두 경로이고, 남기는 것은 문법을 그대로 받는 경로의 그 한 짝이다.
|
||||
|
||||
도달에는 선택 둘이 겹쳐야 한다. 이 팩토리를 부르는 것과, 어떤 필드가 정규식 연산자를 명시로 등록하는 것이다. 기본 연산자 집합에 정규식은 없다. 그리고 지금 저장소에는 앞의 선택이 없다.
|
||||
|
||||
이 파일의 javadoc 은 대체로 자기 한계를 함께 적는다. 중첩 수량자 검사는 안전 증명이 아니라 필터라고 스스로 밝힌다. 조건 없이 단언하는 문장은 금지 팩토리 위의 한 줄뿐이고, 그 한 줄이 참이 아니다.
|
||||
|
||||
수정은 정책에 명시적인 정규식 불허 상태를 두고 검증이 그것을 먼저 보게 하는 것이다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12
|
||||
MongoDB : 8.0.16 단독 서버
|
||||
확인 방식 : 팩토리 값과 검증 경로 실행, 실제 서버에 질의 실행
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
1. 두 팩토리가 만드는 값과 컴팩트 생성자의 길이 검사를 읽는다.
|
||||
2. 검증 메서드의 네 검사와 길이 비교를 읽는다.
|
||||
3. 앵커 문자와 빈 플래그, 앵커 문자와 i 플래그, 빈 문자열, 다른 한 글자, 두 글자, 두 도우미의 최단 결과를 금지 정책에 통과시킨다.
|
||||
4. 길이 검사를 얹은 인스턴스 도우미를 서로 다른 입력으로 부른다.
|
||||
5. 기본 연산자 집합에 정규식이 있는지 확인한다.
|
||||
6. 정책을 갈아 끼우고 어떤 필드에 정규식 연산자를 등록해 질의를 조립한다.
|
||||
7. 문자열과 비문자열과 필드 없는 문서를 섞어 넣고 그 질의를 실제 서버에서 실행한다.
|
||||
8. 이 팩토리와 정책 교체 메서드와 질의 정책 생성을 부르는 곳을 전수로 센다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
정규식 정책은 최대 길이와 허용 플래그와 앵커 요구 세 자리다. 금지를 뜻하는 자리는 없고, 금지 팩토리는 첫 자리를 1 로 잡는다.
|
||||
|
||||
## 0 이 아니라 1 인 이유
|
||||
|
||||
:::evidence key="a06-f011-mongoregexpolicy-forbidden-probe" alt="금지 정책과 기본 정책의 값, 최대 길이 0 인 정책이 생성자에서 거부되는 결과, 패턴과 플래그 일곱 조합을 금지 정책의 검증에 통과시킨 결과와 각각의 거절 사유, 길이 검사를 얹은 인스턴스 도우미가 서로 다른 입력에서 던지는 결과, 기본 연산자 집합에 정규식이 없다는 확인, 조립된 질의의 문서, 그리고 문자열과 비문자열과 필드 없는 문서를 섞어 넣은 실제 서버에서 그 질의가 매치한 것과 매치하지 않은 것을 출력한 터미널 기록." caption="최대 길이 0 은 생성자가 거부 · 통과하는 조합은 앵커 문자와 빈 플래그 하나 · 인스턴스 도우미는 입력이 무엇이든 길이에서 거절 · 기본 연산자 집합에 REGEX 없음 · 저장 8건에 매치 4건, 매치된 것은 전부 문자열 값 — 33줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
```java
|
||||
/** A policy that forbids regular expressions entirely. */
|
||||
public static MongoRegexPolicy forbidden() {
|
||||
return new MongoRegexPolicy(1, Set.of(), true);
|
||||
}
|
||||
```
|
||||
|
||||
같은 레코드의 컴팩트 생성자가 0 이하를 막는다. 음수 길이를 오타로 보는 검사인데, 정규식 없음을 뜻할 수 있는 값도 같이 막는다.
|
||||
|
||||
```text
|
||||
길이 0 정책 : IllegalArgumentException: a regex policy needs a positive maximum length
|
||||
```
|
||||
|
||||
## 통과하는 조합이 하나 있다
|
||||
|
||||
검증은 길이·플래그·앵커·중첩 수량자를 이 순서로 검사한다. 길이 1 이하와 앵커 요구를 함께 만족하는 문자열은 하나뿐이고, 허용 플래그 집합이 비어 있으므로 플래그 자리도 빈 문자열이어야 한다.
|
||||
|
||||
```text
|
||||
("^", "") -> 수용
|
||||
("^", "i") -> 거절, regex flag 'i' is not allowed by this collection's policy
|
||||
("", "") -> 거절, the pattern is not anchored at the start, so it cannot use an index and will scan the collection
|
||||
("a", "") -> 거절, the pattern is not anchored at the start, so it cannot use an index and will scan the collection
|
||||
("^a", "") -> 거절, the pattern is 2 characters, above the limit of 1
|
||||
```
|
||||
|
||||
그 짝을 넘겨 질의를 조립하면 이런 문서가 나온다.
|
||||
|
||||
```text
|
||||
{"$and": [{"name": {"$regularExpression": {"pattern": "^", "options": ""}}}]}
|
||||
```
|
||||
|
||||
## 매치되는 것은 그 필드의 문자열 값이다
|
||||
|
||||
문자열과 비문자열과 필드 없는 문서를 섞어 여덟 건을 넣고 실행한 결과다.
|
||||
|
||||
```text
|
||||
[실행] 저장 8건 · 매치 4건
|
||||
매치 {"name": "ana"}
|
||||
매치 {"name": ""}
|
||||
매치 {"name": "\ud83d\ude00"}
|
||||
매치 {"name": ["zz"]}
|
||||
미매치 {"name": 12345}
|
||||
미매치 {"name": true}
|
||||
미매치 {"name": null}
|
||||
미매치 {"other": "x"}
|
||||
```
|
||||
|
||||
빈 문자열도 이모지도 배열 안의 문자열도 걸린다. 숫자와 불리언과 널과 필드 부재는 걸리지 않는다. 정규식 검색의 대상이 되는 값만 골라서 전부 매치한다는 뜻이다.
|
||||
|
||||
## 두 도우미는 어떤 입력으로도 통과할 수 없다
|
||||
|
||||
두 이스케이프 도우미는 호출자의 텍스트를 인용부호 안에 그대로 넣으므로 호출자가 문법을 기여할 수 없다. 출력 길이는 입력을 따라 늘고, 최단이 다섯 자와 네 자다.
|
||||
|
||||
```text
|
||||
literalPrefix("") = ^\Q\E (5자)
|
||||
prefixPattern("") -> 거절, the pattern is 5 characters, above the limit of 1
|
||||
literalPrefix("ab") = ^\Qab\E (7자)
|
||||
prefixPattern("ab") -> 거절, the pattern is 7 characters, above the limit of 1
|
||||
escapedContains("") = \Q\E (4자)
|
||||
```
|
||||
|
||||
금지 정책이 막는 것은 문법을 받지 않는 두 경로이고, 남기는 것은 문법을 그대로 받는 경로의 한 짝이다.
|
||||
|
||||
## 호출 지점은 선언 자리뿐이다
|
||||
|
||||
:::evidence key="a06-f011-mongoregexpolicy-forbidden" alt="정규식 정책의 두 팩토리가 만드는 값, 검증 메서드의 네 검사와 길이 비교, 금지 팩토리를 부르는 곳 전수와 정책 교체 메서드를 부르는 곳 전수, 질의 정책을 만드는 곳 전수, 그리고 자동 구성이 질의 계열에서 만드는 빈을 출력한 터미널 기록." caption="금지 팩토리는 길이 1·플래그 없음·앵커 요구 참 · 검증 네 검사 어디에도 정규식 자체를 막는 갈래가 없음 · 금지 팩토리 호출 0 · 정책 교체는 시험 한 곳에서 기본 정책으로 · 질의 정책 생성 다섯 곳은 전부 시험 · 질의 계열의 빈은 예산 집행기 하나 — 58줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
기록의 뒤 네 갈래가 호출 지점을 센다. `forbidden()` 은 선언 자리 말고 없고, `withRegexPolicy` 는 시험 한 곳이 기본 정책으로 부르며, 질의 정책을 만드는 다섯 곳은 전부 시험이고, 자동 구성이 질의 계열에서 등록하는 빈은 `MongoBudgetEnforcer` 하나다.
|
||||
|
||||
배선해도 곧장 도달하지는 않는다. 기본 연산자 집합에 정규식이 없어서, 어떤 필드가 `withOperators` 로 정규식을 명시해 등록해야 한다.
|
||||
|
||||
```text
|
||||
[전제] 기본 연산자 집합에 REGEX 가 있는가 : false [GT, GTE, LT, IN, EQ, LTE, EXISTS]
|
||||
```
|
||||
|
||||
필요한 것은 두 선택이 겹치는 일이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
이 팩토리를 부르는 배포는 없으므로, 어떤 필드가 정규식 연산자를 등록할지는 다루지 않았다. 확인한 것은 두 선택이 겹쳤을 때 검증이 무엇을 통과시키고 그 결과가 서버에서 무엇을 매치하는지다.
|
||||
|
||||
<!-- body:end -->
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-admin-f02
|
||||
title: 비밀 필드 검사가 스냅숏의 네 구획 중 하나에만 적용된다
|
||||
topic: security-policy-enforcement
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-admin-f02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-admin-f02
|
||||
file: ../../../final/evidence/rendered/grpc-admin-f02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-admin-f02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-admin.md#L162 이다.
|
||||
module: grpc-admin
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 비밀 필드 검사가 스냅숏의 네 구획 중 하나에만 적용된다
|
||||
|
||||
메시지는 "a platform snapshot must not carry …" 로 스냅숏 전체를 말한다. 검사 대상은 channelProfileHashes 하나다.
|
||||
|
||||
## 문제
|
||||
|
||||
메시지는 "a platform snapshot must not carry …" 로 스냅숏 전체를 말한다.
|
||||
|
||||
검사 대상은 channelProfileHashes 하나다.
|
||||
|
||||
## 결론
|
||||
|
||||
같은 채널 이름 공간을 쓰는 두 맵이 더 있다 — resolverAndLoadBalancerByChannel, retryOwnerByChannel.
|
||||
|
||||
그리고 registeredServices 목록과 serviceHealth 맵이 있다.
|
||||
|
||||
어느 것도 검사되지 않는다.
|
||||
|
||||
세 맵의 키 집합이 같아야 한다는 요구가 없으므로, 어떤 채널이 나머지 두 맵에만 있으면 그 이름은 검사를 지나지 않는다.
|
||||
|
||||
grpc-advanced-diagnostics 의 스냅숏은 같은 형태의 자기 검사를 두 구획(주소 목록, 자원 판본 키)에 적용한다.
|
||||
|
||||
두 리프의 규율이 갈린다.
|
||||
|
||||
수정은 네 구획 전부를 같은 검사에 넣는 것이다.
|
||||
|
||||
값이 아니라 키를 보는 검사이므로 비용이 낮다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : 검사에 들어가는 구획과 스냅숏이 담는 네 구획의 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-admin.md#L162 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
검사는 한 구획에만 걸린다.
|
||||
|
||||
```java
|
||||
List<String> forbidden = GrpcAdminExposurePolicy.forbiddenFields(channelProfileHashes);
|
||||
if (!forbidden.isEmpty()) {
|
||||
throw new IllegalArgumentException("a platform snapshot must not carry " + forbidden + "; hashes and names only");
|
||||
}
|
||||
```
|
||||
|
||||
메시지는 "a platform snapshot must not carry …" 로 스냅숏 전체를 말한다. 검사 대상은 `channelProfileHashes` 하나다.
|
||||
|
||||
## 검사가 걸리는 구획
|
||||
|
||||
:::evidence key="grpc-admin-f02" alt="분석 문서 analysis/grpc/grpc-admin.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/grpc/grpc-admin.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 검사되지 않는 세 구획
|
||||
|
||||
같은 채널 이름 공간을 쓰는 두 맵(`resolverAndLoadBalancerByChannel`, `retryOwnerByChannel`)과 `registeredServices` 목록·`serviceHealth` 맵이다. 세 맵의 키 집합이 같아야 한다는 요구가 없으므로, 어떤 채널이 나머지 두 맵에만 있으면 그 이름은 검사를 지나지 않는다.
|
||||
|
||||
## 두 리프의 규율이 갈린다
|
||||
|
||||
`grpc-advanced-diagnostics` 의 스냅숏은 같은 형태의 자기 검사를 두 구획(주소 목록, 자원 판본 키)에 적용한다. 수정은 네 구획 전부를 같은 검사에 넣는 것이다 — 값이 아니라 키를 보는 검사이므로 비용이 낮다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
검사되지 않는 세 구획에 실제로 비밀 형태의 키를 넣어 통과를 관측하지 않았다. 검사 인자와 구획 목록의 대조로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-advanced-diagnostics-f01
|
||||
title: 마스킹이 IPv4 만 알고, 그 결과 "마스킹되지 않은 주소" 검사가 나머지 형태를 전부 통과시킨다
|
||||
topic: security-policy-enforcement
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-advanced-diagnostics-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-advanced-diagnostics-f01
|
||||
file: ../../../final/evidence/rendered/grpc-advanced-diagnostics-f01.svg
|
||||
- key: grpc-advanced-diagnostics-f01-diagram
|
||||
file: ../../../final/assets/diagrams/grpc-advanced-diagnostics-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-advanced-diagnostics-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-advanced-diagnostics.md#L167 이다.
|
||||
module: grpc-advanced-diagnostics
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 마스킹이 IPv4 만 알고, 그 결과 "마스킹되지 않은 주소" 검사가 나머지 형태를 전부 통과시킨다
|
||||
|
||||
IPv4 가 아닌 주소는 패턴에 맞지 않아 입력 그대로 반환된다. 그리고 스냅숏 생성자의 검사는 이렇게 되어 있다.
|
||||
|
||||
## 문제
|
||||
|
||||
IPv4 가 아닌 주소는 패턴에 맞지 않아 입력 그대로 반환된다.
|
||||
|
||||
그리고 스냅숏 생성자의 검사는 이렇게 되어 있다.
|
||||
|
||||
## 결론
|
||||
|
||||
마스킹 결과가 입력과 같으면 이미 마스킹된 것으로 판정한다.
|
||||
|
||||
그러므로 IPv4 가 아닌 주소는 전부 이 검사를 통과한다.
|
||||
|
||||
세 번째와 네 번째가 문제다.
|
||||
|
||||
이 플랫폼이 겨냥하는 배포 형태가 쿠버네티스이고(grpc-discovery 전체가 그 주제다), 헤드리스 레코드의 엔드포인트는 파드 DNS 이름이며 이중 스택 클러스터에서는 IPv6 주소다.
|
||||
|
||||
편집기가 막으려 한 것이 정확히 그것이다 — "a diagnostics endpoint that publishes peer addresses publishes every tenant's connection." unix 소켓 경로도 통과한다.
|
||||
|
||||
그것은 호스트 파일 시스템 경로다.
|
||||
|
||||
테스트의 주소 리터럴이 전부 IPv4 다 — 10.4.13.201:9090 · 10.9.13.201 · 10.4.x.x · 10.5.x.x.
|
||||
|
||||
IPv6 도 호스트 이름도 없다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : 마스킹 정규식이 받는 주소 형태와 스냅숏 생성자의 판정 조건 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-advanced-diagnostics.md#L167 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
IPv4 가 아닌 주소는 패턴에 맞지 않아 **입력 그대로 반환된다.** 그리고 스냅숏 생성자의 검사는 마스킹 결과가 입력과 같으면 이미 마스킹된 것으로 판정한다.
|
||||
|
||||
## 마스킹이 아는 형태
|
||||
|
||||
:::evidence key="grpc-advanced-diagnostics-f01-diagram" alt="IPv4 점 십진만 마스킹 대상 안에 놓이고 IPv6 주소와 호스트 이름 및 unix 경로가 바깥에 빗금으로 놓인다" caption="마스킹이 아는 형태" zoom="false"
|
||||
:::
|
||||
|
||||
그러므로 IPv4 가 아닌 주소는 전부 이 검사를 통과한다.
|
||||
|
||||
## 마스킹되지 않은 입력이 통과하는 이유
|
||||
|
||||
:::evidence key="grpc-advanced-diagnostics-f01" alt="분석 문서 analysis/grpc/grpc-advanced-diagnostics.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/grpc/grpc-advanced-diagnostics.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 셋째와 넷째가 문제다
|
||||
|
||||
이 플랫폼이 겨냥하는 배포 형태가 쿠버네티스이고(`grpc-discovery` 전체가 그 주제다), 헤드리스 레코드의 엔드포인트는 파드 DNS 이름이며 이중 스택 클러스터에서는 IPv6 주소다. 편집기가 막으려 한 것이 정확히 그것이다 — "a diagnostics endpoint that publishes peer addresses publishes every tenant's connection." `unix` 소켓 경로도 통과하는데, 그것은 호스트 파일 시스템 경로다.
|
||||
|
||||
## 테스트의 주소 리터럴이 전부 IPv4 다
|
||||
|
||||
`10.4.13.201:9090` · `10.9.13.201` · `10.4.x.x` · `10.5.x.x`. IPv6 도 호스트 이름도 없다.
|
||||
|
||||
## 수정
|
||||
|
||||
마스킹을 형태별로 나눈다 — IPv6 는 앞 두 그룹만 남기고 나머지를 `:x:x` 로, 호스트 이름은 최상위 라벨 몇 개만 남기고, 그 밖의 형태는 `unknown` 으로 접는다. 그리고 검사를 "결과가 입력과 같으면 통과" 가 아니라 "알려진 마스킹 형태와 일치해야 통과" 로 뒤집는다. 지금 형태는 마스킹이 모르는 입력을 전부 안전하다고 판정한다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
IPv6 주소로 스냅숏을 만들어 실행으로 재현하지 않았다. 정규식과 생성자 검사로 판정했다. 실제 Channelz 서비스를 띄우지 않았다.
|
||||
|
||||
<!-- body:end -->
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-advanced-diagnostics-f02
|
||||
title: 금지 필드 검사가 키에만 적용되고 값에는 적용되지 않는다
|
||||
topic: security-policy-enforcement
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-advanced-diagnostics-f02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-advanced-diagnostics-f02
|
||||
file: ../../../final/evidence/rendered/grpc-advanced-diagnostics-f02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-advanced-diagnostics-f02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-advanced-diagnostics.md#L206 이다.
|
||||
module: grpc-advanced-diagnostics
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 금지 필드 검사가 키에만 적용되고 값에는 적용되지 않는다
|
||||
|
||||
redact(...) 도 같다 — 금지 이름의 키를 버리고, 남은 값은 주소 필드일 때만 마스킹한다. 값 자체가 자격증명 형태인지는 보지 않는다.
|
||||
|
||||
## 문제
|
||||
|
||||
redact(...) 도 같다 — 금지 이름의 키를 버리고, 남은 값은 주소 필드일 때만 마스킹한다.
|
||||
|
||||
값 자체가 자격증명 형태인지는 보지 않는다.
|
||||
|
||||
## 결론
|
||||
|
||||
grpc-observability 의 태그 정책은 값도 본다(UUID·sha256:·bearer 패턴).
|
||||
|
||||
같은 저장소의 두 관측 편집기가 값 검사에서 갈린다.
|
||||
|
||||
xDS 자원 버전은 보통 짧은 숫자나 해시라 도달성이 낮다.
|
||||
|
||||
기록하는 이유는 두 편집기의 규율이 다르다는 점이다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : redact 가 검사하는 대상(키)과 검사하지 않는 대상(값)의 코드 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-advanced-diagnostics.md#L206 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`redact(...)` 는 금지 이름의 키를 버리고, 남은 값은 주소 필드일 때만 마스킹한다. 값 자체가 자격증명 형태인지는 보지 않는다.
|
||||
|
||||
## redact 가 보는 것
|
||||
|
||||
:::evidence key="grpc-advanced-diagnostics-f02" alt="분석 문서 analysis/grpc/grpc-advanced-diagnostics.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/grpc/grpc-advanced-diagnostics.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 같은 저장소의 다른 편집기는 값도 본다
|
||||
|
||||
`grpc-observability` 의 태그 정책은 UUID·`sha256:`·`bearer ` 패턴을 본다. 두 관측 편집기가 값 검사에서 갈린다.
|
||||
|
||||
## 도달성은 낮다
|
||||
|
||||
xDS 자원 버전은 보통 짧은 숫자나 해시다. 기록하는 이유는 두 편집기의 규율이 다르다는 점이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
자격증명 형태의 값을 실제로 넣어 통과를 관측하지 않았다. 배선 경로가 없어 실제 Channelz 스냅숏을 만들 수 없다.
|
||||
|
||||
<!-- body:end -->
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-advanced-edition-f03
|
||||
title: 정책의 자바독이 하지 않는 거부를 한다고 적고, 승격 승인이 두 곳에 따로 있다
|
||||
topic: security-policy-enforcement
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-advanced-edition-f03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-advanced-edition-f03
|
||||
file: ../../../final/evidence/rendered/grpc-advanced-edition-f03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-advanced-edition-f03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-advanced-edition.md#L216 이다.
|
||||
module: grpc-advanced-edition
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 정책의 자바독이 하지 않는 거부를 한다고 적고, 승격 승인이 두 곳에 따로 있다
|
||||
|
||||
첫째, 서술과 코드가 어긋난다. "refuses an approval nobody recorded" 에 해당하는 검사가 없다.
|
||||
|
||||
## 문제
|
||||
|
||||
첫째, 서술과 코드가 어긋난다.
|
||||
|
||||
"refuses an approval nobody recorded" 에 해당하는 검사가 없다.
|
||||
|
||||
## 결론
|
||||
|
||||
promotionApproved 는 읽히지도 검증되지도 않고 그대로 저장된다.
|
||||
|
||||
new GrpcEdition2024Policy(Set.of(), Set.of(), true) — 옵트인한 모듈도 공개 서비스도 없는데 승인만 참인 정책 — 이 아무 저항 없이 만들어지고, serviceMayMove 는 모든 서비스에 참을 답한다.
|
||||
|
||||
둘째, 같은 사실이 두 곳에 따로 있다.
|
||||
|
||||
게이트는 정책을 인자로 받지도, 참조하지도 않는다.
|
||||
|
||||
그래서 "ADR 이 없다"고 판정한 게이트와 "승인되었다"고 답하는 정책이 동시에 성립할 수 있고, 둘을 맞추는 코드가 없다.
|
||||
|
||||
§17.2 가 지적한 "두 게이트가 서로를 부르지 않는다" 와 같은 구조가 정책과 게이트 사이에도 있다.
|
||||
|
||||
정책도 게이트도 production 호출자가 없고(§12.1), 승격은 사람이 수행하는 절차다.
|
||||
|
||||
다만 이 리프가 존재하는 이유가 "그 절차를 코드로 적어 두는 것" 이므로, 적힌 절차 안에서 같은 사실이 둘로 갈라져 있는 것은 그 목적에 어긋난다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : GrpcEdition2024Policy 참조 11건 검색과 자바독 서술 대비 실제 거부 조건 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-advanced-edition.md#L216 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
서술과 코드가 어긋난다 — "refuses an approval nobody recorded" 에 해당하는 검사가 없다.
|
||||
|
||||
## GrpcEdition2024Policy 참조 위치
|
||||
|
||||
:::evidence key="grpc-advanced-edition-f03" alt="코드베이스에서 GrpcEdition2024Policy 를 검색한 출력 11줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcEdition2024Policy 코드베이스 검색 — 11줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 승인만 참인 정책이 저항 없이 만들어진다
|
||||
|
||||
`promotionApproved` 는 읽히지도 검증되지도 않고 그대로 저장된다. `new GrpcEdition2024Policy(Set.of(), Set.of(), true)` — 옵트인한 모듈도 공개 서비스도 없는데 승인만 참인 정책 — 이 만들어지고, `serviceMayMove` 는 모든 서비스에 참을 답한다.
|
||||
|
||||
## 같은 사실이 두 곳에 따로 있다
|
||||
|
||||
게이트는 정책을 인자로 받지도, 참조하지도 않는다. 그래서 "ADR 이 없다"고 판정한 게이트와 "승인되었다"고 답하는 정책이 동시에 성립할 수 있고, 둘을 맞추는 코드가 없다. §17.2 가 지적한 "두 게이트가 서로를 부르지 않는다" 와 같은 구조다.
|
||||
|
||||
## 왜 그래도 기록하나
|
||||
|
||||
정책도 게이트도 production 호출자가 없고(§12.1), 승격은 사람이 수행하는 절차다. 다만 이 리프가 존재하는 이유가 "그 절차를 코드로 적어 두는 것" 이므로, 적힌 절차 안에서 같은 사실이 둘로 갈라져 있는 것은 그 목적에 어긋난다. 수정은 `promotionBlockers` 가 정책을 받아 `promotionApproved` 를 `promotionAdr` 자리에 쓰고, 정책 생성자가 자바독대로 승인의 근거를 요구하는 것이다. 어느 쪽도 하지 않겠다면 자바독의 그 문장을 지운다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
편집 기능이 proto3 의 optional 과 같은 유선 결과를 내는지 확인하지 않았다. 그것이 이 레인의 질문이고 이 결함이 그 질문에 답할 수 없는 이유다.
|
||||
|
||||
<!-- body:end -->
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-core-api-f03
|
||||
title: RESOURCE_EXHAUSTED 매핑이 그 상태의 두 출처 중 하나만 가정한다
|
||||
topic: security-policy-enforcement
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-core-api-f03
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-core-api-f03
|
||||
file: ../../../final/evidence/rendered/grpc-core-api-f03.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-core-api-f03.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-core-api.md#L180 이다.
|
||||
module: grpc-core-api
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# RESOURCE_EXHAUSTED 매핑이 그 상태의 두 출처 중 하나만 가정한다
|
||||
|
||||
이 분기는 전송이 미시작을 증명하지 못한 뒤에 도달한다. 즉 "보냈는지 모르지만 이 상태 코드는 거절을 뜻한다" 는 판정이다.
|
||||
|
||||
## 문제
|
||||
|
||||
이 분기는 전송이 미시작을 증명하지 못한 뒤에 도달한다.
|
||||
|
||||
즉 "보냈는지 모르지만 이 상태 코드는 거절을 뜻한다" 는 판정이다.
|
||||
|
||||
## 결론
|
||||
|
||||
목록의 나머지 일곱은 서버가 일을 시작하기 전에 답하는 상태다.
|
||||
|
||||
RESOURCE_EXHAUSTED 는 두 출처를 갖는다.
|
||||
|
||||
이 플랫폼 자신의 승인 제어기가 부하를 흘려보낼 때 — 일을 쓰기 전이므로 거절이 맞다.
|
||||
|
||||
원격 서버가 작업 중 자원(할당량·디스크)을 소진했을 때 — 부분 커밋이 있을 수 있다.
|
||||
|
||||
이 클래스의 원칙은 보수적이다.
|
||||
|
||||
자바독이 두 기본값(DEADLINE_EXCEEDED·UNAVAILABLE 를 모호로)을 계획의 전역 제약이라 부르고, 그 이유는 "보냈는지 모르면 모호" 다.
|
||||
|
||||
RESOURCE_EXHAUSTED 는 그 원칙에서 벗어난 유일한 항목이다.
|
||||
|
||||
ABORTED 가 모호에 있는 것과 대비된다 — 트랜잭션 충돌은 서버가 일을 시작한 뒤의 상태이고, 그래서 모호다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : 이 분기에 도달하는 선행 조건 추적과 상태 코드의 두 출처 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-core-api.md#L180 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
이 분기는 전송이 미시작을 증명하지 못한 뒤에 도달한다 — "보냈는지 모르지만 이 상태 코드는 거절을 뜻한다" 는 판정이다. 목록의 나머지 일곱은 서버가 일을 시작하기 전에 답하는 상태다.
|
||||
|
||||
## 이 분기에 도달하는 조건
|
||||
|
||||
:::evidence key="grpc-core-api-f03" alt="분석 문서 analysis/grpc/grpc-core-api.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/grpc/grpc-core-api.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## RESOURCE_EXHAUSTED 는 두 출처를 갖는다
|
||||
|
||||
이 플랫폼 자신의 승인 제어기가 부하를 흘려보낼 때 — 일을 쓰기 전이므로 거절이 맞다. 원격 서버가 작업 중 자원(할당량·디스크)을 소진했을 때 — 부분 커밋이 있을 수 있다.
|
||||
|
||||
## 이 클래스의 원칙은 보수적이다
|
||||
|
||||
자바독이 두 기본값(`DEADLINE_EXCEEDED`·`UNAVAILABLE` 를 모호로)을 계획의 전역 제약이라 부르고, 그 이유는 "보냈는지 모르면 모호" 다. `RESOURCE_EXHAUSTED` 는 그 원칙에서 벗어난 유일한 항목이다. `ABORTED` 가 모호에 있는 것과 대비된다 — 트랜잭션 충돌은 서버가 일을 시작한 뒤의 상태이고, 그래서 모호다.
|
||||
|
||||
## 수정
|
||||
|
||||
`RESOURCE_EXHAUSTED` 를 모호로 옮기거나, 그 상태를 이 플랫폼이 발행한 것과 원격이 발행한 것으로 구분해 전자만 거절로 두는 것이다. 후자는 증거 축에 발신자 정보를 요구하므로 전자가 현실적이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
상태 코드별 매핑을 실제 서버 응답으로 재현하지 않았다. 표와 근거 문장으로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-policy-f01
|
||||
title: 스트림 승인의 경계가 동시성 아래에서 새고, caller별 맵이 줄지 않는다
|
||||
topic: security-policy-enforcement
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-policy-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-policy-f01
|
||||
file: ../../../final/evidence/rendered/grpc-policy-f01.svg
|
||||
- key: grpc-policy-f01-diagram
|
||||
file: ../../../final/assets/diagrams/grpc-policy-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-policy-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-policy.md#L184 이다.
|
||||
module: grpc-policy
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 스트림 승인의 경계가 동시성 아래에서 새고, caller별 맵이 줄지 않는다
|
||||
|
||||
읽고 비교한 뒤 별도로 증가한다. 경계에 있는 N 개 스레드가 모두 통과한다.
|
||||
|
||||
## 문제
|
||||
|
||||
읽고 비교한 뒤 별도로 증가한다.
|
||||
|
||||
경계에 있는 N 개 스레드가 모두 통과한다.
|
||||
|
||||
## 결론
|
||||
|
||||
이 클래스의 javadoc 이 서술하는 실패 상황이 곧 고동시성이다 — "a client that reconnects on every error opens streams faster than the old ones close." 재접속 폭풍에서 경계가 가장 많이 샌다.
|
||||
|
||||
release 도 같은 형태라 음수로 갈 수 있다.
|
||||
|
||||
그리고 perCaller 에서 항목이 제거되지 않는다.
|
||||
|
||||
computeIfAbsent 가 호출자 지문마다 계수기를 만들고 release 는 값만 줄인다.
|
||||
|
||||
서로 다른 호출자 수만큼 맵이 자란다 — grpc-observability 의 GrpcMetricCardinalityPolicy 가 지표 태그에 대해 명시적으로 막는 것과 같은 종류의 증가이고, 여기에는 그 가드가 없다.
|
||||
|
||||
정본이 같은 리프에 있다 — GrpcRetryBudget.tryConsume 의 비교 후 교체 루프.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : GrpcMetricCardinalityPolicy 참조 20건 검색과 승인 경로의 읽기·증가 원자성 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-policy.md#L184 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
읽고 비교한 뒤 별도로 증가한다. 경계에 있는 N 개 스레드가 모두 통과한다.
|
||||
|
||||
## 두 값이 함께 커지는 조건
|
||||
|
||||
:::evidence key="grpc-policy-f01-diagram" alt="재접속 폭주가 같은 값 읽고 통과와 계수기 증가를 지나 경계 초과와 맵 증가로 이어진다" caption="두 값이 함께 커지는 조건" zoom="false"
|
||||
:::
|
||||
|
||||
이 클래스의 javadoc 이 서술하는 실패 상황이 곧 고동시성이다 — "a client that reconnects on every error opens streams faster than the old ones close." 재접속 폭풍에서 경계가 가장 많이 샌다. `release` 도 같은 형태라 음수로 갈 수 있다.
|
||||
|
||||
## GrpcMetricCardinalityPolicy 참조 위치
|
||||
|
||||
:::evidence key="grpc-policy-f01" alt="코드베이스에서 GrpcMetricCardinalityPolicy 를 검색한 출력 20줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcMetricCardinalityPolicy 코드베이스 검색 — 20줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## perCaller 에서 항목이 제거되지 않는다
|
||||
|
||||
`computeIfAbsent` 가 호출자 지문마다 계수기를 만들고 `release` 는 값만 줄인다. 서로 다른 호출자 수만큼 맵이 자란다 — `grpc-observability` 의 `GrpcMetricCardinalityPolicy` 가 지표 태그에 대해 명시적으로 막는 것과 같은 종류의 증가이고, 여기에는 그 가드가 없다.
|
||||
|
||||
## 정본이 같은 리프에 있다
|
||||
|
||||
`GrpcRetryBudget.tryConsume` 의 비교 후 교체 루프.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
동시 승인을 실행으로 재현하지 않았다. 원자성 분석과 JMM 으로 판정했고, 어떤 인터셉터도 실제 서버에 걸어 돌리지 않았다.
|
||||
|
||||
<!-- body:end -->
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-policy-f02
|
||||
title: 자격증명 회전이 비교 후 교체가 아니고, 배수 완료가 진행 중인 회전을 되돌릴 수 있다
|
||||
topic: security-policy-enforcement
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-policy-f02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-policy-f02
|
||||
file: ../../../final/evidence/rendered/grpc-policy-f02.svg
|
||||
- key: grpc-policy-f02-diagram
|
||||
file: ../../../final/assets/diagrams/grpc-policy-f02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-policy-f02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-policy.md#L204 이다.
|
||||
module: grpc-policy
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 자격증명 회전이 비교 후 교체가 아니고, 배수 완료가 진행 중인 회전을 되돌릴 수 있다
|
||||
|
||||
AtomicReference 를 쓰면서 두 메서드 모두 읽고 나서 조건 없이 쓴다. 두 회전이 같은 observed 를 읽으면 둘 다 승계 검사를 통과할 수 있고, 나중 set 이 앞의 것을 덮는다.
|
||||
|
||||
## 문제
|
||||
|
||||
AtomicReference 를 쓰면서 두 메서드 모두 읽고 나서 조건 없이 쓴다.
|
||||
|
||||
두 회전이 같은 observed 를 읽으면 둘 다 승계 검사를 통과할 수 있고, 나중 set 이 앞의 것을 덮는다.
|
||||
|
||||
## 결론
|
||||
|
||||
덮인 회전이 배수 대상으로 기록해 둔 세대가 상태에서 사라진다.
|
||||
|
||||
그 세대 위의 호출은 아무도 배수하지 않는다.
|
||||
|
||||
javadoc 이 이 상황을 이미 알고 있다 — 승계 검사의 존재 이유로 "the usual reason for one is two rotators racing" 를 든다.
|
||||
|
||||
검사는 있고 원자성이 없다.
|
||||
|
||||
completeDrain() 이 자기가 읽은 observed.current() 로 새 상태를 만든다.
|
||||
|
||||
읽기와 쓰기 사이에 회전이 일어나면, 그 회전이 활성화한 세대가 지워지고 이전 세대가 다시 현재가 된다.
|
||||
|
||||
즉 방금 교체된 자격증명이 되살아난다.
|
||||
|
||||
클래스의 존재 이유가 "in-flight 작업을 떨어뜨리지 않고 자격 자재를 교체하는 것" 인데, 이 경로는 교체 자체를 되돌린다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : AtomicReference 를 다루는 두 메서드의 읽기·쓰기 순서 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-policy.md#L204 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`AtomicReference` 를 쓰면서 두 메서드 모두 읽고 나서 조건 없이 쓴다.
|
||||
|
||||
## 두 연산이 겹치는 결과
|
||||
|
||||
:::evidence key="grpc-policy-f02-diagram" alt="회전과 배수 완료가 각자 읽은 상태와 조건 없는 set 을 지나 이전 세대가 현재가 되는 결과로 이어진다" caption="두 연산이 겹치는 결과" zoom="false"
|
||||
:::
|
||||
|
||||
두 회전이 같은 `observed` 를 읽으면 둘 다 승계 검사를 통과할 수 있고, 나중 `set` 이 앞의 것을 덮는다. 덮인 회전이 배수 대상으로 기록해 둔 세대가 상태에서 사라지고, 그 세대 위의 호출은 아무도 배수하지 않는다.
|
||||
|
||||
## 두 메서드가 읽고 쓰는 방식
|
||||
|
||||
:::evidence key="grpc-policy-f02" alt="분석 문서 analysis/grpc/grpc-policy.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/grpc/grpc-policy.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## javadoc 이 이 상황을 이미 알고 있다
|
||||
|
||||
승계 검사의 존재 이유로 "the usual reason for one is two rotators racing" 를 든다. 검사는 있고 원자성이 없다.
|
||||
|
||||
## 배수 완료가 회전을 되돌린다
|
||||
|
||||
`completeDrain()` 이 자기가 읽은 `observed.current()` 로 새 상태를 만든다. 읽기와 쓰기 사이에 회전이 일어나면, 그 회전이 활성화한 세대가 지워지고 **이전 세대가 다시 현재가 된다** — 방금 교체된 자격증명이 되살아난다. 클래스의 존재 이유가 "in-flight 작업을 떨어뜨리지 않고 자격 자재를 교체하는 것" 인데, 이 경로는 교체 자체를 되돌린다.
|
||||
|
||||
## 수정
|
||||
|
||||
`rotate` 는 `compareAndSet(observed, next)` 가 실패하면 다시 읽어 판정하고, `completeDrain` 은 `updateAndGet(s -> new State(s.current(), null, null))` 로 현재 값을 원자적으로 읽어 쓰면 된다. 후자는 한 줄이다. 같은 형태가 `grpc-client` 의 `GrpcChannelRuntimeRegistry.rotate` 에도 있다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
동시 회전과 동시 배수 완료를 실행으로 재현하지 않았다. 조건 없는 set 이라는 코드 형태로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-server-f04
|
||||
title: 승인 제어기의 세 메서드가 원자적이지 않고, 큐 계수기를 되돌리는 경로가 없다
|
||||
topic: security-policy-enforcement
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-server-f04
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-server-f04
|
||||
file: ../../../final/evidence/rendered/grpc-server-f04.svg
|
||||
- key: grpc-server-f04-diagram
|
||||
file: ../../../final/assets/diagrams/grpc-server-f04.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-server-f04.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-server.md#L211 이다.
|
||||
module: grpc-server
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 승인 제어기의 세 메서드가 원자적이지 않고, 큐 계수기를 되돌리는 경로가 없다
|
||||
|
||||
이 리프가 SSOT 이므로 여기에 적는다. grpc-policy §17.1 이 이 클래스를 대조군으로 지목하는데, 지목된 쪽 문서에 판정이 없었다.
|
||||
|
||||
## 문제
|
||||
|
||||
이 리프가 SSOT 이므로 여기에 적는다.
|
||||
|
||||
grpc-policy §17.1 이 이 클래스를 대조군으로 지목하는데, 지목된 쪽 문서에 판정이 없었다.
|
||||
|
||||
## 결론
|
||||
|
||||
첫째, 읽고 나서 따로 증가시킨다.
|
||||
|
||||
경계에 있는 N 개 스레드가 모두 통과한다.
|
||||
|
||||
AtomicInteger 를 쓰면서 비교와 증가를 나눈 형태이고, 같은 가족의 정본이 GrpcRetryBudget.tryConsume 의 비교 후 교체 루프다.
|
||||
|
||||
release()·promoteFromQueue() 도 같다 — get() > 0 을 확인한 뒤 별도로 감소시키므로, 두 스레드가 같은 마지막 하나를 보고 둘 다 감소시켜 음수가 될 수 있다.
|
||||
|
||||
클래스가 Math.max(0, …) 같은 하한도 두지 않는다.
|
||||
|
||||
둘째, 큐 계수기를 되돌리는 경로가 없다.
|
||||
|
||||
큐에 들어간 호출도 admitted=true 를 받는다.
|
||||
|
||||
그런데 그 경로는 queued 만 올리고 inFlight 는 올리지 않는다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : 두 계수기의 증가·감소 지점 대조와 큐 계수기 반납 메서드 유무 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-server.md#L211 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
이 리프가 SSOT 이므로 여기에 적는다. `grpc-policy` §17.1 이 이 클래스를 대조군으로 지목하는데, 지목된 쪽 문서에 판정이 없었다.
|
||||
|
||||
## 첫째 — 읽고 나서 따로 증가시킨다
|
||||
|
||||
```java
|
||||
public Decision tryAdmit() {
|
||||
int running = inFlight.get();
|
||||
if (running < maxConcurrentCalls) {
|
||||
inFlight.incrementAndGet(); // ← 읽기와 증가 사이에 다른 스레드가 들어온다
|
||||
return new Decision(true, …);
|
||||
}
|
||||
int waiting = queued.get();
|
||||
if (waiting < maxQueuedCalls) {
|
||||
queued.incrementAndGet(); // ← 같은 형태
|
||||
```
|
||||
|
||||
경계에 있는 N 개 스레드가 모두 통과한다. 같은 가족의 정본이 `GrpcRetryBudget.tryConsume` 의 비교 후 교체 루프다. `release()`·`promoteFromQueue()` 도 같다 — `get() > 0` 을 확인한 뒤 별도로 감소시키므로 두 스레드가 같은 마지막 하나를 보고 둘 다 감소시켜 음수가 될 수 있고, `Math.max(0, …)` 같은 하한도 없다.
|
||||
|
||||
## 계수기 쌍의 비대칭
|
||||
|
||||
:::evidence key="grpc-server-f04-diagram" alt="진행 계수기 쪽에 tryAdmit 증가와 release 감소가 놓이고 큐 계수기 쪽에 tryAdmit 증가와 반납 메서드 없음이 놓인다" caption="계수기 쌍의 비대칭" zoom="false"
|
||||
:::
|
||||
|
||||
## 대조군으로 지목된 클래스
|
||||
|
||||
:::evidence key="grpc-server-f04" alt="분석 문서 analysis/grpc/grpc-server.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/grpc/grpc-server.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 둘째 — 큐 계수기를 되돌리는 경로가 없다
|
||||
|
||||
큐에 들어간 호출도 `admitted=true` 를 받는데 그 경로는 `queued` 만 올리고 `inFlight` 는 올리지 않는다. 그리고 끝난 호출을 반납하는 메서드는 하나뿐이다. 따라서 호출자가 `promoteFromQueue()` 를 정확히 한 번 끼워 넣지 않으면 계수기가 어긋난다 — 큐에서 실행된 호출이 끝나면 `queued` 는 그대로이고 `inFlight` 만 줄어든다. `releaseQueued()` 같은 메서드도, 그 짝짓기를 요구하는 서술도 없다.
|
||||
|
||||
## 시험이 짝지어 부른다
|
||||
|
||||
두 시험 모두 단일 스레드이고, `releaseAndPromotionTrackCapacity` 는 `release()` 와 `promoteFromQueue()` 를 짝지어 부른다. 짝짓지 않는 경로는 시험되지 않는다.
|
||||
|
||||
## 배선하는 순간 P1 이다
|
||||
|
||||
오늘 호출자가 없으므로(§12.1) P2. 승인 단계를 배선하면 부하 아래에서 경계가 새는 것과, 큐 계수기가 단조 증가해 `at capacity` 가 영구히 참이 되는 것이 함께 온다. 세 메서드를 비교 후 교체 루프로 바꾸고, 큐 경로에 대응하는 반납 메서드를 둔다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
경합을 실행으로 재현하지 않았다. 읽기와 증가가 분리되어 있다는 것과 하한 가드가 없다는 것으로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: grpc-testkit-f06
|
||||
title: 던져 버릴 비밀번호를 만들어 놓고 외부 프로세스의 명령줄에 싣는다
|
||||
topic: security-policy-enforcement
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:grpc-testkit-f06
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: grpc-testkit-f06
|
||||
file: ../../../final/evidence/rendered/grpc-testkit-f06.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/grpc-testkit-f06.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-testkit.md#L262 이다.
|
||||
module: grpc-testkit
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 던져 버릴 비밀번호를 만들어 놓고 외부 프로세스의 명령줄에 싣는다
|
||||
|
||||
GrpcTlsTestMaterial 이 상수 비밀번호를 피하는 이유를 세 줄로 적는다. 그리고 같은 클래스가 그 값을 keytool 인자로 넘긴다.
|
||||
|
||||
## 문제
|
||||
|
||||
GrpcTlsTestMaterial 이 상수 비밀번호를 피하는 이유를 세 줄로 적는다.
|
||||
|
||||
그리고 같은 클래스가 그 값을 keytool 인자로 넘긴다.
|
||||
|
||||
## 결론
|
||||
|
||||
프로세스 명령줄은 같은 호스트의 다른 사용자가 ps 나 /proc/<pid>/cmdline 로 읽을 수 있다.
|
||||
|
||||
소스 리터럴보다 관측 가능성이 오히려 높다.
|
||||
|
||||
영향은 작다 — 값이 매번 새로 만들어지고, 키스토어는 임시 디렉터리에 있으며 close() 가 지운다.
|
||||
|
||||
기록하는 이유는 이 클래스가 정확히 그 위험 계층을 스스로 논증했다는 점이다.
|
||||
|
||||
완화와 노출이 같은 메서드 안에 있다.
|
||||
|
||||
keytool 은 -storepass:file 과 -keypass:file 을 받는다.
|
||||
|
||||
임시 파일 하나면 명령줄에서 값이 사라진다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : GrpcTlsTestMaterial 참조 16건 검색과 비밀번호가 실리는 인자 목록 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/grpc/grpc-testkit.md#L262 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`GrpcTlsTestMaterial` 이 상수 비밀번호를 피하는 이유를 세 줄로 적는다. 그리고 같은 클래스가 그 값을 `keytool` 인자로 넘긴다.
|
||||
|
||||
## GrpcTlsTestMaterial 참조 위치
|
||||
|
||||
:::evidence key="grpc-testkit-f06" alt="코드베이스에서 GrpcTlsTestMaterial 를 검색한 출력 16줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="GrpcTlsTestMaterial 코드베이스 검색 — 16줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 명령줄은 같은 호스트의 다른 사용자가 읽을 수 있다
|
||||
|
||||
`ps` 나 `/proc/<pid>/cmdline` 로 읽는다 — 소스 리터럴보다 관측 가능성이 오히려 높다.
|
||||
|
||||
## 영향은 작다
|
||||
|
||||
값이 매번 새로 만들어지고, 키스토어는 임시 디렉터리에 있으며 `close()` 가 지운다. 기록하는 이유는 이 클래스가 정확히 그 위험 계층을 스스로 논증했다는 점이다 — 완화와 노출이 같은 메서드 안에 있다.
|
||||
|
||||
## 수정
|
||||
|
||||
`keytool` 은 `-storepass:file` 과 `-keypass:file` 을 받는다. 임시 파일 하나면 명령줄에서 값이 사라진다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
keytool 명령줄 노출을 실제로 ps 로 관측하지 않았다. ProcessBuilder 인자 목록에 비밀번호가 들어간다는 것으로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: ipv4-only-mask-passes-every-other-form
|
||||
title: 마스킹이 IPv4 만 알아서 검사가 나머지 주소 형태를 전부 통과시킨다
|
||||
topic: security-policy-enforcement
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:ipv4-only-mask-passes-every-other-form
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
body: case-ipv4-only-mask-passes-every-other-form.body.md
|
||||
assets:
|
||||
- key: ipv4-only-mask-passes-every-other-form
|
||||
file: ../../../final/evidence/rendered/ipv4-only-mask-passes-every-other-form.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/ipv4-only-mask-passes-every-other-form.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/grpc/grpc-advanced-diagnostics.md#L123 이다.
|
||||
---
|
||||
|
||||
# 마스킹이 IPv4 만 알아서 검사가 나머지 주소 형태를 전부 통과시킨다
|
||||
|
||||
주소 마스킹이 정규식 하나만 갖고 맞지 않는 입력을 그대로 돌려준다. 스냅숏 생성자는 마스킹 결과가 입력과 같으면 이미 마스킹된 것으로 판정한다. 두 규칙이 겹쳐 IPv6 주소와 호스트 이름과 소켓 경로가 전부 통과한다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **위험한 조합은 정책이 아니라 생성자가 거부하게 만든다**
|
||||
같은 계열의 규칙이다.
|
||||
- **붉은 test를 제품 결함으로 잘못 읽었다**
|
||||
같은 사이클이 철회한 P1 의 원인과 같은 계열의 가정이다.
|
||||
- **문서끼리의 일치는 아무것도 증명하지 않는다**
|
||||
검사의 방향을 다루는 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
진단 표면은 노출이 위험한 자리다.
|
||||
|
||||
편집기 자바독이 그 이유를 적는다. 이 표면이 정말로 유용하기 때문에 위험하다는 것이다. 모든 소켓의 지역·원격 주소와 각 연결의 보안 세부와 호출별 상태를 담고 있으며, 종점이 존재하는 순간 그 전체가 인가 실수 하나 거리에 있다는 것이다.
|
||||
|
||||
그래서 주소를 버리지 않고 가린다. 운영자가 두 서브채널을 구별할 수 있어야 하기 때문이다.
|
||||
|
||||
그리고 스냅숏이 스스로 검사한다. 마스킹되지 않은 주소를 담은 스냅숏은 만들 수 없다.
|
||||
|
||||
## 결론
|
||||
|
||||
그 검사의 범위가 좁다.
|
||||
|
||||
마스킹 함수는 IPv4 와 선택적 포트를 인식하는 정규식 하나만 갖는다. 맞지 않는 입력은 치환이 일어나지 않아 입력 그대로 반환된다.
|
||||
|
||||
그리고 스냅숏 생성자의 검사는 마스킹 결과가 입력과 같으면 이미 마스킹된 것으로 판정한다.
|
||||
|
||||
두 규칙이 겹치면 IPv4 가 아닌 주소는 전부 검사를 통과한다.
|
||||
|
||||
IPv6 주소가 통과한다. 파드 DNS 이름이 통과한다. 유닉스 소켓 경로도 통과한다. 마지막 것은 호스트 파일 시스템 경로다.
|
||||
|
||||
이 플랫폼이 겨냥하는 배포 형태를 보면 도달 가능성이 낮지 않다. 같은 가족의 탐색 리프 전체가 쿠버네티스 라우팅을 주제로 하고, 헤드리스 레코드의 엔드포인트는 파드 DNS 이름이며 이중 스택 군집에서는 IPv6 주소다.
|
||||
|
||||
편집기가 막으려 한 것이 정확히 그것이다. 피어 주소를 게시하는 진단 종점은 모든 소속의 연결을 게시한다는 문장이다.
|
||||
|
||||
테스트가 이것을 볼 수 없다. 주소 리터럴이 전부 IPv4 다.
|
||||
|
||||
같은 사이클이 철회한 최상위 판정의 원인도 같은 계열이었다. 이중 스택 호스트 이름이 픽스처에서 다른 주소로 풀린 것이다. 두 사례의 공통점은 주소 표현의 다양성이 아니라 판정의 방향이다. 둘 다 모르는 형태를 안전한 쪽이 아니라 통과 쪽으로 접었다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12
|
||||
확인 방식 : 정규식과 생성자 검사 대조, 테스트의 주소 리터럴 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 document-detail 의 analysis/grpc/grpc-advanced-diagnostics.md 에 있다.
|
||||
|
||||
1. 마스킹 함수의 정규식과 치환을 읽는다.
|
||||
2. 맞지 않는 입력에서 무엇이 반환되는지 확인한다.
|
||||
3. 스냅숏 생성자의 마스킹 검사 조건을 읽는다.
|
||||
4. 두 규칙을 겹쳐 어떤 형태가 통과하는지 나열한다.
|
||||
5. 테스트의 주소 리터럴을 전부 확인한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`maskAddress` 는 IPv4 패턴 하나만 갖고 맞지 않는 입력을 그대로 돌려준다. 그리고 스냅숏 생성자는 마스킹 결과가 입력과 같으면 이미 마스킹된 것으로 판정한다.
|
||||
|
||||
## maskAddress 가 가진 패턴
|
||||
|
||||
:::evidence key="ipv4-only-mask-passes-every-other-form" alt="분석 문서 analysis/grpc/grpc-advanced-diagnostics.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/grpc/grpc-advanced-diagnostics.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 두 규칙이 겹치면 나머지가 전부 통과한다
|
||||
|
||||
IPv6 주소·파드 DNS 이름·유닉스 소켓 경로가 검사를 지난다. 이 플랫폼이 겨냥하는 배포가 쿠버네티스이고 헤드리스 레코드의 엔드포인트가 DNS 이름이므로 도달 가능한 형태다.
|
||||
|
||||
## 테스트의 주소 리터럴은 전부 IPv4 다
|
||||
|
||||
같은 사이클이 철회한 P1 의 원인도 듀얼스택 호스트명이었다 — 판정이 모르는 형태를 통과 쪽으로 접는 같은 계열이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
IPv6 주소로 스냅숏을 만들어 통과를 재현하지 않았다. 이 리프는 고급 가족이라 배선 경로가 없다.
|
||||
|
||||
<!-- body:end -->
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-admin-api-f02
|
||||
title: 계획 다이제스트가 승인 정규 형식과 다른 인코딩을 쓴다
|
||||
topic: security-policy-enforcement
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-admin-api-f02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-admin-api-f02
|
||||
file: ../../../final/evidence/rendered/messaging-admin-api-f02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-admin-api-f02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-admin-api.md#L944 이다.
|
||||
module: messaging-admin-api
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 계획 다이제스트가 승인 정규 형식과 다른 인코딩을 쓴다
|
||||
|
||||
ApprovalGrant.canonicalForm() 은 길이 접두를, ReplayPlan.digest()/RedrivePlan.digest() 는 String.join("|", …) 를 쓴다(§12.3(b), EVD-304). 현재는 충돌을 만들 수 없다 — 자유 형식 필드가 topologyVersion 하나뿐이기 때문이다.
|
||||
|
||||
## 문제
|
||||
|
||||
ApprovalGrant.canonicalForm() 은 길이 접두를, ReplayPlan.digest()/RedrivePlan.digest() 는 String.join("|", …) 를 쓴다(§12.3(b), EVD-304).
|
||||
|
||||
현재는 충돌을 만들 수 없다 — 자유 형식 필드가 topologyVersion 하나뿐이기 때문이다.
|
||||
|
||||
## 결론
|
||||
|
||||
그러나 그 조건은 코드 어디에도 적혀 있지 않고, 필드가 하나 추가되면 조용히 깨진다.
|
||||
|
||||
ApprovalGrant 의 appendField 를 PlanDigest 쪽으로 옮겨 재사용하는 편이 낫다 — 규칙과 그 근거가 이미 같은 리프에 있다.
|
||||
|
||||
부수적으로 topologyVersion 에 형식 제약을 주는 것도 검토할 만하다.
|
||||
|
||||
지금은 isBlank() 만 본다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : ApprovalGrant 참조 32건 검색과 두 인코딩 방식의 형태 대조, 자유 형식 필드 전수 조사
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-admin-api.md#L944 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`ApprovalGrant.canonicalForm()` 은 길이 접두를, `ReplayPlan.digest()`/`RedrivePlan.digest()` 는 `String.join("|", …)` 를 쓴다(§12.3(b), `EVD-304`).
|
||||
|
||||
## ApprovalGrant 참조 위치
|
||||
|
||||
:::evidence key="messaging-admin-api-f02" alt="코드베이스에서 ApprovalGrant 를 검색한 출력 32줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="ApprovalGrant 코드베이스 검색 — 32줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 현재는 충돌을 만들 수 없다
|
||||
|
||||
자유 형식 필드가 `topologyVersion` 하나뿐이기 때문이다. 그러나 그 조건은 코드 어디에도 적혀 있지 않고, 필드가 하나 추가되면 조용히 깨진다.
|
||||
|
||||
## 규칙과 근거가 이미 같은 리프에 있다
|
||||
|
||||
`ApprovalGrant` 의 `appendField` 를 `PlanDigest` 쪽으로 옮겨 재사용하는 편이 낫다. 부수적으로 `topologyVersion` 에 형식 제약을 주는 것도 검토할 만하다 — 지금은 `isBlank()` 만 본다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
topologyVersion 이 실제 배포에서 어떤 형식인지 확인하지 못했다. BrokerTopologyInspector 구현이 이 저장소에 없다.
|
||||
|
||||
<!-- body:end -->
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-admin-api-f05
|
||||
title: VerifiedApproval 의 위조 방지가 package-private 에만 의존한다
|
||||
topic: security-policy-enforcement
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-admin-api-f05
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-admin-api-f05
|
||||
file: ../../../final/evidence/rendered/messaging-admin-api-f05.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-admin-api-f05.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-admin-api.md#L962 이다.
|
||||
module: messaging-admin-api
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# VerifiedApproval 의 위조 방지가 package-private 에만 의존한다
|
||||
|
||||
이 저장소는 JPMS 를 쓰지 않는다(module-info.java 0개). 따라서 어떤 모듈이든 package dev.caskeleton.messaging.admin; 을 선언하면 VerifiedApproval.of(grant) 를 호출할 수 있다.
|
||||
|
||||
## 문제
|
||||
|
||||
이 저장소는 JPMS 를 쓰지 않는다(module-info.java 0개).
|
||||
|
||||
따라서 어떤 모듈이든 package dev.caskeleton.messaging.admin; 을 선언하면 VerifiedApproval.of(grant) 를 호출할 수 있다.
|
||||
|
||||
## 결론
|
||||
|
||||
현재 그런 파일은 없지만, 이 타입의 존재 이유가 "아무도 만들 수 없다" 이므로 그 조건을 자동으로 지키는 검사가 있어야 한다.
|
||||
|
||||
ApprovalForgeryTest.aVerifiedApprovalCannotBeConstructedOutsideTheVerifier 가 있으나, 그것은 같은 패키지 안에서 API 표면을 확인하는 테스트지 다른 모듈의 패키지 선언을 막지 못한다.
|
||||
|
||||
ArchUnit 규칙 한 줄 — "dev.caskeleton.messaging.admin 패키지는 messaging-admin-api 소스 경로에만 존재한다" — 이면 된다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : VerifiedApproval 참조 40건 검색과 저장소 전체의 module-info.java 수 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-admin-api.md#L962 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
이 저장소는 JPMS 를 쓰지 않는다(`module-info.java` 0개). 따라서 어떤 모듈이든 `package dev.caskeleton.messaging.admin;` 을 선언하면 `VerifiedApproval.of(grant)` 를 호출할 수 있다.
|
||||
|
||||
## VerifiedApproval 참조 위치
|
||||
|
||||
:::evidence key="messaging-admin-api-f05" alt="코드베이스에서 VerifiedApproval 를 검색한 출력 40줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="VerifiedApproval 코드베이스 검색 — 40줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 이 타입의 존재 이유가 그 조건이다
|
||||
|
||||
"아무도 만들 수 없다" 이므로 그 조건을 자동으로 지키는 검사가 있어야 한다. 현재 그런 파일은 없다.
|
||||
|
||||
## 기존 테스트는 다른 것을 본다
|
||||
|
||||
`ApprovalForgeryTest.aVerifiedApprovalCannotBeConstructedOutsideTheVerifier` 는 같은 패키지 안에서 API 표면을 확인하는 테스트지 다른 모듈의 패키지 선언을 막지 못한다.
|
||||
|
||||
## 수정
|
||||
|
||||
ArchUnit 규칙 한 줄 — "`dev.caskeleton.messaging.admin` 패키지는 `messaging-admin-api` 소스 경로에만 존재한다" — 이면 된다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
같은 패키지를 선언하는 모듈을 만들어 실제로 위조를 시도하지 않았다. JPMS 미사용과 접근 제한자로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-admin-api-f07
|
||||
title: 같은 인가 실패 코드가 세 파일에 문자열 리터럴로 흩어져 있다
|
||||
topic: security-policy-enforcement
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-admin-api-f07
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-admin-api-f07
|
||||
file: ../../../final/evidence/rendered/messaging-admin-api-f07.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-admin-api-f07.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-admin-api.md#L972 이다.
|
||||
module: messaging-admin-api
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 같은 인가 실패 코드가 세 파일에 문자열 리터럴로 흩어져 있다
|
||||
|
||||
APPROVAL_OPERATION_MISMATCH, APPROVAL_SOURCE_MISMATCH, APPROVAL_PLAN_MISMATCH, APPROVAL_EXPIRED 가 HmacApprovalVerifier, DestructiveOperationGuard, ApprovedReplayPlan, ApprovedRedrivePlan 에 각각 리터럴로 존재하며 메시지 문구가 서로 다르다. 검사의 3중화 자체는 의도된 심층 방어지만(§12.2 대조군 2), 코드 문자열은 상수 하나로 모으는 편이 집계와 검색에 낫다.
|
||||
|
||||
## 문제
|
||||
|
||||
APPROVAL_OPERATION_MISMATCH, APPROVAL_SOURCE_MISMATCH, APPROVAL_PLAN_MISMATCH, APPROVAL_EXPIRED 가 HmacApprovalVerifier, DestructiveOperationGuard, ApprovedReplayPlan, ApprovedRedrivePlan 에 각각 리터럴로 존재하며 메시지 문구가 서로 다르다.
|
||||
|
||||
검사의 3중화 자체는 의도된 심층 방어지만(§12.2 대조군 2), 코드 문자열은 상수 하나로 모으는 편이 집계와 검색에 낫다.
|
||||
|
||||
## 결론
|
||||
|
||||
APPROVAL_OPERATION_MISMATCH, APPROVAL_SOURCE_MISMATCH, APPROVAL_PLAN_MISMATCH, APPROVAL_EXPIRED 가 HmacApprovalVerifier, DestructiveOperationGuard, ApprovedReplayPlan, ApprovedRedrivePlan 에 각각 리터럴로 존재하며 메시지 문구가 서로 다르다.
|
||||
|
||||
검사의 3중화 자체는 의도된 심층 방어지만(§12.2 대조군 2), 코드 문자열은 상수 하나로 모으는 편이 집계와 검색에 낫다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : HmacApprovalVerifier 참조 14건 검색과 네 코드가 리터럴로 선언된 파일 집계
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-admin-api.md#L972 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`APPROVAL_OPERATION_MISMATCH`, `APPROVAL_SOURCE_MISMATCH`, `APPROVAL_PLAN_MISMATCH`, `APPROVAL_EXPIRED` 가 `HmacApprovalVerifier`, `DestructiveOperationGuard`, `ApprovedReplayPlan`, `ApprovedRedrivePlan` 에 각각 리터럴로 존재하며 메시지 문구가 서로 다르다.
|
||||
|
||||
## HmacApprovalVerifier 참조 위치
|
||||
|
||||
:::evidence key="messaging-admin-api-f07" alt="코드베이스에서 HmacApprovalVerifier 를 검색한 출력 14줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="HmacApprovalVerifier 코드베이스 검색 — 14줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 3중화 자체는 의도된 심층 방어다
|
||||
|
||||
§12.2 대조군 2. 다만 코드 문자열은 상수 하나로 모으는 편이 집계와 검색에 낫다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
한 리터럴만 바꿔 검증이 조용히 어긋나는 것을 실행으로 재현하지 않았다. 리터럴의 분포로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-admin-runtime-f01
|
||||
title: 파괴적 작업의 승인만 위조 가능한 형태로 남아 있다
|
||||
topic: security-policy-enforcement
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-admin-runtime-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-admin-runtime-f01
|
||||
file: ../../../final/evidence/rendered/messaging-admin-runtime-f01.svg
|
||||
- key: messaging-admin-runtime-f01-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-admin-runtime-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-admin-runtime-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-admin-runtime.md#L883 이다.
|
||||
module: messaging-admin-runtime
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 파괴적 작업의 승인만 위조 가능한 형태로 남아 있다
|
||||
|
||||
생성자는 null·음수만 본다. approval 이 이 operation 을 인가하는지, 이 destination 을 인가하는지, estimatedMessagesAffected 가 승인 상한 이하인지 — 아무것도 검사하지 않는다.
|
||||
|
||||
## 문제
|
||||
|
||||
생성자는 null·음수만 본다.
|
||||
|
||||
approval 이 이 operation 을 인가하는지, 이 destination 을 인가하는지, estimatedMessagesAffected 가 승인 상한 이하인지 — 아무것도 검사하지 않는다.
|
||||
|
||||
## 결론
|
||||
|
||||
계획 다이제스트 필드 자체가 없다.
|
||||
|
||||
이 형태가 정확히 messaging-admin-api 가 고쳤다고 기록한 것이다.
|
||||
|
||||
수정은 REPLAY·REDRIVE(복구 가능한 작업)에 적용되었고, PURGE·DELETE_DESTINATION·OFFSET_RESET(복구 불가능한 작업)에는 적용되지 않았다.
|
||||
|
||||
현재 구현체가 0건이라 실행되는 결함은 아니다(EVD-307).
|
||||
|
||||
그러나 이 인터페이스는 운영자 도구가 구현하라고 존재하는 것이고, 그 도구가 생기는 순간의 모양이 이것이다.
|
||||
|
||||
Approved 를 ApprovedReplayPlan 과 같은 형태로 — VerifiedApproval + 생성자 검사 — 바꾸는 것이 맞다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : Approved record 의 생성자 검사 항목과 검증된 승인 타입이 적용된 작업 범위의 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-admin-runtime.md#L883 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
생성자는 null·음수만 본다. `approval` 이 이 `operation` 을 인가하는지, 이 `destination` 을 인가하는지, `estimatedMessagesAffected` 가 승인 상한 이하인지 — 아무것도 검사하지 않는다. 계획 다이제스트 필드 자체가 없다.
|
||||
|
||||
## 보호가 적용된 절반
|
||||
|
||||
:::evidence key="messaging-admin-runtime-f01-diagram" alt="REPLAY 와 REDRIVE 가 검증된 승인이 덮는 범위 안에 놓이고 PURGE 와 OFFSET_RESET 과 DELETE_DESTINATION 이 바깥에 빗금으로 놓인다" caption="보호가 적용된 절반" zoom="false"
|
||||
:::
|
||||
|
||||
## 생성자가 보는 것
|
||||
|
||||
:::evidence key="messaging-admin-runtime-f01" alt="분석 문서 analysis/messaging/messaging-admin-runtime.md 에서 이 기록의 근거 절을 그대로 잘라낸 15줄. 코드베이스를 측정한 것이 아니라 원본 판정이 무엇을 적었는지를 보여 준다." caption="analysis/messaging/messaging-admin-runtime.md 발췌 — 15줄" zoom="true"
|
||||
:::
|
||||
|
||||
## 정확히 admin-api 가 고쳤다고 기록한 형태다
|
||||
|
||||
수정은 `REPLAY`·`REDRIVE`(복구 가능한 작업)에 적용되었고, `PURGE`·`DELETE_DESTINATION`·`OFFSET_RESET`(복구 불가능한 작업)에는 적용되지 않았다.
|
||||
|
||||
## 지금 실행되는 결함은 아니다
|
||||
|
||||
현재 구현체가 0건이다(`EVD-307`). 그러나 이 인터페이스는 운영자 도구가 구현하라고 존재하는 것이고, 그 도구가 생기는 순간의 모양이 이것이다. `Approved` 를 `ApprovedReplayPlan` 과 같은 형태로 — `VerifiedApproval` + 생성자 검사 — 바꾸는 것이 맞다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
위조 승인을 넣어 실행해 보지 않았다. 구현체가 0 건이라 실행 경로 자체가 없다.
|
||||
|
||||
<!-- body:end -->
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-admin-runtime-f05
|
||||
title: 감사 싱크가 중복 선언되어 있고 레닥션 계약이 유실된다
|
||||
topic: security-policy-enforcement
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-admin-runtime-f05
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-admin-runtime-f05
|
||||
file: ../../../final/evidence/rendered/messaging-admin-runtime-f05.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-admin-runtime-f05.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-admin-runtime.md#L930 이다.
|
||||
module: messaging-admin-runtime
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 감사 싱크가 중복 선언되어 있고 레닥션 계약이 유실된다
|
||||
|
||||
RedriveService.AuditSink 는 MessagingAuditSink 와 시그니처가 같다. admin-runtime 은 이미 messaging-observability 를 의존한다.
|
||||
|
||||
## 문제
|
||||
|
||||
RedriveService.AuditSink 는 MessagingAuditSink 와 시그니처가 같다.
|
||||
|
||||
admin-runtime 은 이미 messaging-observability 를 의존한다.
|
||||
|
||||
## 결론
|
||||
|
||||
표준 싱크를 쓰면 세 가지가 함께 해결된다: ReplayService 가 형제의 중첩 타입에 의존하는 것, InMemory 구현 재작성, 그리고 무엇보다 "모든 기록이 MessagingRedactor 를 통과했다" 는 계약.
|
||||
|
||||
현재 RedriveService:125-136 은 목적지 이름과 details 를 그대로 넣는다.
|
||||
|
||||
목적지 이름은 DestinationName 이라 형식이 제한되어 있어 지금은 문제가 아니지만, 계약이 없는 자리에 값이 늘어나는 것을 막을 것이 없다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : RedriveService 참조 15건 검색과 중첩 인터페이스·플랫폼 싱크의 시그니처 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-admin-runtime.md#L930 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`RedriveService.AuditSink` 는 `MessagingAuditSink` 와 시그니처가 같다. admin-runtime 은 이미 `messaging-observability` 를 의존한다.
|
||||
|
||||
## RedriveService 참조 위치
|
||||
|
||||
:::evidence key="messaging-admin-runtime-f05" alt="코드베이스에서 RedriveService 를 검색한 출력 15줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RedriveService 코드베이스 검색 — 15줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 표준 싱크를 쓰면 셋이 함께 해결된다
|
||||
|
||||
`ReplayService` 가 형제의 중첩 타입에 의존하는 것, `InMemory` 구현 재작성, 그리고 무엇보다 **"모든 기록이 `MessagingRedactor` 를 통과했다" 는 계약**.
|
||||
|
||||
## 지금은 계약이 없는 자리다
|
||||
|
||||
`RedriveService:125-136` 은 목적지 이름과 details 를 그대로 넣는다. 목적지 이름은 `DestinationName` 이라 형식이 제한되어 있어 지금은 문제가 아니지만, 계약이 없는 자리에 값이 늘어나는 것을 막을 것이 없다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
레닥션이 빠진 감사 이벤트를 실제로 기록해 관측하지 않았다. 두 시그니처의 대조와 의존 선언으로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-admin-runtime-f08
|
||||
title: 격리 리플레이의 guard 우회가 dryRun 파라미터로 표현된다
|
||||
topic: security-policy-enforcement
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-admin-runtime-f08
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-admin-runtime-f08
|
||||
file: ../../../final/evidence/rendered/messaging-admin-runtime-f08.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-admin-runtime-f08.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-admin-runtime.md#L948 이다.
|
||||
module: messaging-admin-runtime
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 격리 리플레이의 guard 우회가 dryRun 파라미터로 표현된다
|
||||
|
||||
판단 자체는 근거가 있다. 다만 "승인이 필요 없다" 와 "실제로는 아무것도 하지 않는다" 가 guard 입장에서 구별되지 않는다.
|
||||
|
||||
## 문제
|
||||
|
||||
판단 자체는 근거가 있다.
|
||||
|
||||
다만 "승인이 필요 없다" 와 "실제로는 아무것도 하지 않는다" 가 guard 입장에서 구별되지 않는다.
|
||||
|
||||
## 결론
|
||||
|
||||
DestructiveOperationGuard 에 skipAuthorization 성격의 별도 경로를 두거나, 격리 리플레이는 애초에 guard 를 거치지 않는 편이 의도를 드러낸다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : DestructiveOperationGuard 참조 19건 검색과 guard 에 넘어가는 인자의 의미 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-admin-runtime.md#L948 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
guard 호출이 두 뜻을 한 인자로 합친다.
|
||||
|
||||
```java
|
||||
// ReplayService.java:58-64
|
||||
guard.authorize(REPLAY, request.destination(), approval, request.dryRun() || !needsApproval, now);
|
||||
```
|
||||
|
||||
## DestructiveOperationGuard 참조 위치
|
||||
|
||||
:::evidence key="messaging-admin-runtime-f08" alt="코드베이스에서 DestructiveOperationGuard 를 검색한 출력 19줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DestructiveOperationGuard 코드베이스 검색 — 19줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 판단 자체는 근거가 있다
|
||||
|
||||
다만 "승인이 필요 없다" 와 "실제로는 아무것도 하지 않는다" 가 guard 입장에서 구별되지 않는다. `DestructiveOperationGuard` 에 `skipAuthorization` 성격의 별도 경로를 두거나, 격리 리플레이는 애초에 guard 를 거치지 않는 편이 의도를 드러낸다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
guard 를 실제로 호출해 두 경우가 구별되지 않는 것을 관측하지 않았다. 인자 하나가 두 뜻을 겸한다는 호출 형태로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-observability-f05
|
||||
title: 자격증명 판정이 core-api보다 약하다
|
||||
topic: security-policy-enforcement
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-observability-f05
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-observability-f05
|
||||
file: ../../../final/evidence/rendered/messaging-observability-f05.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-observability-f05.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-observability.md#L708 이다.
|
||||
module: messaging-observability
|
||||
priority: P3
|
||||
---
|
||||
|
||||
# 자격증명 판정이 core-api보다 약하다
|
||||
|
||||
MessagingRedactor.isDenied는 27키 정확 일치다. messaging-core-api의 MessageHeaders.carriesACredential은 세그먼트 매칭 + 인접 결합으로 x-api-key·auth-token·db_password를 잡는다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **타입이 문서화한 불변식은 타입이 강제한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
MessagingRedactor.isDenied는 27키 정확 일치다.
|
||||
|
||||
messaging-core-api의 MessageHeaders.carriesACredential은 세그먼트 매칭 + 인접 결합으로 x-api-key·auth-token·db_password를 잡는다.
|
||||
|
||||
## 결론
|
||||
|
||||
redactor의 denylist에 api_key·apikey는 있으나 x-api-key는 없다.
|
||||
|
||||
두 표면이 다르지만 더 자유로운 입력을 받는 쪽이 더 약하다.
|
||||
|
||||
이 leaf 자신이 진단 맵을 "the one place where a caller can pass arbitrary keys"라고 부른다.
|
||||
|
||||
그리고 recordDiagnostics가 redaction을 첫 단계로 두는 이유가 바로 그것이다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : MessagingRedactor 참조 13건 검색과 core-api 쪽 판정 함수의 매칭 방식 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-observability.md#L708 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`MessagingRedactor.isDenied`는 27키 **정확 일치**다. `messaging-core-api`의 `MessageHeaders.carriesACredential`은 세그먼트 매칭 + 인접 결합으로 `x-api-key`·`auth-token`·`db_password`를 잡는다.
|
||||
|
||||
## MessagingRedactor 참조 위치
|
||||
|
||||
:::evidence key="messaging-observability-f05" alt="코드베이스에서 MessagingRedactor 를 검색한 출력 13줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessagingRedactor 코드베이스 검색 — 13줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 더 자유로운 입력을 받는 쪽이 더 약하다
|
||||
|
||||
redactor의 denylist에 `api_key`·`apikey`는 있으나 `x-api-key`는 없다. 이 leaf 자신이 진단 맵을 "the one place where a caller can pass arbitrary keys"라고 부르고, `recordDiagnostics`가 redaction을 첫 단계로 두는 이유가 바로 그것이다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
두 판정이 실제 헤더 집합에서 얼마나 벌어지는지 측정하지 않았다. 매칭 방식(정확 일치 대 세그먼트 매칭)의 코드 대조로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-security-f01
|
||||
title: 같은 TLS posture를 두 클래스가 다른 엄격도로 검사한다
|
||||
topic: security-policy-enforcement
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-security-f01
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-security-f01
|
||||
file: ../../../final/evidence/rendered/messaging-security-f01.svg
|
||||
- key: messaging-security-f01-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-security-f01.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-security-f01.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-security.md#L634 이다.
|
||||
module: messaging-security
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 같은 TLS posture를 두 클래스가 다른 엄격도로 검사한다
|
||||
|
||||
MessageSecurityValidator는 hostname 검증을 production && !hostnameVerification일 때만 요구하고, BrokerTlsPolicy는 tlsEnabled && !hostnameVerification일 때 요구한다. 전자는 코드 없는 IllegalArgumentException, 후자는 안정 코드가 붙은 MessagingConfigurationException을 던진다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **배선된 게이트는 자기 leaf 레인에서 검증한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **같은 술어가 두 타입에 있으면 하나가 다른 하나를 부른다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **가변 필드로 상태 전이를 표현하면 가시성을 함께 정한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **맵 갱신 함수 안에서 I/O를 하면 그 지연이 락 범위가 된다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
MessageSecurityValidator는 hostname 검증을 production && !hostnameVerification일 때만 요구하고, BrokerTlsPolicy는 tlsEnabled && !hostnameVerification일 때 요구한다.
|
||||
|
||||
전자는 코드 없는 IllegalArgumentException, 후자는 안정 코드가 붙은 MessagingConfigurationException을 던진다.
|
||||
|
||||
## 결론
|
||||
|
||||
둘 다 같은 BrokerSecurityProfile을 받고, 후자만 어댑터에서 실제로 호출된다.
|
||||
|
||||
비운영에서 TLS를 켜고 hostname 검증을 끈 구성을 두 검사가 다르게 판정한다.
|
||||
|
||||
그리고 이 leaf 자신의 javadoc이 그 구성을 "looks encrypted in every dashboard while accepting any certificate a man in the middle presents"라고 부른다 — 즉 더 느슨한 쪽이 그 위험을 통과시킨다.
|
||||
|
||||
실패 형태도 달라서 운영자가 두 어휘를 알아야 한다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : MessageSecurityValidator 참조 7건 검색과 두 클래스의 검사 조건·예외 타입 대조
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-security.md#L634 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`MessageSecurityValidator`는 hostname 검증을 `production && !hostnameVerification`일 때만 요구하고, `BrokerTlsPolicy`는 `tlsEnabled && !hostnameVerification`일 때 요구한다.
|
||||
|
||||
## 두 검사의 차이
|
||||
|
||||
:::evidence key="messaging-security-f01-diagram" alt="검증기 쪽에 production 일 때만과 코드 없는 예외가 빗금으로 놓이고 정책 쪽에 tls 켜지면 언제나와 안정 코드 예외가 놓인다" caption="두 검사의 차이" zoom="false"
|
||||
:::
|
||||
|
||||
전자는 코드 없는 `IllegalArgumentException`, 후자는 안정 코드가 붙은 `MessagingConfigurationException`을 던진다.
|
||||
|
||||
## MessageSecurityValidator 참조 위치
|
||||
|
||||
:::evidence key="messaging-security-f01" alt="코드베이스에서 MessageSecurityValidator 를 검색한 출력 7줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="MessageSecurityValidator 코드베이스 검색 — 7줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 느슨한 쪽이 위험을 통과시킨다
|
||||
|
||||
둘 다 같은 `BrokerSecurityProfile`을 받고, 후자만 어댑터에서 실제로 호출된다. 비운영에서 TLS를 켜고 hostname 검증을 끈 구성을 두 검사가 다르게 판정한다. 이 leaf 자신의 javadoc이 그 구성을 "looks encrypted in every dashboard while accepting any certificate a man in the middle presents"라고 부른다. 실패 형태도 달라서 운영자가 두 어휘를 알아야 한다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
validate 가 실제로 호출되는지 확인하지 못했다. starter 가 bean 을 만들고 같은 파일에서 직접 부를 가능성이 있으며, 그 leaf 가 답한다.
|
||||
|
||||
<!-- body:end -->
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: messaging-security-f02
|
||||
title: 권한 거부가 AUTHORIZATION이 아니라 CONFIGURATION으로 기록된다
|
||||
topic: security-policy-enforcement
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: case:messaging-security-f02
|
||||
evidenceCapturedOn: 2026-09-01
|
||||
assets:
|
||||
- key: messaging-security-f02
|
||||
file: ../../../final/evidence/rendered/messaging-security-f02.svg
|
||||
- key: messaging-security-f02-diagram
|
||||
file: ../../../final/assets/diagrams/messaging-security-f02.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/messaging-security-f02.txt
|
||||
source:
|
||||
- 원본 분석 절은 analysis/messaging/messaging-security.md#L643 이다.
|
||||
module: messaging-security
|
||||
priority: P2
|
||||
---
|
||||
|
||||
# 권한 거부가 AUTHORIZATION이 아니라 CONFIGURATION으로 기록된다
|
||||
|
||||
DestinationAccessValidator.requirePublish는 MessageAuthorizationException("DESTINATION_PUBLISH_DENIED")을 던지고 그 카테고리는 AUTHORIZATION이다. 소비자가 0이다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **배선된 게이트는 자기 leaf 레인에서 검증한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **같은 술어가 두 타입에 있으면 하나가 다른 하나를 부른다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **가변 필드로 상태 전이를 표현하면 가시성을 함께 정한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **맵 갱신 함수 안에서 I/O를 하면 그 지연이 락 범위가 된다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 문제
|
||||
|
||||
DestinationAccessValidator.requirePublish는 MessageAuthorizationException("DESTINATION_PUBLISH_DENIED")을 던지고 그 카테고리는 AUTHORIZATION이다.
|
||||
|
||||
소비자가 0이다.
|
||||
|
||||
## 결론
|
||||
|
||||
실제 발행 경로는 access.mayPublish를 직접 묻고 rejected("PUBLISH_FORBIDDEN", ...)을 반환하는데, rejected(...)는 FailureCategory.CONFIGURATION을 붙인다.
|
||||
|
||||
FailureCategory는 "stable classification a retry engine, DLQ router, and dashboard all agree on"이다(messaging-core-api §4.12).
|
||||
|
||||
권한 거부가 구성 오류로 분류되면 보안 대시보드가 그것을 보지 못하고, 구성 오류 알림이 권한 거부로 오염된다.
|
||||
|
||||
그리고 AUTHORIZATION 카테고리를 쓰는 유일한 코드가 미사용 클래스에 있다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
OpenJDK : 21.0.12 java -version 으로 확인
|
||||
Gradle : 9.0.0 src/gradle/wrapper/gradle-wrapper.properties 의 distributionUrl 로 확인
|
||||
확인 방식 : DestinationAccessValidator 참조 2건 검색과 실제 발행 경로가 붙이는 범주 확인
|
||||
소스 수정 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
원문은 analysis/messaging/messaging-security.md#L643 에 있다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
|
||||
`DestinationAccessValidator.requirePublish`는 `MessageAuthorizationException("DESTINATION_PUBLISH_DENIED")`을 던지고 그 카테고리는 `AUTHORIZATION`이다. 소비자가 0이다.
|
||||
|
||||
## 기록되는 범주
|
||||
|
||||
:::evidence key="messaging-security-f02-diagram" alt="CONFIGURATION 만 실행되는 분류 안에 놓이고 AUTHORIZATION 이 바깥에 빗금으로 놓인다" caption="기록되는 범주" zoom="false"
|
||||
:::
|
||||
|
||||
실제 발행 경로는 `access.mayPublish`를 직접 묻고 `rejected("PUBLISH_FORBIDDEN", ...)`을 반환하는데, `rejected(...)`는 `FailureCategory.CONFIGURATION`을 붙인다.
|
||||
|
||||
## DestinationAccessValidator 참조 위치
|
||||
|
||||
:::evidence key="messaging-security-f02" alt="코드베이스에서 DestinationAccessValidator 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="DestinationAccessValidator 코드베이스 검색 — 2줄 · exit 0" zoom="true"
|
||||
:::
|
||||
|
||||
## 세 소비자의 합의가 어긋난다
|
||||
|
||||
`FailureCategory`는 "stable classification a retry engine, DLQ router, and dashboard all agree on"이다(`messaging-core-api` §4.12). 권한 거부가 구성 오류로 분류되면 보안 대시보드가 그것을 보지 못하고, 구성 오류 알림이 권한 거부로 오염된다. 그리고 `AUTHORIZATION` 카테고리를 쓰는 유일한 코드가 미사용 클래스에 있다.
|
||||
|
||||
## 확인하지 못한 것
|
||||
|
||||
권한 거부를 실제로 발생시켜 대시보드에 어떤 범주로 집계되는지 관측하지 않았다. 두 경로가 붙이는 범주의 코드 대조로 판정했다.
|
||||
|
||||
<!-- body:end -->
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
---
|
||||
kind: QUESTION
|
||||
slug: messaging-security-f03
|
||||
title: ACL 매니페스트 전체가 쓰이지 않는다
|
||||
topic: security-policy-enforcement
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: open-question:messaging-security-f03
|
||||
questionStatus: OPEN
|
||||
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
|
||||
source:
|
||||
- analysis/messaging/messaging-security.md#L652
|
||||
---
|
||||
|
||||
# ACL 매니페스트 전체가 쓰이지 않는다
|
||||
|
||||
브로커가 producer 자격증명에 파괴적 권한을 준 경우를 아무도 보지 않는다. 그것을 보라고 만든 매니페스트가 소비자 0 이다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **배선된 게이트는 자기 leaf 레인에서 검증한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **같은 술어가 두 타입에 있으면 하나가 다른 하나를 부른다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **가변 필드로 상태 전이를 표현하면 가시성을 함께 정한다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
- **맵 갱신 함수 안에서 I/O를 하면 그 지연이 락 범위가 된다**
|
||||
같은 분석 리프에서 끌어낸 규칙이다.
|
||||
|
||||
## 사실
|
||||
|
||||
BrokerAclManifest 의 세 메서드(requireApplicationRuntime · undeclared · missing)와 두 enum 이 소비자 0 이다. git grep -l 'BrokerAclManifest' -- src ':!src/messaging/messaging-security' 가 아무것도 돌려주지 않는다.
|
||||
|
||||
javadoc 은 "The manifest is what the platform checks itself against at startup" 이라고 적는다.
|
||||
|
||||
"애플리케이션 런타임은 파괴적 권한을 갖지 않는다" 가 이 leaf 의 핵심 원칙 중 하나인데, MessageSecurityValidator 는 admin 자격증명의 부재만 검사한다.
|
||||
|
||||
## 미지수
|
||||
|
||||
브로커 ACL 을 읽는 경로가 있는가. 그 답은 messaging-admin-api 가 갖는다. 읽을 수 없다면 이 매니페스트는 자기 점검이 아니라 선언에 그친다.
|
||||
|
||||
## 선택지
|
||||
|
||||
startup 검사에 배선한다
|
||||
선언된 권한과 실제 권한의 차이가 부팅 시점에 드러난다.
|
||||
|
||||
읽기 경로가 없음을 javadoc 에 적는다
|
||||
"platform checks itself" 라는 문장이 현재 상태와 맞아진다.
|
||||
|
||||
## 다음 검증
|
||||
|
||||
messaging-admin-api 에서 브로커 ACL 조회 경로의 존재를 확인한다. 그 leaf 가 답을 소유한다.
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
---
|
||||
kind: REFERENCE
|
||||
slug: messaging-security-f06
|
||||
title: 맵 갱신 함수 안에서 I/O를 하면 그 지연이 락 범위가 된다
|
||||
topic: security-policy-enforcement
|
||||
project: clean-architecture-backend-template
|
||||
status: 게시 전
|
||||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||||
rootTreeNode: reference:messaging-security-f06
|
||||
verifiedOn: # 이 기록은 이번 회차에 실행 확인을 하지 않았다
|
||||
source:
|
||||
- analysis/messaging/messaging-security.md#L679
|
||||
---
|
||||
|
||||
# 맵 갱신 함수 안에서 I/O를 하면 그 지연이 락 범위가 된다
|
||||
|
||||
## 관계
|
||||
|
||||
- **종료 시 자격증명 소거가 호출되지 않는다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
- **같은 TLS posture를 두 클래스가 다른 엄격도로 검사한다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
- **권한 거부가 `AUTHORIZATION`이 아니라 `CONFIGURATION`으로 기록된다**
|
||||
이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.
|
||||
|
||||
## 목적
|
||||
|
||||
single-flight 를 얻은 대가로 같은 credential id 를 요청하는 스레드가 저장소 왕복 동안 막힌다. 그것은 의도이고 옳다. 의도가 아닌 것은 ConcurrentHashMap 의 bin 을 공유하는 다른 credential id 까지 함께 막히는 것, 그리고 저장소 지연이 타임아웃 없이 발행 경로로 전파되는 것이다.
|
||||
|
||||
## 규칙
|
||||
|
||||
1. 갱신 함수 안에서 외부 호출이 일어나는지 본다
|
||||
resolve 가 resolved.compute(credentialId, (key, existing) -> { ... provider.resolve(key) ... }) 형태다. CredentialProvider.resolve 는 외부 비밀 저장소를 호출할 수 있는 port 다.
|
||||
|
||||
2. 락 범위가 키 단위가 아니라 bin 단위임을 계산에 넣는다
|
||||
해시가 충돌하는 다른 키의 요청도 같은 대기에 들어간다.
|
||||
|
||||
3. 구조를 유지한다면 시간 계약을 port 에 적는다
|
||||
CredentialProvider javadoc 에 "구현은 유한 시간 안에 반환해야 한다" 를 명시한다.
|
||||
|
||||
## 적용 조건
|
||||
|
||||
compute · computeIfAbsent · merge 의 함수 인자 안에서 port 를 호출하는 모든 자리.
|
||||
|
||||
## 예외
|
||||
|
||||
호출 대상이 같은 프로세스 안의 순수 계산이면 이 규칙의 대상이 아니다. 여기서는 port 뒤에 외부 저장소가 올 수 있다.
|
||||
|
||||
## 예시
|
||||
|
||||
CredentialRuntimeRegistry.java:71-86 의 람다 본문. 확인 방법은 provider.resolve 호출 위치가 그 람다 안임을 보는 것이다.
|
||||
|
||||
Reference in New Issue
Block a user