The OpenClaw gateway is the single point of contact between your bot and the internet, which makes it the single most valuable target. Production-safe hardening isn't one strong control — it's three independent layers (network isolation via Tailscale, password authentication, and allowFrom authorization) so any single failure still leaves the bot unreachable.
This post is the deep-dive companion to the full hardening guide. The pillar covers the whole 30-minute hardening checklist; this one focuses on the gateway, which is where most of the actual attack surface lives. Defaults and JSON paths come from the OpenClaw docs.
Three-layer defense for the OpenClaw gateway
The gateway listens on a port (default 8080), accepts authenticated requests over that port, and turns those requests into command execution. From an attacker's view it's the bot's mouth and hands: get a request through, get the bot to do something. Compromise the gateway and you compromise everything else, no matter how locked down the rest of your config is.
A single security control isn't enough because every control I've seen has at least one realistic failure mode:
- Tailscale ACLs drift. Misconfigure or forget to scope an exit node and a tailnet device that should not have gateway access suddenly does.
- Passwords leak. Pasted into a chat, committed to git, written to a config backup synced to Dropbox.
- Channel relays get compromised. Telegram add-bot-to-group, hijacked Slack token, Discord webhook posted publicly.
Each of those failures is rare on its own. None of them simultaneously bypasses three independent layers. So the goal is: bind the network so only your tailnet can reach the port; require a password on top in case the tailnet leaks; restrict the allowed senders so even with a valid connection and password, only you can issue commands.
The three layers, in the order a request hits them:
- Network —
gateway.bind: loopback+gateway.tailscale.mode: serve. Defines who can reach the port at all. - Authentication —
gateway.auth.mode: password. Defines who can establish a session with the gateway. - Authorization —
gateway.allowFrom+ per-channeldmPolicyandallowFrom. Defines who can issue commands once the session is up.
The next three sections build each layer in order. The last section verifies the result.
Lock down the network with Tailscale + loopback bind
The single highest-impact change: stop exposing the gateway port to the public internet. Two settings do this together.
gateway.bind: loopback tells the gateway to listen only on 127.0.0.1. Traffic arriving on eth0, wlan0, or any other public-facing interface gets dropped at the kernel level before the gateway sees it. The port becomes invisible to external port scanners.
gateway.tailscale.mode: serve then exposes the gateway over your Tailscale tailnet. With this combination, only devices in your tailnet can reach the gateway, and the traffic between them rides over WireGuard-encrypted Tailscale tunnels.
{
"gateway": {
"bind": "loopback",
"port": 52914,
"tailscale": {
"mode": "serve"
},
"auth": {
"allowTailscale": true
}
}
}
The allowTailscale: true flag tells the gateway to trust requests already authenticated by Tailscale itself. Without it, even tailnet devices would still need to send the gateway password — fine for paranoia, redundant for most setups.
Set up Tailscale on the host once:
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
The first tailscale up opens a one-time login URL. After that, the host is in your tailnet and reachable only from devices you've explicitly added.
Verify the bind worked:
ss -tlnp | grep 52914
You should see exactly one line, and the address should be 127.0.0.1:52914 — never 0.0.0.0:52914 or *:52914. If you see those, the gateway is still reachable from outside; double-check that bind: loopback was actually saved.
What this layer defends against: opportunistic port scanners, default-credential brute-force bots, exposed-service search engines like Shodan. What it does not defend against: someone who is already inside your tailnet (an old laptop you forgot to remove from Tailscale, a phone with stale credentials).
Add password authentication as your second layer
If the network layer fails — the laptop above, a misconfigured ACL, an exit-node bug — the gateway should still reject unauthorized requests. That's what the password layer is for.
Generate a strong password:
openssl rand -base64 30
This produces 30 random bytes, base64-encoded — about 40 characters of unambiguous, paste-safe randomness. That is well above any threshold a brute-forcer can practically crack.
Add it to gateway.auth:
{
"gateway": {
"auth": {
"mode": "password",
"password": "PASTE-YOUR-OPENSSL-OUTPUT-HERE",
"allowTailscale": true
}
}
}
Tighten the file permissions immediately:
chmod 600 ~/.openclaw/openclaw.json
Mode 600 means owner-read-write-only — no group, no world. The password sits next to your allowFrom IDs and channel credentials, so the file is the entire crown jewel.
Two non-negotiables: never commit this file to git, and never sync it to a service that doesn't encrypt at rest. Add it to .gitignore if your home directory is under version control. If you back up ~/.openclaw/, encrypt the backup.
Rotation: rotate the password when someone with access leaves, when you suspect a leak, or when a backup containing the file is exposed. Time-based rotation cadences are mostly cargo-cult for personal-use bots — what matters is rotating on a real signal, not on the calendar.
What this layer defends against: anyone who slipped past the network layer but doesn't know your password. What it does not defend against: a compromised admin laptop where the file is already readable.
Restrict who can connect with allowFrom and dmPolicy
The third layer narrows the allowed senders. Two scopes matter here, and confusing them is the most common config mistake I have seen on this:
- Gateway-level allowFrom —
gateway.allowFrom— restricts which authenticated clients can connect to the gateway at all. - Channel-level allowFrom —
channels.telegram.allowFrom,channels.slack.allowFrom, etc. — restricts which users within that channel can issue commands once a session is established.
Both should be set. Here's a Telegram example with both layers in place:
{
"channels": {
"telegram": {
"dmPolicy": "pairing",
"groupPolicy": "disabled",
"allowFrom": ["tg:YOUR_TELEGRAM_USER_ID"]
}
}
}
dmPolicy: pairing means OpenClaw only accepts direct messages from users explicitly paired in allowFrom. groupPolicy: disabled means it ignores group-chat messages entirely, even if the bot is added to a group. The combination is silent — non-listed users get no error, no rate-limit response, no acknowledgement at all. They simply look at a bot that doesn't exist.
Find your Telegram user ID by messaging @userinfobot and sending /start. The tg: prefix in the allowFrom entry tells OpenClaw which channel namespace the ID belongs to.
What goes wrong if you skip this layer:
- Someone adds your bot to a group chat. Without
groupPolicy: disabled, group members can issue commands. - A coworker glances at your phone, sees the bot's message thread, then DMs the bot from their own account. Without
allowFrom, they can issue commands. - The bot's username gets enumerated. Without
dmPolicy: pairing, anyone who finds it can DM-spam it until you notice.
The same pattern applies to Slack and Discord — different channel keys (channels.slack, channels.discord), same allowFrom semantics. Check the OpenClaw docs for channel-specific ID formats.
Change the default port and verify the lockdown
With three layers configured, the last housekeeping step is to move off the default port. OpenClaw ships with port: 8080, which is one of the most-scanned ports on the public internet — every open-port discovery service has it on their hit list. Pick a high random port instead:
{
"gateway": {
"port": 52914,
"bind": "loopback"
}
}
Any unused port between 10000 and 65535 works. Avoid the well-known low-numbered ports (under 1024 require root anyway). Restart the gateway:
openclaw gateway restart
Now verify the whole lockdown with three commands.
1. The port is loopback-only:
ss -tlnp | grep 52914
The output should show 127.0.0.1:52914 and nothing else. If it shows 0.0.0.0:52914 or :::52914, the gateway is bound to all interfaces — fix bind and restart.
2. The configuration is internally consistent:
openclaw doctor
doctor validates that all three layers are wired up correctly: network bind, password presence, allowFrom scoping. Run with --fix to auto-correct file permissions and other safe issues.
3. The port is invisible from outside the tailnet:
From a different machine that is not in your tailnet (a phone on cellular, a friend's laptop, a cloud VM), run:
nmap -p 52914 YOUR_PUBLIC_IP
The port should report as filtered or closed. If it comes back open, your gateway is reachable from the public internet and one of the previous steps was misapplied — go back to bind: loopback and re-verify with command 1.
While you are auditing, look for these three common misconfigurations:
gateway.bind: 0.0.0.0instead ofloopback— exposes the port to every network interface.gateway.allowFrom: ["*"]or missing entirely — lets any authenticated client connect.~/.openclaw/openclaw.jsonchecked into git or synced to an unencrypted backup — leaks the password and the allowFrom IDs.
If those three commands all return what they should, you've got three independent layers between an attacker and your bot. That's the bar I aim for in production, and it's where I stop adding controls — past that, the marginal hardening rarely justifies the operational friction.