haechi 1.2.0 → 1.3.1
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/README.ko.md +57 -11
- package/README.md +57 -11
- package/docs/current/code-review-risk-register-2026-06-16.ko.md +377 -0
- package/docs/current/code-review-risk-register-2026-06-16.md +377 -0
- package/docs/current/config-version.ko.md +2 -2
- package/docs/current/config-version.md +2 -2
- package/docs/current/configuration.ko.md +28 -11
- package/docs/current/configuration.md +28 -11
- package/docs/current/operations-runbook.ko.md +36 -2
- package/docs/current/operations-runbook.md +39 -2
- package/docs/current/release-process.ko.md +5 -1
- package/docs/current/release-process.md +5 -1
- package/docs/current/risk-register-release-gate.ko.md +34 -8
- package/docs/current/risk-register-release-gate.md +34 -8
- package/docs/current/shared-responsibility.ko.md +12 -3
- package/docs/current/shared-responsibility.md +12 -3
- package/docs/current/threat-model.ko.md +7 -3
- package/docs/current/threat-model.md +7 -3
- package/examples/local-proxy-demo/README.md +51 -0
- package/examples/local-proxy-demo/demo.mjs +144 -0
- package/examples/local-proxy-demo/demo.tape +19 -0
- package/examples/local-proxy-demo/live-demo.mjs +121 -0
- package/examples/local-proxy-demo/live-demo.tape +25 -0
- package/haechi.config.example.json +2 -1
- package/package.json +3 -1
- package/packages/cli/bin/haechi.mjs +95 -5
- package/packages/cli/runtime.mjs +61 -1
- package/packages/core/index.mjs +15 -0
- package/packages/crypto/index.mjs +42 -20
- package/packages/filter/index.mjs +679 -6
- package/packages/privacy-profiles/index.mjs +72 -3
- package/packages/protocol-adapters/index.mjs +99 -1
- package/packages/proxy/index.mjs +270 -29
- package/packages/ssrf/index.mjs +60 -4
- package/packages/stream-filter/index.mjs +194 -17
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Haechi Configuration Reference
|
|
2
2
|
|
|
3
|
-
- Status: Living document (tracks core 1.
|
|
3
|
+
- Status: Living document (tracks core 1.3.x)
|
|
4
4
|
|
|
5
5
|
`haechi init` writes `haechi.config.json`; a non-secret template is at `haechi.config.example.json`. Every command reads it with `--config <path>` (default `haechi.config.json`). Configuration is **validated fail-closed**: unknown providers, out-of-range numbers, and malformed values throw at load time rather than degrading silently. `haechi config` prints this reference; `haechi status` prints the *effective* state of a given config.
|
|
6
6
|
|
|
@@ -38,9 +38,10 @@
|
|
|
38
38
|
|
|
39
39
|
| Key | Type / values | Default | Notes |
|
|
40
40
|
|---|---|---|---|
|
|
41
|
-
| `target.type` | `llm-http` \| `openai-compatible` \| `vllm-openai` \| `ollama` \| `llama-cpp` | `llm-http` | Selects the protocol adapter. `llm-http` aliases `openai-compatible`. Unknown values **fail closed** at load. |
|
|
41
|
+
| `target.type` | `llm-http` \| `openai-compatible` \| `vllm-openai` \| `ollama` \| `llama-cpp` \| `anthropic` \| `gemini` | `llm-http` | Selects the protocol adapter. `llm-http` aliases `openai-compatible`. `anthropic` targets the Anthropic Messages API (`/v1/messages`, `/v1/messages/count_tokens`, `/v1/complete`); the client supplies Anthropic's `x-api-key`/`anthropic-version` headers and the proxy forwards them. `gemini` targets the Google Gemini API, whose endpoints are model-in-path with a `:method` suffix (`/v1beta/models/{model}:generateContent`, `:streamGenerateContent` (SSE), `:countTokens`, `:embedContent`, `:batchEmbedContents`; `/v1` or `/v1beta` prefix, arbitrary `{model}`); the client supplies Gemini's `x-goog-api-key` (or `?key=`) and the proxy forwards it. Unknown values **fail closed** at load. |
|
|
42
42
|
| `target.adapter` | same set | `openai-compatible` | Explicit adapter override; usually leave unset and let `type` decide. |
|
|
43
43
|
| `target.upstream` | URL string | `http://127.0.0.1:9999` | The only upstream the proxy forwards to. Request targets must be origin-form paths; absolute-URL targets are rejected (SSRF guard). |
|
|
44
|
+
| `target.forwardHeaders` | array of lowercase header names | unset (`[]`) | **Additive** extension of the built-in upstream header allowlist. The proxy forwards only an explicit allowlist to the upstream (provider/adapter headers: `x-api-key`, `anthropic-version`, `anthropic-beta`, `x-goog-api-key`, `openai-organization`, `openai-beta`, `accept`, `accept-language`, `user-agent`, and `content-type` rewritten to `application/json`); the client `Authorization` is forwarded only when `auth.provider: none` (it is the upstream provider key) and dropped otherwise (it is the gateway credential); `Cookie`/`Set-Cookie`/`Proxy-Authorization` and hop-by-hop headers are always dropped. List extra lowercase names here for an unusual upstream. **Fail-closed:** must be an array of lowercase non-empty strings and may NOT name an always-dropped credential/hop-by-hop header. |
|
|
44
45
|
|
|
45
46
|
## `proxy`
|
|
46
47
|
|
|
@@ -103,8 +104,9 @@ The detect→decide core. See [Detection types & actions](#detection-types--acti
|
|
|
103
104
|
| Key | Type / values | Default | Notes |
|
|
104
105
|
|---|---|---|---|
|
|
105
106
|
| `filters.customRules` | array of rule objects | `[]` | Extra detection rules: `{ id, type, pattern, flags?, confidence? }`. Patterns are ReDoS-screened (≤500 chars, no nested quantifiers, no backreferences) and rejected at load if unsafe. |
|
|
106
|
-
| `filters.minConfidence` | number in `[0, 1]` | `0` | Precision dial. Each rule carries a `confidence` (0.6–0.95); a detection whose confidence is **below** this threshold is dropped before the policy decides. `0` (the default) gates nothing, preserving prior behavior. **Hard-block exemption:** a hard-block type (`secret`, `api_key`, `kr_rrn`, `card`) is **never** dropped on confidence — `minConfidence` trims only the precision-risky soft types (e.g. `phone`, `email`, `injection`), so a low-confidence credential/PII leak is still acted on (fail-closed). |
|
|
107
|
-
| `filters.allowlist` | array of strings and/or `{ value?, path? }` | `[]` | Operator false-positive exceptions. A detection whose matched **value** equals a string/`value` entry, or whose PII-safe JSON **path** (the hashed `pathText`, as shown in the audit) equals a `path` entry, is suppressed before the policy decides (when an entry sets both `value` and `path`, **both** must match). **Hard-block exemption:** an entry that would suppress a hard-block type (`secret`/`api_key`/`kr_rrn`/`card`) is **ignored** and the detection still fires — the allowlist can only clear a benign **soft-
|
|
107
|
+
| `filters.minConfidence` | number in `[0, 1]` | `0` | Precision dial. Each rule carries a `confidence` (0.6–0.95); a detection whose confidence is **below** this threshold is dropped before the policy decides. `0` (the default) gates nothing, preserving prior behavior. **Hard-block exemption:** a hard-block type (`secret`, `api_key`, `kr_rrn`, `card`, and the strong-anchored national IDs `fr_nir`, `es_dni`, `it_codice_fiscale`, `sg_nric`) is **never** dropped on confidence — `minConfidence` trims only the precision-risky soft/dial-eligible types (e.g. `phone`, `email`, `jp_mynumber`, `uk_nino`, `in_aadhaar`, `de_steuer_id`, `nl_bsn`, `injection`), so a low-confidence credential/PII leak is still acted on (fail-closed). |
|
|
108
|
+
| `filters.allowlist` | array of strings and/or `{ value?, path? }` | `[]` | Operator false-positive exceptions. A detection whose matched **value** equals a string/`value` entry, or whose PII-safe JSON **path** (the hashed `pathText`, as shown in the audit) equals a `path` entry, is suppressed before the policy decides (when an entry sets both `value` and `path`, **both** must match). **Hard-block exemption:** an entry that would suppress a hard-block type (`secret`/`api_key`/`kr_rrn`/`card`/`fr_nir`/`es_dni`/`it_codice_fiscale`/`sg_nric`) is **ignored** and the detection still fires — the allowlist can only clear a benign **soft / dial-eligible** FP (e.g. a `jp_mynumber`/`in_aadhaar` 12-digit-id FP, a `de_steuer_id` 11-digit-id FP, a `nl_bsn` 9-digit-id FP, or a format-only `uk_nino`), never silence a credential/strong-anchored-PII leak. Every suppression and every `minConfidence` drop is **audited** by count and type (`summary.suppressedByType` / `summary.droppedByType` / `suppressedCount` / `droppedCount`) — never the raw value. Use this to clear one benign FP without deleting a whole rule. |
|
|
109
|
+
| `filters.decodeAndRescan` | boolean | `false` | Opt-in base64/percent **decode-and-rescan** (the WS2d residual). With the default `false`, detection is byte-identical to before — a base64- or percent-encoded value is **not** decoded. When `true`, after the normal NFKC scan a string leaf that **looks** base64/base64url (anchored alphabet, valid length, `16…8192` bytes, round-trips to the same leaf, decodes to **valid UTF-8**) or contains a `%XX` escape (`decodeURIComponent`) is decoded and rescanned with the same rules + validators. A decoded hit has no offset in the encoded leaf, so it fails closed to a **whole-leaf** detection (`start:0,end:leaf.length`, value = the whole encoded leaf) — the transform redacts/blocks the entire leaf. **Precision guard:** a decoded hit fires **only** when it is validator-backed or a hard-block type (a Luhn-passing `card`, a checksum `kr_rrn`/`us_ssn`, an IBAN mod-97, or a `secret`/`api_key` on its anchored rule); a decoded soft-type-without-validator match (a bare phone-shaped run) does not fire, so random base64 does not false-positive. No new runtime dependency (`node:buffer` Buffer + the `decodeURIComponent` builtin). Other encodings (gzip/hex/nested/custom-alphabet) stay out of scope. |
|
|
108
110
|
|
|
109
111
|
### Detection benchmark
|
|
110
112
|
|
|
@@ -147,7 +149,7 @@ npm run scan:detection # CI regression gate: fail if any type regresses below
|
|
|
147
149
|
|
|
148
150
|
| Key | Type / values | Default | Notes |
|
|
149
151
|
|---|---|---|---|
|
|
150
|
-
| `privacy.profile` | `null` \| `kr-pipa` \| `eu-gdpr` \| `us-general` | `null` | Applies a regional baseline action set before enforcement. Profiles may **strengthen** but never weaken your explicit actions. Engineering defaults, not legal advice. |
|
|
152
|
+
| `privacy.profile` | `null` \| `kr-pipa` \| `eu-gdpr` \| `asia-pdpa` \| `us-general` \| `jp-appi` | `null` | Applies a regional baseline action set before enforcement. Profiles may **strengthen** but never weaken your explicit actions. `eu-gdpr` blocks the EU national IDs (`fr_nir`/`es_dni`/`uk_nino`/`it_codice_fiscale`/`de_steuer_id`/`nl_bsn`); `asia-pdpa` (Singapore PDPA / India DPDP) blocks `sg_nric`/`in_aadhaar` (plus the other checksummed national IDs for mixed-region payloads); `jp-appi` blocks `jp_mynumber`; every profile blocks `jp_mynumber` (a checksummed national-ID leak). Engineering defaults, not legal advice. |
|
|
151
153
|
|
|
152
154
|
## `logging`
|
|
153
155
|
|
|
@@ -273,13 +275,13 @@ The rate limiter is an **injectable collaborator**, supplied programmatically th
|
|
|
273
275
|
const runtime = createRuntime(config, { rateLimiter });
|
|
274
276
|
```
|
|
275
277
|
|
|
276
|
-
An injected `rateLimiter` must implement `allow(key, limit)
|
|
278
|
+
An injected `rateLimiter` must implement `allow(key, limit)` returning either a `boolean` **or** a `Promise<boolean>` (where `key` is the per-identity bucket and `limit` is the resolved `requestsPerMinute`); `createRuntime` fails closed at construction if it does not. The proxy `await`s the result, so a synchronous boolean and an async shared-store limiter behave identically — the built-in default stays synchronous, while a Redis-backed limiter that resolves asynchronously gates correctly. The proxy consults `runtime.rateLimiter` for every rate-governed request.
|
|
277
279
|
|
|
278
|
-
The **default** is a per-process, in-memory fixed-window counter: it resets on restart and is **not shared across replicas**, so total throughput multiplies by the replica count behind a load balancer. Its window map is self-bounding (a lazy, amortized sweep evicts aged-out one-shot identities — no background timer). For a multi-replica deployment, enforce a per-identity limit at a shared front door **or** inject a shared-store implementation (e.g. Redis-backed) that satisfies the same `allow(key, limit)` contract. See [Shared responsibility §4](./shared-responsibility.md#4-horizontal-scale--multiple-replicas).
|
|
280
|
+
The **default** is a per-process, in-memory fixed-window counter: it resets on restart and is **not shared across replicas**, so total throughput multiplies by the replica count behind a load balancer. Its window map is self-bounding (a lazy, amortized sweep evicts aged-out one-shot identities — no background timer). For a multi-replica deployment, enforce a per-identity limit at a shared front door **or** inject a shared-store implementation (e.g. Redis-backed) that satisfies the same `allow(key, limit)` contract — the [`haechi-ratelimit-redis`](./shared-responsibility.md#4-horizontal-scale--multiple-replicas) satellite is the reference implementation. See [Shared responsibility §4](./shared-responsibility.md#4-horizontal-scale--multiple-replicas).
|
|
279
281
|
|
|
280
282
|
## Detection types & actions
|
|
281
283
|
|
|
282
|
-
Built-in detection `type` values: `email`, `phone`, `kr_rrn`, `card`, `api_key`, `secret`, `us_ssn`, `iban`, and `injection` (response-direction heuristic, report-only by default). Custom rules may introduce new types.
|
|
284
|
+
Built-in detection `type` values: `email`, `phone`, `kr_rrn`, `card`, `api_key`, `secret`, `us_ssn`, `iban`, `jp_mynumber`, `fr_nir`, `es_dni`, `uk_nino`, `it_codice_fiscale`, `sg_nric`, `in_aadhaar`, `de_steuer_id`, `nl_bsn`, and `injection` (response-direction heuristic, report-only by default). Custom rules may introduce new types.
|
|
283
285
|
|
|
284
286
|
### Supported credential & PII matrix
|
|
285
287
|
|
|
@@ -295,11 +297,26 @@ Detection is regex + optional validator (no ML). Every rule is **anchored tightl
|
|
|
295
297
|
| `card` | Payment card (PAN) | Luhn validator, 13–19 digits | — |
|
|
296
298
|
| `us_ssn` | US Social Security Number | `AAA-GG-SSSS` + SSA-range validator (rejects area `000`/`666`/`900-999`, group `00`, serial `0000`) | Separators required; a bare 9-digit id is not an SSN. |
|
|
297
299
|
| `iban` | International Bank Account Number | **mod-97 checksum** validator | The checksum is the precision guard — IBAN-shaped non-97-valid strings are rejected. |
|
|
298
|
-
| `
|
|
300
|
+
| `jp_mynumber` | Japan My Number (個人番号) | 12 digits + **mod-11 weighted check digit** | The check digit is the precision guard; a wrong-check 12-digit run is rejected. **Hard-block.** |
|
|
301
|
+
| `fr_nir` | France NIR / INSEE social-security | 15 chars + **`97 - (first13 mod 97)` control key** (Corsica `2A`→19, `2B`→18) | A wrong control key is rejected. **Hard-block.** |
|
|
302
|
+
| `es_dni` | Spain DNI / NIE | 8 digits (DNI) or `X/Y/Z`+7 digits (NIE) + **mod-23 check letter** (NIE `X/Y/Z`→`0/1/2`) | A wrong check letter is rejected. **Hard-block.** |
|
|
303
|
+
| `uk_nino` | UK National Insurance Number | `[A-CEGHJ-PR-TW-Z][A-CEGHJ-NPR-TW-Z]\d{6}[A-D]` + documented invalid-prefix exclusions (`BG`/`GB`/`NK`/`KN`/`TN`/`NT`/`ZZ`, `O`-as-2nd-letter) | **Format-only — no checksum exists**, so it is NOT a hard-block type (dial-eligible: an operator can allowlist a benign FP). |
|
|
304
|
+
| `it_codice_fiscale` | Italy codice fiscale | `[A-Z]{6}\d{2}[A-Z]\d{2}[A-Z]\d{3}[A-Z]` + **mod-26 check character** (odd/even position tables over the first 15 chars) | A wrong check character is rejected. **Hard-block** — a rare 16-char mixed alpha+digit shape with a non-numeric structural anchor (measured ~3.8% collision over the shape). |
|
|
305
|
+
| `sg_nric` | Singapore NRIC / FIN | `[STFGM]\d{7}[A-Z]` + **weighted-sum check letter** (weights 2,7,6,5,4,3,2; per-prefix offset; per-series letter table) | A wrong check letter is rejected. **Hard-block** — two non-numeric anchors (prefix letter + check letter) over a rare shape (measured ~3.9% collision). |
|
|
306
|
+
| `in_aadhaar` | India Aadhaar | 12 digits (never starting `0`/`1`) + **Verhoeff checksum** | A wrong Verhoeff check digit is rejected. **NOT a hard-block type (dial-eligible)** — Verhoeff over the common 12-digit shape passes ~1/10 of random runs (measured ~9.9%, the `jp_mynumber` footgun), so an operator can allowlist a benign 12-digit-id FP. |
|
|
307
|
+
| `de_steuer_id` | Germany tax ID (Steuer-ID) | 11 digits + **ISO 7064 MOD 11,10** check digit + an "exactly one repeated digit in the first 10" structural test | A wrong check digit or wrong repeat structure is rejected. **NOT a hard-block type (dial-eligible)** — a bare 11-digit run with no non-numeric anchor over a common length (measured ~0.37% collision, but the bare-digit shape keeps it allowlist-clearable per the `jp_mynumber` discipline). |
|
|
308
|
+
| `nl_bsn` | Netherlands BSN | 9 digits + the **"11-proef"** weighted mod-11 | A run that fails the 11-proef is rejected. **NOT a hard-block type (dial-eligible)** — 9 bare digits is very common and the 11-proef passes ~1/11 of random runs (measured ~9.1%), the clearest dial-eligible case. |
|
|
309
|
+
| `api_key` | OpenAI-style / Stripe (`sk_`/`rk_`/`pk_`) | prefix + ≥24 chars | Underscore form — covers Stripe `sk_live_`/`rk_live_`/`sk_test_`/`rk_test_`. |
|
|
299
310
|
| `api_key` | AWS access key id | `AKIA`/`ASIA` + exactly 16 uppercase-alnum | — |
|
|
300
311
|
| `api_key` | Google API key | `AIza` + 35 URL-safe chars | — |
|
|
312
|
+
| `api_key` | SendGrid API key | `SG.` + 22 URL-safe + `.` + 43 URL-safe | The two fixed-length dotted segments are the anchor. |
|
|
313
|
+
| `api_key` | Twilio Account/API SID | `AC`/`SK` + exactly 32 **hex** | Hex-only body rejects random base62; the bare 32-hex AUTH TOKEN is caught via the assignment form (`auth_token`). |
|
|
314
|
+
| `secret` | OpenAI API key | `sk-` (and `sk-proj-`) + ≥20 base62-ish chars | **Hyphen** form, distinct from the underscore Stripe `sk_`; the two prefixes never overlap. |
|
|
315
|
+
| `secret` | Anthropic API key | `sk-ant-` + ≥16 chars | Stricter sibling of the OpenAI `sk-` rule (runs first for attribution). |
|
|
316
|
+
| `secret` | Google OAuth client secret | `GOCSPX-` + exactly 28 URL-safe chars | Distinct from the `AIza` API key. |
|
|
317
|
+
| `secret` | npm token | `npm_` + exactly 36 base62 chars | — |
|
|
301
318
|
| `secret` | `Bearer <token>` | `Bearer` + ≥16 chars | — |
|
|
302
|
-
| `secret` | Assignment `<key> = <value>` | key vocabulary: `api_key`, `api_secret`, `secret`, `secret_key`, `aws_secret_access_key`, `client_secret`, `private_key`, `access_token`, `refresh_token`, `token`, `password` | Catches bare-base64 secrets (
|
|
319
|
+
| `secret` | Assignment `<key> = <value>` | key vocabulary: `api_key`, `api_secret`, `secret`, `secret_key`, `aws_secret_access_key`, `client_secret`, `private_key`, `access_token`, `refresh_token`, `auth_token`, `accountkey`, `token`, `password` | Catches bare-base64 secrets (AWS secret access key, **Azure Storage `AccountKey=`**, **Twilio auth token**) via the assignment form — an un-anchored 88-char-base64 Azure rule would false-fire on any blob, so `AccountKey=` context is the anchor. |
|
|
303
320
|
| `secret` | GitHub token | `gh[pousr]_` + ≥36 base64-ish chars | pat/oauth/user/server/refresh variants. |
|
|
304
321
|
| `secret` | Slack token | `xox[baprs]-` + ≥10-char body | bot/user/refresh/legacy variants. |
|
|
305
322
|
| `secret` | JWT | three base64url segments, first starts `eyJ` (the base64 of `{"`) | The `eyJ` anchor rejects arbitrary dotted tokens. |
|
|
@@ -390,7 +407,7 @@ With `trustForwardedProto: true`, Haechi **refuses any request whose `X-Forwarde
|
|
|
390
407
|
|
|
391
408
|
## Validation cheatsheet
|
|
392
409
|
|
|
393
|
-
These throw at load (fail-closed): unknown `keys.provider`; empty `proxy.host`; out-of-range `proxy.port`; non-boolean `proxy.trustForwardedProto`; a `proxy.tls` that is non-`null` but not an object, sets `keyFile` without `certFile` (or vice-versa), mixes `pfxFile` with `keyFile`/`certFile`, names an unreadable file, or does not resolve to usable material `((key && cert) or pfx)`; non-`jsonl` `audit.sink`; non-`local` `tokenVault.provider`; bad `revealPolicy`; non-positive `retentionDays`; non-boolean `deterministic`/`detokenizeResponses`; empty/non-string `deterministicTypes`; empty/non-string `mcp.allowedMethods`; non-boolean `mcp.*` flags; unknown `privacy.profile`; bad `responseProtection.failureMode`; non-positive `responseProtection.maxBytes`; non-boolean `responseProtection.scanNumbers`; bad `streaming.requestMode`/`streaming.responseMode`; non-positive `streaming.maxMatchBytes`; bad `auth.provider`; empty `auth.store`; non-string `auth.allowedLabelKeys`; non-object `policy.profiles`; `policy.profileBinding` without a valid `default`; non-string `policy.modelAllowlist`; non-positive `policy.rate.requestsPerMinute`; non-positive `limits.maxRequestBytes`/`limits.upstreamTimeoutMs`/`limits.maxNestingDepth`; negative or non-integer `limits.maxInFlight`/`limits.shutdownGraceMs`; non-`null`/negative/non-integer `limits.requestTimeoutMs`/`limits.headersTimeoutMs`; non-positive-integer or **newer-than-supported** `configVersion`; unknown `target.type`/`adapter`; unsafe custom regex; weakening action without `allowUnsafeOverrides`; non-`text`/`json` `logging.format`; non-boolean `metrics.enabled`; an invalid `HAECHI_*` env overlay value (bad `HAECHI_PROXY_PORT`, unknown `HAECHI_MODE`, malformed `HAECHI_UPSTREAM`, …).
|
|
410
|
+
These throw at load (fail-closed): unknown `keys.provider`; empty `proxy.host`; out-of-range `proxy.port`; non-boolean `proxy.trustForwardedProto`; a `proxy.tls` that is non-`null` but not an object, sets `keyFile` without `certFile` (or vice-versa), mixes `pfxFile` with `keyFile`/`certFile`, names an unreadable file, or does not resolve to usable material `((key && cert) or pfx)`; non-`jsonl` `audit.sink`; non-`local` `tokenVault.provider`; bad `revealPolicy`; non-positive `retentionDays`; non-boolean `deterministic`/`detokenizeResponses`; empty/non-string `deterministicTypes`; empty/non-string `mcp.allowedMethods`; non-boolean `mcp.*` flags; unknown `privacy.profile`; bad `responseProtection.failureMode`; non-positive `responseProtection.maxBytes`; non-boolean `responseProtection.scanNumbers`; bad `streaming.requestMode`/`streaming.responseMode`; non-positive `streaming.maxMatchBytes`; bad `auth.provider`; empty `auth.store`; non-string `auth.allowedLabelKeys`; non-object `policy.profiles`; `policy.profileBinding` without a valid `default`; non-string `policy.modelAllowlist`; non-positive `policy.rate.requestsPerMinute`; non-positive `limits.maxRequestBytes`/`limits.upstreamTimeoutMs`/`limits.maxNestingDepth`; negative or non-integer `limits.maxInFlight`/`limits.shutdownGraceMs`; non-`null`/negative/non-integer `limits.requestTimeoutMs`/`limits.headersTimeoutMs`; non-positive-integer or **newer-than-supported** `configVersion`; unknown `target.type`/`adapter`; a `target.forwardHeaders` that is not an array of lowercase non-empty strings or that names an always-dropped credential/hop-by-hop header; unsafe custom regex; weakening action without `allowUnsafeOverrides`; non-`text`/`json` `logging.format`; non-boolean `metrics.enabled`; an invalid `HAECHI_*` env overlay value (bad `HAECHI_PROXY_PORT`, unknown `HAECHI_MODE`, malformed `HAECHI_UPSTREAM`, …).
|
|
394
411
|
|
|
395
412
|
# Satellite operator configuration (0.9)
|
|
396
413
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Haechi 운영 런북 (Day-2)
|
|
2
2
|
|
|
3
|
-
- 상태: Living document (코어 1.
|
|
3
|
+
- 상태: Living document (코어 1.3.x 추적)
|
|
4
4
|
|
|
5
5
|
Haechi를 프로덕션에서 운영하기 위한 실무 가이드입니다: 배포, 환경변수 오버레이를 통한 설정, health/readiness/metrics 모니터링, 우아한 종료, 백프레셔 튜닝, 그리고 해시 체인을 깨지 않는 audit 로그 회전입니다.
|
|
6
6
|
|
|
@@ -107,7 +107,40 @@ audit 로그는 **SHA-256 해시 체인**입니다(`audit.path`): 각 이벤트
|
|
|
107
107
|
|
|
108
108
|
아카이브 파이프라인에 검증 단계를 유지하지 않는 한, 나중에 재검증이 불가능한 방식으로 세그먼트를 압축/암호화하지 **마십시오**. 회전된 세그먼트는 여전히 검증될 때에만 증거로서 유용합니다.
|
|
109
109
|
|
|
110
|
-
## 7.
|
|
110
|
+
## 7. 프록시 처리량 벤치마크
|
|
111
|
+
|
|
112
|
+
`npm run bench:throughput`(`scripts/bench-throughput.mjs`)는 동시성 부하에서
|
|
113
|
+
프록시가 더하는 요청당 오버헤드를 측정합니다. 결정적인 로컬 **스텁**
|
|
114
|
+
OpenAI 호환 업스트림(즉시 응답하는 정해진 답변 — 실제 모델 없음)과 그 앞단의
|
|
115
|
+
**실제** Haechi 프록시를 세우고, 고정 크기 워커 풀의 동시 `fetch`로 부하를
|
|
116
|
+
구동하여 **req/s**와 **p50/p95/p99/max** 지연(정렬된 표본에 대한 nearest-rank
|
|
117
|
+
백분위수)을 보고합니다. 세 가지 시나리오를 실행합니다:
|
|
118
|
+
|
|
119
|
+
1. 고정 동시성에서의 **처리량 + 지연**(워밍업 배치는 보고 통계에서 제외합니다 —
|
|
120
|
+
JIT/연결 워밍업이 초기 요청을 왜곡하기 때문입니다),
|
|
121
|
+
2. **enforce 대 dry-run 오버헤드** — 동일한 부하를 두 모드로 실행하여 지연/처리량
|
|
122
|
+
**델타**를 보고하므로, 보호 비용이 추측이 아닌 측정된 수치가 됩니다,
|
|
123
|
+
3. **백프레셔** — 낮은 `limits.maxInFlight`를 버스트로 포화시켜 `503 + Retry-After`와
|
|
124
|
+
`200`의 비율을 보고합니다(실제 응답을 관찰하여 천장이 부하를 흘려보냄을 증명).
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
npm run bench:throughput
|
|
128
|
+
HAECHI_BENCH_REQUESTS=5000 HAECHI_BENCH_CONCURRENCY=64 npm run bench:throughput
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
노브(env, 매 실행 상단에 출력됨): `HAECHI_BENCH_REQUESTS`(총 요청 수, 기본 2000),
|
|
132
|
+
`HAECHI_BENCH_CONCURRENCY`(기본 32), `HAECHI_BENCH_WARMUP`(제외할 워밍업 수, 기본
|
|
133
|
+
100), `HAECHI_BENCH_PAYLOAD_KB`(기본 1), `HAECHI_BENCH_MAXINFLIGHT`(백프레셔
|
|
134
|
+
시나리오의 천장, 기본 4).
|
|
135
|
+
|
|
136
|
+
> **수치는 머신 상대적입니다.** 이것은 **루프백, 단일 프로세스, 스텁 업스트림
|
|
137
|
+
> 마이크로 벤치마크**입니다: 스텁, 프록시, 부하 생성기가 모두 `127.0.0.1`의 한
|
|
138
|
+
> Node 프로세스에서 실행되므로 실제 네트워크도 실제 모델도 없습니다. 수치는 오직
|
|
139
|
+
> Haechi가 더하는 오버헤드만 측정하며 머신·Node 버전·부하에 따라 달라집니다.
|
|
140
|
+
> 네트워크/하드웨어 처리량 벤치마크가 **아니며** 보장 수치로 인용해서는 **안
|
|
141
|
+
> 됩니다**. 이 벤치는 `release:preflight`에서 실행되지 않습니다.
|
|
142
|
+
|
|
143
|
+
## 8. 빠른 참조
|
|
111
144
|
|
|
112
145
|
| 작업 | 커맨드 |
|
|
113
146
|
|---|---|
|
|
@@ -115,6 +148,7 @@ audit 로그는 **SHA-256 해시 체인**입니다(`audit.path`): 각 이벤트
|
|
|
115
148
|
| Liveness | `curl localhost:11016/__haechi/live` |
|
|
116
149
|
| Readiness | `curl localhost:11016/__haechi/ready` |
|
|
117
150
|
| Metrics | `curl localhost:11016/__haechi/metrics` |
|
|
151
|
+
| 처리량 벤치 | `npm run bench:throughput` |
|
|
118
152
|
| 세그먼트 검증 | `haechi audit-verify --audit <seg>.jsonl --anchor <seg>.anchor.jsonl` |
|
|
119
153
|
| 우아한 정지 | `docker compose stop` (SIGTERM → 드레인) |
|
|
120
154
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Haechi Operations Runbook (Day-2)
|
|
2
2
|
|
|
3
|
-
- Status: Living document (tracks core 1.
|
|
3
|
+
- Status: Living document (tracks core 1.3.x)
|
|
4
4
|
|
|
5
5
|
A practical guide to running Haechi in production: deploy, configure via the
|
|
6
6
|
env-var overlay, monitor with health/readiness/metrics, shut down gracefully,
|
|
@@ -189,7 +189,43 @@ rotation does not purge tokens.
|
|
|
189
189
|
re-verification unless you keep the verification step in your archival pipeline. A
|
|
190
190
|
rotated segment is only useful as evidence if it still verifies.
|
|
191
191
|
|
|
192
|
-
## 7.
|
|
192
|
+
## 7. Benchmarking proxy throughput
|
|
193
|
+
|
|
194
|
+
`npm run bench:throughput` (`scripts/bench-throughput.mjs`) measures the proxy's
|
|
195
|
+
added per-request overhead under concurrency. It stands up a deterministic local
|
|
196
|
+
**stub** OpenAI-compatible upstream (an instant canned reply — no real model) and
|
|
197
|
+
the **real** Haechi proxy in front of it, drives a configurable load with a
|
|
198
|
+
fixed-size worker pool of in-flight `fetch`es, and reports **req/s** plus
|
|
199
|
+
**p50/p95/p99/max** latency (percentiles by nearest-rank over a sorted sample). It
|
|
200
|
+
runs three scenarios:
|
|
201
|
+
|
|
202
|
+
1. **throughput + latency** at a fixed concurrency (a warmup batch is excluded
|
|
203
|
+
from the reported stats — JIT/connection warmup skews the first requests),
|
|
204
|
+
2. **enforce vs dry-run overhead** — the same load run in both modes, reporting
|
|
205
|
+
the latency/throughput **delta** so the cost of protection is a measured number,
|
|
206
|
+
3. **backpressure** — a low `limits.maxInFlight` saturated by a burst, reporting
|
|
207
|
+
how many requests got `503 + Retry-After` vs `200` (observed live, proving the
|
|
208
|
+
ceiling sheds load).
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
npm run bench:throughput
|
|
212
|
+
HAECHI_BENCH_REQUESTS=5000 HAECHI_BENCH_CONCURRENCY=64 npm run bench:throughput
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Knobs (env, printed at the top of every run): `HAECHI_BENCH_REQUESTS` (total,
|
|
216
|
+
default 2000), `HAECHI_BENCH_CONCURRENCY` (default 32), `HAECHI_BENCH_WARMUP`
|
|
217
|
+
(excluded warmup count, default 100), `HAECHI_BENCH_PAYLOAD_KB` (default 1),
|
|
218
|
+
`HAECHI_BENCH_MAXINFLIGHT` (the backpressure scenario's ceiling, default 4).
|
|
219
|
+
|
|
220
|
+
> **The numbers are machine-relative.** This is a **loopback, single-process,
|
|
221
|
+
> stub-upstream micro-benchmark**: the stub, the proxy, and the load generator all
|
|
222
|
+
> run in one Node process on `127.0.0.1`, so there is no real network and no real
|
|
223
|
+
> model. The numbers measure Haechi's added overhead only, and vary by machine,
|
|
224
|
+
> Node version, and load. They are **not** a network/hardware throughput benchmark
|
|
225
|
+
> and must **not** be quoted as guarantees. The bench is not run by
|
|
226
|
+
> `release:preflight`.
|
|
227
|
+
|
|
228
|
+
## 8. Quick reference
|
|
193
229
|
|
|
194
230
|
| Task | Command |
|
|
195
231
|
|---|---|
|
|
@@ -197,6 +233,7 @@ rotated segment is only useful as evidence if it still verifies.
|
|
|
197
233
|
| Liveness | `curl localhost:11016/__haechi/live` |
|
|
198
234
|
| Readiness | `curl localhost:11016/__haechi/ready` |
|
|
199
235
|
| Metrics | `curl localhost:11016/__haechi/metrics` |
|
|
236
|
+
| Throughput bench | `npm run bench:throughput` |
|
|
200
237
|
| Verify a segment | `haechi audit-verify --audit <seg>.jsonl --anchor <seg>.anchor.jsonl` |
|
|
201
238
|
| Graceful stop | `docker compose stop` (SIGTERM → drain) |
|
|
202
239
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Haechi Release Process
|
|
2
2
|
|
|
3
|
-
- 문서 상태: Living document (core 1.
|
|
3
|
+
- 문서 상태: Living document (core 1.3.x 추적)
|
|
4
4
|
- 작성일: 2026-06-10
|
|
5
5
|
|
|
6
6
|
## 1. 로컬 릴리즈 검증
|
|
@@ -70,6 +70,7 @@ npm audit signatures
|
|
|
70
70
|
| `.github/workflows/auth-jwt-publish.yml` | `haechi-auth-jwt` | `auth-jwt-v<semver>` | satellite publish, 동일한 서명 아티팩트 경로 |
|
|
71
71
|
| `.github/workflows/dashboard-publish.yml` | `haechi-dashboard` | `dashboard-v<semver>` | satellite publish, 동일한 서명 아티팩트 경로 |
|
|
72
72
|
| `.github/workflows/auth-oidc-publish.yml` | `haechi-auth-oidc` | `auth-oidc-v<semver>` | satellite publish, 동일한 서명 아티팩트 경로 |
|
|
73
|
+
| `.github/workflows/ratelimit-redis-publish.yml` | `haechi-ratelimit-redis` | `ratelimit-redis-v<semver>` | satellite publish, 동일한 서명 아티팩트 경로 |
|
|
73
74
|
|
|
74
75
|
각 publish 워크플로는 `release: published`에서 트리거되지만 **가드**되어 둘이 교차 발화하지 않습니다. core job은 `v`로 시작하는 태그에서만 실행되고(그리고 `^v[0-9]+\.[0-9]+\.[0-9]+$` 재검증), satellite job은 `crypto-kms-v…`에서만 실행됩니다(그리고 `^crypto-kms-v[0-9]+\.[0-9]+\.[0-9]+$` 재검증 **및** 태그 버전이 satellite `package.json` 버전과 일치하는지 검증). npmjs.com Trusted Publisher는 각 패키지의 **특정 워크플로 파일명**에 바인딩됩니다 — 워크플로 파일 rename은 npm 설정을 갱신할 때까지 OIDC publish를 깨뜨립니다.
|
|
75
76
|
|
|
@@ -92,6 +93,7 @@ Satellite는 npm workspaces 모노레포의 `satellites/*`에 살며 core와 **
|
|
|
92
93
|
| `haechi-auth-jwt` | `auth-jwt-v<semver>` | `auth-jwt-publish.yml` | `satellites/auth-jwt/package.json` |
|
|
93
94
|
| `haechi-dashboard` | `dashboard-v<semver>` | `dashboard-publish.yml` | `satellites/dashboard/package.json` |
|
|
94
95
|
| `haechi-auth-oidc` | `auth-oidc-v<semver>` | `auth-oidc-publish.yml` | `satellites/auth-oidc/package.json` |
|
|
96
|
+
| `haechi-ratelimit-redis` | `ratelimit-redis-v<semver>` | `ratelimit-redis-publish.yml` | `satellites/ratelimit-redis/package.json` |
|
|
95
97
|
|
|
96
98
|
**satellite 릴리스 검증** (core와 동일한 신뢰 앵커):
|
|
97
99
|
|
|
@@ -104,6 +106,8 @@ npm view haechi-crypto-kms --json # dist.attestations 존재 확인; access "p
|
|
|
104
106
|
|
|
105
107
|
**0.9 satellite(새 unscoped 이름 — 첫 태그 *전에* Trusted Publisher 설정):** `haechi-dashboard`와 `haechi-auth-oidc`는 0.9에서 첫 발행되며 위의 satellite별 부트스트랩 순서를 동일하게 따릅니다. 0.8 satellite와 마찬가지로 unscoped 이름은 첫 OIDC publish 시 확보되므로, 각각의 npmjs.com Trusted Publisher를 첫 태그 **전에** 설정해야 합니다 — `raeseoklee/haechi` 저장소와 정확한 워크플로 파일명(`haechi-dashboard`는 `dashboard-publish.yml`, `haechi-auth-oidc`는 `auth-oidc-publish.yml`)을 연결한 뒤, 접두사 태그(`dashboard-v0.1.0`, `auth-oidc-v0.1.0`)를 push하고 GitHub Release를 발행합니다. 기존 두 satellite는 이미 부트스트랩된 태그/워크플로를 그대로 사용합니다: `haechi-auth-jwt@0.2.0`은 `auth-jwt-v<semver>`(`auth-jwt-publish.yml`), `haechi-crypto-kms@0.2.0`은 `crypto-kms-v<semver>`(`crypto-kms-publish.yml`) — 이 둘은 새 Trusted Publisher 설정이 필요 없습니다.
|
|
106
108
|
|
|
109
|
+
**`haechi-ratelimit-redis`(새 unscoped 이름 — 첫 태그 *전에* Trusted Publisher 설정):** 공유 저장소 rate-limiter satellite는 고유의 `ratelimit-redis-v<semver>` 태그에서 첫 발행되며 위의 satellite별 부트스트랩 순서를 동일하게 따릅니다. unscoped 이름은 첫 OIDC publish 시 확보되므로, npmjs.com Trusted Publisher를 첫 태그 **전에** 설정해야 합니다 — `raeseoklee/haechi` 저장소와 정확한 워크플로 파일명 `ratelimit-redis-publish.yml`을 연결한 뒤, 접두사 태그(`ratelimit-redis-v0.1.0`)를 push하고 GitHub Release를 발행합니다. `redis` 클라이언트는 **optional peer dependency**이며 번들된 Redis 어댑터를 쓰는 소비자만 import합니다(store/client는 주입됩니다). 따라서 core는 zero-dependency로 유지됩니다.
|
|
110
|
+
|
|
107
111
|
## 6. 배포 차단 조건
|
|
108
112
|
|
|
109
113
|
다음 중 하나라도 실패하면 npm publish를 하지 않습니다.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Haechi Release Process
|
|
2
2
|
|
|
3
|
-
- Status: Living document (tracks core 1.
|
|
3
|
+
- Status: Living document (tracks core 1.3.x)
|
|
4
4
|
- Date: 2026-06-10
|
|
5
5
|
|
|
6
6
|
## 1. Local Release Verification
|
|
@@ -70,6 +70,7 @@ npm audit signatures
|
|
|
70
70
|
| `.github/workflows/auth-jwt-publish.yml` | `haechi-auth-jwt` | `auth-jwt-v<semver>` | satellite publish, same signed-artifacts path |
|
|
71
71
|
| `.github/workflows/dashboard-publish.yml` | `haechi-dashboard` | `dashboard-v<semver>` | satellite publish, same signed-artifacts path |
|
|
72
72
|
| `.github/workflows/auth-oidc-publish.yml` | `haechi-auth-oidc` | `auth-oidc-v<semver>` | satellite publish, same signed-artifacts path |
|
|
73
|
+
| `.github/workflows/ratelimit-redis-publish.yml` | `haechi-ratelimit-redis` | `ratelimit-redis-v<semver>` | satellite publish, same signed-artifacts path |
|
|
73
74
|
|
|
74
75
|
Each publish workflow triggers on `release: published` but is **guarded** so the two never cross-fire: the core job runs only for tags starting `v` (and re-validates `^v[0-9]+\.[0-9]+\.[0-9]+$`); the satellite job runs only for `crypto-kms-v…` (and re-validates `^crypto-kms-v[0-9]+\.[0-9]+\.[0-9]+$` **and** that the tag version equals the satellite's `package.json` version). The npmjs.com Trusted Publisher for each package is bound to its **specific workflow filename** — renaming a workflow file breaks its OIDC publish until the npm config is updated.
|
|
75
76
|
|
|
@@ -92,6 +93,7 @@ No manual `npm publish` from a laptop is needed. Because the names are unscoped
|
|
|
92
93
|
| `haechi-auth-jwt` | `auth-jwt-v<semver>` | `auth-jwt-publish.yml` | `satellites/auth-jwt/package.json` |
|
|
93
94
|
| `haechi-dashboard` | `dashboard-v<semver>` | `dashboard-publish.yml` | `satellites/dashboard/package.json` |
|
|
94
95
|
| `haechi-auth-oidc` | `auth-oidc-v<semver>` | `auth-oidc-publish.yml` | `satellites/auth-oidc/package.json` |
|
|
96
|
+
| `haechi-ratelimit-redis` | `ratelimit-redis-v<semver>` | `ratelimit-redis-publish.yml` | `satellites/ratelimit-redis/package.json` |
|
|
95
97
|
|
|
96
98
|
**Verify a satellite release** (same anchors as core):
|
|
97
99
|
|
|
@@ -104,6 +106,8 @@ npm view haechi-crypto-kms --json # dist.attestations present; access "public"
|
|
|
104
106
|
|
|
105
107
|
**0.9 satellites (new unscoped names — configure Trusted Publisher *before* the first tag):** `haechi-dashboard` and `haechi-auth-oidc` are first-published in 0.9 and follow the same per-satellite bootstrap order above. As with the 0.8 satellites, the unscoped name is claimed on first OIDC publish, so the npmjs.com Trusted Publisher for each must be configured **before** its first tag — link `raeseoklee/haechi` and the exact workflow filename (`dashboard-publish.yml` for `haechi-dashboard`, `auth-oidc-publish.yml` for `haechi-auth-oidc`), then push the prefixed tag (`dashboard-v0.1.0`, `auth-oidc-v0.1.0`) and publish the GitHub Release. The two existing satellites ride their already-bootstrapped tags/workflows: `haechi-auth-jwt@0.2.0` on `auth-jwt-v<semver>` (`auth-jwt-publish.yml`) and `haechi-crypto-kms@0.2.0` on `crypto-kms-v<semver>` (`crypto-kms-publish.yml`) — no new Trusted Publisher configuration is required for those two.
|
|
106
108
|
|
|
109
|
+
**`haechi-ratelimit-redis` (new unscoped name — configure Trusted Publisher *before* the first tag):** the shared-store rate-limiter satellite is first-published from its own `ratelimit-redis-v<semver>` tag and follows the same per-satellite bootstrap order above. The unscoped name is claimed on its first OIDC publish, so its npmjs.com Trusted Publisher must be configured **before** its first tag — link `raeseoklee/haechi` and the exact workflow filename `ratelimit-redis-publish.yml`, then push the prefixed tag (`ratelimit-redis-v0.1.0`) and publish the GitHub Release. The `redis` client is an **optional peer dependency**, imported only by consumers using the bundled Redis adapter (the store/client is injected), so core stays zero-dependency.
|
|
110
|
+
|
|
107
111
|
## 6. Deployment block conditions
|
|
108
112
|
|
|
109
113
|
npm publish is not performed if any of the following fail.
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
# Haechi 리스크 레지스터 및 릴리스 게이트
|
|
2
2
|
|
|
3
|
-
- 문서 상태: Living document(core 1.
|
|
4
|
-
- 작성일: 2026-06-
|
|
5
|
-
- 기준 버전: 1.
|
|
3
|
+
- 문서 상태: Living document(core 1.3.x 추적)
|
|
4
|
+
- 작성일: 2026-06-16
|
|
5
|
+
- 기준 버전: 1.3.x
|
|
6
6
|
- 기준 브랜치: `main`
|
|
7
7
|
|
|
8
8
|
## 1. 현재 판단
|
|
9
9
|
|
|
10
|
-
Haechi는 `1.x` stable 라인을 출시했습니다. developer preview 게이트(G2, `haechi@0.3.2`)부터
|
|
10
|
+
Haechi는 `1.x` stable 라인을 출시했습니다. developer preview 게이트(G2, `haechi@0.3.2`)부터 G8(1.3.0 backend + detection coverage expansion)까지 모든 게이트가 통과되었으며, 아래 게이트 이력은 감사 추적으로 보존합니다. 1.0.0은 strict semver 하의 frozen API 계약을 선언하고(문서화된 deprecation 정책과 freeze 가드 `tests/api-contract.test.mjs` 포함), signed·sandboxed `authProvider` plugin에 한해 dynamic-loading 금지를 좁게 해제했습니다. 1.1.0은 커널 수준 capability 거부를 갖춘 opt-in `process-isolated` plugin 런타임을 추가했습니다. stable 표현을 막던 조건 — 1.0 API 안정성, 외부 `cryptoProvider`/KMS reference adapter(`haechi-crypto-kms`), stream-aware enforcement(`streaming.requestMode: "inspect"`) — 은 모두 갖춰졌습니다. Haechi는 여전히 컴플라이언스를 보장하지 않는 self-hosted 보안 toolkit이며, 운영 배포는 네트워크 접근 통제, upstream 인증, key custody를 직접 책임집니다(threat model §5 참고).
|
|
11
|
+
|
|
12
|
+
**2026-06-16 코드리뷰 보완 — `haechi@1.3.1`로 발행:** 전체 코드리뷰 결과를 `docs/current/code-review-risk-register-2026-06-16.ko.md`에 등록부로 열었습니다. 이 리뷰에서 P0 credential-boundary leak 1건, P1 릴리스 차단 이슈 4건, P2 하드닝/테스트 공백 8건이 확인됐습니다. **13개 `P*-CR-*` 항목이 모두 Resolved이며(§5.7) `haechi@1.3.1` 보완 컷(2026-06-16, attested OIDC publish)으로 발행되었습니다.** G9은 **Pass**입니다. 운영자는 수정 사항(특히 P0-CR-001 프록시 헤더 경계 패치)을 반영하려면 `haechi@1.3.0`에서 `1.3.1`로 업그레이드해야 합니다.
|
|
11
13
|
|
|
12
14
|
| 구분 | 판단 | 이유 |
|
|
13
15
|
|---|---|---|
|
|
14
16
|
| GitHub public | 허용 | 보안 한계, threat model, shared responsibility가 문서화됨 |
|
|
15
|
-
| GitHub release/tag | 허용 |
|
|
16
|
-
| npm stable |
|
|
17
|
-
| production use | 운영자
|
|
17
|
+
| GitHub release/tag | 허용 (`v1.3.1` 릴리스됨) | `v1.3.1` 보완 컷이 태깅·릴리스됨; §5.7 항목이 모두 Resolved이고 G9은 Pass |
|
|
18
|
+
| npm stable | `haechi@1.3.1` publish됨 | 코드리뷰 보완이 `haechi@1.3.1` attested OIDC publish(2026-06-16)로 발행됨; 이전 `1.3.0`은 수정 이전 동작을 담고 있음 |
|
|
19
|
+
| production use | 운영자 게이트; `1.3.1`로 업그레이드 | 운영자 네트워크 통제, 인가/인증, key custody가 있을 때만 지원; `haechi@1.3.0` 운영자는 민감한 제3자 업스트림 트래픽을 프록시로 라우팅하기 전에 프록시 헤더 경계 수정(P0-CR-001)을 반영하도록 `1.3.1`로 업그레이드해야 함 |
|
|
18
20
|
|
|
19
21
|
## 2. 릴리스 게이트
|
|
20
22
|
|
|
@@ -23,11 +25,13 @@ Haechi는 `1.x` stable 라인을 출시했습니다. developer preview 게이트
|
|
|
23
25
|
| G0 | GitHub source 공개 | 테스트 통과, 보안 한계 문서화, 평문 audit leak 없음 | Pass |
|
|
24
26
|
| G1 | GitHub pre-release | P0 코드 리스크 해결, production-ready 표현 없음 | Pass |
|
|
25
27
|
| G2 | npm developer preview | P0 해결, preflight/SBOM/provenance 경로 준비, npm auth 확인 | Pass (`haechi@0.3.2` 2026-06-10 배포) |
|
|
26
|
-
| G3 | npm stable | P1 운영 reference, stream-aware enforcement, API stability 강화 |
|
|
28
|
+
| G3 | npm stable | P1 운영 reference, stream-aware enforcement, API stability 강화 | Pass (1.0.0 stable 컷에서 달성 — streaming inspection은 0.5, API freeze는 1.0.0에서 출시; G5 참조. G5–G7로 대체됨.) |
|
|
27
29
|
| G4 | 0.9.0 observability + interactive-auth 위성 컷 | P1-SEC-026 / P1-OPS-009 mitigated 및 P2-CRYPTO-001 accepted; `haechi-dashboard` + `haechi-auth-oidc` + `haechi-crypto-kms@0.2.0` 테스트 통과; 위성 tarball zero-dep; core 0.9.0 bump(추가적 FORBIDDEN_KEYS audit 강화만) | Pass |
|
|
28
30
|
| G5 | 1.0.0 stable API contract + signed-plugin sandbox | P1-SEC-024 / P1-SEC-025 mitigated, P2-API-001 / P2-OPS-006 resolved; API freeze + deprecation policy + `tests/api-contract.test.mjs` 통과; Ed25519 signed-plugin contract + `assertAuthProviderConformance` + worker-isolated `authProvider` sandbox 테스트 통과; PR0 위성 peer-range를 `>=0.8.0 <2.0.0`로 확대 및 `check-satellite-peer-ranges.mjs` preflight 게이트 통과; core는 zero runtime dependency 유지; core 1.0.0 bump | Pass |
|
|
29
31
|
| G6 | 1.1.0 plugin capability 강제 (`process-isolated`) | P1-SEC-027 / P1-SEC-028 mitigated; `process-isolated` 런타임(`--permission` 하 자식, 부여 0, `data:` URL 로드, stdio 무시, JSON-string IPC) + fail-closed `--allow-net` 기능 탐지(`netEnforcement:"require-permission"`) + 코어 `haechi/ssrf` 가드 + 호스트 중개 키 자료 + spawn-storm 서킷 브레이커; fs/net/stdio 레드팀 + SSRF + config 테스트 통과(행동 스위트는 `--allow-net` Node에서 실행, 아니면 fail-closed로 skip); API freeze 통과 유지(additive `./ssrf` export + additive config 키); core는 zero runtime dependency 유지; core 1.1.0 bump(additive + opt-in 마이너) | Pass |
|
|
30
32
|
| G7 | 1.2.0 신뢰성 강화 트랙 (WS1–WS6) | 탐지 품질 측정+강화(WS2: 라벨 코퍼스 precision/recall `bench:detection` 게이트, 자격증명+국제 PII 커버리지, 하드블록 타입 불변식이 적용된 `filters.minConfidence` / `filters.allowlist`, offset 무결성을 갖춘 NFKC 유니코드 회피 폴딩); WS3 주입 가능한 `rateLimiter` 시임 + bounded fixed-window map; WS4 운영성(`/__haechi/live`+`/ready` 분리, 주입 가능한 `/metrics`, 구조적 로그 + 요청별 `correlationId`, graceful drain, max-in-flight backpressure, env overlay, 하드닝 Dockerfile/compose/runbook, `configVersion`); WS6 proxy TLS / remote-bind 하드닝(`proxy.tls` / `proxy.trustForwardedProto`, fail-closed `assertSafeProxyTransport`) + OWASP-LLM/NIST 컨트롤 매핑 백서 + RFC 9116 `security.txt` + 취약점 공개 경로. 모든 변경은 1.1 동작을 보존하는 기본값 뒤의 additive(`tests/api-contract.test.mjs` 통과); no-plaintext-in-audit 불변식이 텔레메트리까지 확장; core는 zero runtime dependency 유지; core 1.2.0 bump(additive 마이너) | Pass |
|
|
33
|
+
| G8 | 1.3.0 백엔드 + 탐지 커버리지 확장 | **Anthropic Messages API**(`/v1/messages`, content-block + SSE `delta.text`, `event:` 라인 보존 재직렬화)와 **Google Gemini API**(model-in-path `:generateContent`/`:streamGenerateContent`, 기존 정확-매칭 어댑터를 바이트 동일하게 두는 additive `:method`-suffix 라우트 매처) 프로토콜 어댑터 추가; 탐지 커버리지 확장 — 클라우드/SaaS provider 키(OpenAI/Anthropic/Google-OAuth/SendGrid/Twilio/npm/Azure, anchored)와 국제 PII(FR/ES/JP + IT/SG/IN/DE/NL 국가 ID, 체크섬 validator), 각 하드블록-대-dial-eligible 결정은 측정된 충돌률 기반(하드블록은 비숫자 앵커 또는 비현실적으로 드문 형태가 필요; 흔한 길이의 bare-digit run은 allowlist로 정리 가능 유지); `bench:throughput` proxy 부하 벤치; `haechi-ratelimit-redis` 공유 저장소 rate-limiter 위성(WS3 시임의 운영 소비자; proxy가 이제 `rateLimiter.allow`를 `await`); `haechi-dashboard`가 요청별 `correlationId` 노출. 모든 변경은 additive — 새 `target.type`/탐지타입/`privacy.profile` *값*이며 새 config 키가 아님(`configVersion`은 `1` 유지); `tests/api-contract.test.mjs` 통과; core는 zero runtime dependency 유지; core 1.3.0 bump(additive 마이너) | Pass |
|
|
34
|
+
| G9 | 2026-06-16 전체 코드리뷰 보완 게이트 (1.3.1로 발행) | `P0-CR-001` 및 `P1-CR-002`부터 `P1-CR-005`까지 해결 또는 책임자 명시 수용; P2 항목은 해결 또는 명시적 non-blocking 근거와 일정 기록; 연결된 등록부 갱신. **13개 `P*-CR-*` 항목이 모두 Resolved이며(§5.7) `haechi@1.3.1`(2026-06-16, attested OIDC publish)로 발행되었습니다; core가 1.3.0 → 1.3.1로 bump(patch, 보완 전용 — API/config 표면 변경 없음, `configVersion`은 `1` 유지)되었습니다.** | Pass (`haechi@1.3.1`, 2026-06-16) |
|
|
31
35
|
|
|
32
36
|
## 3. P0 배포 차단 리스크 상태
|
|
33
37
|
|
|
@@ -127,6 +131,26 @@ base64/인코딩 값 디코딩 검사, query string 검사, audit tail truncatio
|
|
|
127
131
|
| P1-SEC-027 | Plugin capability *강제*: 1.0 `worker_threads` sandbox는 memory/crash 격리뿐이라 악의적 signed plugin이 `fs`/`net`을 써서 credential을 exfiltrate할 수 있음. **P1-SEC-024의 수용된 worker 잔여를 강화** — 1.1이 새 opt-in 런타임에 실제 강제 추가 | Mitigated | `packages/plugin/process-sandbox.mjs` `createProcessIsolatedAuthProvider`/`…Sync`(PR #54): signed `authProvider`가 `--permission` 하 자식 `node`에서 **부여 0**(fs/child-process/worker/addons/wasi 없음, `--allow-net` 없음)으로, `data:` URL 로드(fs 권한 없음 → TOCTOU/symlink 표면 없음), `stdio:['ignore','ignore','ignore','ipc']`(stdout/stderr/fd 유출 채널 없음), 정화 env, JSON-string 전용 IPC + 공유 null-proto sanitizer + 호스트측 keyed-HMAC identity로 실행됩니다. **Node 26 실측 검증**: plugin의 `fs`/`net`/`fetch`/`dns`/`child_process`/`worker`와 `process.binding('tcp_wrap')` 우회가 모두 `ERR_ACCESS_DENIED`. 네트워크 봉쇄는 **커널 `--allow-net` 거부**(삭제 가능한 JS 하니스가 아님); 기본값 `netEnforcement:"require-permission"`은 강제 못 하는 Node에서 **fail closed**(동작 probe 기능 탐지; PR #54). spawn-storm 서킷 브레이커(PR #56)가 재spawn 제한. lifecycle audit에 호스트 계산/enum 전용 `isolation`/`grants`/`netEnforcement` 추가(PR #56). config: `auth.plugin.isolation:"process"` fail-closed 배선(PR #56). 테스트: fs/net/stdio 레드팀(`--allow-net` 없는 Node에선 fail-closed라 skip) + 상시 실행 fail-closed 계약 + config 매트릭스. **잔여:** `--allow-net` 없는 Node(fail-closed, 미봉쇄); `networkEgress` 부여 plugin; 자식 메모리의 credential/키 자료(core-dump/swap); V8/Node 탈출(런타임 통제일 뿐 OS 샌드박스 아님) |
|
|
128
132
|
| P1-SEC-028 | 호스트 중개 키 자료 + SSRF: 키 자료가 필요한 커스텀 자격증명 plugin이 plugin 주도 SSRF 벡터가 될 수 있고, 코어엔 SSRF 가드가 없었음(위성 복사본은 코어에서 도달 불가) | Mitigated | 새 node:-only, 의존성 0 **`haechi/ssrf`** 코어 모듈(PR #55): `isBlockedAddress`(private/loopback/link-local/metadata), `guardedFetch`(https 전용, DNS 후 재확인, `redirect:"error"`, 본문 제한 + timeout), `createGuardedKeyFetcher`(TTL 캐시 + cooldown). `process-isolated` 런타임의 선택적 `keyMaterial:{url}`은 **호스트**가 **운영자 선언** URL에서 이 가드로 가져와 IPC로 주입하므로, plugin은 URL을 명명하지 않습니다(plugin 주도 SSRF 없음). kid-refetch cooldown이 아웃바운드 비율을 제한하고, blocked-address URL은 fail closed됩니다. 테스트: 표준 `isBlockedAddress` 벡터 테이블 + 코어-대-`auth-jwt` parity 가드, `guardedFetch` SSRF 거부/제한, cooldown fail-closed, 런타임 키 주입 + no-SSRF. **잔여:** 위성은 의도적으로 로컬 복사본을 유지함(crypto/auth 패키지는 core-ssrf에 런타임 의존 금지; `crypto-kms/ssrf-parity.test.mjs`) — 코어 재import는 연기하며, drift는 제거가 아니라 parity로 가드; 가드의 DNS-rebinding 창(resolve-then-connect)은 운영자 선언 URL에 대해 수용 |
|
|
129
133
|
|
|
134
|
+
## 5.7 2026-06-16 전체 코드리뷰 Open 리스크 상태
|
|
135
|
+
|
|
136
|
+
권위 있는 항목별 등록부는 `docs/current/code-review-risk-register-2026-06-16.ko.md`입니다. 이 절은 릴리스 게이트 요약입니다. **13개 항목이 모두 Resolved이며 `haechi@1.3.1`로 발행되었습니다**(2026-06-16): P0 + 네 개의 P1(프록시 헤더 경계 패치, SSRF IPv4-mapped 정규화, response-header/streaming 경계, streaming-inspection 텍스트 수정)과 여덟 개의 P2 모두(CR-006 mcp-wrap stderr filter, CR-007 init key-file 검증, CR-008 satellite `manifest.bin` check, CR-009 auth-throw 회귀 테스트, CR-010 process-sandbox quota 테스트, CR-011 audit middle-tamper 테스트, CR-012 vault IPv6 테스트, CR-013 SSE multi-line `data:`). **G9은 Pass입니다.**
|
|
137
|
+
|
|
138
|
+
| ID | 리스크 | 상태 | 종료에 필요한 증거 |
|
|
139
|
+
|---|---|---|---|
|
|
140
|
+
| P0-CR-001 | 프록시가 클라이언트 `Authorization`, `Cookie`, proxy-auth 등 주변 자격증명을 모델 업스트림으로 전달 | Resolved | `filteredHeaders()`의 기본 차단 업스트림 헤더 허용목록 + `createHaechiProxy`에서 전달되는 `forwardPolicy`(게이트웨이 클라이언트 인증과 업스트림 공급자 인증 분리: `auth.provider !== none`이면 클라이언트 `Authorization` 폐기, `none`이면 전달); cookie/proxy-auth/hop-by-hop 항상 폐기; 추가 fail-closed `target.forwardHeaders`; `tests/proxy-header-allowlist.test.mjs`가 게이트웨이 bearer는 업스트림에 안 보이고 공급자 헤더(`x-api-key`/`anthropic-version`/`x-goog-api-key`)는 보임을 증명; README/threat-model/shared-responsibility/configuration(+ko) 갱신 |
|
|
141
|
+
| P1-CR-002 | SSRF 가드가 `::ffff:7f00:1` 같은 hex IPv4-mapped IPv6 private 주소를 놓침 | Resolved | 각 `isBlockedAddress` 복사본(core `packages/ssrf`, `satellites/auth-jwt`, `satellites/crypto-kms/vault.mjs`)이 IPv4-mapped IPv6 주소를 16바이트로 파싱해 임베드된 IPv4(dotted `::ffff:127.0.0.1` 및 hex `::ffff:7f00:1`, bracketed, leading-zero, 혼합 `::`, 대소문자 무시)를 private/loopback/link-local/metadata 검사 전에 정규화; 공인 mapped 주소(`::ffff:8.8.8.8` == `::ffff:808:808`)는 허용 유지되고 기존 vault 과차단도 제거. 복사본은 의도적으로 독립 유지(어떤 위성도 `haechi/ssrf`를 import하지 않음 — core peer floor가 올라감); drift는 parity 테스트로 보증. 테스트: `tests/ssrf.test.mjs`(hex/dotted/bracketed loopback+RFC1918+metadata+public 벡터, core-vs-auth-jwt parity), `satellites/auth-jwt/auth-jwt.test.mjs`(mapped-IPv6 생성 차단 + public-mapped 미차단), `satellites/crypto-kms/vault.test.mjs`(확장된 range table + P2-CR-012 IPv6 loopback 테스트), `satellites/crypto-kms/ssrf-parity.test.mjs`(dotted+hex mapped parity 벡터) |
|
|
142
|
+
| P1-CR-003 | 자동 압축 해제된 업스트림 본문이 기존 압축 응답 헤더와 함께 반환될 수 있음 | Resolved | 중앙화 `sanitizeResponseHeaders()`(content-encoding/content-length/transfer-encoding/hop-by-hop 제거)를 모든 응답 경로(pass-through, 전달/미보호, 보호, streaming)에 적용; 올바른 content-length는 버퍼링된 바디에만 재설정; `tests/proxy-header-allowlist.test.mjs` gzip pass-through + 미보호 응답 테스트가 잔존 content-encoding 없음과 downstream 읽기 가능을 증명 |
|
|
143
|
+
| P1-CR-004 | `streaming.requestMode: "pass-through"`가 response-size cap 없이 전체 업스트림 본문을 버퍼링 | Resolved | 실행 바이트 한도(`responseProtection.maxBytes`)를 가진 진정한 경계 streaming pass-through(`pipeUpstreamBodyBounded`); 초과 시 업스트림 취소 + 클라이언트 쓰기 종료; 미보호/전달 raw read도 한도 적용(초과 시 502); `tests/proxy-header-allowlist.test.mjs`가 oversize pass-through 스트림이 경계/중단됨을 증명 |
|
|
144
|
+
| P1-CR-005 | streaming inspection이 non-JSON SSE/NDJSON 프레임을 원문 통과시켜 plain-text PII 우회 가능 | Resolved | `parseFrame`(`packages/stream-filter/index.mjs`)이 parse 실패 frame을 CONTROL allowlist(`[DONE]`, comment-only, empty/keepalive → 원문 통과)와 non-JSON CONTENT frame(`data:` 텍스트)으로 구분; `handleFrame`이 CONTENT frame을 새 `protector.protectText`(`packages/core/index.mjs`, single-shot `transformSegment`, delta `push`/`flush` 버퍼와 DISTINCT하여 JSON sliding buffer를 오염시키지 않음)로 텍스트 검사하고 `serializeTextFrame`로 `data: <protected text>` 재방출, block action 시 stream fail-closed; response-direction marker skip + audit tally 보존; JSON delta 경로 불변. 테스트: `tests/stream-filter.test.mjs`(plain-text SSE redact, block action 차단, PII 포함 malformed/partial JSON, NDJSON non-JSON 텍스트, control-frame 통과, marker 미재플래그) + `tests/proxy-streaming.test.mjs` end-to-end plain-text 재현 |
|
|
145
|
+
| P2-CR-006 | `mcp-wrap`이 child `stderr`를 filtering/audit 없이 상속 | Resolved | `haechi mcp-wrap`에 `--stderr filter\|drop\|inherit`(기본 `filter`) 추가: 각 완성된 stderr 라인을 재방출 전에 `createStreamProtector().protectText`로 보호(chunk 경계 버퍼링, block-action drop, audit-silent), `drop`은 폐기, `inherit`은 명시적 opt-in 경계, 알 수 없는 값은 fail closed; `tests/mcp-wrap.test.mjs`가 네 가지 모드를 모두 커버 |
|
|
146
|
+
| P2-CR-007 | 기존 key file을 `initLocalKeyFile()`이 검증하지 않음 | Resolved | `initLocalKeyFile`의 기존 파일 non-force 경로가 이제 공유 `loadKeyFile({ requireActive:true })`로 검증(corrupted JSON, active key 부재, 잘못된 길이의 active/retired key 모두 throw); 유효한 파일은 비파괴 유지; `tests/crypto.test.mjs`가 네 가지 케이스를 커버 |
|
|
147
|
+
| P2-CR-008 | satellite packaging check가 `manifest.bin` target file을 검증하지 않음 | Resolved | `evaluateSatellitePackaging()`이 모든 `manifest.bin` 타깃(string + object-map 형식)을 packed-file 집합과 대조해 검증; `tests/satellite-packaging-gate.test.mjs`가 positive + negative(bin 누락) 케이스를 추가 |
|
|
148
|
+
| P2-CR-009 | `authProvider.authenticate()` 예외 경로 회귀 테스트 부재 | Resolved | `tests/proxy-auth.test.mjs`가 throw하는 provider를 주입해 fail-closed(전달 안 됨, generic client error), audit status `haechi_auth_provider_error`, raw error/subject/issuer 미노출을 단언; mutation으로 검증 |
|
|
149
|
+
| P2-CR-010 | process-isolated sandbox quota 분기 parity 테스트 부족 | Resolved | `tests/plugin-process-sandbox.test.mjs`(+ crash fixture)가 isolated-process parity를 추가: oversized result 거부, over-capacity 거부, timeout 종료, child-crash fail-closed; 실제 `process-sandbox.mjs`에 대해 mutation으로 검증 |
|
|
150
|
+
| P2-CR-011 | audit chain 중간 변조 분기 집중 테스트 부족 | Resolved | `tests/audit-chain-tamper.test.mjs`가 실제 multi-record 로그를 기록하고 `verifyAuditChain`이 middle-record content mutation, `previousHash` 누락/오류, 잘못된 `eventHash`를 거부함을 단언; tail-truncation 한계는 계속 문서화 |
|
|
151
|
+
| P2-CR-012 | KMS vault IPv6 loopback carve-out의 IPv6 테스트 부족 | Resolved | `satellites/crypto-kms/vault.test.mjs`에 전용 IPv6 loopback 정책 테스트("…enforces the IPv6 loopback policy (::1, [::1], dotted + hex mapped) — P2-CR-012")를 추가해 bare `::1`, bracketed `[::1]`, dotted `::ffff:127.0.0.1`, hex `::ffff:7f00:1`/`::ffff:7f00:0001`(및 bracketed 변형)을 검증하고, 공인 mapped 주소(`::ffff:8.8.8.8`/`::ffff:808:808`)가 과차단되지 않음을 단언; 확장된 range table과 `ssrf-parity.test.mjs`가 auth-jwt와의 dotted+hex 일치를 고정 |
|
|
152
|
+
| P2-CR-013 | SSE multi-line `data:` 필드를 newline separator 없이 합침 | Resolved | `parseFrame`이 여러 `data:` line을 `join("\n")`(스펙 separator)으로 합치고 line별 스펙 선행 공백 1개만 제거; multi-line JSON은 여전히 `JSON.parse`되고 multi-line plain text는 newline과 함께 재구성되어 검사되며 `serializeTextFrame`가 multi-line payload를 여러 `data:` line으로 재방출; `tests/stream-filter.test.mjs`가 multi-line JSON event와 PII 포함 multi-line plain-text event를 커버 |
|
|
153
|
+
|
|
130
154
|
## 6. P2 제품/문서 리스크 상태
|
|
131
155
|
|
|
132
156
|
| ID | 기존 리스크 | 상태 | 해소 증거 |
|
|
@@ -140,6 +164,8 @@ base64/인코딩 값 디코딩 검사, query string 검사, audit tail truncatio
|
|
|
140
164
|
|
|
141
165
|
이 체크리스트는 `1.x` stable 라인의 모든 릴리스에 대한 상시 배포 전 템플릿이며, `0.3.2` developer preview에서 처음 적용되었습니다. 그 결과를 아래에 참조 기록으로 보존합니다.
|
|
142
166
|
|
|
167
|
+
2026-06-16 현재 상태: G9은 `Pass`입니다 — 코드리뷰 보완이 `haechi@1.3.1`로 발행되었습니다. 이 체크리스트는 해당 컷에 대해 해제되었습니다.
|
|
168
|
+
|
|
143
169
|
외부 npm 게이트 확인 결과(`0.3.2` developer preview, 2026-06-10, 배포 후)는 다음과 같습니다.
|
|
144
170
|
|
|
145
171
|
- `npm whoami`: `raeseoklee`
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
# Haechi Risk Register and Release Gates
|
|
2
2
|
|
|
3
|
-
- Status: Living document (tracks core 1.
|
|
4
|
-
- Date: 2026-06-
|
|
5
|
-
- Target version: 1.
|
|
3
|
+
- Status: Living document (tracks core 1.3.x)
|
|
4
|
+
- Date: 2026-06-16
|
|
5
|
+
- Target version: 1.3.x
|
|
6
6
|
- Branch: `main`
|
|
7
7
|
|
|
8
8
|
## 1. Current Assessment
|
|
9
9
|
|
|
10
|
-
Haechi has shipped its `1.x` stable line. The developer-preview gate (G2, `haechi@0.3.2`) and every gate through
|
|
10
|
+
Haechi has shipped its `1.x` stable line. The developer-preview gate (G2, `haechi@0.3.2`) and every gate through G8 (1.3.0 backend + detection coverage expansion) are passed; the gate history below is retained as the audit trail. 1.0.0 declared the frozen API contract under strict semver (with a documented deprecation policy and `tests/api-contract.test.mjs` as the freeze guard) and narrowly lifted the dynamic-loading ban for a signed, sandboxed `authProvider` plugin; 1.1.0 added the opt-in `process-isolated` plugin runtime with kernel-enforced capability denial. The previously distribution-blocking conditions for the stable label — 1.0 API stability, the external `cryptoProvider`/KMS reference adapter (`haechi-crypto-kms`), and stream-aware enforcement (`streaming.requestMode: "inspect"`) — are all in place. Haechi remains a self-hosted security toolkit, not a compliance guarantee, and production deployments still own network access control, upstream authentication, and key custody (see §5 of the threat model).
|
|
11
|
+
|
|
12
|
+
**2026-06-16 code-review remediation — shipped in `haechi@1.3.1`:** a full code review opened the risk register at `docs/current/code-review-risk-register-2026-06-16.md`. The review found one P0 credential-boundary leak, four P1 release-blocking issues, and eight P2 hardening/test gaps. **All 13 `P*-CR-*` findings are Resolved (§5.7) and shipped in the `haechi@1.3.1` remediation cut (2026-06-16, attested OIDC publish).** G9 is **Pass**. Operators must upgrade from `haechi@1.3.0` to `1.3.1` to pick up the fixes (notably the P0-CR-001 proxy header-boundary patch).
|
|
11
13
|
|
|
12
14
|
| Category | Judgment | Rationale |
|
|
13
15
|
|---|---|---|
|
|
14
16
|
| GitHub public | Allowed | Security limitations, threat model, and shared responsibility are documented |
|
|
15
|
-
| GitHub release/tag | Allowed |
|
|
16
|
-
| npm stable |
|
|
17
|
-
| Production use | Operator-gated
|
|
17
|
+
| GitHub release/tag | Allowed (`v1.3.1` released) | The `v1.3.1` remediation cut is tagged and released; all §5.7 findings are Resolved and G9 is Pass |
|
|
18
|
+
| npm stable | `haechi@1.3.1` published | The code-review remediation shipped in the `haechi@1.3.1` attested OIDC publish (2026-06-16); the prior `1.3.0` carries the pre-fix behavior |
|
|
19
|
+
| Production use | Operator-gated; upgrade to `1.3.1` | Supported only with operator network controls, authz/authn, and key custody; operators on `haechi@1.3.0` should upgrade to `1.3.1` to pick up the proxy header-boundary fix (P0-CR-001) before routing sensitive third-party upstream traffic through the proxy |
|
|
18
20
|
|
|
19
21
|
## 2. Release Gates
|
|
20
22
|
|
|
@@ -23,11 +25,13 @@ Haechi has shipped its `1.x` stable line. The developer-preview gate (G2, `haech
|
|
|
23
25
|
| G0 | GitHub source publication | Tests pass, security limitations documented, no plaintext audit leak | Pass |
|
|
24
26
|
| G1 | GitHub pre-release | P0 code risks resolved, no production-ready language | Pass |
|
|
25
27
|
| G2 | npm developer preview | P0 resolved, preflight/SBOM/provenance paths ready, npm auth confirmed | Pass (`haechi@0.3.2` published 2026-06-10) |
|
|
26
|
-
| G3 | npm stable | P1 production reference, stream-aware enforcement, API stability hardened |
|
|
28
|
+
| G3 | npm stable | P1 production reference, stream-aware enforcement, API stability hardened | Pass (achieved at the 1.0.0 stable cut — streaming inspection shipped in 0.5, the API freeze in 1.0.0; see G5. Superseded by G5–G7.) |
|
|
27
29
|
| G4 | 0.9.0 observability + interactive-auth satellite cut | P1-SEC-026 / P1-OPS-009 mitigated and P2-CRYPTO-001 accepted; `haechi-dashboard` + `haechi-auth-oidc` + `haechi-crypto-kms@0.2.0` tests green; satellite tarballs zero-dep; core bumped to 0.9.0 (only an additive FORBIDDEN_KEYS audit hardening) | Pass |
|
|
28
30
|
| G5 | 1.0.0 stable API contract + signed-plugin sandbox | P1-SEC-024 / P1-SEC-025 mitigated, P2-API-001 / P2-OPS-006 resolved; the API freeze + deprecation policy + `tests/api-contract.test.mjs` green; the Ed25519 signed-plugin contract + `assertAuthProviderConformance` + the worker-isolated `authProvider` sandbox tests green; PR0 satellite peer-ranges widened to `>=0.8.0 <2.0.0` and the `check-satellite-peer-ranges.mjs` preflight gate green; core stays zero runtime dependency; core bumped to 1.0.0 | Pass |
|
|
29
31
|
| G6 | 1.1.0 plugin capability enforcement (`process-isolated`) | P1-SEC-027 / P1-SEC-028 mitigated; the `process-isolated` runtime (child under `--permission`, zero grants, `data:`-URL load, stdio-ignored, JSON-string IPC) + the fail-closed `--allow-net` feature detection (`netEnforcement:"require-permission"`) + the core `haechi/ssrf` guard + host-mediated key material + the spawn-storm circuit breaker; the fs/net/stdio red-team + SSRF + config tests green (the behavioral suite runs on a `--allow-net` Node and skips fail-closed otherwise); the API freeze stays green (additive `./ssrf` export + additive config keys); core stays zero runtime dependency; core bumped to 1.1.0 (additive + opt-in minor) | Pass |
|
|
30
32
|
| G7 | 1.2.0 Reliability Hardening Track (WS1–WS6) | Detection quality measured + tightened (WS2: a labeled-corpus precision/recall `bench:detection` gate, credential + international-PII coverage, `filters.minConfidence` / `filters.allowlist` with the hard-block-types invariant, NFKC unicode-evasion folding with offset-integrity); WS3 injectable `rateLimiter` seam + bounded fixed-window map; WS4 operability (`/__haechi/live`+`/ready` split, injectable `/metrics`, structured logs + per-request `correlationId`, graceful drain, max-in-flight backpressure, env overlay, hardened Dockerfile/compose/runbook, `configVersion`); WS6 proxy TLS / remote-bind hardening (`proxy.tls` / `proxy.trustForwardedProto`, fail-closed `assertSafeProxyTransport`) + OWASP-LLM/NIST control-mapping whitepaper + RFC 9116 `security.txt` + vulnerability-disclosure path. Every change is additive behind 1.1-preserving defaults (`tests/api-contract.test.mjs` green); the no-plaintext-in-audit invariant extends to telemetry; core stays zero runtime dependency; core bumped to 1.2.0 (additive minor) | Pass |
|
|
33
|
+
| G8 | 1.3.0 backend + detection coverage expansion | New protocol adapters for the **Anthropic Messages API** (`/v1/messages`, content-block + SSE `delta.text` with `event:`-line-preserving re-serialize) and the **Google Gemini API** (model-in-path `:generateContent`/`:streamGenerateContent` via an additive `:method`-suffix route matcher that leaves the exact-match adapters byte-identical); detection coverage expansion — cloud/SaaS provider keys (OpenAI/Anthropic/Google-OAuth/SendGrid/Twilio/npm/Azure, anchored) and international PII (FR/ES/JP + IT/SG/IN/DE/NL national IDs with checksum validators), each hard-block-vs-dial-eligible decision driven by measured collision rates (a non-numeric anchor or implausibly-rare shape is required for hard-block; a bare-digit run over a common length stays allowlist-clearable); a `bench:throughput` proxy load benchmark; the `haechi-ratelimit-redis` shared-store rate-limiter satellite (the WS3 seam's production consumer; the proxy now `await`s `rateLimiter.allow`); `haechi-dashboard` surfaces the per-request `correlationId`. Every change is additive — new `target.type`/detection-type/`privacy.profile` *values*, not new config keys (`configVersion` stays `1`); `tests/api-contract.test.mjs` green; core stays zero runtime dependency; core bumped to 1.3.0 (additive minor) | Pass |
|
|
34
|
+
| G9 | 2026-06-16 full code-review remediation gate (shipped in 1.3.1) | `P0-CR-001` and `P1-CR-002` through `P1-CR-005` resolved or formally accepted; P2 items either resolved or scheduled with explicit non-blocking rationale; linked register updated. **All 13 `P*-CR-*` findings are Resolved (§5.7) and shipped in `haechi@1.3.1` (2026-06-16, attested OIDC publish); core bumped 1.3.0 → 1.3.1 (patch, remediation-only — no API/config surface change, `configVersion` stays `1`).** | Pass (`haechi@1.3.1`, 2026-06-16) |
|
|
31
35
|
|
|
32
36
|
## 3. P0 Distribution-Blocking Risk Status
|
|
33
37
|
|
|
@@ -135,6 +139,26 @@ Additive, accumulating on `main` toward a later `1.2.0` minor; the seam + honest
|
|
|
135
139
|
|---|---|---|---|
|
|
136
140
|
| P1-OPS-010 | Proxy rate limiter is single-process and **not injectable**, and its fixed-window `Map` is **never pruned** — a one-shot identity's slot lingers forever, so a high-cardinality identity stream is unbounded memory growth keyed by identity; and a multi-replica deployment silently weakens the limit (per-process throughput multiplies by the replica count) with no replaceable seam | Mitigated | The rate limiter is now an **injectable collaborator** mirroring `cryptoProvider`/`auditSink`/`tokenVault`: `createRuntime(config, { rateLimiter })` (`packages/cli/runtime.mjs`) supplies it, `assertProvider("rateLimiter", …, ["allow"])` fails closed at construction if it lacks `allow()`, and it is exposed on the returned runtime object; the proxy consults `runtime.rateLimiter` (`packages/proxy/index.mjs`, with a backward-compatible local-default fallback for a hand-built runtime). The default per-process in-memory fixed-window limiter (the documented default; `allow(key, limit) -> boolean`, 429 semantics unchanged) is **self-bounding**: a lazy, amortized sweep evicts fully-expired window slots once the `Map` crosses a size threshold — **no background timer** (so `node --test` does not hang). A multi-replica operator injects a shared-store implementation (e.g. Redis) satisfying the same contract, or enforces the limit at a shared front door. Docs: `configuration.md`(+ko) "Rate limiter injection" seam, `shared-responsibility.md`(+ko) §4. Tests: `tests/rate-limiter.test.mjs` — an injected limiter is the one consulted (deny→429, allow→pass-through), fail-closed on a missing `allow()`, the default limiter prunes aged-out one-shot identities (bounded `Map` via `_size()`), and the fixed-window limit/isolation semantics are unchanged; the existing `tests/proxy-auth.test.mjs` 429 test stays green. **Residual:** core ships **no** built-in distributed limiter (track non-goal §5) — a shared-store implementation is the operator's injection or a future satellite; the default's per-process scope is the documented honest default |
|
|
137
141
|
|
|
142
|
+
## 5.7 2026-06-16 Full Code Review Open Risk Status
|
|
143
|
+
|
|
144
|
+
The authoritative itemized register is `docs/current/code-review-risk-register-2026-06-16.md`. This section is the release-gate summary. **All 13 findings are Resolved and shipped in `haechi@1.3.1`** (2026-06-16): the P0 + four P1s (proxy header-boundary patch, SSRF IPv4-mapped normalization, response-header/streaming bounds, streaming-inspection text fix) and all eight P2s (CR-006 mcp-wrap stderr filter, CR-007 init key-file validation, CR-008 satellite `manifest.bin` check, CR-009 auth-throw regression test, CR-010 process-sandbox quota tests, CR-011 audit middle-tamper tests, CR-012 vault IPv6 tests, CR-013 SSE multi-line `data:`). **G9 is Pass.**
|
|
145
|
+
|
|
146
|
+
| ID | Risk | Status | Required closure evidence |
|
|
147
|
+
|---|---|---|---|
|
|
148
|
+
| P0-CR-001 | Proxy forwards client `Authorization`, `Cookie`, proxy-auth, and similar ambient credentials to the model upstream | Resolved | Default-drop upstream header allowlist in `filteredHeaders()` with a `forwardPolicy` threaded from `createHaechiProxy` (gateway-client auth separated from upstream-provider auth: client `Authorization` dropped when `auth.provider !== none`, forwarded when `none`); always-drop cookie/proxy-auth/hop-by-hop; additive fail-closed `target.forwardHeaders`; `tests/proxy-header-allowlist.test.mjs` proves the gateway bearer is not seen upstream while provider headers (`x-api-key`/`anthropic-version`/`x-goog-api-key`) are; README/threat-model/shared-responsibility/configuration (+ko) updated |
|
|
149
|
+
| P1-CR-002 | SSRF guard misses hex IPv4-mapped IPv6 private addresses such as `::ffff:7f00:1` | Resolved | Each `isBlockedAddress` copy (core `packages/ssrf`, `satellites/auth-jwt`, `satellites/crypto-kms/vault.mjs`) now parses an IPv4-mapped IPv6 address to its 16 octets and normalizes the embedded IPv4 (dotted `::ffff:127.0.0.1` AND hex `::ffff:7f00:1`, bracketed, leading-zero, mixed `::`, case-insensitive) before the private/loopback/link-local/metadata check; a genuinely public mapped address (`::ffff:8.8.8.8` == `::ffff:808:808`) stays allowed and the old vault over-block is gone. The copies stay DELIBERATELY independent (no satellite imports `haechi/ssrf` — that would raise their core peer floor); drift is guarded by the parity tests. Tests: `tests/ssrf.test.mjs` (hex/dotted/bracketed loopback+RFC1918+metadata+public vectors, core-vs-auth-jwt parity), `satellites/auth-jwt/auth-jwt.test.mjs` (mapped-IPv6 construction blocks + public-mapped not-blocked), `satellites/crypto-kms/vault.test.mjs` (extended range table + P2-CR-012 IPv6 loopback test), `satellites/crypto-kms/ssrf-parity.test.mjs` (dotted+hex mapped parity vectors) |
|
|
150
|
+
| P1-CR-003 | Auto-decompressed upstream body can be returned with original compressed response headers | Resolved | Centralized `sanitizeResponseHeaders()` (strips content-encoding/content-length/transfer-encoding/hop-by-hop) applied on every response path — pass-through, forwarded/unprotected, protected, streaming; correct content-length re-set only for a buffered body; `tests/proxy-header-allowlist.test.mjs` gzip pass-through + unprotected response tests prove no stale content-encoding and a readable downstream body |
|
|
151
|
+
| P1-CR-004 | `streaming.requestMode: "pass-through"` buffers the full upstream body without a response-size cap | Resolved | True bounded streaming pass-through (`pipeUpstreamBodyBounded`) with a running byte cap (`responseProtection.maxBytes`) that cancels upstream + tears down the client write on overrun; the unprotected/forwarded raw read also capped (502 over the cap); `tests/proxy-header-allowlist.test.mjs` proves an oversize pass-through stream is bounded/aborted |
|
|
152
|
+
| P1-CR-005 | Streaming inspection raw-passes non-JSON SSE/NDJSON frames, allowing plain-text PII bypass | Resolved | `parseFrame` (`packages/stream-filter/index.mjs`) splits parse-failed frames into a CONTROL allowlist (`[DONE]`, comment-only, empty/keepalive → pass raw) vs a non-JSON CONTENT frame (its `data:` text); `handleFrame` inspects a CONTENT frame as text via a new `protector.protectText` (`packages/core/index.mjs`, single-shot `transformSegment`, DISTINCT from the delta `push`/`flush` buffer so it never corrupts the JSON sliding buffer), re-emits `data: <protected text>` (`serializeTextFrame`), and fails the stream closed on a block action; response-direction marker skip + audit tally preserved; JSON delta path unchanged. Tests: `tests/stream-filter.test.mjs` (plain-text SSE redacted, block action blocks, malformed/partial JSON with PII, NDJSON non-JSON text, control-frame pass-through, marker not re-flagged) + `tests/proxy-streaming.test.mjs` end-to-end plain-text repro |
|
|
153
|
+
| P2-CR-006 | `mcp-wrap` inherits child `stderr` without filtering or audit | Resolved | `haechi mcp-wrap` gains `--stderr filter\|drop\|inherit` (default `filter`): each complete stderr line is protected via `createStreamProtector().protectText` before re-emit (chunk-boundary buffered, block-action dropped, audit-silent), `drop` discards, `inherit` is an explicit opt-in boundary, unknown value fails closed; `tests/mcp-wrap.test.mjs` covers all four modes |
|
|
154
|
+
| P2-CR-007 | Existing key files are not validated by `initLocalKeyFile()` | Resolved | `initLocalKeyFile` existing-file non-force path now validates via the shared `loadKeyFile({ requireActive:true })` (corrupted JSON, missing active key, wrong-length active/retired key all throw); valid files stay non-destructive; `tests/crypto.test.mjs` covers the four cases |
|
|
155
|
+
| P2-CR-008 | Satellite packaging check does not validate `manifest.bin` target files | Resolved | `evaluateSatellitePackaging()` validates every `manifest.bin` target (string + object-map forms) against the packed-file set; `tests/satellite-packaging-gate.test.mjs` adds positive + negative (missing-bin) cases |
|
|
156
|
+
| P2-CR-009 | `authProvider.authenticate()` exception path lacks regression coverage | Resolved | `tests/proxy-auth.test.mjs` injects a throwing provider and asserts fail-closed (not forwarded, generic client error), audit status `haechi_auth_provider_error`, and no raw error/subject/issuer leak; mutation-verified |
|
|
157
|
+
| P2-CR-010 | Process-isolated sandbox quota branches lack parity tests | Resolved | `tests/plugin-process-sandbox.test.mjs` (+ crash fixture) adds isolated-process parity: oversized result denied, over-capacity rejected, timeout terminated, child-crash fail-closed; mutation-verified against the real `process-sandbox.mjs` |
|
|
158
|
+
| P2-CR-011 | Audit chain middle-tamper branches lack focused tests | Resolved | `tests/audit-chain-tamper.test.mjs` writes a real multi-record log and asserts `verifyAuditChain` rejects middle-record content mutation, missing/wrong `previousHash`, and wrong `eventHash`; the tail-truncation limitation stays documented |
|
|
159
|
+
| P2-CR-012 | KMS vault IPv6 loopback carve-out lacks IPv6-focused tests | Resolved | `satellites/crypto-kms/vault.test.mjs` adds a dedicated IPv6 loopback policy test ("…enforces the IPv6 loopback policy (::1, [::1], dotted + hex mapped) — P2-CR-012") covering bare `::1`, bracketed `[::1]`, dotted `::ffff:127.0.0.1`, and hex `::ffff:7f00:1`/`::ffff:7f00:0001` (plus bracketed variants), and asserts a public mapped address (`::ffff:8.8.8.8`/`::ffff:808:808`) is NOT over-blocked; the extended range table and `ssrf-parity.test.mjs` lock the dotted+hex agreement with auth-jwt |
|
|
160
|
+
| P2-CR-013 | SSE multi-line `data:` fields are joined without newline separators | Resolved | `parseFrame` joins multiple `data:` lines with `join("\n")` (spec separator) and strips only the single spec leading space per line; multi-line JSON still `JSON.parse`s, multi-line plain text is reconstructed with newlines for inspection, and `serializeTextFrame` re-emits a multi-line payload as multiple `data:` lines; `tests/stream-filter.test.mjs` covers a multi-line JSON event and a multi-line plain-text event with PII |
|
|
161
|
+
|
|
138
162
|
## 6. P2 Product/Documentation Risk Status
|
|
139
163
|
|
|
140
164
|
| ID | Risk | Status | Resolution evidence |
|
|
@@ -148,6 +172,8 @@ Additive, accumulating on `main` toward a later `1.2.0` minor; the seam + honest
|
|
|
148
172
|
|
|
149
173
|
This checklist is the standing pre-distribution template for every release on the `1.x` stable line; it was first exercised for the `0.3.2` developer preview, whose results are retained below as the reference record.
|
|
150
174
|
|
|
175
|
+
Current 2026-06-16 status: G9 is `Pass` — the code-review remediation shipped in `haechi@1.3.1`. This checklist is cleared for that cut.
|
|
176
|
+
|
|
151
177
|
External npm gate check results (`0.3.2` developer preview, 2026-06-10, post-publish):
|
|
152
178
|
|
|
153
179
|
- `npm whoami`: `raeseoklee`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Haechi Shared Responsibility
|
|
2
2
|
|
|
3
|
-
- 문서 상태: Living document (core 1.
|
|
3
|
+
- 문서 상태: Living document (core 1.3.x 추적)
|
|
4
4
|
- 작성일: 2026-06-10
|
|
5
5
|
|
|
6
6
|
## 1. 책임 매트릭스
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|---|---|---|
|
|
10
10
|
| 로컬 개발 | CLI, default config, dev key 생성 | dev key를 운영 환경이나 공유 환경에 재사용하지 않습니다 |
|
|
11
11
|
| 정책 집행 | redact/mask/tokenize/encrypt/block pipeline | 규제 정책과 조직 정책에 맞는 action을 선택합니다 |
|
|
12
|
-
| HTTP proxy | loopback 기본값, remote bind guard, body/response limit | 인증, TLS termination, firewall, upstream auth를
|
|
12
|
+
| HTTP proxy | loopback 기본값, remote bind guard, body/response limit, 기본 차단 upstream 헤더 허용목록(gateway-클라이언트 인증과 upstream-제공자 인증 분리) | 인증, TLS termination, firewall, upstream auth를 담당합니다. upstream 제공자 키는 의도적으로 공급합니다(클라이언트 `Authorization`은 `auth.provider: none`일 때만 전달되며, 그 외에는 `x-api-key` 같은 제공자 키 헤더를 설정하거나 추가 헤더를 `target.forwardHeaders`에 나열합니다) |
|
|
13
13
|
| Streaming | 기본 차단 | pass-through를 사용할 때 보호가 적용되지 않는 위험을 감수합니다 |
|
|
14
14
|
| TokenVault | 암호화 저장, reveal 기본 차단, purge | reveal 승인 절차와 DSAR/retention 운영을 담당합니다 |
|
|
15
15
|
| Audit | 평문 제거, hash chain | append-only storage, backup, 보존 기간, 외부 서명을 담당합니다 |
|
|
@@ -41,7 +41,16 @@
|
|
|
41
41
|
|
|
42
42
|
Haechi의 상태 보유 통제는 설계상 단일 프로세스입니다. 로드밸런서 뒤에서 복제본을 2개 이상 실행하면, 운영자가 공유 인프라를 제공하지 않는 한 이들이 **무음으로 약화**됩니다.
|
|
43
43
|
|
|
44
|
-
- **Rate limit**은 프로세스별·인메모리이므로 전체 처리량이 복제본 수만큼 배가됩니다. identity별 한도를 공유 front door에서 강제하거나, `createRuntime(config, { rateLimiter })`를 통해 공유 저장소 기반 `rateLimiter`를 주입하세요(이 시임은 `allow(key, limit)` 계약을
|
|
44
|
+
- **Rate limit**은 프로세스별·인메모리이므로 전체 처리량이 복제본 수만큼 배가됩니다. identity별 한도를 공유 front door에서 강제하거나, `createRuntime(config, { rateLimiter })`를 통해 공유 저장소 기반 `rateLimiter`를 주입하세요(이 시임은 `allow(key, limit)` 계약을 만족하며, `boolean` 또는 `Promise<boolean>`을 반환할 수 있습니다. [`configuration.md` → Rate limiter 주입](./configuration.ko.md#rate-limiter-주입) 참고). [`haechi-ratelimit-redis`](https://github.com/raeseoklee/haechi/tree/main/satellites/ratelimit-redis) satellite가 레퍼런스 공유 저장소(Redis 기반) 구현입니다 — 주입된 클라이언트 위의 fixed-window 카운터입니다. 기본 프로세스별 limiter는 window map도 bounding하므로 identity 기준 무한 메모리 증가가 없습니다.
|
|
45
45
|
- **Audit hash chain + anchor**는 단일 작성자입니다. 각 복제본에 **고유한** `audit.path`(및 anchor 경로)를 주세요. 하나의 audit 파일을 복제본 간에 공유하면 체인이 분기되어 검증 불가 상태가 됩니다.
|
|
46
46
|
- **TokenVault와 auth store**는 whole-file 로컬 저장소입니다 — 단일 호스트에서는 올바르지만 공유 다중 작성자 저장소는 아닙니다. 다중 복제 토큰화에는 공유 `tokenVault`를 주입하세요.
|
|
47
47
|
- 파일 락은 `O_EXCL` + atomic rename에 의존하며 NFS/공유 파일시스템에서는 보장되지 않습니다 — 이 저장소들은 로컬 디스크에 두세요.
|
|
48
|
+
|
|
49
|
+
## 5. Gateway 인증과 upstream 인증 (헤더 전달)
|
|
50
|
+
|
|
51
|
+
Haechi는 **gateway-클라이언트 인증**과 **upstream-제공자 인증**을 분리합니다. proxy는 임의의 클라이언트 헤더를 모델 upstream으로 전달하지 않고 기본 차단 허용목록을 적용합니다(P0-CR-001):
|
|
52
|
+
|
|
53
|
+
- `auth.provider`가 `bearer`/`external`/`plugin`이면 클라이언트의 `Authorization`은 Haechi가 소비한 **gateway credential**이므로 upstream으로 **절대 전달되지 않습니다**. upstream 제공자 키는 별도로 공급하세요 — 클라이언트 요청에 제공자 키 헤더(`x-api-key`, `x-goog-api-key` 등, 모두 허용목록에 포함)를 설정하거나, 자체 credential 주입으로 upstream을 감싸십시오.
|
|
54
|
+
- `auth.provider`가 `none`이면 클라이언트의 `Authorization`은 **upstream 제공자 키**로 간주되어 전달됩니다(OpenAI 호환 pass-through 패턴).
|
|
55
|
+
- `Cookie`, `Set-Cookie`, `Proxy-Authorization`, hop-by-hop 헤더는 항상 폐기되고, 허용목록에 없는 헤더는 기본 폐기됩니다. 특이한 upstream에는 `target.forwardHeaders`(소문자 이름)로 허용목록을 넓히세요 — 항상 폐기되는 credential/hop-by-hop 헤더는 다시 켤 수 없습니다.
|
|
56
|
+
- **운영자 책임:** upstream이 필요한 credential 헤더를 실제로 받는지 확인하고(gateway 인증에서는 gateway가 더 이상 클라이언트 `Authorization`을 중계하지 않습니다), `target.forwardHeaders`는 무분별한 통과 목록이 아니라 검토된 허용목록으로 다루세요.
|