mirror of
https://github.com/dtzp555-max/ocp.git
synced 2026-07-19 09:44:07 +00:00
/health is an unauthenticated, LAN-reachable endpoint that returned the live anonymous bearer key (anonymousKey: PROXY_ANONYMOUS_KEY). Any device that could reach the port harvested a working, quota-spending credential (P0). The anonymousKey field is now included only when the caller is localhost (already fully trusted by the auth path, via the unspoofable req.socket.remoteAddress) OR the admin explicitly opts in with the new PROXY_ADVERTISE_ANON_KEY=1 env var (default off). ocp-connect's absent-field fallback (interactive --key / anonymous access) is unchanged — comment-only update there. ALIGNMENT.md: this gates an OCP proxy-policy field in /health; it does NOT forward, add, or alter any Anthropic API operation, so a cli.js citation is N/A under Rule 2. No blacklisted tokens introduced; alignment.yml passes. Independent fresh-context reviewer (opus): APPROVE WITH MINOR (Iron Rule 10). Closes #109. Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- **#109 P0** — `/health` no longer advertises `PROXY_ANONYMOUS_KEY` to remote callers by default. The `anonymousKey` field is now gated behind a new `PROXY_ADVERTISE_ANON_KEY=1` opt-in env var; localhost callers are always exempt. This prevents any LAN-reachable device from harvesting a working bearer credential from the unauthenticated `/health` endpoint.
|
||||||
|
|
||||||
## v3.17.1 — 2026-05-31
|
## v3.17.1 — 2026-05-31
|
||||||
|
|
||||||
### Fix — code-audit P1/P2 hardening
|
### Fix — code-audit P1/P2 hardening
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ chmod +x ocp-connect
|
|||||||
./ocp-connect <server-ip>
|
./ocp-connect <server-ip>
|
||||||
```
|
```
|
||||||
|
|
||||||
**Zero-config** — when the server admin has set `PROXY_ANONYMOUS_KEY` (see [Anonymous Access](#anonymous-access-optional) below), just pass the server IP and nothing else. `ocp-connect` reads the anonymous key from `/health` and uses it automatically:
|
**Zero-config** — when the server admin has set `PROXY_ANONYMOUS_KEY` *and* opted in with `PROXY_ADVERTISE_ANON_KEY=1` (see [Anonymous Access](#anonymous-access-optional) below), just pass the server IP and nothing else. `ocp-connect` reads the anonymous key from `/health` and uses it automatically. Without the opt-in, `/health` does not expose the key (issue #109); pass `--key` or rely on anonymous access instead:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./ocp-connect <server-ip>
|
./ocp-connect <server-ip>
|
||||||
@@ -370,7 +370,7 @@ OCP Connect v1.3.0
|
|||||||
The script automatically:
|
The script automatically:
|
||||||
- Writes env vars to all relevant shell rc files (`.bashrc`, `.zshrc`)
|
- Writes env vars to all relevant shell rc files (`.bashrc`, `.zshrc`)
|
||||||
- Sets system-level env vars (`launchctl setenv` on macOS, `environment.d` on Linux)
|
- Sets system-level env vars (`launchctl setenv` on macOS, `environment.d` on Linux)
|
||||||
- **Auto-discovers anonymous key** from `/health.anonymousKey` when no `--key` given (v1.3.0+, requires server v3.10.0+)
|
- **Auto-discovers anonymous key** from `/health.anonymousKey` when no `--key` given (v1.3.0+, requires server v3.10.0+; server must also set `PROXY_ADVERTISE_ANON_KEY=1` — see [Anonymous Access](#anonymous-access-optional))
|
||||||
- Configures OpenClaw automatically (including per-agent `auth-profiles.json` for multi-agent setups)
|
- Configures OpenClaw automatically (including per-agent `auth-profiles.json` for multi-agent setups)
|
||||||
- Detects Cline, Continue.dev, Cursor, and opencode, and prints setup hints (manual configuration required for these IDEs)
|
- Detects Cline, Continue.dev, Cursor, and opencode, and prints setup hints (manual configuration required for these IDEs)
|
||||||
|
|
||||||
@@ -440,7 +440,7 @@ node setup.mjs --bind 0.0.0.0 --auth-mode multi
|
|||||||
|
|
||||||
If OCP is already installed without it, re-export the env var and re-run `node setup.mjs` (the installer is idempotent — it refreshes the service unit). Then `ocp restart` so the running proxy picks up the new env. Setting `PROXY_ANONYMOUS_KEY` only in your interactive shell **does not** affect the auto-started proxy — the service unit is the source of truth for its environment.
|
If OCP is already installed without it, re-export the env var and re-run `node setup.mjs` (the installer is idempotent — it refreshes the service unit). Then `ocp restart` so the running proxy picks up the new env. Setting `PROXY_ANONYMOUS_KEY` only in your interactive shell **does not** affect the auto-started proxy — the service unit is the source of truth for its environment.
|
||||||
|
|
||||||
**Client side**: the anonymous key value is exposed via `GET /health` as the field `anonymousKey` (null when not set). Clients like `ocp-connect` can auto-discover and use it, so the end user doesn't need to get a personal key from the admin.
|
**Client side**: the anonymous key value is exposed via `GET /health` as the field `anonymousKey` (null when not set) **only to localhost callers** or when the admin has also set `PROXY_ADVERTISE_ANON_KEY=1` (default off — see issue #109). With that opt-in, clients like `ocp-connect` can auto-discover and use it, so the end user doesn't need to get a personal key from the admin.
|
||||||
|
|
||||||
**Security note**: setting this env var is an **opt-in** to public access — anyone who can reach your OCP endpoint can use it, up to any rate limits you configure. Don't enable this on internet-exposed OCP instances without additional protection.
|
**Security note**: setting this env var is an **opt-in** to public access — anyone who can reach your OCP endpoint can use it, up to any rate limits you configure. Don't enable this on internet-exposed OCP instances without additional protection.
|
||||||
|
|
||||||
@@ -889,7 +889,8 @@ Future `ocp update` invocations sync automatically.
|
|||||||
| `CLAUDE_SKIP_PERMISSIONS` | `false` | Bypass all permission checks |
|
| `CLAUDE_SKIP_PERMISSIONS` | `false` | Bypass all permission checks |
|
||||||
| `CLAUDE_NO_CONTEXT` | `false` | Suppress CLAUDE.md and auto-memory injection (pure API mode) |
|
| `CLAUDE_NO_CONTEXT` | `false` | Suppress CLAUDE.md and auto-memory injection (pure API mode) |
|
||||||
| `PROXY_API_KEY` | *(unset)* | Bearer token for shared-mode authentication |
|
| `PROXY_API_KEY` | *(unset)* | Bearer token for shared-mode authentication |
|
||||||
| `PROXY_ANONYMOUS_KEY` | *(unset)* | Well-known anonymous key allowlist (multi mode). When set, this exact string bypasses `validateKey()` and grants public access. Exposed via `/health.anonymousKey` so clients auto-discover. See [Anonymous Access](#anonymous-access-optional). |
|
| `PROXY_ANONYMOUS_KEY` | *(unset)* | Well-known anonymous key allowlist (multi mode). When set, this exact string bypasses `validateKey()` and grants public access. Exposed via `/health.anonymousKey` only to localhost, or to all callers when `PROXY_ADVERTISE_ANON_KEY=1`. See [Anonymous Access](#anonymous-access-optional). |
|
||||||
|
| `PROXY_ADVERTISE_ANON_KEY` | *(unset)* | When `=1`, advertise `PROXY_ANONYMOUS_KEY` in the public `/health` body for remote zero-config discovery. Default off — `/health` is unauthenticated, so this exposes the shared key to any LAN-reachable device (issue #109). Localhost always sees it regardless. |
|
||||||
| `CLAUDE_TUI_MODE` | `false` | **Opt-in.** Set to `"true"` to serve requests via interactive `claude` (no `-p` / `--output-format` → `cc_entrypoint=cli`, subscription pool). **Single-user only** — see [Subscription-pool (TUI) mode](#subscription-pool-tui-mode) for the security constraint. |
|
| `CLAUDE_TUI_MODE` | `false` | **Opt-in.** Set to `"true"` to serve requests via interactive `claude` (no `-p` / `--output-format` → `cc_entrypoint=cli`, subscription pool). **Single-user only** — see [Subscription-pool (TUI) mode](#subscription-pool-tui-mode) for the security constraint. |
|
||||||
| `CLAUDE_TUI_WALLCLOCK_MS` | `120000` | (TUI-mode) Maximum time in ms to wait for the native transcript to signal turn completion. Increase for long Opus thinking turns. |
|
| `CLAUDE_TUI_WALLCLOCK_MS` | `120000` | (TUI-mode) Maximum time in ms to wait for the native transcript to signal turn completion. Increase for long Opus thinking turns. |
|
||||||
| `OCP_TUI_CWD` | `$HOME/.ocp-tui/work` | (TUI-mode) Scratch working directory where interactive claude sessions run. Transcripts land under `<HOME>/.claude/projects/<encoded-cwd>/`. Created automatically. |
|
| `OCP_TUI_CWD` | `$HOME/.ocp-tui/work` | (TUI-mode) Scratch working directory where interactive claude sessions run. Transcripts land under `<HOME>/.claude/projects/<encoded-cwd>/`. Created automatically. |
|
||||||
|
|||||||
+5
-3
@@ -506,9 +506,11 @@ main() {
|
|||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Step 2.5: auto-discover anonymous key from /health (issue #12 §14 Path A).
|
# Step 2.5: auto-discover anonymous key from /health (issue #12 §14 Path A).
|
||||||
# When the OCP admin set PROXY_ANONYMOUS_KEY, the server advertises it via
|
# The server advertises anonymousKey in /health ONLY when the admin has set
|
||||||
# /health.anonymousKey. If the user didn't pass --key, use it automatically so
|
# PROXY_ADVERTISE_ANON_KEY=1 (default off — /health is unauthenticated, so
|
||||||
# `ocp-connect <host>` works zero-config for OpenClaw multi-agent setups.
|
# advertising exposes the shared key to any LAN-reachable device; issue #109).
|
||||||
|
# Localhost callers always receive it regardless. When the field is absent,
|
||||||
|
# ocp-connect falls back to anonymous access / interactive --key (step 3 below).
|
||||||
if [[ -z "$key" ]]; then
|
if [[ -z "$key" ]]; then
|
||||||
local anon_key
|
local anon_key
|
||||||
anon_key=$(echo "$health_json" | python3 -c "
|
anon_key=$(echo "$health_json" | python3 -c "
|
||||||
|
|||||||
+7
-1
@@ -278,6 +278,12 @@ const NO_CONTEXT = process.env.CLAUDE_NO_CONTEXT === "true";
|
|||||||
const AUTH_MODE = process.env.CLAUDE_AUTH_MODE || (PROXY_API_KEY ? "shared" : "none");
|
const AUTH_MODE = process.env.CLAUDE_AUTH_MODE || (PROXY_API_KEY ? "shared" : "none");
|
||||||
const ADMIN_KEY = process.env.OCP_ADMIN_KEY || "";
|
const ADMIN_KEY = process.env.OCP_ADMIN_KEY || "";
|
||||||
const PROXY_ANONYMOUS_KEY = process.env.PROXY_ANONYMOUS_KEY || "";
|
const PROXY_ANONYMOUS_KEY = process.env.PROXY_ANONYMOUS_KEY || "";
|
||||||
|
// When set to "1", advertise PROXY_ANONYMOUS_KEY in the public /health body so
|
||||||
|
// remote `ocp-connect` devices can zero-config auto-discover it (issue #12 §14 Path A).
|
||||||
|
// Default OFF: /health is unauthenticated, so advertising hands the shared key to any
|
||||||
|
// LAN-reachable device (issue #109 P0). Localhost callers always see it regardless,
|
||||||
|
// since localhost is already fully trusted by the auth path.
|
||||||
|
const ADVERTISE_ANON_KEY = process.env.PROXY_ADVERTISE_ANON_KEY === "1";
|
||||||
let CACHE_TTL = parseInt(process.env.CLAUDE_CACHE_TTL || "0", 10); // 0 = disabled, value in ms
|
let CACHE_TTL = parseInt(process.env.CLAUDE_CACHE_TTL || "0", 10); // 0 = disabled, value in ms
|
||||||
|
|
||||||
// ── TUI-mode (subscription-pool bridge) — opt-in; default OFF ───────────
|
// ── TUI-mode (subscription-pool bridge) — opt-in; default OFF ───────────
|
||||||
@@ -1918,7 +1924,7 @@ const server = createServer(async (req, res) => {
|
|||||||
claudeBinary: CLAUDE,
|
claudeBinary: CLAUDE,
|
||||||
claudeBinaryOk: binaryOk,
|
claudeBinaryOk: binaryOk,
|
||||||
authMode: AUTH_MODE,
|
authMode: AUTH_MODE,
|
||||||
anonymousKey: PROXY_ANONYMOUS_KEY || null,
|
...((isLocalhost || ADVERTISE_ANON_KEY) ? { anonymousKey: PROXY_ANONYMOUS_KEY || null } : {}),
|
||||||
auth: authStatus,
|
auth: authStatus,
|
||||||
config: {
|
config: {
|
||||||
timeout: TIMEOUT,
|
timeout: TIMEOUT,
|
||||||
|
|||||||
@@ -1540,6 +1540,29 @@ if (process.env.OCP_TUI_LIVE === "1") {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── /health anonymousKey gate (issue #109) ──────────────────────────────────
|
||||||
|
// MIRRORS the predicate in server.mjs (search ADVERTISE_ANON_KEY) — copied
|
||||||
|
// verbatim to avoid importing server.mjs (top-level server.listen() would
|
||||||
|
// start a live HTTP server, per the stream-JSON parser tests convention above).
|
||||||
|
console.log("\n/health anonymousKey gate (issue #109):");
|
||||||
|
|
||||||
|
// Replicate the gating predicate from server.mjs line ~286/1927:
|
||||||
|
// ...((isLocalhost || ADVERTISE_ANON_KEY) ? { anonymousKey: ... } : {})
|
||||||
|
function shouldAdvertiseAnonKey(isLocalhost, advertise) { return isLocalhost || advertise; }
|
||||||
|
|
||||||
|
test("(localhost=false, flag=false) → omit key", () => {
|
||||||
|
assert.equal(shouldAdvertiseAnonKey(false, false), false);
|
||||||
|
});
|
||||||
|
test("(localhost=true, flag=false) → include key (localhost always exempt)", () => {
|
||||||
|
assert.equal(shouldAdvertiseAnonKey(true, false), true);
|
||||||
|
});
|
||||||
|
test("(localhost=false, flag=true) → include key (opt-in set)", () => {
|
||||||
|
assert.equal(shouldAdvertiseAnonKey(false, true), true);
|
||||||
|
});
|
||||||
|
test("(localhost=true, flag=true) → include key (both true)", () => {
|
||||||
|
assert.equal(shouldAdvertiseAnonKey(true, true), true);
|
||||||
|
});
|
||||||
|
|
||||||
// ── Cleanup ──
|
// ── Cleanup ──
|
||||||
closeDb();
|
closeDb();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user