{"record":{"id":"c8d3b9d1fce188ae","repo":"thedotmack/claude-mem","slug":"forbidden-c8d3b9","errorCode":"Forbidden","errorMessage":"Admin endpoints are only accessible from localhost","messagePattern":"Admin endpoints are only accessible from localhost","errorType":"http","errorClass":null,"httpStatus":403,"severity":"error","filePath":"src/services/worker/http/middleware.ts","lineNumber":89,"sourceCode":"    next();\n  };\n}\n\nexport function requireLocalhost(req: Request, res: Response, next: NextFunction): void {\n  const clientIp = req.ip || req.connection.remoteAddress || '';\n  const isLocalhost =\n    clientIp === '127.0.0.1' ||\n    clientIp === '::1' ||\n    clientIp === '::ffff:127.0.0.1' ||\n    clientIp === 'localhost';\n\n  if (!isLocalhost) {\n    logger.warn('SECURITY', 'Admin endpoint access denied - not localhost', {\n      endpoint: req.path,\n      clientIp,\n      method: req.method\n    });\n    res.status(403).json({\n      error: 'Forbidden',\n      message: 'Admin endpoints are only accessible from localhost'\n    });\n    return;\n  }\n\n  next();\n}\n\n// ---------------------------------------------------------------------------\n// Observation TV remote read-only broadcast guard.\n//\n// The worker's HTTP surface has no request authentication; its only defence is\n// the loopback bind. When the operator opens the bind (CLAUDE_MEM_WORKER_HOST)\n// so a phone or a spare monitor can watch Observation TV, this guard is the\n// whole security boundary: loopback requests are untouched, and every\n// non-loopback request is default-denied except an exact-match allowlist of\n// four read-only paths behind a shared secret.","sourceCodeStart":71,"sourceCodeEnd":107,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/8bc631a71a487424b866756e43a6efa4574cc66b/src/services/worker/http/middleware.ts#L71-L107","documentation":"HTTP 403 returned by the requireLocalhost middleware guarding the worker's admin endpoints. It compares the socket's client IP against 127.0.0.1, ::1, ::ffff:127.0.0.1 and the literal string 'localhost'; anything else is refused with a SECURITY log entry recording endpoint, IP and method. This keeps destructive admin operations (key rotation, maintenance, resets) off the network.","triggerScenarios":"Calling an /api/admin/* route from a machine other than the worker host; reaching the worker through a LAN IP, docker bridge network, VM NAT, or a reverse proxy that connects from a non-loopback address; IPv6-mapped addresses outside the exact ::ffff:127.0.0.1 form.","commonSituations":"Worker bound to 0.0.0.0 for container use and admin routes hit via the container IP; SSH port-forwarding misconfigured so the connection appears to come from the remote subnet; curl using a hostname that resolves to a non-loopback interface.","solutions":["Issue the admin request from the same host, targeting http://127.0.0.1:<port> explicitly","If remote, tunnel first: ssh -L 37777:127.0.0.1:37777 user@host, then curl the local forwarded port","In containerized setups, exec into the container (docker exec -it …) and curl loopback from inside"],"exampleFix":"# before (refused: client IP is the docker bridge gateway)\ncurl http://172.17.0.2:37777/api/admin/keys\n\n# after (tunnel to loopback)\nssh -L 37777:127.0.0.1:37777 user@host\ncurl http://127.0.0.1:37777/api/admin/keys","handlingStrategy":"validation","validationCode":"const base = 'http://127.0.0.1:' + WORKER_PORT; // loopback literal, never a LAN hostname\nconst r = await fetch(`${base}/api/admin/keys`);\nif (r.status === 403) throw new Error('admin calls must originate from localhost — tunnel first');","typeGuard":"function isAdminForbidden(body: unknown, status: number): boolean {\n  return status === 403 &&\n    typeof body === 'object' && body !== null &&\n    (body as { error?: string }).error === 'Forbidden';\n}","tryCatchPattern":null,"preventionTips":["Hardcode 127.0.0.1 (not a hostname) for admin API bases in scripts","Use SSH local port forwarding for remote admin instead of opening the worker port","Never bind the worker to 0.0.0.0 expecting the localhost check to still admit you"],"tags":["http-403","security","localhost","middleware","admin"],"backgroundTag":null,"analyzedSha":"8bc631a71a487424b866756e43a6efa4574cc66b","analyzedAt":"2026-08-20T23:58:13.836Z","contentChangedAt":"2026-08-20T23:58:13.836Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}