Skill: maverick-software/cloudflare-agent-tunnel (version 1.1.0)

--- name: cloudflare-agent-tunnel description: > Give each OpenClaw agent its own secure HTTPS URL using Cloudflare Tunnel (cloudflared). No SSL certificates to manage, no ports to expose publicly. Use when setting up secure cloud access for OpenClaw agents on a VPS, assigning per-agent subdomains (koda.yourdomain.com), enabling HTTPS without nginx or Let's Encrypt, or connecting a custom domain to an agent. Covers named tunnels (permanent URL, free Cloudflare account — preferred), quick tunnels (temporary, no account), multi-agent setup on a single VPS, and custom domain configuration. --- # Cloudflare Agent Tunnel Give each OpenClaw agent a permanent, secure HTTPS URL via Cloudflare Tunnel — no SSL certs, no nginx, no open ports. ## How It Works ``` User → https://koda.yourdomain.com ↓ (Cloudflare edge — TLS termination here) Cloudflare Tunnel (encrypted) ↓ cloudflared process on VPS ↓ http://localhost:18789 (OpenClaw gateway) ``` - Cloudflare handles TLS — no cert management on the server - The local port never needs to be open to the internet - Each agent gets its own `cloudflared` process + systemd service --- ## ✅ Preferred Method — Named Tunnel (Permanent, Free Cloudflare Account) **Always use this method.** Gives a permanent URL tied to your domain. Requires a free Cloudflare account — takes 2 minutes to set up. ### Step 1: Install cloudflared ```bash curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main" \ | tee /etc/apt/sources.list.d/cloudflared.list apt-get update -qq && apt-get install -y cloudflared ``` ### Step 2: Authenticate — give the user this URL Run on the VPS: ```bash cloudflared tunnel login ``` This prints a Cloudflare auth URL. **Give that URL to the user** — they open it in their browser, log into their Cloudflare account, and click Authorize. This saves `/root/.cloudflared/cert.pem` on the VPS. Poll for completion: ```bash # Wait until cert.pem appears (user has authorized) until [ -f /root/.cloudflared/cert.pem ]; do sleep 3; done && echo "Authorized!" ``` ### Step 3: Create the tunnel ```bash cloudflared tunnel create openclaw-koda # Outputs a UUID — note it TUNNEL_UUID=$(cloudflared tunnel list --output json | python3 -c \ "import json,sys; t=[x for x in json.load(sys.stdin) if x['name']=='openclaw-koda']; print(t[0]['id'])") ``` ### Step 4: Write tunnel config ```bash mkdir -p /etc/cloudflared cat > /etc/cloudflared/openclaw-koda.yml << EOF tunnel: ${TUNNEL_UUID} credentials-file: /root/.cloudflared/${TUNNEL_UUID}.json ingress: - hostname: koda.yourdomain.com service: http://localhost:18789 - service: http_status:404 EOF ``` ### Step 5: Route DNS ```bash cloudflared tunnel route dns openclaw-koda koda.yourdomain.com # Automatically creates CNAME: koda.yourdomain.com → <UUID>.cfargotunnel.com ``` The domain must use **Cloudflare nameservers**. If it doesn't yet, the user transfers DNS management to Cloudflare (free, takes ~5 min). ### Step 6: Install as systemd service ```bash cat > /etc/systemd/system/cloudflared-koda.service << 'EOF' [Unit] Description=Cloudflare Tunnel — openclaw-koda After=network.target openclaw.service [Service] Type=simple User=root ExecStart=/usr/bin/cloudflared tunnel --no-autoupdate --config /etc/cloudflared/openclaw-koda.yml run Restart=always RestartSec=5 [Install] WantedBy=multi-user.target EOF systemctl daemon-reload systemctl enable cloudflared-koda systemctl start cloudflared-koda systemctl is-active cloudflared-koda ``` ### Step 7: Update OpenClaw allowedOrigins ```json "gateway": { "controlUi": { "allowedOrigins": [ "http://localhost:18789", "https://koda.yourdomain.com" ] } } ``` Then: `systemctl restart openclaw-koda` ### Step 8: Lock down the port Block direct public access — all traffic must go through the tunnel: ```bash ufw deny 18789 ufw reload ``` --- ## Quick Tunnel (Fallback Only — Temporary) > ⚠️ **Use only as a temporary fallback** when no domain is available. The URL is random and resets every time the service restarts. Switch to a named tunnel as soon as a domain is ready. ```bash # Start quick tunnel — prints a random https://*.trycloudflare.com URL cloudflared tunnel --url http://localhost:18789 --no-autoupdate # Or as a systemd service (URL logged to /var/log/cloudflared-openclaw.log) ExecStart=/usr/bin/cloudflared tunnel --no-autoupdate --url http://localhost:18789 ``` Read the assigned URL: ```bash grep -o 'https://[a-z0-9-]*\.trycloudflare\.com' /var/log/cloudflared-openclaw.log | tail -1 ``` --- ## Multi-Agent Setup (One VPS, Multiple Agents) Each agent = one OpenClaw gateway port + one named tunnel + one systemd service. ``` Port 18789 → openclaw-koda.service + cloudflared-koda.service → koda.yourdomain.com Port 18790 → openclaw-alex.service + cloudflared-alex.service → alex.yourdomain.com Port 18791 → openclaw-jordan.service + cloudflared-jordan.service → jordan.yourdomain.com ``` **Critical:** Do NOT use `cloudflared service install` for multiple agents — it only supports one tunnel and overwrites the system service. Always write individual systemd service files per agent. --- ## Custom Domains Key facts: - Domain must use **Cloudflare nameservers** (transfer at your registrar — free) - Cloudflare issues and auto-renews TLS certs - CNAME records created automatically via `cloudflared tunnel route dns` - Free Cloudflare plan: unlimited tunnels, unlimited bandwidth See `references/custom-domains.md` for a full walkthrough. --- ## Managing Tunnels ```bash # Status systemctl list-units "cloudflared-*" --no-pager # Logs journalctl -u cloudflared-koda -f # List named tunnels cloudflared tunnel list # Delete a tunnel cloudflared tunnel delete openclaw-koda systemctl disable cloudflared-koda && rm /etc/systemd/system/cloudflared-koda.service ```

path: references/custom-domains.md
content: # Custom Domains with Cloudflare Tunnels

Route each OpenClaw agent to its own subdomain on a domain you own.

## Table of Contents
1. [Prerequisites](#prerequisites)
2. [Add Domain to Cloudflare](#add-domain-to-cloudflare)
3. [Authenticate cloudflared](#authenticate-cloudflared)
4. [Create a Named Tunnel](#create-a-named-tunnel)
5. [Point a Subdomain at the Tunnel](#point-a-subdomain-at-the-tunnel)
6. [Multi-Agent Subdomain Layout](#multi-agent-subdomain-layout)
7. [Automatic HTTPS](#automatic-https)
8. [Troubleshooting](#troubleshooting)

---

## Prerequisites

- A domain name (any registrar — you'll point nameservers to Cloudflare)
- A free Cloudflare account at dash.cloudflare.com
- `cloudflared` installed on the VPS
- OpenClaw agents running on known local ports

---

## Add Domain to Cloudflare

1. Log in to [dash.cloudflare.com](https://dash.cloudflare.com)
2. Click **Add a site** → enter your domain → select **Free plan**
3. Cloudflare scans your existing DNS records — review and confirm
4. Copy the two Cloudflare nameservers shown (e.g. `carter.ns.cloudflare.com`)
5. Go to your domain registrar → update nameservers to the Cloudflare ones
6. Wait 5–30 min for propagation

Verify:
```bash
dig NS yourdomain.com +short
# Should return Cloudflare nameservers
```

---

## Authenticate cloudflared

Run once on the VPS. Opens a browser auth flow — if headless, copy the URL to your local browser.

```bash
cloudflared login
# Prints a URL — open it, pick your domain, authorize
# Saves cert.pem to /root/.cloudflared/cert.pem
```

---

## Create a Named Tunnel

```bash
# Create (produces a UUID + credentials file)
cloudflared tunnel create openclaw-koda

# List to confirm
cloudflared tunnel list
# NAME             ID                                   CREATED
# openclaw-koda    a1b2c3d4-...                         2026-03-03
```

Credentials are saved at `/root/.cloudflared/<UUID>.json`. Keep this — it's how the tunnel authenticates.

---

## Point a Subdomain at the Tunnel

### Option A: Using cloudflared CLI (auto-creates CNAME)

```bash
cloudflared tunnel route dns openclaw-koda koda.yourdomain.com
```

This creates a CNAME record:
```
koda.yourdomain.com  →  <UUID>.cfargotunnel.com  (proxied)
```

### Option B: Manually in Cloudflare dashboard

1. Go to **DNS → Records → Add record**
2. Type: **CNAME**
3. Name: `koda`
4. Target: `<UUID>.cfargotunnel.com`
5. Proxy status: **Proxied** (orange cloud ON)
6. Save

---

## Multi-Agent Subdomain Layout

One domain, multiple subdomains — each pointing to its own tunnel:

```
yourdomain.com
├── koda.yourdomain.com    → openclaw-koda tunnel  → localhost:18789
├── alex.yourdomain.com    → openclaw-alex tunnel  → localhost:18790
├── jordan.yourdomain.com  → openclaw-jordan tunnel → localhost:18791
└── (wildcard optional)    → *.yourdomain.com
```

### Config file per agent

`/etc/cloudflared/openclaw-koda.yml`:
```yaml
tunnel: a1b2c3d4-xxxx-xxxx-xxxx-xxxxxxxxxxxx
credentials-file: /root/.cloudflared/a1b2c3d4-xxxx-xxxx-xxxx-xxxxxxxxxxxx.json

ingress:
  - hostname: koda.yourdomain.com
    service: http://localhost:18789
  - service: http_status:404
```

`/etc/cloudflared/openclaw-alex.yml`:
```yaml
tunnel: b2c3d4e5-xxxx-xxxx-xxxx-xxxxxxxxxxxx
credentials-file: /root/.cloudflared/b2c3d4e5-xxxx-xxxx-xxxx-xxxxxxxxxxxx.json

ingress:
  - hostname: alex.yourdomain.com
    service: http://localhost:18790
  - service: http_status:404
```

### Systemd service per agent

`/etc/systemd/system/cloudflared-koda.service`:
```ini
[Unit]
Description=Cloudflare Tunnel — koda
After=network.target

[Service]
Type=simple
User=root
ExecStart=/usr/bin/cloudflared tunnel --no-autoupdate --config /etc/cloudflared/openclaw-koda.yml run
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
```

```bash
# Enable and start all tunnel services
systemctl daemon-reload
systemctl enable cloudflared-koda cloudflared-alex cloudflared-jordan
systemctl start  cloudflared-koda cloudflared-alex cloudflared-jordan
```

**Important:** Do NOT use `cloudflared service install` — it only supports one tunnel and will conflict with manually managed services.

---

## Automatic HTTPS

No cert management required. Cloudflare handles HTTPS termination at their edge:

- Your VPS serves HTTP on localhost — no TLS setup needed on the server
- Cloudflare issues and renews the certificate for `koda.yourdomain.com` automatically
- All traffic is encrypted between the user and Cloudflare's edge
- Traffic between Cloudflare and your VPS travels through the tunnel (encrypted)

**OpenClaw `allowedOrigins` must include the HTTPS URL** (not just HTTP):

```json
"gateway": {
  "controlUi": {
    "allowedOrigins": [
      "http://localhost:18789",
      "https://koda.yourdomain.com"
    ]
  }
}
```

---

## Troubleshooting

**Tunnel connects but UI shows origin/CORS error:**
- Add `https://koda.yourdomain.com` to `allowedOrigins` in openclaw.json and restart

**`cloudflared tunnel route dns` fails:**
- Ensure domain is in Cloudflare (not just registered — nameservers must point to Cloudflare)
- Ensure `cert.pem` exists: `ls /root/.cloudflared/cert.pem`
- Add the CNAME record manually in the dashboard instead

**Tunnel starts but immediately exits:**
- Check credentials file exists: `ls /root/.cloudflared/<UUID>.json`
- Check config YAML syntax: `cloudflared tunnel --config /etc/cloudflared/openclaw-koda.yml ingress validate`

**Multiple tunnel services conflicting:**
- Never run `cloudflared service install` after the first tunnel — it overwrites the system service
- Each extra agent needs a manually written systemd service file (as shown above)

**Free Cloudflare plan limits:**
- Unlimited tunnels ✅
- Unlimited bandwidth ✅
- No WAF on free plan (available on Pro+)
- Custom domains require a domain you own — `trycloudflare.com` quick tunnels are free with no account


path: scripts/tunnel-setup.sh
content: #!/bin/bash
# tunnel-setup.sh — Add a Cloudflare Tunnel for one OpenClaw agent
#
# Usage:
#   ./tunnel-setup.sh --agent koda --port 18789 [--domain koda.yourdomain.com]
#
# Requires cloudflared installed and authenticated (cloudflared login) before use.
# Without --domain: creates a quick tunnel (random URL, no account needed).
# With --domain: creates a named tunnel (permanent URL, requires Cloudflare account).

set -euo pipefail

AGENT=""
PORT=""
DOMAIN=""
QUICK=false

while [[ $# -gt 0 ]]; do
  case $1 in
    --agent)   AGENT="$2";  shift 2 ;;
    --port)    PORT="$2";   shift 2 ;;
    --domain)  DOMAIN="$2"; shift 2 ;;
    --quick)   QUICK=true;  shift ;;
    *) echo "Unknown arg: $1"; exit 1 ;;
  esac
done

[[ -z "$AGENT" ]] && { echo "Error: --agent is required"; exit 1; }
[[ -z "$PORT"  ]] && { echo "Error: --port is required";  exit 1; }

# Install cloudflared if missing
if ! command -v cloudflared &>/dev/null; then
  echo "Installing cloudflared..."
  curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
  echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main" \
    | tee /etc/apt/sources.list.d/cloudflared.list
  apt-get update -qq && apt-get install -y cloudflared
  echo "cloudflared installed: $(cloudflared --version)"
fi

# ─── Quick Tunnel (no account, temporary URL) ────────────────────────────────
if [[ "$QUICK" == "true" || -z "$DOMAIN" ]]; then
  echo ""
  echo "Starting quick tunnel for ${AGENT} on port ${PORT}..."
  echo "(URL is temporary and resets on restart — use named tunnels for production)"
  echo ""
  exec cloudflared tunnel --url "http://localhost:${PORT}" --no-autoupdate
fi

# ─── Named Tunnel (permanent URL, requires Cloudflare account + domain) ──────

TUNNEL_NAME="openclaw-${AGENT}"
CONFIG_DIR="/etc/cloudflared"
CONFIG_FILE="${CONFIG_DIR}/${TUNNEL_NAME}.yml"
CREDS_DIR="/root/.cloudflared"

mkdir -p "$CONFIG_DIR" "$CREDS_DIR"

# Create tunnel (idempotent — skip if already exists)
if cloudflared tunnel list 2>/dev/null | grep -q "$TUNNEL_NAME"; then
  echo "Tunnel '${TUNNEL_NAME}' already exists — reusing it"
  TUNNEL_UUID=$(cloudflared tunnel list 2>/dev/null | grep "$TUNNEL_NAME" | awk '{print $1}')
else
  echo "Creating tunnel '${TUNNEL_NAME}'..."
  TUNNEL_UUID=$(cloudflared tunnel create "$TUNNEL_NAME" 2>&1 | grep -oP '[0-9a-f-]{36}' | head -1)
  echo "Created tunnel UUID: ${TUNNEL_UUID}"
fi

CREDS_FILE="${CREDS_DIR}/${TUNNEL_UUID}.json"

# Write config file
cat > "$CONFIG_FILE" << EOF
tunnel: ${TUNNEL_UUID}
credentials-file: ${CREDS_FILE}

ingress:
  - hostname: ${DOMAIN}
    service: http://localhost:${PORT}
  - service: http_status:404
EOF

echo "Config written: ${CONFIG_FILE}"

# Route DNS (adds CNAME record in Cloudflare dashboard)
echo "Routing DNS: ${DOMAIN} → ${TUNNEL_NAME}..."
cloudflared tunnel route dns "$TUNNEL_NAME" "$DOMAIN" || {
  echo ""
  echo "⚠ DNS routing failed — you may need to add this CNAME manually in Cloudflare:"
  echo "   Name: $(echo $DOMAIN | cut -d. -f1)"
  echo "   Target: ${TUNNEL_UUID}.cfargotunnel.com"
  echo "   Proxy: ON (orange cloud)"
}

# Create systemd service
SERVICE_NAME="cloudflared-${AGENT}"
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"

cat > "$SERVICE_FILE" << EOF
[Unit]
Description=Cloudflare Tunnel for OpenClaw agent: ${AGENT}
After=network.target

[Service]
Type=simple
User=root
ExecStart=/usr/bin/cloudflared tunnel --no-autoupdate --config ${CONFIG_FILE} run
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable "$SERVICE_NAME"
systemctl restart "$SERVICE_NAME"
sleep 3
systemctl status "$SERVICE_NAME" --no-pager | head -10

echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "  ✅ Tunnel active for agent: ${AGENT}"
echo ""
echo "  URL:     https://${DOMAIN}"
echo "  Port:    localhost:${PORT}"
echo "  Tunnel:  ${TUNNEL_NAME} (${TUNNEL_UUID})"
echo "  Service: ${SERVICE_NAME}.service"
echo ""
echo "  Manage:"
echo "    systemctl status ${SERVICE_NAME}"
echo "    journalctl -u ${SERVICE_NAME} -f"
echo "    systemctl restart ${SERVICE_NAME}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)