security(chat): bind chat service to docker bridge + require Bearer auth
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m38s
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m38s
Address security-review finding: the host-side legal-chat-service was binding 0.0.0.0:8770 with no authentication. The service spawns the claude CLI, whose tool set includes Bash + Edit — so an unauthenticated /chat/start is effectively RCE. Oracle Cloud's security list closes the port externally, but defense-in-depth requires two independent layers: 1. Bind defaults to 10.0.1.1 (docker0 bridge gateway). Reachable from containers on docker bridges (the legal-ai container has a route via the coolify network), invisible to anything outside the host. The --host flag is still configurable for local-dev (127.0.0.1) or special-case deployments, but 0.0.0.0 is explicitly discouraged in the docstring. 2. /chat/start requires Authorization: Bearer <LEGAL_CHAT_SHARED_SECRET>. The secret is loaded from /home/chaim/.legal-chat-service.env (chmod 600, off-repo) by the pm2 ecosystem and mirrored as a Coolify env var so the FastAPI chat_proxy sends a matching header. hmac.compare_digest prevents timing oracles. /health stays unauthenticated (static OK, no subprocess) so the FastAPI proxy can probe liveness without the secret. The service refuses to start if LEGAL_CHAT_SHARED_SECRET is empty or shorter than 24 chars — no silent fallback to an open mode. When the Infisical MCP comes back, migrate the secret into the vault at /_GUIDELINES per the project secrets policy. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,16 @@
|
||||
* pm2 ecosystem entry for legal-chat-service — the host-side SSE bridge
|
||||
* to ``claude`` CLI that powers the /training chat tab.
|
||||
*
|
||||
* Security: the service spawns the claude CLI on behalf of any caller
|
||||
* that hits /chat/start. claude tools include Bash, Read, Edit — so an
|
||||
* unauthenticated request to /chat/start is effectively RCE-equivalent.
|
||||
* Two defenses, both required:
|
||||
* 1. Bind to 10.0.1.1 (docker0 bridge gateway) — only host + containers
|
||||
* on docker bridges can reach the socket; nothing outside the host.
|
||||
* 2. Bearer token auth — secret loaded from /home/chaim/.legal-chat-service.env
|
||||
* (chmod 600) and mirrored in Coolify as LEGAL_CHAT_SHARED_SECRET.
|
||||
* The service refuses to start without the secret set.
|
||||
*
|
||||
* Why pm2:
|
||||
* - Auto-restart if the process dies (claude CLI subprocess failures
|
||||
* should never leave the service in a half-dead state).
|
||||
@@ -13,38 +23,47 @@
|
||||
* pm2 save
|
||||
*
|
||||
* Smoke test:
|
||||
* curl http://127.0.0.1:8770/health
|
||||
* curl http://10.0.1.1:8770/health
|
||||
* # → {"ok":true,"service":"legal-chat-service"}
|
||||
*
|
||||
* Update:
|
||||
* pm2 restart legal-chat-service
|
||||
* pm2 restart legal-chat-service --update-env
|
||||
*
|
||||
* Stop:
|
||||
* pm2 stop legal-chat-service
|
||||
*/
|
||||
const fs = require("fs");
|
||||
|
||||
// Load LEGAL_CHAT_SHARED_SECRET from a chmod 600 file off the repo.
|
||||
// The same value is mirrored in Coolify as the LEGAL_CHAT_SHARED_SECRET
|
||||
// env var so the FastAPI proxy sends a matching Authorization header.
|
||||
// Migrate to Infisical (/_GUIDELINES) once the MCP server is back.
|
||||
const ENV_FILE = "/home/chaim/.legal-chat-service.env";
|
||||
const env = {
|
||||
HOME: "/home/chaim",
|
||||
PATH: "/home/chaim/.local/bin:/usr/local/bin:/usr/bin:/bin",
|
||||
PYTHONUNBUFFERED: "1",
|
||||
};
|
||||
try {
|
||||
const text = fs.readFileSync(ENV_FILE, "utf8");
|
||||
for (const line of text.split("\n")) {
|
||||
if (!line || line.trim().startsWith("#")) continue;
|
||||
const m = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*?)\s*$/);
|
||||
if (m) env[m[1]] = m[2];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`legal-chat-service: failed to load ${ENV_FILE}: ${e.message}`);
|
||||
console.error("Service will refuse to start without LEGAL_CHAT_SHARED_SECRET.");
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: "legal-chat-service",
|
||||
cwd: "/home/chaim/legal-ai/mcp-server",
|
||||
// Run the in-package server via the venv interpreter so all
|
||||
// imports (claude_session, etc) resolve.
|
||||
script: "/home/chaim/legal-ai/mcp-server/.venv/bin/python",
|
||||
// Bind to 0.0.0.0 so the legal-ai container can reach the service
|
||||
// via the docker bridge gateway (10.0.1.1:8770). Oracle Cloud's
|
||||
// security list keeps port 8770 closed to the public internet;
|
||||
// ufw is inactive but iptables INPUT default ACCEPT is fine here
|
||||
// because the cloud-level firewall is the actual perimeter (same
|
||||
// pattern paperclip uses for its 0.0.0.0:3100 binding).
|
||||
args: "-m legal_mcp.chat_service.server --port 8770 --host 0.0.0.0",
|
||||
// claude CLI looks up credentials under HOME — make sure it
|
||||
// sees Daphna's session, not an empty container HOME.
|
||||
env: {
|
||||
HOME: "/home/chaim",
|
||||
PATH: "/home/chaim/.local/bin:/usr/local/bin:/usr/bin:/bin",
|
||||
PYTHONUNBUFFERED: "1",
|
||||
},
|
||||
args: "-m legal_mcp.chat_service.server --port 8770 --host 10.0.1.1",
|
||||
env,
|
||||
restart_delay: 5000,
|
||||
max_restarts: 10,
|
||||
autorestart: true,
|
||||
|
||||
Reference in New Issue
Block a user