Security Hardening: Windows Host for OpenClaw (Native Node, Optional WSL)
Practical hardening runbook for a Windows 11 machine that takes part in an OpenClaw setup. Covers Windows Firewall, RDP/SSH/SMB lockdown, the native node and its credentials, sleep prevention, and what changes if you also keep WSL2 around.
Why This Guide Changed Shape
The first version of this guide assumed the agent lived inside WSL2: two attack surfaces, Windows and Linux, with port proxies bridging them. That is no longer how I run the machine. The Windows desktop is now a native execution host. An OpenClaw node runs as a Windows Scheduled Task and dials out to the gateway on my Linux control host. Brigade workers, Codex, Claude Code, and the other harnesses run as native Windows processes. WSL2 Ubuntu is still installed, capped at 2 GiB of RAM with no swap, and stopped. Nothing needs it for ordinary work.
That changes the hardening list:
- The agent’s API keys and workspace are on the Windows filesystem, so Windows ACLs and Windows credential storage matter most.
- The node connects outbound to the gateway. It does not need an inbound rule for the gateway port, and it does not need port proxies.
- Port proxies, WSL file permissions, and WSL service binding only matter if you still run services in WSL. Those sections are now optional (section 6).
If you do run your agent inside WSL2, keep section 6 and the port-proxy cleanup in section 2 as your main path. The Windows-side firewall rules apply either way.
Overview of Changes
| Component | What Changed |
|---|---|
| Windows Firewall | RDP and SSH restricted to the LAN subnet. SMB restricted or disabled |
| OpenSSH server | Key-only, with the administrators-group key file gotcha understood |
| Native node | Runs as a Scheduled Task, outbound only, version kept in step with the gateway |
| Credentials | Machine-bound credential storage instead of plaintext files |
| Port Proxies | None needed natively. Audit them if you run WSL services |
| Sleep Prevention | System unattended sleep timeout disabled |
| WSL (optional) | Capped, stopped unless used, services bound deliberately |
1. Windows Firewall: RDP, SSH, SMB
The Problem
Default Windows installs often have RDP (3389) and SSH (22) listening on all interfaces. If your machine has a public IP or is on a flat network, anyone can attempt authentication.
Restrict RDP to LAN Only
Open PowerShell as Administrator:
# Remove any existing broad RDP rules
Get-NetFirewallRule -DisplayName "*Remote Desktop*" | Remove-NetFirewallRule
# Create LAN-only RDP rule
New-NetFirewallRule -DisplayName "RDP - LAN Only" `
-Direction Inbound `
-LocalPort 3389 `
-Protocol TCP `
-Action Allow `
-RemoteAddress <LAN_SUBNET_CIDR>
Replace <LAN_SUBNET_CIDR> with your LAN subnet.
Restrict SSH to LAN Only
# Remove existing SSH rules
Get-NetFirewallRule -DisplayName "*SSH*" | Remove-NetFirewallRule
# Create LAN-only SSH rule
New-NetFirewallRule -DisplayName "SSH - LAN Only" `
-Direction Inbound `
-LocalPort 22 `
-Protocol TCP `
-Action Allow `
-RemoteAddress <LAN_SUBNET_CIDR>
SMB: Disable It or Scope It
If you do not use file sharing, disable SMB entirely:
# Disable SMB server
Set-SmbServerConfiguration -EnableSMB1Protocol $false -Force
Set-SmbServerConfiguration -EnableSMB2Protocol $false -Force
# Or block via firewall
New-NetFirewallRule -DisplayName "SMB - Block All" `
-Direction Inbound `
-LocalPort 445 `
-Protocol TCP `
-Action Block
Note: My desktop shares media over SMB, so port 445 is listening on it. If you use a NAS or local file shares, do not disable SMB. Restrict it to your LAN subnet like the RDP and SSH rules above, and keep SMB1 off.
The gateway port needs no inbound rule here
The OpenClaw gateway runs on the Linux control host, and the Windows node dials out to it. If you copied a “gateway LAN-only” rule onto the Windows machine from an older version of this guide, it is opening a port nothing listens on. Remove it unless you run a gateway on this machine.
Verify
# List all inbound allow rules
Get-NetFirewallRule -Direction Inbound -Action Allow |
Where-Object { $_.Enabled -eq 'True' } |
Format-Table DisplayName, Profile, Direction -AutoSize
# Check for broad allow rules (should return nothing suspicious)
Get-NetFirewallRule -Direction Inbound -Action Allow |
Get-NetFirewallAddressFilter |
Where-Object { $_.RemoteAddress -eq 'Any' }
2. Port Proxies (Only If You Run WSL Services)
WSL2 uses a NAT network. To reach a WSL service from other devices on your LAN, you add a port proxy with netsh interface portproxy. Over time these accumulate as you spin dev servers up and down, leaving stale proxies pointing at nothing. Every port proxy is a potential entry point.
On a native-node machine the list should be empty:
netsh interface portproxy show all
Expected output on my desktop: nothing at all. If you see entries, audit each one:
- Is the WSL service still running? If not, delete the proxy.
- Does it need to listen on 0.0.0.0? If only you use it, bind to your LAN IP.
- Is there a matching, LAN-scoped firewall rule? A proxy without a firewall rule is still blocked (good). A proxy with a broad firewall rule is open (bad).
Remove a stale one:
netsh interface portproxy delete v4tov4 listenport=<port> listenaddress=0.0.0.0
Every service you expose from WSL needs BOTH a port proxy AND a firewall rule. Miss either one and it will not work, or it will work but unfiltered. That is the most common mistake in WSL networking.
3. Windows OpenSSH
The Windows OpenSSH server is how my control host reaches this machine. Keep it key-only and know one gotcha that cost me an evening:
- If the Windows user is in the local Administrators group,
sshdignores the user’s normal~\.ssh\authorized_keysand readsC:\ProgramData\ssh\administrators_authorized_keysinstead, which must be ACL’d to Administrators and SYSTEM only. Promoting a user to admin therefore silently breaks key auth until you move the key. - The client side matters too. Windows OpenSSH will give up after the server’s
MaxAuthTriesif your agent offers several unrelated keys first. UseIdentitiesOnly yesand an explicitIdentityFilein the client config. - Unattended Git over an SSH session into this machine can stall even when desktop Git works. Keep GitHub writes on the control host unless you have tested the path unattended.
Verify the service and listeners:
sc query sshd | findstr STATE
netstat -an | findstr LISTENING | findstr ":22 "
Expected: STATE : 4 RUNNING and a listener on port 22.
4. The Native Node and Its Credentials
How the node runs
The node is a Windows Scheduled Task that launches openclaw ... node run. Two facts to know before you audit it:
- The task can show
Readywith result0while the child process keeps running and stays connected. Do not judge health by the task state alone. - Local
openclaw node statuscan report “stopped” while the node is connected. Verify from the gateway withopenclaw nodes status, then confirm the child process on Windows if you need to.
Never paste raw node status, approval JSON, or service launcher contents into notes or issue reports. They can contain gateway token material.
Keep the node version in step with the gateway
Build or stage the exact OpenClaw package on the control host, copy the tarball to the node, install it with npm.cmd install -g <tarball>, then run openclaw node install --force and openclaw node restart. Do not install openclaw@latest independently. A node can connect while publishing stale or empty capabilities, so after every upgrade check from the gateway and invoke system.which for powershell, node, npm, and git.
Credentials on Windows
The Linux-side pattern in secret-management.md (service account to a rendered env file in tmpfs) has a Windows counterpart with different storage:
- Use a read-only service account for the runtime vault, and keep its token sealed with DPAPI (user-bound) rather than in a plaintext file or an environment variable in the registry.
- DPAPI has two incompatible on-disk forms: the text written by
ConvertFrom-SecureStringand rawProtectedDatabytes. Mixing them gives a misleading “input string was not in a correct format” error. - Render or inject only the values a task needs. Do not hand the final application the service-account token.
- Keep the config directory private to your user:
icacls "$env:USERPROFILE\.openclaw" /inheritance:r /grant:r "$($env:USERNAME):(OI)(CI)F"
Check the result with icacls "$env:USERPROFILE\.openclaw". It should list only your user (and, if you choose to keep them, SYSTEM and Administrators).
5. Windows Sleep Prevention
The Problem
Windows has a hidden “System Unattended Sleep Timeout” that is separate from your normal power settings. Even with sleep disabled in Settings and PowerToys Awake running, the machine can still sleep after this hidden timeout expires. When it does, the node and any workers go offline.
The Fix
PowerShell as Administrator:
# Disable unattended sleep timeout (the hidden one)
powercfg /SETACTIVETIMEOUT 0
powercfg /SETDCVALUEINDEX SCHEME_CURRENT SUB_SLEEP UNATTENDSLEEP 0
powercfg /SETACVALUEINDEX SCHEME_CURRENT SUB_SLEEP UNATTENDSLEEP 0
powercfg /S SCHEME_CURRENT
# Disable hibernate
powercfg /hibernate off
# Disable regular sleep too
powercfg /change standby-timeout-ac 0
powercfg /change standby-timeout-dc 0
Note: PowerToys Awake is NOT sufficient. It does not override the system unattended sleep timeout. Set these power config values directly.
Verify
powercfg /query SCHEME_CURRENT SUB_SLEEP
Look for UNATTENDSLEEP set to 0x00000000.
6. If You Also Keep WSL2
WSL is optional on this machine and I keep it stopped. If you run services in it, harden it like any Linux host (see linux-hardening.md) plus the points below.
Contain it
Cap its resources in %USERPROFILE%\.wslconfig so a runaway process cannot eat the host (mine is capped at 2 GiB of RAM with no swap):
[wsl2]
memory=2GB
swap=0
Check whether it is running before you do anything that restarts it, since wsl --shutdown kills every distro and any workload in it:
wsl -l -v
Expected on my desktop: the Ubuntu distro with state Stopped.
File permissions inside WSL
chmod 700 ~/.openclaw
chmod 600 ~/.openclaw/openclaw.json
chmod -R 700 ~/.openclaw/workspace
Audit inside WSL
who # who is logged in
ls -la ~/.ssh/ # any unexpected keys
ss -tlnp # what is listening
ss -tnp | grep ESTABLISHED # any unexpected outbound connections
crontab -l; ls /etc/cron.d/ # unexpected scheduled jobs
All outbound connections should be explainable: your AI provider’s API, your messaging platform’s API, and the gateway. Anything else warrants investigation.
Service binding
A WSL service that must be reachable from other devices has to bind to 0.0.0.0 (not 127.0.0.1), which also makes it reachable from the Windows host. Access control then happens at the Windows Firewall layer, with a matching port proxy (section 2). Keep services that should stay local (databases, Ollama) on 127.0.0.1.
DNS sinkholes and Claude Code in WSL
If you run Mullvad, Pi-hole, AdGuard, or another DNS sinkhole, an older Claude Code build hung at startup when telemetry hosts resolved to 0.0.0.0. Setting CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC to 1 in the env block of ~/.claude/settings.json was the workaround. I found this on a 2026 spring build and have not re-tested it on 2.1.302, so treat it as a fallback if you see a startup hang.
7. Tool Permissions
The agent’s exec and elevation settings are covered in agent-security-hardening.md. On a native Windows node, also check which commands the gateway is allowed to run on the node: node commands run as the node’s Windows user, so treat that user’s file access as the agent’s file access. Run the node under a standard (non-administrator) account if your workloads allow it.
Verification
Windows Side (PowerShell as Admin)
Write-Host "=== Firewall Rules ==="
Get-NetFirewallRule -Direction Inbound -Action Allow |
Where-Object { $_.Enabled -eq 'True' } |
Format-Table DisplayName -AutoSize
Write-Host "`n=== Port Proxies ==="
netsh interface portproxy show all
Write-Host "`n=== Listeners (22, 445, 3389) ==="
netstat -an | findstr LISTENING | findstr ":22 :445 :3389"
Write-Host "`n=== Sleep Config ==="
powercfg /query SCHEME_CURRENT SUB_SLEEP UNATTENDSLEEP
Write-Host "`n=== SMB Status ==="
Get-SmbServerConfiguration | Select-Object EnableSMB1Protocol, EnableSMB2Protocol
Write-Host "`n=== WSL ==="
wsl -l -v
Expected: no broad (Any remote address) allow rules for 22, 3389, or 445, an empty port-proxy list, EnableSMB1Protocol false, UNATTENDSLEEP at 0x00000000, and WSL either absent or Stopped.
From the gateway
openclaw nodes status
Expected: the Windows node listed as connected. Filter the output and do not paste it anywhere, because raw node status can include token material.
What This Doesn’t Cover
- BitLocker disk encryption (enable it if you have not)
- Windows Defender configuration (review exclusions for any agent working directories)
- Windows Update policies (keep auto-updates on for security patches)
- Network segmentation (VLANs for isolating a dev machine)
- Cloudflare Zero Trust / Tailscale (for secure remote access without port exposure)
- Backup and disaster recovery (encrypted backups of the OpenClaw workspace)
See the Linux hardening guide for patterns that apply to the Linux side (fail2ban, SSH config, UFW), and wazuh-triage.md for putting a Windows agent into your SIEM.
Gotchas
-
Two surfaces only if you use both. A native-node Windows machine has one main surface, Windows. A WSL-based setup has two. Audit the ones you actually run, and stop an unused WSL distro rather than leaving it up.
-
Port proxies accumulate. On my first audit of the WSL-era setup I found 14 stale proxies from archived projects. Run
netsh interface portproxy show allmonthly, even if you think you have none. -
The sleep timeout is invisible. It does not show up in the normal Windows Settings UI. Use
powercfgto find and disable it. This is the most common cause of “my agent went offline overnight” on Windows. -
Firewall profile matters. Make sure your network is set to “Private”. Public profile rules are more restrictive and can block expected traffic unexpectedly.
-
Test from another device. After applying firewall changes, test access from another machine on the network. Verify that allowed services work and everything else is blocked.
-
Admin group membership breaks SSH keys. Promoting a Windows user to local Administrators makes
sshdread the shared administrators key file instead of the user’s own, and key auth fails until you move the key there. -
A
ReadyScheduled Task is not a healthy node. The task can return toReadywith exit code 0 while the child runs, and local node status can say “stopped” for a connected node. Check from the gateway. -
Windows machines running their own Claude Code need the same cross-machine rules. If you run a separate Claude Code instance on the Windows desktop, mirror the cross-machine guardrails into
C:\Users\<you>\.claude\CLAUDE.md. Otherwise the two instances drift on safety rules. -
Keep node versions in lockstep with the gateway. Install the exact tarball built on the control host, then verify capabilities from the gateway. A connected node with stale capabilities is easy to miss.