<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://rexgnu.com/journal/</id>
    <title>rexgnu.com — Journal</title>
    <updated>2026-05-03T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <author>
        <name>Gabriel Ledung</name>
    </author>
    <link rel="alternate" href="https://rexgnu.com/journal/"/>
    <link rel="self" href="https://rexgnu.com/journal/feed.xml"/>
    <subtitle>Journal from rexgnu.com</subtitle>
    <rights>© 2026 Gabriel Ledung</rights>
    <entry>
        <title type="html"><![CDATA[Feeds and cards]]></title>
        <id>tag:rexgnu.com,2026:journal/feeds-and-cards</id>
        <link href="https://rexgnu.com/journal/feeds-and-cards/"/>
        <updated>2026-05-03T00:00:00.000Z</updated>
        <content type="html"><![CDATA[<p>A site is more than the page you open in a browser.</p>
<p>It needs a feed. It needs metadata. It needs cards that do not look accidental when a link gets shared. These are small surfaces, but they are part of the publishing system.</p>
<p>The feed is the cleanest one. No chrome, no layout, just entries. The card is the opposite: pure presentation, generated at build time so the site still stays static.</p>
<p>Both make the site feel more real. Not bigger. Just reachable from more angles.</p>
]]></content>
        <author>
            <name>Gabriel Ledung</name>
        </author>
        <category label="journal" term="journal"/>
        <published>2026-05-03T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Visual kit]]></title>
        <id>tag:rexgnu.com,2026:journal/visual-kit</id>
        <link href="https://rexgnu.com/journal/visual-kit/"/>
        <updated>2026-04-29T00:00:00.000Z</updated>
        <content type="html"><![CDATA[<p>The visual language started making sense when it became a kit.</p>
<p>Badge. Counter. Clock. Ticker. Density grid. Globe. Status panel. Danger frame. Small parts with names, not one-off decoration.</p>
<p>That matters because this aesthetic can get sloppy fast. Manga cyberpunk HUD is close to parody if every page invents new chrome. The kit gives the pages a shared vocabulary. It also keeps the content in the middle, where it belongs.</p>
<p>Most of the chrome is decorative. It still needs rules.</p>
<p>Inspiration for this site comes from many places, but most recently when I came across the beutifual art made by <a href="https://www.threads.com/@atmonez">Artem Filippov</a>. It deeply resonated with me and inspired this direction of the site. Thank you Artem.</p>
]]></content>
        <author>
            <name>Gabriel Ledung</name>
        </author>
        <category label="journal" term="journal"/>
        <published>2026-04-29T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Hand-rolled site]]></title>
        <id>tag:rexgnu.com,2026:journal/hand-rolled-site</id>
        <link href="https://rexgnu.com/journal/hand-rolled-site/"/>
        <updated>2026-04-27T00:00:00.000Z</updated>
        <content type="html"><![CDATA[<p>Ah finally something fresh on the table.</p>
<p>The site is hand-rolled because I wanted to understand the whole surface.</p>
<p>Markdown in, templates out. A small content loader. Explicit schemas. A build script that writes files, feeds, sitemap, and cards. Nothing here needed a framework.</p>
<p>That is not a general argument against frameworks. It is a local preference for this project. The site is small enough that the boring parts are still readable, and the weird parts are easier to shape when I own the pipeline.</p>
<p>The tradeoff is obvious: I have to maintain the little system myself. For a personal scratchpad, that feels like the point.</p>
]]></content>
        <author>
            <name>Gabriel Ledung</name>
        </author>
        <category label="journal" term="journal"/>
        <published>2026-04-27T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Inspectable sync]]></title>
        <id>tag:rexgnu.com,2026:journal/inspectable-sync</id>
        <link href="https://rexgnu.com/journal/inspectable-sync/"/>
        <updated>2026-04-21T00:00:00.000Z</updated>
        <content type="html"><![CDATA[<p>Sync should be boring before it gets clever.</p>
<p>Especially for notes. Markdown files are easy to understand until the sync layer turns them into a magic trick. I want to know which folder is canonical, what moves where, how conflicts are handled, and how to recover when one machine goes stale.</p>
<p>That sounds like plumbing because it is plumbing.</p>
<p>The temptation is to design the future architecture first. The better note says what is actually running. What sync path exists. What is backed up. What happens when it fails.</p>
]]></content>
        <author>
            <name>Gabriel Ledung</name>
        </author>
        <category label="journal" term="journal"/>
        <published>2026-04-21T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Model routing for notes]]></title>
        <id>tag:rexgnu.com,2026:journal/model-routing-for-notes</id>
        <link href="https://rexgnu.com/journal/model-routing-for-notes/"/>
        <updated>2026-04-16T00:00:00.000Z</updated>
        <content type="html"><![CDATA[<p>Not every note task deserves the best model.</p>
<p>Routine maintenance should be cheap: ingest a small batch, update existing pages, add links, keep indexes current. The model needs to be consistent, not brilliant.</p>
<p>The stronger model should be saved for the work that needs judgment: restructuring a messy topic, resolving ambiguous merges, writing a synthesis, reviewing whether the system is still useful.</p>
<p>Spending intelligence evenly is lazy. The workflow should route work by risk and ambiguity.</p>
]]></content>
        <author>
            <name>Gabriel Ledung</name>
        </author>
        <category label="journal" term="journal"/>
        <published>2026-04-16T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Second brain stack]]></title>
        <id>tag:rexgnu.com,2026:journal/second-brain-stack</id>
        <link href="https://rexgnu.com/journal/second-brain-stack/"/>
        <updated>2026-04-16T00:00:00.000Z</updated>
        <content type="html"><![CDATA[<p>The useful second brain is not a life log.</p>
<p>For me, the shape is closer to three layers: raw inputs, maintained wiki pages, and generated outputs. Raw notes stay messy. The wiki layer gets maintained. Outputs are briefings, summaries, drafts, or decisions that can be thrown away or promoted back into the wiki.</p>
<p>The mistake is asking AI to search the raw pile forever. That turns every question into archaeology. The better pattern is persistent synthesis: keep a maintained layer so knowledge can compound.</p>
<p>The raw material still matters. I just do not want to reread it every time I ask a question.</p>
]]></content>
        <author>
            <name>Gabriel Ledung</name>
        </author>
        <category label="journal" term="journal"/>
        <published>2026-04-16T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Agent hardening]]></title>
        <id>tag:rexgnu.com,2026:journal/agent-hardening</id>
        <link href="https://rexgnu.com/journal/agent-hardening/"/>
        <updated>2026-03-08T00:00:00.000Z</updated>
        <content type="html"><![CDATA[<p>A useful agent should close loops.</p>
<p>Noticed, acted, verified, reported. Escalated only when blocked.</p>
<p>That is the difference between an agent and a chatbot wearing a tool belt. The weak version notices something and talks about it. The useful version does the next safe thing, checks whether it worked, and reports only when the message carries signal.</p>
<p>The hard part is not sounding proactive. The hard part is being precise about state. What was observed directly? What was reported by another system? What was inferred? What is the next concrete action?</p>
<p>False confidence is worse than silence. If an agent says it is watching, there should be a real watcher. If it says something changed, it should know how it verified that change.</p>
]]></content>
        <author>
            <name>Gabriel Ledung</name>
        </author>
        <category label="journal" term="journal"/>
        <published>2026-03-08T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Agent identity should belong to the domain]]></title>
        <id>tag:rexgnu.com,2026:journal/agent-identity</id>
        <link href="https://rexgnu.com/journal/agent-identity/"/>
        <updated>2026-03-06T00:00:00.000Z</updated>
        <content type="html"><![CDATA[<p>I do not want agent infrastructure tied too tightly to one inbox vendor.</p>
<p>The useful split is simple: identity belongs to the domain, mail hosting is replaceable. The public address should survive provider changes. Forwarding, aliases, and mailbox backends are implementation details.</p>
<p>This matters more for agents than for normal email because the address becomes part of the system boundary. It receives alerts, forwards tasks, signs up for services, and becomes a durable contact point for automation.</p>
<p>If the identity is portable, the rest can stay boring.</p>
]]></content>
        <author>
            <name>Gabriel Ledung</name>
        </author>
        <category label="journal" term="journal"/>
        <published>2026-03-06T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Where agent patterns differ]]></title>
        <id>tag:rexgnu.com,2026:journal/agent-orchestration-patterns</id>
        <link href="https://rexgnu.com/journal/agent-orchestration-patterns/"/>
        <updated>2026-02-18T00:00:00.000Z</updated>
        <content type="html"><![CDATA[<p>Most agent frameworks look similar in the happy path.</p>
<p>There is an input, a plan, a tool call, a result, maybe a memory layer. The names change. The diagrams get more complicated. The core loop is usually the same.</p>
<p>The real differences show up when something breaks. Can the agent recover from a bad tool result? Can it preserve state across a long task? Can a human interrupt without corrupting the workflow? Can the system explain what happened after the fact?</p>
<p>That is where the architecture becomes visible. Not in the demo. In the retry, the handoff, the checkpoint, the review boundary.</p>
]]></content>
        <author>
            <name>Gabriel Ledung</name>
        </author>
        <category label="journal" term="journal"/>
        <published>2026-02-18T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[The review bottleneck is here]]></title>
        <id>tag:rexgnu.com,2026:journal/review-bottleneck</id>
        <link href="https://rexgnu.com/journal/review-bottleneck/"/>
        <updated>2026-02-13T00:00:00.000Z</updated>
        <content type="html"><![CDATA[<p>AI did not remove review. It moved more work there.</p>
<p>That is the part I keep coming back to. The demos are all about generation: write the component, build the app, fix the bug, scaffold the service. The quieter cost shows up after the code exists. Someone still has to decide whether the code should exist, whether the shape is right, and whether the extra surface area is worth owning.</p>
<p>The frontier teams are already living in that world. OpenAI said GPT-5.3-Codex was “instrumental in creating itself.” Reports around Claude Code and Cowork tell the same story from the Anthropic side: small human teams describe the work, steer the agent, and review the output. That sounds like a productivity win until you follow the burden all the way to the reviewer.</p>
<p>The old bottleneck was getting enough code written. The new bottleneck is proving that a larger amount of code is necessary, understandable, and covered by the right tests.</p>
<p>I think the review question is changing. It used to be “does this work?” That question still matters, but it is no longer enough. The more useful question is “should this much code exist?”</p>
<p>LogRocket had an example that stuck with me: a 29-line implementation turned into 186 lines from Claude Code. Same feature. More branches, more defensive handling, more surface area. That is not automatically bad. Sometimes the generated version is catching real edge cases. But someone has to read every line and decide which parts are care and which parts are clutter.</p>
<p>The OCaml maintainers ran into the harsher version of this. They rejected a 13,000-line AI-generated pull request. The issue was not simply whether the code compiled. It was review bandwidth, ownership, copyright risk, and whether future maintainers could explain the code at 2 a.m. when it broke.</p>
<p>That last part matters more than the benchmark charts. Code that works today but is not understood by anyone on the team is not free. It is borrowed confidence.</p>
<p>Cursor buying Graphite made more sense to me after that. Code generation is crowded. Review is where the pressure lands. Agent Trace is interesting for the same reason. Knowing which model produced a line, which conversation led to it, and which commit carried it gives teams a better audit trail. It does not prove the code is correct, but it gives reviewers a starting point.</p>
<p>Payments makes this less abstract. A sloppy UI branch is annoying. A sloppy payment path is a support queue, a reconciliation problem, a chargeback trail, or an audit conversation. “The AI wrote it and I glanced at the diff” is not a serious answer when money moved incorrectly.</p>
<p>The security numbers are ugly enough that I do not want to overstate them. The exact rates will move around as models and tooling change. The direction is still hard to ignore: AI-generated code is producing more review findings, more logic mistakes, and more security issues than teams expected. If generated code touches auth, money movement, customer data, or accounting state, the burden of proof needs to go up, not down.</p>
<p>This is the uncomfortable part: most developers do not love review. We understand why it exists, but given a choice between building something and auditing someone else’s patch, most of us would rather build. AI makes that tradeoff sharper. Now the patch may be larger, more polished, and less connected to a human author’s intent.</p>
<p>So the engineering job shifts. Less typing, more specification. Less authorship, more verification. The valuable skill is not producing lines quickly. The valuable skill is knowing what correct means before the lines exist, then building enough evidence that the team can trust the result.</p>
<p>That is why tests keep coming back into the center of this for me. Tests are not magic, and a green suite can still miss the important bug. But without tests, AI-assisted development turns into review by vibes. The code compiles. The assistant sounds confident. Everyone is tired. The diff goes in.</p>
<p>The path forward is probably less glamorous than the demos: smaller PRs, clearer ownership, traceable AI use, tests written against the requirement, property checks where invariants matter, and reviewers who are allowed to reject code they cannot explain.</p>
<p>I do not think this makes AI coding bad. I think it makes review discipline more important than generation speed. If the team cannot review the work, the speed is fake.</p>
<p>Sources I kept around while editing:</p>
<ul>
<li><a href="https://openai.com/index/introducing-gpt-5-3-codex">OpenAI on GPT-5.3-Codex</a></li>
<li><a href="https://www.axios.com/2026/01/13/anthropic-claude-code-cowork-vibe-coding">Axios on Anthropic Cowork</a></li>
<li><a href="https://blog.logrocket.com/ai-coding-tools-shift-bottleneck-to-review/">LogRocket on AI moving the bottleneck to review</a></li>
<li><a href="https://devclass.com/2025/11/27/ocaml-maintainers-reject-massive-ai-generated-pull-request/">DevClass on the OCaml AI-generated pull request</a></li>
<li><a href="https://github.com/cursor/agent-trace">Cursor’s Agent Trace spec</a></li>
<li><a href="https://cursor.com/blog/graphite">Cursor on Graphite joining Cursor</a></li>
<li><a href="https://archive.ph/2026.01.08-113354/https%3A/addyosmani.com/blog/code-review-ai/">Addy Osmani on proving AI-written code works</a></li>
<li><a href="https://www.itpro.com/software/development/ai-generated-code-is-now-the-cause-of-one-in-five-breaches-but-developers-and-security-leaders-alike-are-convinced-the-technology-will-come-good-eventually">ITPro on AI-generated code security findings</a></li>
<li><a href="https://www.cortex.io/post/engineering-in-the-age-of-ai-2026-benchmark-webinar-recap">Cortex on engineering in the age of AI</a></li>
</ul>
]]></content>
        <author>
            <name>Gabriel Ledung</name>
        </author>
        <category label="journal" term="journal"/>
        <published>2026-02-13T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Tests as the quality gate]]></title>
        <id>tag:rexgnu.com,2026:journal/tests-as-quality-gate</id>
        <link href="https://rexgnu.com/journal/tests-as-quality-gate/"/>
        <updated>2026-02-13T00:00:00.000Z</updated>
        <content type="html"><![CDATA[<p>AI code makes tests less optional.</p>
<p>That sounds obvious, but the pressure changes when code volume goes up. A human reviewer can skim a small patch and build a mental model. With generated code, that model gets expensive quickly. There is more surface area, more defensive branches, more plausible-looking glue.</p>
<p>Tests become the review surface. They are not proof that the design is good, but they force the generated work to state what it thinks should be true.</p>
<p>Without tests, AI-assisted development drifts toward vibes. The code compiles, the assistant sounds confident, and everyone is tired enough to accept the diff.</p>
]]></content>
        <author>
            <name>Gabriel Ledung</name>
        </author>
        <category label="journal" term="journal"/>
        <published>2026-02-13T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Deploying multiple OpenClaw agents on a single VPS]]></title>
        <id>tag:rexgnu.com,2026:journal/openclaw-agents-single-vps</id>
        <link href="https://rexgnu.com/journal/openclaw-agents-single-vps/"/>
        <updated>2026-02-10T00:00:00.000Z</updated>
        <content type="html"><![CDATA[<p>A practical guide to running multiple OpenClaw AI gateway instances on one server. Each agent gets its own identity, tools, memory system, and access controls. This was built for ARM64 VPS hosts, secured with Tailscale, and designed to scale from two agents to as many as the hardware allows.</p>
<h2>Why this setup?</h2>
<p>Running your own AI gateway gives you control over your agents: their tools, memory, model providers, and who can access them. This guide sets up shared infrastructure where each agent is managed independently but uses a single Docker image and provisioning system.</p>
<p>Use cases:</p>
<ul>
<li>Personal agent and partner/family agent on the same server</li>
<li>Team of specialized agents under one roof: research, ops, comms</li>
<li>Development and production agents side by side</li>
</ul>
<h2>Architecture</h2>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>/opt/openclaw/</span></span>
<span class="line"><span>├── repo/                             # Cloned openclaw source</span></span>
<span class="line"><span>├── Dockerfile                        # Custom image with extra CLI tools</span></span>
<span class="line"><span>├── templates/</span></span>
<span class="line"><span>│   ├── docker-compose.template.yml   # Per-agent compose template</span></span>
<span class="line"><span>│   └── env.template                  # Per-agent .env template</span></span>
<span class="line"><span>├── scripts/</span></span>
<span class="line"><span>│   ├── provision-agent.sh            # One-command agent provisioning</span></span>
<span class="line"><span>│   └── status.sh                     # Multi-agent status dashboard</span></span>
<span class="line"><span>├── total-recall/                     # Shared Total Recall source</span></span>
<span class="line"><span>├── docker-compose.&#x3C;agent>.yml        # Generated per agent</span></span>
<span class="line"><span>├── .env.&#x3C;agent>                      # Secrets per agent (mode 0600)</span></span>
<span class="line"><span>└── ports.conf                        # Port registry</span></span>
<span class="line"><span></span></span>
<span class="line"><span>/home/&#x3C;agent>/.openclaw/              # Per-agent persistent data</span></span>
<span class="line"><span>├── openclaw.json                     # Agent config</span></span>
<span class="line"><span>├── workspace/                        # Working directory</span></span>
<span class="line"><span>│   ├── memory/                       # Total Recall observations</span></span>
<span class="line"><span>│   └── logs/                         # Observer/reflector logs</span></span>
<span class="line"><span>└── skills/</span></span>
<span class="line"><span>    └── total-recall/                 # Memory system installation</span></span>
<span class="line"><span></span></span></code></pre>
<p>Each agent runs in its own Docker container with:</p>
<ul>
<li>Dedicated Linux user and home directory</li>
<li>Unique port bound to <code>127.0.0.1</code></li>
<li>Independent systemd unit for auto-start</li>
<li>Its own gateway token and secrets</li>
<li>Total Recall memory system with autonomous observation</li>
</ul>
<p>Access is exposed via Tailscale Serve, with HTTPS inside the tailnet, and locked down with Tailscale ACLs per user.</p>
<h2>Prerequisites</h2>
<ul>
<li>A VPS or dedicated server, ARM64 or x86_64</li>
<li>Ubuntu 22.04+ or Debian 12+</li>
<li>Docker Engine installed</li>
<li>Tailscale installed and authenticated</li>
<li>At least 2 GB RAM per agent, plus OS overhead</li>
</ul>
<h3>Tested on</h3>
<ul>
<li>Hetzner CAX11: 4 ARM64 cores, 8 GB RAM, 80 GB disk, Ubuntu 24.04</li>
<li>Comfortable for 2 agents, feasible for 3</li>
</ul>
<h2>Step 1: System preparation</h2>
<h3>Add your user to the Docker group</h3>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> usermod</span><span style="color:#79B8FF"> -aG</span><span style="color:#9ECBFF"> docker</span><span style="color:#E1E4E8"> $USER</span></span>
<span class="line"><span style="color:#B392F0">newgrp</span><span style="color:#9ECBFF"> docker</span><span style="color:#6A737D">  # or re-login</span></span>
<span class="line"></span></code></pre>
<h3>Create swap</h3>
<p>This is recommended for a small VPS.</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> fallocate</span><span style="color:#79B8FF"> -l</span><span style="color:#9ECBFF"> 4G</span><span style="color:#9ECBFF"> /swapfile</span></span>
<span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> chmod</span><span style="color:#79B8FF"> 600</span><span style="color:#9ECBFF"> /swapfile</span></span>
<span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> mkswap</span><span style="color:#9ECBFF"> /swapfile</span></span>
<span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> swapon</span><span style="color:#9ECBFF"> /swapfile</span></span>
<span class="line"><span style="color:#79B8FF">echo</span><span style="color:#9ECBFF"> '/swapfile none swap sw 0 0'</span><span style="color:#F97583"> |</span><span style="color:#B392F0"> sudo</span><span style="color:#9ECBFF"> tee</span><span style="color:#79B8FF"> -a</span><span style="color:#9ECBFF"> /etc/fstab</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D"># Low swappiness: prefer RAM, use swap as safety net</span></span>
<span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> sysctl</span><span style="color:#9ECBFF"> vm.swappiness=</span><span style="color:#79B8FF">10</span></span>
<span class="line"><span style="color:#79B8FF">echo</span><span style="color:#9ECBFF"> 'vm.swappiness=10'</span><span style="color:#F97583"> |</span><span style="color:#B392F0"> sudo</span><span style="color:#9ECBFF"> tee</span><span style="color:#9ECBFF"> /etc/sysctl.d/99-openclaw.conf</span></span>
<span class="line"></span></code></pre>
<h3>Configure Docker log rotation</h3>
<p>Prevent logs from eating the disk:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> tee</span><span style="color:#9ECBFF"> /etc/docker/daemon.json</span><span style="color:#F97583"> &#x3C;&#x3C;</span><span style="color:#9ECBFF"> 'EOF'</span></span>
<span class="line"><span style="color:#9ECBFF">{</span></span>
<span class="line"><span style="color:#9ECBFF">  "log-driver": "json-file",</span></span>
<span class="line"><span style="color:#9ECBFF">  "log-opts": {</span></span>
<span class="line"><span style="color:#9ECBFF">    "max-size": "10m",</span></span>
<span class="line"><span style="color:#9ECBFF">    "max-file": "3"</span></span>
<span class="line"><span style="color:#9ECBFF">  }</span></span>
<span class="line"><span style="color:#9ECBFF">}</span></span>
<span class="line"><span style="color:#9ECBFF">EOF</span></span>
<span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> systemctl</span><span style="color:#9ECBFF"> restart</span><span style="color:#9ECBFF"> docker</span></span>
<span class="line"></span></code></pre>
<h2>Step 2: Directory structure</h2>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> groupadd</span><span style="color:#9ECBFF"> openclaw</span></span>
<span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> usermod</span><span style="color:#79B8FF"> -aG</span><span style="color:#9ECBFF"> openclaw</span><span style="color:#E1E4E8"> $USER</span></span>
<span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> mkdir</span><span style="color:#79B8FF"> -p</span><span style="color:#9ECBFF"> /opt/openclaw/{repo,templates,scripts}</span></span>
<span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> chown</span><span style="color:#79B8FF"> -R</span><span style="color:#E1E4E8"> $USER</span><span style="color:#9ECBFF">:openclaw</span><span style="color:#9ECBFF"> /opt/openclaw</span></span>
<span class="line"><span style="color:#B392F0">chmod</span><span style="color:#79B8FF"> 750</span><span style="color:#9ECBFF"> /opt/openclaw</span></span>
<span class="line"></span></code></pre>
<h2>Step 3: Build the Docker image</h2>
<h3>Clone OpenClaw</h3>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> clone</span><span style="color:#9ECBFF"> https://github.com/openclaw/openclaw.git</span><span style="color:#9ECBFF"> /opt/openclaw/repo</span></span>
<span class="line"></span></code></pre>
<h3>Create a custom Dockerfile</h3>
<p>This extends the official image with additional CLI tools. Adjust the tool list to your needs. The example includes Gmail, WhatsApp, and Google Places CLIs.</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span># /opt/openclaw/Dockerfile</span></span>
<span class="line"><span></span></span>
<span class="line"><span># Stage 1: Build any tools that lack prebuilt binaries for your arch</span></span>
<span class="line"><span>FROM golang:1.25-bookworm AS wacli-builder</span></span>
<span class="line"><span>RUN apt-get update &#x26;&#x26; apt-get install -y --no-install-recommends gcc libc6-dev git ca-certificates \</span></span>
<span class="line"><span>    &#x26;&#x26; rm -rf /var/lib/apt/lists/*</span></span>
<span class="line"><span>WORKDIR /build</span></span>
<span class="line"><span>RUN git clone --depth 1 https://github.com/steipete/wacli.git .</span></span>
<span class="line"><span>RUN CGO_ENABLED=1 go build -tags sqlite_fts5 -o /usr/local/bin/wacli ./cmd/wacli</span></span>
<span class="line"><span></span></span>
<span class="line"><span># Stage 2: Extend the base image</span></span>
<span class="line"><span>FROM openclaw:base</span></span>
<span class="line"><span></span></span>
<span class="line"><span>USER root</span></span>
<span class="line"><span></span></span>
<span class="line"><span># System dependencies for Total Recall (inotify, cron) and networking (socat)</span></span>
<span class="line"><span>RUN apt-get update &#x26;&#x26; DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \</span></span>
<span class="line"><span>    socat \</span></span>
<span class="line"><span>    inotify-tools \</span></span>
<span class="line"><span>    cron \</span></span>
<span class="line"><span>    &#x26;&#x26; apt-get clean \</span></span>
<span class="line"><span>    &#x26;&#x26; rm -rf /var/lib/apt/lists/*</span></span>
<span class="line"><span></span></span>
<span class="line"><span># Install prebuilt CLI tools (adjust URLs for your architecture)</span></span>
<span class="line"><span>RUN curl -fsSL https://github.com/steipete/gogcli/releases/download/v0.11.0/gogcli_0.11.0_linux_arm64.tar.gz \</span></span>
<span class="line"><span>    | tar -xz -C /usr/local/bin gog \</span></span>
<span class="line"><span>    &#x26;&#x26; chmod +x /usr/local/bin/gog</span></span>
<span class="line"><span></span></span>
<span class="line"><span>RUN curl -fsSL https://github.com/steipete/goplaces/releases/download/v0.3.0/goplaces_0.3.0_linux_arm64.tar.gz \</span></span>
<span class="line"><span>    | tar -xz -C /usr/local/bin goplaces \</span></span>
<span class="line"><span>    &#x26;&#x26; chmod +x /usr/local/bin/goplaces</span></span>
<span class="line"><span></span></span>
<span class="line"><span># Install tools built from source</span></span>
<span class="line"><span>COPY --from=wacli-builder /usr/local/bin/wacli /usr/local/bin/wacli</span></span>
<span class="line"><span>RUN chmod +x /usr/local/bin/wacli</span></span>
<span class="line"><span></span></span>
<span class="line"><span>USER node</span></span>
<span class="line"><span></span></span></code></pre>
<p>For x86_64, replace <code>arm64</code> with <code>amd64</code> in the download URLs.</p>
<h3>Build</h3>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D"># Build base image from repo</span></span>
<span class="line"><span style="color:#B392F0">docker</span><span style="color:#9ECBFF"> build</span><span style="color:#79B8FF"> -t</span><span style="color:#9ECBFF"> openclaw:base</span><span style="color:#79B8FF"> -f</span><span style="color:#9ECBFF"> /opt/openclaw/repo/Dockerfile</span><span style="color:#9ECBFF"> /opt/openclaw/repo</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D"># Build custom image with tools</span></span>
<span class="line"><span style="color:#B392F0">docker</span><span style="color:#9ECBFF"> build</span><span style="color:#79B8FF"> -t</span><span style="color:#9ECBFF"> openclaw:latest</span><span style="color:#79B8FF"> -f</span><span style="color:#9ECBFF"> /opt/openclaw/Dockerfile</span><span style="color:#9ECBFF"> /opt/openclaw</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D"># Verify</span></span>
<span class="line"><span style="color:#B392F0">docker</span><span style="color:#9ECBFF"> run</span><span style="color:#79B8FF"> --rm</span><span style="color:#9ECBFF"> openclaw:latest</span><span style="color:#9ECBFF"> sh</span><span style="color:#79B8FF"> -c</span><span style="color:#9ECBFF"> "which gog wacli goplaces"</span></span>
<span class="line"></span></code></pre>
<h2>Step 4: Create templates</h2>
<h3>Docker Compose template</h3>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D"># /opt/openclaw/templates/docker-compose.template.yml</span></span>
<span class="line"></span>
<span class="line"><span style="color:#85E89D">services</span><span style="color:#E1E4E8">:</span></span>
<span class="line"><span style="color:#85E89D">  openclaw-__AGENT_NAME__</span><span style="color:#E1E4E8">:</span></span>
<span class="line"><span style="color:#85E89D">    image</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">openclaw:latest</span></span>
<span class="line"><span style="color:#85E89D">    container_name</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">openclaw-__AGENT_NAME__</span></span>
<span class="line"><span style="color:#85E89D">    env_file</span><span style="color:#E1E4E8">:</span></span>
<span class="line"><span style="color:#E1E4E8">      - </span><span style="color:#9ECBFF">/opt/openclaw/.env.__AGENT_NAME__</span></span>
<span class="line"><span style="color:#85E89D">    environment</span><span style="color:#E1E4E8">:</span></span>
<span class="line"><span style="color:#85E89D">      HOME</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">/home/node</span></span>
<span class="line"><span style="color:#85E89D">      TERM</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">xterm-256color</span></span>
<span class="line"><span style="color:#85E89D">    volumes</span><span style="color:#E1E4E8">:</span></span>
<span class="line"><span style="color:#E1E4E8">      - </span><span style="color:#9ECBFF">/home/__AGENT_NAME__/.openclaw:/home/node/.openclaw</span></span>
<span class="line"><span style="color:#E1E4E8">      - </span><span style="color:#9ECBFF">/home/__AGENT_NAME__/.openclaw/workspace:/home/node/.openclaw/workspace</span></span>
<span class="line"><span style="color:#85E89D">    ports</span><span style="color:#E1E4E8">:</span></span>
<span class="line"><span style="color:#E1E4E8">      - </span><span style="color:#9ECBFF">"127.0.0.1:__AGENT_PORT__:18789"</span></span>
<span class="line"><span style="color:#85E89D">    init</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">true</span></span>
<span class="line"><span style="color:#85E89D">    restart</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">unless-stopped</span></span>
<span class="line"><span style="color:#85E89D">    deploy</span><span style="color:#E1E4E8">:</span></span>
<span class="line"><span style="color:#85E89D">      resources</span><span style="color:#E1E4E8">:</span></span>
<span class="line"><span style="color:#85E89D">        limits</span><span style="color:#E1E4E8">:</span></span>
<span class="line"><span style="color:#85E89D">          memory</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">2560M</span></span>
<span class="line"><span style="color:#85E89D">          cpus</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"2"</span></span>
<span class="line"><span style="color:#85E89D">    healthcheck</span><span style="color:#E1E4E8">:</span></span>
<span class="line"><span style="color:#85E89D">      test</span><span style="color:#E1E4E8">: [</span><span style="color:#9ECBFF">"CMD"</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">"curl"</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">"-fsS"</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">"http://127.0.0.1:18789/"</span><span style="color:#E1E4E8">]</span></span>
<span class="line"><span style="color:#85E89D">      interval</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">30s</span></span>
<span class="line"><span style="color:#85E89D">      timeout</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">10s</span></span>
<span class="line"><span style="color:#85E89D">      retries</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">3</span></span>
<span class="line"><span style="color:#85E89D">      start_period</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">30s</span></span>
<span class="line"><span style="color:#85E89D">    logging</span><span style="color:#E1E4E8">:</span></span>
<span class="line"><span style="color:#85E89D">      driver</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">json-file</span></span>
<span class="line"><span style="color:#85E89D">      options</span><span style="color:#E1E4E8">:</span></span>
<span class="line"><span style="color:#85E89D">        max-size</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"10m"</span></span>
<span class="line"><span style="color:#85E89D">        max-file</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"3"</span></span>
<span class="line"><span style="color:#85E89D">    command</span><span style="color:#E1E4E8">: [</span><span style="color:#9ECBFF">"node"</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">"openclaw.mjs"</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">"gateway"</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">"--allow-unconfigured"</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">"--bind"</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">"lan"</span><span style="color:#E1E4E8">]</span></span>
<span class="line"></span></code></pre>
<p>Key design choices:</p>
<ul>
<li><code>127.0.0.1</code> binding: ports are never exposed to the public internet</li>
<li><code>init: true</code>: proper PID 1 for signal handling and zombie reaping</li>
<li>Resource limits: prevents one agent from starving the other</li>
<li>Health checks: Docker and systemd know when an agent is actually working</li>
</ul>
<h3>Environment template</h3>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D"># /opt/openclaw/templates/env.template</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D"># OpenClaw Gateway: __AGENT_NAME__</span></span>
<span class="line"><span style="color:#E1E4E8">OPENCLAW_GATEWAY_TOKEN</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">__GATEWAY_TOKEN__</span></span>
<span class="line"><span style="color:#E1E4E8">OPENAI_API_KEY</span><span style="color:#F97583">=</span></span>
<span class="line"><span style="color:#E1E4E8">GOG_KEYRING_PASSWORD</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">__GOG_KEYRING_PASSWORD__</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D"># Total Recall (memory system LLM: cheap model via OpenRouter)</span></span>
<span class="line"><span style="color:#E1E4E8">LLM_BASE_URL</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">https://openrouter.ai/api/v1</span></span>
<span class="line"><span style="color:#E1E4E8">LLM_API_KEY</span><span style="color:#F97583">=</span></span>
<span class="line"><span style="color:#E1E4E8">LLM_MODEL</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">google/gemini-2.5-flash</span></span>
<span class="line"></span></code></pre>
<h2>Step 5: Install Total Recall</h2>
<p><a href="https://github.com/gavdalf/total-recall">Total Recall</a> gives each agent autonomous memory. It watches conversations, compresses them into observations, and consolidates nightly. With cheap models through OpenRouter, it costs about $0.10/month per agent.</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> clone</span><span style="color:#9ECBFF"> https://github.com/gavdalf/total-recall.git</span><span style="color:#9ECBFF"> /opt/openclaw/total-recall</span></span>
<span class="line"></span></code></pre>
<p>The provisioning script handles per-agent installation automatically.</p>
<h2>Step 6: The provisioning script</h2>
<p>This is the core automation. Running <code>provision-agent.sh &lt;name&gt;</code> creates everything an agent needs.</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D"># /opt/openclaw/scripts/provision-agent.sh</span></span>
<span class="line"></span></code></pre>
<p>What it does:</p>
<ol>
<li>Allocates the next available port from <code>ports.conf</code></li>
<li>Creates a dedicated Linux user with a nologin shell and <code>openclaw</code> group</li>
<li>Creates the agent’s data directories with correct ownership, using uid 1000 for the container’s <code>node</code> user</li>
<li>Generates a unique gateway token and keyring password</li>
<li>Renders the compose file and <code>.env</code> from templates</li>
<li>Installs Total Recall into the agent’s workspace</li>
<li>Sets up host-level cron for memory observation every 15 minutes, reflection hourly, and dream cycles at 3am nightly</li>
<li>Creates and enables a systemd unit for auto-start on boot</li>
</ol>
<p>The full script is available in the repository. This is the key pattern for the systemd unit it generates:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>[Unit]</span></span>
<span class="line"><span>Description=OpenClaw Gateway - &#x3C;agent_name></span></span>
<span class="line"><span>After=docker.service</span></span>
<span class="line"><span>Requires=docker.service</span></span>
<span class="line"><span></span></span>
<span class="line"><span>[Service]</span></span>
<span class="line"><span>Type=oneshot</span></span>
<span class="line"><span>RemainAfterExit=yes</span></span>
<span class="line"><span>WorkingDirectory=/opt/openclaw</span></span>
<span class="line"><span>ExecStart=/usr/bin/docker compose -f &#x3C;compose_file> up -d</span></span>
<span class="line"><span>ExecStop=/usr/bin/docker compose -f &#x3C;compose_file> down</span></span>
<span class="line"><span>ExecReload=/usr/bin/docker compose -f &#x3C;compose_file> restart</span></span>
<span class="line"><span>TimeoutStartSec=120</span></span>
<span class="line"><span></span></span>
<span class="line"><span>[Install]</span></span>
<span class="line"><span>WantedBy=multi-user.target</span></span>
<span class="line"><span></span></span></code></pre>
<p>And the Total Recall cron entries it installs:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span># Observer: compress recent conversations every 15 minutes</span></span>
<span class="line"><span>*/15 * * * * docker exec openclaw-&#x3C;agent> bash -c 'OPENCLAW_WORKSPACE=... bash .../observer-agent.sh'</span></span>
<span class="line"><span></span></span>
<span class="line"><span># Reflector: consolidate when observations grow large</span></span>
<span class="line"><span>0 * * * * docker exec openclaw-&#x3C;agent> bash -c 'OPENCLAW_WORKSPACE=... bash .../reflector-agent.sh'</span></span>
<span class="line"><span></span></span>
<span class="line"><span># Dream Cycle: nightly memory consolidation</span></span>
<span class="line"><span>0 3 * * * docker exec openclaw-&#x3C;agent> bash -c 'OPENCLAW_WORKSPACE=... bash .../dream-cycle.sh preflight'</span></span>
<span class="line"><span></span></span></code></pre>
<h2>Step 7: Provision your agents</h2>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> /opt/openclaw/scripts/provision-agent.sh</span><span style="color:#9ECBFF"> alice</span></span>
<span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> /opt/openclaw/scripts/provision-agent.sh</span><span style="color:#9ECBFF"> bob</span></span>
<span class="line"></span></code></pre>
<p>Each run prints the gateway token and next steps. Save those tokens. You need them to connect.</p>
<h3>Configure the gateway</h3>
<p>Each agent needs an <code>openclaw.json</code> with allowed origins for the Control UI. Create one per agent:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D"># Replace the hostname and port for each agent</span></span>
<span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> tee</span><span style="color:#9ECBFF"> /home/alice/.openclaw/openclaw.json</span><span style="color:#F97583"> &#x3C;&#x3C;</span><span style="color:#9ECBFF"> 'EOF'</span></span>
<span class="line"><span style="color:#9ECBFF">{</span></span>
<span class="line"><span style="color:#9ECBFF">  "gateway": {</span></span>
<span class="line"><span style="color:#9ECBFF">    "mode": "local",</span></span>
<span class="line"><span style="color:#9ECBFF">    "controlUi": {</span></span>
<span class="line"><span style="color:#9ECBFF">      "allowedOrigins": [</span></span>
<span class="line"><span style="color:#9ECBFF">        "https://your-tailscale-hostname:18789",</span></span>
<span class="line"><span style="color:#9ECBFF">        "http://127.0.0.1:18789"</span></span>
<span class="line"><span style="color:#9ECBFF">      ]</span></span>
<span class="line"><span style="color:#9ECBFF">    }</span></span>
<span class="line"><span style="color:#9ECBFF">  }</span></span>
<span class="line"><span style="color:#9ECBFF">}</span></span>
<span class="line"><span style="color:#9ECBFF">EOF</span></span>
<span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> chown</span><span style="color:#9ECBFF"> 1000:1000</span><span style="color:#9ECBFF"> /home/alice/.openclaw/openclaw.json</span></span>
<span class="line"></span></code></pre>
<h3>Configure model access</h3>
<p>Option A: add <code>OPENAI_API_KEY</code> to <code>/opt/openclaw/.env.&lt;agent&gt;</code>.</p>
<p>Option B: use a ChatGPT/Codex subscription with OAuth:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">docker</span><span style="color:#9ECBFF"> exec</span><span style="color:#79B8FF"> -it</span><span style="color:#9ECBFF"> openclaw-alice</span><span style="color:#9ECBFF"> openclaw</span><span style="color:#9ECBFF"> models</span><span style="color:#9ECBFF"> auth</span><span style="color:#9ECBFF"> login</span><span style="color:#79B8FF"> --provider</span><span style="color:#9ECBFF"> openai-codex</span></span>
<span class="line"></span></code></pre>
<h3>Start</h3>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> systemctl</span><span style="color:#9ECBFF"> start</span><span style="color:#9ECBFF"> openclaw-alice</span><span style="color:#9ECBFF"> openclaw-bob</span></span>
<span class="line"></span></code></pre>
<h2>Step 8: Expose via Tailscale Serve</h2>
<p>Tailscale Serve gives you HTTPS access within your tailnet: no port forwarding, no certificates to manage, and no exposure to the public internet.</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> tailscale</span><span style="color:#9ECBFF"> serve</span><span style="color:#79B8FF"> --bg</span><span style="color:#79B8FF"> --https=18789</span><span style="color:#9ECBFF"> http://127.0.0.1:18789</span></span>
<span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> tailscale</span><span style="color:#9ECBFF"> serve</span><span style="color:#79B8FF"> --bg</span><span style="color:#79B8FF"> --https=18790</span><span style="color:#9ECBFF"> http://127.0.0.1:18790</span></span>
<span class="line"></span></code></pre>
<p>You may need to enable Serve for your node first in the <a href="https://login.tailscale.com/admin/machines">Tailscale admin console</a>.</p>
<p>Access from any device on your tailnet:</p>
<ul>
<li><code>https://your-vps-hostname:18789/</code>: Alice</li>
<li><code>https://your-vps-hostname:18790/</code>: Bob</li>
</ul>
<h2>Step 9: Lock down access with Tailscale ACLs</h2>
<p>This is where multi-user access gets interesting. Tailscale ACLs let you control exactly who can reach which agent.</p>
<p>Go to <a href="https://login.tailscale.com/admin/acls">https://login.tailscale.com/admin/acls</a> and set your policy:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>{</span></span>
<span class="line"><span>  "tagOwners": {</span></span>
<span class="line"><span>    "tag:openclaw": ["your-login@example.com"]</span></span>
<span class="line"><span>  },</span></span>
<span class="line"><span></span></span>
<span class="line"><span>  "groups": {</span></span>
<span class="line"><span>    "group:admin":  ["your-login@example.com"],</span></span>
<span class="line"><span>    "group:family": ["partner@example.com"]</span></span>
<span class="line"><span>  },</span></span>
<span class="line"><span></span></span>
<span class="line"><span>  "acls": [</span></span>
<span class="line"><span>    // Admin: full access to everything</span></span>
<span class="line"><span>    {</span></span>
<span class="line"><span>      "action": "accept",</span></span>
<span class="line"><span>      "src":    ["group:admin"],</span></span>
<span class="line"><span>      "dst":    ["*:*"]</span></span>
<span class="line"><span>    },</span></span>
<span class="line"><span></span></span>
<span class="line"><span>    // Family: only their agent, nothing else</span></span>
<span class="line"><span>    {</span></span>
<span class="line"><span>      "action": "accept",</span></span>
<span class="line"><span>      "src":    ["group:family"],</span></span>
<span class="line"><span>      "dst":    ["your-vps-hostname:18790"]</span></span>
<span class="line"><span>    }</span></span>
<span class="line"><span>  ],</span></span>
<span class="line"><span></span></span>
<span class="line"><span>  // Restrict SSH to admins only</span></span>
<span class="line"><span>  "ssh": [</span></span>
<span class="line"><span>    {</span></span>
<span class="line"><span>      "action": "accept",</span></span>
<span class="line"><span>      "src":    ["group:admin"],</span></span>
<span class="line"><span>      "dst":    ["tag:openclaw"],</span></span>
<span class="line"><span>      "users":  ["your-ssh-user"]</span></span>
<span class="line"><span>    }</span></span>
<span class="line"><span>  ],</span></span>
<span class="line"><span></span></span>
<span class="line"><span>  // Prevent accidental Funnel exposure</span></span>
<span class="line"><span>  "nodeAttrs": [</span></span>
<span class="line"><span>    {</span></span>
<span class="line"><span>      "target": ["tag:openclaw"],</span></span>
<span class="line"><span>      "attr":   ["funnel": false]</span></span>
<span class="line"><span>    }</span></span>
<span class="line"><span>  ]</span></span>
<span class="line"><span>}</span></span>
<span class="line"><span></span></span></code></pre>
<p>Then tag your VPS as <code>tag:openclaw</code> in the admin console: Machines, your VPS, Edit, Tags.</p>
<h3>Invite additional users</h3>
<ol>
<li>Tailscale admin console: Users, Invite by email</li>
<li>They install Tailscale and join with their email</li>
<li>Add their email to the appropriate group in your ACL policy</li>
<li>Add an ACL rule granting access to their specific agent port</li>
<li>Share only their agent’s gateway token</li>
</ol>
<h2>Step 10: Connecting your client</h2>
<p>Install the OpenClaw client on your local machine following the <a href="https://github.com/openclaw/openclaw#installation">official installation docs</a>. Once installed, point it at your self-hosted gateway:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">openclaw</span><span style="color:#9ECBFF"> gateway</span><span style="color:#9ECBFF"> connect</span><span style="color:#79B8FF"> \</span></span>
<span class="line"><span style="color:#79B8FF">  --url</span><span style="color:#9ECBFF"> "https://your-tailscale-hostname:18789"</span><span style="color:#79B8FF"> \</span></span>
<span class="line"><span style="color:#79B8FF">  --token</span><span style="color:#9ECBFF"> "your-gateway-token-here"</span></span>
<span class="line"></span></code></pre>
<p>The gateway URL is your VPS’s Tailscale hostname plus the agent’s port. The token was printed during provisioning and is stored in <code>/opt/openclaw/.env.&lt;agent&gt;</code> on the server.</p>
<h3>Per-agent profiles</h3>
<p>If you connect to multiple agents from the same machine, use profiles to switch between them:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D"># Save each agent as a named profile</span></span>
<span class="line"><span style="color:#B392F0">openclaw</span><span style="color:#9ECBFF"> gateway</span><span style="color:#9ECBFF"> connect</span><span style="color:#79B8FF"> \</span></span>
<span class="line"><span style="color:#79B8FF">  --url</span><span style="color:#9ECBFF"> "https://your-tailscale-hostname:18789"</span><span style="color:#79B8FF"> \</span></span>
<span class="line"><span style="color:#79B8FF">  --token</span><span style="color:#9ECBFF"> "..."</span><span style="color:#79B8FF"> \</span></span>
<span class="line"><span style="color:#79B8FF">  --profile</span><span style="color:#9ECBFF"> alice</span></span>
<span class="line"></span>
<span class="line"><span style="color:#B392F0">openclaw</span><span style="color:#9ECBFF"> gateway</span><span style="color:#9ECBFF"> connect</span><span style="color:#79B8FF"> \</span></span>
<span class="line"><span style="color:#79B8FF">  --url</span><span style="color:#9ECBFF"> "https://your-tailscale-hostname:18790"</span><span style="color:#79B8FF"> \</span></span>
<span class="line"><span style="color:#79B8FF">  --token</span><span style="color:#9ECBFF"> "..."</span><span style="color:#79B8FF"> \</span></span>
<span class="line"><span style="color:#79B8FF">  --profile</span><span style="color:#9ECBFF"> bob</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D"># Switch between agents</span></span>
<span class="line"><span style="color:#B392F0">openclaw</span><span style="color:#9ECBFF"> gateway</span><span style="color:#9ECBFF"> use</span><span style="color:#9ECBFF"> alice</span></span>
<span class="line"><span style="color:#B392F0">openclaw</span><span style="color:#9ECBFF"> gateway</span><span style="color:#9ECBFF"> use</span><span style="color:#9ECBFF"> bob</span></span>
<span class="line"></span></code></pre>
<h3>For other users</h3>
<p>Share only what they need:</p>
<ol>
<li>The gateway URL for their agent, for example <code>https://your-tailscale-hostname:18790</code></li>
<li>Their agent’s gateway token</li>
<li>A link to install the OpenClaw client</li>
</ol>
<p>They do not need SSH access, Docker access, or server credentials. Tailscale handles authentication and encryption. They just need Tailscale installed and access to your tailnet.</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D"># What you send them:</span></span>
<span class="line"><span style="color:#B392F0">openclaw</span><span style="color:#9ECBFF"> gateway</span><span style="color:#9ECBFF"> connect</span><span style="color:#79B8FF"> \</span></span>
<span class="line"><span style="color:#79B8FF">  --url</span><span style="color:#9ECBFF"> "https://your-tailscale-hostname:18790"</span><span style="color:#79B8FF"> \</span></span>
<span class="line"><span style="color:#79B8FF">  --token</span><span style="color:#9ECBFF"> "their-token-here"</span></span>
<span class="line"></span></code></pre>
<h3>Web UI alternative</h3>
<p>Every agent also serves a Control UI in the browser. No client install needed. Visit the gateway URL directly:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>https://your-tailscale-hostname:18790/</span></span>
<span class="line"><span></span></span></code></pre>
<p>The browser prompts for the gateway token on first visit.</p>
<h2>Scaling</h2>
<h3>Adding a new agent</h3>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> /opt/openclaw/scripts/provision-agent.sh</span><span style="color:#F97583"> &#x3C;</span><span style="color:#9ECBFF">nam</span><span style="color:#E1E4E8">e</span><span style="color:#F97583">></span></span>
<span class="line"><span style="color:#6A737D"># Add OPENAI_API_KEY to /opt/openclaw/.env.&#x3C;name></span></span>
<span class="line"><span style="color:#6A737D"># Create /home/&#x3C;name>/.openclaw/openclaw.json with allowed origins</span></span>
<span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> systemctl</span><span style="color:#9ECBFF"> start</span><span style="color:#9ECBFF"> openclaw-</span><span style="color:#F97583">&#x3C;</span><span style="color:#9ECBFF">nam</span><span style="color:#E1E4E8">e</span><span style="color:#F97583">></span></span>
<span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> tailscale</span><span style="color:#9ECBFF"> serve</span><span style="color:#79B8FF"> --bg</span><span style="color:#79B8FF"> --https=</span><span style="color:#F97583">&#x3C;</span><span style="color:#79B8FF">port</span><span style="color:#F97583">></span><span style="color:#9ECBFF"> http://127.0.0.1:</span><span style="color:#F97583">&#x3C;</span><span style="color:#9ECBFF">por</span><span style="color:#E1E4E8">t</span><span style="color:#F97583">></span></span>
<span class="line"><span style="color:#6A737D"># Update Tailscale ACLs if new users need access</span></span>
<span class="line"></span></code></pre>
<h3>Checking status</h3>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> /opt/openclaw/scripts/status.sh</span></span>
<span class="line"></span></code></pre>
<p>Output:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>========================================</span></span>
<span class="line"><span>OpenClaw Agent Status</span></span>
<span class="line"><span>========================================</span></span>
<span class="line"><span></span></span>
<span class="line"><span>AGENT        PORT   CONTAINER    SYSTEMD    HEALTH</span></span>
<span class="line"><span>-----        ----   ---------    -------    ------</span></span>
<span class="line"><span>alice        18789  running      active     healthy</span></span>
<span class="line"><span>bob          18790  running      active     healthy</span></span>
<span class="line"><span></span></span>
<span class="line"><span>Resource Usage:</span></span>
<span class="line"><span>  openclaw-alice: CPU=0.01% MEM=334MiB / 2.5GiB</span></span>
<span class="line"><span>  openclaw-bob:   CPU=0.00% MEM=324MiB / 2.5GiB</span></span>
<span class="line"><span></span></span></code></pre>
<h3>Capacity planning</h3>
<table>
<thead>
<tr>
<th>Agents</th>
<th>Min RAM</th>
<th>Comfortable RAM</th>
</tr>
</thead>
<tbody>
<tr>
<td>2</td>
<td>4 GB</td>
<td>8 GB</td>
</tr>
<tr>
<td>3</td>
<td>6 GB</td>
<td>8 GB</td>
</tr>
<tr>
<td>4+</td>
<td>8 GB</td>
<td>12+ GB</td>
</tr>
</tbody>
</table>
<p>Each agent idles at about 330 MiB and peaks around 1-1.5 GiB under load.</p>
<h2>Verification checklist</h2>
<p>After deployment, run through these checks:</p>
<ul>
<li>[ ] <code>docker ps</code>: all containers running and healthy</li>
<li>[ ] <code>curl http://127.0.0.1:&lt;port&gt;/</code>: each gateway returns HTTP 200</li>
<li>[ ] Access <code>https://your-hostname:&lt;port&gt;/</code> from your device: UI loads</li>
<li>[ ] Access from restricted user’s device: only their agent is reachable</li>
<li>[ ] <code>sudo /opt/openclaw/scripts/status.sh</code>: all agents show healthy</li>
<li>[ ] Reboot the VPS: agents auto-start via systemd</li>
<li>[ ] <code>docker exec openclaw-&lt;agent&gt; which gog wacli goplaces</code>: CLI tools present</li>
<li>[ ] <code>ls /home/&lt;agent&gt;/.openclaw/workspace/memory/</code>: <code>observations.md</code> exists</li>
<li>[ ] <code>sudo crontab -l</code>: Total Recall cron entries present for all agents</li>
</ul>
<h2>Troubleshooting</h2>
<h3>Gateway fails with an allowedOrigins error</h3>
<p>The gateway refuses to start when binding to LAN without explicit CORS origins. Create or update <code>openclaw.json</code>:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#E1E4E8">{</span></span>
<span class="line"><span style="color:#79B8FF">  "gateway"</span><span style="color:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#79B8FF">    "mode"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"local"</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#79B8FF">    "controlUi"</span><span style="color:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#79B8FF">      "allowedOrigins"</span><span style="color:#E1E4E8">: [</span><span style="color:#9ECBFF">"https://your-hostname:PORT"</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">"http://127.0.0.1:PORT"</span><span style="color:#E1E4E8">]</span></span>
<span class="line"><span style="color:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>
<h3>Container keeps restarting</h3>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">docker</span><span style="color:#9ECBFF"> logs</span><span style="color:#9ECBFF"> openclaw-</span><span style="color:#F97583">&#x3C;</span><span style="color:#9ECBFF">agen</span><span style="color:#E1E4E8">t</span><span style="color:#F97583">></span><span style="color:#79B8FF"> --tail</span><span style="color:#79B8FF"> 20</span></span>
<span class="line"></span></code></pre>
<p>Common causes: missing config, port conflict, permission issues on mounted volumes.</p>
<h3>Total Recall is not observing</h3>
<p>Check that the cron entries exist and the container is named correctly:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> crontab</span><span style="color:#79B8FF"> -l</span><span style="color:#F97583"> |</span><span style="color:#B392F0"> grep</span><span style="color:#F97583"> &#x3C;</span><span style="color:#9ECBFF">agen</span><span style="color:#E1E4E8">t</span><span style="color:#F97583">></span></span>
<span class="line"><span style="color:#B392F0">docker</span><span style="color:#9ECBFF"> exec</span><span style="color:#9ECBFF"> openclaw-</span><span style="color:#F97583">&#x3C;</span><span style="color:#9ECBFF">agen</span><span style="color:#E1E4E8">t</span><span style="color:#F97583">></span><span style="color:#9ECBFF"> ls</span><span style="color:#9ECBFF"> /home/node/.openclaw/skills/total-recall/scripts/</span></span>
<span class="line"></span></code></pre>
<p>Check observer logs:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">tail</span><span style="color:#79B8FF"> -f</span><span style="color:#9ECBFF"> /home/</span><span style="color:#F97583">&#x3C;</span><span style="color:#9ECBFF">agen</span><span style="color:#E1E4E8">t</span><span style="color:#F97583">></span><span style="color:#9ECBFF">/.openclaw/workspace/logs/observer.log</span></span>
<span class="line"></span></code></pre>
<h3>Tailscale Serve will not start</h3>
<p>Visit the link in the error output to enable Serve for your node in the Tailscale admin console. This is a one-time approval per node.</p>
]]></content>
        <author>
            <name>Gabriel Ledung</name>
        </author>
        <category label="journal" term="journal"/>
        <published>2026-02-10T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[VPS hardening guide]]></title>
        <id>tag:rexgnu.com,2026:journal/vps-hardening-guide</id>
        <link href="https://rexgnu.com/journal/vps-hardening-guide/"/>
        <updated>2026-01-08T00:00:00.000Z</updated>
        <content type="html"><![CDATA[<p>Steps taken to harden an Ubuntu Noble (24.04) VPS on Hetzner (arm64).</p>
<h2>SSH hardening</h2>
<p>Move SSH to a non-standard port and lock down authentication.</p>
<p>Config is split across <code>/etc/ssh/sshd_config</code> and <code>/etc/ssh/sshd_config.d/*.conf</code>.</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D"># Key settings to ensure:</span></span>
<span class="line"><span style="color:#B392F0">Port</span><span style="color:#79B8FF"> 2222</span></span>
<span class="line"><span style="color:#B392F0">PermitRootLogin</span><span style="color:#9ECBFF"> no</span></span>
<span class="line"><span style="color:#B392F0">PasswordAuthentication</span><span style="color:#9ECBFF"> no</span></span>
<span class="line"><span style="color:#B392F0">PubkeyAuthentication</span><span style="color:#9ECBFF"> yes</span></span>
<span class="line"><span style="color:#B392F0">KbdInteractiveAuthentication</span><span style="color:#9ECBFF"> no</span></span>
<span class="line"><span style="color:#B392F0">PermitEmptyPasswords</span><span style="color:#9ECBFF"> no</span></span>
<span class="line"><span style="color:#B392F0">MaxAuthTries</span><span style="color:#79B8FF"> 2</span></span>
<span class="line"><span style="color:#B392F0">X11Forwarding</span><span style="color:#9ECBFF"> no</span></span>
<span class="line"><span style="color:#B392F0">AllowAgentForwarding</span><span style="color:#9ECBFF"> no</span></span>
<span class="line"></span></code></pre>
<p>Reload after changes:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> systemctl</span><span style="color:#9ECBFF"> reload</span><span style="color:#9ECBFF"> ssh</span></span>
<span class="line"></span></code></pre>
<h2>SSH keypair</h2>
<p>Generate an Ed25519 keypair on your local machine:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">ssh-keygen</span><span style="color:#79B8FF"> -t</span><span style="color:#9ECBFF"> ed25519</span></span>
<span class="line"></span></code></pre>
<p>Copy the public key to the server’s <code>~/.ssh/authorized_keys</code>.</p>
<h2>Firewall (UFW)</h2>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> ufw</span><span style="color:#9ECBFF"> default</span><span style="color:#9ECBFF"> deny</span><span style="color:#9ECBFF"> incoming</span></span>
<span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> ufw</span><span style="color:#9ECBFF"> default</span><span style="color:#9ECBFF"> allow</span><span style="color:#9ECBFF"> outgoing</span></span>
<span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> ufw</span><span style="color:#9ECBFF"> enable</span></span>
<span class="line"></span></code></pre>
<p>Initially allow SSH on the public interface:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> ufw</span><span style="color:#9ECBFF"> allow</span><span style="color:#79B8FF"> 2222</span></span>
<span class="line"></span></code></pre>
<p>After Tailscale is set up, restrict SSH to the Tailscale interface only:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> ufw</span><span style="color:#9ECBFF"> allow</span><span style="color:#9ECBFF"> in</span><span style="color:#9ECBFF"> on</span><span style="color:#9ECBFF"> tailscale0</span><span style="color:#9ECBFF"> to</span><span style="color:#9ECBFF"> any</span><span style="color:#9ECBFF"> port</span><span style="color:#79B8FF"> 2222</span></span>
<span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> ufw</span><span style="color:#9ECBFF"> delete</span><span style="color:#9ECBFF"> allow</span><span style="color:#79B8FF"> 2222</span></span>
<span class="line"></span></code></pre>
<h2>Fail2Ban</h2>
<p>Install and enable fail2ban to block brute-force attempts:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> apt</span><span style="color:#9ECBFF"> install</span><span style="color:#9ECBFF"> fail2ban</span><span style="color:#79B8FF"> -y</span></span>
<span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> systemctl</span><span style="color:#9ECBFF"> enable</span><span style="color:#79B8FF"> --now</span><span style="color:#9ECBFF"> fail2ban</span></span>
<span class="line"></span></code></pre>
<h2>Unattended upgrades</h2>
<p>Ensure automatic security updates are enabled:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> apt</span><span style="color:#9ECBFF"> install</span><span style="color:#9ECBFF"> unattended-upgrades</span><span style="color:#79B8FF"> -y</span></span>
<span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> dpkg-reconfigure</span><span style="color:#79B8FF"> -plow</span><span style="color:#9ECBFF"> unattended-upgrades</span></span>
<span class="line"></span></code></pre>
<h2>Tailscale (zero-trust network)</h2>
<p>Install Tailscale:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">curl</span><span style="color:#79B8FF"> -fsSL</span><span style="color:#9ECBFF"> https://tailscale.com/install.sh</span><span style="color:#F97583"> |</span><span style="color:#B392F0"> sh</span></span>
<span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> tailscale</span><span style="color:#9ECBFF"> up</span></span>
<span class="line"></span></code></pre>
<p>Tailscale starts on boot automatically (<code>tailscaled.service</code> is enabled by default).</p>
<p>Once connected, lock down the firewall to Tailscale-only SSH. This makes SSH invisible on the public internet.</p>
<h2>Disable unused services</h2>
<p>Disable and mask serial consoles that are not needed:</p>
<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> systemctl</span><span style="color:#9ECBFF"> disable</span><span style="color:#79B8FF"> --now</span><span style="color:#9ECBFF"> serial-getty@ttyAMA0.service</span><span style="color:#9ECBFF"> serial-getty@ttyS0.service</span></span>
<span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> systemctl</span><span style="color:#9ECBFF"> mask</span><span style="color:#9ECBFF"> serial-getty@ttyAMA0.service</span><span style="color:#9ECBFF"> serial-getty@ttyS0.service</span></span>
<span class="line"></span></code></pre>
<h2>Final state</h2>
<ul>
<li>SSH only accessible via Tailscale on port 2222</li>
<li>Key-only authentication, no root login</li>
<li>Brute-force protection via fail2ban</li>
<li>Automatic security updates</li>
<li>No unnecessary services running</li>
</ul>
]]></content>
        <author>
            <name>Gabriel Ledung</name>
        </author>
        <category label="journal" term="journal"/>
        <published>2026-01-08T00:00:00.000Z</published>
    </entry>
</feed>