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
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# 2026-06-16 Full Code Review Risk Register
|
|
2
|
+
|
|
3
|
+
Status: remediation complete and shipped in `haechi@1.3.1` (all 13 findings Resolved; G9 Pass, 2026-06-16)
|
|
4
|
+
Scope: `main` at `a47a6a79c380db412b6a464a2798b7df61f3b68d`
|
|
5
|
+
Review date: 2026-06-16
|
|
6
|
+
Source: full repository code review with focused security, protocol, packaging, and regression-test passes
|
|
7
|
+
|
|
8
|
+
This register captures risks discovered after the 0.3.2 and 1.3.x hardening work. It is intentionally separate from the historical release-gate register so future remediation can update each item without rewriting the earlier release record.
|
|
9
|
+
|
|
10
|
+
## Release Decision
|
|
11
|
+
|
|
12
|
+
Until the P0/P1 items below are fixed or explicitly accepted with a documented owner decision, new release tags and npm publishes should be blocked.
|
|
13
|
+
|
|
14
|
+
Public source availability can continue because the repository is already public and these findings are tracked openly. The client-credential forwarding risk (P0-CR-001) is now Resolved — the proxy applies a default-drop upstream header allowlist and never forwards the gateway `Authorization`/`Cookie`/`Proxy-Authorization` to the model upstream. The hex IPv4-mapped IPv6 SSRF gap (P1-CR-002) and its vault test gap (P2-CR-012) are also now Resolved — every `isBlockedAddress` copy normalizes an IPv4-mapped IPv6 address to its embedded IPv4 before the private-range check. The streaming-inspection bypass (P1-CR-005) and the SSE multi-line `data:` correctness gap (P2-CR-013) are also now Resolved — a parse-failed non-JSON CONTENT frame is inspected as text and multi-line `data:` lines are joined with the spec-required newline. The final six P2s (P2-CR-006 mcp-wrap stderr, P2-CR-007 init key-file validation, P2-CR-008 satellite `manifest.bin` check, P2-CR-009 auth-throw test, P2-CR-010 process-sandbox quota tests, P2-CR-011 audit middle-tamper tests) are now Resolved as well. **All 13 findings are Resolved and shipped in `haechi@1.3.1`** (2026-06-16, attested OIDC publish; core bumped 1.3.0 → 1.3.1, a remediation-only patch). The **G9** release-block gate is **Pass**. Operators should upgrade from `1.3.0` to `1.3.1` to pick up the fixes.
|
|
15
|
+
|
|
16
|
+
## Severity Policy
|
|
17
|
+
|
|
18
|
+
- `P0`: direct credential/data leak across a trust boundary, or a bypass that defeats the core security promise.
|
|
19
|
+
- `P1`: SSRF, protection bypass, denial-of-service, or protocol behavior that can break protected deployments.
|
|
20
|
+
- `P2`: operational, packaging, correctness, or regression-test gaps that should be resolved before broad adoption.
|
|
21
|
+
|
|
22
|
+
## Summary
|
|
23
|
+
|
|
24
|
+
| ID | Severity | Area | Risk | Status | Release impact |
|
|
25
|
+
| --- | --- | --- | --- | --- | --- |
|
|
26
|
+
| P0-CR-001 | P0 | Proxy headers | Client `Authorization`, `Cookie`, proxy-auth, and similar ambient credentials can be forwarded to the model upstream. | Resolved | Was blocking release |
|
|
27
|
+
| P1-CR-002 | P1 | SSRF guard | Hex-form IPv4-mapped IPv6 addresses such as `::ffff:7f00:1` are not classified as private loopback. | Resolved | Was blocking release |
|
|
28
|
+
| P1-CR-003 | P1 | Proxy responses | Auto-decompressed upstream bodies can be returned with original compressed `content-encoding` / `content-length` headers. | Resolved | Was blocking release |
|
|
29
|
+
| P1-CR-004 | P1 | Streaming | `streaming.requestMode: "pass-through"` buffers the full upstream body and has no response-size cap. | Resolved | Was blocking release |
|
|
30
|
+
| P1-CR-005 | P1 | Streaming inspection | Non-JSON SSE/NDJSON frames are passed raw, so plain-text PII can bypass protection. | Resolved | Was blocking release |
|
|
31
|
+
| P2-CR-006 | P2 | MCP wrap | Child process `stderr` is inherited and unfiltered. | Resolved | Was a hardening gap |
|
|
32
|
+
| P2-CR-007 | P2 | Key custody | `initLocalKeyFile()` reports success for existing files without validating key-file shape. | Resolved | Was a hardening gap |
|
|
33
|
+
| P2-CR-008 | P2 | Satellite packaging | Satellite packaging checks do not validate `manifest.bin` targets. | Resolved | Was a hardening gap |
|
|
34
|
+
| P2-CR-009 | P2 | Auth tests | `authProvider.authenticate()` throw path lacks a focused regression test. | Resolved | Was a test gap |
|
|
35
|
+
| P2-CR-010 | P2 | Plugin sandbox tests | Process-isolated quota and oversize branches lack parity with worker sandbox tests. | Resolved | Was a test gap |
|
|
36
|
+
| P2-CR-011 | P2 | Audit tests | Middle-record audit-chain tamper paths lack focused regression coverage. | Resolved | Was a test gap |
|
|
37
|
+
| P2-CR-012 | P2 | Vault tests | KMS vault IPv6 loopback carve-out has only IPv4 coverage. | Resolved | Was a test gap |
|
|
38
|
+
| P2-CR-013 | P2 | SSE correctness | Multi-line SSE `data:` fields are joined without the spec-required newline. | Resolved | Was a correctness gap |
|
|
39
|
+
|
|
40
|
+
## Detailed Findings
|
|
41
|
+
|
|
42
|
+
### P0-CR-001: Proxy Forwards Client Credentials Upstream
|
|
43
|
+
|
|
44
|
+
Status: Resolved (2026-06-16, toward 1.3.1)
|
|
45
|
+
Affected code: `packages/proxy/index.mjs` `forward()` and `filteredHeaders()`
|
|
46
|
+
Evidence:
|
|
47
|
+
|
|
48
|
+
- `forward()` sends `filteredHeaders(request.headers)` into upstream `fetch()`.
|
|
49
|
+
- `filteredHeaders()` currently drops only `host` and `content-length`, then rewrites JSON `content-type`.
|
|
50
|
+
- A local upstream repro received the same Haechi bearer token that the client used for gateway authentication.
|
|
51
|
+
|
|
52
|
+
Impact:
|
|
53
|
+
|
|
54
|
+
Client credentials cross from the local gateway trust boundary into the model provider boundary. This can leak Haechi bearer tokens, cookies, `Proxy-Authorization`, browser-origin headers, or other ambient client secrets. It also makes it hard to reason about future auth modules because client identity and upstream provider credentials are not separated.
|
|
55
|
+
|
|
56
|
+
Required remediation:
|
|
57
|
+
|
|
58
|
+
- Replace the current header pass-through with an explicit upstream header allowlist.
|
|
59
|
+
- Separate gateway-client auth from upstream-provider auth.
|
|
60
|
+
- Preserve provider-required headers only through explicit adapter or config rules, for example Anthropic/Gemini API-key headers when intentionally configured as upstream credentials.
|
|
61
|
+
- Drop hop-by-hop, cookie, proxy-auth, and gateway-client authorization headers by default.
|
|
62
|
+
|
|
63
|
+
Minimum verification:
|
|
64
|
+
|
|
65
|
+
- Regression test proving gateway bearer tokens are not visible to a local upstream.
|
|
66
|
+
- Regression test for intentionally configured upstream credentials.
|
|
67
|
+
- README and release-process wording updated so users do not assume client auth is forwarded safely.
|
|
68
|
+
|
|
69
|
+
Resolution evidence:
|
|
70
|
+
|
|
71
|
+
- `filteredHeaders()` (`packages/proxy/index.mjs`) is now a DEFAULT-DROP allowlist: a `FORWARD_HEADER_ALLOWLIST` (provider/adapter headers `x-api-key`, `anthropic-version`, `anthropic-beta`, `x-goog-api-key`, `openai-organization`, `openai-beta`, `accept`, `accept-language`, `user-agent`; `content-type` rewritten to `application/json`), an always-drop `FORWARD_HEADER_DENYLIST` (`host`, `content-length`, `cookie`, `set-cookie`, `proxy-authorization`, and hop-by-hop `connection`/`keep-alive`/`te`/`trailer`/`transfer-encoding`/`upgrade`), and a conditional `authorization` rule.
|
|
72
|
+
- `createHaechiProxy` derives a `forwardPolicy` once and threads it through every `forward()` callsite (protected path, streaming pass-through, inspected stream). `gatewayConsumedAuthorization` is `auth.provider !== "none"`: when the gateway authenticated the client the request `Authorization` (the gateway credential) is DROPPED; with `auth.provider: none` it is FORWARDED (the upstream provider key, OpenAI-compatible pass-through pattern).
|
|
73
|
+
- Additive config escape hatch `target.forwardHeaders` (array of lowercase header names), validated fail-closed in `normalizeConfig` (`validateForwardHeaders`): non-array, non-lowercase, or always-dropped credential/hop-by-hop names throw at load; it can only widen, never re-enable a dropped header.
|
|
74
|
+
- Regression tests in `tests/proxy-header-allowlist.test.mjs`: a gateway bearer token (`auth.provider: bearer`) is NOT in the headers a stub upstream receives while provider headers ARE; cookie/proxy-authorization/hop-by-hop and unlisted headers are dropped; `auth.provider: none` forwards the client `Authorization`; `target.forwardHeaders` widens additively; the config validator is fail-closed. The existing `tests/proxy-auth.test.mjs` 401/profile/rate suites stay green.
|
|
75
|
+
- Docs: README.md(+ko) "Gateway auth vs upstream auth (header forwarding)" + config-table row; `threat-model.md`(+ko) "Gateway credential forwarded upstream" control row; `shared-responsibility.md`(+ko) §5 + matrix row; `configuration.md`(+ko) `target.forwardHeaders` + the fail-closed throws list.
|
|
76
|
+
|
|
77
|
+
Release decision: blocks any new release or npm publish until fixed or formally accepted. Resolved.
|
|
78
|
+
|
|
79
|
+
### P1-CR-002: SSRF Guard Misses Hex IPv4-Mapped IPv6
|
|
80
|
+
|
|
81
|
+
Status: Resolved (2026-06-16, toward 1.3.1)
|
|
82
|
+
Affected code: `packages/ssrf/index.mjs`, `satellites/auth-jwt/index.mjs` (and `satellites/crypto-kms/vault.mjs`, which had a related over-block)
|
|
83
|
+
Evidence:
|
|
84
|
+
|
|
85
|
+
Manual classification check (after the fix — the formerly-misclassified rows are now correct):
|
|
86
|
+
|
|
87
|
+
| Input | Result | Expected |
|
|
88
|
+
| --- | --- | --- |
|
|
89
|
+
| `::ffff:127.0.0.1` | Private | Private |
|
|
90
|
+
| `::ffff:7f00:1` | Private | Private |
|
|
91
|
+
| `[::ffff:7f00:1]` | Private | Private |
|
|
92
|
+
| `::ffff:10.0.0.1` | Private | Private |
|
|
93
|
+
| `::ffff:a00:1` | Private | Private |
|
|
94
|
+
| `::ffff:8.8.8.8` / `::ffff:808:808` | Public | Public (not over-blocked) |
|
|
95
|
+
|
|
96
|
+
Impact (was):
|
|
97
|
+
|
|
98
|
+
Guarded fetch paths could misclassify private loopback or RFC1918 IPv4 targets when they were represented as hexadecimal IPv4-mapped IPv6. This affected core guarded fetch behavior and the auth-jwt JWKS/OIDC fetch guard. The KMS vault copy had a related defect in the opposite direction: any `::ffff:` form it did not recognize fell through to a "blocked" first hextet, over-blocking a public mapped address such as `::ffff:808:808`.
|
|
99
|
+
|
|
100
|
+
Resolution:
|
|
101
|
+
|
|
102
|
+
- Each `isBlockedAddress` copy now parses an IPv4-mapped IPv6 address into its 16 octets and normalizes the embedded IPv4 (last 32 bits, recognized only when bytes 0..9 are zero and bytes 10..11 are `0xffff`) before the private/loopback/link-local/metadata check. This handles every textual form: dotted (`::ffff:127.0.0.1`), hex (`::ffff:7f00:1`), bracketed (`[::ffff:7f00:1]`), leading-zero (`::ffff:7f00:0001`), mixed `::` compression, and case-insensitive `ffff`. A genuinely public mapped address (`::ffff:8.8.8.8` == `::ffff:808:808`) classifies as its public v4 and stays allowed.
|
|
103
|
+
- The DELIBERATE 1.1 decoupling is preserved: no satellite imports `haechi/ssrf` (that would raise their `haechi` peer floor and republish them). The SAME normalization is applied to EACH independent copy and the agreement is locked by the parity tests, so the copies stay independent-but-consistent.
|
|
104
|
+
|
|
105
|
+
Closure evidence (new/extended tests):
|
|
106
|
+
|
|
107
|
+
- `tests/ssrf.test.mjs` — the canonical vector table gains hex/dotted/bracketed IPv4-mapped loopback, RFC1918, and metadata vectors plus an allowed public mapped pair, all also asserted equal to the auth-jwt copy (core-vs-auth-jwt parity).
|
|
108
|
+
- `satellites/auth-jwt/auth-jwt.test.mjs` — `createJwtAuthProvider` construction now rejects dotted AND hex IPv4-mapped IPv6 private/metadata hosts, and does NOT SSRF-block a public mapped host.
|
|
109
|
+
- `satellites/crypto-kms/vault.test.mjs` — the documented range table adds the hex mapped private/metadata forms and the public mapped allow cases.
|
|
110
|
+
- `satellites/crypto-kms/ssrf-parity.test.mjs` — a new "IPv4-mapped IPv6 (dotted + hex)" group pins auth-jwt ⇄ crypto-kms agreement; the parity test stays green.
|
|
111
|
+
|
|
112
|
+
Release decision: resolved; this finding no longer blocks release.
|
|
113
|
+
|
|
114
|
+
### P1-CR-003: Decompressed Body Returned With Compressed Headers
|
|
115
|
+
|
|
116
|
+
Status: Resolved (2026-06-16, toward 1.3.1)
|
|
117
|
+
Affected code: `packages/proxy/index.mjs` unprotected response paths
|
|
118
|
+
Evidence:
|
|
119
|
+
|
|
120
|
+
- Node `fetch()` auto-decompresses gzip/br/deflate bodies.
|
|
121
|
+
- Unprotected and allow/pass-through response paths return the decoded body while preserving original upstream headers.
|
|
122
|
+
- A local gzip upstream repro caused downstream fetch to fail with `incorrect header check`.
|
|
123
|
+
|
|
124
|
+
Impact:
|
|
125
|
+
|
|
126
|
+
The proxy can emit protocol-inconsistent responses. Clients can fail, retry, or mis-handle protected responses. This also complicates any future response-protection decision because safe and unsafe paths differ in header sanitation.
|
|
127
|
+
|
|
128
|
+
Required remediation:
|
|
129
|
+
|
|
130
|
+
- Strip or recompute `content-encoding`, `content-length`, transfer, and compression metadata whenever the body has been read or transformed by Node.
|
|
131
|
+
- Centralize response-header sanitation so protected, unprotected, and allow paths share the same invariant.
|
|
132
|
+
- Add gzip/br response tests for protected and unprotected paths.
|
|
133
|
+
|
|
134
|
+
Resolution evidence:
|
|
135
|
+
|
|
136
|
+
- A single centralized `sanitizeResponseHeaders(upstreamResponse)` (`packages/proxy/index.mjs`, generalizing the former `streamingResponseHeaders`) strips `content-encoding`, `content-length`, `transfer-encoding`, and hop-by-hop headers (`connection`/`keep-alive`/`te`/`trailer`/`upgrade`/`proxy-authenticate`). It is applied on EVERY response path: streaming pass-through, the inspected-stream `writeHead`, the unprotected/forwarded path, the protected JSON path (`transformedJsonHeaders` now strips the full set), and the `failureMode: allow` path. A correct `content-length` is re-set only for a fully-buffered body.
|
|
137
|
+
- Regression tests in `tests/proxy-header-allowlist.test.mjs`: a gzip upstream response (Node fetch auto-decompresses) returns with no `content-encoding` and a downstream fetch reads the plain body on BOTH the pass-through and the unprotected/forwarded paths.
|
|
138
|
+
|
|
139
|
+
Release decision: blocks release until fixed. Resolved.
|
|
140
|
+
|
|
141
|
+
### P1-CR-004: Streaming Pass-Through Is Buffered And Unbounded
|
|
142
|
+
|
|
143
|
+
Status: Resolved (2026-06-16, toward 1.3.1)
|
|
144
|
+
Affected code: `packages/proxy/index.mjs` streaming `pass-through` branch
|
|
145
|
+
Evidence:
|
|
146
|
+
|
|
147
|
+
- The pass-through branch reads `await readUpstreamBody(upstreamResponse)` and then writes `response.end(rawBody)`.
|
|
148
|
+
- No response `maxBytes` limit is applied in that path.
|
|
149
|
+
- True SSE/NDJSON streaming is delayed until the upstream closes.
|
|
150
|
+
|
|
151
|
+
Impact:
|
|
152
|
+
|
|
153
|
+
The setting name promises pass-through streaming, but the implementation creates full-response buffering. A long-lived or malicious stream can hold memory and connection resources indefinitely.
|
|
154
|
+
|
|
155
|
+
Required remediation:
|
|
156
|
+
|
|
157
|
+
- Either implement true bounded streaming pass-through or rename/fail-closed the mode until implemented.
|
|
158
|
+
- Apply byte and duration limits to all raw upstream-body reads.
|
|
159
|
+
- Add tests for long-lived stream behavior, response-size overrun, and cancellation on client disconnect.
|
|
160
|
+
|
|
161
|
+
Resolution evidence:
|
|
162
|
+
|
|
163
|
+
- The pass-through branch now does TRUE bounded streaming (`pipeUpstreamBodyBounded` in `packages/proxy/index.mjs`): the upstream body is piped to the client response as it arrives, with a running byte count against `streamingPassThroughMaxBytes(config)` (reuses `responseProtection.maxBytes`). Exceeding the cap cancels the upstream reader and destroys the client response (fail-closed on size); downstream backpressure is respected via `response.write` + `drain`. The former `readUpstreamBody(...)` + `response.end(rawBody)` full-buffering is removed from this path.
|
|
164
|
+
- The unprotected/forwarded raw-body read in `maybeProtectResponse` now also passes the same byte cap to `readUpstreamBody({ maxBytes })` and fails closed (502 `haechi_response_too_large`) on `tooLarge`, so no raw upstream-body read lacks a cap.
|
|
165
|
+
- Regression tests in `tests/proxy-header-allowlist.test.mjs`: an oversize pass-through stream (no content-length, > 8× the cap) is bounded/aborted near the cap and never delivers the full stream; the unprotected/forwarded path returns 502 `response_body_too_large` on an oversize buffered body.
|
|
166
|
+
|
|
167
|
+
Release decision: blocks release until fixed or the mode is disabled by default and documented as unavailable. Resolved.
|
|
168
|
+
|
|
169
|
+
### P1-CR-005: Streaming Inspect Raw-Passes Non-JSON Frames
|
|
170
|
+
|
|
171
|
+
Status: Resolved (2026-06-16, toward 1.3.1)
|
|
172
|
+
Affected code: `packages/stream-filter/index.mjs`, `packages/core/index.mjs`
|
|
173
|
+
Evidence (was):
|
|
174
|
+
|
|
175
|
+
- SSE parser returned non-JSON `data:` frames with `ok: false`.
|
|
176
|
+
- The inspect flow passed failed-parse frames through raw (`if (!parsed.ok) { sink.write(frame.raw); return; }`).
|
|
177
|
+
- A local repro with `data: minji.kim@example.com\n\n` produced `blocked: false` and leaked the email.
|
|
178
|
+
|
|
179
|
+
Impact (was):
|
|
180
|
+
|
|
181
|
+
When streaming inspection was enabled, plain-text SSE or NDJSON-like frames could bypass PII/secret protection. This weakened the value of streaming hardening because malformed, non-JSON, or provider-specific frames may carry sensitive text.
|
|
182
|
+
|
|
183
|
+
Resolution:
|
|
184
|
+
|
|
185
|
+
- `parseFrame` (`packages/stream-filter/index.mjs`) now distinguishes a CONTROL frame from a non-JSON CONTENT frame. CONTROL is an explicit allowlist with no inspectable text: the SSE `[DONE]` sentinel, a comment-only frame (only `:`/field lines, no `data:`), and an empty/whitespace/keepalive frame. It returns `{ ok:false, control:true, text:null }` for those and `{ ok:false, control:false, text }` (the reconstructed `data:` payload) for a non-JSON CONTENT frame.
|
|
186
|
+
- `handleFrame`'s parse-failed branch passes CONTROL frames through raw (unchanged), but INSPECTS a non-JSON CONTENT frame as text: it calls a new `protector.protectText(text)` (single-shot detect → decide → tally → transform), re-emits `data: <protected text>` via `serializeTextFrame` (preserving `event:`/`id:`/`:` lines and re-emitting a multi-line payload as multiple `data:` lines), and fails the stream closed (`blocked = true`) on a block-action detection.
|
|
187
|
+
- `createStreamProtector` (`packages/core/index.mjs`) gains `protectText(text)`, which reuses the existing `transformSegment` logic. It is DISTINCT from the delta-channel `push`/`flush` cross-frame buffer — it never touches `pending` — so inspecting a non-JSON frame's text cannot corrupt the JSON delta sliding-buffer state. Per-frame text inspection closes the bypass; cross-frame buffering of arbitrary non-JSON frames is out of scope (the delta channel keeps its own buffer; noted in code).
|
|
188
|
+
- The response-direction marker skip and the audit tally are preserved because `protectText` runs the same `transformSegment` with the protector's response-direction `context`, so a tokenized round-trip (`[REDACTED:…]`, `[TOKEN:…]`) echoed by the model is not re-flagged. The JSON path (delta channel, `protectFrameExtras`, cross-frame sliding buffer, `event:`-line preservation) is unchanged.
|
|
189
|
+
|
|
190
|
+
Closure evidence:
|
|
191
|
+
|
|
192
|
+
- `tests/stream-filter.test.mjs` adds: a plain-text SSE `data: <email>` frame is redacted (not leaked); a plain-text frame with a `card: block` action BLOCKS the stream; malformed/partial JSON with PII is inspected as text; an NDJSON non-JSON content frame with PII is inspected; comment-only/keepalive/`event:` control frames pass untouched; a tokenized-round-trip marker is not re-flagged. The existing within-frame and cross-frame JSON delta tests, `[DONE]`/keepalive pass-through, and report-only tests stay green.
|
|
193
|
+
- `tests/proxy-streaming.test.mjs` adds an end-to-end repro: an upstream emitting `data: minji.kim@example.com\n\n` (plain text) is redacted to `[REDACTED:email]` through the proxy, `stream_inspected` is audited, and the audit chain verifies with no plaintext.
|
|
194
|
+
- Follow-up (adversarial verify caught a residual leak in the first cut): a **trim-mismatch** let a leading-whitespace `data:` line (` data: <pii>`) be parsed+redacted but then re-emitted VERBATIM by the serializers (which used a stricter `startsWith("data:")` on the untrimmed raw line), leaking the original — and the same class affected the JSON `serializeFrame`. Fixed by a single shared lenient matcher `SSE_DATA_LINE` / `sseDataPayload` used by `parseFrame` AND both serializers, so a ` data:`/`\tdata:` line is always recognized and replaced, never emitted verbatim. Also hardened `handleFrame` to route a bare PRIMITIVE JSON frame (e.g. `data: "<pii>"`) to text inspection instead of the object delta path (which would throw an uncaught `setByPath`-on-string-root TypeError). Regression tests added in `tests/stream-filter.test.mjs` (leading-space/tab `data:` plaintext, leading-space JSON non-delta field, bare-primitive JSON).
|
|
195
|
+
|
|
196
|
+
Release decision: blocks release until fixed. Resolved.
|
|
197
|
+
|
|
198
|
+
### P2-CR-006: MCP Wrap Inherits Child `stderr`
|
|
199
|
+
|
|
200
|
+
Status: Resolved (2026-06-16, toward 1.3.1)
|
|
201
|
+
Affected code: `packages/cli/bin/haechi.mjs` `mcpWrapCommand()`
|
|
202
|
+
Evidence:
|
|
203
|
+
|
|
204
|
+
- Child MCP server is spawned with `stdio: ["pipe", "pipe", "inherit"]`.
|
|
205
|
+
- `stderr` is not filtered, audited, redacted, or tokenized.
|
|
206
|
+
|
|
207
|
+
Impact:
|
|
208
|
+
|
|
209
|
+
Sensitive values printed by an MCP server can bypass Haechi controls and appear in the parent terminal, editor logs, or process supervisor logs. This may be acceptable as an explicit local-process boundary, but it is currently not called out strongly enough.
|
|
210
|
+
|
|
211
|
+
Resolution / closure evidence:
|
|
212
|
+
|
|
213
|
+
- `haechi mcp-wrap` gains an explicit `--stderr filter|drop|inherit` flag (default `filter`). `filter` pipes the child's stderr and runs each complete line through the same protection (`runtime.haechi.createStreamProtector().protectText`) before re-emitting to the parent's stderr — redact/mask detected secrets/PII in place, drop a line entirely on a block-action detection — with partial lines buffered across chunk boundaries (split on `\n`, trailing partial flushed on end) and re-emitted in source order. `drop` discards child stderr (consumed via `resume()` so the child never stalls); `inherit` keeps the prior raw passthrough as an explicit, documented opt-in local-process boundary; an unknown `--stderr` value throws a clear fail-closed error before any child is spawned. The stderr filter path records nothing to the audit sink (no plaintext reaches the audit log), and the stdin/stdout JSON-RPC wrap behavior is byte-identical. `COMMAND_HELP` documents the flag, including that `filter` follows the configured policy mode (dry-run/report-only detects but does not transform).
|
|
214
|
+
- `tests/mcp-wrap.test.mjs` adds four cases (filter redacts/masks/drops so the parent never sees a raw secret/PII/card/phone value; drop emits nothing; inherit passes raw; unknown value exits non-zero). Adversarial verify confirmed the default is now `filter` (was the vulnerable `inherit`), chunk-split secrets are reassembled and protected, and block-action lines are dropped not leaked.
|
|
215
|
+
|
|
216
|
+
Release decision: resolved.
|
|
217
|
+
|
|
218
|
+
### P2-CR-007: Existing Key File Not Validated During Init
|
|
219
|
+
|
|
220
|
+
Status: Resolved (2026-06-16, toward 1.3.1)
|
|
221
|
+
Affected code: `packages/crypto/index.mjs` `initLocalKeyFile()`
|
|
222
|
+
Evidence:
|
|
223
|
+
|
|
224
|
+
- Existing key-file path returns success without validating that active/retired keys are parseable and usable.
|
|
225
|
+
|
|
226
|
+
Impact:
|
|
227
|
+
|
|
228
|
+
`haechi init` can report success for corrupted-but-parseable key material. Users discover the problem later when encryption, decryption, token vault, or bundle verification fails.
|
|
229
|
+
|
|
230
|
+
Resolution / closure evidence:
|
|
231
|
+
|
|
232
|
+
- The provider's existing key-load/validation logic (JSON parse, per-key base64url + 32-byte check, active-key resolution) was extracted into a shared module-level `loadKeyFile(keyFile, { requireActive })` that the private `loadKeys()` now delegates to (preserving its historical `keys[0]` fallback). `initLocalKeyFile`'s existing-file non-force path now calls `loadKeyFile` with `requireActive: true` before returning, throwing a specific error per defect (corrupted JSON; "No active key found in local key file"; "AES-256-GCM local key must be 32 bytes" for an active or retired key). A valid existing file stays non-destructive and returns the same `{ created: false, keyFile }` shape; `--force` rotation (retire-not-delete) is unchanged.
|
|
233
|
+
- `tests/crypto.test.mjs` adds four cases: corrupted JSON throws; missing active key throws; wrong-length active key throws; a valid file with retired keys succeeds byte-for-byte unchanged. Adversarial verify confirmed each defect is caught and the valid path is non-destructive.
|
|
234
|
+
|
|
235
|
+
Release decision: resolved.
|
|
236
|
+
|
|
237
|
+
### P2-CR-008: Satellite Packaging Check Misses `manifest.bin`
|
|
238
|
+
|
|
239
|
+
Status: Resolved (2026-06-16, toward 1.3.1)
|
|
240
|
+
Affected code: `scripts/check-satellite-packaging.mjs`
|
|
241
|
+
Evidence:
|
|
242
|
+
|
|
243
|
+
- Package checks validate exported files but do not prove that `manifest.bin` points to present executable files.
|
|
244
|
+
|
|
245
|
+
Impact:
|
|
246
|
+
|
|
247
|
+
A satellite package can pass the local packaging check while shipping a broken CLI entrypoint. This is a release quality risk, especially as auth/KMS/dashboard satellites expand.
|
|
248
|
+
|
|
249
|
+
Resolution / closure evidence:
|
|
250
|
+
|
|
251
|
+
- `evaluateSatellitePackaging()` in `scripts/check-satellite-packaging.mjs` now validates every `manifest.bin` target against the packed-file set: both the string form (`bin: "bin/x.mjs"`) and the object-map form (`bin: { name: "bin/x.mjs" }`) are normalized the same way as `files`/`exports`, and a clear problem is reported for any bin target not present in the tarball. Existing checks are unchanged.
|
|
252
|
+
- `tests/satellite-packaging-gate.test.mjs` adds positive (present bin → no problem) and negative (missing bin, string + object-map forms → bin-specific problem) cases. Adversarial verify confirmed a mutation removing the bin-check block fails the negative test.
|
|
253
|
+
|
|
254
|
+
Release decision: resolved.
|
|
255
|
+
|
|
256
|
+
### P2-CR-009: Auth Provider Throw Path Test Gap
|
|
257
|
+
|
|
258
|
+
Status: Resolved (2026-06-16, toward 1.3.1)
|
|
259
|
+
Affected code: `packages/proxy/index.mjs` auth handling, `tests/proxy-auth.test.mjs`
|
|
260
|
+
Evidence:
|
|
261
|
+
|
|
262
|
+
- Runtime wraps `authProvider.authenticate()` errors as fail-closed `haechi_auth_provider_error`.
|
|
263
|
+
- Existing tests cover several auth outcomes but not provider exceptions.
|
|
264
|
+
|
|
265
|
+
Impact:
|
|
266
|
+
|
|
267
|
+
Future auth-provider changes can accidentally leak raw errors, fail open, or return inconsistent audit status without tests catching it.
|
|
268
|
+
|
|
269
|
+
Resolution / closure evidence:
|
|
270
|
+
|
|
271
|
+
- `tests/proxy-auth.test.mjs` adds a regression test that injects an `authProvider` whose `authenticate()` throws and asserts the proxy fails closed: the request is rejected (not forwarded upstream) with a generic client error, the audit event records the fail-closed status `haechi_auth_provider_error`, and no raw error/stack and no raw subject/issuer leak into the audit event. Adversarial verify confirmed a fail-open mutant (forwards upstream / returns 200) and an audit-leak mutant both make the test fail.
|
|
272
|
+
|
|
273
|
+
Release decision: resolved.
|
|
274
|
+
|
|
275
|
+
### P2-CR-010: Process-Isolated Sandbox Quota Test Gap
|
|
276
|
+
|
|
277
|
+
Status: Resolved (2026-06-16, toward 1.3.1)
|
|
278
|
+
Affected code: `packages/plugin/process-sandbox.mjs`
|
|
279
|
+
Evidence:
|
|
280
|
+
|
|
281
|
+
- Oversized result and over-capacity branches are not mirrored with the same focused coverage as worker sandbox tests.
|
|
282
|
+
|
|
283
|
+
Impact:
|
|
284
|
+
|
|
285
|
+
Process isolation is a security boundary for future plugin work. Missing parity tests increase the chance of regressions in denial-of-service controls.
|
|
286
|
+
|
|
287
|
+
Resolution / closure evidence:
|
|
288
|
+
|
|
289
|
+
- `tests/plugin-process-sandbox.test.mjs` (with a crash fixture added to `tests/helpers/sandbox-fixtures.mjs`) adds isolated-process parity tests mirroring the worker-sandbox DoS-control coverage: oversized result denied, queue/over-capacity rejected, timeout terminated, and child-crash fail-closed (a crash mid-call surfaces as a `crash`-caused denial without killing sibling calls). Adversarial verify confirmed mutations disabling the oversize / capacity / timeout / crash guards each fail the corresponding test (the crash boundary is pinned by the mid-call crash test).
|
|
290
|
+
|
|
291
|
+
Release decision: resolved.
|
|
292
|
+
|
|
293
|
+
### P2-CR-011: Audit Chain Middle-Tamper Test Gap
|
|
294
|
+
|
|
295
|
+
Status: Resolved (2026-06-16, toward 1.3.1)
|
|
296
|
+
Affected code: `packages/audit/index.mjs` `verifyAuditChain()`
|
|
297
|
+
Evidence:
|
|
298
|
+
|
|
299
|
+
- Existing coverage does not focus on middle-record tampering branches.
|
|
300
|
+
|
|
301
|
+
Impact:
|
|
302
|
+
|
|
303
|
+
Audit integrity is a core claim. The chain-verification code is present, but branch-specific tests should prove it rejects middle-record tampering, missing previous hashes, and hash mismatches.
|
|
304
|
+
|
|
305
|
+
Resolution / closure evidence:
|
|
306
|
+
|
|
307
|
+
- `tests/audit-chain-tamper.test.mjs` writes a real multi-record audit log via the sink, then tampers a MIDDLE record and asserts `verifyAuditChain` returns `{ valid: false }` with the correct reason for each branch: middle-record content mutation (stale `eventHash`), missing `previousHash`, wrong `previousHash`, and wrong `integrity` hash. The known tail-truncation limitation (trailing-record removal is detectable only via the separate append-only anchor stream, not the chain alone) is kept explicit. Adversarial verify confirmed the logs are produced by the real sink and the assertions pin each tamper branch.
|
|
308
|
+
|
|
309
|
+
Release decision: resolved.
|
|
310
|
+
|
|
311
|
+
### P2-CR-012: KMS Vault IPv6 Loopback Test Gap
|
|
312
|
+
|
|
313
|
+
Status: Resolved (2026-06-16, toward 1.3.1)
|
|
314
|
+
Affected code: `satellites/crypto-kms/vault.mjs`
|
|
315
|
+
Evidence (was):
|
|
316
|
+
|
|
317
|
+
- Localhost carve-out coverage proved IPv4 loopback behavior but not IPv6 loopback variants.
|
|
318
|
+
|
|
319
|
+
Impact (was):
|
|
320
|
+
|
|
321
|
+
The vault guard is security-sensitive and has slightly different URL parsing logic from the core SSRF guard. IPv6-specific tests were needed to prevent future divergence.
|
|
322
|
+
|
|
323
|
+
Resolution / closure evidence:
|
|
324
|
+
|
|
325
|
+
- `satellites/crypto-kms/vault.test.mjs` adds a dedicated test, "isBlockedAddress 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 its bracketed form), and hex `::ffff:7f00:1` / `::ffff:7f00:0001` (and its bracketed form), all per the intended vault policy (blocked), and asserting that a public IPv4-mapped address (`::ffff:8.8.8.8` / `::ffff:808:808`) is NOT over-blocked.
|
|
326
|
+
- The vault range table is extended with the hex mapped private/metadata forms and the public mapped allow cases, and `satellites/crypto-kms/ssrf-parity.test.mjs` pins the dotted+hex agreement with the auth-jwt copy — the intentional non-IP fail-closed divergence stays explicitly pinned, so future divergence is caught.
|
|
327
|
+
|
|
328
|
+
Release decision: resolved; the test gap is closed.
|
|
329
|
+
|
|
330
|
+
### P2-CR-013: SSE Multi-Line `data:` Join Semantics
|
|
331
|
+
|
|
332
|
+
Status: Resolved (2026-06-16, toward 1.3.1)
|
|
333
|
+
Affected code: `packages/stream-filter/index.mjs`
|
|
334
|
+
Evidence (was):
|
|
335
|
+
|
|
336
|
+
- The SSE parser joined multiple `data:` lines with `join("")`.
|
|
337
|
+
- The SSE processing model joins multiple data lines with newline separators.
|
|
338
|
+
|
|
339
|
+
Impact (was):
|
|
340
|
+
|
|
341
|
+
Valid multi-line SSE events could be mutated before parsing or inspection. This can cause false negatives, false positives, or malformed forwarded events.
|
|
342
|
+
|
|
343
|
+
Resolution:
|
|
344
|
+
|
|
345
|
+
- `parseFrame` now joins multiple `data:` lines with `join("\n")` (the SSE spec separator) and strips only the single spec-defined leading space per line (`replace(/^ /, "")` instead of `trim()`, so interior/trailing text whitespace is not corrupted). A multi-line JSON event still `JSON.parse`s because newlines are valid JSON whitespace between tokens / inside the reconstructed value; a multi-line plain-text event is reconstructed with its newlines before text inspection. The non-JSON CONTENT re-serializer (`serializeTextFrame`) re-emits a multi-line protected payload as multiple `data:` lines, so the newline survives the round-trip.
|
|
346
|
+
|
|
347
|
+
Closure evidence:
|
|
348
|
+
|
|
349
|
+
- `tests/stream-filter.test.mjs` adds a multi-line `data:` JSON event (split across two `data:` lines) that still parses and is protected, and a multi-line plain-text `data:` event whose PII (on the second line) is caught and re-emitted with two `data:` lines preserved.
|
|
350
|
+
|
|
351
|
+
Release decision: should be fixed with the streaming remediation group. Resolved.
|
|
352
|
+
|
|
353
|
+
## Remediation Order
|
|
354
|
+
|
|
355
|
+
1. Fix `P0-CR-001` first because it is a direct credential-boundary leak.
|
|
356
|
+
2. Fix `P1-CR-002` before adding new URL-fetching surfaces such as auth-provider discovery or KMS integrations.
|
|
357
|
+
3. Fix `P1-CR-003` and `P1-CR-004` together because they share response-forwarding invariants.
|
|
358
|
+
4. Fix `P1-CR-005` and `P2-CR-013` together as the streaming-inspection group.
|
|
359
|
+
5. Resolve `P2-CR-006` before recommending MCP wrap for sensitive local tools.
|
|
360
|
+
6. Finish P2 key, packaging, and regression-test gaps before the next npm publish.
|
|
361
|
+
|
|
362
|
+
## Closure Rules
|
|
363
|
+
|
|
364
|
+
An item can move to `Resolved` only when all of the following are true:
|
|
365
|
+
|
|
366
|
+
- Code or documentation remediation is merged.
|
|
367
|
+
- A focused regression test or explicit non-test rationale is recorded.
|
|
368
|
+
- The release-gate register links the remediation evidence.
|
|
369
|
+
- Any accepted residual risk is moved into the threat model or shared-responsibility documentation with operator guidance.
|
|
370
|
+
|
|
371
|
+
## Traceability
|
|
372
|
+
|
|
373
|
+
This document is linked from:
|
|
374
|
+
|
|
375
|
+
- `docs/current/risk-register-release-gate.md`
|
|
376
|
+
- `docs/current/risk-register-release-gate.ko.md`
|
|
377
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Haechi `configVersion` & 업그레이드 노트
|
|
2
2
|
|
|
3
|
-
- 상태: Living document (코어 1.
|
|
3
|
+
- 상태: Living document (코어 1.3.x 추적)
|
|
4
4
|
|
|
5
5
|
`configVersion`는 `haechi.config.json`(및 `haechi.config.example.json`) 최상위에 찍히는 단일 정수입니다. 향후 호환성을 깨는 설정 스키마 변경이 구체적으로 게이트할 수 있는 **버전 앵커**로서, 다른 Haechi 빌드가 쓴 설정을 조용히 잘못 읽는 일을 막습니다.
|
|
6
6
|
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
| `configVersion` | 코어 라인 | 노트 |
|
|
23
23
|
|---|---|---|
|
|
24
|
-
| `1` | 1.0 – 1.
|
|
24
|
+
| `1` | 1.0 – 1.3.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`)는 모두 이전 동작을 기본값으로 합니다. 1.3.0의 추가는 새 키가 아니라 새 *값*입니다 — `target.type`의 `anthropic`/`gemini`, 추가 탐지 타입, `asia-pdpa`/`jp-appi` `privacy.profile` 값 — 따라서 설정 스키마(및 `configVersion`)는 변경되지 않습니다. 마이그레이션 불필요. |
|
|
25
25
|
|
|
26
26
|
## 업그레이드
|
|
27
27
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Haechi `configVersion` & Upgrade Notes
|
|
2
2
|
|
|
3
|
-
- Status: Living document (tracks core 1.
|
|
3
|
+
- Status: Living document (tracks core 1.3.x)
|
|
4
4
|
|
|
5
5
|
`configVersion` is a single integer stamped at the top of `haechi.config.json`
|
|
6
6
|
(and `haechi.config.example.json`). It is a **versioned anchor** so a future
|
|
@@ -34,7 +34,7 @@ the "policies only get stronger / fail closed" invariant intact.
|
|
|
34
34
|
|
|
35
35
|
| `configVersion` | Core line | Notes |
|
|
36
36
|
|---|---|---|
|
|
37
|
-
| `1` | 1.0 – 1.
|
|
37
|
+
| `1` | 1.0 – 1.3.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. The 1.3.0 additions are new *values*, not new keys — `target.type` `anthropic`/`gemini`, additional detection types, and the `asia-pdpa`/`jp-appi` `privacy.profile` values — so the config schema (and `configVersion`) is unchanged. No migration needed. |
|
|
38
38
|
|
|
39
39
|
## Upgrading
|
|
40
40
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Haechi 설정 레퍼런스
|
|
2
2
|
|
|
3
|
-
- 문서 상태: Living document(core 1.
|
|
3
|
+
- 문서 상태: Living document(core 1.3.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
|
|
|
@@ -38,9 +38,10 @@
|
|
|
38
38
|
|
|
39
39
|
| 키 | 타입 / 값 | 기본값 | 설명 |
|
|
40
40
|
|---|---|---|---|
|
|
41
|
-
| `target.type` | `llm-http` \| `openai-compatible` \| `vllm-openai` \| `ollama` \| `llama-cpp` | `llm-http` | 프로토콜 adapter를 선택합니다. `llm-http`는 `openai-compatible`의 별칭입니다. 알 수 없는 값은 로드 시 **fail-closed**로 처리됩니다. |
|
|
41
|
+
| `target.type` | `llm-http` \| `openai-compatible` \| `vllm-openai` \| `ollama` \| `llama-cpp` \| `anthropic` \| `gemini` | `llm-http` | 프로토콜 adapter를 선택합니다. `llm-http`는 `openai-compatible`의 별칭입니다. `anthropic`은 Anthropic Messages API(`/v1/messages`, `/v1/messages/count_tokens`, `/v1/complete`)를 대상으로 합니다. 클라이언트가 Anthropic의 `x-api-key`/`anthropic-version` 헤더를 제공하면 프록시가 이를 그대로 전달합니다. `gemini`는 Google Gemini API를 대상으로 합니다. 엔드포인트는 모델명이 경로에 포함되고 `:method` 접미사를 사용하는 형태입니다(`/v1beta/models/{model}:generateContent`, `:streamGenerateContent`(SSE), `:countTokens`, `:embedContent`, `:batchEmbedContents`; `/v1` 또는 `/v1beta` 접두사, 임의의 `{model}`). 클라이언트가 Gemini의 `x-goog-api-key`(또는 `?key=`)를 제공하면 프록시가 이를 그대로 전달합니다. 알 수 없는 값은 로드 시 **fail-closed**로 처리됩니다. |
|
|
42
42
|
| `target.adapter` | 동일한 값 집합 | `openai-compatible` | adapter를 명시적으로 지정합니다. 보통은 설정하지 않고 `type`이 결정하도록 두면 됩니다. |
|
|
43
43
|
| `target.upstream` | URL 문자열 | `http://127.0.0.1:9999` | proxy가 요청을 전달하는 유일한 upstream입니다. 요청 대상은 origin-form 경로여야 하며, 절대 URL 대상은 거부됩니다(SSRF 방어). |
|
|
44
|
+
| `target.forwardHeaders` | 소문자 헤더 이름 배열 | 미설정(`[]`) | 내장 upstream 헤더 허용목록의 **추가** 확장입니다. proxy는 명시적 허용목록만 upstream으로 전달합니다(제공자/어댑터 헤더: `x-api-key`, `anthropic-version`, `anthropic-beta`, `x-goog-api-key`, `openai-organization`, `openai-beta`, `accept`, `accept-language`, `user-agent`, 그리고 `application/json`으로 재작성되는 `content-type`). 클라이언트 `Authorization`은 `auth.provider: none`일 때만(upstream 제공자 키이므로) 전달되고 그 외에는(gateway credential이므로) 폐기됩니다. `Cookie`/`Set-Cookie`/`Proxy-Authorization`와 hop-by-hop 헤더는 항상 폐기됩니다. 특이한 upstream을 위한 추가 소문자 이름을 여기에 나열하세요. **fail-closed:** 소문자 비어있지 않은 문자열 배열이어야 하며, 항상 폐기되는 credential/hop-by-hop 헤더를 지정할 수 없습니다. |
|
|
44
45
|
|
|
45
46
|
## `proxy`
|
|
46
47
|
|
|
@@ -103,8 +104,9 @@ upstream JSON 응답을 검사합니다(기본적으로 꺼져 있습니다 —
|
|
|
103
104
|
| 키 | 타입 / 값 | 기본값 | 설명 |
|
|
104
105
|
|---|---|---|---|
|
|
105
106
|
| `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`는 정밀도 위험이 큰
|
|
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)
|
|
107
|
+
| `filters.minConfidence` | `[0, 1]` 범위의 숫자 | `0` | 정밀도 다이얼입니다. 각 규칙은 `confidence`(0.6~0.95)를 가지며, confidence가 이 임계값 **미만**인 탐지는 policy 결정 전에 버려집니다. 기본값 `0`은 아무것도 게이트하지 않아 기존 동작을 보존합니다. **하드 블록 예외:** 하드 블록 타입(`secret`, `api_key`, `kr_rrn`, `card`, 그리고 강한 앵커 국가-ID `fr_nir`, `es_dni`, `it_codice_fiscale`, `sg_nric`)은 confidence만으로는 **절대** 버려지지 않습니다 — `minConfidence`는 정밀도 위험이 큰 소프트/다이얼 가능 타입(예: `phone`, `email`, `jp_mynumber`, `uk_nino`, `in_aadhaar`, `de_steuer_id`, `nl_bsn`, `injection`)만 다듬으므로, confidence가 낮은 자격증명/PII 누출도 여전히 조치됩니다(fail-closed). |
|
|
108
|
+
| `filters.allowlist` | 문자열 및/또는 `{ value?, path? }` 의 배열 | `[]` | 운영자 false-positive 예외입니다. 매칭된 **value**가 문자열/`value` 항목과 같거나, PII-safe JSON **path**(audit에 표시되는 해시된 `pathText`)가 `path` 항목과 같은 탐지는 policy 결정 전에 억제됩니다(항목이 `value`와 `path`를 모두 설정하면 **둘 다** 일치해야 합니다). **하드 블록 예외:** 하드 블록 타입(`secret`/`api_key`/`kr_rrn`/`card`/`fr_nir`/`es_dni`/`it_codice_fiscale`/`sg_nric`)을 억제하려는 항목은 **무시되며** 탐지는 그대로 발생합니다 — allowlist는 양성(benign) **소프트/다이얼 가능** 타입 FP(예: `jp_mynumber`/`in_aadhaar` 12자리 ID 오탐, `de_steuer_id` 11자리 ID 오탐, `nl_bsn` 9자리 ID 오탐, format-only `uk_nino`)만 정리할 수 있고, 자격증명/강한-앵커-PII 누출은 절대 침묵시킬 수 없습니다. 모든 억제와 모든 `minConfidence` 드롭은 개수와 타입으로 **감사 로그에 기록됩니다**(`summary.suppressedByType` / `summary.droppedByType` / `suppressedCount` / `droppedCount`) — 원시 값은 절대 기록하지 않습니다. 규칙 전체를 삭제하지 않고 양성 FP 하나만 정리할 때 사용하십시오. |
|
|
109
|
+
| `filters.decodeAndRescan` | boolean | `false` | opt-in base64/percent **디코딩 후 재검사**입니다(WS2d 잔여). 기본값 `false`에서는 탐지가 이전과 바이트 단위로 동일합니다 — base64·percent로 인코딩된 값을 디코딩하지 **않습니다**. `true`일 때, 일반 NFKC 스캔 이후 base64/base64url로 **보이는** string leaf(고정 알파벳, 유효한 길이, `16…8192` 바이트, 같은 leaf로 round-trip, **유효한 UTF-8** 디코딩)이거나 `%XX` 이스케이프를 포함하는 leaf(`decodeURIComponent`)를 디코딩하여 같은 규칙·validator로 재검사합니다. 디코딩된 매칭은 인코딩된 leaf에 offset이 없으므로, **WHOLE-LEAF** 탐지(`start:0,end:leaf.length`, value = 인코딩된 leaf 전체)로 fail closed됩니다 — transform이 leaf 전체를 redact/block합니다. **정밀도 가드:** 디코딩된 매칭은 validator 기반이거나 하드 블록 타입일 때만 발생합니다(Luhn 통과 `card`, 체크섬 `kr_rrn`/`us_ssn`, IBAN mod-97, 또는 앵커된 규칙의 `secret`/`api_key`). validator 없는 디코딩된 소프트 타입 매칭(맨 전화번호 형태)은 발생하지 않으므로 무작위 base64는 오탐하지 않습니다. 새 런타임 의존성은 없습니다(`node:buffer` Buffer + `decodeURIComponent` 빌트인). 다른 인코딩(gzip/hex/중첩/커스텀 알파벳)은 범위 밖입니다. |
|
|
108
110
|
|
|
109
111
|
### 탐지 벤치마크
|
|
110
112
|
|
|
@@ -147,7 +149,7 @@ npm run scan:detection # CI 회귀 게이트: 어떤 type이라도 baseline
|
|
|
147
149
|
|
|
148
150
|
| 키 | 타입 / 값 | 기본값 | 설명 |
|
|
149
151
|
|---|---|---|---|
|
|
150
|
-
| `privacy.profile` | `null` \| `kr-pipa` \| `eu-gdpr` \| `us-general` | `null` | 집행 전에 지역별 기준 action 집합을 적용합니다. 프로필은 명시적 action을 **강화**할 수는 있지만 약화할 수는 없습니다. 엔지니어링 기본값이며, 법적 자문이 아닙니다. |
|
|
152
|
+
| `privacy.profile` | `null` \| `kr-pipa` \| `eu-gdpr` \| `asia-pdpa` \| `us-general` \| `jp-appi` | `null` | 집행 전에 지역별 기준 action 집합을 적용합니다. 프로필은 명시적 action을 **강화**할 수는 있지만 약화할 수는 없습니다. `eu-gdpr`는 EU 국가 ID(`fr_nir`/`es_dni`/`uk_nino`/`it_codice_fiscale`/`de_steuer_id`/`nl_bsn`)를 block하고, `asia-pdpa`(싱가포르 PDPA / 인도 DPDP)는 `sg_nric`/`in_aadhaar`를 block하며(혼합 지역 페이로드를 위해 다른 체크섬 국가 ID도 함께 block), `jp-appi`는 `jp_mynumber`를 block하고, 모든 프로필이 `jp_mynumber`를 block합니다(체크섬 국가-ID 누출). 엔지니어링 기본값이며, 법적 자문이 아닙니다. |
|
|
151
153
|
|
|
152
154
|
## `logging`
|
|
153
155
|
|
|
@@ -273,13 +275,13 @@ rate limiter는 **주입 가능한 collaborator**이며, `createRuntime(config,
|
|
|
273
275
|
const runtime = createRuntime(config, { rateLimiter });
|
|
274
276
|
```
|
|
275
277
|
|
|
276
|
-
주입된 `rateLimiter`는 `allow(key, limit)
|
|
278
|
+
주입된 `rateLimiter`는 `allow(key, limit)`을 구현해야 하며, `boolean` **또는** `Promise<boolean>`을 반환합니다(`key`는 identity별 버킷, `limit`은 resolve된 `requestsPerMinute`입니다). 구현하지 않으면 `createRuntime`이 construction 시점에 fail-closed로 throw합니다. proxy는 결과를 `await`하므로 동기 boolean과 비동기 공유 저장소 limiter가 동일하게 동작합니다 — 내장 기본값은 동기로 유지되고, 비동기로 resolve되는 Redis 기반 limiter도 올바르게 gate합니다. proxy는 rate 통제 대상 요청마다 `runtime.rateLimiter`를 참조합니다.
|
|
277
279
|
|
|
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 기반)을
|
|
280
|
+
**기본값**은 프로세스별 인메모리 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 기반)을 주입하십시오 — [`haechi-ratelimit-redis`](./shared-responsibility.ko.md#4-수평-확장--다중-복제) satellite가 레퍼런스 구현입니다. [Shared responsibility §4](./shared-responsibility.ko.md#4-수평-확장--다중-복제)를 참고하십시오.
|
|
279
281
|
|
|
280
282
|
## Detection type과 action
|
|
281
283
|
|
|
282
|
-
내장 탐지 `type` 값은 다음과 같습니다: `email`, `phone`, `kr_rrn`, `card`, `api_key`, `secret`, `us_ssn`, `iban`, `injection`(응답 방향 휴리스틱, 기본 report-only). 커스텀 규칙으로 새로운 type을 추가할 수 있습니다.
|
|
284
|
+
내장 탐지 `type` 값은 다음과 같습니다: `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`, `injection`(응답 방향 휴리스틱, 기본 report-only). 커스텀 규칙으로 새로운 type을 추가할 수 있습니다.
|
|
283
285
|
|
|
284
286
|
### 지원하는 자격증명·PII 매트릭스
|
|
285
287
|
|
|
@@ -295,11 +297,26 @@ const runtime = createRuntime(config, { rateLimiter });
|
|
|
295
297
|
| `card` | 결제 카드(PAN) | Luhn validator, 13–19자리 | — |
|
|
296
298
|
| `us_ssn` | 미국 사회보장번호 | `AAA-GG-SSSS` + SSA 범위 validator(area `000`/`666`/`900-999`, group `00`, serial `0000` 거부) | 구분자 필수이며, bare 9자리 id는 SSN이 아닙니다. |
|
|
297
299
|
| `iban` | 국제 은행계좌번호 | **mod-97 checksum** validator | checksum이 정밀도 가드입니다 — IBAN 형태이지만 97 비검증 문자열은 거부됩니다. |
|
|
298
|
-
| `
|
|
300
|
+
| `jp_mynumber` | 일본 마이넘버(個人番号) | 12자리 + **mod-11 가중 check digit** | check digit이 정밀도 가드입니다; check 불일치 12자리 run은 거부됩니다. **하드 블록.** |
|
|
301
|
+
| `fr_nir` | 프랑스 NIR / INSEE 사회보장번호 | 15자 + **`97 - (앞13 mod 97)` 제어키**(코르시카 `2A`→19, `2B`→18) | 제어키 불일치는 거부됩니다. **하드 블록.** |
|
|
302
|
+
| `es_dni` | 스페인 DNI / NIE | 8자리(DNI) 또는 `X/Y/Z`+7자리(NIE) + **mod-23 check letter**(NIE `X/Y/Z`→`0/1/2`) | check letter 불일치는 거부됩니다. **하드 블록.** |
|
|
303
|
+
| `uk_nino` | 영국 국민보험번호 | `[A-CEGHJ-PR-TW-Z][A-CEGHJ-NPR-TW-Z]\d{6}[A-D]` + 문서화된 무효 prefix 제외(`BG`/`GB`/`NK`/`KN`/`TN`/`NT`/`ZZ`, 2번째 글자 `O`) | **format-only — checksum이 없으므로** 하드 블록 타입이 아닙니다(dial 가능: 운영자가 양성 FP를 allowlist 할 수 있음). |
|
|
304
|
+
| `it_codice_fiscale` | 이탈리아 codice fiscale | `[A-Z]{6}\d{2}[A-Z]\d{2}[A-Z]\d{3}[A-Z]` + **mod-26 check character**(앞 15자에 대한 홀수/짝수 위치 테이블) | check character 불일치는 거부됩니다. **하드 블록** — 희귀한 16자 알파+숫자 혼합 형태에 비숫자 구조 앵커가 있습니다(형태에 대한 측정 충돌률 ~3.8%). |
|
|
305
|
+
| `sg_nric` | 싱가포르 NRIC / FIN | `[STFGM]\d{7}[A-Z]` + **가중합 check letter**(가중치 2,7,6,5,4,3,2; prefix별 offset; series별 letter 테이블) | check letter 불일치는 거부됩니다. **하드 블록** — 희귀한 형태에 비숫자 앵커가 둘(prefix letter + check letter)입니다(측정 충돌률 ~3.9%). |
|
|
306
|
+
| `in_aadhaar` | 인도 Aadhaar | 12자리(`0`/`1`로 시작 불가) + **Verhoeff 체크섬** | Verhoeff check digit 불일치는 거부됩니다. **하드 블록 타입이 아닙니다(dial 가능)** — 흔한 12자리 형태에 대한 Verhoeff는 무작위 run의 ~1/10이 통과하므로(측정 ~9.9%, `jp_mynumber` footgun), 운영자가 양성 12자리 ID FP를 allowlist 할 수 있습니다. |
|
|
307
|
+
| `de_steuer_id` | 독일 세금 ID(Steuer-ID) | 11자리 + **ISO 7064 MOD 11,10** check digit + 앞 10자리에 "정확히 한 숫자만 반복" 구조 테스트 | check digit 또는 반복 구조 불일치는 거부됩니다. **하드 블록 타입이 아닙니다(dial 가능)** — 흔한 길이의 비숫자 앵커 없는 11자리 run이므로(측정 충돌률 ~0.37%이지만 `jp_mynumber` 원칙상 bare-digit 형태는 allowlist로 정리 가능하게 둡니다). |
|
|
308
|
+
| `nl_bsn` | 네덜란드 BSN | 9자리 + **"11-proef"** 가중 mod-11 | 11-proef를 통과하지 못하는 run은 거부됩니다. **하드 블록 타입이 아닙니다(dial 가능)** — 9 bare-digit은 매우 흔하고 11-proef는 무작위 run의 ~1/11이 통과하므로(측정 ~9.1%), 가장 명확한 dial 가능 사례입니다. |
|
|
309
|
+
| `api_key` | OpenAI 형식 / Stripe(`sk_`/`rk_`/`pk_`) | prefix + 24자 이상 | 언더스코어 형식 — Stripe `sk_live_`/`rk_live_`/`sk_test_`/`rk_test_`를 포함합니다. |
|
|
299
310
|
| `api_key` | AWS access key id | `AKIA`/`ASIA` + 정확히 16자 대문자-alnum | — |
|
|
300
311
|
| `api_key` | Google API key | `AIza` + 35자 URL-safe 문자 | — |
|
|
312
|
+
| `api_key` | SendGrid API key | `SG.` + 22자 URL-safe + `.` + 43자 URL-safe | 고정 길이 두 개의 점-구분 세그먼트가 anchor입니다. |
|
|
313
|
+
| `api_key` | Twilio Account/API SID | `AC`/`SK` + 정확히 32자 **hex** | hex 전용 본문이 무작위 base62를 거부합니다; bare 32-hex AUTH TOKEN은 할당식(`auth_token`)으로 포착합니다. |
|
|
314
|
+
| `secret` | OpenAI API key | `sk-`(및 `sk-proj-`) + 20자 이상 base62 유사 문자 | **하이픈** 형식으로 언더스코어 Stripe `sk_`와 구분되며, 두 prefix는 절대 겹치지 않습니다. |
|
|
315
|
+
| `secret` | Anthropic API key | `sk-ant-` + 16자 이상 | OpenAI `sk-` 규칙의 더 엄격한 형제 규칙입니다(attribution을 위해 먼저 실행). |
|
|
316
|
+
| `secret` | Google OAuth client secret | `GOCSPX-` + 정확히 28자 URL-safe 문자 | `AIza` API key와 구분됩니다. |
|
|
317
|
+
| `secret` | npm token | `npm_` + 정확히 36자 base62 문자 | — |
|
|
301
318
|
| `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 시크릿(
|
|
319
|
+
| `secret` | 할당식 `<key> = <value>` | 키 어휘: `api_key`, `api_secret`, `secret`, `secret_key`, `aws_secret_access_key`, `client_secret`, `private_key`, `access_token`, `refresh_token`, `auth_token`, `accountkey`, `token`, `password` | bare-base64 시크릿(AWS secret access key, **Azure Storage `AccountKey=`**, **Twilio auth token**)을 할당식 형태로 포착합니다 — 앵커 없는 88자 base64 Azure 규칙은 임의의 blob에 오탐하므로 `AccountKey=` 컨텍스트가 anchor입니다. |
|
|
303
320
|
| `secret` | GitHub token | `gh[pousr]_` + 36자 이상 base64 유사 문자 | pat/oauth/user/server/refresh 변형. |
|
|
304
321
|
| `secret` | Slack token | `xox[baprs]-` + 10자 이상 본문 | bot/user/refresh/legacy 변형. |
|
|
305
322
|
| `secret` | JWT | 점으로 구분된 3개 base64url 세그먼트, 첫 세그먼트가 `eyJ`(즉 `{"`의 base64)로 시작 | `eyJ` anchor가 임의의 점-구분 토큰을 거부합니다. |
|
|
@@ -390,7 +407,7 @@ haechi proxy --config haechi.config.json --host 0.0.0.0 --allow-remote-bind
|
|
|
390
407
|
|
|
391
408
|
## 검증 요약
|
|
392
409
|
|
|
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` 등).
|
|
410
|
+
다음은 로드 시 오류(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`; 소문자 비어있지 않은 문자열 배열이 아니거나 항상 폐기되는 credential/hop-by-hop 헤더를 지정한 `target.forwardHeaders`; 안전하지 않은 커스텀 정규식; `allowUnsafeOverrides` 없이 action을 약화하려는 시도; `text`/`json`이 아닌 `logging.format`; boolean이 아닌 `metrics.enabled`; 잘못된 `HAECHI_*` 환경변수 오버레이 값(잘못된 `HAECHI_PROXY_PORT`, 알 수 없는 `HAECHI_MODE`, 형식이 잘못된 `HAECHI_UPSTREAM` 등).
|
|
394
411
|
|
|
395
412
|
# Satellite 운영자 설정 (0.9)
|
|
396
413
|
|