Files
clean-architecture-backend-…/docs/messaging/security.md
T
DongHyeonka d646c2f12f feat(messaging): 브로커 중립 메시징 플랫폼 24개 leaf 추가
messaging-superpowers-package 설계서/계획서 기반 구현.
registry를 19 → 43 leaf로 확장하고 src/messaging 아래 24개 leaf를 등록.

- core-api: M1 publish/consume + M2 batch·delayed·pause-resume
- policy/transport-spi: 재시도 결정, DLQ orchestration, admission control, lifecycle
- kafka·rabbit(Stable): contiguous commit, confirm/return 상관, 배치, 보안 설정
- pulsar·nats(Experimental): 기본 비활성, live 인증 없음을 코드로 기록
- outbox/inbox/claim-check: 트랜잭션 결합, lease, 무결성 검증
- admin: plan → approve → execute를 타입으로 강제
- 문서 9종, infra compose 7종, JMH 벤치마크 3종

검증: 아키텍처 게이트 3종 통과, 24개 leaf 전부 check 통과,
messaging 테스트 604개 통과/0 실패.

미완: 계획서가 요구한 실 브로커 IT 40개 중 7개만 작성.
Rabbit 13 / Outbox 6 / Inbox 4 / NATS·Pulsar·Share 5 / testkit 2 /
starter·admin 3, 그리고 TLS·ACL 2개가 남음.
2026-08-14 14:55:38 +09:00

93 lines
3.8 KiB
Markdown

# Messaging 보안
## Credential 분리
producer / consumer / admin은 **서로 다른 credential**이다.
`MessageSecurityValidator`가 startup에서 강제한다.
```text
producer credential == consumer credential → 실패
admin credential == producer|consumer → 실패
production 프로필에 admin credential 존재 → 실패
```
마지막 규칙이 "애플리케이션은 topic을 purge할 수 없다"를 **구조적으로** 만든다.
runtime이 admin 자격 증명을 아예 보유하지 않으므로, 침해된 handler가 상승시킬 대상이 없다.
## Production 필수 조건
- TLS 활성
- TLS hostname verification 활성
- broker authentication 활성
- topology auto-create 비활성
Kafka는 추가로 `enable.idempotence=true`, `acks=all`,
`max.in.flight.requests.per.connection <= 5`, consumer auto-commit 금지.
RabbitMQ는 추가로 publisher confirm, publisher return, `mandatory=true`,
durable work queue의 quorum queue, consumer auto-ack 금지.
## Credential은 값이 아니라 참조다
`BrokerCredentialProfile`의 어떤 variant도 secret을 담지 않는다.
식별자만 보관하고 connect 시점에 `CredentialProvider`로 해석한다.
heap dump나 설정 출력에서 사용 가능한 credential이 나오지 않는다.
`CredentialIds``bearer `, `sk-`, `-----begin`, `eyJ` 같은 접두사를 거부한다.
참조가 들어갈 자리에 secret 자체를 붙여넣는 가장 흔한 사고를 막는다.
## Rotation
`CredentialRotationPlan.isDue()`는 만료 **전에** 참이 된다.
broker가 연결을 거부하기 시작한 시점에는 이미 publish가 실패하고 consumer가 멈춰 있다.
rotation은 세대 교체다. `DefaultMessagingRuntimeRegistry.install()`이 새 세대를 원자적으로
게시하고, 이전 세대는 마지막 lease가 닫힐 때까지 열려 있다가 닫힌다.
진행 중인 publish는 시작한 연결에서 confirm을 받는다.
drain deadline이 이 대기를 제한한다. 없으면 lease 하나가 새면 폐기된 credential이
무기한 열려 있고, rotation이 보안상 무의미해진다.
## Header
금지 header는 application·platform 양쪽에서 거부한다.
```text
Authorization, Proxy-Authorization, Cookie, Set-Cookie,
access_token, refresh_token, api_key, password, client_secret
```
credential이 header에 들어가면 broker storage, DLQ dump, 운영 도구에 남는다.
downstream redaction으로는 되돌릴 수 없다.
예약 header(`msg.*`, `traceparent`, `tracestate`, `baggage`)는 platform만 쓴다.
application이 `msg.id`를 설정할 수 있으면 Inbox 중복 제거와 DLQ 상관관계가 의존하는
logical identity가 호출자 제어가 된다.
## ACL
`DestinationAccessValidator`가 broker ACL **이전에** 검사한다.
broker ACL 거부는 애플리케이션 컨텍스트가 없는 연결 수준 오류로 도착하므로
"어느 모듈이 어디에 publish하려 했는가"가 조사 대상이 된다.
## 관측성 누출
`MessagingRedactor`는 denylist다.
- secret: authorization, cookie, token, password, secret, credential
- per-message identity: messageId, correlationId, causationId, partitionKey, key, offset, deliveryTag, sequence
- payload: payload, body, data
- 예외 상세: exceptionMessage, stackTrace
identity를 지우는 이유는 두 가지다. bounded metric을 message당 하나의 series로 만들고,
support log를 재식별 표면으로 만들기 때문이다.
`CardinalityGuard`는 dimension당 값 개수를 상한한다.
cardinality 사고는 점진적이지 않다. 테스트 10건에서는 멀쩡하고 운영에서 백엔드를 죽인다.
## 감사
`MessagingAuditEvent`는 replay, redrive, offset reset, purge, delete를 기록한다.
subject(운영자 identity), approval ticket, 그리고 redactor를 통과한 details만 담는다.
누가 무엇을 했는지 증명하되 payload의 두 번째 사본이 되지 않는다.