Ingress is the public entry point for webhooks and channel callbacks. It lets platforms such as Microsoft Teams, Facebook, GitLab, Graylog, and custom webhook senders reach a GoClaw process that is running on a local or private port.
flowchart TD
EXT["<b>External Platform</b><br/>Teams, Facebook, GitLab, Graylog, custom webhook"] --> ING["<b>Ingress Layer</b><br/>ngrok, nginx, Caddy, HAProxy, or another reverse proxy"]
ING --> GW["<b>GoClaw Gateway or Dedicated Webhook Listener</b><br/>• /hook/msteams, /hook/facebook, /hook/telegram<br/>• /hook/<slug>/msteams, /hook/<slug>/facebook, /hook/<slug>/telegram<br/>• /goclaw/hook/<type>[/<key>]<br/>• /api/webhook"]
gateway:
ingress:
type: "" # "ngrok", "proxy", "nginx", or "" (no tunnel; see Ingress Types)
url: "" # The address outside systems reach this gateway at
authtoken: "" # Ngrok auth token, only used with type: "ngrok"
allow_preset: "" # "microsoft-teams", "none", or empty
allow_ips: [] # CIDR/IP allowlist; when set, it replaces the preset
port: "" # Optional dedicated webhook port
bind: "" # Webhook port's interface: "loopback" (default), "lan", or an explicit IP
trust_proxy: false # Trust X-Forwarded-For / X-Real-IP from a known proxy
If gateway.ingress.port is empty, webhook routes share the main gateway port. If it is set, GoClaw starts a separate webhook server for ingress traffic. This is useful when the WebUI and admin API should stay private while public webhooks are exposed. That server listens on gateway.ingress.bind, which defaults to loopback: set bind: "lan" (or an IP) when the sender reaches it directly rather than through a proxy on the same host.
With a dedicated port, the webhook routes (/hook/* and /goclaw/hook/*) leave the Web UI port: they are served only on gateway.ingress.port, and a hook URL that points at server.port gets 404. Health probes and /api/webhook are served on both ports; /api/webhook needs the gateway token on either. The Web UI, /gateway and the rest of /api/* stay on server.port only.
url is the gateway's address as the sender sees it, including a proxy's path prefix if it has one. Features that give this address to another system read it whatever type is: the Webhooks page, the agent's graylog_alert tool (Graylog calls <url>/goclaw/hook/graylog/<key>) and the goclaw gitlab hook wizard. On a LAN with no proxy it can be the gateway's own address, for example http://192.0.2.10:8080; the GitLab wizard, though, accepts only an https:// address.
| Type | Behavior |
|---|---|
| empty | No tunnel is started. url, when set, is still the address the Webhooks page, graylog_alert and the GitLab wizard give senders; without it the Webhooks page builds hook URLs from the address you opened it at. This does not limit who can reach the gateway: that is wherever gateway.bind (or gateway.ingress.bind, with a dedicated port) listens, subject to allow_ips / allow_preset |
ngrok |
GoClaw starts or uses an ngrok tunnel for webhook traffic |
proxy |
An external reverse proxy forwards traffic to GoClaw |
nginx |
Alias-style value for reverse proxy deployments |
| Route | Purpose |
|---|---|
/hook/msteams |
Microsoft Teams channel webhook |
/hook/facebook |
Facebook channel webhook |
/hook/telegram |
Telegram channel webhook (only with channels.telegram.mode: webhook) |
/hook/<slug>/msteams, /hook/<slug>/facebook, /hook/<slug>/telegram |
The same webhooks when gateway.webhook_secret_slug is set: the plain /hook/<channel> path is then not served |
/goclaw/hook/<type>[/<key>] |
Generic webhook router |
/api/webhook |
Inbound message API: the agent answers a text as if it came from the named channel (goclaw message uses it) |
/api/send |
Outbound message API for local scripts: main gateway port only, loopback callers only, with the gateway token. Not a route to expose |
/gateway |
Web chat WebSocket when the main gateway is exposed |
gateway:
ingress:
type: "ngrok"
authtoken: "${NGROK_AUTHTOKEN}"
url: "https://my-agent.ngrok.app"
port: "8001"
allow_preset: "microsoft-teams"
The built-in tunnel delivers requests to GoClaw from the local machine (loopback), which the allowlist always lets through, so with trust_proxy: false (the default) allow_preset does not filter ngrok traffic. The Teams webhook is still protected by its Bot Framework token check (see Microsoft Teams). The allowlist filters callers that connect directly or through a reverse proxy.
Use a stable ngrok domain when the external platform stores a fixed callback URL. For Microsoft Teams, set the Azure Bot messaging endpoint to:
https://my-agent.ngrok.app/hook/msteams
or, with a secret slug:
https://my-agent.ngrok.app/hook/<slug>/msteams
One domain can serve multiple agents by prefix. The proxy must strip the prefix before forwarding to GoClaw.
https://agents.example.com/finance/ -> 127.0.0.1:8001
https://agents.example.com/analyst/ -> 127.0.0.1:8002
Agent config:
gateway:
bind: "loopback"
ingress:
type: "nginx"
url: "https://agents.example.com/finance"
port: "8001"
bind: "loopback"
allow_preset: "microsoft-teams"
trust_proxy: true
Nginx example:
location /finance/ {
proxy_pass http://127.0.0.1:8001/;
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 $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_buffering off;
}
location = /finance {
return 301 $scheme://$host/finance/;
}
The trailing slash in proxy_pass http://127.0.0.1:8001/; is important. It strips /finance/ before forwarding, so /finance/health reaches GoClaw as /health.
Use a subdomain per agent when DNS and TLS automation are already in place.
server {
server_name finance.agents.example.com;
location / {
proxy_pass http://127.0.0.1:8001/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_read_timeout 300s;
proxy_buffering off;
}
}
gateway:
ingress:
type: "nginx"
url: "https://finance.agents.example.com"
port: "8001"
bind: "loopback"
trust_proxy: true
allow_preset: "microsoft-teams" admits the Microsoft 365 address ranges Teams calls from. GoClaw downloads them from endpoints.office.com at start-up; if the download fails, the gateway does not start (Ingress failed: …), so the host needs outbound HTTPS to that address.
allow_ips is a list of IPs and CIDR ranges. When it has any entry, it replaces the preset: the Teams ranges are not loaded, and only the listed addresses get in. To admit Teams and your own addresses, use one of these:
allow_ips empty and keep the preset. Callers on loopback are always allowed, so health checks and local tools still get in. Behind a reverse proxy on the same host, set trust_proxy: true: the allowlist then judges the client address the proxy forwards, not the proxy's own loopback address.allow_ips: the Teams ranges (Microsoft publishes them at endpoints.office.com, service area "Skype") and your own proxy or test addresses.gateway:
ingress:
allow_ips: # no allow_preset: these are the only addresses let in
- "10.0.1.5" # the reverse proxy
- "203.0.113.10/32" # a test machine
The allowlist covers every request on the listener it guards. With gateway.ingress.port set, that is the dedicated webhook listener. Without it, webhooks share the main port, and the allowlist covers the Web UI and the API there too: a browser on an address that is not listed gets 403.
Set trust_proxy: true only when the immediate upstream is a proxy you control, and have that proxy set or append X-Forwarded-For with the client address it saw (proxy_set_header X-Forwarded-For $remote_addr; in nginx, as the examples above do; $proxy_add_x_forwarded_for works too). GoClaw takes the last entry, the one your proxy wrote: anything before it is what the caller sent. Otherwise, GoClaw uses the TCP remote address and ignores spoofable forwarding headers.
Loopback callers are always allowed for local health checks and reverse proxy deployments.
| Symptom | Likely cause | Fix |
|---|---|---|
| Prefix URL returns 404 | The reverse proxy did not strip the prefix | Use a trailing slash in proxy_pass |
| WebSocket does not connect | Upgrade and Connection headers are missing |
Forward WebSocket headers |
| Teams endpoint validation fails | Wrong path or method filtering in proxy | Forward all methods to GoClaw; let GoClaw validate |
| Requests are denied by allowlist | GoClaw sees the proxy IP, not the external IP | Enable trust_proxy for a trusted proxy, or add the proxy IP to allow_ips (which replaces allow_preset) |
Gateway exits at start-up with Ingress failed |
allow_preset: microsoft-teams could not download the Microsoft 365 ranges |
Allow outbound HTTPS to endpoints.office.com, or list the ranges in allow_ips |
| Hook URL returns 404 | gateway.ingress.port is set and the URL points at server.port |
Point the sender (or the proxy) at the webhook port |
| WebUI is public unintentionally | Webhook and WebUI share the same public listener | Use gateway.ingress.port and expose only that port |
internal/config/config.gointernal/config/config_loader.gocmd/gateway/main.gocmd/gateway/startup.gointernal/gateway/handler_api.gointernal/runtimeinfo/runtimeinfo.go