[{"content":"Backups run daily at 03:00 UTC via systemd timer and are stored in ~/backups/ on the VPS.\nMake Targets 1 2 3 4 make backup-now # Trigger backup immediately make backup-pull # Download latest backup archive locally make restore # List available backups (dry-run — safe) make restore EXECUTE=1 BACKUP=\u0026lt;file\u0026gt; # Restore from backup make restore without EXECUTE=1 is always safe — it just lists available backups.\nBackup Contents Each archive (openclaw_backup_*.tar.gz) includes:\n~/.openclaw/ — full OpenClaw state (config, workspace, LLM history) ⚠️ Security Note: The SOPS age key is NOT included in backups. This prevents a stolen backup from being decrypted. You must keep your age key safe elsewhere (password manager, secure storage). Without it, secrets cannot be recovered.\nRequired: Age Key Backup The age key at ~/.config/sops/age/keys.txt is NOT included in automatic backups. You must back it up separately:\n1 2 3 4 5 # View your age key and save the output to a password manager or encrypted storage cat ~/.config/sops/age/keys.txt # To restore: copy the key back to ~/.config/sops/age/keys.txt # If you lost it: make secrets-generate-key \u0026amp;\u0026amp; make secrets-encrypt # re-creates all secrets Restore Procedure 1 2 make restore # list available backups (safe, no changes) make restore EXECUTE=1 BACKUP=openclaw_backup_20260101_030000.tar.gz With EXECUTE=1, the restore play:\nValidates the backup file and compose directory Stops containers Creates a safety backup of current state (including SOPS age key) Extracts the archive Validates critical files exist after extraction Pulls latest container images Restarts containers Waits for gateway health endpoint (30s timeout) Prints a summary with undo instructions Undoing a Restore If a restore goes wrong, use the safety backup created in step 3:\n1 make restore EXECUTE=1 BACKUP=openclaw_pre_restore_\u0026lt;timestamp\u0026gt;.tar.gz The safety backup is stored at ~/backups/openclaw_pre_restore_*.tar.gz on the VPS.\nBefore Upgrading Always back up before bumping the OpenClaw version:\n1 2 3 make backup-now # Edit docker/Dockerfile — bump OPENCLAW_VERSION make deploy REBUILD=1 ","permalink":"http://tardigrde.github.io/openclaw-deploy/operations/backup-restore/","summary":"\u003cp\u003eBackups run daily at \u003cstrong\u003e03:00 UTC\u003c/strong\u003e via systemd timer and are stored in \u003ccode\u003e~/backups/\u003c/code\u003e on the VPS.\u003c/p\u003e\n\u003ch2 id=\"make-targets\"\u003eMake Targets\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\n\u003ctable style=\"border-spacing:0;padding:0;margin:0;border:0;\"\u003e\u003ctr\u003e\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e1\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e2\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e3\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e4\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;;width:100%\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003emake backup-now   \u003cspan style=\"color:#75715e\"\u003e# Trigger backup immediately\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003emake backup-pull  \u003cspan style=\"color:#75715e\"\u003e# Download latest backup archive locally\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003emake restore      \u003cspan style=\"color:#75715e\"\u003e# List available backups (dry-run — safe)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003emake restore EXECUTE\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e BACKUP\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u0026lt;file\u0026gt;  \u003cspan style=\"color:#75715e\"\u003e# Restore from backup\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e\u003ccode\u003emake restore\u003c/code\u003e without \u003ccode\u003eEXECUTE=1\u003c/code\u003e is always safe — it just lists available backups.\u003c/p\u003e","title":"Backup \u0026 Restore"},{"content":"For a long-lived production setup, the recommended pattern is a private fork — tracking this repo as upstream and keeping credentials, personal config, and extensions in your fork. This lets you pull upstream improvements with minimal friction while keeping your private files out of the public repo.\nRepo layout Repo Purpose openclaw-deploy (this repo) Public template — source of truth for all shared files your-private-fork Your production deployment — credentials, overrides, extensions All shared infrastructure changes should be made in openclaw-deploy first, then pulled into your fork — never the reverse.\nOne-time setup 1. Create your private fork Create a private repo on GitHub (or any host), then clone it and add this repo as upstream:\n1 2 3 4 5 git clone git@github.com:you/openclaw-prod.git cd openclaw-prod git remote add upstream https://github.com/tardigrde/openclaw-deploy.git git fetch upstream git merge upstream/main 2. Register custom merge drivers These are stored in .git/config (not committed) and must be set once per clone:\n1 2 3 4 git config merge.theirs.name \u0026#34;always take theirs\u0026#34; git config merge.theirs.driver \u0026#34;cp %B %A\u0026#34; git config merge.ours.name \u0026#34;always keep ours\u0026#34; git config merge.ours.driver \u0026#34;true\u0026#34; merge=theirs — upstream wins; used for almost all shared files merge=ours — your fork wins; used for files you extend (e.g. a Tailscale serve config with extra ports) merge=union — keeps lines from both sides; used for .gitignore and .gitattributes 3. Add a .gitattributes Tell git which driver to use for each file. Copy and adapt:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 # Shared files — upstream always wins CLAUDE.md merge=theirs README.md merge=theirs SECURITY.md merge=theirs LICENSE merge=theirs Makefile merge=theirs Makefile.local.example merge=theirs docker-compose.yml merge=theirs docker-compose.override.example.yml merge=theirs docker/Dockerfile merge=theirs docker/entrypoint.sh merge=theirs docker/** merge=theirs ansible/plays/** merge=theirs ansible/templates/** merge=theirs ansible/ansible.cfg merge=theirs ansible/site.yml merge=theirs ansible/site.local.example.yml merge=theirs scripts/** merge=theirs docs/** merge=theirs terraform/modules/** merge=theirs terraform/envs/prod/main.tf merge=theirs terraform/envs/prod/variables.tf merge=theirs terraform/envs/prod/backend.tf.example merge=theirs terraform/envs/prod/terraform.tfvars.example merge=theirs secrets/inputs.example.sh merge=theirs secrets/.env.example merge=theirs openclaw.example.json merge=theirs .github/workflows/** merge=theirs .githooks/** merge=theirs .sops.yaml merge=theirs go.mod merge=theirs go.sum merge=theirs # Files your fork extends — your version wins ansible/templates/tailscale-serve.json.j2 merge=ours # Both sides\u0026#39; rules kept .gitignore merge=union .gitattributes merge=union With these drivers in place, git merge upstream/main resolves nearly all files automatically.\nPulling upstream changes 1 2 3 git fetch upstream git merge upstream/main # merge drivers resolve most files automatically git push If upstream release commits (e.g. chore(release): x.y.z) conflict, skip them:\n1 git rebase --skip # repeat for each release commit What lives where Upstream wins — always edit in openclaw-deploy, never directly in your fork Makefile, docker-compose.yml, Dockerfile, all Ansible plays and templates, scripts, docs, Terraform modules, and example/template files.\nYour fork wins — files your fork extends beyond the upstream version Any file where you add ports, services, or settings on top of the upstream version (e.g. a Tailscale serve config exposing additional ports).\nUnion merge — both sides\u0026rsquo; lines are kept .gitignore (your fork adds private-file rules) and .gitattributes (your fork extends the driver rules).\nPrivate-only — gitignored in openclaw-deploy, no conflict possible openclaw.json, docker-compose.override.yml, Makefile.local, private Ansible plays (*.local.yml), scripts/local/, secrets/, Terraform backend and tfvars, CI workflows for your fork.\nAutomating upstream syncs If you use Claude Code, you can define a skill in your private fork that automates the fetch → merge → PR workflow. The skill can handle expected release-commit conflicts, check for unexpected conflicts, and open a clean PR — so pulling upstream becomes a single command.\n","permalink":"http://tardigrde.github.io/openclaw-deploy/configuration/private-fork/","summary":"\u003cp\u003eFor a long-lived production setup, the recommended pattern is a \u003cstrong\u003eprivate fork\u003c/strong\u003e — tracking this repo as \u003ccode\u003eupstream\u003c/code\u003e and keeping credentials, personal config, and extensions in your fork. This lets you pull upstream improvements with minimal friction while keeping your private files out of the public repo.\u003c/p\u003e\n\u003ch2 id=\"repo-layout\"\u003eRepo layout\u003c/h2\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eRepo\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003ePurpose\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eopenclaw-deploy\u003c/code\u003e (this repo)\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ePublic template — source of truth for all shared files\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eyour-private-fork\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYour production deployment — credentials, overrides, extensions\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eAll shared infrastructure changes should be made in \u003ccode\u003eopenclaw-deploy\u003c/code\u003e first, then pulled into your fork — never the reverse.\u003c/p\u003e","title":"Private Fork"},{"content":"Agents OpenClaw supports multiple isolated agents, each with their own workspace and model. Define them in openclaw.json under agents.list.\nAdding an Agent 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { \u0026#34;agents\u0026#34;: { \u0026#34;list\u0026#34;: [ { \u0026#34;id\u0026#34;: \u0026#34;main\u0026#34;, \u0026#34;workspace\u0026#34;: \u0026#34;~/.openclaw/workspace\u0026#34;, \u0026#34;default\u0026#34;: true }, { \u0026#34;id\u0026#34;: \u0026#34;researcher\u0026#34;, \u0026#34;workspace\u0026#34;: \u0026#34;~/.openclaw/workspace-researcher\u0026#34;, \u0026#34;model\u0026#34;: \u0026#34;anthropic/claude-haiku-4-5\u0026#34; } ] } } Then deploy:\n1 make deploy Each agent gets its own isolated workspace directory on the VPS. Seed files into docker/workspace-templates/ to populate the main workspace on first start (no-clobber — existing files are never overwritten).\nExec Approvals OpenClaw exec approvals use a two-part system:\nPolicy — ask=off, security=full set in openclaw.json Socket daemon — started by entrypoint.sh via openclaw node run before the gateway Without the daemon, the gateway falls back to broadcasting approval requests to WebSocket clients, which time out after 120s. ask=off in the config has no effect without the daemon running.\nIf execs are still requiring approval after a fresh deploy:\n1 2 3 4 5 6 7 8 # Verify policy is loaded make exec CMD=\u0026#34;openclaw approvals get\u0026#34; # Expected: Defaults | security=full, ask=off # Verify socket exists make exec CMD=\u0026#34;ls -la /home/node/.openclaw/exec-approvals.sock\u0026#34; # If missing, check container logs: make logs ","permalink":"http://tardigrde.github.io/openclaw-deploy/guides/agents/","summary":"\u003ch1 id=\"agents\"\u003eAgents\u003c/h1\u003e\n\u003cp\u003eOpenClaw supports multiple isolated agents, each with their own workspace and model. Define them in \u003ccode\u003eopenclaw.json\u003c/code\u003e under \u003ccode\u003eagents.list\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"adding-an-agent\"\u003eAdding an Agent\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\n\u003ctable style=\"border-spacing:0;padding:0;margin:0;border:0;\"\u003e\u003ctr\u003e\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 1\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 2\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 3\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 4\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 5\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 6\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 7\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 8\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 9\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e10\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e11\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e12\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e13\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e14\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e15\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e16\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;;width:100%\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e{\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003e\u0026#34;agents\u0026#34;\u003c/span\u003e: {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026#34;list\u0026#34;\u003c/span\u003e: [\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003e\u0026#34;id\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;main\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003e\u0026#34;workspace\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;~/.openclaw/workspace\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003e\u0026#34;default\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#66d9ef\"\u003etrue\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      },\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003e\u0026#34;id\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;researcher\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003e\u0026#34;workspace\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;~/.openclaw/workspace-researcher\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003e\u0026#34;model\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;anthropic/claude-haiku-4-5\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    ]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003eThen deploy:\u003c/p\u003e","title":"Agents \u0026 Sessions"},{"content":"CI/CD The built-in CI validates and tests Terraform (terraform fmt, terraform validate, tflint, terraform test). Ansible is never run in CI — all make bootstrap / make deploy operations are local only.\nExample workflows for terraform plan (on PRs) and terraform apply (manual dispatch) are included as .example.yml files — copy them to enable (see Required Secrets below).\nAn optional GitOps deployment workflow is also included — copy .github/workflows/deploy.yml.example to .github/workflows/deploy.yml to enable automatic Ansible-based deployments on push. See GitOps auto-deploy for setup instructions.\nTerraform Plan (example — copy to enable) Copy .github/workflows/terraform-plan.yml.example to .github/workflows/terraform-plan.yml. When a pull request touches terraform/**, the workflow runs automatically and posts the plan diff as a PR comment.\nTerraform Apply (example — copy to enable) Copy .github/workflows/terraform-apply.yml.example to .github/workflows/terraform-apply.yml. Then trigger manually after merging:\nGo to Actions → Terraform Apply → Run workflow Select the branch and click Run workflow The apply workflow uses a production environment — you can configure approval gates in Settings → Environments → production.\nRequired GitHub Variables Set these in Settings → Variables and secrets → Actions → Variables (not secrets — WIF is keyless):\nVariable Value GCP_WIF_PROVIDER Full resource name of the WIF provider (output from my-gcp-project, e.g. projects/\u0026lt;proj-number\u0026gt;/locations/global/workloadIdentityPools/github/providers/github) GCP_SERVICE_ACCOUNT Automation SA email (e.g. automation@\u0026lt;project\u0026gt;.iam.gserviceaccount.com) Required GitHub Secrets These are for the Terraform CI workflows only:\nSecret Description HCLOUD_TOKEN Hetzner Cloud API token SSH_KEY_FINGERPRINT SSH key fingerprint registered in Hetzner The optional GitOps deployment workflow requires additional secrets (SSH_PRIVATE_KEY, SOPS_AGE_KEY, and optionally Tailscale OAuth credentials). See GitOps auto-deploy.\n","permalink":"http://tardigrde.github.io/openclaw-deploy/operations/cicd/","summary":"\u003ch1 id=\"cicd\"\u003eCI/CD\u003c/h1\u003e\n\u003cp\u003eThe built-in CI \u003cstrong\u003evalidates and tests\u003c/strong\u003e Terraform (\u003ccode\u003eterraform fmt\u003c/code\u003e, \u003ccode\u003eterraform validate\u003c/code\u003e, \u003ccode\u003etflint\u003c/code\u003e, \u003ccode\u003eterraform test\u003c/code\u003e). Ansible is never run in CI — all \u003ccode\u003emake bootstrap\u003c/code\u003e / \u003ccode\u003emake deploy\u003c/code\u003e operations are local only.\u003c/p\u003e\n\u003cp\u003eExample workflows for \u003ccode\u003eterraform plan\u003c/code\u003e (on PRs) and \u003ccode\u003eterraform apply\u003c/code\u003e (manual dispatch) are included as \u003ccode\u003e.example.yml\u003c/code\u003e files — copy them to enable (see \u003ca href=\"#required-github-secrets\"\u003eRequired Secrets\u003c/a\u003e below).\u003c/p\u003e\n\u003cp\u003eAn optional GitOps deployment workflow is also included — copy \u003ccode\u003e.github/workflows/deploy.yml.example\u003c/code\u003e to \u003ccode\u003e.github/workflows/deploy.yml\u003c/code\u003e to enable automatic Ansible-based deployments on push. See \u003ca href=\"/operations/gitops-auto-deploy/\"\u003eGitOps auto-deploy\u003c/a\u003e for setup instructions.\u003c/p\u003e","title":"CI/CD Setup"},{"content":"Prerequisites Terraform \u0026gt;= 1.5 (install) Ansible (install) age and sops — required for secret encryption (make secrets-encrypt). Install via your package manager or age releases / sops releases. jq — required for make validate. Install via your package manager. Hetzner Cloud account with API token (console) SSH key uploaded to Hetzner Cloud. Default path: ~/.ssh/id_rsa. Override with SSH_KEY env var. (Optional) Remote Terraform state — the default is local (no setup needed). For GCS or other remote backends, see Remote State Backend. 1. Clone 1 2 git clone https://github.com/tardigrde/openclaw-deploy.git cd openclaw-deploy 2. Configure infrastructure secrets 1 2 cp secrets/inputs.example.sh secrets/inputs.sh vim secrets/inputs.sh Required: HCLOUD_TOKEN, TF_VAR_ssh_key_fingerprint (from Hetzner Console → Security → SSH Keys). See Secrets Reference for the full variable reference and Tailscale-specific notes.\n3. Configure OpenClaw 1 2 cp openclaw.example.json openclaw.json vim openclaw.json Customize: Telegram IDs, timezone, AI models. See the official OpenClaw configuration docs for details on every option.\nTailscale users: Skip the allowedOrigins Tailscale hostname for now — you won\u0026rsquo;t know it until after bootstrap. You\u0026rsquo;ll fill it in at the Tailscale setup step.\n4. Configure Terraform backend 1 cp terraform/envs/prod/backend.tf.example terraform/envs/prod/backend.tf The default (backend \u0026quot;local\u0026quot;) requires no setup. To use GCS remote state instead, edit backend.tf — see Remote State Backend.\nNext Step Continue to Deployment \u0026amp; Bootstrap to provision the VPS and start containers.\n","permalink":"http://tardigrde.github.io/openclaw-deploy/getting-started/installation/","summary":"\u003ch2 id=\"prerequisites\"\u003ePrerequisites\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eTerraform\u003c/strong\u003e \u0026gt;= 1.5 (\u003ca href=\"https://developer.hashicorp.com/terraform/install\"\u003einstall\u003c/a\u003e)\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAnsible\u003c/strong\u003e (\u003ca href=\"https://docs.ansible.com/projects/ansible/latest/installation_guide/intro_installation.html\"\u003einstall\u003c/a\u003e)\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eage\u003c/strong\u003e and \u003cstrong\u003esops\u003c/strong\u003e — required for secret encryption (\u003ccode\u003emake secrets-encrypt\u003c/code\u003e). Install via your package manager or \u003ca href=\"https://github.com/FiloSottile/age/releases\"\u003eage releases\u003c/a\u003e / \u003ca href=\"https://github.com/getsops/sops/releases\"\u003esops releases\u003c/a\u003e.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ejq\u003c/strong\u003e — required for \u003ccode\u003emake validate\u003c/code\u003e. Install via your package manager.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eHetzner Cloud account\u003c/strong\u003e with API token (\u003ca href=\"https://console.hetzner.cloud/\"\u003econsole\u003c/a\u003e)\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSSH key\u003c/strong\u003e uploaded to Hetzner Cloud. Default path: \u003ccode\u003e~/.ssh/id_rsa\u003c/code\u003e. Override with \u003ccode\u003eSSH_KEY\u003c/code\u003e env var.\u003c/li\u003e\n\u003cli\u003e\u003cem\u003e(Optional)\u003c/em\u003e \u003cstrong\u003eRemote Terraform state\u003c/strong\u003e — the default is local (no setup needed). For GCS or other remote backends, see \u003ca href=\"/operations/remote-state/\"\u003eRemote State Backend\u003c/a\u003e.\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"1-clone\"\u003e1. Clone\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\n\u003ctable style=\"border-spacing:0;padding:0;margin:0;border:0;\"\u003e\u003ctr\u003e\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e1\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e2\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;;width:100%\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003egit clone https://github.com/tardigrde/openclaw-deploy.git\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecd openclaw-deploy\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"2-configure-infrastructure-secrets\"\u003e2. Configure infrastructure secrets\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\n\u003ctable style=\"border-spacing:0;padding:0;margin:0;border:0;\"\u003e\u003ctr\u003e\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e1\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e2\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;;width:100%\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecp secrets/inputs.example.sh secrets/inputs.sh\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003evim secrets/inputs.sh\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003eRequired: \u003ccode\u003eHCLOUD_TOKEN\u003c/code\u003e, \u003ccode\u003eTF_VAR_ssh_key_fingerprint\u003c/code\u003e (from \u003ca href=\"https://console.hetzner.cloud/\"\u003eHetzner Console\u003c/a\u003e → Security → SSH Keys). See \u003ca href=\"/configuration/secrets/\"\u003eSecrets Reference\u003c/a\u003e for the full variable reference and Tailscale-specific notes.\u003c/p\u003e","title":"Installation"},{"content":"OpenClaw Configuration Overview Configuration lives in openclaw.json (gitignored — create from openclaw.example.json) and is deployed via make deploy.\nAgent Defaults Key Value Purpose userTimezone UTC Proper time context for scheduling, logs, heartbeat timeFormat 24 European 24h format imageMaxDimensionPx 800 Downscale images before vision API — saves ~30% tokens contextTokens 180000 Safety margin below 200k model limit Telegram Channel Retry Logic Resilience for transient network errors (outbound sendMessage, editMessage, sendChatAction):\n1 2 3 4 5 6 \u0026#34;retry\u0026#34;: { \u0026#34;attempts\u0026#34;: 3, \u0026#34;minDelayMs\u0026#34;: 400, \u0026#34;maxDelayMs\u0026#34;: 30000, \u0026#34;jitter\u0026#34;: 0.1 } Access Control DM policy: pairing (default) + allowFrom restriction allowFrom: [\u0026quot;\u0026lt;YOUR_TELEGRAM_ID\u0026gt;\u0026quot;] — restricts DM access (extra layer beyond pairing) groupAllowFrom: [\u0026quot;\u0026lt;USER_ID_1\u0026gt;\u0026quot;, \u0026quot;\u0026lt;USER_ID_2\u0026gt;\u0026quot;] — global group sender restriction Security model:\nDM pairing approvals apply to DM access only Group sender authorization is explicit (does NOT inherit DM pairing) groupAllowFrom is global fallback; per-group allowFrom overrides Groups 1 2 3 4 5 6 7 8 \u0026#34;groups\u0026#34;: { \u0026#34;*\u0026#34;: { \u0026#34;requireMention\u0026#34;: true }, \u0026#34;\u0026lt;YOUR_GROUP_ID\u0026gt;\u0026#34;: { \u0026#34;requireMention\u0026#34;: false, \u0026#34;groupPolicy\u0026#34;: \u0026#34;open\u0026#34;, \u0026#34;systemPrompt\u0026#34;: \u0026#34;You\u0026#39;re in a group chat...\u0026#34; } } \u0026quot;*\u0026quot; = all other groups require @mention (default behavior) Specific group (\u0026lt;YOUR_GROUP_ID\u0026gt;) = no mention needed, any member can trigger Session key: agent:main:telegram:group:\u0026lt;YOUR_GROUP_ID\u0026gt; (separate from DM) Group has separate conversation history from DM No heartbeat in groups (avoids noise) Shared workspace/memory (both sessions access same files) Mention Patterns Added to agents.list[].groupChat.mentionPatterns for fallback detection:\n1 \u0026#34;mentionPatterns\u0026#34;: [\u0026#34;@your_bot_username\u0026#34;, \u0026#34;bot\u0026#34;] These are case-insensitive regexes. Native Telegram @-mentions still work; patterns are a safety net.\nPrivacy Mode Required: Bot must have privacy mode disabled in BotFather for group messages without @mention.\n/setprivacy → Disable in BotFather Remove + re-add bot to group for change to take effect Commands \u0026amp; Permissions 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026#34;commands\u0026#34;: { \u0026#34;native\u0026#34;: \u0026#34;auto\u0026#34;, \u0026#34;nativeSkills\u0026#34;: \u0026#34;auto\u0026#34;, \u0026#34;bash\u0026#34;: true, \u0026#34;config\u0026#34;: true, \u0026#34;debug\u0026#34;: true }, \u0026#34;tools\u0026#34;: { \u0026#34;elevated\u0026#34;: { \u0026#34;enabled\u0026#34;: true, \u0026#34;allowFrom\u0026#34;: { \u0026#34;telegram\u0026#34;: [\u0026#34;\u0026lt;YOUR_TELEGRAM_ID\u0026gt;\u0026#34;] } } } bash, config, debug commands enabled for troubleshooting Only authorized users (via allowFrom) can use elevated/exec commands Session Architecture Chat Session Key Heartbeat Scope DM agent:main:main Yes Private Group agent:main:telegram:group:\u0026lt;YOUR_GROUP_ID\u0026gt; No Shared Separate context: Group can\u0026rsquo;t see DM history, DM can\u0026rsquo;t see group history Shared brain: Same workspace, same memory files No heartbeat in groups — avoids noisy broadcasts Troubleshooting Polling Stall Telegram long-polling may stall (no getUpdates for N seconds). Auto-recovers after restart.\n1 openclaw gateway restart Network Errors If you see sendMessage failed: Network request failed, check:\nOutbound DNS/HTTPS to api.telegram.org IPv6 issues — current config uses dnsResultOrder: ipv4first (default Node 22+) Proxy if needed: channels.telegram.proxy Rate Limits Anthropic API rate limits (429) trigger automatic fallback to configured model fallbacks. No action needed unless persistent — if 429s continue, check your agents.defaults.model.fallbacks chain in openclaw.json, or switch to a different provider (e.g. OpenRouter) via OPENROUTER_API_KEY.\nFuture Considerations pdfModel for dedicated PDF handling compaction.memoryFlush to store memories before compaction contextPruning with cache-ttl mode (30m) thinkingDefault: \u0026quot;low\u0026quot; for cost optimization timeoutSeconds: 300 for faster failure detection Multi-channel support (Discord, WhatsApp) Sandbox mode for group isolation if untrusted members added ","permalink":"http://tardigrde.github.io/openclaw-deploy/configuration/openclaw-config/","summary":"\u003ch1 id=\"openclaw-configuration\"\u003eOpenClaw Configuration\u003c/h1\u003e\n\u003ch2 id=\"overview\"\u003eOverview\u003c/h2\u003e\n\u003cp\u003eConfiguration lives in \u003ccode\u003eopenclaw.json\u003c/code\u003e (gitignored — create from \u003ccode\u003eopenclaw.example.json\u003c/code\u003e) and is deployed via \u003ccode\u003emake deploy\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"agent-defaults\"\u003eAgent Defaults\u003c/h2\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eKey\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eValue\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003ePurpose\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003euserTimezone\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eUTC\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eProper time context for scheduling, logs, heartbeat\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003etimeFormat\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e24\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eEuropean 24h format\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eimageMaxDimensionPx\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e800\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDownscale images before vision API — saves ~30% tokens\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003econtextTokens\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e180000\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eSafety margin below 200k model limit\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"telegram-channel\"\u003eTelegram Channel\u003c/h2\u003e\n\u003ch3 id=\"retry-logic\"\u003eRetry Logic\u003c/h3\u003e\n\u003cp\u003eResilience for transient network errors (outbound \u003ccode\u003esendMessage\u003c/code\u003e, \u003ccode\u003eeditMessage\u003c/code\u003e, \u003ccode\u003esendChatAction\u003c/code\u003e):\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\n\u003ctable style=\"border-spacing:0;padding:0;margin:0;border:0;\"\u003e\u003ctr\u003e\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e1\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e2\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e3\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e4\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e5\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e6\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;;width:100%\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;retry\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e:\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003e\u0026#34;attempts\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e3\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003e\u0026#34;minDelayMs\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e400\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003e\u0026#34;maxDelayMs\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e30000\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003e\u0026#34;jitter\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e0.1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"access-control\"\u003eAccess Control\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eDM policy:\u003c/strong\u003e \u003ccode\u003epairing\u003c/code\u003e (default) + \u003ccode\u003eallowFrom\u003c/code\u003e restriction\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003eallowFrom\u003c/code\u003e:\u003c/strong\u003e \u003ccode\u003e[\u0026quot;\u0026lt;YOUR_TELEGRAM_ID\u0026gt;\u0026quot;]\u003c/code\u003e — restricts DM access (extra layer beyond pairing)\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003egroupAllowFrom\u003c/code\u003e:\u003c/strong\u003e \u003ccode\u003e[\u0026quot;\u0026lt;USER_ID_1\u0026gt;\u0026quot;, \u0026quot;\u0026lt;USER_ID_2\u0026gt;\u0026quot;]\u003c/code\u003e — global group sender restriction\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eSecurity model:\u003c/strong\u003e\u003c/p\u003e","title":"OpenClaw Configuration"},{"content":"Security Hardening Overview of all security controls applied to the OpenClaw VPS.\nFirewall Two firewall layers run in series.\nHetzner Cloud Firewall (network-level) Managed in Terraform (terraform/modules/hetzner-vps/main.tf). Applied at the hypervisor — traffic is dropped before it reaches the OS.\nDirection Protocol Port Source Condition Inbound TCP 22 ssh_allowed_cidrs variable Always Inbound UDP 41641 0.0.0.0/0, ::/0 Only if Tailscale on Outbound TCP 1–65535 0.0.0.0/0, ::/0 Always Outbound UDP 1–65535 0.0.0.0/0, ::/0 Always Outbound ICMP — 0.0.0.0/0, ::/0 Always ssh_allowed_cidrs defaults to [\u0026quot;0.0.0.0/0\u0026quot;]. For tighter access, set it to specific IPs in secrets/inputs.sh, or set it to [] to block public SSH entirely (Tailscale-only mode).\nUFW (OS-level) Base rules configured during cloud-init. Tailscale rule added by Ansible (plays/tailscale.yml) during bootstrap.\n1 2 3 4 ufw default deny incoming ufw default allow outgoing ufw allow ssh ufw allow 41641/udp # Tailscale — added by Ansible, not cloud-init IPv6 IPv6 is disabled on all OpenClaw VPS instances at the OS level via sysctl. UFW only manages IPv4; Hetzner Cloud assigns a public IPv6 address to every VPS by default, meaning the server has an unfiltered IPv6 interface exposed to the internet unless explicitly addressed.\nRather than maintaining separate ip6tables rules, we disable IPv6 entirely. This aligns with the current IPv4-only Tailscale + UFW setup.\nA sysctl configuration file is written during cloud-init:\n1 /etc/sysctl.d/99-disable-ipv6.conf 1 2 3 net.ipv6.conf.all.disable_ipv6 = 1 net.ipv6.conf.default.disable_ipv6 = 1 net.ipv6.conf.lo.disable_ipv6 = 1 Applying to existing servers:\nCloud-init only runs on first boot. To apply this to an existing server:\n1 2 3 4 5 # One-time: via Ansible bootstrap (recommended) make bootstrap # Or via the Hetzner web console (root shell): sysctl --system | grep ipv6 Verifying:\n1 2 3 4 5 # Should return 1 (disabled) ssh openclaw@\u0026lt;server-ip\u0026gt; \u0026#39;cat /proc/sys/net/ipv6/conf/all/disable_ipv6\u0026#39; # Should show no IPv6 addresses (except link-local fe80::) ssh openclaw@\u0026lt;server-ip\u0026gt; \u0026#39;ip -6 addr show\u0026#39; SSH Hardening sshd drop-in (/etc/ssh/sshd_config.d/99-hardening.conf) Written during cloud-init (permissions 0600, owner root):\n1 2 3 4 5 PasswordAuthentication no PermitRootLogin no MaxAuthTries 3 LoginGraceTime 30 X11Forwarding no Key-based authentication only. Root login is disabled; all access is via the openclaw user (passwordless sudo, scoped to specific commands — see Sudo Permissions).\nfail2ban (/etc/fail2ban/jail.local) Installed and started during cloud-init:\n1 2 3 4 5 6 7 8 [sshd] enabled = true port = ssh filter = sshd logpath = /var/log/auth.log maxretry = 3 bantime = 1h findtime = 600 Bans IPs after 3 failed attempts within 10 minutes. Ban duration is 1 hour.\nApplying to an existing server Cloud-init only runs on first boot. To apply SSH hardening to a running server, follow the checklist below, then run these commands manually:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 # 1. Write sshd drop-in sudo tee /etc/ssh/sshd_config.d/99-hardening.conf \u0026gt; /dev/null \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; PasswordAuthentication no PermitRootLogin no MaxAuthTries 3 LoginGraceTime 30 X11Forwarding no EOF sudo chmod 600 /etc/ssh/sshd_config.d/99-hardening.conf # 2. Write fail2ban jail sudo apt-get install -y fail2ban sudo tee /etc/fail2ban/jail.local \u0026gt; /dev/null \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; [sshd] enabled = true port = ssh filter = sshd logpath = /var/log/auth.log maxretry = 3 bantime = 1h findtime = 600 EOF # 3. Validate sshd config before reloading — stop here if this fails sudo sshd -t \u0026amp;\u0026amp; echo CONFIG OK # 4. Reload (preserves existing sessions) sudo systemctl reload ssh sudo systemctl reload fail2ban Safety checklist for applying to a running server: see SSH Hardening Safety Checklist below.\nSudo Permissions The openclaw user has passwordless sudo, scoped to specific commands only (no NOPASSWD:ALL).\nContainers are managed via docker compose (no sudo needed — the user is in the docker group). The sudo whitelist is for system-level operations only.\nAllowed commands Command Purpose /usr/bin/tailscale status View VPN status (used by make status) /usr/bin/tailscale up Re-authenticate Tailscale (used by make tailscale-up) /usr/sbin/ufw status View firewall status (read-only) /usr/bin/journalctl * Read system/service logs for debugging /usr/bin/systemctl daemon-reload Reload systemd after installing units /usr/bin/systemctl restart docker Restart Docker daemon if it crashes /usr/bin/fail2ban-client status * View fail2ban jail status (used by make status) Not included by design:\ndocker * — redundant (user is in the docker group) and equivalent to full root systemctl restart openclaw-* — these are Docker containers, not systemd services; use docker compose restart apt upgrade/autoremove — handled by unattended-upgrades Applying to an existing server Cloud-init only runs on first boot. To apply scoped sudo on a running server:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 sudo tee /etc/sudoers.d/openclaw \u0026gt; /dev/null \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; # OpenClaw user sudo permissions — scoped for security openclaw ALL=(root) NOPASSWD: /usr/bin/tailscale status openclaw ALL=(root) NOPASSWD: /usr/bin/tailscale up openclaw ALL=(root) NOPASSWD: /usr/sbin/ufw status openclaw ALL=(root) NOPASSWD: /usr/bin/journalctl * openclaw ALL=(root) NOPASSWD: /usr/bin/systemctl daemon-reload openclaw ALL=(root) NOPASSWD: /usr/bin/systemctl restart docker openclaw ALL=(root) NOPASSWD: /usr/bin/fail2ban-client status * EOF sudo chmod 440 /etc/sudoers.d/openclaw # Verify — this should succeed sudo -u openclaw sudo -n tailscale status # Verify dangerous commands are blocked — these should fail sudo -u openclaw sudo -n whoami # → NOT in sudoers sudo -u openclaw sudo -n cat /etc/shadow # → NOT in sudoers Before applying to production: open two SSH sessions, apply in one, and verify the other still works. The Hetzner Cloud console (web VNC) is the safety net if you lock yourself out.\nTailscale Tailscale provides a WireGuard-based VPN mesh. When enabled, it allows SSH and gateway access over the Tailnet without exposing any ports to the public internet.\nTerraform variable:\nVariable Default Where set Description enable_tailscale false terraform/envs/prod/terraform.tfvars (copy from terraform.tfvars.example) Opens Hetzner firewall UDP 41641 The auth key is not a Terraform variable. It is passed to Ansible as TAILSCALE_AUTH_KEY (set in secrets/inputs.sh).\nAnsible provisioning (ansible/plays/tailscale.yml):\nRuns as part of make bootstrap, or standalone via make tailscale-setup. Skipped if TAILSCALE_AUTH_KEY is unset.\n1 2 3 4 curl -fsSL https://tailscale.com/install.sh | sh systemctl enable --now tailscaled ufw allow 41641/udp tailscale up --auth-key \u0026lt;key\u0026gt; --accept-routes --hostname openclaw-prod If no auth key is set, run make tailscale-up after make bootstrap to authenticate interactively.\nUseful commands:\n1 2 3 make tailscale-status # Show connection state and peers make tailscale-ip # Get Tailscale IPv4 address (100.x.x.x) make tailscale-up # Re-authenticate Tailscale manually Gateway allowlisting: The OpenClaw gateway\u0026rsquo;s allowTailscale: true setting in openclaw.json trusts requests arriving over Tailscale without requiring an explicit token.\nSecrets Encryption (SOPS + age) All secrets are encrypted at rest using SOPS with age keys. The encrypted file secrets/.env.enc is committed to git — safe to share, since only keyholders can decrypt.\nHow It Works Layer Mechanism At rest (git) secrets/.env.enc — AES-256-GCM encrypted by SOPS, safe to commit In transit (CI) GitHub Actions decrypts using SOPS_AGE_KEY secret before make deploy On VPS (docker-compose) Ansible decrypts .env.enc → .env temporarily, then shred -u after containers start On VPS (OpenClaw native) Gateway container decrypts .env.enc on-the-fly via SOPS exec provider Key Management Age key: Generated once with age-keygen, stored locally in secrets/age-key.txt (gitignored) GitHub secret: Full content of age-key.txt stored as SOPS_AGE_KEY VPS: Age key mounted read-only into gateway container at /home/node/.config/sops/age/keys.txt Local Workflow 1 2 3 4 5 6 7 8 9 10 11 12 # First-time setup make secrets-generate-key # Creates secrets/age-key.txt # Add the public key to .sops.yaml, then: make secrets-encrypt # Encrypts secrets/.env → secrets/.env.enc git add secrets/.env.enc \u0026amp;\u0026amp; git commit # Commit encrypted file # Day-to-day make secrets-edit # Edit encrypted file in-place (opens $EDITOR) make secrets-decrypt # Decrypt to secrets/.env for local use # Deploy (decrypts automatically in CI; locally run secrets-decrypt first) make deploy OpenClaw Native SOPS Provider The gateway container uses SOPS as an exec-based secrets provider (openclaw.json → secrets.providers.sops_env). This allows OpenClaw to decrypt individual secrets on-the-fly without persisting plaintext on disk.\nThe encrypted .env.enc is mounted into the container at /home/node/.openclaw/secrets/.env.enc, and the age key is available via SOPS_AGE_KEY_FILE. Individual secrets are extracted via a shell command that decrypts and greps the value:\n1 sops -d /home/node/.openclaw/secrets/.env.enc | grep -E \u0026#39;^{{secret}}=\u0026#39; | cut -d= -f2- | tr -d \u0026#39;\\n\u0026#39; First-Time Setup Checklist make secrets-generate-key — creates secrets/age-key.txt Copy the public key (shown after generation) into .sops.yaml under age: make secrets-encrypt — creates secrets/.env.enc Add SOPS_AGE_KEY to GitHub Secrets (Settings → Secrets → Actions) — paste the full content of secrets/age-key.txt On VPS: ensure age key exists at /home/openclaw/.config/sops/age/keys.txt (or set SOPS_AGE_KEY_FILE in env) GitHub Actions Validate (validate.yml) Runs on every PR and push to main. Fails the build if any of these fail:\nterraform fmt -check — formatting terraform validate — Terraform validity shellcheck — shell script linting ansible-playbook --syntax-check — Ansible syntax Checkov IaC scan (soft fail — reports but doesn\u0026rsquo;t block) Deploy (deploy.yml) Triggers on push to main when docker/, docker-compose.yml, ansible/, or secrets/.env.enc change.\nConnects to Tailscale using OAuth credentials (TAILSCALE_OAUTH_CLIENT_ID, TAILSCALE_OAUTH_CLIENT_SECRET) with tag tag:ci-runner Writes SSH_PRIVATE_KEY to a temp key file (0600) Installs SOPS, decrypts secrets/.env.enc using SOPS_AGE_KEY secret Runs make deploy (or make deploy REBUILD=1 if Docker files changed) Required GitHub secrets: TAILSCALE_OAUTH_CLIENT_ID, TAILSCALE_OAUTH_CLIENT_SECRET, SSH_PRIVATE_KEY, SERVER_IP, SOPS_AGE_KEY.\nTemplate: .github/workflows/deploy.yml.example — copy to deploy.yml to enable. See GitOps auto-deploy for full setup.\nRollback (rollback.yml) Manual workflow dispatch. Takes a Git SHA, checks out that commit, decrypts secrets with SOPS, and runs make deploy REBUILD=1 to redeploy from that point.\nAutomatic Security Updates unattended-upgrades is installed and enabled during cloud-init. It applies OS security patches automatically without manual intervention.\nSSH Hardening Safety Checklist Use this when applying SSH hardening to a server that is already running.\nPre-flight (do BEFORE touching anything) Open two SSH sessions to the VPS as the openclaw user — one to apply changes, one as a bail-out lifeline Verify openclaw user SSH works — hardening sets PermitRootLogin no, so root SSH will be lost Verify sudo -n tailscale status works (scoped passwordless sudo is configured in cloud-init) Note your current IP — echo $SSH_CLIENT — so you can unban yourself if fail2ban triggers Confirm ssh-add -l is non-empty, or that you\u0026rsquo;re using -i \u0026lt;keyfile\u0026gt; directly Apply changes (keep the second session open throughout) Write /etc/ssh/sshd_config.d/99-hardening.conf with 0600 permissions owned by root Write /etc/fail2ban/jail.local Install fail2ban: sudo apt-get install -y fail2ban Validate sshd config: sudo sshd -t — if this fails, do not proceed Reload sshd: sudo systemctl reload ssh Verify the second session still responds Reload fail2ban: sudo systemctl reload fail2ban Post-flight verification Open a third terminal and confirm a fresh SSH session connects Verify root login is rejected: ssh root@\u0026lt;ip\u0026gt; should return Permission denied Check fail2ban: sudo systemctl status fail2ban and sudo fail2ban-client status sshd Rollback (if locked out) Use Hetzner Cloud console (web VNC) to regain access Remove /etc/ssh/sshd_config.d/99-hardening.conf and run sudo systemctl reload ssh Unban your IP: sudo fail2ban-client set sshd unbanip \u0026lt;YOUR_IP\u0026gt; ","permalink":"http://tardigrde.github.io/openclaw-deploy/security/hardening/","summary":"\u003ch1 id=\"security-hardening\"\u003eSecurity Hardening\u003c/h1\u003e\n\u003cp\u003eOverview of all security controls applied to the OpenClaw VPS.\u003c/p\u003e\n\u003ch2 id=\"firewall\"\u003eFirewall\u003c/h2\u003e\n\u003cp\u003eTwo firewall layers run in series.\u003c/p\u003e\n\u003ch3 id=\"hetzner-cloud-firewall-network-level\"\u003eHetzner Cloud Firewall (network-level)\u003c/h3\u003e\n\u003cp\u003eManaged in Terraform (\u003ccode\u003eterraform/modules/hetzner-vps/main.tf\u003c/code\u003e). Applied at the hypervisor — traffic is dropped before it reaches the OS.\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eDirection\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eProtocol\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003ePort\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eSource\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eCondition\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eInbound\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eTCP\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e22\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003essh_allowed_cidrs\u003c/code\u003e variable\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eAlways\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eInbound\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eUDP\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e41641\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e0.0.0.0/0\u003c/code\u003e, \u003ccode\u003e::/0\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eOnly if Tailscale on\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eOutbound\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eTCP\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e1–65535\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e0.0.0.0/0\u003c/code\u003e, \u003ccode\u003e::/0\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eAlways\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eOutbound\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eUDP\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e1–65535\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e0.0.0.0/0\u003c/code\u003e, \u003ccode\u003e::/0\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eAlways\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eOutbound\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eICMP\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e—\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e0.0.0.0/0\u003c/code\u003e, \u003ccode\u003e::/0\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eAlways\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003ccode\u003essh_allowed_cidrs\u003c/code\u003e defaults to \u003ccode\u003e[\u0026quot;0.0.0.0/0\u0026quot;]\u003c/code\u003e. For tighter access, set it to specific IPs in \u003ccode\u003esecrets/inputs.sh\u003c/code\u003e, or set it to \u003ccode\u003e[]\u003c/code\u003e to block public SSH entirely (Tailscale-only mode).\u003c/p\u003e","title":"Security Hardening"},{"content":"Morning Weather Report A lightweight daily weather summary sent to Telegram each morning. Uses wttr.in and the Telegram Bot API — no LLM involved, zero token cost.\nFormat:\n1 2 3 Good morning! 2026-03-05, Wednesday Now: 8°C (feels like 5°C) Max: 12°C / Min: 3°C | Rain: 40% (2.4 mm) Rain line only appears when chance \u0026gt;= 20%.\nSetup Add to secrets/.env:\n1 2 TELEGRAM_BOT_TOKEN=your-bot-token TELEGRAM_CHAT_ID=your-chat-id Then install the cron job on the VPS:\n1 make addon-weather This copies scripts/local/morning-weather.py to the VPS and registers a host cron entry (copy scripts/local.example/morning-weather.py to scripts/local/morning-weather.py first — scripts/local/ is gitignored for personal customization) (0 5 * * * UTC). Adjust the hour in the Ansible task to match your timezone. Credentials are sourced from ~/openclaw/.env at runtime.\nTest 1 2 make ssh ~/run-morning-weather.sh Logs: ~/logs/morning-weather.log on the VPS.\nNote: If you have a morning-weather cron job configured in OpenClaw, you can remove it once this is working — this host-level cron replaces it.\n","permalink":"http://tardigrde.github.io/openclaw-deploy/addons/weather-report/","summary":"\u003ch1 id=\"morning-weather-report\"\u003eMorning Weather Report\u003c/h1\u003e\n\u003cp\u003eA lightweight daily weather summary sent to Telegram each morning. Uses \u003ccode\u003ewttr.in\u003c/code\u003e and the Telegram Bot API — no LLM involved, zero token cost.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eFormat:\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\n\u003ctable style=\"border-spacing:0;padding:0;margin:0;border:0;\"\u003e\u003ctr\u003e\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e1\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e2\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e3\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;;width:100%\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eGood morning! 2026-03-05, Wednesday\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eNow: 8°C (feels like 5°C)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eMax: 12°C / Min: 3°C | Rain: 40% (2.4 mm)\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003eRain line only appears when chance \u0026gt;= 20%.\u003c/p\u003e\n\u003ch2 id=\"setup\"\u003eSetup\u003c/h2\u003e\n\u003cp\u003eAdd to \u003ccode\u003esecrets/.env\u003c/code\u003e:\u003c/p\u003e","title":"Weather Report"},{"content":"Sandbox Configuration Guide This guide explains how to enable agent sandboxing in OpenClaw. Sandboxing isolates tool execution from the host system, providing an extra layer of security.\nNote: This guide is for the openclaw-deploy repository. For the full OpenClaw sandboxing documentation, see Gateway Sandboxing.\nWhy Enable Sandboxing? Without sandboxing, agent tool execution (file operations, shell commands) runs directly on your VPS. If the agent is compromised or makes a mistake, the blast radius is your entire server.\nWith sandboxing enabled, tools run in isolated containers or remote environments.\nSecurity Considerations Without Sandbox With Sandbox Full host access Isolated execution Any file readable/writable Restricted to sandbox workspace Host secrets at risk Secrets stay on host Quick Start To enable sandboxing, add this to your openclaw.json:\n1 2 3 4 5 6 7 8 9 10 11 { \u0026#34;agents\u0026#34;: { \u0026#34;defaults\u0026#34;: { \u0026#34;sandbox\u0026#34;: { \u0026#34;mode\u0026#34;: \u0026#34;all\u0026#34;, // \u0026#34;off\u0026#34;, \u0026#34;non-main\u0026#34;, or \u0026#34;all\u0026#34; \u0026#34;scope\u0026#34;: \u0026#34;session\u0026#34;, // \u0026#34;session\u0026#34;, \u0026#34;agent\u0026#34;, or \u0026#34;shared\u0026#34; \u0026#34;workspaceAccess\u0026#34;: \u0026#34;none\u0026#34; // \u0026#34;none\u0026#34;, \u0026#34;ro\u0026#34;, or \u0026#34;rw\u0026#34; } } } } Mode Options Mode Description \u0026quot;off\u0026quot; No sandboxing (default) \u0026quot;non-main\u0026quot; Sandbox non-main sessions only (good for dev) \u0026quot;all\u0026quot; Every session runs in sandbox (recommended for production) Scope Options Scope Description \u0026quot;session\u0026quot; One container per session (default) \u0026quot;agent\u0026quot; One container per agent \u0026quot;shared\u0026quot; One container shared by all sessions Workspace Access Access Description \u0026quot;none\u0026quot; Sandbox has isolated workspace (default, most secure) \u0026quot;ro\u0026quot; Agent workspace mounted read-only \u0026quot;rw\u0026quot; Agent workspace mounted read/write Docker Sandbox Setup The sandbox image is built automatically when you run make deploy REBUILD=1 — no manual setup required.\nPer-Agent Override You can enable sandboxing for specific agents only:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { \u0026#34;agents\u0026#34;: { \u0026#34;defaults\u0026#34;: { \u0026#34;sandbox\u0026#34;: { \u0026#34;mode\u0026#34;: \u0026#34;off\u0026#34; } }, \u0026#34;list\u0026#34;: [ { \u0026#34;id\u0026#34;: \u0026#34;untrusted-code-review\u0026#34;, \u0026#34;sandbox\u0026#34;: { \u0026#34;mode\u0026#34;: \u0026#34;all\u0026#34;, \u0026#34;scope\u0026#34;: \u0026#34;session\u0026#34; } } ] } } Tool Policy with Sandbox Tool policies still apply inside sandbox. To deny specific tools even in sandbox:\n1 2 3 4 5 { \u0026#34;tools\u0026#34;: { \u0026#34;deny\u0026#34;: [\u0026#34;group:runtime\u0026#34;, \u0026#34;group:automation\u0026#34;] } } Elevated Exec (Bypasses Sandbox) The tools.elevated setting allows certain users to run exec on the host, bypassing sandboxing. This is an escape hatch — use with caution:\n1 2 3 4 5 6 7 8 9 10 { \u0026#34;tools\u0026#34;: { \u0026#34;elevated\u0026#34;: { \u0026#34;enabled\u0026#34;: true, \u0026#34;allowFrom\u0026#34;: { \u0026#34;telegram\u0026#34;: [\u0026#34;\u0026lt;your-user-id\u0026gt;\u0026#34;] } } } } Troubleshooting Sandbox container not starting 1 2 3 4 5 # Check sandbox status docker ps -a | grep openclaw-sandbox # Check logs docker logs \u0026lt;container-id\u0026gt; Permission errors Ensure your workspace directory is owned by the correct UID:\n1 sudo chown -R 1000:1000 ~/.openclaw/workspace See Also OpenClaw Sandboxing Docs OpenClaw Security OpenShell Backend ","permalink":"http://tardigrde.github.io/openclaw-deploy/guides/sandboxing/","summary":"\u003ch1 id=\"sandbox-configuration-guide\"\u003eSandbox Configuration Guide\u003c/h1\u003e\n\u003cp\u003eThis guide explains how to enable agent sandboxing in OpenClaw. Sandboxing isolates tool execution from the host system, providing an extra layer of security.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eNote:\u003c/strong\u003e This guide is for the openclaw-deploy repository. For the full OpenClaw sandboxing documentation, see \u003ca href=\"https://docs.openclaw.ai/gateway/sandboxing\"\u003eGateway Sandboxing\u003c/a\u003e.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"why-enable-sandboxing\"\u003eWhy Enable Sandboxing?\u003c/h2\u003e\n\u003cp\u003eWithout sandboxing, agent tool execution (file operations, shell commands) runs directly on your VPS. If the agent is compromised or makes a mistake, the blast radius is your entire server.\u003c/p\u003e","title":"Sandbox Configuration"},{"content":"ACP + Claude Code Use Claude Code (and other AI coding harnesses) from your messaging platform — no SSH required. Runs inside the Gateway container via ACP (Agent Client Protocol).\nArchitecture 1 Telegram/Discord → OpenClaw Gateway → ACP runtime → acpx → Claude Code CLI You send a message like \u0026ldquo;review this PR with Claude Code\u0026rdquo; and OpenClaw spawns an ACP session. Claude Code does the work, results stream back to your chat.\nSupported Harnesses Harness Command Claude Code /acp spawn claude (default) OpenAI Codex /acp spawn codex Google Gemini /acp spawn gemini OpenCode /acp spawn opencode Pi /acp spawn pi Kimi /acp spawn kimi Prerequisites ANTHROPIC_API_KEY in secrets/.env (required for Claude Code) For other harnesses: their respective API keys Setup ACP requires two things: the packages baked into the image, and config in openclaw.json. Both are managed through the normal deploy flow — no separate Ansible step needed.\n1. Add packages to docker/install-optional.sh In your private fork, edit docker/install-optional.sh and add:\n1 2 3 npm install -g \\ acpx@0.3.1 \\ @anthropic-ai/claude-code@2.1.81 2. Add ACP config to openclaw.json Add the following to your local openclaw.json (merge into existing keys as needed):\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 { \u0026#34;acp\u0026#34;: { \u0026#34;enabled\u0026#34;: true, \u0026#34;dispatch\u0026#34;: { \u0026#34;enabled\u0026#34;: true }, \u0026#34;backend\u0026#34;: \u0026#34;acpx\u0026#34;, \u0026#34;defaultAgent\u0026#34;: \u0026#34;claude\u0026#34;, \u0026#34;allowedAgents\u0026#34;: [\u0026#34;claude\u0026#34;, \u0026#34;codex\u0026#34;, \u0026#34;pi\u0026#34;, \u0026#34;opencode\u0026#34;, \u0026#34;gemini\u0026#34;, \u0026#34;kimi\u0026#34;], \u0026#34;maxConcurrentSessions\u0026#34;: 4, \u0026#34;stream\u0026#34;: { \u0026#34;coalesceIdleMs\u0026#34;: 300, \u0026#34;maxChunkChars\u0026#34;: 1200 }, \u0026#34;runtime\u0026#34;: { \u0026#34;ttlMinutes\u0026#34;: 120 } }, \u0026#34;plugins\u0026#34;: { \u0026#34;entries\u0026#34;: { \u0026#34;acpx\u0026#34;: { \u0026#34;enabled\u0026#34;: true, \u0026#34;config\u0026#34;: { \u0026#34;permissionMode\u0026#34;: \u0026#34;approve-all\u0026#34;, \u0026#34;nonInteractivePermissions\u0026#34;: \u0026#34;deny\u0026#34; } } } } } For subagent spawning, also add \u0026quot;claude\u0026quot; to your main agent\u0026rsquo;s subagents.allowAgents list.\n3. Deploy 1 make deploy REBUILD=1 This rebuilds the image with the new packages and pushes the updated config.\nUsage Persistent session (recommended) Start a long-lived session in a Telegram thread or Discord channel:\n1 /acp spawn claude --mode persistent --thread auto Follow-up messages in the same thread go to the same Claude Code session.\nOne-shot handoff Tell OpenClaw directly:\n1 use Claude Code to review the changes in this PR OpenClaw spawns a session, runs the task, and reports back.\nFrom another agent OpenClaw can spawn ACP sessions via the sessions_spawn tool:\n1 runtime: \u0026#34;acp\u0026#34;, agentId: \u0026#34;claude\u0026#34;, mode: \u0026#34;run\u0026#34; Config reference Permissions Setting Value Why permissionMode approve-all No TTY in ACP sessions — interactive approval doesn\u0026rsquo;t work nonInteractivePermissions deny Graceful degradation instead of crashing on permission prompts To tighten permissions, change permissionMode to approve-reads (auto-approve reads, ask before writes). This only works if someone is watching the session to approve write operations.\nSession TTL runtime.ttlMinutes: 120 — sessions expire after 2 hours of inactivity. Increase for long-running coding tasks.\nTroubleshooting 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # Verify acpx is installed docker compose exec openclaw-gateway acpx --version # Verify Claude Code is installed docker compose exec openclaw-gateway claude --version # Check plugin is loaded docker compose exec openclaw-gateway openclaw plugins list | grep acpx # Check ACP config docker compose exec openclaw-gateway cat /home/node/.openclaw/openclaw.json | jq .acp # View ACP session logs docker compose logs openclaw-gateway --tail 50 | grep -i acp Common issues \u0026ldquo;acpx: not found\u0026rdquo; — packages aren\u0026rsquo;t in the image. Add them to docker/install-optional.sh and run make deploy REBUILD=1. Claude Code crashes on file write — check permissionMode is set to approve-all in openclaw.json. Session doesn\u0026rsquo;t persist across messages — use --thread auto or stay in the same Telegram thread/Discord channel. \u0026ldquo;ANTHROPIC_API_KEY not set\u0026rdquo; — add it with make secrets-edit then make deploy. ","permalink":"http://tardigrde.github.io/openclaw-deploy/addons/acp-claude-code/","summary":"\u003ch1 id=\"acp--claude-code\"\u003eACP + Claude Code\u003c/h1\u003e\n\u003cp\u003eUse Claude Code (and other AI coding harnesses) from your messaging platform — no SSH required. Runs inside the Gateway container via \u003ca href=\"https://agentclientprotocol.com/\"\u003eACP (Agent Client Protocol)\u003c/a\u003e.\u003c/p\u003e\n\u003ch2 id=\"architecture\"\u003eArchitecture\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\n\u003ctable style=\"border-spacing:0;padding:0;margin:0;border:0;\"\u003e\u003ctr\u003e\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;;width:100%\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eTelegram/Discord → OpenClaw Gateway → ACP runtime → acpx → Claude Code CLI\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003eYou send a message like \u003cem\u003e\u0026ldquo;review this PR with Claude Code\u0026rdquo;\u003c/em\u003e and OpenClaw spawns an ACP session. Claude Code does the work, results stream back to your chat.\u003c/p\u003e","title":"ACP + Claude Code"},{"content":"Continues from Installation. Run source secrets/inputs.sh before any make target that touches the VPS.\n5. Provision the VPS 1 2 3 4 source secrets/inputs.sh make init make plan make apply 6. Configure container secrets 1 2 cp secrets/.env.example secrets/.env vim secrets/.env Required: OPENCLAW_GATEWAY_TOKEN, ANTHROPIC_API_KEY (or leave empty for subscription auth), TELEGRAM_BOT_TOKEN. See Secrets Reference for the full variable reference.\n7. (Optional) Encrypt secrets at rest with SOPS Complete step 6 first — make secrets-encrypt reads secrets/.env.\n1 2 3 make secrets-generate-key # generates secrets/age-key.txt and prints the public key # Edit .sops.yaml — paste the age public key printed above into the `age:` field make secrets-encrypt # encrypts secrets/.env → secrets/.env.enc Plain .env works for local use. SOPS is required for the GitOps auto-deploy workflow. See Secrets Reference § SOPS for the full workflow.\nAdding extra services? Copy docker-compose.override.example.yml → docker-compose.override.yml before the next step — it cannot be merged in after bootstrap without re-running it.\n1 2 cp docker-compose.override.example.yml docker-compose.override.yml vim docker-compose.override.yml 8. Bootstrap OpenClaw 1 2 source secrets/inputs.sh make bootstrap Creates directories, builds Docker images, pushes config, and starts containers — everything in one command.\nClaude subscription auth: If you left ANTHROPIC_API_KEY empty, run make setup-auth after bootstrap to link your Claude subscription.\n9. Verify 1 2 3 make status # all containers should show \u0026#34;healthy\u0026#34; or \u0026#34;running\u0026#34; make logs # look for \u0026#34;[entrypoint] Skill installation complete\u0026#34; and \u0026#34;Gateway listening\u0026#34; make tunnel # opens SSH tunnel: localhost:18789 → VPS:18789 Open http://localhost:18789 and paste your OPENCLAW_GATEWAY_TOKEN to authenticate.\nSuccess looks like: Gateway UI loads, token is accepted, you can start a conversation.\nCommon failures:\nmake status shows container restarting → check make logs for missing env vars (most likely OPENCLAW_GATEWAY_TOKEN not set in .env) make tunnel hangs → SSH key or SERVER_IP issue; verify with make ssh Gateway UI shows \u0026ldquo;Unauthorized\u0026rdquo; → wrong OPENCLAW_GATEWAY_TOKEN Access the Gateway The gateway binds to 127.0.0.1:18789 (localhost only).\nVia SSH tunnel (always available):\n1 make tunnel # localhost:18789 -\u0026gt; VPS:18789 Open http://localhost:18789 and paste your OPENCLAW_GATEWAY_TOKEN.\nVia Tailscale Serve (if Tailscale is enabled):\nmake bootstrap deploys the Tailscale serve config automatically — no manual steps needed. The gateway is available at:\n1 https://openclaw-prod.\u0026lt;tailnet\u0026gt;.ts.net from any tailnet device. See Tailscale for serve config details and troubleshooting.\n","permalink":"http://tardigrde.github.io/openclaw-deploy/getting-started/bootstrap/","summary":"\u003cp\u003eContinues from \u003ca href=\"/getting-started/installation/\"\u003eInstallation\u003c/a\u003e. Run \u003ccode\u003esource secrets/inputs.sh\u003c/code\u003e before any make target that touches the VPS.\u003c/p\u003e\n\u003ch2 id=\"5-provision-the-vps\"\u003e5. Provision the VPS\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\n\u003ctable style=\"border-spacing:0;padding:0;margin:0;border:0;\"\u003e\u003ctr\u003e\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e1\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e2\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e3\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e4\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;;width:100%\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003esource secrets/inputs.sh\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003emake init\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003emake plan\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003emake apply\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"6-configure-container-secrets\"\u003e6. Configure container secrets\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\n\u003ctable style=\"border-spacing:0;padding:0;margin:0;border:0;\"\u003e\u003ctr\u003e\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e1\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e2\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;;width:100%\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecp secrets/.env.example secrets/.env\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003evim secrets/.env\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003eRequired: \u003ccode\u003eOPENCLAW_GATEWAY_TOKEN\u003c/code\u003e, \u003ccode\u003eANTHROPIC_API_KEY\u003c/code\u003e (or leave empty for subscription auth), \u003ccode\u003eTELEGRAM_BOT_TOKEN\u003c/code\u003e. See \u003ca href=\"/configuration/secrets/\"\u003eSecrets Reference\u003c/a\u003e for the full variable reference.\u003c/p\u003e","title":"Deployment \u0026 Bootstrap"},{"content":"GitOps Auto-Deploy A GitHub Actions pipeline can automatically deploy to the VPS on every push to main. This is optional — if you don\u0026rsquo;t configure the secrets below, the workflow won\u0026rsquo;t run and you can continue deploying manually with make deploy.\nWhat It Does Triggered by changes to openclaw.json, docker/, docker-compose.yml, ansible/, or secrets/.env.enc Connects to the VPS via an ephemeral Tailscale node (no public SSH required) Decrypts secrets/.env.enc using SOPS + age key from GitHub Secrets If docker/ or docker-compose.yml changed: runs make deploy REBUILD=1 Otherwise: runs make deploy A manual rollback workflow is also included (.github/workflows/rollback.yml) — trigger it from the GitHub Actions UI with any previous git SHA. It also decrypts secrets with SOPS before deploying.\nOne-Time Setup 1. Generate a dedicated CI deploy SSH key\n1 2 3 4 5 6 7 8 9 10 ssh-keygen -t ed25519 -C \u0026#34;ci-deploy@openclaw\u0026#34; -f ci-deploy-key -N \u0026#34;\u0026#34; # Authorize on the VPS ssh openclaw@\u0026lt;VPS_IP\u0026gt; \u0026#39;cat \u0026gt;\u0026gt; ~/.ssh/authorized_keys\u0026#39; \u0026lt; ci-deploy-key.pub # Copy the private key — you\u0026#39;ll add it as a GitHub Secret cat ci-deploy-key # Clean up — never commit the private key rm ci-deploy-key ci-deploy-key.pub 2. Create a Tailscale OAuth client\nFull reference: Tailscale CI/CD guide\nTailscale admin → Settings → OAuth clients → Create new client Required scopes: Devices → Write and Auth keys → Write Assign tag: tag:ci-runner (this must be the only tag — do not add other tags to this client) Important: Tailscale OAuth clients with 2+ tags require all tags simultaneously on every key request. Keep this client single-tag. The Terraform provider uses a separate OAuth client for tag:openclaw-vps.\nSave Client ID and Client Secret 3. Tag the VPS in Tailscale\nTailscale admin → Machines → your VPS → Tags → add tag:openclaw-vps.\n4. Set up SOPS encryption\n1 2 3 4 5 6 7 8 9 10 # Generate age key make secrets-generate-key # Encrypt your .env file make secrets-encrypt # Add the public key to .sops.yaml (shown after generation) # Commit the encrypted file git add secrets/.env.enc .sops.yaml git commit -m \u0026#34;chore: add SOPS-encrypted secrets\u0026#34; 5. Add GitHub repository secrets (Settings → Secrets and variables → Actions → Repository secrets):\nSecret Value TAILSCALE_OAUTH_CLIENT_ID From step 2 TAILSCALE_OAUTH_CLIENT_SECRET From step 2 SSH_PRIVATE_KEY Private key from step 1 SERVER_IP Tailscale IP of the VPS (e.g. 100.x.x.x) — use the IP, not the MagicDNS hostname (MagicDNS resolution is unreliable on ephemeral runners) SOPS_AGE_KEY Full content of secrets/age-key.txt (the age private key — multiline is fine) Updating Secrets When you need to change environment variables:\n1 2 3 4 5 6 7 8 9 10 11 12 # Edit the encrypted file (opens $EDITOR with decrypted content) make secrets-edit # Or manually: make secrets-decrypt # → secrets/.env # ... edit secrets/.env ... make secrets-encrypt # → secrets/.env.enc # Commit and push — CI will auto-deploy with the new secrets git add secrets/.env.enc git commit -m \u0026#34;chore: update environment secrets\u0026#34; git push Verification Push a change to openclaw.json → confirm the Deploy to VPS workflow triggers and completes Push a change to docker/Dockerfile → confirm the rebuild step runs Push a change to secrets/.env.enc → confirm the decrypt step works and deploy succeeds Trigger Rollback deployment from the GitHub Actions UI with a previous SHA → confirm the VPS reverts ","permalink":"http://tardigrde.github.io/openclaw-deploy/operations/gitops-auto-deploy/","summary":"\u003ch1 id=\"gitops-auto-deploy\"\u003eGitOps Auto-Deploy\u003c/h1\u003e\n\u003cp\u003eA GitHub Actions pipeline can automatically deploy to the VPS on every push to \u003ccode\u003emain\u003c/code\u003e. This is \u003cstrong\u003eoptional\u003c/strong\u003e — if you don\u0026rsquo;t configure the secrets below, the workflow won\u0026rsquo;t run and you can continue deploying manually with \u003ccode\u003emake deploy\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"what-it-does\"\u003eWhat It Does\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eTriggered by changes to \u003ccode\u003eopenclaw.json\u003c/code\u003e, \u003ccode\u003edocker/\u003c/code\u003e, \u003ccode\u003edocker-compose.yml\u003c/code\u003e, \u003ccode\u003eansible/\u003c/code\u003e, or \u003ccode\u003esecrets/.env.enc\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eConnects to the VPS via an ephemeral Tailscale node (no public SSH required)\u003c/li\u003e\n\u003cli\u003eDecrypts \u003ccode\u003esecrets/.env.enc\u003c/code\u003e using SOPS + age key from GitHub Secrets\u003c/li\u003e\n\u003cli\u003eIf \u003ccode\u003edocker/\u003c/code\u003e or \u003ccode\u003edocker-compose.yml\u003c/code\u003e changed: runs \u003ccode\u003emake deploy REBUILD=1\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eOtherwise: runs \u003ccode\u003emake deploy\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eA manual \u003cstrong\u003erollback workflow\u003c/strong\u003e is also included (\u003ccode\u003e.github/workflows/rollback.yml\u003c/code\u003e) — trigger it from the GitHub Actions UI with any previous git SHA. It also decrypts secrets with SOPS before deploying.\u003c/p\u003e","title":"GitOps Auto-Deploy"},{"content":"lossless-claw — Lossless Context Management What lossless-claw is an OpenClaw plugin that replaces the built-in sliding-window compaction with a DAG-based summarization system. Instead of truncating older messages when context fills up, it:\nPersists every message in a SQLite database Summarizes chunks of older messages into compressed nodes Condenses summaries into higher-level nodes as they accumulate (forming a DAG) Assembles context each turn by combining summaries + recent raw messages Provides agent tools (lcm_grep, lcm_describe, lcm_expand) so the agent can search and recall details from compacted history Why we install it The problem: Without LCM, OpenClaw\u0026rsquo;s default compaction simply drops older messages when context fills up. In long conversations (multi-hour sessions, daily check-ins), context gets lost — the agent forgets decisions made hours or days ago.\nThe solution: LCM ensures nothing is ever lost. The agent can drill into any summary to recover original details. For a personal assistant that needs to remember ongoing projects, house hunting progress, business decisions — this is critical.\nIn practice: The difference between \u0026ldquo;my AI forgot what we discussed yesterday\u0026rdquo; and \u0026ldquo;my AI remembers everything.\u0026rdquo; Essential for long sessions, daily check-ins, or any workflow where continuity across conversations matters.\nCost: Minimal. Summarization uses Anthropic Haiku (~$0.001-0.003 per compaction pass). For typical usage, adds ~€1-5/month. Configured via LCM_SUMMARY_MODEL to use a cheap model, not the main conversation model.\nConfiguration openclaw.json 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { \u0026#34;plugins\u0026#34;: { \u0026#34;entries\u0026#34;: { \u0026#34;lossless-claw\u0026#34;: { \u0026#34;enabled\u0026#34;: true, \u0026#34;config\u0026#34;: { \u0026#34;freshTailCount\u0026#34;: 32, \u0026#34;contextThreshold\u0026#34;: 0.75, \u0026#34;incrementalMaxDepth\u0026#34;: -1 } } }, \u0026#34;slots\u0026#34;: { \u0026#34;contextEngine\u0026#34;: \u0026#34;lossless-claw\u0026#34; } } } Key settings explained Setting Value Why freshTailCount 32 Protects last 32 messages from compaction — enough recent context for continuity contextThreshold 0.75 Triggers compaction at 75% of context window — leaves headroom for responses incrementalMaxDepth -1 Unlimited DAG depth — summaries cascade as deep as needed automatically LCM_SUMMARY_MODEL (env) anthropic/claude-haiku-4-5 Direct Anthropic API (not OpenRouter) — cheapest option for summarization. Set in .env. Installation The plugin is installed automatically via the Docker entrypoint script on every container start. It is enabled by default — no action needed to opt in.\nTo disable it, set INSTALL_LOSSLESS_CLAW=0 in secrets/.env:\n1 INSTALL_LOSSLESS_CLAW=0 Then redeploy: make deploy REBUILD=1.\nInstallation is idempotent — safe to run repeatedly. No Dockerfile changes needed.\nReferences GitHub repo Animated visualization LCM paper Configuration guide ","permalink":"http://tardigrde.github.io/openclaw-deploy/guides/lossless-claw/","summary":"\u003ch1 id=\"lossless-claw--lossless-context-management\"\u003elossless-claw — Lossless Context Management\u003c/h1\u003e\n\u003ch2 id=\"what\"\u003eWhat\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/Martian-Engineering/lossless-claw\"\u003elossless-claw\u003c/a\u003e is an OpenClaw plugin that replaces the built-in sliding-window compaction with a DAG-based summarization system. Instead of truncating older messages when context fills up, it:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003ePersists every message\u003c/strong\u003e in a SQLite database\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSummarizes chunks\u003c/strong\u003e of older messages into compressed nodes\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCondenses summaries\u003c/strong\u003e into higher-level nodes as they accumulate (forming a DAG)\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAssembles context\u003c/strong\u003e each turn by combining summaries + recent raw messages\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eProvides agent tools\u003c/strong\u003e (\u003ccode\u003elcm_grep\u003c/code\u003e, \u003ccode\u003elcm_describe\u003c/code\u003e, \u003ccode\u003elcm_expand\u003c/code\u003e) so the agent can search and recall details from compacted history\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"why-we-install-it\"\u003eWhy we install it\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eThe problem:\u003c/strong\u003e Without LCM, OpenClaw\u0026rsquo;s default compaction simply drops older messages when context fills up. In long conversations (multi-hour sessions, daily check-ins), context gets lost — the agent forgets decisions made hours or days ago.\u003c/p\u003e","title":"Lossless Context"},{"content":"Secrets Reference Two secret files are required. Neither is ever committed to git.\nsecrets/inputs.sh — Infrastructure config Sourced by the Makefile at parse time (and re-exported for Terraform). Contains deployment-level config, not runtime secrets.\n1 2 cp secrets/inputs.example.sh secrets/inputs.sh vim secrets/inputs.sh Variable Required Description HCLOUD_TOKEN Yes Hetzner Cloud API token — console.hetzner.cloud → Security → API Tokens SSH_KEY_FINGERPRINT Yes Fingerprint of your SSH key uploaded to Hetzner — console → Security → SSH Keys SERVER_IP Tailscale only Set to openclaw-prod (MagicDNS) after locking down SSH. Leave unset to auto-detect from Terraform output. TAILSCALE_AUTH_KEY Tailscale only Reusable pre-authorized key from login.tailscale.com/admin/settings/keys. Must be set before make bootstrap. SSH_KEY No Path to SSH private key. Defaults to ~/.ssh/id_rsa. The Makefile auto-sources inputs.sh for Terraform vars. For SSH-based targets (make ssh, make deploy, etc.), SERVER_IP must resolve — either auto-detected from Terraform output or explicitly set.\nsecrets/.env — Container runtime secrets Deployed to the VPS by Ansible and loaded by Docker Compose. Contains API keys and service tokens.\n1 2 cp secrets/.env.example secrets/.env vim secrets/.env Variable Required Description OPENCLAW_GATEWAY_TOKEN Yes Token for accessing the OpenClaw gateway API. Generate with openssl rand -hex 32. ANTHROPIC_API_KEY Yes* Anthropic API key. Leave empty if using subscription auth (make setup-auth). TELEGRAM_BOT_TOKEN Yes Telegram bot token from @BotFather. Use a dummy value if not using Telegram. TELEGRAM_CHAT_ID No Your Telegram user/chat ID. OPENROUTER_API_KEY No OpenRouter API key for alternative model providers. BRAVE_API_KEY No Brave Search API key for web search skill. GH_TOKEN No GitHub personal access token for GitHub skill. * Required unless using make setup-auth for Claude subscription auth.\nHow secrets reach the VPS Ansible (called by make deploy / make bootstrap) uses one of two paths:\nsecrets/.env exists locally → copied directly to ~/openclaw/.env on the VPS. Simplest for local-only workflows. secrets/.env.enc + secrets/age-key.txt exist → both are pushed to the VPS, SOPS decrypts .env.enc to .env there. Used when the plaintext .env should never leave your machine. If neither exists, Ansible warns and containers start without updated secrets.\nSOPS Encryption (optional for local, required for CI) SOPS encrypts secrets/.env at rest using an age key. The encrypted file (secrets/.env.enc) is safe to commit.\n1 2 3 4 5 6 # One-time setup make secrets-generate-key # generates secrets/age-key.txt + prints public key # Paste the public key into .sops.yaml (shown after generation) make secrets-encrypt # secrets/.env → secrets/.env.enc git add secrets/.env.enc .sops.yaml Day-to-day editing:\n1 2 3 4 5 make secrets-edit # opens $EDITOR with decrypted content, re-encrypts on save # or manually: make secrets-decrypt # → secrets/.env vim secrets/.env make secrets-encrypt # → secrets/.env.enc SOPS encryption is required if you use the GitOps auto-deploy workflow — the CI workflow only handles .env.enc, never a plaintext .env.\nCI Secrets The GitOps auto-deploy workflow needs additional GitHub repository secrets beyond what Terraform CI uses. See GitOps auto-deploy for the full list.\n","permalink":"http://tardigrde.github.io/openclaw-deploy/configuration/secrets/","summary":"\u003ch1 id=\"secrets-reference\"\u003eSecrets Reference\u003c/h1\u003e\n\u003cp\u003eTwo secret files are required. Neither is ever committed to git.\u003c/p\u003e\n\u003ch2 id=\"secretsinputssh--infrastructure-config\"\u003e\u003ccode\u003esecrets/inputs.sh\u003c/code\u003e — Infrastructure config\u003c/h2\u003e\n\u003cp\u003eSourced by the Makefile at parse time (and re-exported for Terraform). Contains deployment-level config, not runtime secrets.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\n\u003ctable style=\"border-spacing:0;padding:0;margin:0;border:0;\"\u003e\u003ctr\u003e\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e1\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e2\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;;width:100%\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecp secrets/inputs.example.sh secrets/inputs.sh\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003evim secrets/inputs.sh\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eVariable\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eRequired\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eDescription\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eHCLOUD_TOKEN\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eHetzner Cloud API token — \u003ca href=\"https://console.hetzner.cloud/\"\u003econsole.hetzner.cloud\u003c/a\u003e → Security → API Tokens\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eSSH_KEY_FINGERPRINT\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eFingerprint of your SSH key uploaded to Hetzner — console → Security → SSH Keys\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eSERVER_IP\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eTailscale only\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eSet to \u003ccode\u003eopenclaw-prod\u003c/code\u003e (MagicDNS) after locking down SSH. Leave unset to auto-detect from Terraform output.\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eTAILSCALE_AUTH_KEY\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eTailscale only\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eReusable pre-authorized key from \u003ca href=\"https://login.tailscale.com/admin/settings/keys\"\u003elogin.tailscale.com/admin/settings/keys\u003c/a\u003e. Must be set before \u003ccode\u003emake bootstrap\u003c/code\u003e.\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eSSH_KEY\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ePath to SSH private key. Defaults to \u003ccode\u003e~/.ssh/id_rsa\u003c/code\u003e.\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eThe Makefile auto-sources \u003ccode\u003einputs.sh\u003c/code\u003e for Terraform vars. For SSH-based targets (\u003ccode\u003emake ssh\u003c/code\u003e, \u003ccode\u003emake deploy\u003c/code\u003e, etc.), \u003ccode\u003eSERVER_IP\u003c/code\u003e must resolve — either auto-detected from Terraform output or explicitly set.\u003c/p\u003e","title":"Secrets Reference"},{"content":"Security Policy Reporting a Vulnerability If you discover a security vulnerability in this project, please report it responsibly:\nDo NOT open a public GitHub issue Use GitHub Security Advisories to report vulnerabilities privately Include: Description of the vulnerability Steps to reproduce Potential impact Suggested fix (if you have one) We aim to respond to security reports within 48 hours and will work with you to understand and address the issue.\nSupported Versions Version Supported main :white_check_mark: \u0026lt; 1.0 :x: Security Considerations Infrastructure Security This project deploys cloud infrastructure. Please be aware of:\nAPI Credentials\nNever commit secrets/inputs.sh or secrets/.env — both are gitignored See docs/configuration/secrets.md for the full secrets model (two-file layout, SOPS encryption, how secrets reach the VPS) Rotate API tokens regularly Use separate tokens for different environments SSH Access\nDefault opens SSH (port 22) to 0.0.0.0/0 — restrict before production Minimum: restrict to your IP via TF_VAR_ssh_allowed_cidrs Best: enable Tailscale (see section 3) and set TF_VAR_ssh_allowed_cidrs='[]' so SSH is invisible to the public internet entirely Hetzner Ubuntu images disable password authentication by default; the VPS uses key-only auth Protect your SSH private key — it is the only credential that grants access Tailscale VPN (Recommended)\nTailscale builds a WireGuard-encrypted mesh network between your devices and the VPS. When enabled, the server gets a private 100.x.x.x address that is only reachable from devices authenticated to your tailnet.\nWhat this protects against:\nSSH scanners and brute-force bots — they see a closed port on the public IP Network-level eavesdropping — all traffic is end-to-end WireGuard encrypted, regardless of the network you\u0026rsquo;re connecting from (hotel WiFi, mobile data, etc.) Accidental gateway exposure — Tailscale Serve makes the OpenClaw dashboard accessible only to authenticated tailnet members, not the public internet What this does not protect against:\nA compromised device that is already enrolled in your tailnet A stolen or leaked Tailscale auth key (mitigate with expiring keys and MFA) Vulnerabilities in OpenClaw or its dependencies Tailscale is installed automatically on first boot when TF_VAR_enable_tailscale=true is set. See Firewall / Network Access in the README for the step-by-step setup.\nFirewall Rules\nInbound by default: SSH (22/tcp) only — no HTTP/HTTPS exposed at the Hetzner level With Tailscale: additionally opens UDP 41641 for WireGuard; SSH port can then be closed entirely Gateway binds to 127.0.0.1 (localhost only) and is never directly reachable from the internet Review terraform/modules/hetzner-vps/main.tf for the full rule set Cloud-Init Scripts\nReview terraform/modules/hetzner-vps/cloud-init/user-data.yml.tpl before deploying Runs with root privileges on first boot Modifying this can affect server security Tailscale installation uses official packages from the Tailscale repository State Files\nTerraform state contains sensitive data — never commit .tfstate files Default backend is local; GCS remote state (with versioning and encryption at rest) is recommended for teams — see docs/remote-state.md Application Security OpenClaw Gateway\nSet strong OPENCLAW_GATEWAY_TOKEN in secrets/.env Use SSH tunneling to access gateway (don\u0026rsquo;t expose publicly) Keep OpenClaw updated to latest version API Keys\nClaude/Anthropic API keys grant access to your account Monitor usage and set spending limits Use setup-token (subscription) instead of API keys when possible Telegram bot tokens should be kept secret Tailscale Security Tailscale Auth Keys Generate auth keys at login.tailscale.com/admin/settings/keys Use reusable, pre-authorized keys for easier deployment Set expiration (90 days recommended) and rotate regularly Never commit auth keys to version control Passed to Ansible via TAILSCALE_AUTH_KEY env var (set in secrets/inputs.sh) Enable MFA on your Tailscale account Cost Security Resource Limits Set up billing alerts in Hetzner Console Start with small server types (cx23) for testing Monitor resource usage regularly Destroy test deployments when done (make destroy) Best Practices For Maintainers Review all PRs for security implications Run automated security scanning (tfsec, checkov) Keep dependencies updated Document security-relevant changes in release notes For Users Read the code before deploying to your infrastructure Start with a test deployment in a separate Hetzner project Use separate API credentials for testing vs production Enable two-factor authentication on your Hetzner account Regularly update to the latest version Known Security Considerations By Design Cloud-init runs with root privileges (standard for server provisioning) SSH access is initially broad (users should narrow this per the README) Gateway uses API tokens for authentication (consider adding TLS) journalctl * sudo rule — the app user (openclaw) can read all systemd journal entries, including auth logs and anything containers write to stderr. This is intentional to support container log streaming from Ansible/Makefile commands. Side effect: API keys accidentally printed to stderr by containers are readable by the app user. Mitigate by ensuring containers do not log secrets. Tailscale installed via curl | sh — bootstrapping runs curl -fsSL https://tailscale.com/install.sh | sh as root without checksum verification. This is the upstream-recommended install path. Risk: a supply chain compromise of tailscale.com at bootstrap time would result in RCE. Mitigate by reviewing the script before running or switching to the APT package method. Out of Scope This project does NOT provide:\nDDoS protection (use Hetzner\u0026rsquo;s DDoS protection or Cloudflare) Automated security patching (you must update OpenClaw manually — see docs/versions.md) The following are already included (not out of scope):\nIntrusion detection — fail2ban is active with maxretry=3, bantime=1h (configured via cloud-init) Secrets encryption at rest — SOPS/age encryption for .env (see docs/secrets.md) Automatic security updates — unattended-upgrades is installed and enabled via cloud-init Security Updates Security updates will be released as patch versions and announced via:\nGitHub Security Advisories Release notes README badges (if applicable) Responsible Disclosure We follow responsible disclosure practices:\n90-day disclosure timeline for vulnerabilities Coordinated public disclosure after fix is available Credit given to reporters (if desired) CVE assignment for significant vulnerabilities (if applicable) Acknowledgments We appreciate the security research community and all contributors who help keep this project secure.\n","permalink":"http://tardigrde.github.io/openclaw-deploy/security/policy/","summary":"\u003ch1 id=\"security-policy\"\u003eSecurity Policy\u003c/h1\u003e\n\u003ch2 id=\"reporting-a-vulnerability\"\u003eReporting a Vulnerability\u003c/h2\u003e\n\u003cp\u003eIf you discover a security vulnerability in this project, please report it responsibly:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eDo NOT\u003c/strong\u003e open a public GitHub issue\u003c/li\u003e\n\u003cli\u003eUse \u003ca href=\"../../security/advisories/new\"\u003eGitHub Security Advisories\u003c/a\u003e to report vulnerabilities privately\u003c/li\u003e\n\u003cli\u003eInclude:\n\u003cul\u003e\n\u003cli\u003eDescription of the vulnerability\u003c/li\u003e\n\u003cli\u003eSteps to reproduce\u003c/li\u003e\n\u003cli\u003ePotential impact\u003c/li\u003e\n\u003cli\u003eSuggested fix (if you have one)\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eWe aim to respond to security reports within 48 hours and will work with you to understand and address the issue.\u003c/p\u003e\n\u003ch2 id=\"supported-versions\"\u003eSupported Versions\u003c/h2\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eVersion\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eSupported\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003emain\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e:white_check_mark:\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u0026lt; 1.0\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e:x:\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"security-considerations\"\u003eSecurity Considerations\u003c/h2\u003e\n\u003ch3 id=\"infrastructure-security\"\u003eInfrastructure Security\u003c/h3\u003e\n\u003cp\u003eThis project deploys cloud infrastructure. Please be aware of:\u003c/p\u003e","title":"Security Policy"},{"content":"Remote State Backend By default Terraform uses a local backend (terraform.tfstate on disk). This requires zero setup and works fine for solo use. For team workflows or CI/CD, a remote backend gives you shared state, versioning, and locking.\nChoosing a Backend terraform/envs/prod/backend.tf (copied from backend.tf.example) controls the backend:\n1 2 3 4 5 6 7 8 9 10 11 12 # Option A — Local (default, no setup required) terraform { backend \u0026#34;local\u0026#34; {} } # Option B — GCS (recommended for teams / CI) terraform { backend \u0026#34;gcs\u0026#34; { bucket = \u0026#34;your-project-tfstate\u0026#34; prefix = \u0026#34;openclaw/prod\u0026#34; } } Terraform supports other backends too (S3, Azure Blob, Terraform Cloud). Any Terraform-compatible backend works — update backend.tf accordingly. Note that S3 requires a separate DynamoDB table for state locking, whereas GCS locks natively.\nGCS Setup GCS is the recommended remote backend: free tier covers Terraform state (5 GB, 5K/50K ops/month), native locking with no extra services, and automatic encryption at rest.\n1. Install the Google Cloud SDK\n2. Create a GCP project with billing:\n1 2 gcloud projects create my-project --set-as-default gcloud billing projects link my-project --billing-account=\u0026lt;ACCOUNT_ID\u0026gt; 3. Enable Cloud Storage and authenticate:\n1 2 gcloud services enable storage.googleapis.com gcloud auth application-default login 4. Create the bucket:\n1 2 gsutil mb -p \u0026lt;project\u0026gt; -l us-east1 -b on gs://\u0026lt;project\u0026gt;-tfstate gsutil versioning set on gs://\u0026lt;project\u0026gt;-tfstate 5. Update backend.tf with your bucket name (Option B above).\nMigrating from Local State If you\u0026rsquo;ve already run make apply with the local backend:\n1 2 cd terraform/envs/prod terraform init -migrate-state Terraform will prompt you to copy local state to the remote backend.\nCI Authentication (GCS + WIF) The included Terraform CI workflows authenticate to GCS via keyless Workload Identity Federation — no service account JSON needed. Set these GitHub repository variables:\nVariable Value GCP_WIF_PROVIDER Full WIF provider resource name (e.g. projects/\u0026lt;n\u0026gt;/locations/global/workloadIdentityPools/github/providers/github) GCP_SERVICE_ACCOUNT Automation SA email (e.g. automation@\u0026lt;project\u0026gt;.iam.gserviceaccount.com) See Configuring OIDC in GCP for WIF setup instructions.\n","permalink":"http://tardigrde.github.io/openclaw-deploy/operations/remote-state/","summary":"\u003ch1 id=\"remote-state-backend\"\u003eRemote State Backend\u003c/h1\u003e\n\u003cp\u003eBy default Terraform uses a \u003cstrong\u003elocal backend\u003c/strong\u003e (\u003ccode\u003eterraform.tfstate\u003c/code\u003e on disk). This requires zero setup and works fine for solo use. For team workflows or CI/CD, a remote backend gives you shared state, versioning, and locking.\u003c/p\u003e\n\u003ch2 id=\"choosing-a-backend\"\u003eChoosing a Backend\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eterraform/envs/prod/backend.tf\u003c/code\u003e (copied from \u003ccode\u003ebackend.tf.example\u003c/code\u003e) controls the backend:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\n\u003ctable style=\"border-spacing:0;padding:0;margin:0;border:0;\"\u003e\u003ctr\u003e\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 1\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 2\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 3\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 4\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 5\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 6\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 7\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 8\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e 9\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e10\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e11\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e12\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;;width:100%\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-hcl\" data-lang=\"hcl\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# Option A — Local (default, no setup required)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eterraform\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#66d9ef\"\u003ebackend\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;local\u0026#34;\u003c/span\u003e {}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\u003cspan style=\"color:#75715e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# Option B — GCS (recommended for teams / CI)\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eterraform\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#66d9ef\"\u003ebackend\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;gcs\u0026#34;\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    bucket \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;your-project-tfstate\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    prefix \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;openclaw/prod\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003eTerraform supports other backends too (S3, Azure Blob, Terraform Cloud). Any Terraform-compatible backend works — update \u003ccode\u003ebackend.tf\u003c/code\u003e accordingly. Note that S3 requires a separate DynamoDB table for state locking, whereas GCS locks natively.\u003c/p\u003e","title":"Remote State"},{"content":"Skills OpenClaw is extended via skills — slash commands, hooks, templates, and tool definitions. docker/skills-manifest.txt lists which skills are auto-installed at container startup via clawhub install.\nAdding a ClawHub Skill ClawHub is the community skill registry.\nFind the skill at clawhub.ai (e.g. pdf, jira)\nAdd it to docker/skills-manifest.txt:\n1 pdf Rebuild and deploy (the manifest is baked into the image):\n1 make deploy REBUILD=1 Included Skills Skill Description yt YouTube transcript fetching and video search agent-browser Headless browser for JS-heavy/paywalled pages system-monitor CPU/RAM/GPU status check conventional-commits Format commit messages per convention github GitHub issues, PRs, CI runs via gh CLI summarize Summarize URLs, PDFs, images, audio, YouTube weather Weather lookup ","permalink":"http://tardigrde.github.io/openclaw-deploy/guides/skills/","summary":"\u003ch1 id=\"skills\"\u003eSkills\u003c/h1\u003e\n\u003cp\u003eOpenClaw is extended via skills — slash commands, hooks, templates, and tool definitions. \u003ccode\u003edocker/skills-manifest.txt\u003c/code\u003e lists which skills are auto-installed at container startup via \u003ccode\u003eclawhub install\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"adding-a-clawhub-skill\"\u003eAdding a ClawHub Skill\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://clawhub.ai/\"\u003eClawHub\u003c/a\u003e is the community skill registry.\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003eFind the skill at \u003ca href=\"https://clawhub.ai/\"\u003eclawhub.ai\u003c/a\u003e (e.g. \u003ccode\u003epdf\u003c/code\u003e, \u003ccode\u003ejira\u003c/code\u003e)\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eAdd it to \u003ccode\u003edocker/skills-manifest.txt\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\n\u003ctable style=\"border-spacing:0;padding:0;margin:0;border:0;\"\u003e\u003ctr\u003e\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;;width:100%\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003epdf\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eRebuild and deploy (the manifest is baked into the image):\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\n\u003ctable style=\"border-spacing:0;padding:0;margin:0;border:0;\"\u003e\u003ctr\u003e\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;;width:100%\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003emake deploy REBUILD\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"included-skills\"\u003eIncluded Skills\u003c/h2\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eSkill\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eDescription\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eyt\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYouTube transcript fetching and video search\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eagent-browser\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eHeadless browser for JS-heavy/paywalled pages\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003esystem-monitor\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eCPU/RAM/GPU status check\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003econventional-commits\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eFormat commit messages per convention\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003egithub\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eGitHub issues, PRs, CI runs via gh CLI\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003esummarize\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eSummarize URLs, PDFs, images, audio, YouTube\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eweather\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eWeather lookup\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e","title":"Skills \u0026 Tools"},{"content":"Version Management All dependencies are pinned in their respective config files. Renovate Bot opens weekly PRs when updates are available (see renovate.json).\nWhere versions live Dependency File Notes OpenClaw docker/Dockerfile (ARG OPENCLAW_VERSION) Never auto-merged — check release notes + run make backup-now first Node.js base image docker/Dockerfile (FROM node:...) Terraform CLI terraform/envs/prod/main.tf (required_version) Also enforced in CI via hashicorp/setup-terraform input hcloud provider terraform/envs/prod/main.tf (version =) ansible-core .github/workflows/*.yml (pip install) Range constraint — bump minor manually GitHub Actions .github/workflows/*.yml (uses:) Auto-merged by Renovate Auto-update policy Renovate runs weekly and opens PRs grouped by ecosystem:\nGitHub Actions — auto-merged (low risk) Everything else — manual review required To activate Renovate on the repo: install the Renovate GitHub App and grant it access to this repository.\n","permalink":"http://tardigrde.github.io/openclaw-deploy/configuration/versions/","summary":"\u003ch1 id=\"version-management\"\u003eVersion Management\u003c/h1\u003e\n\u003cp\u003eAll dependencies are pinned in their respective config files. Renovate Bot opens\nweekly PRs when updates are available (see \u003ccode\u003erenovate.json\u003c/code\u003e).\u003c/p\u003e\n\u003ch2 id=\"where-versions-live\"\u003eWhere versions live\u003c/h2\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eDependency\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eFile\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eNotes\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eOpenClaw\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003edocker/Dockerfile\u003c/code\u003e (\u003ccode\u003eARG OPENCLAW_VERSION\u003c/code\u003e)\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNever auto-merged — check release notes + run \u003ccode\u003emake backup-now\u003c/code\u003e first\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eNode.js base image\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003edocker/Dockerfile\u003c/code\u003e (\u003ccode\u003eFROM node:...\u003c/code\u003e)\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eTerraform CLI\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eterraform/envs/prod/main.tf\u003c/code\u003e (\u003ccode\u003erequired_version\u003c/code\u003e)\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eAlso enforced in CI via \u003ccode\u003ehashicorp/setup-terraform\u003c/code\u003e input\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003ehcloud provider\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eterraform/envs/prod/main.tf\u003c/code\u003e (\u003ccode\u003eversion =\u003c/code\u003e)\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eansible-core\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e.github/workflows/*.yml\u003c/code\u003e (\u003ccode\u003epip install\u003c/code\u003e)\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eRange constraint — bump minor manually\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eGitHub Actions\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e.github/workflows/*.yml\u003c/code\u003e (\u003ccode\u003euses:\u003c/code\u003e)\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eAuto-merged by Renovate\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"auto-update-policy\"\u003eAuto-update policy\u003c/h2\u003e\n\u003cp\u003eRenovate runs weekly and opens PRs grouped by ecosystem:\u003c/p\u003e","title":"Version Management"},{"content":"Headless Browser OpenClaw includes a built-in browser powered by agent-browser running inside the gateway container. Chromium and its dependencies are installed at Docker build time via agent-browser install --with-deps.\nNo sidecar container is needed — the browser runs in-process within the OpenClaw gateway.\nHow It Works The agent-browser npm package manages a local Chromium installation and exposes browser automation capabilities to OpenClaw skills. The gateway configures it automatically.\nVerifying the Installation 1 make exec CMD=\u0026#34;npx agent-browser --version\u0026#34; The browser is ready when the gateway logs show agent-browser initialization complete.\n","permalink":"http://tardigrde.github.io/openclaw-deploy/guides/headless-browser/","summary":"\u003ch1 id=\"headless-browser\"\u003eHeadless Browser\u003c/h1\u003e\n\u003cp\u003eOpenClaw includes a built-in browser powered by \u003ca href=\"https://github.com/nicedoc/agent-browser\"\u003eagent-browser\u003c/a\u003e running inside the gateway container. Chromium and its dependencies are installed at Docker build time via \u003ccode\u003eagent-browser install --with-deps\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003eNo sidecar container is needed — the browser runs in-process within the OpenClaw gateway.\u003c/p\u003e\n\u003ch2 id=\"how-it-works\"\u003eHow It Works\u003c/h2\u003e\n\u003cp\u003eThe \u003ccode\u003eagent-browser\u003c/code\u003e npm package manages a local Chromium installation and exposes browser automation capabilities to OpenClaw skills. The gateway configures it automatically.\u003c/p\u003e\n\u003ch2 id=\"verifying-the-installation\"\u003eVerifying the Installation\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\n\u003ctable style=\"border-spacing:0;padding:0;margin:0;border:0;\"\u003e\u003ctr\u003e\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e1\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;;width:100%\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003emake exec CMD\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;npx agent-browser --version\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003eThe browser is ready when the gateway logs show \u003ccode\u003eagent-browser\u003c/code\u003e initialization complete.\u003c/p\u003e","title":"Headless Browser"},{"content":"Workspace Git Sync Back up ~/.openclaw/workspace to a private GitHub repo automatically. Runs as a native cron job on the VPS, pushing to a configurable branch (default: auto).\nSetup Add to secrets/.env:\n1 2 3 4 GIT_WORKSPACE_REPO=your-username/openclaw-workspace GIT_WORKSPACE_BRANCH=auto GIT_WORKSPACE_TOKEN=ghp_your_personal_access_token GIT_WORKSPACE_SYNC_SCHEDULE=0 4 * * * The cron job auto-enables when GIT_WORKSPACE_REPO is set. Deploy with make deploy.\nOperations 1 2 3 4 5 # Trigger sync immediately make workspace-sync # Disable: remove or clear GIT_WORKSPACE_REPO from .env and redeploy make deploy Logs The cron job writes to ~/openclaw/logs/workspace-sync.log on the VPS:\n1 ssh openclaw@\u0026lt;server\u0026gt; \u0026#39;tail -f ~/openclaw/logs/workspace-sync.log\u0026#39; ","permalink":"http://tardigrde.github.io/openclaw-deploy/guides/workspace-git-sync/","summary":"\u003ch1 id=\"workspace-git-sync\"\u003eWorkspace Git Sync\u003c/h1\u003e\n\u003cp\u003eBack up \u003ccode\u003e~/.openclaw/workspace\u003c/code\u003e to a private GitHub repo automatically. Runs as a native cron job on the VPS, pushing to a configurable branch (default: \u003ccode\u003eauto\u003c/code\u003e).\u003c/p\u003e\n\u003ch2 id=\"setup\"\u003eSetup\u003c/h2\u003e\n\u003cp\u003eAdd to \u003ccode\u003esecrets/.env\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\n\u003ctable style=\"border-spacing:0;padding:0;margin:0;border:0;\"\u003e\u003ctr\u003e\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e1\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e2\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e3\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e4\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;;width:100%\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eGIT_WORKSPACE_REPO=your-username/openclaw-workspace\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eGIT_WORKSPACE_BRANCH=auto\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eGIT_WORKSPACE_TOKEN=ghp_your_personal_access_token\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eGIT_WORKSPACE_SYNC_SCHEDULE=0 4 * * *\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003eThe cron job auto-enables when \u003ccode\u003eGIT_WORKSPACE_REPO\u003c/code\u003e is set. Deploy with \u003ccode\u003emake deploy\u003c/code\u003e.\u003c/p\u003e\n\u003ch2 id=\"operations\"\u003eOperations\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\n\u003ctable style=\"border-spacing:0;padding:0;margin:0;border:0;\"\u003e\u003ctr\u003e\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e1\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e2\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e3\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e4\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f\"\u003e5\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;;width:100%\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# Trigger sync immediately\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003emake workspace-sync\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e# Disable: remove or clear GIT_WORKSPACE_REPO from .env and redeploy\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003emake deploy\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"logs\"\u003eLogs\u003c/h2\u003e\n\u003cp\u003eThe cron job writes to \u003ccode\u003e~/openclaw/logs/workspace-sync.log\u003c/code\u003e on the VPS:\u003c/p\u003e","title":"Workspace Git Sync"},{"content":"Tailscale OpenClaw uses Tailscale for private networking: SSH access from tailnet devices only, and HTTPS access to the gateway via Tailscale Serve.\nHow it works Terraform manages the OAuth-generated pre-auth key, tailnet settings, and MagicDNS. Ansible installs Tailscale on the VPS, registers the node, and deploys the serve config. make bootstrap runs both automatically — no manual Tailscale console steps after initial setup. make deploy re-applies the serve config if it changed (idempotent, skipped before Tailscale is installed). Serve config The gateway is exposed over Tailscale Serve via a JSON config file deployed by Ansible to /etc/tailscale/serve.json.\nTemplate: ansible/templates/tailscale-serve.json.j2\nDefault (single service, port 18789 → HTTPS 443):\n1 2 3 4 5 6 7 8 9 10 11 12 { \u0026#34;TCP\u0026#34;: { \u0026#34;443\u0026#34;: { \u0026#34;HTTPS\u0026#34;: true } }, \u0026#34;Web\u0026#34;: { \u0026#34;${TS_CERT_DOMAIN}:443\u0026#34;: { \u0026#34;Handlers\u0026#34;: { \u0026#34;/\u0026#34;: { \u0026#34;Proxy\u0026#34;: \u0026#34;http://127.0.0.1:18789\u0026#34; } } } } } ${TS_CERT_DOMAIN} is a Tailscale runtime placeholder — it is expanded by Tailscale to your device\u0026rsquo;s FQDN (e.g. openclaw-prod.tailnet-name.ts.net). Do not replace it with a Jinja2 variable.\nExposing additional services To expose extra ports (e.g. a monitoring dashboard on 3001), add TCPForward entries:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 { \u0026#34;TCP\u0026#34;: { \u0026#34;443\u0026#34;: { \u0026#34;HTTPS\u0026#34;: true }, \u0026#34;4000\u0026#34;: { \u0026#34;TCPForward\u0026#34;: \u0026#34;127.0.0.1:4000\u0026#34; }, \u0026#34;3001\u0026#34;: { \u0026#34;TCPForward\u0026#34;: \u0026#34;127.0.0.1:3001\u0026#34; } }, \u0026#34;Web\u0026#34;: { \u0026#34;${TS_CERT_DOMAIN}:443\u0026#34;: { \u0026#34;Handlers\u0026#34;: { \u0026#34;/\u0026#34;: { \u0026#34;Proxy\u0026#34;: \u0026#34;http://127.0.0.1:18789\u0026#34; } } } } } Then run make deploy — Ansible re-applies the config only if the file changed.\nPrivate fork users: If you maintain a private fork, override the template in your fork and protect it from upstream merges with merge=ours in .gitattributes. See your docs/local/CLAUDE.md for details.\nACL management (optional) Set tailscale_enable_acl=true in terraform.tfvars to manage your tailnet ACL via Terraform. Warning: this replaces the entire tailnet ACL on every make apply. Leave it false if you manage ACL rules in the Tailscale console.\nRotating the auth key The pre-auth key expires after 90 days. To rotate:\n1 source secrets/inputs.sh \u0026amp;\u0026amp; make apply Terraform detects the expired key and creates a new one. Re-running make tailscale-setup or make bootstrap with the new key is only needed if the VPS node itself needs to re-register.\nTroubleshooting make tailscale-enable aborts — Tailscale verification failed. SSH is still open. Check make tailscale-status and make logs.\nCan\u0026rsquo;t reach VPS after locking SSH — use the Hetzner web console → server → Console for emergency access.\nServe not responding — check config on VPS: sudo tailscale serve status. Re-apply with make deploy.\n","permalink":"http://tardigrde.github.io/openclaw-deploy/tailscale/","summary":"\u003ch1 id=\"tailscale\"\u003eTailscale\u003c/h1\u003e\n\u003cp\u003eOpenClaw uses Tailscale for private networking: SSH access from tailnet devices only, and HTTPS access to the gateway via Tailscale Serve.\u003c/p\u003e\n\u003ch2 id=\"how-it-works\"\u003eHow it works\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eTerraform\u003c/strong\u003e manages the OAuth-generated pre-auth key, tailnet settings, and MagicDNS.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAnsible\u003c/strong\u003e installs Tailscale on the VPS, registers the node, and deploys the serve config.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003emake bootstrap\u003c/code\u003e\u003c/strong\u003e runs both automatically — no manual Tailscale console steps after initial setup.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003emake deploy\u003c/code\u003e\u003c/strong\u003e re-applies the serve config if it changed (idempotent, skipped before Tailscale is installed).\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"serve-config\"\u003eServe config\u003c/h2\u003e\n\u003cp\u003eThe gateway is exposed over Tailscale Serve via a JSON config file deployed by Ansible to \u003ccode\u003e/etc/tailscale/serve.json\u003c/code\u003e.\u003c/p\u003e","title":"Tailscale"}]