thedotmack/claude-mem · error

Forbidden

Forbidden

Error message

Admin endpoints are only accessible from localhost

What it means

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.

Source

Thrown at src/services/worker/http/middleware.ts:89

    next();
  };
}

export function requireLocalhost(req: Request, res: Response, next: NextFunction): void {
  const clientIp = req.ip || req.connection.remoteAddress || '';
  const isLocalhost =
    clientIp === '127.0.0.1' ||
    clientIp === '::1' ||
    clientIp === '::ffff:127.0.0.1' ||
    clientIp === 'localhost';

  if (!isLocalhost) {
    logger.warn('SECURITY', 'Admin endpoint access denied - not localhost', {
      endpoint: req.path,
      clientIp,
      method: req.method
    });
    res.status(403).json({
      error: 'Forbidden',
      message: 'Admin endpoints are only accessible from localhost'
    });
    return;
  }

  next();
}

// ---------------------------------------------------------------------------
// Observation TV remote read-only broadcast guard.
//
// The worker's HTTP surface has no request authentication; its only defence is
// the loopback bind. When the operator opens the bind (CLAUDE_MEM_WORKER_HOST)
// so a phone or a spare monitor can watch Observation TV, this guard is the
// whole security boundary: loopback requests are untouched, and every
// non-loopback request is default-denied except an exact-match allowlist of
// four read-only paths behind a shared secret.

View on GitHub (pinned to 8bc631a71a)

Solutions

  1. Issue the admin request from the same host, targeting http://127.0.0.1:<port> explicitly
  2. If remote, tunnel first: ssh -L 37777:127.0.0.1:37777 user@host, then curl the local forwarded port
  3. In containerized setups, exec into the container (docker exec -it …) and curl loopback from inside

Example fix

# before (refused: client IP is the docker bridge gateway)
curl http://172.17.0.2:37777/api/admin/keys

# after (tunnel to loopback)
ssh -L 37777:127.0.0.1:37777 user@host
curl http://127.0.0.1:37777/api/admin/keys
Defensive patterns

Strategy: validation

Validate before calling

const base = 'http://127.0.0.1:' + WORKER_PORT; // loopback literal, never a LAN hostname
const r = await fetch(`${base}/api/admin/keys`);
if (r.status === 403) throw new Error('admin calls must originate from localhost — tunnel first');

Type guard

function isAdminForbidden(body: unknown, status: number): boolean {
  return status === 403 &&
    typeof body === 'object' && body !== null &&
    (body as { error?: string }).error === 'Forbidden';
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of thedotmack/claude-mem@8bc631a71a (2026-08-20). Data as JSON: /api/errors/c8d3b9d1fce188ae. Report an issue: GitHub.