--- title: branch / feature-webhook-outbound-contract source_type: branch-note status: raw branch: feature-webhook-outbound-contract parent_branch: governing_docs: [wiki/projects/ca-tmpl/data-layer-persistence-cache-outbound] related_projects: [ca-skeleton] tags: [branch, ca-skeleton, security, observability, messaging, event-schema, retry-policy] created: 2026-05-31 last_reviewed: 2026-06-29 target_merge: status_label: in-progress id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-044 kind: project-work-item project: ca-skeleton-operational-contract work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-044 inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-RESILIENCE-001@1] refines: [] overrides: [] depends_on: [] contract_packet: 1 contract_packet_sha256: b51881dc440e5170e65a78716a673c73ecce8c17900e7ee80fd4c9ba5f778a67 --- # branch: feature-webhook-outbound-contract > Layer: `raw/branch-notes/` — outbound webhook (서버 → 외부 consumer) 발송의 signature/replay/retry/observability/security 계약을 정의합니다. > > **범위 정합 (2026-06-29 ground-truth 대조)**: outbound HTTP 클라이언트의 공통 factory (`OutboundHttpRestClientFactory.java`) 및 설정 객체 (`OutboundHttpSettings.java`)는 `adapter-outbound` 모듈 내에 이미 구현되어 있으며 (Phase C2), 본 branch는 Webhook 발송 특유의 보안 및 신뢰성 정책을 얹기 위해 (a) Egress Proxy 설정 추가, (b) Redirect 강제 차단 설정, (c) HMAC-SHA256 서명 계산 모듈 및 (d) Full Jitter 재시도 백오프를 주입하는 구체적 구현 사양을 규정합니다. ## 부모 (필수) - **Project 의 직접 자식 branch** (`parent_branch:` 비어있음): [[raw/project-notes/ca-skeleton-operational-contract]] 만 명시 ### 형제 branch (cross-cite) - [[raw/branch-notes/feature-api-contract-baseline]] - [[raw/branch-notes/feature-outbound-http-client-baseline]] - [[raw/branch-notes/feature-rate-limit-idempotency-contract]] - [[raw/branch-notes/feature-background-job-async-contract]] - [[raw/branch-notes/feature-security-operational-baseline]] - [[raw/branch-notes/feature-domain-event-outbox-contract]] ## 브랜치 계약 패킷 - **생성 시 프로젝트 개정**: `1` - **패킷 스키마**: `contract_packet: 1` - **완료 조건**: signature·replay·retry·observability contract test가 통과한다 ### 상속한 프로젝트 결정 | Decision Ref | Project Summary | Branch Application | Source | |---|---|---|---| | `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-RESILIENCE-001@1` | Resilience4j가 default이며 Spring Retry는 simple blocking retry에만 허용한다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] | ### 브랜치 지역 결정 > 기존 branch-local 결정은 아래 `## Decision Evidence Map / 결정-근거 매핑`의 D-row가 소유하며 이 packet에서 복제하지 않는다. | Decision ID | Decision | Relation | Supporting Claims | Status | |---|---|---|---|---| ### 선언한 예외 | Override ID | Overrides | Reason | Approval | Status | |---|---|---|---|---| ## 목표 서버가 외부 consumer 에게 webhook 을 발송할 때 다음을 *임의 결정 없이* 일관되게 제공해야 합니다: 1. **Signature 검증** — payload 변조 감지 (consumer 가 발송 서버를 인증) 2. **Replay protection** — 동일 webhook 의 중복 수신을 consumer 가 감지/거부할 수 있는 식별자 3. **Retry semantics** — consumer 의 일시 장애 시 재발송 정책 (간격 / 횟수 / DLQ) 4. **Delivery observability** — 발송 시도/성공/실패의 로그/메트릭/runbook 5. **Endpoint registration / management** — consumer 의 webhook URL 등록·검증·rotation 절차 6. **Payload contract** — webhook body 의 envelope shape (inbound API envelope 와 다른가? versioning?) 7. **SSRF Defence** — 외부 사용자가 입력한 엔드포인트 URL 호출 시 내부망 자원 보호 - 이슈: - PR: ## 범위 ### 포함 범위 - webhook payload signature scheme (HMAC algorithm + header name + timestamp inclusion) - replay protection identifier (`X-Webhook-Id` UUID + 5분 skew tolerance) - consumer endpoint registration 절차 (https 강제, Smokescreen egress proxy 활용 SSRF 방어, redirects 차단) - retry policy (exponential backoff + Full Jitter, 최대 5회 시도 후 DLQ) - delivery status state machine 정의 (PENDING / SENT / DELIVERED / FAILED / RETRYING / DEAD_LETTERED) - webhook event versioning 정책 수립 (header `X-Webhook-Version` 지정) - webhook payload envelope shape 정의 (event_type, event_id, timestamp, data 구조) - observability 메트릭 및 로그 계약 수립 (`webhook.delivery.requests`, `webhook.dlq.size`) - consumer timeout 정책 결정 (최대 5초 커넥션/응답 제한) ### 제외 범위 - inbound webhook 수신 (별도 endpoint 의 consumer 측 처리 — [[raw/branch-notes/feature-rate-limit-idempotency-contract]] 영역, `Idempotency-Key` 적용) - webhook 의 GraphQL subscription / Server-Sent Events 대체 — 별도 branch [[raw/branch-notes/feature-streaming-response-contract]] - consumer 측 SDK 자동 생성 — out of skeleton scope - domain event → webhook 변환 매핑 자체 — [[raw/branch-notes/feature-domain-event-outbox-contract]] SSOT - payload encryption (TLS 외) — confidential payload 영역, 별도 branch (예: end-to-end encryption requirements) ## 근거 (필수, 최소 1개+) | Source | 정당화하는 결정 | |---|---| | [[raw/official-docs/stripe-webhook-signature]] | D1, D2 — HMAC-SHA256 signature scheme 및 replay protection 5분 window 설계 | | [[raw/official-docs/github-webhook-signature]] | D1 — X-Hub-Signature-256 헤더 명명 및 raw body HMAC 검증 설계 | | [[raw/official-docs/svix-webhook-best-practices]] | D1, D2 — timestamp + message ID + body를 마침표(.)로 결합하는 서명 payload 포맷 | | [[raw/official-docs/rfc9421-http-message-signatures]] | D1 대안 — IETF HTTP Message Signatures 표준 대비 단순 vendor HMAC의 한계 비교 | | [[raw/official-docs/aws-builders-retry-jitter]] | D3 — exponential backoff와 Full Jitter 조합을 통한 재시도 폭풍 방지 설계 | | [[raw/official-docs/owasp-ssrf-prevention]] | D4 — redirect 비활성화 및 egress proxy(Smokescreen) 활용을 통한 SSRF 방어 | ## TODO - [ ] D1: HMAC-SHA256 signature scheme helper (`WebhookSignatureCalculator`) 구현 — 등급: `planned` - [ ] D2: client call 시 Replay protection window validation 및 타임스탬프 계산 바인딩 — 등급: `planned` - [ ] D3: Full Jitter Exponential Backoff calculator (`WebhookRetryBackoffCalculator`) 구현 — 등급: `planned` - [ ] D4: OutboundHttpRestClientFactory 내 Egress Proxy 및 Redirects NEVER 설정 수정 — 등급: `planned` - [ ] Registry Updates (`error-codes.yaml`, `env-keys.yaml`, `headers.yaml`, `metrics.yaml` 업데이트) — 등급: `planned` ## 진행 중 메모 - `OutboundHttpRestClientFactory`의 `HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NEVER)` 설정을 명시해 JDK 기본값 의존을 줄이고, 리다이렉트 차단 계약을 테스트로 고정해야 한다. - `OutboundHttpSettings`에서 `app.outbound.http.egress-proxy` 설정을 Fail-fast 생성자로 검증하도록 조치할 예정이다. ## 결정 사항 - 2026-06-29: HMAC-SHA256 서명 스키마 결정 / 이유: payload 변조 방지 및 consumer 수신 신뢰성 확보 / 검토한 대안: IETF HTTP Message Signatures (복잡하여 기각) / 근거: [[raw/official-docs/stripe-webhook-signature]] - 2026-06-29: 타임스탬프 및 Unique Message ID 기반 Replay 방지 결정 / 이유: replay attack 방지 / 검토한 대안: UUID 단독 사용 (stateful 중복 체크 비용 증가로 기각) / 근거: [[raw/official-docs/svix-webhook-best-practices]] - 2026-06-29: Full Jitter 백오프 재시도 및 DLQ 적용 결정 / 이유: retry storms 방지 및 consumer 부하 분산 / 검토한 대안: 단순 선형 재시도 (재장애 유발 위험으로 기각) / 근거: [[raw/official-docs/aws-builders-retry-jitter]] - 2026-06-29: Egress Proxy 라우팅 및 Redirect 차단 결정 / 이유: 내부 IP 노출 및 SSRF 우회 경로 축소 / 검토한 대안: Application level DNS lookup 검증 (DNS rebinding 취약성으로 기각) / 근거: [[raw/official-docs/owasp-ssrf-prevention]] ## 결정-근거 매핑 | Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk | |---|---|---|---|---|---| | D1 | HMAC-SHA256 서명 스키마 (`X-Webhook-Signature: t=...,v1=...`) | 일반 B2B/B2C webhook 아웃바운드 발송에 기본 적용 | `raw/official-docs/stripe-webhook-signature.md#STRIPE-WEBHOOK-C3`, `raw/official-docs/stripe-webhook-signature.md#STRIPE-WEBHOOK-C4`, `raw/official-docs/github-webhook-signature.md#GITHUB-WEBHOOK-C3`, `raw/official-docs/svix-webhook-best-practices.md#SVIX-WEBHOOK-C2` | `official-vendor-doc + project-local-convention` | `X-Webhook-Signature` 헤더명과 Hex 인코딩은 프로젝트 로컬 convention 이므로 consumer 문서/샘플과 동기화 필요 | | D2 | Replay protection & Message ID | replay attack 및 수신 멱등성 보장이 필수적인 금융/결제/주요 상태 동기화 webhook | `raw/official-docs/stripe-webhook-signature.md#STRIPE-WEBHOOK-C7`, `raw/official-docs/svix-webhook-best-practices.md#SVIX-WEBHOOK-C2` | `official-vendor-doc` | 송수신 시스템 간 clock skew 오차 (5분 초과 시 실패) | | D3 | Retry backoff with Full Jitter | 아웃바운드 비동기 발송의 일시적 장애 복원력이 필요할 때 기본 적용 | `raw/official-docs/aws-builders-retry-jitter.md#AWS-JITTER-C4`, `raw/official-docs/aws-builders-retry-jitter.md#AWS-JITTER-C7` | `official-vendor-doc` | 재시도 중 지연 시간 증가로 인한 즉각성 저하 | | D4 | SSRF 방어 및 리다이렉트 차단 | 외부 사용자가 등록하는 임의의 URL 엔드포인트 호출 시 기본 적용 | `raw/official-docs/owasp-ssrf-prevention.md#OWASP-SSRF-C3`, `raw/official-docs/owasp-ssrf-prevention.md#OWASP-SSRF-C4`, `raw/official-docs/owasp-ssrf-prevention.md#OWASP-SSRF-C2` | `official-standard` | egress proxy 추가 인프라 비용 및 단일 장애점(SPOF) 위험 | ## 구현 가이드 > 본 구현 가이드는 `adapter-outbound` 모듈 내 HTTP 클라이언트 팩토리와 설정 파일에 대한 **구체적인 수정 방향과 설계 규칙**을 정의합니다. (R1, R2, R3, R4 준수) ### 1. HTTP Client 및 Egress Proxy 설정 수정 (D4, `OWASP-SSRF-C2`, `C3`) - **수정 대상 파일**: - `src/adapter-outbound/src/main/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpSettings.java` - `src/adapter-outbound/src/main/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpRestClientFactory.java` - **UNSUPPORTED_IMPL_DECISION**: - `java.net.http.HttpClient`를 빌드할 때, `app.outbound.http` 설정 하위에 `egress-proxy` 설정을 결합하여 ProxySelector를 직접 바인딩하도록 설계함 / trade-off: Spring Cloud Gateway 등의 전역 프록시 설정을 타지 않고, 외부 아웃바운드 템플릿용 RestClient만 격리하여 프록시를 태움으로써 내부 통신(Kafka, DB 등)이 프록시 영향으로 단절되는 것을 방지함. - `followRedirects(HttpClient.Redirect.NEVER)`를 명시적으로 호출함 / trade-off: `java.net.http.HttpClient` 기본값도 `Redirect.NEVER`로 확인되어 보안 요구에 부합하지만, 코드 리뷰와 회귀 테스트에서 redirect 차단 계약이 드러나도록 명시성을 선택함. - **수정 사양**: - `OutboundHttpSettings` 레코드에 `boolean egressProxyEnabled`, `String egressProxyHost`, `Integer egressProxyPort` 필드를 추가하고, compact constructor에서 `egressProxyEnabled`가 `true`일 때 host 및 port의 null/blank/범위 초과 여부를 Fail-Fast로 검증함. - `OutboundHttpRestClientFactory.create` 메서드를 다음과 같이 리다이렉트 차단 및 프록시 주입이 가능하도록 수정함: ```java // dev.caskeleton.adapter.outbound.httpclient.OutboundHttpRestClientFactory.java static Clients create(String dependencyName, String baseUrl, OutboundHttpSettings settings) { HttpClient.Builder builder = HttpClient.newBuilder() .connectTimeout(settings.connectTimeout()) .followRedirects(HttpClient.Redirect.NEVER); // D4: Redirects disabled // D4: Route all outbound requests through Smokescreen Egress Proxy if enabled if (settings.egressProxyEnabled()) { builder.proxy(ProxySelector.of( new InetSocketAddress(settings.egressProxyHost(), settings.egressProxyPort()) )); } HttpClient httpClient = builder.build(); JdkClientHttpRequestFactory requestFactory = new JdkClientHttpRequestFactory(httpClient); requestFactory.setReadTimeout(settings.readTimeout()); // rest client 빌드 생략... } ``` ### 2. Webhook 서명 생성기 구현 (D1, D2, `STRIPE-WEBHOOK-C4`, `SVIX-WEBHOOK-C2`) - **신규 추가 클래스**: - `dev.caskeleton.adapter.outbound.httpclient.webhook.WebhookSignatureCalculator` (application layer 또는 outbound helper) - **UNSUPPORTED_IMPL_DECISION**: - 서명 대상 payload 조립 시 JSON Body of serialization 형태 변형으로 인한 서명 깨짐을 막기 위해, **반드시 RestClient에서 송신하기 직전의 raw byte array를 그대로 활용**하도록 서명 계산 유틸을 바이트 단위로 설계함. - 서명 헤더명을 `X-Webhook-Signature`로 정하고, 서명 결과를 Hex 문자열로 인코딩함 / trade-off: Stripe/GitHub/Svix 문서는 HMAC-SHA256과 raw payload 기반 서명을 뒷받침하지만, Svix는 Base64 인코딩을 사용하므로 Hex vs Base64 및 자체 헤더명은 프로젝트 로컬 convention 으로 문서화하고 consumer 검증 샘플을 함께 제공해야 함. - **서명 조립 알고리즘**: - `SignaturePayload (bytes) = (X-Webhook-Id + "." + X-Webhook-Timestamp + ".").getBytes(StandardCharsets.UTF_8) + rawBodyBytes` - 이 페이로드를 shared secret key(HMAC-SHA256)로 해싱하고, 결과값을 프로젝트 로컬 convention 인 16진수(Hexadecimal) 문자열로 변환하여 헤더에 바인딩함. - 서명 헤더 구조: `X-Webhook-Signature: t=1672531199,v1=a1b2c3d4...` ### 3. Full Jitter 백오프 계산식 구현 (D3, `AWS-JITTER-C4`) - **신규 추가 클래스**: - `dev.caskeleton.adapter.outbound.httpclient.webhook.WebhookRetryBackoffCalculator` - **백오프 수식**: - $Interval = \text{random}(0, \min(\text{cap}, \text{base} \times 2^{\text{attempt}}))$ - `base` = 10,000ms (10초), `cap` = 3,600,000ms (1시간), `maxAttempts` = 5 - Java 구현 예시: ```java public static long calculateBackoff(int attempt, long baseMs, long capMs) { long temp = Math.min(capMs, baseMs * (1L << attempt)); return ThreadLocalRandom.current().nextLong(0, temp); } ``` ## 엣지·실패·의존 - **실패·엣지 경로**: - **Egress Proxy 장애 (SPOF)**: Smokescreen 프록시가 다운되는 경우 모든 외부 웹훅 발송이 즉시 차단됨. 이 경우 retryable 에러(`WEBHOOK_DELIVERY_FAILED`)로 로깅 및 메트릭 기록을 남겨 재시도 큐에 보관해야 함. - **Redirect 우회 시도**: 수신 서버가 정상적인 퍼블릭 IP를 제공한 후, HTTP 응답 시 `302 Found` 등의 리다이렉션을 반환하여 내부 `http://169.254.169.254`로 우회를 유도할 때, HTTP 클라이언트가 리다이렉션 추적을 금지(`Redirect.NEVER`)했으므로 302 응답을 그대로 받아 `WEBHOOK_REDIRECT_BLOCKED` 에러로 격리하고 전송을 영구 중단함. - **Clock Skew 엣지**: 송신 서버와 수신 서버의 NTP 동기화가 깨져 시각 차이가 5분을 초과하는 경우 서명 검증은 통과하나 타임스탬프 스큐 검증에서 거절당함. 이를 모니터링하기 위해 `X-Webhook-Timestamp` 값이 수신 측 시간 대비 300초 이상 벗어난 경우의 예외 처리를 디버깅할 수 있도록 로깅해야 함. - **다른 계약 의존**: - [[raw/branch-notes/feature-background-job-async-contract]] 의 `D3`(DLQ 및 비동기 스케줄러 계약)에 의존. - [[raw/branch-notes/feature-security-operational-baseline]] 의 `D4`(서명 키 로테이션 및 복수 시크릿 유예 기간)에 의존. ## 검증해야 할 주장 | Claim | Why uncertain | How to verify | Status | |---|---|---|---| | HttpClient의 `followRedirects(Redirect.NEVER)`가 실제로 3xx 리다이렉션을 따라가지 않고, 3xx 응답을 `WEBHOOK_REDIRECT_BLOCKED`로 매핑할 수 있는가 | JDK 기본값은 `Redirect.NEVER`로 확인됐지만, RestClient/JdkClientHttpRequestFactory 조합에서 응답 처리 경로를 프로젝트 테스트로 고정해야 함 | Testcontainers에 MockWebServer를 띄우고 `301/302 Redirect` 응답을 던져 리다이렉션을 따라가지 않으며 3xx 응답을 차단 에러로 매핑하는지 JUnit 테스트로 검증 | `planned` | | Smokescreen Egress Proxy가 사설 IP 대역 호출 시도를 정책대로 차단하고 차단 응답을 반환하는가 | 프록시 룰셋이 잘못 설정되어 우회 경로가 존재할 위험이 있음 | 로컬 docker-compose에 Smokescreen을 띄우고 `http://10.0.0.1`로의 웹훅 발송이 프록시에 의해 차단됨을 확인 | `planned` | | Full Jitter Exponential Backoff 난수 분포가 편향 없이 고르게 분포하는가 | Java의 `ThreadLocalRandom` 사용 시 특정 스레드 경쟁 조건에서 Jitter가 편향되어 스파이크 부하를 일으킬 수 있음 | 시뮬레이션을 통해 1,000회 재시도 대기시간의 표준 편차 및 분포 균일성을 검증 | `planned` | | shared secret key rotation 시 헤더에 다중 서명이 들어올 때 수신 측이 순회하며 성공적으로 하나라도 매칭하는가 | 다중 서명 파싱 및 서명 목록 추출 파서가 예외를 던질 위험이 있음 | 두 개 이상의 active secret을 임의로 생성하고 파싱 로직을 통과하는지 검증 | `planned` | ## 관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때) > 2026-06-29 보정: `governing_docs`는 현재 존재하는 outbound HTTP canonical인 `wiki/projects/ca-tmpl/data-layer-persistence-cache-outbound`를 가리킨다. 아래 표는 webhook outbound 세부 관심사 초안이며, `/coverage feature-webhook-outbound-contract` 재실행으로 canonical 요구사항 대비 covered/delegated/missing 판정을 갱신해야 한다. | 관심사 | 상태 | owner | 심각도 | 근거 | |--------|------|-------|--------|------| | C1: HMAC-SHA256 서명 알고리즘 설계 및 구현 | covered-here | — | — | D1 | | C2: Replay attack 방지를 위한 타임스탬프 결합 포맷 | covered-here | — | — | D2 | | C3: Full Jitter Exponential Backoff 공식 | covered-here | — | — | D3 | | C4: Egress Proxy (Smokescreen) 라우팅 주입 | covered-here | — | — | D4 | | C5: HTTP Client Redirect 강제 차단 | covered-here | — | — | D4 | | C6: DB 기반 Key Rotation 24시간 오버랩 윈도우 | delegated | [[raw/branch-notes/feature-security-operational-baseline]] | OK | §엣지·의존 링크 | | C7: 비동기 발송 멱등성 및 DLQ 아키텍처 | delegated | [[raw/branch-notes/feature-background-job-async-contract]] | OK | §엣지·의존 링크 | ## Audit & Findings (2026-06-29 ground-truth 대조) - **FINDING-1 — Outbound HTTP Client 내 Redirect / Proxy 바인딩 코드 부재**: - ca-tmpl 의 `src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/OutboundHttpRestClientFactory.java:21` 을 확인한 결과, 단순히 `HttpClient.newBuilder().connectTimeout(settings.connectTimeout()).build()` 로 HTTP 클라이언트를 생성하고 있음. (2026-07-21 실물 소스에서 재확인. 원래 인용은 repomix 덤프 `ca-tmpl코드내용.xml` 의 병합 행번호 L39733-39735 를 가리켰으나, 덤프는 재생성 시 행번호가 바뀌는 일회성 산출물이라 정본 경로로 교체.) - 리다이렉트 정책은 JDK 기본값(`Redirect.NEVER`)에 의존해도 요구를 만족할 수 있으나, 코드에 명시되어 있지 않아 보안 계약이 리뷰/테스트 표면에 드러나지 않는다. Egress Proxy 설정을 바인딩하는 `builder.proxy(...)` 코드는 누락된 상태임. - 권고: 본 branch note의 **§구현 가이드 1**에 명시된 대로 `OutboundHttpSettings` 및 `OutboundHttpRestClientFactory` 에 Egress Proxy 바인딩을 추가하고, redirect 차단은 명시 설정 + 테스트로 회귀를 방지해야 함. - **FINDING-2 — Webhook 관련 에러 코드 및 레지스트리 설정 부재**: - `docs/registries/error-codes.yaml` 에 webhook 전송 실패, SSRF 차단, 리다이렉트 차단과 관련된 에러 코드가 정의되지 않음. - 권고: 본 branch note의 **§Registry Updates** 에 정의된 신규 YAML 설정을 레지스트리 파일에 통합해야 함. ## Registry Updates (자체 명세) > 본 branch merge 시, `docs/registries/` 하위 파일들에 아래 항목을 반드시 추가/업데이트해야 합니다. ### 1. `docs/registries/error-codes.yaml` ```yaml # ============================================================ # WEBHOOK OUTBOUND (feature-webhook-outbound-contract) # ============================================================ - code: WEBHOOK_DELIVERY_FAILED category: TRANSIENT_DEPENDENCY http_status: 500 retryable: true retry_after_seconds: 10 owner_branch: feature-webhook-outbound-contract owner_layer: infrastructure client_safe_message: "Webhook delivery attempt failed. Retrying..." log_level: WARN runbook_link: "runbook://webhook/delivery-failed" compatibility_impact: none required_test: contract-verification:webhook-retry-policy - code: WEBHOOK_SSRF_BLOCKED category: CONFLICT http_status: 400 retryable: false retry_after_seconds: null owner_branch: feature-webhook-outbound-contract owner_layer: infrastructure client_safe_message: "Webhook target endpoint blocked due to SSRF policy" log_level: ERROR runbook_link: "runbook://webhook/ssrf-blocked" compatibility_impact: none required_test: contract-verification:webhook-ssrf-prevention - code: WEBHOOK_REDIRECT_BLOCKED category: CONFLICT http_status: 400 retryable: false retry_after_seconds: null owner_branch: feature-webhook-outbound-contract owner_layer: infrastructure client_safe_message: "Webhook target redirected. Redirects are forbidden." log_level: ERROR runbook_link: "runbook://webhook/redirect-blocked" compatibility_impact: none required_test: contract-verification:webhook-redirect-blocked ``` ### 2. `docs/registries/env-keys.yaml` ```yaml # === Webhook Egress Proxy (feature-webhook-outbound-contract) === - name: APP_WEBHOOK_EGRESS_PROXY_ENABLED type: boolean default: false allowed_values: [true, false] classification: public-config required: false reload_policy: restart-only owner_branch: feature-webhook-outbound-contract validation: boolean_only compatibility_impact: behavior-change required_test: env-contract:webhook-proxy - name: APP_WEBHOOK_EGRESS_PROXY_HOST type: string default: null allowed_values: null classification: public-config required: false reload_policy: restart-only owner_branch: feature-webhook-outbound-contract validation: non_empty_string compatibility_impact: behavior-change required_test: env-contract:webhook-proxy - name: APP_WEBHOOK_EGRESS_PROXY_PORT type: int default: null allowed_values: null classification: public-config required: false reload_policy: restart-only owner_branch: feature-webhook-outbound-contract validation: port_range_1_65535 compatibility_impact: behavior-change required_test: env-contract:webhook-proxy ``` ### 3. `docs/registries/headers.yaml` ```yaml # === Webhook Outbound Headers (feature-webhook-outbound-contract) === - name: X-Webhook-Signature direction: outbound type: string required: true generated_if_missing: true mdc_key: null envelope_meta_field: null owner_branch: feature-webhook-outbound-contract case_style: kebab compatibility_impact: additive required_test: contract-verification:webhook-headers - name: X-Webhook-Id direction: outbound type: uuid required: true generated_if_missing: true mdc_key: null envelope_meta_field: null owner_branch: feature-webhook-outbound-contract case_style: kebab compatibility_impact: additive required_test: contract-verification:webhook-headers - name: X-Webhook-Timestamp direction: outbound type: numeric-seconds required: true generated_if_missing: true mdc_key: null envelope_meta_field: null owner_branch: feature-webhook-outbound-contract case_style: kebab compatibility_impact: additive required_test: contract-verification:webhook-headers ``` ### 4. `docs/registries/metrics.yaml` ```yaml # === Webhook Outbound Metrics (feature-webhook-outbound-contract) === - name: webhook.delivery.requests type: timer unit: seconds tags: - name: outcome cardinality_limit: 5 allowed_values: [SUCCESS, FAILURE, TIMEOUT, RETRYING, BLOCKED] - name: event_type cardinality_limit: 20 percentiles: [0.5, 0.9, 0.95, 0.99] histogram_buckets: slo_driven alert_severity_thresholds: p1: "error_rate > 5% for 5m" p2: "error_rate > 1% for 10m" owner_branch: feature-webhook-outbound-contract log_field_mapping: [outcome, event_type] compatibility_impact: additive required_test: contract-verification:webhook-metrics - name: webhook.dlq.size type: gauge unit: total tags: - name: event_type cardinality_limit: 20 percentiles: null histogram_buckets: null alert_severity_thresholds: p1: "webhook.dlq.size > 100" p2: "webhook.dlq.size > 10" owner_branch: feature-webhook-outbound-contract log_field_mapping: [event_type] compatibility_impact: additive required_test: contract-verification:webhook-metrics ``` ## 마주친 문제 - 아직 없음. ## 묶음 (이 branch에서 파생된 자료) - [[raw/official-docs/aws-builders-retry-jitter]] - [[raw/official-docs/github-webhook-signature]] - [[raw/official-docs/owasp-ssrf-prevention]] - [[raw/official-docs/rfc9421-http-message-signatures]] - [[raw/official-docs/stripe-webhook-signature]] - [[raw/official-docs/svix-webhook-best-practices]] - [[raw/blog-topics/webhook-full-jitter-dlq-observability-2026-07-02]] - [[raw/blog-topics/webhook-signature-replay-contract-2026-07-02]] - [[raw/blog-topics/webhook-ssrf-egress-proxy-redirect-block-2026-07-02]] ### Sub-branches (세부 작업) - 없음 ### 오류 기록 (이 branch 작업 중 발생) - 없음 ### 면접 준비 (이 작업에서 나올 수 있는 면접 질문) - 없음 ### 강의 (이 작업을 위해 학습한 강의) - 없음 ### job-posting tie-ins (이 작업에서 파생된 글감) - 없음 ## 관련 일일 노트 - `[[raw/daily-notes/2026-06-29]]` ## 완료 후 정리 - PR 링크: - 리뷰 메모: - 머지 결과 / 배포 환경: - **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출): - `actually-implemented` 항목: - `locally-verified` 항목: - `prod-verified` 항목: - **추출하지 않을 항목** (planned / documented-only / abandoned):