feat(gateway): add HTTP health check endpoints and Cloudflare Tunnel docs
Add unauthenticated /healthz and /health endpoints for load balancer probes.
Returns {"status":"ok"} with HTTP 200 when the gateway is running.
Also adds documentation for:
- Cloudflare Tunnel as an alternative to Tailscale for secure gateway exposure
- Health check endpoint usage for K8s, AWS ALB, and Cloudflare monitoring
Closes #1971
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e38ae0eda5
commit
b3533ac0be
@ -446,7 +446,61 @@ Avoid:
|
||||
- Exposing relay/control ports over LAN or public Internet.
|
||||
- Tailscale Funnel for browser control endpoints (public exposure).
|
||||
|
||||
### 0.7) Secrets on disk (what’s sensitive)
|
||||
### 0.6.2) Cloudflare Tunnel (alternative to Tailscale)
|
||||
|
||||
[Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) provides another way to expose your Gateway securely without opening firewall ports. The tunnel runs a local daemon (`cloudflared`) that connects outbound to Cloudflare's edge, then routes traffic to your local Gateway.
|
||||
|
||||
**Quick tunnel (ephemeral, for testing):**
|
||||
|
||||
```bash
|
||||
# Install cloudflared (https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/)
|
||||
brew install cloudflared # macOS
|
||||
# or: sudo apt install cloudflared # Debian/Ubuntu
|
||||
|
||||
# Start a quick tunnel (generates a random *.trycloudflare.com URL)
|
||||
cloudflared tunnel --url http://127.0.0.1:18789
|
||||
```
|
||||
|
||||
**Persistent tunnel (recommended for production):**
|
||||
|
||||
1. Authenticate: `cloudflared tunnel login`
|
||||
2. Create tunnel: `cloudflared tunnel create clawdbot-gateway`
|
||||
3. Configure `~/.cloudflared/config.yml`:
|
||||
|
||||
```yaml
|
||||
tunnel: <tunnel-id>
|
||||
credentials-file: /path/to/credentials.json
|
||||
|
||||
ingress:
|
||||
- hostname: gateway.yourdomain.com
|
||||
service: http://127.0.0.1:18789
|
||||
- service: http_status:404
|
||||
```
|
||||
|
||||
4. Route DNS: `cloudflared tunnel route dns clawdbot-gateway gateway.yourdomain.com`
|
||||
5. Run: `cloudflared tunnel run clawdbot-gateway`
|
||||
|
||||
**Security notes:**
|
||||
- Always set `gateway.auth.mode: "token"` when exposing via Cloudflare Tunnel.
|
||||
- Use Cloudflare Access policies for additional authentication (SSO, email verification).
|
||||
- The Gateway binds to loopback; only `cloudflared` can reach it.
|
||||
- Monitor tunnel metrics in the Cloudflare dashboard.
|
||||
|
||||
### 0.6.3) Health check endpoint
|
||||
|
||||
The Gateway exposes unauthenticated health check endpoints for load balancer probes:
|
||||
|
||||
- `GET /healthz` - Returns `{"status":"ok"}` with HTTP 200 when the Gateway is running.
|
||||
- `GET /health` - Alias for `/healthz`.
|
||||
|
||||
These endpoints:
|
||||
- Do **not** require authentication (safe for external probes).
|
||||
- Do **not** expose any sensitive information.
|
||||
- Return `Cache-Control: no-cache, no-store` to prevent caching.
|
||||
|
||||
Use these for Kubernetes liveness/readiness probes, AWS ALB health checks, or Cloudflare origin health monitoring.
|
||||
|
||||
### 0.7) Secrets on disk (what's sensitive)
|
||||
|
||||
Assume anything under `~/.openclaw/` (or `$OPENCLAW_STATE_DIR/`) may contain secrets or private data:
|
||||
|
||||
|
||||
@ -235,6 +235,17 @@ export function createGatewayHttpServer(opts: {
|
||||
// Don't interfere with WebSocket upgrades; ws handles the 'upgrade' event.
|
||||
if (String(req.headers.upgrade ?? "").toLowerCase() === "websocket") return;
|
||||
|
||||
// Health check endpoint for load balancers (no auth required).
|
||||
// Returns 200 OK if the gateway is running. Does not expose any sensitive info.
|
||||
const url = new URL(req.url ?? "/", "http://localhost");
|
||||
if (req.method === "GET" && (url.pathname === "/healthz" || url.pathname === "/health")) {
|
||||
res.statusCode = 200;
|
||||
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
||||
res.setHeader("Cache-Control", "no-cache, no-store");
|
||||
res.end(JSON.stringify({ status: "ok" }));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const configSnapshot = loadConfig();
|
||||
const trustedProxies = configSnapshot.gateway?.trustedProxies ?? [];
|
||||
|
||||
@ -298,4 +298,26 @@ describe("gateway server health/presence", () => {
|
||||
|
||||
ws.close();
|
||||
});
|
||||
|
||||
test("HTTP /healthz endpoint returns 200 OK", async () => {
|
||||
const res = await fetch(`http://127.0.0.1:${port}/healthz`);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get("content-type")).toContain("application/json");
|
||||
expect(res.headers.get("cache-control")).toBe("no-cache, no-store");
|
||||
const body = await res.json();
|
||||
expect(body).toEqual({ status: "ok" });
|
||||
});
|
||||
|
||||
test("HTTP /health endpoint returns 200 OK", async () => {
|
||||
const res = await fetch(`http://127.0.0.1:${port}/health`);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get("content-type")).toContain("application/json");
|
||||
const body = await res.json();
|
||||
expect(body).toEqual({ status: "ok" });
|
||||
});
|
||||
|
||||
test("HTTP health endpoints only accept GET", async () => {
|
||||
const postRes = await fetch(`http://127.0.0.1:${port}/healthz`, { method: "POST" });
|
||||
expect(postRes.status).not.toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user