<?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/"
xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>c0t0d0s0.org</title>
<description>A blog about Life, Solaris, Cycling and all the rest.</description>
<link>https://www.c0t0d0s0.org/</link>
<atom:link href="https://www.c0t0d0s0.org/c0t0d0s0.xml" rel="self" type="application/rss+xml"/>
<language>de</language>
<lastBuildDate>Thu, 21 May 2026 13:01:18 +0000</lastBuildDate>
<image>
  <url>https://www.c0t0d0s0.org/favicon.svg</url>
  <title>c0t0d0s0.org</title>
  <link>https://www.c0t0d0s0.org/</link>
</image>
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
<item>
<title>After the ban</title>
<description>Redirecting instead of rejecting!</description>
<content:encoded>&lt;p&gt;At some point it also became clear to me that evaluating the result codes from all IPs (not what they loaded, only what the HTTP server thought of it) had, on the one hand, turned into a fairly substantial PII collection (which is why I kept it only in main memory) — one that under certain circumstances would have allowed me to “depseudonymize” the data in the pseudonymized logfile within the first 24h. Stopgap solution … yes. Final architecture … not really.&lt;/p&gt;

&lt;p&gt;On top of that, this table was pretty annoying. Reboot the machine? Data gone. New daemon version? In most cases, data gone. I did learn a technique along the way that let me swap code on the fly, but that didn’t apply to data structures. I need a new field? Data gone! And once the data was gone, the reaction component lost a key data element. At some point a function had been added that checked whether the service had even been running long enough to have collected enough data. Which, due to changes, was rarely the case.&lt;/p&gt;

&lt;p&gt;Beyond that, the signal was mostly fairly weak. Essentially the signal was only strong enough when an IP was making regular feed requests. The feed requests were a kind of heartbeat that the system monitored. My hypothesis was: a scanner doesn’t simulate a feed reader for hours on end.&lt;/p&gt;

&lt;p&gt;For everyone else, the desired signal “there’s more behind this IP  than just the abuser” was rather weak. Or to put it more precisely: in one direction it was indeed quite informative. If between hour -24 and hour -1 there were several valid accesses from an IP, I could assume it was probably a CGNAT, NAT or VPN. The reverse, however, didn’t hold — the number of accesses to my blog is simply too low for that. I think you’d need several orders of magnitude&lt;sup id=&quot;fnref:4&quot;&gt;&lt;a href=&quot;https://www.c0t0d0s0.org/blog/aftertheban.html#fn:4&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; more traffic. The data was so thin that the statement “because I haven’t seen a 200 response, it’s not a NAT, CGNAT or VPN” would have been pure speculation. So this approach was of limited value to me.&lt;/p&gt;

&lt;p&gt;Now, ideas usually don’t come to you in front of the computer, but on the toilet, while making tea, or in the shower. I had this one while reading: I’m looking at the wrong side of the ban. For me, the information about what happens &lt;em&gt;after&lt;/em&gt; the ban would have considerably more value than the information about what happens &lt;em&gt;before&lt;/em&gt; the ban.&lt;/p&gt;

&lt;p&gt;In the previous solution I was hooked into the firewall log. So I knew that something was happening. A request had been rejected by the firewall. I knew from the rule what it was: HTTP or HTTPS. But I didn’t know what was inside it. My server didn’t either. It refused to have the conversation. I had no log of what the client would have liked to do.&lt;/p&gt;

&lt;h2 id=&quot;rebuild&quot;&gt;Rebuild&lt;/h2&gt;

&lt;p&gt;With this thought in mind, I rebuilt the setup. I had originally come at this from the angle that all the noise in the logfiles annoyed me. So a new solution had to keep requests away from my web server just like an IP block does. What doesn’t reach the server doesn’t get logged.&lt;/p&gt;

&lt;p&gt;But I don’t necessarily have to block the request entirely. I can also redirect it elsewhere. And that’s exactly what the new solution does. As before, the system hooks into the honeypot log. The detection component is Apache. As before, an IP address can only end up in this file by being associated with a request that violated a detection pattern, so that — also as before — every IP address in it is handed over to the reaction component.&lt;/p&gt;

&lt;p&gt;However, there is now only one reason why an IP address gets blocked outright. If an IP comes from an ASN on Spamhaus’s “Filter-on-Sight” list, a reject ban is issued. Nothing good comes out of those gates. I don’t even want to know what comes out of there. And that’s straight away for seven days. For all practical purposes that’s a permanent ban, since on the next violation the ban gets extended by another 7d.&lt;/p&gt;

&lt;p&gt;I didn’t want to make the ban permanent outright. If no traffic is coming from an IP — or in some cases a subnet — then I don’t need a filter rule for it. And the existence of the filter rule gives me some visibility that something is coming out of those networks, without me having to look more closely into the logfile. I can also see more quickly where a subnet ban is worthwhile and where it isn’t.&lt;/p&gt;

&lt;p&gt;The decisive difference between this solution and the old one is that alongside the rejects there are also redirects. And a redirect is essentially the default response when hitting the honeypot. If you’ve triggered the detection rules, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nftables&lt;/code&gt; redirects you internally from the original webserver&lt;sup id=&quot;fnref:6&quot;&gt;&lt;a href=&quot;https://www.c0t0d0s0.org/blog/aftertheban.html#fn:6&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; to another web server&lt;sup id=&quot;fnref:5&quot;&gt;&lt;a href=&quot;https://www.c0t0d0s0.org/blog/aftertheban.html#fn:5&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;. Instead of port 80 you land on port 10080. Instead of port 443 you get internally redirected to port 10043&lt;sup id=&quot;fnref:1&quot;&gt;&lt;a href=&quot;https://www.c0t0d0s0.org/blog/aftertheban.html#fn:1&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;. This means all further accesses from these IP addresses are no longer in the logfile of the primary server. So, my first requirement was met. Second, I now have an additional logfile that contains only the actions of banned users. I now know what happens after the ban.&lt;/p&gt;

&lt;h2 id=&quot;what-happened-after-the-ban&quot;&gt;What happened after the ban?&lt;/h2&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# cat c0d0s0.org-access.log-blocked | head -1
a:b:c:: - - [23/Apr/2026:05:25:30 +0200] &quot;GET / HTTP/2.0&quot; 301 854 &quot;-&quot; &quot;curl/8.14.1&quot;
# cat c0d0s0.org-access.log-blocked | tail -1
a.b.c.d - - [27/Apr/2026:05:14:05 +0200] &quot;GET /setup.php HTTP/1.1&quot; 404 14601 &quot;-&quot; &quot;-&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The dataset covers about 4 days.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# wc -l c0d0s0.org-access.log-blocked 
3208 c0d0s0.org-access.log-blocked
# wc -l honeypot.log-blocked 
2098 honeypot.log-blocked
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Based on this, 1100 requests landed on the ban web server that didn’t match the patterns in the detection component.&lt;/p&gt;

&lt;p&gt;The vast majority of these accesses are, frankly, candidates for new filter patterns. There was only one IP address from which actual accesses to &lt;abbr title=&quot;HyperText Markup Language&quot;&gt;HTML&lt;/abbr&gt; files had taken place. There are clear indications, though, that this was just a camouflage pattern. But even if it were a genuine access, I’d consider a false-positive count of one IP acceptable. At least the first four days suggest a high specificity of the measures.&lt;/p&gt;

&lt;h2 id=&quot;new-detection-logic&quot;&gt;New detection logic&lt;/h2&gt;

&lt;p&gt;I took the opportunity to put the detection logic on a new footing as well.&lt;/p&gt;

&lt;p&gt;Determining that something should become a 403, and actually making it a 403, are now separate functions.&lt;/p&gt;

&lt;p&gt;A large set of SetEnvIfNoCase statements sets an environment variable. This contains a short identifier for the rule.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;SetEnvIfNoCase Request_URI &quot;/wp-[a-z-]+\.php&quot;  honeypot=S2-WP001
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If this environment variable is set, a central rewrite rule kicks in that actually produces the 403.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;RewriteCond %{ENV:honeypot} !^$
RewriteRule .* - [F,L]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;improvement-in-monitoring&quot;&gt;Improvement in monitoring&lt;/h2&gt;

&lt;p&gt;This rebuild was necessary primarily because I wanted to introduce one significant change. The environment variable &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;honeypot&lt;/code&gt; now contains a short identifier that names the rule. This means I can carry that information through into the log.&lt;/p&gt;

&lt;p&gt;The last matching rule wins, not the one that fits best. Unfortunately I couldn’t get the correct content to reliably end up there within my tangle of RewriteRules. Sometimes it would say &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-&lt;/code&gt;, sometimes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1&lt;/code&gt;. Rarely did it contain the correct rule identifier. Only after the rewrite to SetEnvIfNoCase was the variable &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;honeypot&lt;/code&gt; reliably set. Or so I thought. Here too I had to deal with the issue of redirects and the resulting renaming of environment variables. Internally, displaying the ErrorDocument is a redirect, which means the environment variables are renamed accordingly. I log both the variable &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;honeypot&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;REDIRECT_honeypot&lt;/code&gt;. A somewhat brute-force solution, but it makes sure I get the information I want into the files.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;LogFormat &quot;%t %a %{remote}p %A %{local}p \&quot;%r\&quot; %{honeypot}e %{REDIRECT_honeypot}e&quot; connlog
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;flyswatter&lt;/code&gt; then assembles the correct reason for filtering from both:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;2026-04-27 19:28:54,469 INFO ipapi check a.b.c.d: triggered (datacenter) -&amp;gt; extending ban to 86400s (redirect)
2026-04-27 19:28:54,541 INFO Verdict on a.b.c.d: REASON:S2-WP008 NOSELF NOFASTTRACK NOBADASN:9999999/NARF-ASN NOABUSE:32 IPAPI:datacenter NOGROUP NOPREBAN DURATION:1d/redirect
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I can see right away which rule led to the ban: Section 2 — Wordpress rule 8.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# grep &quot;S2-WP008&quot; .htaccess 
SetEnvIfNoCase Request_URI &quot;(wp-conflg|setup-config|wp-setup|readme\.html|license\.txt)&quot; honeypot=S2-WP008
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;implementing-the-new-logic-on-two-servers&quot;&gt;Implementing the new logic on two servers&lt;/h2&gt;

&lt;p&gt;For this I have two &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.htaccess&lt;/code&gt; templates in my repository&lt;sup id=&quot;fnref:2&quot;&gt;&lt;a href=&quot;https://www.c0t0d0s0.org/blog/aftertheban.html#fn:2&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;5&lt;/a&gt;&lt;/sup&gt;. &lt;a href=&quot;https://codeberg.org/c0t0d0s0/flyswatter/src/tag/v0.03/htaccess-primary&quot;&gt;This&lt;/a&gt; version goes into the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.htaccess&lt;/code&gt; of the primary server (reachable on 80, 443).&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://codeberg.org/c0t0d0s0/flyswatter/src/tag/v0.03/htaccess.redirect&quot;&gt;A slightly modified ruleset&lt;/a&gt; is meant for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.htaccess&lt;/code&gt; of the redirect server. Note in particular the second whitelist rul  &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SetEnvIf Request_URI &quot;^/notfound/&quot;&lt;/code&gt;. That’s where the always-served document I mentioned earlier lives. This second whitelist entry was needed to make sure the rewrite logic doesn’t fire on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/notfound/index.html&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/notfound/index.html&lt;/code&gt; in turn is wired up via the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ErrorDocument&lt;/code&gt; block.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ErrorDocument 404 /notfound/
ErrorDocument 403 /notfound/
ErrorDocument 410 /notfound/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Since there’s almost nothing else on the web server, practically every request becomes a 404 and the corresponding ErrorDocument is served. And because I’ve assigned the same ErrorDocument to 403 and 410, the redirect server only ever responds with this one document.&lt;/p&gt;

&lt;p&gt;You could also just copy the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.htaccess&lt;/code&gt; from the primary server into the redirect server and add the second whitelist rule. Since the redirect server is supposed to display exactly one page, please clear out all other rewrite rules and redirects in the process. Anything else leads to surprisingly hard-to-find problems (for example, the main web server’s site still being served).&lt;/p&gt;

&lt;h2 id=&quot;feeds&quot;&gt;Feeds&lt;/h2&gt;

&lt;p&gt;There is one exception: the feeds remain reachable even on the redirect server. So the server isn’t entirely contentless. My readership consumes this blog largely as an &lt;abbr title=&quot;Really Simple Syndication&quot;&gt;RSS&lt;/abbr&gt; feed. If for whatever reason a feed reader’s IP gets banned, they can still read the feeds. The collateral damage of a false-positive block is therefore considerably reduced.&lt;/p&gt;

&lt;p&gt;I had previously considered building an exception for IPs that regularly poll the &lt;abbr title=&quot;Really Simple Syndication&quot;&gt;RSS&lt;/abbr&gt; feeds, until I noticed that it’s significantly simpler and more stable to just serve the feeds from the redirect server as well. That way I can also block an IP that scans and pulls feeds at the same time.&lt;/p&gt;

&lt;h2 id=&quot;additional-information-source&quot;&gt;Additional information source&lt;/h2&gt;

&lt;p&gt;The previous version already queried an IP’s reputation (at AbuseIPDB) and issued a 1-day ban once a certain threshold was exceeded.&lt;/p&gt;

&lt;p&gt;In this version another information source has been added. The system queries ipapi.is to learn more about those IP numbers that ended up in the honeypot (and only those). Part of the information this service provides is whether an IP is a VPN endpoint, a TOR exit node or a proxy. If that’s the case, the ban is shortened to 1 hour, since here the probability is highest that legitimate users may also be behind that IP. I built a caching daemon for the lookup. The free queries are limited and I want to stay within that budget.&lt;/p&gt;

&lt;p&gt;That said: if the quota is exhausted, the worst that can happen is that all bans are issued at the default length of one day. The system is set up to fail conservatively at this point.&lt;/p&gt;

&lt;p&gt;There are a number of additional conditions in my version of the script that shorten or extend a ban. I’ve removed those from the version in the Codeberg repository, because they work for me but not necessarily for anyone else. Feel free to go wild here.&lt;/p&gt;

&lt;h2 id=&quot;weakness&quot;&gt;Weakness&lt;/h2&gt;

&lt;p&gt;While I was at it, I found another weakness in this mechanism. A non-trivial number of honeypot requests still arrive at the primary server. This happens whenever the scanner works with high parallelism. For example, 16 requests come in at the same time, but the mechanism hasn’t fired yet because it’s hooked into the logfile and has to wait for something to appear there. The follow-up batches do then end up on the redirect server.&lt;/p&gt;

&lt;p&gt;The response to these requests, however, is identical on the primary server and on the redirect server: 403 Forbidden for everything that’s known, 404 for everything that isn’t.&lt;/p&gt;

&lt;p&gt;The access log of the redirect server is very tidy. Which is why a single command is also quite handy for distilling out new rules.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cat /var/log/apache2/c0d0s0.org-access.log-blocked | grep &quot;404&quot; | cut -d &quot; &quot; -f 7 | sort | uniq -c | sort -n
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Anything that’s a 403 is already covered by rules. Anything that returns 200 is intentional. Anything that’s a 404 isn’t covered by the rules. It’s relatively easy to define rules from the rest.&lt;/p&gt;

&lt;p&gt;Strictly speaking, this expansion isn’t necessary. From what I’ve observed, while individual requests may not be caught by the ruleset, scan runs almost always contain at least one pattern that is caught. And that’s enough to trigger the redirect. So not full coverage, but enough tripwires that one of them fires during typical scanner runs.&lt;/p&gt;

&lt;h2 id=&quot;new-version&quot;&gt;New version&lt;/h2&gt;

&lt;p&gt;I’ve uploaded a new version of the setup. You can find it with instructions &lt;a href=&quot;https://codeberg.org/c0t0d0s0/flyswatter/src/tag/v0.03&quot;&gt;here&lt;/a&gt;. The warning still applies that this is mostly vibecoded. So: caveat usor!&lt;sup id=&quot;fnref:3&quot;&gt;&lt;a href=&quot;https://www.c0t0d0s0.org/blog/aftertheban.html#fn:3&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;6&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;h2 id=&quot;closing-remark&quot;&gt;Closing remark&lt;/h2&gt;

&lt;p&gt;I think this will be the last blog entry on this topic, because the experimenting — both with the idea itself and with vibecoding — has run its course. I’m still working through my conclusions on both fronts. But that will take a while before it makes it into its own post. That said, if a new idea on this topic hits me on the toilet or in the kitchen, I’ll definitely post about the implementation here.&lt;/p&gt;

&lt;hr /&gt;

&lt;div class=&quot;footnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:4&quot;&gt;
      &lt;p&gt;In my opinion, “order of magnitude” is misused about as often as “quantum leap”. &lt;a href=&quot;https://www.c0t0d0s0.org/blog/aftertheban.html#fnref:4&quot; class=&quot;reversefootnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:6&quot;&gt;
      &lt;p&gt;I will call this server  “primary server”. &lt;a href=&quot;https://www.c0t0d0s0.org/blog/aftertheban.html#fnref:6&quot; class=&quot;reversefootnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:5&quot;&gt;
      &lt;p&gt;I call it “redirect server” from now on. &lt;a href=&quot;https://www.c0t0d0s0.org/blog/aftertheban.html#fnref:5&quot; class=&quot;reversefootnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:1&quot;&gt;
      &lt;p&gt;Yes, that was a typo. I just never bothered to fix it. &lt;a href=&quot;https://www.c0t0d0s0.org/blog/aftertheban.html#fnref:1&quot; class=&quot;reversefootnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:2&quot;&gt;
      &lt;p&gt;I still need to restructure these so the ruleset isn’t duplicated. Later …. &lt;a href=&quot;https://www.c0t0d0s0.org/blog/aftertheban.html#fnref:2&quot; class=&quot;reversefootnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:3&quot;&gt;
      &lt;p&gt;“Caveat utilitor” is probably more correct (my Latin is so far back that there were still native speakers around), but “caveat usor” sounds better … &lt;a href=&quot;https://www.c0t0d0s0.org/blog/aftertheban.html#fnref:3&quot; class=&quot;reversefootnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</content:encoded>
<pubDate>Mon, 27 Apr 2026 15:21:00 +0000</pubDate>
<link>https://www.c0t0d0s0.org/blog/aftertheban.html</link>
<guid isPermaLink="true">https://www.c0t0d0s0.org/blog/aftertheban.html</guid>
      
<dc:creator>c0t0d0s0</dc:creator>
      
      
<category>Blog</category>
      
<category>Security</category>
      
<category>Jekyll</category>
      
<category>Apache</category>
      
<category>English</category>
      
      
</item>
    
    
    
    
    
    
    
    
    
<item>
<title>New release schedule for Solaris 11.4</title>
<description>Solaris 11.4 moves from three to two SRUs per quarter</description>
<content:encoded>&lt;p&gt;I would like to draw your attention to a blog post by Joost Pronk van Hoogeveen: &lt;a href=&quot;https://blogs.oracle.com/solaris/announcing-a-new-simplified-support-repository-update-sru-schedule-for-oracle-solaris-11-4-and-zfs-storage-appliance-software&quot;&gt;“Announcing a new simplified Support Repository Update (&lt;abbr title=&quot;Support Repository Update&quot;&gt;SRU&lt;/abbr&gt;) schedule for Oracle Solaris 11.4 and &lt;abbr title=&quot;Zettabyte File System&quot;&gt;ZFS&lt;/abbr&gt; Storage Appliance software”&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Up to now, Solaris 11&lt;sup id=&quot;fnref:2&quot;&gt;&lt;a href=&quot;https://www.c0t0d0s0.org/blog/new-s114-release-schedule.html#fn:2&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; had three &lt;abbr title=&quot;Support Repository Update&quot;&gt;SRU&lt;/abbr&gt; releases per quarter: one in the “Critical Patch Update (CPU)” timeframe containing mostly security fixes&lt;sup id=&quot;fnref:3&quot;&gt;&lt;a href=&quot;https://www.c0t0d0s0.org/blog/new-s114-release-schedule.html#fn:3&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;, one with all the new features&lt;sup id=&quot;fnref:3:1&quot;&gt;&lt;a href=&quot;https://www.c0t0d0s0.org/blog/new-s114-release-schedule.html#fn:3&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;, and one in between with regular bug fixes only. So a new &lt;abbr title=&quot;Support Repository Update&quot;&gt;SRU&lt;/abbr&gt; was released every 4 weeks.&lt;/p&gt;

&lt;p&gt;In the future there will be two releases per quarter, one with the security fixes&lt;sup id=&quot;fnref:3:2&quot;&gt;&lt;a href=&quot;https://www.c0t0d0s0.org/blog/new-s114-release-schedule.html#fn:3&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; and one with the new features&lt;sup id=&quot;fnref:3:3&quot;&gt;&lt;a href=&quot;https://www.c0t0d0s0.org/blog/new-s114-release-schedule.html#fn:3&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;. The cadence of new SRUs will be 6 weeks.&lt;/p&gt;

&lt;p&gt;If you go from CPU to CPU&lt;sup id=&quot;fnref:1&quot;&gt;&lt;a href=&quot;https://www.c0t0d0s0.org/blog/new-s114-release-schedule.html#fn:1&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;, nothing changes. There is one every quarter as before.&lt;/p&gt;

&lt;hr /&gt;

&lt;div class=&quot;footnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:2&quot;&gt;
      &lt;p&gt;And the &lt;abbr title=&quot;Zettabyte File System&quot;&gt;ZFS&lt;/abbr&gt; Storage Appliance Software (albeit under a diffent name) &lt;a href=&quot;https://www.c0t0d0s0.org/blog/new-s114-release-schedule.html#fnref:2&quot; class=&quot;reversefootnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:3&quot;&gt;
      &lt;p&gt;… and normal fixes &lt;a href=&quot;https://www.c0t0d0s0.org/blog/new-s114-release-schedule.html#fnref:3&quot; class=&quot;reversefootnote&quot;&gt;&amp;#8617;&lt;/a&gt; &lt;a href=&quot;https://www.c0t0d0s0.org/blog/new-s114-release-schedule.html#fnref:3:1&quot; class=&quot;reversefootnote&quot;&gt;&amp;#8617;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;https://www.c0t0d0s0.org/blog/new-s114-release-schedule.html#fnref:3:2&quot; class=&quot;reversefootnote&quot;&gt;&amp;#8617;&lt;sup&gt;3&lt;/sup&gt;&lt;/a&gt; &lt;a href=&quot;https://www.c0t0d0s0.org/blog/new-s114-release-schedule.html#fnref:3:3&quot; class=&quot;reversefootnote&quot;&gt;&amp;#8617;&lt;sup&gt;4&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:1&quot;&gt;
      &lt;p&gt;Which is in many cases my practical recommendation for production systems. That said, i’m a big fan of “Patch early, Patch often”. &lt;a href=&quot;https://www.c0t0d0s0.org/blog/new-s114-release-schedule.html#fnref:1&quot; class=&quot;reversefootnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</content:encoded>
<pubDate>Sun, 26 Apr 2026 12:30:00 +0000</pubDate>
<link>https://www.c0t0d0s0.org/blog/new-s114-release-schedule.html</link>
<guid isPermaLink="true">https://www.c0t0d0s0.org/blog/new-s114-release-schedule.html</guid>
      
<dc:creator>c0t0d0s0</dc:creator>
      
      
<category>Solaris</category>
      
<category>SRU</category>
      
<category>Updates</category>
      
<category>English</category>
      
<category>ZFS</category>
      
      
</item>
    
    
    
    
    
    
    
    
    
<item>
<title>Java 17 available on SPARC</title>
<description>New Java version for Solaris/SPARC</description>
<content:encoded>&lt;p&gt;A new Java release has been published by &lt;a href=&quot;https://www.oracle.com/java/technologies/downloads/#java17-solaris&quot;&gt;Oracle&lt;/a&gt;: the Java SE Development Kit 17.0.19 is available for download, giving you a more current version of the language. Please read the installation notes regarding certain features that are missing from this &lt;a href=&quot;https://docs.oracle.com/en/java/javase/17/install/installation-jdk-oracle-solaris.html&quot;&gt;release&lt;/a&gt;.&lt;/p&gt;
</content:encoded>
<pubDate>Thu, 23 Apr 2026 20:00:00 +0000</pubDate>
<link>https://www.c0t0d0s0.org/blog/java17sparc.html</link>
<guid isPermaLink="true">https://www.c0t0d0s0.org/blog/java17sparc.html</guid>
      
<dc:creator>c0t0d0s0</dc:creator>
      
      
<category>Solaris</category>
      
<category>Java</category>
      
<category>English</category>
      
      
</item>
    
    
    
    
    
    
    
    
    
<item>
<title>Schroedingers Honeypot revisited</title>
<description>Now with even more honey!</description>
<content:encoded>&lt;p&gt;In &lt;a href=&quot;https://www.c0t0d0s0.org/blog/flyswatter.html?&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Fly swatter&lt;/code&gt;&lt;/a&gt;, I wrote that the first implementation of &lt;a href=&quot;https://www.c0t0d0s0.org/blog/schroedingershoneypot.html&quot;&gt;‘Schrödinger’s Honeypot’&lt;/a&gt; carried an inherent potential for Denial of Service, since it blocked the IP address for a whole day on every single violation.&lt;/p&gt;

&lt;h2 id=&quot;denial-of-service&quot;&gt;Denial-of-Service&lt;/h2&gt;

&lt;p&gt;Over the following evenings, I gave some thought to how this problem could be addressed: How do you solve the dilemma of keeping scanners off your back on one side, while keeping as few legitimate users away as possible on the other? And, taking it one step further: how do you prevent someone from making a sport out of abusing the detection component to block access for everyone else behind a NAT gateway? It’s relatively easy to figure out what the detection component is looking for — just try a few well-known scanner patterns. When the lights go out, you’ve found a pattern.&lt;/p&gt;

&lt;h2 id=&quot;why&quot;&gt;Why?&lt;/h2&gt;

&lt;p&gt;Before I go on, let me answer one question up front. Do I actually need all this? Probably not. I went down this path out of curiosity. My current suspicion is that, given the traffic volume my blog has, the mechanism I’m presenting here consumes more resources than the webserver itself. The complexity is probably grotesque. After all, it just serves static pages. Sendfile on incoming requests to port 80. Just about the simplest thing a server on the internet can do. But with dynamic sites, the picture might already look different. What actually bugged me was that my Apache log file was full of scan attempts. For me, all of this is primarily a hypothesis-tester. I had a few hypotheses about this scanner crowd before I implemented this, and I’ve picked up a few more from the process, which I’m in turn now testing.&lt;/p&gt;

&lt;p&gt;I haven’t finished implementing everything yet. I’ll say up front: large parts of this were written for me by an LLM based on a mile-long specification. I couldn’t have gotten this prototype up and running this quickly on my own. I haven’t done the exact math, but in the time since the idea for Schrödinger’s Honeypot came to me, I wouldn’t have been able to type it all out by hand.&lt;sup id=&quot;fnref:1&quot;&gt;&lt;a href=&quot;https://www.c0t0d0s0.org/blog/schroedingerrevisited.html#fn:1&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; To put it mildly, the process of building this was at times like backing up a car with a trailer. Look away for a second and everything has veered off in the wrong direction.&lt;sup id=&quot;fnref:3&quot;&gt;&lt;a href=&quot;https://www.c0t0d0s0.org/blog/schroedingerrevisited.html#fn:3&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; This was exhausting.&lt;/p&gt;

&lt;p&gt;The reason I let myself in for this is that without it I wouldn’t have had the time to build it, but above all because this mechanism has no interface of its own facing the outside. Everything that flows in from the outside goes through Apache. It consumes log files. Aside from that, it makes &lt;abbr title=&quot;Domain Name System&quot;&gt;DNS&lt;/abbr&gt; and HTTP requests to a service. I think the risk is manageable, at least for now and at least for me.&lt;/p&gt;

&lt;h2 id=&quot;architecture&quot;&gt;Architecture&lt;/h2&gt;

&lt;p&gt;Essentially, the mechanism consists of a series of daemons that supply data and in some cases pre-process it, plus one daemon that takes action based on that data.&lt;/p&gt;

&lt;p&gt;The daemons listen exclusively on 127.0.0.1. I didn’t want to bolt &lt;abbr title=&quot;Transport Layer Security&quot;&gt;TLS&lt;/abbr&gt; onto this. It’s a hypothesis tester, after all. And all you can query through these daemons is information — “how often have you seen this IP in the honeypot over the last 24 hours?”, “is this IP in an autonomous system worth blocking?” If an attacker can reach these ports, I’ve got a very different problem on my hands.&lt;/p&gt;

&lt;p&gt;There’s no detection logic in these daemons; that’s handled by Apache itself through the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mod_rewrite&lt;/code&gt; rules. Those rules determine what ends up going through this mechanism. It writes a log file that contains only accesses matching those rules. I’ve put a selection of these rules in &lt;a href=&quot;https://codeberg.org/c0t0d0s0/flyswatter/src/tag/v0.01/apache-config&quot;&gt;a file&lt;/a&gt; derived from what I was seeing in my logfiles. As long as I’m still actively tweaking them, I’m keeping them in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.htaccess&lt;/code&gt; so I can change them easily. Eventually, for performance reasons, they should move into the Apache configuration itself to avoid constant re-parsing.&lt;/p&gt;

&lt;p&gt;In the end it turned into quite a Rube Goldberg machine. If anyone wants to blame the LLM for the wonky architecture: nope… that was me.&lt;/p&gt;

&lt;h2 id=&quot;approach&quot;&gt;Approach&lt;/h2&gt;

&lt;p&gt;Back to the underlying question: How high is the collateral damage when I hand out a ban? Maybe the IP is a VPN gateway, a NAT device, maybe even CGNAT. Behind that IP there might not be just one user, but many. Only one of them scanned, but a ban locks all of them out of the blog for a day.&lt;/p&gt;

&lt;p&gt;My first reaction to this was to block offenders for only 120 seconds. That stopped the really coarse mass scans, but most scanners came right back, others after an hour or so.&lt;/p&gt;

&lt;p&gt;Something in between was needed. Not the orbital anvil, but not the flyswatter either. Maybe an orbital socket wrench.&lt;sup id=&quot;fnref:2&quot;&gt;&lt;a href=&quot;https://www.c0t0d0s0.org/blog/schroedingerrevisited.html#fn:2&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Rule zero is: you have to misbehave to come into contact with any of this. A regular user making proper use of the system isn’t looking for WordPress artifacts or web shells. It’s impossible for a legitimate user to trigger this mechanism. Nobody types in a scanner pattern by accident.&lt;/p&gt;

&lt;p&gt;The first rule is probably the simplest: if you scan again within the 24 hours following a block, you get another 24 hours to wait. Until you’ve learned. FlySwatter decides this on its own. It’s the shortest path from the honeypot into the firewall rules.&lt;/p&gt;

&lt;p&gt;Okay, the second rule checks whether the IP is a known cloud IP. The hypothesis is: it’s considerably more likely that someone — legitimately or not — has obtained the credentials of a cloud OS instance and is scanning from there, than that this is a NAT exit node, when I’m seeing clear signs of a scan coming from it. The risk of collateral damage is much lower than the probability of a scanner. This was the first test I implemented, which is also why my implementation lives under &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/opt/cloudcheck&lt;/code&gt;. If you come from a cloud, it’s off to the quiet corner for a day. The first collateral-damage problem already shows up here: VPN providers put their endpoints in exactly such clouds.&lt;/p&gt;

&lt;p&gt;The third check works on the basis of the IP’s ASN. First, the responsible ASN is determined for the IP. That happens via Team Cymru’s IP-to-ASN mapping service. The result is checked against the Spamhaus DROP list — which I refresh regularly in the background — and against a local list. That local list contains ASNs like those of Hetzner, OVH, or Contabo. Contabo servers have proven to be repeat offenders in my logfiles. If this check decides you’re trouble, that also means a one-day block. Only the specific IP that misbehaved gets blocked. The whole thing isn’t meant as collective punishment. With those providers, you get a server with an IP number, and if you use one of those servers to scan, it’s pretty unlikely that regular users would also be coming through it. With one exception: What does worry me here are egress points of VPN providers hosted with these kinds of providers.&lt;/p&gt;

&lt;p&gt;The fourth rule queries an external resource for the first time: AbuseIPDB. If you’re flagged as “bad” there with a certain confidence, that results in a one-day ban. It’s deliberately placed at the end. The number of requests to AbuseIPDB is limited in the free tier, and later on depends on how much you pay. The mechanism is meant to keep as many requests away from AbuseIPDB as possible.&lt;/p&gt;

&lt;p&gt;The rule ordering follows a simple logic: from cheap to expensive. Rule 0 is a by-product of the web server, Rule 1 queries an internal in-memory state table, Rule 2 queries daemons that access a local dataset, Rule 3 makes a &lt;abbr title=&quot;Domain Name System&quot;&gt;DNS&lt;/abbr&gt; query against Cymru’s offering (if it’s not cached) and cross-references it with the regularly refreshed DROP list and a manually maintained dataset of my own, and with Rule 4 we finally reach a service that is resource-limited.&lt;/p&gt;

&lt;p&gt;And even though the current implementation works the other way around, the whole mechanism is actually thought from the opposite direction. I started from “I see you in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;honeypot.log&lt;/code&gt;, you’re out!” In essence, I started to look for reasons &lt;em&gt;not&lt;/em&gt; to do it. “You’re from a cloud, I’m blocking you for that” also means “you’re not from a cloud, I’m not blocking you for that. But maybe for other reasons.” Blocking everything with a reputation score of 80 or higher also means not blocking things on the basis of the reputation rule if the reputation is lower than 80%.&lt;/p&gt;

&lt;h2 id=&quot;massive-retaliation&quot;&gt;Massive Retaliation&lt;/h2&gt;

&lt;p&gt;There’s another feature too: Group Bans. I’ve observed that some scanners don’t come from a single IP; instead an entire subnet lights up, with each individual IP sending only one or two requests. When I spot that in my log files, an entry goes into a groupban list that blocks whole groups of IPs at once. The first request to the honeypot then prevents 254 others (or more) from scanning. I have a rule to close off several subnets at once, because they tend to come all at the same time. They live, for instance, at a bulletproof hoster that doesn’t care where traffic comes from. These are manually added bans where I’m reasonably sure that no normal users are moving behind them. I can also use this group feature to hand out long-term bans or permanent bans.&lt;/p&gt;

&lt;h2 id=&quot;why-403-and-filter&quot;&gt;Why 403 and filter?&lt;/h2&gt;

&lt;p&gt;Why both, actually? Why either one at all? Since I don’t have the resources of a WordPress on my box, every scan would just hit a 404. That said, the 404 page on my site — as I explained elsewhere — is pretty heavyweight, because it also hosts a Lunr-based search function with a pre-generated index. If a user arrives at my blog via a dead link, they should be able to check whether comparable information exists elsewhere. But even that barely causes any damage. It just fills up the log file. So why didn’t I just leave it at a 404?&lt;/p&gt;

&lt;p&gt;Schrödinger’s Honeypot cheats in my implementation. A 404 would give the attacker the information that something is not there. A 403 communicates that something is forbidden. The difference is significant, because a 403 doesn’t reveal whether the thing exists. So the superposition, as far as the attacker is concerned, doesn’t just always collapse into a ban — they never had any chance of collapsing it into the other position either, because over there a different superposition is posing a different question: “present and forbidden = WordPress” or “not present and forbidden = no WordPress”.&lt;/p&gt;

&lt;p&gt;Why the IP filtering on top? My ruleset only blocks patterns I know about, and in the same scan run it would hand over the requested information (there or not there) for unknown patterns. By blocking at the firewall level, unknown requests don’t even get far enough to reveal any information. Unless, by chance, they use a pattern unknown to me first, they don’t even get to learn whether that pattern is familiar to me (they could, of course, infer that it’s unknown to me from the 404 I would send, if it’s not specifically blocked by the rules).&lt;/p&gt;

&lt;h2 id=&quot;data-protection&quot;&gt;Data Protection&lt;/h2&gt;

&lt;p&gt;There are several points in this solution where data protection questions come into play. In my opinion, there’s no better way to develop a migraine than to ponder the implications of data protection rules as a layperson. It might be different for lawyers. They probably look forward to it with delight, given its potential for “billable hours.”&lt;/p&gt;

&lt;p&gt;My system queries AbuseIPDB about IP addresses, asking what reputation the IP has. Now, an IP address is explicitly PII under GDPR. I take the position that I have a legitimate interest in finding out the reputation of an IP that is engaged in non-intended-use behaviour (any only if).&lt;/p&gt;

&lt;p&gt;And one question that preoccupies me: can an automated system whose purpose is to collect information about me itself have rights under GDPR? Especially considering that GDPR talks about &lt;em&gt;natural persons&lt;/em&gt;? I understand that both regulations protect the person behind the IP, but is the person behind the IP of a VM running an automated process a natural person — namely, the one who owns the VM — or is it the company (and thus not a natural person) whose infrastructure hosts the VM? I don’t know. But then again, I’m not a lawyer.&lt;/p&gt;

&lt;p&gt;But I have a more direct problem. One of my ideas that hasn’t been implemented yet needs the IP address of every request that hits my blog in order to collect successful requests of the last 24 hours. I’ve solved this problem as follows. I have a log file on disk where the IP address is pseudonymized by replacing the last octet with a 0. There is a second log area that carries a non-anonymized set of data. It’s considerably reduced — time, IP address, and result code (that’s all) — and this is piped to a daemon that keeps its information exclusively in main memory. This introduced two intentional problems for me: a restart means data loss, which in turn means that the function’s usefulness is limited for the next few hours.&lt;/p&gt;

&lt;p&gt;More importantly, I can’t query information with the anonymized IP address. In the current implementation I need the exact IP. I could of course try all 254 IP addresses and thus de-pseudonymize a pseudonymized address, but that only works if (first) only one IP in that /24 subnet is the single user in question. If, within the entire 1.2.3.0/24 subnet, only one IP address (say 1.2.3.4) has 200-result-code data recorded for it, then it’s clear from whom the retrieval in the access log came, even though it was pseudonymized to 1.2.3.0. On one hand, GDPR does permit storing PII on the basis of legitimate interest; on the other hand, you really have to go out of your way to pull this off, and it only works under specific conditions. Full pseudonymization kicks in after 24 hours, in a way I don’t have to do anything for. I haven’t yet found a way to achieve immediate anonymization.&lt;/p&gt;

&lt;p&gt;And if you want a really migraine-worthy question: is creating a firewall rule for a single IP itself a storage of PII?&lt;/p&gt;

&lt;p&gt;(Side note of 23.04.2026: I solved the problem by turning the problem on the head, but more on that in a future blog entry)&lt;/p&gt;

&lt;h2 id=&quot;building-your-own&quot;&gt;Building Your Own&lt;/h2&gt;

&lt;p&gt;You can build this mechanism yourself. It would be pointless, though, to write the instructions into this blog post. You’ll find all the components along with instructions in my Codeberg repository.&lt;/p&gt;

&lt;p&gt;The Flyswatter for Apache is available at &lt;a href=&quot;https://codeberg.org/c0t0d0s0/flyswatter/src/tag/v0.01/flyswatter&quot;&gt;https://codeberg.org/c0t0d0s0/flyswatter/src/tag/v0.01/flyswatter&lt;/a&gt;. The daemons Flyswatter needs for its work are available at &lt;a href=&quot;https://codeberg.org/c0t0d0s0/flyswatter/src/tag/v0.01/cloudcheck-suite&quot;&gt;https://codeberg.org/c0t0d0s0/flyswatter/src/tag/v0.01/cloudcheck-suite&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Should you build it on your own? Probably not.&lt;/p&gt;

&lt;p&gt;One comment: The ASN check still uses the term &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BADASN&lt;/code&gt;. That’s an artifact from the time when only the DROP list was being compared against and so such ASNs were bad. At some point I added a second list, which I can populate manually, to flag names or ASNs as worth a day’s ban. I didn’t remove the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BADASN&lt;/code&gt; term. A cosmetic issue. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BADASN&lt;/code&gt; can simply mean “is from a cloud provider”. Pushing a reason through here that could also stand for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLOUDASN&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DROPASN&lt;/code&gt; is already on my list to integrate.&lt;/p&gt;

&lt;h1 id=&quot;ideas-not-yet-implemented&quot;&gt;Ideas Not Yet Implemented&lt;/h1&gt;

&lt;p&gt;There are some statistics I’m already maintaining, but not yet using, because I want to gather numbers first.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Number of prebans in the last 24 hours. The idea is: if you’ve run into a preban, say, 19 times in the last 24 hours, you get a full day’s time-out on the twentieth.&lt;/li&gt;
  &lt;li&gt;There’s another daemon that counts successful access attempts over a longer period. The assumption is: if I see many 200s from one IP, then the probability is high that there is more than one user behind that IP. In that case, the time-out is only 120 seconds — maybe I’ll extend that to 5 minutes when I implement it.&lt;/li&gt;
  &lt;li&gt;It’s also being counted how often you stepped into the honeypot in the last 24 hours and how often you ran into the firewall block.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For some of these ideas there are already prototypes that haven’t yet made their way into the codebase, because they still need some fine-tuning before they’re even remotely usable. And I need much more data to know if those ideas are somewhat sensible.&lt;/p&gt;

&lt;h2 id=&quot;weakness&quot;&gt;Weakness&lt;/h2&gt;

&lt;p&gt;This is important if you really want to use this: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;honeypotd&lt;/code&gt; has a weakness. It also looks for requests containing a particular string in it. If that string is included, the IP of that request is considered &lt;em&gt;your&lt;/em&gt; IP. The consequence in Flyswatter is that no firewall rule gets set. You can essentially use this to switch off the main function of this entire contraption. I therefore suggest treating the string like a password and keeping it equally complex. Important: it appears, of course, in log files, in the config file, and gets transmitted across the network. By the requests, but as a referrer as well. Maybe it’s not a bad idea, once everything works, to unset the string.&lt;/p&gt;

&lt;p&gt;I’m aware that the magic-string hack I used to implement a half-dry-run of the logic is a security hole. If you know the string, you can bypass the whole mechanism. I built it this way because I don’t have a static IP and it made testing easier for me. I use a browser plugin that appends it to every request (and only those) for https://www.c0t0d0s0.org/. Actually, that plugin has a different job: I use it to filter my own requests out of the log files.&lt;/p&gt;

&lt;p&gt;So why did I still build it this way? Because I locked myself out a few times. Because it was something that was already there.&lt;/p&gt;

&lt;p&gt;There are some protections in place: First of all, the default is for the function to be switched off. There is no default. If the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SELF_MARKER&lt;/code&gt; parameter isn’t set, then the “remember my own IP” function is disabled and there is no IP address that would only trigger a dry run. Second: What’s the possible damage? The requests in question aren’t just logged, they’re also answered with a 403. So more will be written to the log, and if other, unknown probes follow the initial known ones, those won’t be blocked. The damage is manageable, at least initially. There is no compromise of the security goals confidentiality and integrity, and with high probability — in my environment — also not of the goal “availability”. Second: in my environment there’s only one risk that the secret might leak to the outside, namely when someone follows an external link. In that case, the magic would appear in the Referer. Otherwise, the blog is HTTPS-encrypted, so the magic is invisible in-transit thanks to &lt;abbr title=&quot;Transport Layer Security&quot;&gt;TLS&lt;/abbr&gt; encryption. And anyone with access to the log file can probably just switch off the whole kit and caboodle in my configuration anyway.&lt;/p&gt;

&lt;p&gt;Otherwise, if you really want to use what I’ve built here (which please only do if you’ve examined the code yourself and found it fit for purpose), I would, after successful tests where you didn’t accidentally always block yourself, simply remove the magic completely. Then there’s no way left to switch off the test. That’s how I handle it now that I no longer need to test whether the reactive mechanisms work. If I need it again, I can switch it back on and restart the corresponding daemon.&lt;/p&gt;

&lt;h1 id=&quot;assessment&quot;&gt;Assessment&lt;/h1&gt;

&lt;ol&gt;
  &lt;li&gt;The collateral damage problem isn’t finally solved in the current version, but it seems solvable to a large extend. Empirically, I can say that an IP that gets blocked has almost never shown significant 200s in the last 23 hours. That’s only a weak signal given my blog, though. I don’t have millions of hits. On the other hand, a peculiarity of my readership helps me out. I have well over 1000 subscribers, which was a very surprising number for me given how little has been happening on the blog for so long. These subscribers query the blog regularly and automatically, producing a steady stream of successful requests. If an attacker and a feed reader are behind the same IP, the attack is accompanied by a certain number of 200 requests. That can be used to mitigate the VPN egress problem.&lt;/li&gt;
  &lt;li&gt;Here too, a signal you get for free — the feed-reader heartbeat — is being put to use. The feed readers, as the most loyal audience, produce their own heartbeat. Yes, sure, that could be exploited in turn.&lt;/li&gt;
  &lt;li&gt;All of this could still be built with a sufficiently well-implemented Fail2Ban script. But I’d have had to build the helper daemons there too, so that the scripts in Fail2Ban would have something to lean on.&lt;/li&gt;
&lt;/ol&gt;

&lt;h1 id=&quot;observations&quot;&gt;Observations&lt;/h1&gt;

&lt;p&gt;I’ve been collecting data for a few days now with increasing levels of detail. Pseudonymized. But sufficient to draw some conclusions. However, this is all data for one day — the day before yesterday (21 April 2026). It’s a private blog with relatively little readership. The conclusions are therefore to be taken with a big grain of salt.&lt;/p&gt;

&lt;p&gt;A few things I noticed:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Yesterday, 572 honey pot requests came through in 24 hours.&lt;/li&gt;
  &lt;li&gt;They came from 427 IP addresses.
    &lt;ul&gt;
      &lt;li&gt;That fits with the observation that scan runs are terminated fairly quickly.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;For me, scanning is primarily an IPv4 phenomenon.
    &lt;ul&gt;
      &lt;li&gt;11 IP addresses on the honeypot are IPv6.&lt;/li&gt;
      &lt;li&gt;416 IP addresses are IPv4.&lt;/li&gt;
      &lt;li&gt;I still have to invest some time into improving the handling of IPv6.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;This resulted in a total stock of 149 entries in the firewall, which are now being blocked.&lt;/li&gt;
  &lt;li&gt;In total, the firewall rules have rejected 6913 connection attempts in that time. Scanners rarely let themselves be deterred. Even when their connection attempts are slapped away with an RST, they keep trying again.&lt;/li&gt;
  &lt;li&gt;Almost never is a scan attempt preceded by significant amounts of traffic with result code 200. That allows two conclusions:
    &lt;ul&gt;
      &lt;li&gt;The collateral damage may have been small so far.&lt;/li&gt;
      &lt;li&gt;For the scanners encountered so far, they don’t seem to use shared IPs. But to be honest, the data is insufficient for this conclusion to be more than anecdotal.&lt;/li&gt;
      &lt;li&gt;The idea of checking, on a ban, whether legitimate traffic preceded the scan attempt could prove useful to avoid collateral damage.&lt;/li&gt;
      &lt;li&gt;I still need to come up with something to prevent a scanner from simply switching off the protection by, say, requesting &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/&lt;/code&gt; before the scan attempt. Perhaps a useful hypothesis here is that a mass scanner isn’t going to simulate the picture of a legitimate user for several hours in advance just to come back later and scan. At least not for a small target like my blog.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;I see relatively many scanners from an AS in Vietnam. Those, however, are all one-shot scanners. One request is made and the IP doesn’t show up again. At least not during the short measurement interval this article is based on.&lt;/li&gt;
  &lt;li&gt;For the main goal of this project I need much more data. I think I will need a few weeks worth of data, to test the hypotheses I mentioned before.&lt;/li&gt;
  &lt;li&gt;First data from a new mechanism, not described in this post (it will be in a future blog entry) suggests a near-zero false positive rate of this mechanism. However this is currently just anecdotal based on one day of data and could prove totally wrong.&lt;/li&gt;
  &lt;li&gt;In 1200 runs of the mechanism with the fully implemented AbuseIP daemon in place, the mechanism was skipped in roughly a third of the cases. However this is not the full picture. The AbuseIP-Daemon caches and I didn’t put that information into my debug lines. Thus it’s entirely possible that further requests didn’t hit the service as it was answered by the cache. This could happen if a scanner isn’t blocked for a day, but just for two minutes, and appears multiple times of the day.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;outlook-and-postscript&quot;&gt;Outlook and Postscript&lt;/h2&gt;

&lt;p&gt;I’ve significantly expanded the concept and the script over the past few days. I may have found a solution for the PII problem that would allow breaking through pseudonymization for 24h. But all of that still needs more testing over the coming days. I will write about this in the next few days.&lt;/p&gt;

&lt;hr /&gt;

&lt;div class=&quot;footnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:1&quot;&gt;
      &lt;p&gt;An idea that is so obvious that I’m sure it isn’t new. &lt;a href=&quot;https://www.c0t0d0s0.org/blog/schroedingerrevisited.html#fnref:1&quot; class=&quot;reversefootnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:3&quot;&gt;
      &lt;p&gt;Without Trailer Assist. I simply despair at backing up with a trailer without this feature. It’s easier for me to unhitch and just push the silly thing. Since I only have a small trailer for the occasional trip to the garden waste disposal (vulgo: “Klaufix”), that works… &lt;a href=&quot;https://www.c0t0d0s0.org/blog/schroedingerrevisited.html#fnref:3&quot; class=&quot;reversefootnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:2&quot;&gt;
      &lt;p&gt;And yes, I’m aware that this would probably burn up on the way to Earth. Let’s just assume a socket wrench made of unobtanium. &lt;a href=&quot;https://www.c0t0d0s0.org/blog/schroedingerrevisited.html#fnref:2&quot; class=&quot;reversefootnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</content:encoded>
<pubDate>Thu, 23 Apr 2026 19:03:00 +0000</pubDate>
<link>https://www.c0t0d0s0.org/blog/schroedingerrevisited.html</link>
<guid isPermaLink="true">https://www.c0t0d0s0.org/blog/schroedingerrevisited.html</guid>
      
<dc:creator>c0t0d0s0</dc:creator>
      
      
<category>Blog</category>
      
<category>Security</category>
      
<category>Jekyll</category>
      
<category>Apache</category>
      
<category>English</category>
      
      
</item>
    
    
    
    
    
    
    
    
    
<item>
<title>Fly swatter</title>
<description>New approach to block potentially malicious requests with less collateral damage</description>
<content:encoded>&lt;p&gt;Sometimes you have a good idea, implement it, and only afterwards realize that this idea has a denial-of-service attack built into it. And you ask yourself: Hmm, really a good idea?&lt;/p&gt;

&lt;p&gt;I then moved away from the &lt;a href=&quot;https://www.c0t0d0s0.org/blog/schroedingershoneypot.html&quot;&gt;old solution&lt;/a&gt; and thought about what I could continue to use and what I needed to redesign.&lt;/p&gt;

&lt;p&gt;The detection side remained intact. Requests matching known questionable patterns still trigger the reaction component. However, the reaction component is now built quite differently.&lt;/p&gt;

&lt;p&gt;For the first iteration, I kept the reaction component very simple&lt;sup id=&quot;fnref:3&quot;&gt;&lt;a href=&quot;https://www.c0t0d0s0.org/blog/flyswatter.html#fn:3&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;The source IP is blocked in the firewall for 120 seconds. It’s a rule with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reject with tcp reset&lt;/code&gt; and not &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;drop&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;I remove the connection from the connection tracking via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;conntrack&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;I terminate the connection via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ss&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first step prevents new connections for 120 seconds, while the other two steps actively sever the connection. A false positive no longer leads to blocking access for an entire day. The scanner should know that I don’t want it here.&lt;/p&gt;

&lt;p&gt;Why do I use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reject with tcp reset&lt;/code&gt;? People often use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;drop&lt;/code&gt; to conceal that something is there. But the scanners already know the web server exists — I don’t need to hide it from them anymore. And I’ll probably lose the race for resources anyway. So I convey the message that there’s a mechanism at work here that’s keeping an eye on them. “Listen, I just told you that you’re not getting through here, because I know what you’re doing.”&lt;/p&gt;

&lt;p&gt;I consider tarpitting to be out of date. It worked when there was a balance of resources. Resources in the cloud paid for with stolen credit card information or started with stolen cloud credentials mean that this balance no longer exists. The other side has more and no financial constraints, because they’re probably not paying for it. In some cases they even set themselves up “parasitically” on free services. I’ve found some scans originating from GitHub Actions.&lt;/p&gt;

&lt;p&gt;Although I’m not sure whether there’s any intelligence at work here. When I look at the patterns, an attempt is made at 12:00 to find out whether I have a WordPress blog, and the same thing again at 15:00. The probability that this has changed in the last 3 hours and I’ve suddenly switched to WordPress is low. I can’t tell from my log files that this is being taken into account.&lt;sup id=&quot;fnref:1&quot;&gt;&lt;a href=&quot;https://www.c0t0d0s0.org/blog/flyswatter.html#fn:1&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Of course, I could nail down all access paths so that hardly any scanner gets through. But this is a public blog. As such, by definition, it has to be accessible. The price of accessibility is the massive influx of requests that my web server configuration sends into 403 territory. My goal is not to prevent this access entirely, just to thin it out without taking on too high a risk of false positives or collateral damage.&lt;/p&gt;

&lt;p&gt;And that’s exactly what the mechanism does. I’ve named the tool accordingly: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;apache-flyswatter&lt;/code&gt;. I can’t keep the flies out entirely if I want someone to come into the room, but if I see one, I can swat it.&lt;/p&gt;

&lt;h2 id=&quot;how-does-this-work-in-practice&quot;&gt;How does this work in practice?&lt;/h2&gt;

&lt;p&gt;Now I’ve talked a lot about what I did. I’d like to briefly explain how I set it up.&lt;/p&gt;

&lt;p&gt;The detection side works as it did yesterday. I still use rewrite rules that “tag” certain requests with an environment variable.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;RewriteCond %{REQUEST_URI} wlwmanifest\.xml$ [NC]
RewriteRule .* - [E=honeypot:1,F,L]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I use conditional logging here so that the tagged requests end up in the corresponding additional log files.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;SetEnvIf REDIRECT_honeypot 1 honeypot
SetEnvIf Remote_Addr &quot;^203\.0\.113\.1$&quot; !honeypot
SetEnvIf Remote_Addr &quot;^127\.&quot; !honeypot
SetEnvIf Remote_Addr &quot;^::1$&quot; !honeypot
LogFormat &quot;%h %{%Y-%m-%dT%H:%M:%S%z}t \&quot;%r\&quot; %&amp;gt;s \&quot;%{User-Agent}i\&quot;&quot; honeypot
CustomLog /var/log/apache2/honeypot.log honeypot env=honeypot
LogFormat &quot;%t %a %{remote}p %A %{local}p&quot; connlog
CustomLog /var/log/apache2/honeypot_connections.log connlog env=honeypot
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I’m again working with the hypothesis that every access that lands in this log file is malicious, or at least questionable. There is no valid reason why these requests would be legitimate under intended use.&lt;/p&gt;

&lt;p&gt;I have two log files. The first log file, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/var/log/apache2/honeypot.log&lt;/code&gt;, was used by the old mechanism. It’s no longer used by any tool. I’ve kept it so I can manually check what kind of requests land there.&lt;/p&gt;

&lt;p&gt;I would strongly recommend keeping it. You can also use the timestamp in the normal log to find out what kind of request it was. But if you have &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tail -f&lt;/code&gt; running on both &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/var/log/apache2/honeypot.log&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/var/log/apache2/honeypot_connections.log&lt;/code&gt; in parallel, it’s significantly easier to quickly establish the correlation.&lt;/p&gt;

&lt;p&gt;The log file I’m working with now contains different information:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Timestamp&lt;/li&gt;
  &lt;li&gt;Source IP&lt;/li&gt;
  &lt;li&gt;Source port&lt;/li&gt;
  &lt;li&gt;Destination IP&lt;/li&gt;
  &lt;li&gt;Destination port&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The new mechanism doesn’t need more information than that. This is the data the script needs to carry out the three steps.&lt;/p&gt;

&lt;p&gt;You need three components:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The &lt;a href=&quot;https://codeberg.org/c0t0d0s0/blogtools/src/branch/main/flyswatter/apache-flyswatter.conf&quot;&gt;config file&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;the &lt;a href=&quot;https://codeberg.org/c0t0d0s0/blogtools/src/branch/main/flyswatter/apache-flyswatter.service&quot;&gt;systemd unit&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;the &lt;a href=&quot;https://codeberg.org/c0t0d0s0/blogtools/src/branch/main/flyswatter/apache-flyswatter&quot;&gt;script&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Download the three components to your system and install them:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;install -m 0755 apache-flyswatter /usr/local/bin/apache-flyswatter
install -m 0644 apache-flyswatter.conf /etc/apache-flyswatter.conf
install -m 0644 apache-flyswatter.service /etc/systemd/system/apache-flyswatter.service
apt install nftables conntrack iproute2
systemctl daemon-reload
systemctl enable --now apache-flyswatter
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you already have the packages installed, that step isn’t necessary, of course. With &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;journalctl -u apache-flyswatter -f&lt;/code&gt; you should now see the script doing its work.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;root@v2202604350965450827:~# journalctl -u apache-flyswatter -f
Apr 18 20:00:26 v2202604350965450827 apache-flyswatter[2853]: 2026-04-18 20:00:26,868 INFO SWAT 203.0.113.254:23562 -&amp;gt; 159.195.145.249:443
Apr 18 20:05:31 v2202604350965450827 apache-flyswatter[2853]: 2026-04-18 20:05:31,861 INFO SWAT 203.0.113.254:42408 -&amp;gt; 159.195.145.249:443
Apr 18 20:16:52 v2202604350965450827 apache-flyswatter[2853]: 2026-04-18 20:16:52,293 INFO SWAT 203.0.113.254:50682 -&amp;gt; 159.195.145.249:443
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nft list ruleset&lt;/code&gt;, you’ll get the following output:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;table inet flyswatter {
 set blocked4 {
  type ipv4_addr
  flags timeout
  elements = { aaa.bbb.ccc.ddd timeout 2m expires 1m30s900ms }
 }

 set blocked6 {
  type ipv6_addr
  flags timeout
 }

 chain input {
  type filter hook input priority filter - 10; policy accept;
  ip saddr @blocked4 tcp dport { 80, 443 } reject with tcp reset
  ip6 saddr @blocked6 tcp dport { 80, 443 } reject with tcp reset
 }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I’d like to draw your attention to the element in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;blocked4&lt;/code&gt; set. You can see that it’s an element with a timeout. The entries clean themselves up — nftables removes them automatically after the timeout expires. I don’t have to go around mopping up after the rules.&lt;/p&gt;

&lt;h1 id=&quot;observations&quot;&gt;Observations&lt;/h1&gt;

&lt;p&gt;After running the script for a few hours now, the following became apparent:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;They come back. The scanners won’t be deterred.&lt;/li&gt;
  &lt;li&gt;120 seconds is too short to largely relieve the log files of scanner noise. You still frequently find scanning activity, but it gets shut down quickly&lt;sup id=&quot;fnref:2&quot;&gt;&lt;a href=&quot;https://www.c0t0d0s0.org/blog/flyswatter.html#fn:2&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;. However, in the config file you can set a significantly longer ban time. Possibly even the 24h used in yesterday’s solution.&lt;/li&gt;
  &lt;li&gt;What’s missing are scan runs that fire off 200-300 requests within a short time. The mechanism seems to set limits on such activity.&lt;/li&gt;
  &lt;li&gt;With some bots I’ve noticed that they appear to start their scan run from scratch again. After a certain time, the same request is executed again from the same IP address. As if they were working through a list, and after I’ve cut the connection, they start over from the beginning.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;postscript&quot;&gt;Postscript&lt;/h1&gt;

&lt;p&gt;I probably could have implemented all of this in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fail2ban&lt;/code&gt; with nearly equivalent functionality. But I’m currently trying to find ways to assess IP addresses in terms of how high their risk of collateral damage is, because multiple users sit behind them. Such a mechanism would allow me to more safely increase the ban time significantly. I already have a few ideas. It’s easier for me to implement such logic in my own script. I’ll be reporting on this in the coming days.&lt;/p&gt;

&lt;div class=&quot;footnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:3&quot;&gt;
      &lt;p&gt;This blog entry is a work-in-progress report. By the time you read it, the reaction component is probably a little more sophisticated than what’s described here. &lt;a href=&quot;https://www.c0t0d0s0.org/blog/flyswatter.html#fnref:3&quot; class=&quot;reversefootnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:1&quot;&gt;
      &lt;p&gt;Maybe you really do need LLMs to avoid doing mass vulnerability scanning in the dumbest possible way. That would still be a script kiddie, but a script kiddie on steroids and with a sugar rush. &lt;a href=&quot;https://www.c0t0d0s0.org/blog/flyswatter.html#fnref:1&quot; class=&quot;reversefootnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:2&quot;&gt;
      &lt;p&gt;Provided they use patterns that are caught by the detection component. &lt;a href=&quot;https://www.c0t0d0s0.org/blog/flyswatter.html#fnref:2&quot; class=&quot;reversefootnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</content:encoded>
<pubDate>Mon, 20 Apr 2026 06:45:00 +0000</pubDate>
<link>https://www.c0t0d0s0.org/blog/flyswatter.html</link>
<guid isPermaLink="true">https://www.c0t0d0s0.org/blog/flyswatter.html</guid>
      
<dc:creator>c0t0d0s0</dc:creator>
      
      
<category>Blog</category>
      
<category>Security</category>
      
<category>Jekyll</category>
      
<category>Apache</category>
      
<category>English</category>
      
      
</item>
    
    
    
    
    
    
    
    
    
<item>
<title>Schroedingers Honeypot</title>
<description>How to use Fail2Ban to block potentially malicious requests and still reduce logging of PII</description>
<content:encoded>&lt;p&gt;I’ve moved my website back to a self-managed server. c0t0d0s0.org now runs at &lt;a href=&quot;https://www.netcup.com&quot;&gt;netcup&lt;/a&gt;&lt;sup id=&quot;fnref:1&quot;&gt;&lt;a href=&quot;https://www.c0t0d0s0.org/blog/schroedingershoneypot.html#fn:1&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;. I was just getting increasingly annoyed at not being able to do anything administratively about the various things I was observing in my logfile. For example, to cut down the background noise in the Apache logfile by blocking accesses on an IP basis.&lt;/p&gt;

&lt;p&gt;One of the problems was the anonymisation of the logfile. I knew that I was getting thousands of requests hitting WordPress components at times. But I haven’t used WordPress at all for almost 20 years. I switched to s9y afterwards and used it for years. And with the migration to Jekyll in 2021, there wasn’t even a single &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;php&lt;/code&gt; file necessary for the operation of the blog. So I knew for sure all those WordPress &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.php&lt;/code&gt; accesses weren’t kosher.&lt;/p&gt;

&lt;p&gt;However, thanks to the anonymisation in the Strato logfiles there was little I could do about it, because I only had a very imprecise idea of where they were coming from. That made it impossible to implement firewall blocks&lt;sup id=&quot;fnref:2&quot;&gt;&lt;a href=&quot;https://www.c0t0d0s0.org/blog/schroedingershoneypot.html#fn:2&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; or aggressive Apache access controls. The collateral damage would have been too large. The bot operators have also become too clever by now to offer any other distinguishing feature by which you could reliably identify and block them.&lt;/p&gt;

&lt;p&gt;A dilemma arose. I don’t want to collect any PII, but IP addresses count as PII. So my new configuration on my own webserver would also have to anonymise the IP addresses.&lt;/p&gt;

&lt;p&gt;At the same time, though, I wanted to be able to surgically block a single IP address at the network level. But for that I need the complete, non-anonymised IP address.&lt;/p&gt;

&lt;p&gt;I have a solution that addresses both requirements. I redirect all that nonsense traffic aimed at WordPress components&lt;sup id=&quot;fnref:4&quot;&gt;&lt;a href=&quot;https://www.c0t0d0s0.org/blog/schroedingershoneypot.html#fn:4&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;3&lt;/a&gt;&lt;/sup&gt; to a 403 anyway. Let me show you the part of my &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.htaccess&lt;/code&gt; that handles all the WordPress scans. I could of course also let the requests run into a 404, but in my case that’s a page styled like the rest of the blog, which would generate a lot of follow-up traffic. Suboptimal.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;RewriteCond %{REQUEST_URI} /wp-[a-z-]+\.php [NC]
RewriteRule .* - [E=honeypot:1,F,L]

RewriteCond %{REQUEST_URI} (wp-admin|wp-json|wp-signup|wp-cron) [NC]
RewriteRule .* - [E=honeypot:1,F,L]

RewriteCond %{REQUEST_URI} xmlrpc\.php$ [NC]
RewriteRule .* - [E=honeypot:1,F,L]

RewriteCond %{REQUEST_URI} wlwmanifest\.xml$ [NC]
RewriteRule .* - [E=honeypot:1,F,L]

RewriteCond %{REQUEST_URI} (wp-includes|wp-content) [NC]
RewriteRule .* - [E=honeypot:1,F,L]

RewriteCond %{REQUEST_URI} wp-config [NC]
RewriteRule .* - [E=honeypot:1,F,L]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I could surely fold the rules into a single one, but the attempt at doing so looked extremely messy and unmaintainable. If the multiple regexps ever cause problems down the road, I can still rebuild it then. Besides, I’m potentially saving a lot of requests from hitting the webserver daemon in the first place. I think I still come out ahead on net.&lt;/p&gt;

&lt;p&gt;The decisive bit here is the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[E=honeypot:1,F,L]&lt;/code&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;L&lt;/code&gt; to end the discussion with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mod_rewrite&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;F&lt;/code&gt; to throw a 403. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;E&lt;/code&gt; sets the environment variable &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;honeypot&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It’s Schroedinger’s honeypot, so to speak. Without checking, a scanner doesn’t know whether the file is there or not. The honeypot sits in a superposition. I might be running a WordPress blog — or I might not. The scanner has to look, and gets a 403.&lt;/p&gt;

&lt;p&gt;The interesting information for me is the fact that it asked at all: by doing so, the scanner has told me it’s a scanner, because there’s no other reason to be looking for WordPress artefacts on my Jekyll site.&lt;/p&gt;

&lt;p&gt;You’d actually expect the 403 to be taken as a signal: “I know what you’re doing. I’ve done something about it. Stop it. Now!” But the scanners keep trying over and over.&lt;/p&gt;

&lt;p&gt;So with those rules I’ve marked the requests that may not necessarily be malicious, but are certainly questionable. That’s something I can work with.&lt;/p&gt;

&lt;p&gt;In a second step, I use conditional logging.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    LogFormat &quot;%a %l %u %t \&quot;%r\&quot; %&amp;gt;s %O \&quot;%{Referer}i\&quot; \&quot;%{User-Agent}i\&quot;&quot; combined_anon
    CustomLog &quot;|/usr/local/bin/anonymise-log /var/log/apache2/c0d0s0.org-access.log&quot; combined_anon
    ErrorLog &quot;|/usr/local/bin/anonymise-errorlog /var/log/apache2/c0d0s0.org-error.log&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;These first three lines set up the normal logging. I pipe the log through an AWK script that anonymises the IP addresses. For IPv4 addresses, that simply means replacing the last octet with a zero. For IPv6, I anonymise down to /48.&lt;/p&gt;

&lt;p&gt;In the configuration fragments below you’ll find an IP address &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;203.0.113.1&lt;/code&gt;. That’s a placeholder for your own IP, the one you reach your own webserver from. In most cases this will be the external IP of your router.&lt;/p&gt;

&lt;p&gt;Please add the following lines to the VHost configuration of your webserver. You have to repeat this in the &lt;abbr title=&quot;Transport Layer Security&quot;&gt;TLS&lt;/abbr&gt; section of the VHost config.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    SetEnvIf REDIRECT_honeypot 1 honeypot
    SetEnvIf Remote_Addr &quot;^127\.&quot; !honeypot
    SetEnvIf Remote_Addr &quot;^::1$&quot; !honeypot
    SetEnvIf Remote_Addr &quot;^203\.0\.113\.1$&quot; !honeypot
    
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;and&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;    LogFormat &quot;%h %{%Y-%m-%dT%H:%M:%S%z}t \&quot;%r\&quot; %&amp;gt;s \&quot;%{User-Agent}i\&quot;&quot; honeypot
    CustomLog /var/log/apache2/honeypot.log honeypot env=honeypot
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;REDIRECT_&lt;/code&gt; in the construct &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SetEnvIf REDIRECT_honeypot 1 honeypot&lt;/code&gt; cost me a moment and some debugging.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;F&lt;/code&gt; flag in the rewrite rule doesn’t produce a direct 403 internally; instead it produces an internal redirect to the error document. During that internal redirect, Apache renames all environment variables. That’s how &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;honeypot&lt;/code&gt; turns into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;REDIRECT_honeypot&lt;/code&gt;. If you don’t notice this, you’ll spend hours trying &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SetEnvIf honeypot 1 honeypot&lt;/code&gt; and wonder with an increasingly furrowed brow and dwindling patience why nothing ever ends up in the log. This behaviour can be found in the &lt;a href=&quot;https://httpd.apache.org/docs/current/custom-error.html#variables&quot;&gt;documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Once the environment variable is set, a second log kicks in. And that one doesn’t anonymise the IP numbers.&lt;/p&gt;

&lt;p&gt;At this point I’d like to mention that you can protect yourself even further against locking yourself out.&lt;/p&gt;

&lt;p&gt;To do so, add the following line to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.htaccess&lt;/code&gt; right at the very top, after enabling the rewrite:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;RewriteCond %{REMOTE_ADDR} ^203\.0\.113\.1$ 
RewriteRule .* - [E=honeypot_whitelist:1]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Whatever else happens, this unsets the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;honeypot&lt;/code&gt; environment variable further down the line.&lt;/p&gt;

&lt;p&gt;In the VHost configuration you can then insert the following instead of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SetEnvIf&lt;/code&gt; block from before.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;SetEnvIf REDIRECT_honeypot 1 honeypot
SetEnvIf REDIRECT_honeypot_whitelist 1 !honeypot 
SetEnvIf honeypot_whitelist 1 !honeypot
SetEnvIf Remote_Addr &quot;^127\.&quot; !honeypot
SetEnvIf Remote_Addr &quot;^::1$&quot; !honeypot
SetEnvIf Remote_Addr &quot;^203\.0\.113\.1$&quot; !honeypot
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The charm of this optional configuration is that your own IP is also stored in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.htaccess&lt;/code&gt;. That file is often part of the deployment process. It’s easier to transfer along with the website via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rsync&lt;/code&gt; than to modify the Apache configuration on the webserver via &lt;abbr title=&quot;Secure Shell&quot;&gt;SSH&lt;/abbr&gt;.&lt;/p&gt;

&lt;p&gt;Now I can take this log and drop it in front of Fail2Ban’s feet. Configuring it can be dead simple. One time and you are out. If an IP address shows up in this logfile, I don’t have to give it any benefit of the doubt about maybe being a legitimate user. I know it isn’t.&lt;/p&gt;

&lt;p&gt;The Fail2Ban configuration therefore looks like this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# cat /etc/fail2ban/jail.d/apache-honeypot.conf

[apache-honeypot]
enabled   = true
filter    = apache-honeypot
logpath   = /var/log/apache2/honeypot.log
maxretry  = 1
findtime  = 60
bantime   = 86399
banaction = nftables-multiport
port      = http,https
protocol  = tcp

ignoreip  = 127.0.0.1/8 ::1 203.0.113.1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ignoreip&lt;/code&gt; line isn’t strictly necessary, but it’s a second&lt;sup id=&quot;fnref:5&quot;&gt;&lt;a href=&quot;https://www.c0t0d0s0.org/blog/schroedingershoneypot.html#fn:5&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;4&lt;/a&gt;&lt;/sup&gt; safety net in case I mess something up in the Apache config file. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fail2ban&lt;/code&gt; has sent me to the console too many times because I managed to lock myself out. I’m not taking any more chances with that.&lt;/p&gt;

&lt;p&gt;And then a filter rule that simply looks for the IP address at the beginning.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# cat /etc/fail2ban/filter.d/apache-honeypot.conf
[Definition]
failregex = ^&amp;lt;HOST&amp;gt;\s
ignoreregex =
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;With that, every IP address caught red-handed in a scan gets blocked. It can still happen that a scanner gets quite a few requests through. It can take a second until &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fail2ban&lt;/code&gt; has set the filter rule. Depending on how fast the scanner fires its requests, a handful of requests get through before the door slams shut and the “You shall not pass” is pronounced.&lt;/p&gt;

&lt;p&gt;There are ways to speed up the processing, but since the request itself is already stopped by the 403, I didn’t want to raise the complexity of the solution.&lt;/p&gt;

&lt;p&gt;Since then, my logfile has been a lot quieter. In return, though, the firewall configuration grows over time. At the time I’m writing this article, I have 39 entries after running this configuration for 3 hours. Let’s check with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nft list ruleset&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;	set addr-set-apache-honeypot {
		type ipv4_addr
		elements = { aaa.bbb.ccc.ddd, eee.fff.ggg.hhh,
			     [...]
			     qqq.xxx.yyy.zzz }
	}

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;For anyone wondering about the missing ports in that structure: They are defined in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;f2b-chain&lt;/code&gt; nftables chain.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;	chain f2b-chain {
		type filter hook input priority filter - 1; policy accept;
		[...]
		tcp dport { 80, 443 } ip saddr @addr-set-apache-honeypot reject with icmp port-unreachable
	}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The reason I use multiport here and not allport is that on top of belt and braces I also brought in double-sided tape on the waistband.&lt;sup id=&quot;fnref:3&quot;&gt;&lt;a href=&quot;https://www.c0t0d0s0.org/blog/schroedingershoneypot.html#fn:3&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot;&gt;5&lt;/a&gt;&lt;/sup&gt; Even if this mechanism accidentally blocks my own IP, only the webserver is affected. ssh stays available. For locking myself out of &lt;abbr title=&quot;Secure Shell&quot;&gt;SSH&lt;/abbr&gt;, I have another Fail2Ban configuration.&lt;/p&gt;

&lt;hr /&gt;

&lt;div class=&quot;footnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:1&quot;&gt;
      &lt;p&gt;There are people whose technical judgement I trust, and they trust netcup. So I decided to trust them as well. &lt;a href=&quot;https://www.c0t0d0s0.org/blog/schroedingershoneypot.html#fnref:1&quot; class=&quot;reversefootnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:2&quot;&gt;
      &lt;p&gt;Which I couldn’t have configured anyway, because I didn’t have admin access to the system. &lt;a href=&quot;https://www.c0t0d0s0.org/blog/schroedingershoneypot.html#fnref:2&quot; class=&quot;reversefootnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:4&quot;&gt;
      &lt;p&gt;requesting a lot of files like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.env&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.git&lt;/code&gt; &lt;a href=&quot;https://www.c0t0d0s0.org/blog/schroedingershoneypot.html#fnref:4&quot; class=&quot;reversefootnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:5&quot;&gt;
      &lt;p&gt;Or third … &lt;a href=&quot;https://www.c0t0d0s0.org/blog/schroedingershoneypot.html#fnref:5&quot; class=&quot;reversefootnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:3&quot;&gt;
      &lt;p&gt;In case you were wondering how Kylie Minogue’s costume in “Can’t Get You Out of My Head” stayed in place … it was explained to me: “double-sided tape”. &lt;a href=&quot;https://www.c0t0d0s0.org/blog/schroedingershoneypot.html#fnref:3&quot; class=&quot;reversefootnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</content:encoded>
<pubDate>Fri, 17 Apr 2026 19:03:00 +0000</pubDate>
<link>https://www.c0t0d0s0.org/blog/schroedingershoneypot.html</link>
<guid isPermaLink="true">https://www.c0t0d0s0.org/blog/schroedingershoneypot.html</guid>
      
<dc:creator>c0t0d0s0</dc:creator>
      
      
<category>Blog</category>
      
<category>Security</category>
      
<category>Jekyll</category>
      
<category>Apache</category>
      
<category>English</category>
      
      
</item>
    
    
</channel>
</rss>

