haechi 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SECURITY.md CHANGED
@@ -12,7 +12,13 @@ Only the current `1.x` stable line is considered in scope. From 1.0 the public A
12
12
 
13
13
  ## Reporting
14
14
 
15
- Report suspected vulnerabilities privately to the repository maintainer. Do not include real secrets, production prompts, customer data, or personal information in reports.
15
+ **Preferred channel: GitHub private vulnerability reporting.** Open a private report through the repository's Security Advisories at <https://github.com/raeseoklee/haechi/security/advisories> (the "Report a vulnerability" button). This keeps the report confidential until a fix is coordinated. The machine-readable disclosure metadata is published at [`/.well-known/security.txt`](.well-known/security.txt) (RFC 9116; mirrored at the repo root).
16
+
17
+ Do not include real secrets, production prompts, customer data, or personal information in reports — describe the issue and a minimal, sanitized reproduction instead.
18
+
19
+ **Triage target (best-effort, not an SLA for a pre-1.0/OSS project):** we aim to **acknowledge a report within 3 business days** and to share an initial assessment (accepted / needs-info / out-of-scope) within **10 business days**. A disclosed, in-scope vulnerability is eligible for an in-minor security fix under the `1.x` stability exception (`docs/current/api-stability.md`).
20
+
21
+ A control mapping (OWASP LLM Top 10 2025 / NIST AI RMF) and a structured self-pentest are documented in [`docs/current/security-whitepaper.md`](docs/current/security-whitepaper.md). This is a self-assessment, not an independent audit or certification.
16
22
 
17
23
  ## Security Invariants
18
24
 
package/docs/README.md CHANGED
@@ -21,6 +21,8 @@ English is the primary documentation language. Korean translations are maintaine
21
21
  - `docs/current/configuration.md`: full configuration reference (every key, defaults, validation, presets, common setups)
22
22
  - `docs/current/risk-register-release-gate.md`: release-blocking risks, security/operational risk status, npm release gates (0.3.2 baseline)
23
23
  - `docs/current/threat-model.md`: Haechi trust boundaries, protected assets, key threats and controls
24
+ - `docs/current/security-whitepaper.md`: shipped-control mapping to OWASP LLM Top 10 (2025) + NIST AI RMF, with a structured self-pentest (a self-assessment, not a certification)
25
+ - `docs/current/compliance-mapping.md`: control-to-obligation-category mapping + DSAR/retention operational workflow (a mapping, not a certification)
24
26
  - `docs/current/shared-responsibility.md`: responsibility split between Haechi and users/operators in self-hosted deployments
25
27
  - `docs/current/api-stability.md`: developer preview API stability and migration note criteria
26
28
  - `docs/current/release-process.md`: release preflight, SBOM, npm provenance publish procedure
@@ -0,0 +1,53 @@
1
+ # 컴플라이언스 통제 매핑 & DSAR/Retention 워크플로
2
+
3
+ - 문서 상태: Living document (WS6 — reliability-hardening-track §WS6)
4
+ - 이 문서의 성격: **통제 매핑**이며 컴플라이언스 **인증**이 아닙니다. reliability-hardening-track §5는 인증을 명시적 비목표로 두며, `SECURITY.md` Scope는 이 저장소가 컴플라이언스 인증·법률 의견·보증 보고서가 아님을 밝힙니다. 이 문서는 Haechi 통제를, 운영자가 충족하도록 돕는 *의무 범주*에 매핑합니다 — Haechi를 배포하면 어떤 규제를 준수하게 된다고 주장하지 않습니다.
5
+
6
+ ## 0. 읽는 법
7
+
8
+ 규제 의무(예: "데이터 최소화", "접근 로깅", "정보주체 권리")는 *프로그램* — 사람·프로세스·기술 — 으로 충족됩니다. Haechi는 운영자가 그 프로그램의 LLM/MCP 게이트웨이 경계에 배선하는 하나의 **기술 통제**입니다. 아래에서는 각 의무 **범주**를 이를 지원하는 Haechi 통제에 매핑하며, Haechi가 하는 일과 하지 않는 일의 경계를 기록합니다. 권위 있는 통제 정의는 코드와 `docs/current/threat-model.md`에 있으며, 이 문서는 그것을 매핑할 뿐 재서술하지 않습니다.
9
+
10
+ ## 1. 통제 → 의무 범주 매핑
11
+
12
+ | 의무 범주 | Haechi 통제 | Haechi의 기여 | 운영자가 여전히 소유 |
13
+ |---|---|---|---|
14
+ | **데이터 최소화** | 탐지 + redact/mask/tokenize/encrypt/block 파이프라인(`packages/core`, `packages/filter`, `packages/policy`); privacy profile(`kr-pipa`/`eu-gdpr`/`us-general`) | PII/비밀이 모델·도구·로그에 도달하기 전에 제거하거나 가명화하여 최소 필요 데이터만 하류로 흐르게 합니다. tokenization은 값을 vault에만 보관되는 가역 참조로 대체합니다. | 적법 근거 정책, 지역 profile, 어떤 필드가 실제로 필요한지의 선택. |
15
+ | **접근 로깅 / 감사성** | SHA-256 hash chain + head anchoring을 가진 audit JSONL(`packages/audit`); request별 `correlationId`; PII 없는 이벤트(`FORBIDDEN_KEYS`) | *어떤 범주*가 탐지되었고, *어떤 action*이 집행되었으며, *어떤*(keyed-hashed) identity인지, *언제*인지를 — 민감 값 자체는 저장하지 않고 — 변조 증거와 함께 기록합니다. | append-only/불변 저장 매체, 로그 전송, retention 일정(§3). |
16
+ | **목적 제한 / 접근 통제** | body 읽기 전 auth gate; named policy profile; model allowlist; identity별 rate limit(`packages/proxy`, `packages/auth`) | 누가 게이트웨이를 사용할 수 있고 각 identity가 어떤 모델/연산/쿼터를 받는지를, payload를 읽기 전에 제약합니다. | identity 수명주기, token 발급/폐기 정책, 게이트웨이 너머의 인가 모델. |
17
+ | **보관 제한 / retention** | token-vault retention(`tokenVault.retentionDays`, mutation 시 만료 정리); chain-aware audit rotation/retention 절차(`operations-runbook.md` §6) | bounded token 수명과, hash-chain을 보존하는 문서화된 audit rotation/retention 절차. | 법적 요구에 따른 retention 윈도 설정과 rotation 일정 운영. |
18
+ | **정보주체 권리(열람 / 삭제)** | token-vault reveal 거버넌스(`revealPolicy`) + purge, 둘 다 token id 기준 audit; §2의 DSAR 워크플로 | reveal/purge 프리미티브와 그 거버넌스/audit이 DSAR 대응의 기술적 빌딩 블록입니다(§2 참고). | 각 요청의 법적 접수, 신원 확인, 결정, 기록 보존. |
19
+ | **전송 중 기밀성** | proxy TLS / remote-bind 강화(`proxy.tls` / `proxy.trustForwardedProto`); 기본 loopback(`packages/proxy`) | remote bind는 bearer token + payload를 평문으로 제공할 수 없습니다 — TLS를 종단하거나 검증된 `X-Forwarded-Proto: https` hop 뒤에 있어야 하며, 아니면 기동 시 fail-closed. | 인증서 발급/회전과 네트워크 경계. |
20
+ | **무결성 & 변조 증거** | audit hash chain + anchoring; canonical-AAD 결합 암호화(`packages/crypto`); 강화-전용 정책(`ACTION_STRENGTH`) | 변조 증거 audit, AEAD 결합 ciphertext, 조용히 약화될 수 없는 정책 격자. | 키 보관(프로덕션 KMS/HSM은 주입된 `cryptoProvider`이며 절대 코어 아님)과 사고 대응. |
21
+ | **처리의 안전성 / 복원력** | fail-closed 집행; depth/byte/encoding 가드; readiness(`/__haechi/ready`) + backpressure(`packages/proxy`, `packages/core`) | 인라인 집행과 fail-closed 가용성 통제가 미보호 payload나 unbounded-consumption 사건의 가능성을 줄입니다. | 용량 계획, 모니터링, 더 넓은 보안 프로그램. |
22
+
23
+ ## 2. DSAR / retention 운영 워크플로
24
+
25
+ **정보주체 열람/삭제 요청(DSAR)**은 법적/프로세스 워크플로이며, Haechi는 그것이 귀결되는 기술 연산을 제공합니다. 아래 흐름은 요청을 구체적 Haechi 프리미티브에 매핑합니다. **모든 reveal/purge 연산은 token id 기준으로 audit되며(평문 아님)**, `tokenVault.revealPolicy`로 거버넌스됩니다.
26
+
27
+ ### 2.1 열람 요청 (정보주체가 "내 데이터를 무엇을 보유/처리하는가?"라고 물을 때)
28
+ 1. **위치 파악.** audit 로그로 정보주체에 관련된 이벤트를 찾습니다 — keyed-HMAC `subjectHash`(audit는 원문 subject를 저장하지 않음), `correlationId`, 시간 윈도, 탐지 summary로 매칭합니다. audit는 *어떤 범주*가 처리되고 *어떤 action*이 취해졌는지를 값 없이 알려줍니다.
29
+ 2. **거버넌스될 때에 한해 token 해석.** 값이 **tokenize**되었다면 가역 참조가 token vault에 있습니다. reveal은 `tokenVault.revealPolicy`로 게이트됩니다:
30
+ - `disabled`(기본): reveal 거부. 이것이 프로덕션 안전 자세입니다 — DSAR 열람 응답은 live reveal이 아니라 audit 메타데이터 + 운영자의 upstream 기록으로 구성합니다.
31
+ - `local-dev`: 명시적 로컬 개발 워크플로에서만 reveal 허용(`haechi token-reveal <token> --allow-dev-reveal`). `--allow-dev-reveal`을 프로덕션 DSAR 절차로 **사용하지 마십시오**(`shared-responsibility.md` §2 참고).
32
+ 모든 reveal 결정은 token id 기준으로 audit 로그에 기록됩니다.
33
+ 3. **응답.** 법적/프로세스 채널을 통해 응답합니다. Haechi는 기술 증거를 제공하고, 운영자가 신원 확인과 응답을 소유합니다.
34
+
35
+ ### 2.2 삭제 요청 (정보주체가 "내 데이터를 삭제하라"고 할 때)
36
+ 1. **token 매핑 purge.** `haechi token-purge`가 vault 매핑을 제거해 tokenize된 값을 더 이상 reveal할 수 없게 합니다; 만료된 token도 vault mutation 시 자동 정리됩니다. purge 결정은 token id 기준으로 audit됩니다.
37
+ 2. **retention 윈도 밖의 audit 세그먼트 만료.** `operations-runbook.md` §6에 따라 audit 로그는 세그먼트로 rotation되며, retention은 **세그먼트 전체를 만료**합니다(부분 라인은 hash chain을 깨므로 절대 아님). audit는 의도적으로 **평문 PII를 보유하지 않으므로** — *내용*에 대한 삭제 의무는 대체로 upstream/운영자 저장소에서 충족되며, audit는 keyed-hashed 식별자와 범주 메타데이터만 보유합니다.
38
+ 3. **upstream 복사본 삭제.** 모델 제공자의 로그, 애플리케이션 DB, 백업은 **Haechi 밖**입니다 — 운영자가 자신의 데이터 맵에 따라 삭제해야 합니다.
39
+
40
+ ### 2.3 retention 운영 (상시)
41
+ - **token vault:** `tokenVault.retentionDays` 설정; 만료는 vault mutation 시 정리됩니다.
42
+ - **audit 로그:** `operations-runbook.md` §6의 chain-aware rotation 운영 — 유지보수 경계에서 rotation하고, 각 rotation된 세그먼트 **와 그 anchor**를 retention 윈도 동안 보관해 이력이 여전히 검증되게 한 뒤, 세그먼트 전체를 만료합니다. token-vault retention과 audit retention은 독립이며, audit rotation이 token을 purge하지 않습니다.
43
+
44
+ ## 3. 경계 & 비목표 (정직하게)
45
+ - 이것은 **매핑**이며, 인증이나 법률 자문이 아닙니다. Haechi 배포가 시스템을 "GDPR/PIPA 등 준수"로 만들지 않습니다.
46
+ - Haechi는 **게이트웨이 경계**만 통제합니다. 모델 제공자의 retention, 애플리케이션 저장소, 백업은 운영자 책임입니다(`shared-responsibility.md`).
47
+ - 탐지는 regex + validator(ML 없음)이며 문서화된 제외는 유효합니다(`threat-model.md` §4). DSAR/삭제 프로그램은 Haechi가 어떤 값의 *모든* 인스턴스를 잡았다고 가정해서는 안 됩니다.
48
+
49
+ ## 4. 상호 참조
50
+ - `docs/current/shared-responsibility.md` — Haechi 대 운영자 책임 매트릭스(DSAR/retention 구분이 거기에 명시됨).
51
+ - `docs/current/operations-runbook.md` — §6 chain-aware audit rotation & retention.
52
+ - `docs/current/security-whitepaper.md` — OWASP-LLM / NIST-AI-RMF 통제 매핑 + self-pentest.
53
+ - `docs/current/threat-model.md` — 제외 항목과 수용된 잔여 위험.
@@ -0,0 +1,53 @@
1
+ # Compliance Control Mapping & DSAR/Retention Workflow
2
+
3
+ - Status: Living document (WS6 — reliability-hardening-track §WS6)
4
+ - Nature of this document: a **control mapping**, NOT a compliance **certification**. The reliability-hardening-track §5 lists a certification as an explicit non-goal, and `SECURITY.md` Scope states this repository is not a compliance certification, legal opinion, or assurance report. This document maps Haechi controls to the *obligation categories* they help an operator satisfy — it does not assert that deploying Haechi makes a system compliant with any regulation.
5
+
6
+ ## 0. How to read this
7
+
8
+ A regulatory obligation (e.g. "data minimization", "access logging", "subject rights") is satisfied by a *program* — people, process, and technology. Haechi is one **technical control** an operator wires into that program at the LLM/MCP gateway boundary. Below, each obligation **category** is mapped to the Haechi control(s) that support it, with the boundary of what Haechi does and does not do. The authoritative control definitions are in the code and `docs/current/threat-model.md`; this maps them, it does not restate them.
9
+
10
+ ## 1. Control → obligation-category mapping
11
+
12
+ | Obligation category | Haechi control(s) | What Haechi contributes | Operator still owns |
13
+ |---|---|---|---|
14
+ | **Data minimization** | Detection + redact/mask/tokenize/encrypt/block pipeline (`packages/core`, `packages/filter`, `packages/policy`); privacy profiles (`kr-pipa`/`eu-gdpr`/`us-general`) | Strips or pseudonymizes PII/secrets before they reach the model, tools, or logs — so the minimum necessary data flows downstream. Tokenization replaces a value with a reversible reference held only in the vault. | Choosing the lawful-basis policy, the regional profile, and which fields are truly necessary. |
15
+ | **Access logging / auditability** | Audit JSONL with SHA-256 hash chain + head anchoring (`packages/audit`); per-request `correlationId`; PII-free events (`FORBIDDEN_KEYS`) | A tamper-evident record of *what category* was detected, *what action* was enforced, *which* (keyed-hashed) identity, and *when* — without storing the sensitive value itself. | Append-only/immutable storage media, log shipping, and the retention schedule (see §3). |
16
+ | **Purpose limitation / access control** | Auth gate before body-read; named policy profiles; model allowlist; per-identity rate limit (`packages/proxy`, `packages/auth`) | Constrains who may use the gateway and which models/operations/quotas each identity gets, enforced before any payload is read. | Identity lifecycle, token issuance/revocation policy, and the authorization model beyond the gateway. |
17
+ | **Storage limitation / retention** | Token-vault retention (`tokenVault.retentionDays`, expiry pruned on mutation); chain-aware audit rotation/retention procedure (`operations-runbook.md` §6) | Bounded token lifetime and a documented, hash-chain-preserving rotation/retention procedure for the audit log. | Setting the retention window per legal requirement and operating the rotation schedule. |
18
+ | **Subject rights (access / erasure)** | Token-vault reveal governance (`revealPolicy`) + purge, both audited by token id; the DSAR workflow in §2 | The reveal/purge primitives and their governance/audit are the technical building blocks of a DSAR response (see §2). | The legal intake, identity verification, decision, and recordkeeping of each request. |
19
+ | **Confidentiality in transit** | Proxy TLS / remote-bind hardening (`proxy.tls` / `proxy.trustForwardedProto`); loopback-by-default (`packages/proxy`) | A remote bind cannot serve bearer tokens + payloads in plaintext — it must terminate TLS or sit behind a verified `X-Forwarded-Proto: https` hop, else it fails closed at startup. | Certificate issuance/rotation and the network perimeter. |
20
+ | **Integrity & tamper evidence** | Audit hash chain + anchoring; canonical-AAD-bound encryption (`packages/crypto`); policies-only-get-stronger (`ACTION_STRENGTH`) | Tamper-evident audit, AEAD-bound ciphertext, and a policy lattice that cannot silently weaken. | Key custody (a production KMS/HSM is an injected `cryptoProvider`, never core), and incident response. |
21
+ | **Security of processing / resilience** | Fail-closed enforcement; depth/byte/encoding guards; readiness (`/__haechi/ready`) + backpressure (`packages/proxy`, `packages/core`) | Inline enforcement and fail-closed availability controls reduce the chance of an unprotected payload or an unbounded-consumption event. | Capacity planning, monitoring, and the broader security program. |
22
+
23
+ ## 2. DSAR / retention operational workflow
24
+
25
+ A **Data Subject Access/erasure Request (DSAR)** is a legal/process workflow; Haechi provides the technical operations it bottoms out on. The flow below maps a request to concrete Haechi primitives. **All reveal/purge operations are audited by token id (never plaintext)** and are governed by `tokenVault.revealPolicy`.
26
+
27
+ ### 2.1 Access request (the subject asks "what data of mine do you hold / process?")
28
+ 1. **Locate.** Use the audit log to find the events touching the subject — match on the keyed-HMAC `subjectHash` (the audit never stores a raw subject), the `correlationId`, the time window, and the detection summary. The audit tells you *that* a category was processed and *which action* was taken, without the value.
29
+ 2. **Resolve tokens, if and only if governed.** If a value was **tokenized**, the reversible reference lives in the token vault. Revealing it is gated by `tokenVault.revealPolicy`:
30
+ - `disabled` (the default): reveal is refused. This is the production-safe posture — a DSAR access response is assembled from the audit metadata + the operator's upstream records, not from a live reveal.
31
+ - `local-dev`: reveal is permitted only for explicit local-development workflows (`haechi token-reveal <token> --allow-dev-reveal`). **Do not** use `--allow-dev-reveal` as a production DSAR procedure (see `shared-responsibility.md` §2).
32
+ Every reveal decision is written to the audit log by token id.
33
+ 3. **Respond** through your legal/process channel. Haechi supplies the technical evidence; the operator owns identity verification and the response.
34
+
35
+ ### 2.2 Erasure request (the subject asks "delete my data")
36
+ 1. **Purge the token mapping.** `haechi token-purge` removes the vault mapping so the tokenized value can no longer be revealed; expired tokens are also pruned automatically on vault mutations. The purge decision is audited by token id.
37
+ 2. **Expire the audit segments** that fall outside the retention window. Per `operations-runbook.md` §6, the audit log rotates into segments; retention **expires whole segments** (never partial lines, which would break the hash chain). The audit deliberately holds **no plaintext PII** — so an erasure obligation against the *content* is largely satisfied by the upstream/operator store, while the audit holds only keyed-hashed identifiers and category metadata.
38
+ 3. **Erase upstream copies.** The model provider's logs, your application database, and any backups are **outside Haechi** — the operator must erase those per their own data map.
39
+
40
+ ### 2.3 Retention operation (ongoing)
41
+ - **Token vault:** set `tokenVault.retentionDays`; expiry is pruned on vault mutations.
42
+ - **Audit log:** operate the chain-aware rotation in `operations-runbook.md` §6 — rotate at a maintenance boundary, keep each rotated segment **and its anchor** for the retention window so the history still verifies, then expire whole segments. Token-vault retention and audit retention are independent; rotating the audit does not purge tokens.
43
+
44
+ ## 3. Boundaries & non-goals (honest)
45
+ - This is a **mapping**, not a certification or legal advice. Deploying Haechi does not make a system "GDPR/PIPA/etc. compliant."
46
+ - Haechi controls the **gateway boundary** only. The model provider's retention, your application store, and backups are the operator's responsibility (`shared-responsibility.md`).
47
+ - Detection is regex + validators (no ML); documented exclusions stand (`threat-model.md` §4). A DSAR/erasure program must not assume Haechi caught *every* instance of a value.
48
+
49
+ ## 4. Cross-references
50
+ - `docs/current/shared-responsibility.md` — the Haechi-vs-operator responsibility matrix (the DSAR/retention split is called out there).
51
+ - `docs/current/operations-runbook.md` — §6 chain-aware audit rotation & retention.
52
+ - `docs/current/security-whitepaper.md` — the OWASP-LLM / NIST-AI-RMF control mapping + self-pentest.
53
+ - `docs/current/threat-model.md` — exclusions and accepted residuals.
@@ -0,0 +1,30 @@
1
+ # Haechi `configVersion` & 업그레이드 노트
2
+
3
+ - 상태: Living document (코어 1.2.x 추적)
4
+
5
+ `configVersion`는 `haechi.config.json`(및 `haechi.config.example.json`) 최상위에 찍히는 단일 정수입니다. 향후 호환성을 깨는 설정 스키마 변경이 구체적으로 게이트할 수 있는 **버전 앵커**로서, 다른 Haechi 빌드가 쓴 설정을 조용히 잘못 읽는 일을 막습니다.
6
+
7
+ ## 동작
8
+
9
+ - **기본값 / 없음:** `configVersion`를 생략한 설정(예: 스탬프가 생기기 전의 1.1 파일)은 **현재** 버전으로 간주합니다. 필드 추가는 기존 설정에 아무 영향이 없었습니다.
10
+ - **현재 버전:** `1`.
11
+ - **더 높은/알 수 없는 값은 fail-closed:** 빌드가 이해하는 값보다 **큰** `configVersion`는 로드 시 throw합니다 — *더 새로운* Haechi가 쓴 설정은 이 빌드가 구현하지 않은 의미에 의존할 수 있으므로, 추측 대신 거부합니다. Haechi를 업그레이드하거나, 호환성을 확인한 뒤 스탬프를 낮추십시오.
12
+ - **형식이 잘못되면 fail-closed:** 양수 정수가 아닌 `configVersion`는 throw합니다(`configVersion must be a positive integer`).
13
+
14
+ 이는 `normalizeConfig`의 나머지와 동일한 fail-closed 자세입니다: 모호하거나 미래 시점의 설정은 게이트웨이를 약화시키는 대신 멈춥니다.
15
+
16
+ ## 더 높은 버전에 fail-closed인 이유
17
+
18
+ 알 수 없는 설정을 조용히 실행하는 보안 게이트웨이는, 예를 들어 인식하지 못한 미래의 집행 키를 무시하고 운영자 의도보다 약하게 동작할 수 있습니다. 기동을 거부하면 불일치가 즉시 드러나며 "정책은 더 강해질 뿐 / fail closed" 불변식이 유지됩니다.
19
+
20
+ ## 버전 맵
21
+
22
+ | `configVersion` | 코어 라인 | 노트 |
23
+ |---|---|---|
24
+ | `1` | 1.0 – 1.2.x | 최초 스탬프. 모든 키는 1.0 frozen 설정 표면(`api-stability.md` §2.4)에 대해 additive입니다. 1.1.x의 additive 키(`logging`, `metrics`, WS4-B의 `limits.maxInFlight` / `limits.shutdownGraceMs` / `limits.requestTimeoutMs` / `limits.headersTimeoutMs`, 그리고 `configVersion` 자체)와 1.2.0 신뢰성 강화 키(`filters.minConfidence` / `filters.allowlist`, `proxy.tls` / `proxy.trustForwardedProto`)는 모두 이전 동작을 기본값으로 합니다. 마이그레이션 불필요. |
25
+
26
+ ## 업그레이드
27
+
28
+ 향후 마이너가 설정 키를 추가할 때, 그 키들은 **additive**(이전 동작 기본값)로 유지되고 `configVersion`는 `1`에 머뭅니다 — 조치 불필요. `configVersion`는 의도적인 호환성 파괴 스키마 변경과 함께서만 **올라가며**, 그 변경은 `api-stability.md` §2.2에 따라 메이저 버전 상승과 deprecation 노트도 동반합니다. 그 시점에 이 표에 마이그레이션을 설명하는 행이 추가되며, 더 낮은 버전으로 찍힌 설정은 조용히가 아니라 명시적으로 마이그레이션(또는 호환 규칙으로 읽기)됩니다.
29
+
30
+ 핀 고정: 설정 최상위에 `"configVersion": 1`을 설정하십시오(예제 설정은 이미 그렇게 합니다). 향후 스키마 상승을 넘어 Haechi를 업그레이드하려면, 스탬프를 올리기 전에 대상 버전의 마이그레이션 행을 따르십시오.
@@ -0,0 +1,51 @@
1
+ # Haechi `configVersion` & Upgrade Notes
2
+
3
+ - Status: Living document (tracks core 1.2.x)
4
+
5
+ `configVersion` is a single integer stamped at the top of `haechi.config.json`
6
+ (and `haechi.config.example.json`). It is a **versioned anchor** so a future
7
+ breaking config-schema change has something concrete to gate on, rather than
8
+ silently mis-reading a config written by a different Haechi build.
9
+
10
+ ## Behavior
11
+
12
+ - **Default / absent:** a config that omits `configVersion` (e.g. a 1.1 file
13
+ written before the stamp existed) is treated as the **current** version. Adding
14
+ the field changed nothing for existing configs.
15
+ - **Current version:** `1`.
16
+ - **Fail-closed on newer/unknown:** a `configVersion` **greater** than the build
17
+ understands throws at load — a config a *newer* Haechi wrote may rely on
18
+ semantics this build does not implement, so Haechi refuses rather than guessing.
19
+ Upgrade Haechi, or lower the stamp once you have confirmed compatibility.
20
+ - **Fail-closed on malformed:** a non-positive or non-integer `configVersion`
21
+ throws (`configVersion must be a positive integer`).
22
+
23
+ This is the same fail-closed posture as the rest of `normalizeConfig`: an
24
+ ambiguous or forward-dated config stops the gateway rather than degrading it.
25
+
26
+ ## Why fail-closed on a newer version
27
+
28
+ A security gateway that silently runs an unfamiliar config could, for example,
29
+ ignore a future enforcement key it does not recognize and run weaker than the
30
+ operator intended. Refusing to start surfaces the mismatch immediately and keeps
31
+ the "policies only get stronger / fail closed" invariant intact.
32
+
33
+ ## Version map
34
+
35
+ | `configVersion` | Core line | Notes |
36
+ |---|---|---|
37
+ | `1` | 1.0 – 1.2.x | Initial stamp. All keys are additive over the 1.0 frozen config surface (`api-stability.md` §2.4). The 1.1.x additive keys (`logging`, `metrics`, the WS4-B `limits.maxInFlight` / `limits.shutdownGraceMs` / `limits.requestTimeoutMs` / `limits.headersTimeoutMs`, `configVersion` itself) and the 1.2.0 Reliability-Hardening keys (`filters.minConfidence` / `filters.allowlist`, `proxy.tls` / `proxy.trustForwardedProto`) all default to prior behavior. No migration needed. |
38
+
39
+ ## Upgrading
40
+
41
+ When a future minor adds config keys, they remain **additive** (default to prior
42
+ behavior) and `configVersion` stays `1` — no action required. `configVersion`
43
+ will only be **bumped** alongside a deliberate breaking schema change, which would
44
+ also carry a major version bump and a deprecation note per `api-stability.md`
45
+ §2.2. At that point this table gains a row describing the migration, and a config
46
+ stamped with the older version is migrated (or read under compatibility rules)
47
+ explicitly — never silently.
48
+
49
+ To pin: set `"configVersion": 1` at the top of your config (the example config
50
+ already does). To upgrade Haechi past a future schema bump, follow the migration
51
+ row for the target version before raising the stamp.
@@ -1,6 +1,6 @@
1
1
  # Haechi 설정 레퍼런스
2
2
 
3
- - 문서 상태: Living document(core 1.1.x 추적)
3
+ - 문서 상태: Living document(core 1.2.x 추적)
4
4
 
5
5
  `haechi init`은 `haechi.config.json`을 생성하며, 비밀 정보를 포함하지 않는 템플릿은 `haechi.config.example.json`에 있습니다. 모든 커맨드는 `--config <path>`로 설정 파일을 읽습니다(기본값: `haechi.config.json`). 설정은 **fail-closed 방식으로 검증**됩니다. 알 수 없는 provider, 범위를 벗어난 숫자, 잘못된 형식의 값은 자동으로 무시되지 않고 로드 시점에 오류를 발생시킵니다. `haechi config`는 이 레퍼런스를 출력하며, `haechi status`는 특정 설정 파일의 *실제 적용* 상태를 출력합니다.
6
6
 
@@ -8,18 +8,21 @@
8
8
 
9
9
  ```json
10
10
  {
11
+ "configVersion": 1,
11
12
  "mode": "dry-run",
12
13
  "target": { "type": "llm-http", "adapter": "openai-compatible", "upstream": "http://127.0.0.1:9999" },
13
- "proxy": { "host": "127.0.0.1", "port": 11016 },
14
+ "proxy": { "host": "127.0.0.1", "port": 11016, "tls": null, "trustForwardedProto": false },
14
15
  "responseProtection": { "enabled": false, "mode": "enforce", "failureMode": "fail-closed", "allowNonJson": false, "allowCompressed": false, "maxBytes": 1048576 },
15
16
  "streaming": { "requestMode": "block" },
16
- "limits": { "maxRequestBytes": 1048576, "upstreamTimeoutMs": 120000, "maxNestingDepth": 256 },
17
+ "limits": { "maxRequestBytes": 1048576, "upstreamTimeoutMs": 120000, "maxNestingDepth": 256, "maxInFlight": 0, "shutdownGraceMs": 10000, "requestTimeoutMs": null, "headersTimeoutMs": null },
17
18
  "policy": { "mode": "dry-run", "presets": ["korean-pii", "secrets-only", "llm-redact"], "defaultAction": "redact", "actions": { "card": "block" } },
18
19
  "filters": { "customRules": [] },
19
20
  "keys": { "provider": "local", "keyFile": ".haechi/dev.keys.json" },
20
21
  "audit": { "sink": "jsonl", "path": ".haechi/audit.jsonl" },
21
22
  "tokenVault": { "provider": "local", "path": ".haechi/token-vault.json", "revealPolicy": "disabled", "retentionDays": 30, "deterministic": false, "deterministicTypes": null, "detokenizeResponses": false },
22
23
  "privacy": { "profile": null },
24
+ "logging": { "format": "text" },
25
+ "metrics": { "enabled": true },
23
26
  "mcp": { "allowedMethods": ["initialize", "tools/call", "resources/read", "prompts/get"], "protectParams": true, "protectResults": true, "requireJsonRpc": true }
24
27
  }
25
28
  ```
@@ -28,6 +31,7 @@
28
31
 
29
32
  | 키 | 타입 / 값 | 기본값 | 설명 |
30
33
  |---|---|---|---|
34
+ | `configVersion` | 양의 정수 | `1` | 설정 스키마 버전 스탬프입니다. 값이 없으면 현재 버전으로 간주합니다. 이 빌드가 이해하는 값보다 **더 높은** 값은 로드 시 **fail-closed**로 실패하며, 양수 정수가 아닌 값은 오류를 발생시킵니다. [`config-version.md`](./config-version.md)를 참고하십시오. |
31
35
  | `mode` | `dry-run` \| `report-only` \| `enforce` | `dry-run` | 전역 집행 모드입니다. `dry-run`/`report-only`는 탐지와 audit만 수행하며, `enforce`는 변환/차단을 적용합니다. `policy.mode`가 설정된 경우 해당 값이 우선합니다. |
32
36
 
33
37
  ## `target`
@@ -44,6 +48,8 @@
44
48
  |---|---|---|---|
45
49
  | `proxy.host` | 비어 있지 않은 문자열 | `127.0.0.1` | 바인드 주소입니다. loopback이 아닌 host를 사용하려면 `--allow-remote-bind` CLI 플래그가 필요합니다. 설정 파일만으로는 시작되지 않습니다([loopback 밖으로 바인딩](#binding-beyond-loopback) 참고). |
46
50
  | `proxy.port` | 정수 0–65535 | `11016` | 리슨 포트입니다(`0` = 임시 포트). `--port`로 실행할 때마다 덮어쓸 수 있습니다. |
51
+ | `proxy.tls` | `null` 또는 `{ keyFile, certFile }` / `{ pfxFile, passphrase? }` | `null` | 기동 시 **파일 경로**에서 읽어들이는 TLS 자료입니다. 설정되면 Haechi가 직접 TLS를 종단합니다(`https` 제공). remote bind에는 `trustForwardedProto`와 함께 둘 중 하나가 필요합니다([loopback 밖으로 바인딩](#binding-beyond-loopback) 참고). fail-closed: non-null이지만 사용 가능한 자료 `((key && cert) 또는 pfx)`로 해석되지 않거나, `pfxFile`을 `keyFile`/`certFile`과 함께 쓰거나, 읽을 수 없는 파일을 지정하면 로드 시 throw합니다. |
52
+ | `proxy.trustForwardedProto` | boolean | `false` | **신뢰하는 reverse proxy가 Haechi 앞단에서 TLS를 종단함**을 운영자가 명시적으로 확인하는 값입니다. `true`이면 remote bind가 plain `http`로 유지될 수 있으나, Haechi는 **`X-Forwarded-Proto`가 `https`가 아닌 모든 요청을 거부**합니다(auth/body 이전에 검사하며, `/__haechi/*` liveness 라우트는 예외입니다). Haechi 자체가 인터넷에 직접 노출될 때는 실제 TLS를 대체하지 못합니다. |
47
53
 
48
54
  ## `responseProtection`
49
55
 
@@ -74,6 +80,10 @@ upstream JSON 응답을 검사합니다(기본적으로 꺼져 있습니다 —
74
80
  | `limits.maxRequestBytes` | 양의 정수 | `1048576` | 요청 바디 크기 상한입니다. 초과 시 `413`을 반환합니다. 바디를 전부 버퍼링하지 않고 증분 방식으로 적용됩니다. |
75
81
  | `limits.upstreamTimeoutMs` | 양의 정수 | `120000` | upstream 요청 타임아웃입니다. 만료 시 `504 haechi_upstream_timeout`을 반환합니다. 연결 실패 시에는 `502 haechi_upstream_unreachable`을 반환합니다. |
76
82
  | `limits.maxNestingDepth` | 양의 정수 | `256` | 탐지 시 walk하는 JSON 최대 중첩 깊이입니다. 이보다 깊게 중첩된 바디는 `413 haechi_request_too_deeply_nested`로 거부되어(upstream 이전, fail-closed), 재귀적 payload walk를 스택 오버플로로부터 보호합니다. 컨테이너 하강을 제한하며, 한도에 있는 leaf는 여전히 검사됩니다. (별도로, 비UTF-8 요청 바디는 fail-closed로 거부됩니다: `400 haechi_request_body_not_utf8`.) |
83
+ | `limits.maxInFlight` | 음이 아닌 정수 | `0` | 전역 max-in-flight 백프레셔 상한입니다. `0`은 비활성화이며(상한 없음 — 1.1 동작), `> 0`이고 현재 in-flight 수가 상한에 도달하면 **새** 요청은 인증/바디 읽기 **이전에** `Retry-After` 헤더와 `{ "error": "haechi_overloaded" }`와 함께 `503`으로 거부됩니다. `/__haechi/*` 관측 라우트는 **예외**입니다(포화 상태에서도 liveness·metrics 스크레이프 가능). 거부마다 `haechi_overloaded_total`이 증가합니다. [운영 런북](./operations-runbook.md#5-backpressure-tuning)을 참고하십시오. |
84
+ | `limits.shutdownGraceMs` | 음이 아닌 정수(ms) | `10000` | 우아한 종료(graceful shutdown) 유예 기간입니다. `SIGINT`/`SIGTERM` 시 프록시는 새 연결 수락을 멈추고, idle keep-alive 소켓을 즉시 닫고, in-flight 요청이 빠질 때까지 기다린 뒤, 이 유예가 지나면 남은 소켓을 강제 종료하여 멈춘 keep-alive가 종료를 무한정 붙잡지 못하게 합니다. 백프레셔 `Retry-After` 초 값의 기준이기도 합니다. 오케스트레이터의 종료 유예를 이 값보다 **크게** 설정하십시오. |
85
+ | `limits.requestTimeoutMs` | `null` \| 음이 아닌 정수(ms) | `null` | Node HTTP 서버의 `requestTimeout`에 매핑됩니다. `null`은 Node 기본값을 그대로 둡니다(동작 불변). 느린 전체 요청 전달을 제한하려면 숫자를 설정하고, `0`은 타임아웃 비활성화입니다(Node 의미). |
86
+ | `limits.headersTimeoutMs` | `null` \| 음이 아닌 정수(ms) | `null` | Node HTTP 서버의 `headersTimeout`에 매핑됩니다. `null`은 Node 기본값을 그대로 둡니다. 느린 헤더 전달(slow-loris)을 제한하려면 숫자를 설정하고, `0`은 비활성화입니다. |
77
87
 
78
88
  ## `policy`
79
89
 
@@ -93,6 +103,19 @@ upstream JSON 응답을 검사합니다(기본적으로 꺼져 있습니다 —
93
103
  | 키 | 타입 / 값 | 기본값 | 설명 |
94
104
  |---|---|---|---|
95
105
  | `filters.customRules` | 규칙 객체 배열 | `[]` | 추가 탐지 규칙입니다: `{ id, type, pattern, flags?, confidence? }`. 패턴은 ReDoS 검사를 통과해야 하며(≤500자, 중첩 한정자 없음, 역참조 없음), 안전하지 않으면 로드 시 거부됩니다. |
106
+ | `filters.minConfidence` | `[0, 1]` 범위의 숫자 | `0` | 정밀도 다이얼입니다. 각 규칙은 `confidence`(0.6~0.95)를 가지며, confidence가 이 임계값 **미만**인 탐지는 policy 결정 전에 버려집니다. 기본값 `0`은 아무것도 게이트하지 않아 기존 동작을 보존합니다. **하드 블록 예외:** 하드 블록 타입(`secret`, `api_key`, `kr_rrn`, `card`)은 confidence만으로는 **절대** 버려지지 않습니다 — `minConfidence`는 정밀도 위험이 큰 소프트 타입(예: `phone`, `email`, `injection`)만 다듬으므로, confidence가 낮은 자격증명/PII 누출도 여전히 조치됩니다(fail-closed). |
107
+ | `filters.allowlist` | 문자열 및/또는 `{ value?, path? }` 의 배열 | `[]` | 운영자 false-positive 예외입니다. 매칭된 **value**가 문자열/`value` 항목과 같거나, PII-safe JSON **path**(audit에 표시되는 해시된 `pathText`)가 `path` 항목과 같은 탐지는 policy 결정 전에 억제됩니다(항목이 `value`와 `path`를 모두 설정하면 **둘 다** 일치해야 합니다). **하드 블록 예외:** 하드 블록 타입(`secret`/`api_key`/`kr_rrn`/`card`)을 억제하려는 항목은 **무시되며** 탐지는 그대로 발생합니다 — allowlist는 양성(benign) **소프트 타입** FP만 정리할 수 있고, 자격증명/PII 누출은 절대 침묵시킬 수 없습니다. 모든 억제와 모든 `minConfidence` 드롭은 개수와 타입으로 **감사 로그에 기록됩니다**(`summary.suppressedByType` / `summary.droppedByType` / `suppressedCount` / `droppedCount`) — 원시 값은 절대 기록하지 않습니다. 규칙 전체를 삭제하지 않고 양성 FP 하나만 정리할 때 사용하십시오. |
108
+
109
+ ### 탐지 벤치마크
110
+
111
+ 탐지 정밀도(precision)/재현율(recall)은 가정하지 않고 측정합니다. 합성 테스트 픽스처로 구성된 라벨링 코퍼스(`tests/fixtures/detection-corpus.json` — type별 양성 샘플과 양성처럼 보이는 hard-negative)를 기반으로 type별 채점기를 돌립니다.
112
+
113
+ ```bash
114
+ npm run bench:detection # type별 TP/FP/FN + precision/recall 표를 출력합니다
115
+ npm run scan:detection # CI 회귀 게이트: 어떤 type이라도 baseline 아래로 떨어지면 실패합니다
116
+ ```
117
+
118
+ `bench:detection`(`scripts/bench-detection.mjs`)은 기본 필터 엔진을 각 코퍼스 케이스에 적용하여 type별 true/false positive와 false negative를 보고합니다. `scan:detection`은 실측 점수를 고정된 baseline(`scripts/detection-baseline.json`)과 비교하며 **회귀일 때만 실패합니다** — 즉 precision 또는 recall이 기록된 수치 아래로 떨어진 경우입니다. baseline에는 현재의 불완전한 상태(`phone`/`card`/`secret`에서 audit이 재현한 오탐, 그리고 AWS/GitHub/Google/Slack 키·JWT·PEM 헤더에 대한 알려진 커버리지 공백 누락)가 의도적으로 포함되어 있으므로, 게이트는 오늘은 통과하고 변경이 탐지를 악화시킬 때만 실패합니다. 이 게이트는 `release:preflight`에서 doc-freshness 게이트 다음에 실행됩니다. 의도적인 규칙 변경 후에는 `node scripts/bench-detection.mjs --write-baseline`으로 baseline을 재생성하고 diff를 검토하십시오. 기록된 공백과 오탐을 닫는 작업은 reliability-hardening 트랙의 WS2b/WS2c입니다.
96
119
 
97
120
  ## `keys`
98
121
 
@@ -126,6 +149,69 @@ upstream JSON 응답을 검사합니다(기본적으로 꺼져 있습니다 —
126
149
  |---|---|---|---|
127
150
  | `privacy.profile` | `null` \| `kr-pipa` \| `eu-gdpr` \| `us-general` | `null` | 집행 전에 지역별 기준 action 집합을 적용합니다. 프로필은 명시적 action을 **강화**할 수는 있지만 약화할 수는 없습니다. 엔지니어링 기본값이며, 법적 자문이 아닙니다. |
128
151
 
152
+ ## `logging`
153
+
154
+ | 키 | 타입 / 값 | 기본값 | 설명 |
155
+ |---|---|---|---|
156
+ | `logging.format` | `text` \| `json` | `text` | `text`는 사람이 읽는 기동/종료/오류 로그 줄을 그대로 유지합니다(변경 없음). `json`은 이벤트마다 한 줄짜리 JSON 객체를 출력합니다. fail-closed이며, 다른 값은 예외를 던집니다. |
157
+
158
+ `json` 모드에서 프록시 내부 오류 로그는 `{ "level": "error", "event": "proxy_internal_error", "correlationId", "errorName", "statusCode" }` 한 줄이며, 기동/종료는 `proxy_listening` / `proxy_shutdown`을 출력합니다(원격 바인드/비-enforce 모드/응답 보호 비활성화에 대한 `*_warn` 이벤트도 함께). **어떤 로그 필드도 요청/응답 페이로드, 헤더, 토큰, PII를 절대 담지 않습니다.** 오류 로그는 오류 *클래스 이름*과 요청 `correlationId`만 담습니다.
159
+
160
+ ## `metrics`
161
+
162
+ | 키 | 타입 / 값 | 기본값 | 설명 |
163
+ |---|---|---|---|
164
+ | `metrics.enabled` | boolean | `true` | `GET /__haechi/metrics` 라우트를 제어합니다. `false`이면 해당 라우트는 `404`를 반환합니다. fail-closed이며, boolean이 아니면 예외를 던집니다. |
165
+
166
+ 메트릭 수집기는 **주입 가능한 협력 객체**이기도 합니다(`createRuntime(config, { metrics })`). 계약과 no-PII 보장은 [운영 엔드포인트](#운영-엔드포인트)를 참고하십시오.
167
+
168
+ ## 운영 엔드포인트
169
+
170
+ 프록시는 예약된 `/__haechi/*` 접두어 아래에 네 개의 인증 없는 엔드포인트를 제공하며, 이들은 인증과 본문 읽기 **이전**에 처리되고 업스트림으로 프록시되지 않습니다.
171
+
172
+ | 엔드포인트 | 상태 코드 | 본문 | 용도 |
173
+ |---|---|---|---|
174
+ | `GET /__haechi/live` | `200` | `{ ok: true, version }` | 저비용 프로세스 liveness. |
175
+ | `GET /__haechi/ready` | `200` / `503` | `{ ready, version, checks }` | readiness. **fail-closed**: audit 로그에 append할 수 없는 게이트웨이는 ready가 **아닙니다**(`503`). 기본 JSONL sink의 `checks.auditWritable`는 이벤트를 쓰지 않고 audit 디렉터리/파일의 쓰기 가능 여부를 확인하며, `ready()`/`healthCheck()` 메서드가 없는 sink는 ready로 간주합니다. |
176
+ | `GET /__haechi/health` | `200` | `{ ok: true, mode, version }` | back-compat(기존 health 엔드포인트이며 이제 `version`을 포함). |
177
+ | `GET /__haechi/metrics` | `200` / `404` | Prometheus 텍스트 | 텔레메트리(아래 참고). `metrics.enabled: false`이면 `404`. |
178
+
179
+ `version`은 실행 중인 패키지 버전(`package.json`)입니다.
180
+
181
+ ### 텔레메트리 (`/__haechi/metrics`)
182
+
183
+ 이 엔드포인트는 **Prometheus 텍스트 노출 형식**(`# HELP` / `# TYPE` + `name{label="..."} value`)을 `Content-Type: text/plain`으로 렌더링합니다. 카운터: `haechi_requests_total{route,mode,decision}`와 `haechi_blocks_total`, `haechi_auth_denied_total`, `haechi_rate_limited_total`, `haechi_upstream_timeout_total`, `haechi_upstream_error_total`, `haechi_response_unprotected_total`, `haechi_internal_error_total`. 히스토그램 하나: `haechi_request_duration_seconds{route}`.
184
+
185
+ **텔레메트리 no-PII 불변식.** 모든 메트릭 이름과 **모든 라벨 값**은 경계가 정해진 enum입니다 — 라우트 id, 정책 모드, 고정된 decision 클래스(`forwarded` / `blocked` / `auth_denied` / `rate_limited` / `model_not_allowed` / …)입니다. 메트릭 라벨은 identity id/subject, 토큰, 탐지된 값을 **절대** 담지 않습니다. identity별·값별 라벨 카디널리티가 존재하지 않습니다. 이는 audit에 평문을 남기지 않는 불변식을 텔레메트리로 확장한 것이며, 메트릭 모듈은 방어적으로 라벨 값의 길이를 제한하고 문자셋을 정제합니다.
186
+
187
+ ### `providers.metrics` 주입 seam
188
+
189
+ 메트릭 수집기는 `createRuntime(config, providers)`를 통해 프로그램적으로 공급됩니다 — `cryptoProvider`/`authProvider`/`rateLimiter`와 동일한 seam이며, JSON 설정 키가 **아닙니다**.
190
+
191
+ ```js
192
+ const runtime = createRuntime(config, { metrics });
193
+ ```
194
+
195
+ 주입된 `metrics`는 `increment(name, labels?, amount?)`, `observe(name, value, labels?)`, `render() -> string`을 구현해야 하며, 그렇지 않으면 `createRuntime`은 생성 시점에 fail-closed로 실패합니다. **기본값**은 위 Prometheus 텍스트를 렌더링하는 무의존성 인메모리 수집기입니다. 다중 레플리카 운영자는 동일한 계약을 만족하는 공유/원격 수집기를 주입합니다.
196
+
197
+ ### `correlationId` (audit + 로그)
198
+
199
+ 프록시는 요청마다 `correlationId`(UUID)를 생성합니다. 이 값은 protect 컨텍스트로 전달되어 한 요청의 request·response 방향 audit 이벤트가 동일한 추가(additive) 최상위 `correlationId` 필드를 갖게 하며, 프록시 내부 오류 로그 줄에도 전달되어 운영자가 기록된 오류를 그 audit 추적과 연결할 수 있게 합니다. 프록시가 아닌 `protectJson()` 호출에서는 `null`입니다(기존 동작 보존). 이 id는 UUID이며 페이로드/identity/PII 값을 **절대** 담지 않습니다.
200
+
201
+ ## 환경변수 설정 오버레이 (배포)
202
+
203
+ 컨테이너 / 12-factor 배포를 위해, **비밀이 아닌 운영 키의 고정 allowlist**를 환경변수로 덮어쓸 수 있습니다. 환경변수 값은 **설정 파일보다 우선**하며 **fail-closed**로 검증됩니다 — 잘못된 값은 프로세스를 기동 실패시킵니다. `loadConfig()`에서 파일을 읽은 뒤 검증 이전에 적용됩니다.
204
+
205
+ | 환경변수 | 설정 키 | 타입 / 값 |
206
+ |---|---|---|
207
+ | `HAECHI_PROXY_PORT` | `proxy.port` | 정수 0–65535 |
208
+ | `HAECHI_PROXY_HOST` | `proxy.host` | 비어 있지 않은 문자열 |
209
+ | `HAECHI_UPSTREAM` | `target.upstream` | URL 문자열 |
210
+ | `HAECHI_MODE` | `mode` | `dry-run` \| `report-only` \| `enforce` |
211
+ | `HAECHI_LOG_FORMAT` | `logging.format` | `text` \| `json` |
212
+
213
+ **비밀은 설계상 오버레이 대상이 아닙니다.** `keys.*`, auth 토큰 저장소, 토큰/비밀에 대한 `HAECHI_*` 변수는 **없습니다**. 비밀은 설정 파일에 두거나 주입된 provider(`createRuntime(config, { cryptoProvider, authProvider, … })`)로 공급합니다. 비밀을 프로세스 환경에 두면 `/proc`, 크래시 덤프, 오케스트레이터 inspect 출력으로 누출될 위험이 있으므로 오버레이 allowlist에서 제외합니다. [운영 런북](./operations-runbook.md#2-configuration-via-the-env-var-overlay)을 참고하십시오.
214
+
129
215
  ## `mcp`
130
216
 
131
217
  `mcp-stdio`와 `mcp-wrap`에 적용됩니다.
@@ -173,15 +259,54 @@ upstream JSON 응답을 검사합니다(기본적으로 꺼져 있습니다 —
173
259
  | `policy.profiles` | `{ <name>: { presets?, actions?, modelAllowlist?, rate? } }` | `{}` | Named profile입니다. 각각 기본 policy를 재정의합니다. |
174
260
  | `policy.profileBinding` | `{ byScope?, byLabel?, default }` | 미설정 | identity scope/label(`"k=v"` 형태)을 profile 이름으로 매핑합니다. `profiles`가 설정된 경우 `default`는 **필수**이며 가장 엄격한 profile이어야 합니다(fail-closed). |
175
261
  | `policy.modelAllowlist` | 문자열 배열 | 미설정 | 허용된 `model` 값입니다(기본 레벨; profile별로도 설정 가능). 허용되지 않은 모델 → `403`. 비어 있거나 없으면 모두 허용합니다. |
176
- | `policy.rate` | `{ requestsPerMinute }` | 미설정 | identity별 요청 rate limit입니다(기본 레벨 또는 profile별). 초과 시 → `429`. 인메모리, 프로세스별입니다. |
262
+ | `policy.rate` | `{ requestsPerMinute }` | 미설정 | identity별 요청 rate limit입니다(기본 레벨 또는 profile별). 초과 시 → `429`. 인메모리, 프로세스별입니다. 다중 replica 시임은 [Rate limiter 주입](#rate-limiter-주입)을 참고하십시오. |
177
263
 
178
264
  ### Named profiles
179
265
 
180
266
  identity가 인증되면 **scope → label → `default`** 순으로 profile이 resolve됩니다. scope가 label보다 우선하며 첫 번째 매칭이 적용됩니다. `profiles`가 없거나 `auth.provider: none`인 경우 기본 policy가 적용됩니다. Resolve된 profile의 policy 엔진, `modelAllowlist`, `rate`가 해당 요청을 처리합니다.
181
267
 
268
+ ### Rate limiter 주입
269
+
270
+ rate limiter는 **주입 가능한 collaborator**이며, `createRuntime(config, providers)`의 `providers` 인자를 통해 프로그래밍 방식으로 공급됩니다 — 외부 `cryptoProvider`/`authProvider`와 동일한 시임입니다. JSON config 키가 **아닙니다**.
271
+
272
+ ```js
273
+ const runtime = createRuntime(config, { rateLimiter });
274
+ ```
275
+
276
+ 주입된 `rateLimiter`는 `allow(key, limit) -> boolean`을 구현해야 합니다(`key`는 identity별 버킷, `limit`은 resolve된 `requestsPerMinute`입니다). 구현하지 않으면 `createRuntime`이 construction 시점에 fail-closed로 throw합니다. proxy는 rate 통제 대상 요청마다 `runtime.rateLimiter`를 참조합니다.
277
+
278
+ **기본값**은 프로세스별 인메모리 fixed-window 카운터입니다. 재시작 시 초기화되며 **replica 간에 공유되지 않으므로**, load balancer 뒤에서 총 처리량은 replica 수만큼 곱해집니다. window map은 self-bounding입니다(lazy, amortized sweep로 만료된 one-shot identity를 제거합니다 — 백그라운드 timer 없음). 다중 replica 배포에서는 공유 front door에서 identity별 limit을 강제하거나, 동일한 `allow(key, limit)` 계약을 만족하는 공유 저장소 구현(예: Redis 기반)을 주입하십시오. [Shared responsibility §4](./shared-responsibility.ko.md#4-수평-확장--다중-복제)를 참고하십시오.
279
+
182
280
  ## Detection type과 action
183
281
 
184
- 내장 탐지 `type` 값은 다음과 같습니다: `email`, `phone`, `kr_rrn`, `card`, `api_key`, `secret`, `injection`(응답 방향 휴리스틱, 기본 report-only). 커스텀 규칙으로 새로운 type을 추가할 수 있습니다.
282
+ 내장 탐지 `type` 값은 다음과 같습니다: `email`, `phone`, `kr_rrn`, `card`, `api_key`, `secret`, `us_ssn`, `iban`, `injection`(응답 방향 휴리스틱, 기본 report-only). 커스텀 규칙으로 새로운 type을 추가할 수 있습니다.
283
+
284
+ ### 지원하는 자격증명·PII 매트릭스
285
+
286
+ 탐지는 정규식 + 선택적 validator로 동작합니다(ML 미사용). 모든 규칙은 정밀도를 높게 유지하기 위해 **단단히 anchoring**되어 있으며, recall보다 precision을 우선합니다. 코퍼스(`tests/fixtures/detection-corpus.json`)에는 규칙마다 hard-negative가 포함됩니다. KR phone 규칙과 US SSN/IBAN validator는 유사 형태의 id·timestamp를 거부합니다.
287
+
288
+ | Type | 탐지 대상 | Anchor / validator | 비고 |
289
+ |---|---|---|---|
290
+ | `email` | RFC 형식 주소 | local + domain + TLD | — |
291
+ | `phone` | KR 휴대폰(`01[016789]`, `+82`) | 구분자 없는 bare run은 `0`으로 시작해야 함 | KR 유선번호는 범위 외입니다. |
292
+ | `phone` | E.164 국제번호 | **선행 `+` 필수**(`+[1-9]` + 6–14자리) | bare 숫자열은 절대 매칭하지 않습니다(id·timestamp와 충돌). |
293
+ | `phone` | US/NANP 국내번호 | **구분자 필수**(`(NXX) NXX-XXXX` 또는 `NXX-NXX-XXXX`) | 구분자 없는 10자리 숫자열은 매칭하지 않습니다. |
294
+ | `kr_rrn` | 주민등록번호 | 검증 숫자 validator | 형식은 맞으나 checksum 불일치 → 거부. |
295
+ | `card` | 결제 카드(PAN) | Luhn validator, 13–19자리 | — |
296
+ | `us_ssn` | 미국 사회보장번호 | `AAA-GG-SSSS` + SSA 범위 validator(area `000`/`666`/`900-999`, group `00`, serial `0000` 거부) | 구분자 필수이며, bare 9자리 id는 SSN이 아닙니다. |
297
+ | `iban` | 국제 은행계좌번호 | **mod-97 checksum** validator | checksum이 정밀도 가드입니다 — IBAN 형태이지만 97 비검증 문자열은 거부됩니다. |
298
+ | `api_key` | OpenAI 형식(`sk_`/`rk_`/`pk_`) | prefix + 24자 이상 | — |
299
+ | `api_key` | AWS access key id | `AKIA`/`ASIA` + 정확히 16자 대문자-alnum | — |
300
+ | `api_key` | Google API key | `AIza` + 35자 URL-safe 문자 | — |
301
+ | `secret` | `Bearer <token>` | `Bearer` + 16자 이상 | — |
302
+ | `secret` | 할당식 `<key> = <value>` | 키 어휘: `api_key`, `api_secret`, `secret`, `secret_key`, `aws_secret_access_key`, `client_secret`, `private_key`, `access_token`, `refresh_token`, `token`, `password` | bare-base64 시크릿(예: AWS secret access key)을 할당식 형태로 포착합니다. |
303
+ | `secret` | GitHub token | `gh[pousr]_` + 36자 이상 base64 유사 문자 | pat/oauth/user/server/refresh 변형. |
304
+ | `secret` | Slack token | `xox[baprs]-` + 10자 이상 본문 | bot/user/refresh/legacy 변형. |
305
+ | `secret` | JWT | 점으로 구분된 3개 base64url 세그먼트, 첫 세그먼트가 `eyJ`(즉 `{"`의 base64)로 시작 | `eyJ` anchor가 임의의 점-구분 토큰을 거부합니다. |
306
+ | `secret` | PEM private key | `-----BEGIN … PRIVATE KEY-----` armor 헤더 | 헤더 존재가 신호이며, "private key"를 언급한 산문은 매칭하지 않습니다. |
307
+ | `injection` | 프롬프트 인젝션 휴리스틱 | 응답 방향 전용, 기본 `allow` | [Action strength](#action-strength) 참고; report-only. |
308
+
309
+ 탐지는 문자열 값, JSON number leaf(요청 방향), object key를 대상으로 합니다. 각 **string leaf는 매칭 전 NFKC 정규화**되므로, 유니코드 난독화 형태(전각 숫자 `4242…`, 전각 `@`, 수학·원문자 영숫자)도 ASCII 호환 형태로 접혀 탐지됩니다. 접힘이 UTF-16 길이를 보존하면 우회된 정확한 구간을 redact/block하고, 길이가 달라지면(예: 수학 숫자·합자) 탐지가 fail closed되어 leaf 전체를 redact/block합니다. base64/percent-encoded 값(디코딩 후)과 URL query 문자열은 문서화된 제외 항목으로 남습니다(`docs/current/threat-model.md` 참고). 응답 방향에서는 Haechi 자체 transform marker와 bare JSON number leaf를 건너뜁니다(요청 방향은 항상 전체 스캔).
185
310
 
186
311
  Action(약한 것 → 강한 것 순):
187
312
 
@@ -240,17 +365,32 @@ preset과 override(또는 privacy profile)가 충돌할 경우 **강한** action
240
365
 
241
366
  ## loopback 밖으로 바인딩
242
367
 
243
- proxy는 CLI 플래그를 명시적으로 전달하지 않으면 loopback이 아닌 host를 거부합니다 — 설정 파일에 `proxy.host: "0.0.0.0"`을 지정해도 의도적으로 시작되지 않습니다:
368
+ proxy는 CLI 플래그를 명시적으로 전달하지 않으면 loopback이 아닌 host를 거부합니다 — 설정 파일에 `proxy.host: "0.0.0.0"`을 지정해도 의도적으로 시작되지 않습니다. remote bind에는 **TLS가 추가로 필요합니다**: Haechi가 직접 TLS를 종단하거나(`proxy.tls`), 앞단의 TLS 종단기를 명시적으로 확인해야 합니다(`proxy.trustForwardedProto`). 둘 다 없는 remote bind는 **기동 시 throw**합니다 — Haechi는 loopback이 아닌 리스너에서 bearer token과 payload를 평문으로 제공하지 않습니다.
369
+
370
+ **옵션 A — Haechi가 직접 TLS를 종단**(`https` 제공):
244
371
 
372
+ ```jsonc
373
+ // haechi.config.json
374
+ "proxy": { "host": "0.0.0.0", "tls": { "keyFile": "/etc/haechi/tls/key.pem", "certFile": "/etc/haechi/tls/cert.pem" } }
375
+ // 또는 PKCS#12: "tls": { "pfxFile": "/etc/haechi/tls/server.pfx", "passphrase": "…" }
376
+ ```
245
377
  ```bash
246
378
  haechi proxy --config haechi.config.json --host 0.0.0.0 --allow-remote-bind
379
+ # → Haechi proxy listening on https://0.0.0.0:11016
380
+ ```
381
+
382
+ **옵션 B — 신뢰하는 reverse proxy가 앞단에서 TLS를 종단**(Haechi는 그 뒤 사설망에서 plain `http`로 유지):
383
+
384
+ ```jsonc
385
+ "proxy": { "host": "0.0.0.0", "trustForwardedProto": true }
247
386
  ```
387
+ `trustForwardedProto: true`이면 Haechi는 **`X-Forwarded-Proto`가 `https`가 아닌 모든 요청을**(TLS hop을 우회한 평문 요청을) auth/body 이전에 fail-closed `403`으로 거부합니다. `/__haechi/*` liveness/metrics 라우트는 loopback sidecar가 스크레이프할 수 있도록 예외입니다. 오직 신뢰하는 종단기만 `X-Forwarded-Proto`를 설정해야 합니다 — 신뢰할 수 없는 클라이언트가 Haechi 포트에 직접 도달할 수 있다면 이 옵션을 켜지 마십시오.
248
388
 
249
389
  **proxy는 bearer 클라이언트 인증을 제공합니다**(`auth.provider: bearer`, 0.6에서 출시). 해시 기반 token 저장소, identity별 policy profile, model allowlist, identity별 rate limit을 함께 제공합니다([`auth`](#auth)와 [Named profiles](#named-profiles) 참고). 기본값 `auth.provider: none`은 proxy를 인증 없이 둡니다 — `none`에서는 포트에 접근할 수 있는 누구든 upstream과 token round-trip 경로를 사용할 수 있습니다. 내장 rate limit은 단일 프로세스(인메모리, 프로세스별)이므로, 여러 replica는 공유 limiter를 앞에 두어야 합니다. `--allow-remote-bind`는 어느 경우에도 명시적인 네트워크 통제 하에서만 사용해야 합니다 — 컨테이너 안에서 `0.0.0.0`으로 바인드하고 host 포트 매핑을 제한하거나(`-p 127.0.0.1:11016:11016`), 방화벽/VPN/인증 reverse proxy 뒤에 두어야 합니다.
250
390
 
251
391
  ## 검증 요약
252
392
 
253
- 다음은 로드 시 오류(fail-closed)를 발생시킵니다: 알 수 없는 `keys.provider`; 빈 `proxy.host`; 범위를 벗어난 `proxy.port`; `jsonl`이 아닌 `audit.sink`; `local`이 아닌 `tokenVault.provider`; 잘못된 `revealPolicy`; 양수가 아닌 `retentionDays`; boolean이 아닌 `deterministic`/`detokenizeResponses`; 비어 있거나 문자열이 아닌 `deterministicTypes`; 비어 있거나 문자열이 아닌 `mcp.allowedMethods`; boolean이 아닌 `mcp.*` 플래그; 알 수 없는 `privacy.profile`; 잘못된 `responseProtection.failureMode`; 양수가 아닌 `responseProtection.maxBytes`; boolean이 아닌 `responseProtection.scanNumbers`; 잘못된 `streaming.requestMode`; 잘못된 `streaming.responseMode`; 양수가 아닌 `streaming.maxMatchBytes`; 잘못된 `auth.provider`; 빈 `auth.store`; 문자열이 아닌 `auth.allowedLabelKeys`; 객체가 아닌 `policy.profiles`; 유효한 `default` 없는 `policy.profileBinding`; 문자열이 아닌 `policy.modelAllowlist`; 양수가 아닌 `policy.rate.requestsPerMinute`; 양수가 아닌 `limits.*`; 알 수 없는 `target.type`/`adapter`; 안전하지 않은 커스텀 정규식; `allowUnsafeOverrides` 없이 action을 약화하려는 시도.
393
+ 다음은 로드 시 오류(fail-closed)를 발생시킵니다: 알 수 없는 `keys.provider`; 빈 `proxy.host`; 범위를 벗어난 `proxy.port`; boolean이 아닌 `proxy.trustForwardedProto`; non-`null`이지만 object가 아니거나, `keyFile`만 있고 `certFile`이 없거나(또는 그 반대), `pfxFile`을 `keyFile`/`certFile`과 함께 쓰거나, 읽을 수 없는 파일을 지정하거나, 사용 가능한 자료 `((key && cert) 또는 pfx)`로 해석되지 않는 `proxy.tls`; `jsonl`이 아닌 `audit.sink`; `local`이 아닌 `tokenVault.provider`; 잘못된 `revealPolicy`; 양수가 아닌 `retentionDays`; boolean이 아닌 `deterministic`/`detokenizeResponses`; 비어 있거나 문자열이 아닌 `deterministicTypes`; 비어 있거나 문자열이 아닌 `mcp.allowedMethods`; boolean이 아닌 `mcp.*` 플래그; 알 수 없는 `privacy.profile`; 잘못된 `responseProtection.failureMode`; 양수가 아닌 `responseProtection.maxBytes`; boolean이 아닌 `responseProtection.scanNumbers`; 잘못된 `streaming.requestMode`; 잘못된 `streaming.responseMode`; 양수가 아닌 `streaming.maxMatchBytes`; 잘못된 `auth.provider`; 빈 `auth.store`; 문자열이 아닌 `auth.allowedLabelKeys`; 객체가 아닌 `policy.profiles`; 유효한 `default` 없는 `policy.profileBinding`; 문자열이 아닌 `policy.modelAllowlist`; 양수가 아닌 `policy.rate.requestsPerMinute`; 양수가 아닌 `limits.maxRequestBytes`/`limits.upstreamTimeoutMs`/`limits.maxNestingDepth`; 음수이거나 정수가 아닌 `limits.maxInFlight`/`limits.shutdownGraceMs`; `null`이 아니면서 음수이거나 정수가 아닌 `limits.requestTimeoutMs`/`limits.headersTimeoutMs`; 양수 정수가 아니거나 **지원 범위를 넘는** `configVersion`; 알 수 없는 `target.type`/`adapter`; 안전하지 않은 커스텀 정규식; `allowUnsafeOverrides` 없이 action을 약화하려는 시도; `text`/`json`이 아닌 `logging.format`; boolean이 아닌 `metrics.enabled`; 잘못된 `HAECHI_*` 환경변수 오버레이 값(잘못된 `HAECHI_PROXY_PORT`, 알 수 없는 `HAECHI_MODE`, 형식이 잘못된 `HAECHI_UPSTREAM` 등).
254
394
 
255
395
  # Satellite 운영자 설정 (0.9)
256
396