Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,9 @@ Alternative with SSH:
3. Set `STUDIO_ACCESS_TOKEN` if Studio binds to a public host.
4. Configure the gateway URL and token inside Studio.

For Entra ID / Azure AD deployments with nginx, Let's Encrypt, and oauth2-proxy,
see [`docs/deployment/entra-oauth2-proxy.md`](docs/deployment/entra-oauth2-proxy.md).

### Studio on LAN or Tailscale for other devices

1. Start Studio with `HOST=0.0.0.0` (or a specific LAN/Tailscale host).
Expand Down Expand Up @@ -296,6 +299,7 @@ See [`.env.example`](.env.example) for the full local development template.
- [`docs/pi-chat-streaming.md`](docs/pi-chat-streaming.md): gateway runtime streaming and transcript rendering.
- [`docs/permissions-sandboxing.md`](docs/permissions-sandboxing.md): Studio permissions and OpenClaw behavior.
- [`docs/hermes-gateway.md`](docs/hermes-gateway.md): Hermes adapter setup, capabilities, and current limitations.
- [`docs/deployment/entra-oauth2-proxy.md`](docs/deployment/entra-oauth2-proxy.md): Entra ID, oauth2-proxy, nginx, and TLS deployment recipe.

## Current Limitations

Expand Down
1 change: 1 addition & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ Please report issues related to:
- In production, set `CUSTOM_RUNTIME_ALLOWLIST` if you use `/api/runtime/custom`. If unset, it falls back to `UPSTREAM_ALLOWLIST`.
- Empty allowlists are intended for local development only.
- If you enable `STUDIO_ACCESS_TOKEN`, you must also provision the `studio_access` cookie through your deployment/auth layer.
- For Entra ID / oauth2-proxy deployments, see [`docs/deployment/entra-oauth2-proxy.md`](docs/deployment/entra-oauth2-proxy.md).
293 changes: 293 additions & 0 deletions docs/deployment/entra-oauth2-proxy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
# Deploy Claw3D Behind Entra ID And oauth2-proxy

This guide describes a production deployment pattern for Claw3D on an Azure VM
behind nginx, Let's Encrypt, oauth2-proxy, and Microsoft Entra ID.

Use this when you want enterprise SSO in front of Claw3D without adding a
built-in OIDC login flow to the app itself.

## Topology

```text
Browser
-> https://claw3d.example.com
-> nginx / Let's Encrypt
-> oauth2-proxy / Entra ID
-> Claw3D Studio on 127.0.0.1:3000
-> OpenClaw Gateway over ws:// or wss:// from the Studio host
```

Claw3D still owns the Studio UI and same-origin gateway WebSocket proxy. Entra
ID and oauth2-proxy own user authentication before requests reach Claw3D.

## Prerequisites

- Azure VM with Node.js 20+ and npm 10+.
- DNS record for the Claw3D host, for example `claw3d.example.com`.
- nginx installed on the VM.
- TLS certificate from Let's Encrypt or another trusted CA.
- oauth2-proxy installed on the VM.
- An Entra ID tenant where you can create an app registration.
- A running OpenClaw Gateway reachable from the Claw3D Studio host.

## Entra ID app registration

Create an Entra ID app registration for oauth2-proxy.

Recommended settings:

- Platform type: Web.
- Redirect URI:

```text
https://claw3d.example.com/oauth2/callback
```

- Supported account type: choose the tenant policy your organization requires.
- Client secret: create one and store it only on the VM.
- API permissions:
- `openid`
- `profile`
- `email`

Record these values:

- Tenant ID.
- Client ID.
- Client secret.

## Claw3D environment

Run Claw3D on loopback and let nginx expose it publicly.

Example `.env` values:

```bash
HOST=127.0.0.1
PORT=3000
TRUSTED_PROXY=1
UPSTREAM_ALLOWLIST=openclaw-gateway.example.com,localhost,127.0.0.1
CLAW3D_GATEWAY_URL=wss://openclaw-gateway.example.com
CLAW3D_GATEWAY_ADAPTER_TYPE=openclaw
CLAW3D_OFFICE_EVENTS_SECRET=<long-random-webhook-secret>
```

Notes:

- Keep `HOST=127.0.0.1` when nginx and oauth2-proxy are on the same VM.
- Set `TRUSTED_PROXY=1` only when nginx is the only public entry point and
direct public access to Claw3D is blocked.
- Set `UPSTREAM_ALLOWLIST` in production so Studio only proxies approved gateway
hosts.
- `CLAW3D_OFFICE_EVENTS_SECRET` is only needed if you use
`POST /api/office/events`.

`STUDIO_ACCESS_TOKEN` is optional in this deployment. oauth2-proxy is the main
browser-facing auth gate. If you also enable `STUDIO_ACCESS_TOKEN`, configure
oauth2-proxy or nginx to issue the expected `studio_access` cookie, otherwise
Claw3D will return `401 Studio access token required`.

Start Claw3D:

```bash
npm run build
npm run start
```

Use systemd, pm2, or another process manager for production.

## oauth2-proxy configuration

Example `/etc/oauth2-proxy/claw3d.cfg`:

```ini
provider = "oidc"
oidc_issuer_url = "https://login.microsoftonline.com/<tenant-id>/v2.0"

client_id = "<application-client-id>"
client_secret = "<application-client-secret>"
redirect_url = "https://claw3d.example.com/oauth2/callback"

email_domains = [ "*" ]
scope = "openid profile email"

cookie_secret = "<base64-32-byte-secret>"
cookie_secure = true
cookie_httponly = true
cookie_samesite = "lax"
cookie_expire = "8h"
cookie_refresh = "1h"

reverse_proxy = true
set_xauthrequest = true
pass_authorization_header = true
pass_access_token = false

upstreams = [ "http://127.0.0.1:3000" ]
http_address = "127.0.0.1:4180"
```

Generate a cookie secret:

```bash
python3 - <<'PY'
import base64, os
print(base64.urlsafe_b64encode(os.urandom(32)).decode())
PY
```

Restrict access further with oauth2-proxy options such as `allowed_groups` or
`allowed_email_domains` if your Entra tenant requires it.

## nginx configuration

Example server block:

```nginx
server {
listen 80;
server_name claw3d.example.com;
return 301 https://$host$request_uri;
}

server {
listen 443 ssl http2;
server_name claw3d.example.com;

ssl_certificate /etc/letsencrypt/live/claw3d.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/claw3d.example.com/privkey.pem;

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

location /oauth2/ {
proxy_pass http://127.0.0.1:4180;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

location = /oauth2/auth {
internal;
proxy_pass http://127.0.0.1:4180;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
}

location / {
auth_request /oauth2/auth;
error_page 401 = /oauth2/start;

proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

location /api/gateway/ws {
auth_request /oauth2/auth;
error_page 401 = /oauth2/start;

proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}
```

The explicit `/api/gateway/ws` location is important. Claw3D uses this
same-origin WebSocket endpoint for browser-to-Studio gateway traffic.

## Webhook ingress

If you expose `POST /api/office/events` to systems such as HubSpot or CI, keep
the endpoint behind the same TLS host and require signed payloads.

Example signed request:

```bash
body='{"source":"hubspot","eventType":"deal.closed","title":"Deal closed","message":"Automotive account signed.","effect":"confetti"}'
ts="$(date +%s000)"
sig="sha256=$(printf '%s.%s' "$ts" "$body" | openssl dgst -sha256 -hmac "$CLAW3D_OFFICE_EVENTS_SECRET" -hex | awk '{print $2}')"

curl -X POST https://claw3d.example.com/api/office/events \
-H "Content-Type: application/json" \
-H "X-Claw3D-Timestamp: $ts" \
-H "X-Claw3D-Signature: $sig" \
--data "$body"
```

Recommended nginx hardening for webhooks:

- Keep TLS enabled.
- Keep request body size small.
- Allowlist source IP ranges when your provider publishes stable ranges.
- Rotate `CLAW3D_OFFICE_EVENTS_SECRET` if it is exposed.

## Health checks

From the VM:

```bash
curl -I http://127.0.0.1:3000/
curl -I http://127.0.0.1:4180/oauth2/start
```

From a browser:

1. Open `https://claw3d.example.com/office`.
2. Confirm Entra redirects you to sign in.
3. Confirm the office UI loads after sign-in.
4. Confirm the browser WebSocket connects through `/api/gateway/ws`.
5. Confirm the configured OpenClaw Gateway is reachable from the Studio host.

## Troubleshooting

### Redirect loop

- Confirm `redirect_url` in oauth2-proxy exactly matches the Entra redirect URI.
- Confirm nginx sends `X-Forwarded-Proto: https`.
- Confirm oauth2-proxy has `reverse_proxy = true`.

### WebSocket does not connect

- Confirm nginx has a dedicated `/api/gateway/ws` location.
- Confirm `proxy_set_header Upgrade $http_upgrade`.
- Confirm `proxy_set_header Connection "upgrade"`.
- Confirm oauth2-proxy protects the WebSocket route with `auth_request`.

### `401 Studio access token required`

This means `STUDIO_ACCESS_TOKEN` is enabled but the expected `studio_access`
cookie is not present. Either disable `STUDIO_ACCESS_TOKEN` and rely on
oauth2-proxy, or configure your deployment to issue the cookie before requests
reach Claw3D.

### Gateway proxy blocked

Set `UPSTREAM_ALLOWLIST` to include the upstream OpenClaw Gateway host. For
direct runtime integrations, also set `CUSTOM_RUNTIME_ALLOWLIST` or include the
runtime host in `UPSTREAM_ALLOWLIST`.

### User can reach Claw3D without Entra

- Confirm Claw3D listens only on `127.0.0.1`.
- Confirm the VM firewall does not expose port `3000`.
- Confirm nginx is the only public listener.
- Confirm `TRUSTED_PROXY=1` is only set behind your controlled nginx proxy.
Loading