<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[SSH rate limit on ports 22/202/222 also throttles *outgoing* connections from apps (breaks Termix and other SSH clients)]]></title><description><![CDATA[<p dir="auto"><strong>Cloudron version:</strong> 10.1.5</p>
<h3>Summary</h3>
<p dir="auto">The brute-force rate limit on ports 22, 202 and 222 doesn't only apply to incoming connections. Apps hit it too when they open SSH connections to <em>external</em> servers. Once an app container opens more than 4 new connections to port 22 within 10 seconds, its SYN packets are dropped. Every retry refreshes the <code>recent</code> entry, so an app that keeps retrying never gets out.</p>
<h3>Impact</h3>
<p dir="auto">I run <a href="https://termix.site" target="_blank" rel="noopener noreferrer nofollow ugc">Termix</a> (web SSH client / server management) as a custom app. It runs periodic status checks and metrics collection for every configured host. With 6 hosts, two of them reached through a jump host, it opens enough SSH connections per round to hit the limit. After that, <strong>every</strong> outgoing SSH connection from the Termix container times out, and the app can't reach any server:</p>
<pre><code>Error: Timed out while waiting for handshake
[ERROR] Jump host 1/1 connection failed
[ERROR] Stats collector connection failed
</code></pre>
<p dir="auto">Inside the container, <code>ss -tan</code> shows connections to <code>:22</code> stuck in <code>SYN-SENT</code>. The same destinations work fine from other app containers and from outside the server.</p>
<h3>Steps to reproduce</h3>
<p dir="auto">From any app's web terminal (I used n8n), open a few quick connections to an external port 22 and compare with another port:</p>
<pre><code>for i in 1 2 3 4 5 6 7; do
  timeout 3 bash -c "exec 3&lt;&gt;/dev/tcp/portquiz.net/22" 2&gt;/dev/null &amp;&amp; echo "$i open" || echo "$i FAIL"
done
# 1-4 open, 5-7 FAIL

for i in 1 2 3 4 5 6 7; do
  timeout 3 bash -c "exec 3&lt;&gt;/dev/tcp/portquiz.net/2222" 2&gt;/dev/null &amp;&amp; echo -n "ok " || echo -n "FAIL "
done
# ok ok ok ok ok ok ok

sleep 15; timeout 3 bash -c "exec 3&lt;&gt;/dev/tcp/portquiz.net/22" &amp;&amp; echo open
# open again after 10+ s without new attempts
</code></pre>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Destination</th>
<th>Termix container (stays throttled)</th>
<th>Other app containers</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="http://github.com:22" target="_blank" rel="noopener noreferrer nofollow ugc">github.com:22</a></td>
<td>timeout</td>
<td>open</td>
</tr>
<tr>
<td><a href="http://portquiz.net:22" target="_blank" rel="noopener noreferrer nofollow ugc">portquiz.net:22</a></td>
<td>timeout</td>
<td>open</td>
</tr>
<tr>
<td><a href="http://portquiz.net:2222" target="_blank" rel="noopener noreferrer nofollow ugc">portquiz.net:2222</a></td>
<td>open</td>
<td>open</td>
</tr>
</tbody>
</table>
<h3>Cause</h3>
<p dir="auto">In <code>setup/start/cloudron-firewall.sh</code>:</p>
<pre><code>for port in 22 202 222; do
    ipxtables -A CLOUDRON_RATELIMIT -p tcp --dport ${port} -m state --state NEW -m recent --set --name "public-${port}"
    ipxtables -A CLOUDRON_RATELIMIT -p tcp --dport ${port} -m state --state NEW -m recent --update --name "public-${port}" --seconds 10 --hitcount 5 -j CLOUDRON_RATELIMIT_LOG
done
...
ipxtables -I FORWARD 1 -j CLOUDRON_RATELIMIT
</code></pre>
<p dir="auto"><code>CLOUDRON_RATELIMIT</code> is hooked into <code>FORWARD</code>, which makes sense for incoming SSH that gets DNAT'ed to app containers (e.g. Gitea's SSH port). The rules have no source restriction, though, so traffic <em>leaving</em> containers towards the internet also matches. <code>recent</code> keys on the source address, so each app container gets its own counter, shared across all destinations.</p>
<h3>Suggested fix</h3>
<p dir="auto">Exclude container-originated traffic from these rules, e.g.:</p>
<pre><code>$iptables -A CLOUDRON_RATELIMIT -p tcp ! -s 172.18.0.0/16 --dport ${port} -m state --state NEW -m recent --set ...
$iptables -A CLOUDRON_RATELIMIT -p tcp ! -s 172.18.0.0/16 --dport ${port} -m state --state NEW -m recent --update ...
</code></pre>
<p dir="auto">(plus the equivalent for the Docker IPv6 subnet in the <code>ip6tables</code> rules, since containers can also go out over IPv6). Alternatively, restrict the match to packets coming in on the external interface.</p>
<h3>Current workaround</h3>
<p dir="auto">On the host:</p>
<pre><code>iptables -I CLOUDRON_RATELIMIT 1 -s 172.18.0.0/16 ! -d 172.18.0.0/16 -p tcp --dport 22 -j RETURN
</code></pre>
<p dir="auto">This is lost whenever Cloudron rebuilds its firewall rules, so it isn't a lasting fix.</p>
<h3><code>cloudron-support --troubleshoot</code></h3>
<pre><code>═══ Host ═══
Vendor: netcup Product: KVM Server
Linux: 6.8.0-147-generic
Ubuntu: noble 24.04
Cloudron: 10.1.5
Execution environment: kvm
Processor: AMD EPYC 7702P 64-Core Processor
BIOS pc-i440fx-6.2  CPU @ 2.0GHz x 6
RAM: 16376312KB
Disk: /dev/vda3       256G

═══ System ═══
[OK]	Root disk usage is OK (58%)
[OK]	Memory usage is OK (77%)
[WARN]	Disk I/O on /home/yellowtent (vda, HDD): 180 MB/s write, iowait 30% — sustained iowait at or above 20% means the system is I/O bound. Investigate with 'iotop' or 'pidstat -d 2'.
[OK]	Clock is NTP-synchronized
[OK]	node version is correct
[OK]	IPv6 is enabled in kernel. Public IPv6 address detected
[OK]	docker is running
[OK]	docker version is correct

═══ Networking ═══
[OK]	no incompatible persistent-firewall packages installed
[OK]	DNS is resolving via systemd-resolved
[OK]	unbound is running
[OK]	IPv4 HTTPS to api.cloudron.io/api/v1/helper/public_ip
[OK]	IPv6 HTTPS to api.cloudron.io/api/v1/helper/public_ip
[OK]	IPv4 HTTPS to auth.docker.io/token
[OK]	IPv6 HTTPS to auth.docker.io/token
[OK]	IPv4 HTTPS to acme-v02.api.letsencrypt.org
[OK]	IPv6 HTTPS to acme-v02.api.letsencrypt.org
[OK]	netplan is good

═══ Cloudron Platform ═══
[OK]	MySQL is running
[OK]	nginx is running
[OK]	Dashboard cert is valid (33 days remaining, expires Nov 21 05:12:04 2026 GMT)
[OK]	dashboard is reachable via loopback
[OK]	No pending database migrations
[OK]	Service 'mysql' is running and healthy
[OK]	Service 'postgresql' is running and healthy
[OK]	Service 'mongodb' is lazy-stopped (no installed Cloudron app uses the 'mongodb' addon)
[OK]	Service 'mail' is running and healthy
[OK]	Service 'graphite' is running and healthy
[OK]	Service 'sftp' is running and healthy
[OK]	box v10.1.5 is running
[OK]	Dashboard is reachable via IPv4 (https://my.9wd.eu)
[OK]	Dashboard is reachable via IPv6 (https://my.9wd.eu)
[SKIP]	Domain expiry check — RDAP lookup for 9wd.eu failed (TLD may not support RDAP, or network unreachable)

======== Summary ========
PASS:  31
WARN:  1
FAIL:  0
SKIP:  1
</code></pre>
<h3>Logs</h3>
<p dir="auto"><code>/home/yellowtent/platformdata/logs/box.log</code>: no errors; the drops happen in the kernel firewall, not in box.</p>
<p dir="auto">The kernel log shows a drop by <code>CLOUDRON_RATELIMIT_LOG</code> of a SYN going <strong>out</strong> of an app container (<code>IN=br-… OUT=eth0</code>, <code>SRC</code> = the Termix container, <code>DST</code> = an external SSH server). The log rule is limited to 2 entries/min shared with all the inbound scanner drops, so only a few container drops make it into the log:</p>
<pre><code>Oct 18 17:38:00 kermit kernel: IPTables RateLimit: IN=br-6a947611c07d OUT=eth0 PHYSIN=vethc6c0e7b SRC=172.18.17.61 DST=188.90.223.109 LEN=60 TOS=0x00 PREC=0x00 TTL=63 ID=14768 DF PROTO=TCP SPT=59040 DPT=22 WINDOW=42340 RES=0x00 SYN URGP=0
</code></pre>
<p dir="auto"><code>/proc/net/xt_recent/public-22</code> lists container addresses next to the internet scanners (<code>172.18.17.61</code> is the Termix container):</p>
<pre><code>src=172.18.17.61 ttl: 64 last_seen: 4302953152 oldest_pkt: 7 4302952079, 4302952128, 4302952128, 4302953088, 4302953088, 4302953152, 4302953152, ...
</code></pre>
<p dir="auto">The hook in <code>FORWARD</code>:</p>
<pre><code>-A FORWARD -j CLOUDRON_RATELIMIT
-A CLOUDRON_RATELIMIT -p tcp -m tcp --dport 22 -m state --state NEW -m recent --set --name public-22 ...
-A CLOUDRON_RATELIMIT -p tcp -m tcp --dport 22 -m state --state NEW -m recent --update --seconds 10 --hitcount 5 --name public-22 ...
</code></pre>
]]></description><link>https://forum.cloudron.io/topic/16151/ssh-rate-limit-on-ports-22-202-222-also-throttles-outgoing-connections-from-apps-breaks-termix-and-other-ssh-clients</link><generator>RSS for Node</generator><lastBuildDate>Thu, 29 Oct 2026 19:06:45 GMT</lastBuildDate><atom:link href="https://forum.cloudron.io/topic/16151.rss" rel="self" type="application/rss+xml"/><pubDate>Sun, 18 Oct 2026 17:41:53 GMT</pubDate><ttl>60</ttl><item><title><![CDATA[Reply to SSH rate limit on ports 22/202/222 also throttles *outgoing* connections from apps (breaks Termix and other SSH clients) on Mon, 19 Oct 2026 10:44:25 GMT]]></title><description><![CDATA[<p dir="auto">Hello <a class="plugin-mentions-user plugin-mentions-a" href="/user/fbartels" aria-label="Profile: fbartels">@<bdi>fbartels</bdi></a><br />
Thanks for the report. This will be fixed with the next release.</p>
]]></description><link>https://forum.cloudron.io/post/131224</link><guid isPermaLink="true">https://forum.cloudron.io/post/131224</guid><dc:creator><![CDATA[james]]></dc:creator><pubDate>Mon, 19 Oct 2026 10:44:25 GMT</pubDate></item></channel></rss>