127.0.0.1, verify a shared token on every connect, and merge both lanes into one regional health record. It is the SSH-forward article’s cousin—same low-exposure posture, different transport.
Most OpenClaw fleets on vpshalo end up with one of three perimeters: a Tailscale MagicDNS overlay, a hand-rolled SSH reverse tunnel from loopback, or—covered here—an frp STCP chain. STCP is attractive when overlay meshes are forbidden and GatewayPorts on bastions has scared the security team: visitors must present a secret key before any TCP byte reaches your Mac, and both ends bind to 127.0.0.1. Pair the layout with the GeoDNS and SSH latency matrix for PoP picks, the Cloudflare Tunnel write-up when public TLS belongs at the edge, and the JumpHost & merged probes runbook for upstream check shapes. Public CTAs land on Home, Pricing, Help, and Purchase—no account wall.
Three operational pains shape the design. 1) “Demo” bindings on 0.0.0.0 are how perimeters die; STCP makes the easy path also the loopback path. 2) Token sprawl—each region copying secrets into systemd—becomes one auth.token for frps plus one secretKey per STCP proxy, both rotatable on a calendar. 3) Probe noise—gateway green while doctor stalls, or vice versa—merges into a single regional record so on-call sees “JP red,” not two pages for the same flap.
| Concern | Mechanism | Acceptance signal |
|---|---|---|
| Hide gateway port from the internet | frp STCP visitor + frps token | ss -ltn shows OpenClaw only on 127.0.0.1 on builder and visitor |
| Authenticate before any TCP reaches OpenClaw | frps auth.token + STCP secretKey |
Reject counters spike on key drift; OpenClaw logs unchanged |
| Per-region isolation | One STCP proxy_name per PoP (e.g. openclaw-sin-gw) |
Tokyo visitor cannot dial Singapore by typo—name mismatch fails |
| Health rollup across gateway + doctor | Merge worker joins both lanes per region | One alert per regional incident, not per endpoint |
Roles, ports, and the gateway/doctor chain
frps runs on a small Linux bastion you trust. It binds bindPort = 7000, enables auth.method = "token" with a long random auth.token, and optionally exposes a dashboard on 7500 behind HTTPS and basic auth. Firewall 7000/tcp to your operator subnets and your builder egress IPs—never the open internet if you can avoid it. frpc on each vpshalo Mac builder is outbound only to bastion 7000 and registers two STCP proxies per region: openclaw-{region}-gw mapping 127.0.0.1:18080 (the OpenClaw gateway) and openclaw-{region}-doctor mapping 127.0.0.1:18081 (the doctor health endpoint). Each proxy carries its own secretKey.
frpc visitors live on the operator workstation or on the merge worker. They are also outbound only and bind back to 127.0.0.1—never a LAN address. By convention we map 28080 for gateway and 28081 for doctor, so a single curl http://127.0.0.1:28080/healthz exercises the whole chain: visitor → frps token check → STCP secret check → builder loopback → OpenClaw. The gateway serves model traffic; the doctor answers a richer health JSON (queue depth, model version, GPU/Metal context). Chaining both means a stuck doctor (slow tokenizer reload) is visible even when gateway 200s look fine.
Minimal reproducible steps
Run these once per region; substitute your inventory tags for {region} (e.g. sin, tyo, hkg, icn, lax).
- Step 1 — Pin OpenClaw to loopback. Set
OPENCLAW_HOME=/var/lib/openclaw/{region}(or the macOS equivalent under/usr/local/var) and start with--gateway-bind 127.0.0.1:18080 --doctor-bind 127.0.0.1:18081. Verifycurl http://127.0.0.1:18080/healthzandcurl http://127.0.0.1:18081/doctoronly from localhost. - Step 2 — Stand up frps on the bastion.
frps -c frps.tomlwithbindPort = 7000,auth.method = "token", and a long randomauth.token. Restrict 7000/tcp to operator and builder subnets at the SG/UFW/nftlayer. - Step 3 — Wire each builder’s frpc. Two STCP proxies, distinct
secretKeyvalues, noremotePort—STCP refuses to expose a public port by design. Run frpc as a systemd or launchd unit with restart-on-failure and exponential backoff so flaps do not tempt operators to relax firewall rules. - Step 4 — Wire the visitor side. Two STCP visitors on the operator workstation or merge worker, both binding
127.0.0.1(28080 gateway, 28081 doctor). ThesecretKeyon the visitor must match the proxy. Verify with a localhostcurlbefore granting any teammate access. - Step 5 — Chain gateway and doctor in one supervisor. A small worker calls
127.0.0.1:28080/healthzthen127.0.0.1:28081/doctor, joins both into{region, gw_ms, doctor_ms, status, error_class}, and posts to your TSDB. Page only when both lanes fail in the same five-minute bucket; escalate to multi-region only when two regions degrade together.
# frpc.toml — builder side, two STCP proxies on loopback
serverAddr = "bastion-sin.vpshalo.example"
serverPort = 7000
auth.method = "token"
auth.token = "<long-random-frps-token>"
[[proxies]]
name = "openclaw-sin-gw"
type = "stcp"
secretKey = "<sk-sin-gw>"
localIP = "127.0.0.1"
localPort = 18080
[[proxies]]
name = "openclaw-sin-doctor"
type = "stcp"
secretKey = "<sk-sin-doctor>"
localIP = "127.0.0.1"
localPort = 18081
# frpc.toml — visitor side, both lanes bind only to 127.0.0.1
[[visitors]]
name = "v-openclaw-sin-gw"
type = "stcp"
serverName = "openclaw-sin-gw"
secretKey = "<sk-sin-gw>"
bindAddr = "127.0.0.1"
bindPort = 28080
[[visitors]]
name = "v-openclaw-sin-doctor"
type = "stcp"
serverName = "openclaw-sin-doctor"
secretKey = "<sk-sin-doctor>"
bindAddr = "127.0.0.1"
bindPort = 28081
0.0.0.0. The whole point is that loopback stays loopback.Three things worth quoting in your runbook
- Two scalars per region rotate: one frps
auth.tokenshared by all builders in a region, plus one STCPsecretKeyper proxy. That is far less surface than a forest ofssh_configentries. - Brute-force scans never reach OpenClaw. STCP requires the visitor secret to match before any TCP segment is forwarded; failed connects increment frps reject counters, not OpenClaw access logs.
- Loopback on both ends means the auditor’s preferred posture (“all traffic is on localhost”) is also your default—exceptions are rare, documented, and reviewed.
Troubleshooting playbook
Visitor cannot connect. Confirm bastion 7000/tcp is reachable from the visitor IP and that the operator subnet is allow-listed. frps logs will show auth failed on token mismatch and visitor not allowed on STCP secret mismatch—rotate one variable at a time. Gateway 200 but doctor stalls. Almost always a runaway model thread on the builder; the doctor lane is doing its job. Restart the OpenClaw worker, do not touch frp. STCP works but latency is high. When NAT traversal fails, STCP relays through frps; pin builders to a vpshalo PoP close to the bastion (use the SCP/SFTP RTT matrix for picks). Token drift after rotation. Visitor still uses the old secretKey; rotate proxy and visitor in the same maintenance window and confirm with curl 127.0.0.1:28080/healthz before silencing alerts. Probes flap together. If gateway and doctor both fail at the exact same second, it is the tunnel, not OpenClaw—check frps process supervision and ServerAliveInterval-style keepalives on frpc.
auth.method, STCP option spellings, and visitor bindAddr against the build you deploy. OpenClaw releases differ in --gateway-bind and --doctor-bind spellings—this article illustrates intent, not a vendor SLA. The pattern complements rather than replaces SSH or Tailscale guides linked above.
When the steps above are scripted, the perimeter becomes boring on purpose: loopback inside each vpshalo region, an authenticated frp STCP tunnel, no public listener, calendar-driven token rotation, and a single merged alert per region instead of an inbox of probe noise. Provision remote Mac builders in the PoPs you actually measured; capacity, registry mirrors, and frp visitors should move together.
Place builders next to your bastion
Open Home, compare Pricing, read Help, browse the Blog, or jump to Purchase. Lower RTT between frps and frpc stabilises STCP relays and probe windows together—no login wall on public pricing or help pages.
Region purchase pages: Tokyo · Seoul · Hong Kong · Singapore · US West