<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: OwnAgent</title>
    <description>The latest articles on DEV Community by OwnAgent (@ownagent).</description>
    <link>https://dev.to/ownagent</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4200148%2Fb5fe544d-4fb9-4470-880e-8e77738c2054.png</url>
      <title>DEV Community: OwnAgent</title>
      <link>https://dev.to/ownagent</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ownagent"/>
    <language>en</language>
    <item>
      <title>Don't trust the config: 6 tests that prove your self-hosted AI agent server is locked down</title>
      <dc:creator>OwnAgent</dc:creator>
      <pubDate>Thu, 22 Oct 2026 10:03:15 +0000</pubDate>
      <link>https://dev.to/ownagent/dont-trust-the-config-6-tests-that-prove-your-self-hosted-ai-agent-server-is-locked-down-2k8k</link>
      <guid>https://dev.to/ownagent/dont-trust-the-config-6-tests-that-prove-your-self-hosted-ai-agent-server-is-locked-down-2k8k</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: I wrote this with AI assistance and checked every command and number against my own test runs.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A personal AI agent on a VPS (OpenClaw, Hermes) tends to get "secured" by reading config files. The config says one thing; the machine does another. Here are six tests I run that check behavior, not settings. Each takes a few minutes. Replace &lt;code&gt;AGENT_UNIT&lt;/code&gt;, &lt;code&gt;YOUR_IP&lt;/code&gt; and &lt;code&gt;USER&lt;/code&gt; with yours.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Scan from outside, not &lt;code&gt;ufw status&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;From another machine, not the server itself and not localhost:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nmap &lt;span class="nt"&gt;-Pn&lt;/span&gt; &lt;span class="nt"&gt;-p-&lt;/span&gt; &lt;span class="nt"&gt;-sS&lt;/span&gt; YOUR_IP
nmap &lt;span class="nt"&gt;-Pn&lt;/span&gt; &lt;span class="nt"&gt;-sU&lt;/span&gt; &lt;span class="nt"&gt;--top-ports&lt;/span&gt; 100 YOUR_IP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected: only &lt;code&gt;22/tcp&lt;/code&gt; open. Run it after the agent is started, since the agent's own ports are open by then. If you see the agent gateway port or a dashboard, fix that before anything else. On the server, also check that the agent listens on loopback only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ss &lt;span class="nt"&gt;-tulnp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything except sshd should be on loopback: &lt;code&gt;127.0.0.1&lt;/code&gt; or &lt;code&gt;::1&lt;/code&gt; for the agent (OpenClaw's gateway is on 18789). Two harmless system entries show up on any Ubuntu: the DNS resolver on &lt;code&gt;127.0.0.53&lt;/code&gt;/&lt;code&gt;127.0.0.54&lt;/code&gt; and the DHCP client on &lt;code&gt;udp/68&lt;/code&gt;. Open the panel through a tunnel: &lt;code&gt;ssh -N -L PORT:127.0.0.1:PORT USER@YOUR_IP&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Read the effective SSH config
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;grep /etc/ssh/sshd_config&lt;/code&gt; misses drop-ins (cloud images often ship one in &lt;code&gt;sshd_config.d/&lt;/code&gt; that re-enables password login). Ask the daemon:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;sshd &lt;span class="nt"&gt;-T&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-Ei&lt;/span&gt; &lt;span class="s1"&gt;'passwordauthentication|kbdinteractiveauthentication|permitrootlogin'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected: &lt;code&gt;no&lt;/code&gt;, &lt;code&gt;no&lt;/code&gt;, &lt;code&gt;no&lt;/code&gt;. Then test it from outside:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;PreferredAuthentications&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;password &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;PubkeyAuthentication&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;no USER@YOUR_IP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should get &lt;code&gt;Permission denied&lt;/code&gt; without a password prompt.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Make fail2ban actually ban someone
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;systemctl is-active fail2ban&lt;/code&gt; proves a process exists. From a second host, fail a few SSH logins on purpose, then on the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;fail2ban-client status sshd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The attacker IP should appear under banned. On my test VMs the ban came after 7 and 127 seconds, depending on timing of retries. Unban yourself afterwards: &lt;code&gt;sudo fail2ban-client set sshd unbanip IP&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Kill the agent and time the recovery
&lt;/h3&gt;

&lt;p&gt;Agents usually run as a user service, so ask the user's systemd, not the system one (the system one will happily print &lt;code&gt;MainPID=0&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;-M&lt;/span&gt; USER@ show AGENT_UNIT &lt;span class="nt"&gt;-p&lt;/span&gt; MainPID &lt;span class="nt"&gt;-p&lt;/span&gt; Restart
&lt;span class="nb"&gt;sudo kill&lt;/span&gt; &lt;span class="nt"&gt;-9&lt;/span&gt; &amp;lt;PID&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The unit should restart on its own (&lt;code&gt;Restart=always&lt;/code&gt; or &lt;code&gt;on-failure&lt;/code&gt;), and the agent should answer in Telegram without you touching it. A user service also needs lingering, or it won't survive a reboot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;loginctl show-user USER &lt;span class="nt"&gt;-p&lt;/span&gt; Linger
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I measured 6–7 s for the process and 22–42 s until the first Telegram reply.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Test the alert by stopping the service
&lt;/h3&gt;

&lt;p&gt;An alert that was never triggered is a hope. Stop the agent and see whether a message reaches your phone within a couple of minutes. Two traps: the alert must go through your own bot, not the bot of whoever set it up; and no alert can fire if the whole VPS dies, so keep an external uptime ping too. Proof of delivery in a script: the Bot API &lt;code&gt;sendMessage&lt;/code&gt; response has &lt;code&gt;"ok": true&lt;/code&gt; and a &lt;code&gt;message_id&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Check the spending limit on the key, not in the agent
&lt;/h3&gt;

&lt;p&gt;Neither OpenClaw nor Hermes has a built-in hard spend cap. The cap that holds is on the API key. With OpenRouter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://openrouter.ai/api/v1/key &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at &lt;code&gt;limit&lt;/code&gt; (not null) and &lt;code&gt;limit_reset&lt;/code&gt; (monthly, if you want a monthly ceiling). Don't paste the key into logs or shell history; read it from a root-only file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bonus: rehearse the rollback
&lt;/h3&gt;

&lt;p&gt;Take a snapshot of the agent state before each update (OpenClaw: &lt;code&gt;openclaw backup create --verify&lt;/code&gt;; Hermes: &lt;code&gt;hermes backup&lt;/code&gt;), pin the version, turn off self-update, and once, on a quiet evening, install a deliberately bad version and make sure you can return. My rollback runs took 1–3.5 minutes including checks.&lt;/p&gt;

&lt;h3&gt;
  
  
  What these tests don't prove
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A scan from a neighbor machine on the same network is weaker than one from the internet. Use a throwaway VPS as the scanner.&lt;/li&gt;
&lt;li&gt;Hermes lets the agent write skill files itself, so a "skill allowlist" detects extras but cannot fully prevent them. I'd rather say that than pretend.&lt;/li&gt;
&lt;li&gt;A passing check today doesn't cover the next update.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Questions or checks I missed? I'd like to add them.&lt;/p&gt;




&lt;p&gt;About the author: I set up and harden personal AI agents on VPSes.&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>security</category>
      <category>linux</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
