[{"data":1,"prerenderedAt":1701},["ShallowReactive",2],{"writing-open-claw-secure-hardened-setup":3},{"id":4,"title":5,"body":6,"category":1687,"date":1688,"description":12,"excerpt":1689,"extension":1690,"image":1691,"keywords":1692,"meta":1693,"navigation":987,"path":1694,"readingTime":1695,"seo":1696,"slug":1697,"stem":1698,"tags":1699,"__hash__":1700},"blog\u002Fblog\u002Fopen-claw-secure-hardened-setup.md","Secure and hardened OpenClaw setup on a VPS",{"type":7,"value":8,"toc":1672},"minimark",[9,13,16,24,40,43,48,56,78,85,88,114,117,141,144,167,170,193,196,212,215,219,222,229,296,302,316,319,323,326,339,345,352,381,384,444,451,463,470,473,514,517,527,533,539,543,549,573,576,599,602,633,636,640,647,661,664,668,671,678,703,709,712,724,727,1118,1121,1135,1145,1152,1156,1161,1164,1176,1179,1190,1193,1202,1205,1209,1212,1228,1231,1254,1269,1272,1295,1298,1302,1305,1321,1324,1341,1348,1352,1355,1365,1368,1384,1390,1405,1408,1419,1426,1432,1601,1604,1626,1629,1633,1636,1639,1659,1662,1665,1668],[10,11,12],"p",{},"I have been experimenting with OpenClaw recently, and while the initial setup is fairly straightforward, the secure setup is where most of the interesting decisions actually are.",[10,14,15],{},"The difference between “it works” and “it is safe enough to leave running on a VPS” is not small. If you are running an agent that can read files, write code, install packages, and expose a gateway for device pairing, you really do not want to treat the box like a throwaway toy project.",[10,17,18,19,23],{},"So in this post I am going to walk through the setup I ended up with for OpenClaw on a VPS, but with an emphasis on ",[20,21,22],"strong",{},"hardening the machine first",", and only then installing the app. The main goal is simple:",[25,26,27,31,34,37],"ul",{},[28,29,30],"li",{},"lock down SSH",[28,32,33],{},"keep the OpenClaw gateway on loopback only",[28,35,36],{},"avoid exposing unnecessary ports",[28,38,39],{},"run the service in a way that survives rebuilds and reboots",[10,41,42],{},"This is not the only way to do it, but it is a pragmatic setup that I would be comfortable running.",[44,45,47],"h2",{"id":46},"first-harden-the-server-before-installing-anything","First: harden the server before installing anything",[10,49,50,51,55],{},"Start by connecting to your VPS as ",[52,53,54],"code",{},"root",".",[57,58,63],"pre",{"className":59,"code":60,"language":61,"meta":62,"style":62},"language-bash shiki shiki-themes github-light github-dark","ssh root@YOUR_VPS_IP\n","bash","",[52,64,65],{"__ignoreMap":62},[66,67,70,74],"span",{"class":68,"line":69},"line",1,[66,71,73],{"class":72},"sScJk","ssh",[66,75,77],{"class":76},"sZZnC"," root@YOUR_VPS_IP\n",[10,79,80,81,84],{},"If this is the first time you connect, type ",[52,82,83],{},"yes"," at the SSH fingerprint prompt.",[10,86,87],{},"Once you are in, update the machine.",[57,89,91],{"className":59,"code":90,"language":61,"meta":62,"style":62},"apt-get update && apt-get upgrade -y\n",[52,92,93],{"__ignoreMap":62},[66,94,95,98,101,105,107,110],{"class":68,"line":69},[66,96,97],{"class":72},"apt-get",[66,99,100],{"class":76}," update",[66,102,104],{"class":103},"sVt8B"," && ",[66,106,97],{"class":72},[66,108,109],{"class":76}," upgrade",[66,111,113],{"class":112},"sj4cs"," -y\n",[10,115,116],{},"Then install the basic packages we need along the way.",[57,118,120],{"className":59,"code":119,"language":61,"meta":62,"style":62},"apt-get install -y git curl ca-certificates\n",[52,121,122],{"__ignoreMap":62},[66,123,124,126,129,132,135,138],{"class":68,"line":69},[66,125,97],{"class":72},[66,127,128],{"class":76}," install",[66,130,131],{"class":112}," -y",[66,133,134],{"class":76}," git",[66,136,137],{"class":76}," curl",[66,139,140],{"class":76}," ca-certificates\n",[10,142,143],{},"Now install Docker:",[57,145,147],{"className":59,"code":146,"language":61,"meta":62,"style":62},"curl -fsSL https:\u002F\u002Fget.docker.com | sh\n",[52,148,149],{"__ignoreMap":62},[66,150,151,154,157,160,164],{"class":68,"line":69},[66,152,153],{"class":72},"curl",[66,155,156],{"class":112}," -fsSL",[66,158,159],{"class":76}," https:\u002F\u002Fget.docker.com",[66,161,163],{"class":162},"szBVR"," |",[66,165,166],{"class":72}," sh\n",[10,168,169],{},"And after that, install the basic hardening tools:",[57,171,173],{"className":59,"code":172,"language":61,"meta":62,"style":62},"apt install ufw fail2ban unattended-upgrades -y\n",[52,174,175],{"__ignoreMap":62},[66,176,177,180,182,185,188,191],{"class":68,"line":69},[66,178,179],{"class":72},"apt",[66,181,128],{"class":76},[66,183,184],{"class":76}," ufw",[66,186,187],{"class":76}," fail2ban",[66,189,190],{"class":76}," unattended-upgrades",[66,192,113],{"class":112},[10,194,195],{},"Enable automatic security updates as well:",[57,197,199],{"className":59,"code":198,"language":61,"meta":62,"style":62},"dpkg-reconfigure -plow unattended-upgrades\n",[52,200,201],{"__ignoreMap":62},[66,202,203,206,209],{"class":68,"line":69},[66,204,205],{"class":72},"dpkg-reconfigure",[66,207,208],{"class":112}," -plow",[66,210,211],{"class":76}," unattended-upgrades\n",[10,213,214],{},"There is nothing OpenClaw-specific here yet, and that is the point. Before giving an AI agent a home on your VPS, make sure the VPS itself is not casually exposed.",[44,216,218],{"id":217},"configure-the-firewall-before-openclaw-exists","Configure the firewall before OpenClaw exists",[10,220,221],{},"This is one of the most important parts of the whole setup.",[10,223,224,225,228],{},"The gateway should ",[20,226,227],{},"not"," be reachable from the public internet. It should bind to loopback and only be reachable locally on the server. So before installing OpenClaw, configure the firewall to allow only SSH.",[57,230,232],{"className":59,"code":231,"language":61,"meta":62,"style":62},"ufw default deny incoming\nufw default allow outgoing\nufw allow 22\u002Ftcp comment 'SSH'\nufw enable\nufw status verbose\n",[52,233,234,248,261,277,285],{"__ignoreMap":62},[66,235,236,239,242,245],{"class":68,"line":69},[66,237,238],{"class":72},"ufw",[66,240,241],{"class":76}," default",[66,243,244],{"class":76}," deny",[66,246,247],{"class":76}," incoming\n",[66,249,251,253,255,258],{"class":68,"line":250},2,[66,252,238],{"class":72},[66,254,241],{"class":76},[66,256,257],{"class":76}," allow",[66,259,260],{"class":76}," outgoing\n",[66,262,264,266,268,271,274],{"class":68,"line":263},3,[66,265,238],{"class":72},[66,267,257],{"class":76},[66,269,270],{"class":76}," 22\u002Ftcp",[66,272,273],{"class":76}," comment",[66,275,276],{"class":76}," 'SSH'\n",[66,278,280,282],{"class":68,"line":279},4,[66,281,238],{"class":72},[66,283,284],{"class":76}," enable\n",[66,286,288,290,293],{"class":68,"line":287},5,[66,289,238],{"class":72},[66,291,292],{"class":76}," status",[66,294,295],{"class":76}," verbose\n",[10,297,298,299,301],{},"What you should ",[20,300,227],{}," do is this:",[57,303,305],{"className":59,"code":304,"language":61,"meta":62,"style":62},"ufw allow 18789\u002Ftcp\n",[52,306,307],{"__ignoreMap":62},[66,308,309,311,313],{"class":68,"line":69},[66,310,238],{"class":72},[66,312,257],{"class":76},[66,314,315],{"class":76}," 18789\u002Ftcp\n",[10,317,318],{},"That would expose the OpenClaw gateway directly to the internet, which defeats the point of keeping it local in the first place.",[44,320,322],{"id":321},"create-a-separate-user-and-stop-using-root","Create a separate user and stop using root",[10,324,325],{},"Next, create a dedicated user for running and managing OpenClaw.",[57,327,329],{"className":59,"code":328,"language":61,"meta":62,"style":62},"adduser openclaw\n",[52,330,331],{"__ignoreMap":62},[66,332,333,336],{"class":68,"line":69},[66,334,335],{"class":72},"adduser",[66,337,338],{"class":76}," openclaw\n",[10,340,341,342,55],{},"Set a password, skip the optional name and phone fields, and confirm with ",[52,343,344],{},"Y",[10,346,347,348,351],{},"Then give that user ",[52,349,350],{},"sudo"," and Docker access:",[57,353,355],{"className":59,"code":354,"language":61,"meta":62,"style":62},"usermod -aG sudo openclaw\nusermod -aG docker openclaw\n",[52,356,357,370],{"__ignoreMap":62},[66,358,359,362,365,368],{"class":68,"line":69},[66,360,361],{"class":72},"usermod",[66,363,364],{"class":112}," -aG",[66,366,367],{"class":76}," sudo",[66,369,338],{"class":76},[66,371,372,374,376,379],{"class":68,"line":250},[66,373,361],{"class":72},[66,375,364],{"class":112},[66,377,378],{"class":76}," docker",[66,380,338],{"class":76},[10,382,383],{},"Copy over your SSH key so you can log in as the new user:",[57,385,387],{"className":59,"code":386,"language":61,"meta":62,"style":62},"mkdir -p \u002Fhome\u002Fopenclaw\u002F.ssh\ncp \u002Froot\u002F.ssh\u002Fauthorized_keys \u002Fhome\u002Fopenclaw\u002F.ssh\u002F\nchown -R openclaw:openclaw \u002Fhome\u002Fopenclaw\u002F.ssh\nchmod 700 \u002Fhome\u002Fopenclaw\u002F.ssh\nchmod 600 \u002Fhome\u002Fopenclaw\u002F.ssh\u002Fauthorized_keys\n",[52,388,389,400,411,424,434],{"__ignoreMap":62},[66,390,391,394,397],{"class":68,"line":69},[66,392,393],{"class":72},"mkdir",[66,395,396],{"class":112}," -p",[66,398,399],{"class":76}," \u002Fhome\u002Fopenclaw\u002F.ssh\n",[66,401,402,405,408],{"class":68,"line":250},[66,403,404],{"class":72},"cp",[66,406,407],{"class":76}," \u002Froot\u002F.ssh\u002Fauthorized_keys",[66,409,410],{"class":76}," \u002Fhome\u002Fopenclaw\u002F.ssh\u002F\n",[66,412,413,416,419,422],{"class":68,"line":263},[66,414,415],{"class":72},"chown",[66,417,418],{"class":112}," -R",[66,420,421],{"class":76}," openclaw:openclaw",[66,423,399],{"class":76},[66,425,426,429,432],{"class":68,"line":279},[66,427,428],{"class":72},"chmod",[66,430,431],{"class":112}," 700",[66,433,399],{"class":76},[66,435,436,438,441],{"class":68,"line":287},[66,437,428],{"class":72},[66,439,440],{"class":112}," 600",[66,442,443],{"class":76}," \u002Fhome\u002Fopenclaw\u002F.ssh\u002Fauthorized_keys\n",[10,445,446,447,450],{},"Now test it in a ",[20,448,449],{},"new terminal tab",", while keeping your root session open:",[57,452,454],{"className":59,"code":453,"language":61,"meta":62,"style":62},"ssh openclaw@YOUR_VPS_IP\n",[52,455,456],{"__ignoreMap":62},[66,457,458,460],{"class":68,"line":69},[66,459,73],{"class":72},[66,461,462],{"class":76}," openclaw@YOUR_VPS_IP\n",[10,464,465,466,469],{},"If you see a shell prompt for ",[52,467,468],{},"openclaw",", you are good.",[10,471,472],{},"Only after that should you lock down root SSH access:",[57,474,476],{"className":59,"code":475,"language":61,"meta":62,"style":62},"echo \"PermitRootLogin no\" >> \u002Fetc\u002Fssh\u002Fsshd_config\necho \"PasswordAuthentication no\" >> \u002Fetc\u002Fssh\u002Fsshd_config\nsystemctl restart ssh\n",[52,477,478,492,503],{"__ignoreMap":62},[66,479,480,483,486,489],{"class":68,"line":69},[66,481,482],{"class":112},"echo",[66,484,485],{"class":76}," \"PermitRootLogin no\"",[66,487,488],{"class":162}," >>",[66,490,491],{"class":76}," \u002Fetc\u002Fssh\u002Fsshd_config\n",[66,493,494,496,499,501],{"class":68,"line":250},[66,495,482],{"class":112},[66,497,498],{"class":76}," \"PasswordAuthentication no\"",[66,500,488],{"class":162},[66,502,491],{"class":76},[66,504,505,508,511],{"class":68,"line":263},[66,506,507],{"class":72},"systemctl",[66,509,510],{"class":76}," restart",[66,512,513],{"class":76}," ssh\n",[10,515,516],{},"And verify:",[57,518,519],{"className":59,"code":60,"language":61,"meta":62,"style":62},[52,520,521],{"__ignoreMap":62},[66,522,523,525],{"class":68,"line":69},[66,524,73],{"class":72},[66,526,77],{"class":76},[10,528,529,530,55],{},"This should now fail with ",[52,531,532],{},"Permission denied",[10,534,535,536,538],{},"From this point on, use the ",[52,537,468],{}," user only.",[44,540,542],{"id":541},"clone-the-repo-and-create-persistent-directories","Clone the repo and create persistent directories",[10,544,545,546,548],{},"Log in as the ",[52,547,468],{}," user and update packages once more:",[57,550,552],{"className":59,"code":551,"language":61,"meta":62,"style":62},"sudo apt-get update && sudo apt-get upgrade -y\n",[52,553,554],{"__ignoreMap":62},[66,555,556,558,561,563,565,567,569,571],{"class":68,"line":69},[66,557,350],{"class":72},[66,559,560],{"class":76}," apt-get",[66,562,100],{"class":76},[66,564,104],{"class":103},[66,566,350],{"class":72},[66,568,560],{"class":76},[66,570,109],{"class":76},[66,572,113],{"class":112},[10,574,575],{},"Then clone the repository:",[57,577,579],{"className":59,"code":578,"language":61,"meta":62,"style":62},"git clone https:\u002F\u002Fgithub.com\u002Fopenclaw\u002Fopenclaw.git\ncd openclaw\n",[52,580,581,592],{"__ignoreMap":62},[66,582,583,586,589],{"class":68,"line":69},[66,584,585],{"class":72},"git",[66,587,588],{"class":76}," clone",[66,590,591],{"class":76}," https:\u002F\u002Fgithub.com\u002Fopenclaw\u002Fopenclaw.git\n",[66,593,594,597],{"class":68,"line":250},[66,595,596],{"class":112},"cd",[66,598,338],{"class":76},[10,600,601],{},"Create the directories that should survive container rebuilds:",[57,603,605],{"className":59,"code":604,"language":61,"meta":62,"style":62},"sudo mkdir -p \u002Fhome\u002Fopenclaw\u002F.openclaw\u002Fworkspace\nsudo chown -R openclaw:openclaw \u002Fhome\u002Fopenclaw\u002F.openclaw\n",[52,606,607,619],{"__ignoreMap":62},[66,608,609,611,614,616],{"class":68,"line":69},[66,610,350],{"class":72},[66,612,613],{"class":76}," mkdir",[66,615,396],{"class":112},[66,617,618],{"class":76}," \u002Fhome\u002Fopenclaw\u002F.openclaw\u002Fworkspace\n",[66,620,621,623,626,628,630],{"class":68,"line":250},[66,622,350],{"class":72},[66,624,625],{"class":76}," chown",[66,627,418],{"class":112},[66,629,421],{"class":76},[66,631,632],{"class":76}," \u002Fhome\u002Fopenclaw\u002F.openclaw\n",[10,634,635],{},"I prefer keeping this data outside the container so configuration, auth state, and workspace files do not disappear when rebuilding images or replacing containers.",[44,637,639],{"id":638},"protect-the-environment-file","Protect the environment file",[10,641,642,643,646],{},"Once you create your ",[52,644,645],{},".env",", lock it down immediately:",[57,648,650],{"className":59,"code":649,"language":61,"meta":62,"style":62},"chmod 600 .env\n",[52,651,652],{"__ignoreMap":62},[66,653,654,656,658],{"class":68,"line":69},[66,655,428],{"class":72},[66,657,440],{"class":112},[66,659,660],{"class":76}," .env\n",[10,662,663],{},"That is just a small step, but it is one of those small steps that should always be there.",[44,665,667],{"id":666},"why-the-default-docker-compose-file-is-not-enough","Why the default Docker Compose file is not enough",[10,669,670],{},"This is where the setup starts getting more opinionated.",[10,672,673,674,677],{},"The default ",[52,675,676],{},"docker-compose.yml"," is not what I would call a hardened setup. There are a few things we want to change:",[679,680,681,688,691,694,697],"ol",{},[28,682,683,684,687],{},"We want the gateway to build from the local ",[52,685,686],{},"Dockerfile",", because we are going to tweak that file.",[28,689,690],{},"We want host networking so device pairing works reliably.",[28,692,693],{},"We want the gateway bound to loopback, not something reachable from outside.",[28,695,696],{},"We want restart policies so the service comes back after reboot or crashes.",[28,698,699,700,702],{},"We do ",[20,701,227],{}," want leftover Anthropic session variables hanging around if we are not using them.",[10,704,705,706,55],{},"If you want to edit comfortably, this is one of those cases where VS Code Remote SSH is much nicer than editing everything with ",[52,707,708],{},"nano",[10,710,711],{},"The file we are changing is:",[57,713,715],{"className":59,"code":714,"language":61,"meta":62,"style":62},"nano \u002Fhome\u002Fopenclaw\u002Fopenclaw\u002Fdocker-compose.yml\n",[52,716,717],{"__ignoreMap":62},[66,718,719,721],{"class":68,"line":69},[66,720,708],{"class":72},[66,722,723],{"class":76}," \u002Fhome\u002Fopenclaw\u002Fopenclaw\u002Fdocker-compose.yml\n",[10,725,726],{},"And this is the version I ended up using:",[57,728,732],{"className":729,"code":730,"language":731,"meta":62,"style":62},"language-yaml shiki shiki-themes github-light github-dark","services:\n  openclaw-gateway:\n    image: ${OPENCLAW_IMAGE:-openclaw:local}\n    build: .\n    environment:\n      HOME: \u002Fhome\u002Fnode\n      TERM: xterm-256color\n      OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN}\n      NODE_ENV: production\n      OPENCLAW_GATEWAY_BIND: ${OPENCLAW_GATEWAY_BIND:-loopback}\n      OPENCLAW_GATEWAY_PORT: ${OPENCLAW_GATEWAY_PORT:-18789}\n      OPENCLAW_SECRET: ${OPENCLAW_SECRET}\n      XDG_CONFIG_HOME: ${XDG_CONFIG_HOME}\n    volumes:\n      - ${OPENCLAW_CONFIG_DIR}:\u002Fhome\u002Fnode\u002F.openclaw\n      - ${OPENCLAW_WORKSPACE_DIR}:\u002Fhome\u002Fnode\u002F.openclaw\u002Fworkspace\n    network_mode: host\n    init: true\n    restart: unless-stopped\n    command:\n      [\"node\", \"dist\u002Findex.js\", \"gateway\",\n       \"--bind\", \"${OPENCLAW_GATEWAY_BIND:-loopback}\",\n       \"--port\", \"18789\"]\n\n  openclaw-cli:\n    image: ${OPENCLAW_IMAGE:-openclaw:local}\n    environment:\n      HOME: \u002Fhome\u002Fnode\n      TERM: xterm-256color\n      OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN}\n      BROWSER: echo\n    volumes:\n      - ${OPENCLAW_CONFIG_DIR}:\u002Fhome\u002Fnode\u002F.openclaw\n      - ${OPENCLAW_WORKSPACE_DIR}:\u002Fhome\u002Fnode\u002F.openclaw\u002Fworkspace\n    stdin_open: true\n    tty: true\n    init: true\n    entrypoint: [\"node\", \"dist\u002Findex.js\"]\n","yaml",[52,733,734,743,750,761,771,778,789,800,811,822,833,844,855,866,874,883,891,902,913,924,932,955,968,982,989,997,1006,1013,1022,1031,1040,1051,1058,1065,1072,1082,1092,1101],{"__ignoreMap":62},[66,735,736,740],{"class":68,"line":69},[66,737,739],{"class":738},"s9eBZ","services",[66,741,742],{"class":103},":\n",[66,744,745,748],{"class":68,"line":250},[66,746,747],{"class":738},"  openclaw-gateway",[66,749,742],{"class":103},[66,751,752,755,758],{"class":68,"line":263},[66,753,754],{"class":738},"    image",[66,756,757],{"class":103},": ",[66,759,760],{"class":76},"${OPENCLAW_IMAGE:-openclaw:local}\n",[66,762,763,766,768],{"class":68,"line":279},[66,764,765],{"class":738},"    build",[66,767,757],{"class":103},[66,769,770],{"class":112},".\n",[66,772,773,776],{"class":68,"line":287},[66,774,775],{"class":738},"    environment",[66,777,742],{"class":103},[66,779,781,784,786],{"class":68,"line":780},6,[66,782,783],{"class":738},"      HOME",[66,785,757],{"class":103},[66,787,788],{"class":76},"\u002Fhome\u002Fnode\n",[66,790,792,795,797],{"class":68,"line":791},7,[66,793,794],{"class":738},"      TERM",[66,796,757],{"class":103},[66,798,799],{"class":76},"xterm-256color\n",[66,801,803,806,808],{"class":68,"line":802},8,[66,804,805],{"class":738},"      OPENCLAW_GATEWAY_TOKEN",[66,807,757],{"class":103},[66,809,810],{"class":76},"${OPENCLAW_GATEWAY_TOKEN}\n",[66,812,814,817,819],{"class":68,"line":813},9,[66,815,816],{"class":738},"      NODE_ENV",[66,818,757],{"class":103},[66,820,821],{"class":76},"production\n",[66,823,825,828,830],{"class":68,"line":824},10,[66,826,827],{"class":738},"      OPENCLAW_GATEWAY_BIND",[66,829,757],{"class":103},[66,831,832],{"class":76},"${OPENCLAW_GATEWAY_BIND:-loopback}\n",[66,834,836,839,841],{"class":68,"line":835},11,[66,837,838],{"class":738},"      OPENCLAW_GATEWAY_PORT",[66,840,757],{"class":103},[66,842,843],{"class":76},"${OPENCLAW_GATEWAY_PORT:-18789}\n",[66,845,847,850,852],{"class":68,"line":846},12,[66,848,849],{"class":738},"      OPENCLAW_SECRET",[66,851,757],{"class":103},[66,853,854],{"class":76},"${OPENCLAW_SECRET}\n",[66,856,858,861,863],{"class":68,"line":857},13,[66,859,860],{"class":738},"      XDG_CONFIG_HOME",[66,862,757],{"class":103},[66,864,865],{"class":76},"${XDG_CONFIG_HOME}\n",[66,867,869,872],{"class":68,"line":868},14,[66,870,871],{"class":738},"    volumes",[66,873,742],{"class":103},[66,875,877,880],{"class":68,"line":876},15,[66,878,879],{"class":103},"      - ",[66,881,882],{"class":76},"${OPENCLAW_CONFIG_DIR}:\u002Fhome\u002Fnode\u002F.openclaw\n",[66,884,886,888],{"class":68,"line":885},16,[66,887,879],{"class":103},[66,889,890],{"class":76},"${OPENCLAW_WORKSPACE_DIR}:\u002Fhome\u002Fnode\u002F.openclaw\u002Fworkspace\n",[66,892,894,897,899],{"class":68,"line":893},17,[66,895,896],{"class":738},"    network_mode",[66,898,757],{"class":103},[66,900,901],{"class":76},"host\n",[66,903,905,908,910],{"class":68,"line":904},18,[66,906,907],{"class":738},"    init",[66,909,757],{"class":103},[66,911,912],{"class":112},"true\n",[66,914,916,919,921],{"class":68,"line":915},19,[66,917,918],{"class":738},"    restart",[66,920,757],{"class":103},[66,922,923],{"class":76},"unless-stopped\n",[66,925,927,930],{"class":68,"line":926},20,[66,928,929],{"class":738},"    command",[66,931,742],{"class":103},[66,933,935,938,941,944,947,949,952],{"class":68,"line":934},21,[66,936,937],{"class":103},"      [",[66,939,940],{"class":76},"\"node\"",[66,942,943],{"class":103},", ",[66,945,946],{"class":76},"\"dist\u002Findex.js\"",[66,948,943],{"class":103},[66,950,951],{"class":76},"\"gateway\"",[66,953,954],{"class":103},",\n",[66,956,958,961,963,966],{"class":68,"line":957},22,[66,959,960],{"class":76},"       \"--bind\"",[66,962,943],{"class":103},[66,964,965],{"class":76},"\"${OPENCLAW_GATEWAY_BIND:-loopback}\"",[66,967,954],{"class":103},[66,969,971,974,976,979],{"class":68,"line":970},23,[66,972,973],{"class":76},"       \"--port\"",[66,975,943],{"class":103},[66,977,978],{"class":76},"\"18789\"",[66,980,981],{"class":103},"]\n",[66,983,985],{"class":68,"line":984},24,[66,986,988],{"emptyLinePlaceholder":987},true,"\n",[66,990,992,995],{"class":68,"line":991},25,[66,993,994],{"class":738},"  openclaw-cli",[66,996,742],{"class":103},[66,998,1000,1002,1004],{"class":68,"line":999},26,[66,1001,754],{"class":738},[66,1003,757],{"class":103},[66,1005,760],{"class":76},[66,1007,1009,1011],{"class":68,"line":1008},27,[66,1010,775],{"class":738},[66,1012,742],{"class":103},[66,1014,1016,1018,1020],{"class":68,"line":1015},28,[66,1017,783],{"class":738},[66,1019,757],{"class":103},[66,1021,788],{"class":76},[66,1023,1025,1027,1029],{"class":68,"line":1024},29,[66,1026,794],{"class":738},[66,1028,757],{"class":103},[66,1030,799],{"class":76},[66,1032,1034,1036,1038],{"class":68,"line":1033},30,[66,1035,805],{"class":738},[66,1037,757],{"class":103},[66,1039,810],{"class":76},[66,1041,1043,1046,1048],{"class":68,"line":1042},31,[66,1044,1045],{"class":738},"      BROWSER",[66,1047,757],{"class":103},[66,1049,1050],{"class":76},"echo\n",[66,1052,1054,1056],{"class":68,"line":1053},32,[66,1055,871],{"class":738},[66,1057,742],{"class":103},[66,1059,1061,1063],{"class":68,"line":1060},33,[66,1062,879],{"class":103},[66,1064,882],{"class":76},[66,1066,1068,1070],{"class":68,"line":1067},34,[66,1069,879],{"class":103},[66,1071,890],{"class":76},[66,1073,1075,1078,1080],{"class":68,"line":1074},35,[66,1076,1077],{"class":738},"    stdin_open",[66,1079,757],{"class":103},[66,1081,912],{"class":112},[66,1083,1085,1088,1090],{"class":68,"line":1084},36,[66,1086,1087],{"class":738},"    tty",[66,1089,757],{"class":103},[66,1091,912],{"class":112},[66,1093,1095,1097,1099],{"class":68,"line":1094},37,[66,1096,907],{"class":738},[66,1098,757],{"class":103},[66,1100,912],{"class":112},[66,1102,1104,1107,1110,1112,1114,1116],{"class":68,"line":1103},38,[66,1105,1106],{"class":738},"    entrypoint",[66,1108,1109],{"class":103},": [",[66,1111,940],{"class":76},[66,1113,943],{"class":103},[66,1115,946],{"class":76},[66,1117,981],{"class":103},[10,1119,1120],{},"The most important line here is probably this one:",[57,1122,1124],{"className":729,"code":1123,"language":731,"meta":62,"style":62},"OPENCLAW_GATEWAY_BIND: ${OPENCLAW_GATEWAY_BIND:-loopback}\n",[52,1125,1126],{"__ignoreMap":62},[66,1127,1128,1131,1133],{"class":68,"line":69},[66,1129,1130],{"class":738},"OPENCLAW_GATEWAY_BIND",[66,1132,757],{"class":103},[66,1134,832],{"class":76},[10,1136,1137,1140,1141,1144],{},[52,1138,1139],{},"loopback"," means the gateway listens on ",[52,1142,1143],{},"127.0.0.1"," only. That is exactly what we want.",[10,1146,1147,1148,1151],{},"Another important change is removing the ",[52,1149,1150],{},"ports:"," section entirely. If you expose the gateway with normal Docker port publishing and then open it in the firewall, you have made the box much more reachable than necessary.",[44,1153,1155],{"id":1154},"modify-the-dockerfile","Modify the Dockerfile",[10,1157,1158,1159,55],{},"There is one small but practical modification I had to make in the ",[52,1160,686],{},[10,1162,1163],{},"Open it:",[57,1165,1167],{"className":59,"code":1166,"language":61,"meta":62,"style":62},"nano Dockerfile\n",[52,1168,1169],{"__ignoreMap":62},[66,1170,1171,1173],{"class":68,"line":69},[66,1172,708],{"class":72},[66,1174,1175],{"class":76}," Dockerfile\n",[10,1177,1178],{},"Near the bottom you will find:",[57,1180,1184],{"className":1181,"code":1182,"language":1183,"meta":62,"style":62},"language-dockerfile shiki shiki-themes github-light github-dark","USER node\n","dockerfile",[52,1185,1186],{"__ignoreMap":62},[66,1187,1188],{"class":68,"line":69},[66,1189,1182],{},[10,1191,1192],{},"Comment that out:",[57,1194,1196],{"className":1181,"code":1195,"language":1183,"meta":62,"style":62},"# USER node\n",[52,1197,1198],{"__ignoreMap":62},[66,1199,1200],{"class":68,"line":69},[66,1201,1195],{},[10,1203,1204],{},"Normally I prefer dropping privileges inside containers, but in this case the agent needs to install tools, manage packages, and write files in ways that quickly run into permissions friction. Leaving that line in place caused more trouble than value for my use case.",[44,1206,1208],{"id":1207},"build-and-run-onboarding","Build and run onboarding",[10,1210,1211],{},"Now build the images:",[57,1213,1215],{"className":59,"code":1214,"language":61,"meta":62,"style":62},"docker compose build\n",[52,1216,1217],{"__ignoreMap":62},[66,1218,1219,1222,1225],{"class":68,"line":69},[66,1220,1221],{"class":72},"docker",[66,1223,1224],{"class":76}," compose",[66,1226,1227],{"class":76}," build\n",[10,1229,1230],{},"Then run the onboarding wizard:",[57,1232,1234],{"className":59,"code":1233,"language":61,"meta":62,"style":62},"docker compose run --rm openclaw-cli onboard\n",[52,1235,1236],{"__ignoreMap":62},[66,1237,1238,1240,1242,1245,1248,1251],{"class":68,"line":69},[66,1239,1221],{"class":72},[66,1241,1224],{"class":76},[66,1243,1244],{"class":76}," run",[66,1246,1247],{"class":112}," --rm",[66,1249,1250],{"class":76}," openclaw-cli",[66,1252,1253],{"class":76}," onboard\n",[10,1255,1256,1257,1260,1261,1264,1265,1268],{},"I went with ",[52,1258,1259],{},"QuickStart",", then ",[52,1262,1263],{},"OpenAI (Codex OAuth + API key)",", followed by ",[52,1266,1267],{},"OpenAI Codex (ChatGPT OAuth)",", but you can pick whichever provider works for you.",[10,1270,1271],{},"The OAuth flow is slightly awkward the first time, but it is simple enough:",[679,1273,1274,1277,1280,1283,1289,1292],{},[28,1275,1276],{},"The wizard prints a URL.",[28,1278,1279],{},"Open it in your laptop browser.",[28,1281,1282],{},"Log in and approve.",[28,1284,1285,1286],{},"The browser redirects to something like ",[52,1287,1288],{},"http:\u002F\u002Flocalhost:xxxx\u002Fauth\u002Fcallback?code=...",[28,1290,1291],{},"The page may say it cannot connect. That is normal.",[28,1293,1294],{},"Copy the entire redirected URL and paste it back into the terminal.",[10,1296,1297],{},"If you want Telegram, configure it during onboarding. I skipped the optional skills, hooks, and extra API key prompts for the initial setup.",[44,1299,1301],{"id":1300},"fix-the-bind-mode-if-the-wizard-changes-it","Fix the bind mode if the wizard changes it",[10,1303,1304],{},"After onboarding, verify that the gateway bind value is still correct:",[57,1306,1308],{"className":59,"code":1307,"language":61,"meta":62,"style":62},"grep '\"bind\"' \u002Fhome\u002Fopenclaw\u002F.openclaw\u002Fopenclaw.json\n",[52,1309,1310],{"__ignoreMap":62},[66,1311,1312,1315,1318],{"class":68,"line":69},[66,1313,1314],{"class":72},"grep",[66,1316,1317],{"class":76}," '\"bind\"'",[66,1319,1320],{"class":76}," \u002Fhome\u002Fopenclaw\u002F.openclaw\u002Fopenclaw.json\n",[10,1322,1323],{},"It should say:",[57,1325,1329],{"className":1326,"code":1327,"language":1328,"meta":62,"style":62},"language-json shiki shiki-themes github-light github-dark","\"bind\": \"loopback\"\n","json",[52,1330,1331],{"__ignoreMap":62},[66,1332,1333,1336,1338],{"class":68,"line":69},[66,1334,1335],{"class":76},"\"bind\"",[66,1337,757],{"class":103},[66,1339,1340],{"class":76},"\"loopback\"\n",[10,1342,1343,1344,1347],{},"If it says ",[52,1345,1346],{},"lan",", change it. This is not a cosmetic setting. It controls whether the gateway stays local or starts listening more broadly than intended.",[44,1349,1351],{"id":1350},"fix-the-gateway-token-mismatch","Fix the gateway token mismatch",[10,1353,1354],{},"This was the part that felt most likely to waste time later if left unnoticed.",[10,1356,1357,1358,1361,1362,1364],{},"The onboarding wizard can write its own gateway token into ",[52,1359,1360],{},"openclaw.json",", and that token may not match the one in your ",[52,1363,645],{},". If the tokens differ, you will get weird failures later: CLI commands fail, subagents do not spawn correctly, cron jobs misbehave, and the setup feels broken in ways that are harder to reason about than they should be.",[10,1366,1367],{},"Check the token written by the wizard:",[57,1369,1371],{"className":59,"code":1370,"language":61,"meta":62,"style":62},"python3 -c \"import json; c=json.load(open('\u002Fhome\u002Fopenclaw\u002F.openclaw\u002Fopenclaw.json')); print(c['gateway']['auth']['token'])\"\n",[52,1372,1373],{"__ignoreMap":62},[66,1374,1375,1378,1381],{"class":68,"line":69},[66,1376,1377],{"class":72},"python3",[66,1379,1380],{"class":112}," -c",[66,1382,1383],{"class":76}," \"import json; c=json.load(open('\u002Fhome\u002Fopenclaw\u002F.openclaw\u002Fopenclaw.json')); print(c['gateway']['auth']['token'])\"\n",[10,1385,1386,1387,1389],{},"Then compare it with the token in ",[52,1388,645],{},":",[57,1391,1393],{"className":59,"code":1392,"language":61,"meta":62,"style":62},"grep OPENCLAW_GATEWAY_TOKEN \u002Fhome\u002Fopenclaw\u002Fopenclaw\u002F.env\n",[52,1394,1395],{"__ignoreMap":62},[66,1396,1397,1399,1402],{"class":68,"line":69},[66,1398,1314],{"class":72},[66,1400,1401],{"class":76}," OPENCLAW_GATEWAY_TOKEN",[66,1403,1404],{"class":76}," \u002Fhome\u002Fopenclaw\u002Fopenclaw\u002F.env\n",[10,1406,1407],{},"If they are different, edit the JSON file and make them match:",[57,1409,1411],{"className":59,"code":1410,"language":61,"meta":62,"style":62},"nano \u002Fhome\u002Fopenclaw\u002F.openclaw\u002Fopenclaw.json\n",[52,1412,1413],{"__ignoreMap":62},[66,1414,1415,1417],{"class":68,"line":69},[66,1416,708],{"class":72},[66,1418,1320],{"class":76},[44,1420,1422,1423,1425],{"id":1421},"the-final-openclawjson-gateway-shape","The final ",[52,1424,1360],{}," gateway shape",[10,1427,1428,1429,1431],{},"The gateway section in ",[52,1430,1360],{}," should look like this:",[57,1433,1435],{"className":1326,"code":1434,"language":1328,"meta":62,"style":62},"\"gateway\": {\n  \"port\": 18789,\n  \"mode\": \"local\",\n  \"bind\": \"loopback\",\n  \"auth\": {\n    \"mode\": \"token\",\n    \"token\": \"YOUR_TOKEN_FROM_ENV_FILE\",\n    \"rateLimit\": {\n      \"maxAttempts\": 10,\n      \"windowMs\": 60000,\n      \"lockoutMs\": 300000\n    }\n  },\n  \"controlUi\": {\n    \"enabled\": true,\n    \"allowInsecureAuth\": false\n  }\n}\n",[52,1436,1437,1444,1456,1468,1480,1487,1499,1511,1518,1530,1542,1552,1557,1562,1569,1581,1591,1596],{"__ignoreMap":62},[66,1438,1439,1441],{"class":68,"line":69},[66,1440,951],{"class":76},[66,1442,1443],{"class":103},": {\n",[66,1445,1446,1449,1451,1454],{"class":68,"line":250},[66,1447,1448],{"class":112},"  \"port\"",[66,1450,757],{"class":103},[66,1452,1453],{"class":112},"18789",[66,1455,954],{"class":103},[66,1457,1458,1461,1463,1466],{"class":68,"line":263},[66,1459,1460],{"class":112},"  \"mode\"",[66,1462,757],{"class":103},[66,1464,1465],{"class":76},"\"local\"",[66,1467,954],{"class":103},[66,1469,1470,1473,1475,1478],{"class":68,"line":279},[66,1471,1472],{"class":112},"  \"bind\"",[66,1474,757],{"class":103},[66,1476,1477],{"class":76},"\"loopback\"",[66,1479,954],{"class":103},[66,1481,1482,1485],{"class":68,"line":287},[66,1483,1484],{"class":112},"  \"auth\"",[66,1486,1443],{"class":103},[66,1488,1489,1492,1494,1497],{"class":68,"line":780},[66,1490,1491],{"class":112},"    \"mode\"",[66,1493,757],{"class":103},[66,1495,1496],{"class":76},"\"token\"",[66,1498,954],{"class":103},[66,1500,1501,1504,1506,1509],{"class":68,"line":791},[66,1502,1503],{"class":112},"    \"token\"",[66,1505,757],{"class":103},[66,1507,1508],{"class":76},"\"YOUR_TOKEN_FROM_ENV_FILE\"",[66,1510,954],{"class":103},[66,1512,1513,1516],{"class":68,"line":802},[66,1514,1515],{"class":112},"    \"rateLimit\"",[66,1517,1443],{"class":103},[66,1519,1520,1523,1525,1528],{"class":68,"line":813},[66,1521,1522],{"class":112},"      \"maxAttempts\"",[66,1524,757],{"class":103},[66,1526,1527],{"class":112},"10",[66,1529,954],{"class":103},[66,1531,1532,1535,1537,1540],{"class":68,"line":824},[66,1533,1534],{"class":112},"      \"windowMs\"",[66,1536,757],{"class":103},[66,1538,1539],{"class":112},"60000",[66,1541,954],{"class":103},[66,1543,1544,1547,1549],{"class":68,"line":835},[66,1545,1546],{"class":112},"      \"lockoutMs\"",[66,1548,757],{"class":103},[66,1550,1551],{"class":112},"300000\n",[66,1553,1554],{"class":68,"line":846},[66,1555,1556],{"class":103},"    }\n",[66,1558,1559],{"class":68,"line":857},[66,1560,1561],{"class":103},"  },\n",[66,1563,1564,1567],{"class":68,"line":868},[66,1565,1566],{"class":112},"  \"controlUi\"",[66,1568,1443],{"class":103},[66,1570,1571,1574,1576,1579],{"class":68,"line":876},[66,1572,1573],{"class":112},"    \"enabled\"",[66,1575,757],{"class":103},[66,1577,1578],{"class":112},"true",[66,1580,954],{"class":103},[66,1582,1583,1586,1588],{"class":68,"line":885},[66,1584,1585],{"class":112},"    \"allowInsecureAuth\"",[66,1587,757],{"class":103},[66,1589,1590],{"class":112},"false\n",[66,1592,1593],{"class":68,"line":893},[66,1594,1595],{"class":103},"  }\n",[66,1597,1598],{"class":68,"line":904},[66,1599,1600],{"class":103},"}\n",[10,1602,1603],{},"There are three fields here that matter a lot:",[25,1605,1606,1614,1620],{},[28,1607,1608,1611,1612],{},[52,1609,1610],{},"bind: \"loopback\""," keeps the gateway local to ",[52,1613,1143],{},[28,1615,1616,1619],{},[52,1617,1618],{},"allowInsecureAuth: false"," ensures you do not weaken device pairing",[28,1621,1622,1625],{},[52,1623,1624],{},"rateLimit"," gives you a basic brute-force safety net",[10,1627,1628],{},"This is the part where the setup stops being “just a local tool running in Docker” and starts becoming a service you can operate with a bit more confidence.",[44,1630,1632],{"id":1631},"final-thoughts","Final thoughts",[10,1634,1635],{},"OpenClaw is not particularly hard to get running. The hard part is resisting the temptation to stop the moment you see the happy path working.",[10,1637,1638],{},"A secure setup here is mostly about discipline:",[25,1640,1641,1644,1647,1650,1653,1656],{},[28,1642,1643],{},"do the firewall first",[28,1645,1646],{},"move off root",[28,1648,1649],{},"keep the gateway on loopback",[28,1651,1652],{},"do not open the port publicly",[28,1654,1655],{},"keep persistent data outside the container",[28,1657,1658],{},"make sure the tokens are actually in sync",[10,1660,1661],{},"If you skip those parts, the setup is technically “done”, but not in a way I would feel good about leaving on a VPS.",[10,1663,1664],{},"My general rule is simple: whenever a tool can execute commands, install packages, and read or write files, the surrounding environment matters as much as the tool itself.",[10,1666,1667],{},"That is exactly the kind of software that deserves a hardened setup from day one.",[1669,1670,1671],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":62,"searchDepth":250,"depth":250,"links":1673},[1674,1675,1676,1677,1678,1679,1680,1681,1682,1683,1684,1686],{"id":46,"depth":250,"text":47},{"id":217,"depth":250,"text":218},{"id":321,"depth":250,"text":322},{"id":541,"depth":250,"text":542},{"id":638,"depth":250,"text":639},{"id":666,"depth":250,"text":667},{"id":1154,"depth":250,"text":1155},{"id":1207,"depth":250,"text":1208},{"id":1300,"depth":250,"text":1301},{"id":1350,"depth":250,"text":1351},{"id":1421,"depth":250,"text":1685},"The final openclaw.json gateway shape",{"id":1631,"depth":250,"text":1632},"AI","2026\u002F03\u002F23",null,"md","\u002Fimages\u002Fopenclaw.png","openclaw, vps, docker, ubuntu, secure openclaw setup, hardened server setup, ssh hardening, ufw, fail2ban",{},"\u002Fblog\u002Fopen-claw-secure-hardened-setup","☕️☕️ 12 min read",{"title":5,"description":12},"open-claw-secure-hardened-setup","blog\u002Fopen-claw-secure-hardened-setup","ai, devops, docker, security, vps","1ndE6Pq3cRoQjtThOp6cmCWCXXUcKnBO70YryaJNr70",1774945272080]