<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Ozelot's blog</title>
        <link>https://ozelot.fyi/</link>
        <description>undefined</description>
        <lastBuildDate>Mon, 17 Aug 2026 20:44:41 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Ozelot's blog</title>
            <url>https://ozelot.fyi/favicon.png</url>
            <link>https://ozelot.fyi/</link>
        </image>
        <copyright>Copyright 2026 - Ozelot</copyright>
        <item>
            <title><![CDATA[Weird DNS behavior on Alpine Linux - .local and mDNS]]></title>
            <link>https://ozelot.fyi/blog/weird-dns-behavior-on-alpine-linux/</link>
            <guid>https://ozelot.fyi/blog/weird-dns-behavior-on-alpine-linux/</guid>
            <pubDate>Sun, 11 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Learn why .local name resolution behaves differently on Alpine Linux and minimal environments.]]></description>
            <content:encoded><![CDATA[
                <p>You are viewing this post as a preview for RSS readers and it may be slightly different from the original version due to markdown formatting issues. <a href='https://ozelot.fyi/blog/weird-dns-behavior-on-alpine-linux/'>Click here</a> to see full post.</p> 
                <h2 id="intro">Intro</h2><p>Alpine Linux is popular for its simplicity and minimalism, especially in containers and lab environments. Most of the time, this works in your favor, until you run into name resolution issues that seem inconsistent or outright broken. One common example is resolving <code>.local</code> hostnames: DNS appears to work, but tools like <code>ping</code> or <code>curl</code> fail.</p><p>This post tries to explain why that happens, why it's expected behavior on Alpine, and how it differs from more <em>full-featured</em> Linux distributions.</p><h2 id="why-local-is-special-what-is-mdns-and-why-it-works-for-you">Why .local is special, what is mDNS <del>and why it works for you</del></h2><p>The <code>.local</code> top-level domain is reserved for Multicast DNS (mDNS) and <code>.local</code> names are meant to be resolved via multicast on the local network, instead of querying a unicast DNS server. This is defined in <a href="https://datatracker.ietf.org/doc/html/rfc6762" rel="nofollow">RFC 6762</a> and implemented by tools like Avahi and Bonjour.</p><p><a href="https://wiki.alpinelinux.org/wiki/MDNS" rel="nofollow">According to the Alpine documentation</a>, name resolution is handled by <strong>musl libc</strong>, which only supports classic DNS by default. mDNS isn't integrated into the resolver stack unless additional components are explicitly configured.</p><p>As a result, <code>.local</code> hostnames aren't resolved automatically, even though they may work on other distributions where mDNS is integrated transparently.</p><p>On glibc-based systems, <code>nss-mdns</code> bridges this gap by wiring mDNS into the system resolver. Alpine doesn't have that option, because musl just doesn't support NSS at all.</p><h2 id="why-tools-behave-differently">Why tools behave differently</h2><p>Tools like <code>dig</code> or <code>nslookup</code> send DNS queries directly to a DNS server, bypassing most resolver logic. If your DNS server has a <code>.local</code> zone configured, these tools will return valid answers.</p><p>Other tools, such as <code>ping</code> or <code>curl</code>, rely on the system resolver, which on Alpine doesn't integrate mDNS into name resolution, so lookups simply fail. The DNS server is never queried, which makes the behavior look confusing.</p><p>It's a resolver limitation by design.</p><h2 id="minimal-environments-make-this-more-obvious">Minimal environments make this more obvious</h2><p>This behavior isn't unique to Alpine and also appears in minimal environments like BusyBox-based or scratch Docker images. As described by <a href="https://nathanpeck.com/mdns-resolution-in-scratch-docker-containers/" rel="nofollow">Nathan Peck</a>, <code>.local</code> hostnames don't resolve inside such containers unless a full mDNS stack or a DNS forwarding workaround is added. Minimal images simply don't include the pieces needed for multicast name resolution.</p><p>Alpine fits into this category - small, efficient, and intentionally minimal. Cute, even, but won't hold your hand.</p><h2 id="key-points">Key points</h2><ul><li><code>.local</code> isn't a regular DNS domain,</li><li>Alpine Linux doesn't resolve <code>.local</code> via mDNS by default,</li><li><code>nslookup</code> working doesn't mean the system resolver will behave the same,</li><li>avoid using <code>.local</code> for custom DNS zones if possible; just use something else instead, such as <code>.test</code>, <code>.lab</code>, <code>.internal</code>, <code>.silly</code>, <code>.cute</code> or whatever you want.</li></ul><h2 id="conclusion">Conclusion</h2><p>This thing may look like broken DNS, but it's just Alpine's resolver behaving as intended.</p><p>Alpine is small and minimal by design, a bit like a fox - quiet, efficient, and not interested in doing more than necessary.</p>
                <p>For more articles, visit <a href="https://ozelot.fyi/">ozelot.fyi</a> or subscribe this RSS channel - <a href="https://ozelot.fyi/rss.xml">ozelot.fyi/rss.xml</a>.</p>
            ]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Why are foxes so special?]]></title>
            <link>https://ozelot.fyi/blog/why-are-foxes-so-special/</link>
            <guid>https://ozelot.fyi/blog/why-are-foxes-so-special/</guid>
            <pubDate>Sun, 21 Sep 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Learn why foxes are smarter than most algorithms and cuter than any commit history.]]></description>
            <content:encoded><![CDATA[
                <p>You are viewing this post as a preview for RSS readers and it may be slightly different from the original version due to markdown formatting issues. <a href='https://ozelot.fyi/blog/why-are-foxes-so-special/'>Click here</a> to see full post.</p> 
                <h2 id="intro">Intro</h2><p>Soft ears, fluffy tails, and mischievous eyes... sometimes the cutest things in life are also the slyest. Today, we're taking a break from the usual tech stuff, curling up like a fox tail around a warm cup of curiosity, and diving into why this little creature deserves a standing ovation - and maybe even a little spot in your next daydream.</p><p><img alt="Fox" src="/media/fox-4388014_1920.jpg" title="Fox"></img><small><a href="https://pixabay.com/photos/fox-red-fox-nature-animals-wild-4388014/" rel="nofollow">Image</a> by <a href="https://pixabay.com/users/leuchtpunkt-13009256/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=4388014" rel="nofollow">Andreas Neumann</a> from <a href="https://pixabay.com//?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=4388014" rel="nofollow">Pixabay</a></small></p><p>They glide silently through forests, leap lightly across snowy fields, and even around city streets, always alert, always curious. Young foxes tumble in play, chasing leaves, pouncing on shadows, learning the art of mischief. Adults move with elegant grace, balancing stealth and poise. Each flick of a tail, twitch of an ear, or tilt of a head is both functional and utterly charming. Urban foxes, despite human noise, keep their soft, wild poise.</p><p>They teach us patience, observation, and the beauty of subtle gestures. From their nose to the tip of their tail, foxes are amazing little animals. Even a quick look at them can feel like a playful wink. Watching them is like seeing a story come to life. They remind us that little magical moments can happen anytime, if we notice them.</p><h2 id="masters-of-wit-and-wisdom">Masters of Wit and Wisdom</h2><p>Foxes are smart hunters with quick minds. In the snow, they can hear rodents moving under leaves or ice and jump very accurately to catch them. They use tricks like pretending to run away to lure prey closer, or they pounce quietly. Urban foxes adjust to human schedules, avoid cars, and find hidden food, showing they can learn and adapt.</p><p><img alt="More fox" src="/media/pexels-jmhcarriger-13579970.jpg" title="More fox"></img><small><a href="https://www.pexels.com/photo/a-brown-and-gray-cub-lying-on-ground-13579970/" rel="nofollow">Photo</a> by Janice Carriger on <a href="https://pexels.com" rel="nofollow">Pexels</a></small></p><p>Foxes communicate using <a href="https://www.sunshinecoast.qld.gov.au/environment/education-resources-and-events/environment-resources-and-publications/pest-management-plant-and-animal/european-red-fox" rel="nofollow">over 40 sounds</a>: barks, screams, and chatters - each with a different meaning. They also use body language, tail movements, and ear positions to send signals. Foxes remember safe paths, hiding spots, and food locations, showing <a href="https://bioone.org/journals/wildlife-biology/volume-2020/issue-1/wlb.00602/Does-recursive-use-of-resource-locations-shape-a-home-range/10.2981/wlb.00602.full" rel="nofollow">spatial intelligence</a>.</p><p>They are patient, flexible, and opportunistic. Hunting combines timing, precision, and creativity. Every jump, sniff, and pounce shows planning and skill. Their intelligence is not just instinct. It involves observation, planning, and learning. Watching a fox hunt is like seeing a small, clever animal solving problems. Adorable, isn't it?</p><h2 id="fluff-that-steal-your-heart">Fluff that steal your heart</h2><p>Foxes have thick, warm, and fluffy fur. Their tails have multiple uses: keep the fox warm, help with balance while running, and are used to communicate with other foxes. Their cute paws are covered in fur, allowing quiet movement on snow or rough ground.</p><p>Foxes groom themselves regularly and often groom each other for social bonding. Young foxes start with thinner fur that thickens as they grow. Even ears and faces are covered in fur for warmth and sensory function. Urban foxes keep their coats in good condition despite city life.</p><p>Their tails, eyes, and ears also communicate their feelings and intentions. Seasonal shedding keeps them warm while preventing overheating. The fur is functional and also part of their appearance. It helps them move, stay safe, and interact with other foxes, and look cute.</p><p><img alt="Another fox" src="/media/david-gomez-VVJdmbvdx5E-unsplash.jpg" title="Another fox"></img><small>Photo by <a href="https://unsplash.com/@dcanadianphotographer?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash" rel="nofollow">David Gomez</a> on <a href="https://unsplash.com/photos/a-red-fox-sitting-on-the-ground-next-to-a-fence-VVJdmbvdx5E?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash" rel="nofollow">Unsplash</a></small></p><p>Foxes are small, but their bodies are very well designed. <a href="https://foxesworlds.com/fox-claws/" rel="nofollow">Retractable claws</a> let them walk quietly, jump precisely, and climb easily. Their ears move independently to catch even tiny sounds. Their eyes reflect light at night, giving them good night vision for hunting. Whiskers sense small changes in air, helping them move in tight spaces without bumping into things. Their snouts are long and sensitive, good for tracking prey or noticing danger. Tails are not just ultra fluffy - they help with balance, communication, and staying warm. Paws are soft but precise, letting them move silently on any surface.</p><p>Their body posture shows mood or intention without making noise. Urban foxes handle fences, stairs, and buildings easily. Every part of their body has a purpose. Paw pads, bones, and muscles are made for agility and stealth. Nature made foxes to be both survival experts and adorable little charmers. <small>(<a href="https://www.wildlifeonline.me.uk/animals/article/red-fox-senses" rel="nofollow">Wildlife Online</a>)</small></p><h2 id="tail-tales-and-body-language">Tail tales and body language</h2><p>Foxes communicate using gestures and body language - and their tails are very important. A curled tail can keep them warm or show they are happy. A flicking tail can mean excitement, irritation, or alertness. Their posture shows dominance, submission, curiosity, or readiness.</p><p>Ears, eyes, and snout positions add more information. Tails also help with balance during jumps. Curling the tail while sleeping keeps them warm. Twitching ears can warn of danger or show attention. Tail patterns can help identify different foxes. Small movements convey feelings or intentions. Foxes can communicate over distances without making noise. Urban foxes still use these signals even in cities. Their social intelligence works with hunting and survival skills. Every gesture shows thought, flexibility, and charm. <small>(<a href="https://www.wildlifeonline.me.uk/animals/article/red-fox-behaviour-fox-body-language" rel="nofollow">Wildlife Online</a>)</small></p><h2 id="adapting-to-humans">Adapting to humans</h2><p>Some foxes live in cities without losing their wild skills. Urban foxes move quietly through streets, parks, and alleys, always watching for humans and traffic. They learn human schedules to avoid danger and find food in trash, gardens, or leftover meals.</p><p><img alt="More fox" src="/media/fox-7519590_1920.jpg" title="More fox"></img><small><a href="https://pixabay.com/photos/fox-food-handfeeding-nature-7519590/" rel="nofollow">Image</a> by <a href="https://pixabay.com/users/dcanandianphotographer1-30577253/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=7519590" rel="nofollow">David Gomez</a> from <a href="https://pixabay.com//?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=7519590" rel="nofollow">Pixabay</a></small></p><p>Even near people, they keep natural behaviors - stopping, listening, and watching before moving forward. Young foxes learn by watching older foxes how to navigate the city. They use curiosity and caution to use city resources safely. Their movements stay agile, precise, and controlled. Being active at night helps them avoid humans and hunt quietly.</p><p>They keep sharp senses and hunting tricks, even in cities. Urban foxes are flexible with diet, timing, and social interaction. They still play, chase, and practice hunting skills. Watching them climb fences, squeeze through gaps, or jump over walls shows their skill. Urban foxes balance curiosity, caution, and stealth effectively. Each movement shows thought, skill, and awareness. <small>(<a href="https://www.theguardian.com/world/2023/aug/08/urban-foxes-no-more-cunning-than-rural-ones-researchers-find" rel="nofollow">The Guardian</a>) (<a href="https://denver.prelive.opencities.com/files/assets/public/v/1/parks-and-recreation/documents/fox-information.pdf" rel="nofollow">Denver Parks & Recreation</a>)</small></p><h2 id="foxes-in-culture-and-myth">Foxes in culture and myth</h2><p>The fox is known for sneaking into homes and getting away without being caught. Because of its cunning nature, this animal has acquired the image of trickster in a number of cultures. <small>(<a href="https://www.spiritanimal.info/fox-spirit-animal/" rel="nofollow">Spirit Animal</a>)</small></p><p>In European stories, the fox is seen as smart and tricky. It often outsmarts stronger animals or humans, teaching lessons along the way. People also link the fox to lying or sneaky behavior, and sometimes even to magic or supernatural powers. Its sharp senses and cleverness make it a symbol of guidance or hidden wisdom. Overall, the fox shows both cleverness and the risk of getting into trouble if you trust it too much. <small>(<a href="https://en.wikipedia.org/wiki/Foxes_in_popular_culture" rel="nofollow">Wikipedia</a>)</small></p><p>In Japan, foxes are called kitsune. Kitsune are special because they can change shape, play tricks on humans, or protect villages. They are also considered sacred animals in Shinto, the traditional Japanese religion, often linked to the god Inari, who is connected with rice and prosperity.</p><p>Some kitsune can grow up to nine tails. Each tail shows how old and powerful the fox is. Usually, a kitsune grows a new tail every 100 years, gaining more wisdom, magical skill, and strength with each tail. Kitsune can be playful, mischievous, or romantic, teaching lessons about cleverness, planning, and emotions. <small>(<a href="https://bokksu.com/blogs/news/kitsune-the-enigmatic-fox-of-japanese-folklore" rel="nofollow">Bokksu</a>)</small></p><p><img alt="Kitsune" src="/media/fox-8653812_1920.jpg" title="Kitsune"></img><small><a href="https://pixabay.com/illustrations/fox-kitsune-japanese-myth-8653812/" rel="nofollow">Image</a> by <a href="https://pixabay.com/users/crocerossina-39485415/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=8653812" rel="nofollow">Cathe</a> from <a href="https://pixabay.com//?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=8653812" rel="nofollow">Pixabay</a></small></p><p>Across cultures, foxes represent intelligence, strategy, and subtle power. Folklore mixes real fox behavior with imagination, creating magical, clever animals. Myths show that even small creatures can leave a big impact. Foxes continue to inspire creativity, storytelling, and wonder, proving that cunning, charm, and beauty often go together.</p><h2 id="misunderstood-and-hunted">Misunderstood and hunted</h2><p>Although most foxes cause no real harm, <strong>every year countless foxes are brutally killed by poachers (some call them hunters)</strong>. Hunting takes away their playful lives. Foxes can't roam or play as they wish. <small>(<a href="https://www.mdpi.com/2076-2615/14/11/1672" rel="nofollow">MDPI</a>)</small></p><blockquote><p>Imagine resting in your cozy den, ears twitching at every sound, when suddenly it's taken away, without knowing why. Or just wandering through the forest, when suddenly your paw tightens, then you realize you're trapped in a snare and you'll stay there for several hours before you bleed out... or maybe some idiot sets a pack of aggressive dogs on you and starts shooting while you run.</p></blockquote><p>Millions of foxes live on fur farms. They are kept in tiny cages and <strong>they are killed just for <del>fun</del> fur</strong>. These foxes never feel the joy of running in snow. They never feel the thrill of jumping. They never feel the comfort of wrapping their fluffy tails around themselves. Bright eyes, clever paws, and twitching ears are replaced with sadness. They suffer from wounds, eye infections, and deformed paws, and killing methods like <strong>rectal electric shock</strong> are commonly used. <small>(<a href="https://www.humaneworld.org/en/blog/undercover-investigation-fur-farm-cruelty" rel="nofollow">Humane World</a>)</small></p><blockquote><p>POV: You open your eyes, but all you see are wire bars. No forest, no snow, no space to run. Your paws press against cold metal, your tail has no room to curl around you. Days blur together in the cage - no play, no jumping, no joy. One day, rough hands take you out. Not for love, not for care, but only for your fur.</p></blockquote><p><img alt="One more fox" src="/media/pexels-brett-sayles-977951.jpg" title="One more fox"></img><small><a href="https://www.pexels.com/photo/close-up-photo-of-fox-977951/" rel="nofollow">Photo</a> by Brett Sayles on <a href="https://pexels.com" rel="nofollow">Pexels</a></small></p><p>Even urban foxes can be targeted for just existing. People forget foxes are smart and sensitive. They are playful, mischievous, and affectionate. Foxes deserve to curl up, chase, explore, and live freely. They should not suffer because of fear, greed, or misunderstanding. Watching a fox leap or flick its tail is magical. Every life lost takes some magic away. Foxes deserve care, love, and soft moments in the sun.</p><h2 id="why-we-cant-resist-them">Why we can't resist them?</h2><p>Foxes are like the ultimate combo of soft uwu-fluff and big-brain logic. They're smol hunters with 200 IQ, moving sleek and smooth like optimized Rust code. Every head tilt, ear twitch, or tail flick is basically an API call straight to your heart. They adapt everywhere - forests, snow, even urban jungles - without losing their wild kernel.</p><p>Watching a fox is like running a process full of cuteness threads: leap(), pounce(), sniff(). Each function call is pure joy, wrapped in mischief and charm. Even the tiniest gestures - twitchy ears, swishy tails, sparkly eyes - return values full of feels.</p><p><img alt="The last one" src="/media/fox-7637062_1920.jpg" title="The last one"></img><small><a href="https://pixabay.com/photos/fox-silver-fox-black-fox-red-fox-7637062/" rel="nofollow">Image</a> by <a href="https://pixabay.com/users/ambquinn-4464111/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=7637062" rel="nofollow">Angela</a> from <a href="https://pixabay.com//?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=7637062" rel="nofollow">Pixabay</a></small></p><p>Foxes are both real and mythic, like a program that compiles on the first try. They embody curiosity, elegance, and sly cleverness in one fluffy package. Their presence is magical, but also practical, like a well-written script that just works.</p><p>Every glance, jump, or sneaky move leaves logs in your memory you can't delete. They prove that intelligence + stealth + fluff = the ultimate soft fox energy uwu.</p><p>Don't you just wanna curl up next to one and let their vibe run in your background thread forever?</p>
                <p>For more articles, visit <a href="https://ozelot.fyi/">ozelot.fyi</a> or subscribe this RSS channel - <a href="https://ozelot.fyi/rss.xml">ozelot.fyi/rss.xml</a>.</p>
            ]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to set up Fail2ban with nftables and AbuseIPDB]]></title>
            <link>https://ozelot.fyi/blog/how-to-set-up-fail2ban-with-nftables-and-abuseipdb/</link>
            <guid>https://ozelot.fyi/blog/how-to-set-up-fail2ban-with-nftables-and-abuseipdb/</guid>
            <pubDate>Sun, 29 Jun 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Learn how to set up Fail2Ban with nftables and AbuseIPDB for reporting malicious IPs.]]></description>
            <content:encoded><![CDATA[
                <p>You are viewing this post as a preview for RSS readers and it may be slightly different from the original version due to markdown formatting issues. <a href='https://ozelot.fyi/blog/how-to-set-up-fail2ban-with-nftables-and-abuseipdb/'>Click here</a> to see full post.</p> 
                <h2 id="intro">Intro</h2><p>If you're running a Linux server, protecting it from brute-force attacks is one of the first things worth setting up - and Fail2ban is the usual answer.</p><p>There's one catch on modern Debian, though. Since Debian 12, iptables is no longer installed by default, and Fail2ban's default action expects it. Without a firewall backend to talk to, <strong>Fail2ban will not be able to block any IPs</strong> - it will be useless with default configuration.</p><p>You can check it yourself:</p><pre className="language-text" code="fail2ban-client set sshd banip 198.51.100.42<br>nft list ruleset | grep 198.51.100.42<br>fail2ban-client set sshd unbanip 198.51.100.42<br>" language="text"><code __ignoreMap="">fail2ban-client set sshd banip 198.51.100.42<br>nft list ruleset | grep 198.51.100.42<br>fail2ban-client set sshd unbanip 198.51.100.42<br></code></pre><p>If <code>nft</code> returns nothing, the ban never reached the firewall.</p><p>In this guide we'll set up Fail2ban with <strong>nftables</strong> - the framework Debian already<br>ships - and add automatic reporting of blocked IPs to <strong>AbuseIPDB</strong>.</p><h2 id="why-nftables-when-i-have-iptables">Why nftables when I have iptables?</h2><p>You have two ways out of this. Install iptables and keep the legacy setup, or point<br>Fail2ban at the firewall your system is already running.</p><p><strong>nftables</strong> has been the default framework since Debian 10. It unifies IPv4 and IPv6 filtering, replaces the four separate legacy tools with one, and sits underneath your system whether you use it directly or not. Installing iptables on top of it means adding a compatibility layer to work around a problem that doesn't need working around.</p><p>Of course, you can go ahead and install iptables, but in this guide we will focus on the cleaner and default choice for modern Debian.</p><h2 id="configuration">Configuration</h2><p>You have to install fail2ban. You can use apt or whatever:</p><pre className="language-text" code="apt install fail2ban<br>" language="text"><code __ignoreMap="">apt install fail2ban<br></code></pre><h3 id="set-up-jail">Set up jail</h3><p>You need to make some changes in <code>/etc/fail2ban/jail.conf</code>.</p><p>You can adjust <code>bantime</code>, <code>findtime</code>, and <code>maxretry</code> to your needs, or leave the default values, which are usually sufficient.</p><p>Then, find the <code>[sshd]</code> section. You have to make them look like this:</p><pre className="language-text" code="[sshd]<br><br>port = ssh<br>backend = systemd<br>action = nftables[name=SSH,port=ssh,protocol=tcp]<br>         abuseipdb[abuseipdb_category="18,22"]<br>" language="text"><code __ignoreMap="">[sshd]<br><br>port = ssh<br>backend = systemd<br>action = nftables[name=SSH,port=ssh,protocol=tcp]<br>         abuseipdb[abuseipdb_category="18,22"]<br></code></pre><p>Or if you don't want to use AbuseIPDB, leave the last line like this:</p><pre className="language-text" code="action = nftables[name=SSH,port=ssh,protocol=tcp]<br>" language="text"><code __ignoreMap="">action = nftables[name=SSH,port=ssh,protocol=tcp]<br></code></pre><p>Now fail2ban will use nftables to ban malicious addresses.</p><h3 id="set-up-abuseipdb">Set up AbuseIPDB</h3><p>Create or edit <code>/etc/fail2ban/action.d/abuseipdb.conf</code>:</p><pre className="language-text" code="[Definition]<br>actionban = lgm=$(printf '%%.1000s' "<matches>"); curl -sSf "https://api.abuseipdb.com/api/v2/report" \<br>  -H "Accept: application/json" \<br>  -H "Key: <abuseipdb_apikey>" \<br>  --data-urlencode "comment=$lgm" \<br>  --data-urlencode "ip=<ip>" \<br>  --data "categories=<abuseipdb_category>"<br><br>[Init]<br>abuseipdb_apikey = "your-abuseipdb-apikey-here"<br>" language="text"><code __ignoreMap="">[Definition]<br>actionban = lgm=$(printf '%%.1000s' "<matches>"); curl -sSf "https://api.abuseipdb.com/api/v2/report" \<br>  -H "Accept: application/json" \<br>  -H "Key: <abuseipdb_apikey>" \<br>  --data-urlencode "comment=$lgm" \<br>  --data-urlencode "ip=<ip>" \<br>  --data "categories=<abuseipdb_category>"<br><br>[Init]<br>abuseipdb_apikey = "your-abuseipdb-apikey-here"<br></code></pre><p>You can generate your API key after logging into your AbuseIPDB account: <code>AbuseIPDB -> User Account -> API</code>.</p><h3 id="other-services">Other services</h3><p>The <code>[sshd]</code> jail is just one example. The same approach works with any service that logs failed login attempts.</p><p>There's one catch if the service is behind a reverse proxy or Cloudflare: nftables only sees the proxy's IP, so the attacker's address never gets blocked. For that setup, I covered a different approach in <a href="/blog/how-to-set-up-fail2ban-for-portainer-via-cloudflare/">setting up Fail2ban for Portainer via Cloudflare</a>, where bans are handled by Cloudflare instead.</p><h3 id="test-your-configuration">Test your configuration</h3><p>Finally, restart Fail2ban:</p><pre className="language-text" code="systemctl restart fail2ban<br>" language="text"><code __ignoreMap="">systemctl restart fail2ban<br></code></pre><p>Then be sure to check the status:</p><pre className="language-text" code="systemctl status fail2ban<br>" language="text"><code __ignoreMap="">systemctl status fail2ban<br></code></pre><p>Check that the jail itself is running:</p><pre className="language-text" code="fail2ban-client status sshd<br>" language="text"><code __ignoreMap="">fail2ban-client status sshd<br></code></pre><p>And confirm that bans actually reach the firewall:</p><pre className="language-text" code="nft list ruleset | grep f2b<br>" language="text"><code __ignoreMap="">nft list ruleset | grep f2b<br></code></pre><p>If the Fail2ban chain is there, everything is wired up correctly. Fail2ban will block IPs that exceed allowed attempts via nftables, and report them to AbuseIPDB if configured.</p>
                <p>For more articles, visit <a href="https://ozelot.fyi/">ozelot.fyi</a> or subscribe this RSS channel - <a href="https://ozelot.fyi/rss.xml">ozelot.fyi/rss.xml</a>.</p>
            ]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to use multiple GitHub accounts on one machine]]></title>
            <link>https://ozelot.fyi/blog/how-to-use-multiple-github-accounts-on-one-machine/</link>
            <guid>https://ozelot.fyi/blog/how-to-use-multiple-github-accounts-on-one-machine/</guid>
            <pubDate>Thu, 05 Sep 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Learn how to configure Git for multiple profiles on a single machine, depending on the repository directory.]]></description>
            <content:encoded><![CDATA[
                <p>You are viewing this post as a preview for RSS readers and it may be slightly different from the original version due to markdown formatting issues. <a href='https://ozelot.fyi/blog/how-to-use-multiple-github-accounts-on-one-machine/'>Click here</a> to see full post.</p> 
                <h2 id="intro">Intro</h2><p>Managing multiple GitHub accounts on a single machine can be tricky. You may have one account for work and another for your <del>lewd</del> side projects, but you'd prefer to keep them entirely separate. Switching between accounts usually means setting Git configuration per every repository or updating credentials manually-tedious, right?</p><p>In this guide, I'll show you a better approach to managing multiple GitHub accounts using Git's configuration files, so you can seamlessly work across different repositories without manual changes. This tutorial focuses on Windows, but the method applies to Linux as well. I assume you have a <strong>basic understanding of Git</strong>.</p><h2 id="prepare-your-environment">Prepare your environment</h2><h3 id="install-git">Install Git</h3><p><a href="https://git-scm.com/downloads" rel="nofollow">Download Git</a> and install it. And if you already have Git, ensure it's in a fresh state or reconfigure it as necessary for this guide.</p><h3 id="create-directories">Create directories</h3><p>To keep your repositories organized and properly configured, you'll need separate directories for each account's repositories. I suggest something like this:</p><pre className="language-text" code="Projects<br>├── Personal<br>└── Work<br>" language="text"><code __ignoreMap="">Projects<br>├── Personal<br>└── Work<br></code></pre><p>Personal account repositories will be located in <code>Projects/Personal/</code> and work account repositories will be located in <code>Projects/Work/</code>. For this guide, we'll assume the <code>Projects</code> directory is created on the C drive (<code>C:\Projects</code>).</p><h2 id="set-up-git-config">Set up Git config</h2><p>As you probably know, Git uses a global configuration file called <code>.gitconfig</code> to store details like your username and email. We need separate configuration files for each account. Let's create two configuration files: <code>.gitconfig-personal</code> and <code>.gitconfig-work</code> in the <code>Projects</code> folder.</p><p>Your directory should now look like this:</p><pre className="language-text" code="Projects<br>├── Personal<br>├── Work<br>├── .gitconfig-personal<br>└── .gitconfig-work<br>" language="text"><code __ignoreMap="">Projects<br>├── Personal<br>├── Work<br>├── .gitconfig-personal<br>└── .gitconfig-work<br></code></pre><h3 id="sample-configurations">Sample configurations</h3><p>Then configure them accordingly according to your needs. Here's an example of how each config file might look:</p><p><strong>.gitconfig-personal</strong>:</p><pre className="language-text" code="[user]<br>    email = personal@example.com<br>    name = LewdPuppyUwU<br>[init]<br>    defaultBranch = main<br>[credential]<br>    username = lewdpuppyuwu # e.g. your github username<br>" language="text"><code __ignoreMap="">[user]<br>    email = personal@example.com<br>    name = LewdPuppyUwU<br>[init]<br>    defaultBranch = main<br>[credential]<br>    username = lewdpuppyuwu # e.g. your github username<br></code></pre><p><strong>.gitconfig-work</strong>:</p><pre className="language-text" code="[user]<br>    email = work@example.com<br>    name = John Doe<br>[init]<br>    defaultBranch = main<br>[credential]<br>    username = johndoe # e.g. your github username<br>" language="text"><code __ignoreMap="">[user]<br>    email = work@example.com<br>    name = John Doe<br>[init]<br>    defaultBranch = main<br>[credential]<br>    username = johndoe # e.g. your github username<br></code></pre><p>In the <span>credential</span> section, replace the username with your respective GitHub username or anything unique - it will be used to recognize your account on the local machine.</p><h3 id="modify-the-global-git-config">Modify the global Git config</h3><p>But that's not all. Next, we need to instruct Git to use these custom configuration files based on the directory where your repositories are located. This is done by modifying the main <code>.gitconfig</code> file, typically located at <code>%USERPROFILE%/.gitconfig</code> (on Windows). If this file doesn't exist, create it.</p><p>Here's the configuration of what your file should look like:</p><pre className="language-text" code="[includeIf "gitdir:C:/Projects/Personal/"]<br>    path = C:/Projects/.gitconfig-personal<br>    <br>[includeIf "gitdir:C:/Projects/Work/"]<br>    path = C:/Projects/.gitconfig-work<br>" language="text"><code __ignoreMap="">[includeIf "gitdir:C:/Projects/Personal/"]<br>    path = C:/Projects/.gitconfig-personal<br>    <br>[includeIf "gitdir:C:/Projects/Work/"]<br>    path = C:/Projects/.gitconfig-work<br></code></pre><p>Make sure to update the paths if you're using a different directory structure. This setup ensures that Git automatically switches between the personal and work configurations based on where the repository is located.</p><p>Such proper configuration will cause that for repositories located in <code>Projects/Personal/</code> a <code>.gitconfig-personal</code> will be used, which points to the personal account. For <code>Projects/Work/</code> similarly <code>.gitconfig-work</code> will be used.</p><h2 id="lets-test-it">Let's test it</h2><p>To test your configuration:</p><ol><li>Create a private repository on both GitHub accounts for testing purposes.</li><li>Go to <code>Projects/Personal/</code> directory and clone your personal repository.</li><li>When prompted, log in with your personal GitHub account. Once authenticated, you can commit and push changes as usual.</li><li>Now, repeat the process in the <code>Projects/Work/</code> directory for your work account.</li><li>Again, log in with your work account when prompted.</li></ol><p>Now you should be able to work with repositories under <code>Projects/Personal/</code> using your personal account and those under <code>Projects/Work/</code> using your work account without needing to switch credentials manually.</p><h2 id="conclusion">Conclusion</h2><p>Managing multiple GitHub accounts on a single machine doesn't have to be a hassle. By setting up separate Git configuration files for each account and organizing your repositories smartly, you can seamlessly switch between different profiles. Whether you're working on personal projects or company repositories, Git will always use the right credentials.</p>
                <p>For more articles, visit <a href="https://ozelot.fyi/">ozelot.fyi</a> or subscribe this RSS channel - <a href="https://ozelot.fyi/rss.xml">ozelot.fyi/rss.xml</a>.</p>
            ]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to use a GPG key for SSH authentication on Windows]]></title>
            <link>https://ozelot.fyi/blog/how-to-use-gpg-key-for-ssh-authentication/</link>
            <guid>https://ozelot.fyi/blog/how-to-use-gpg-key-for-ssh-authentication/</guid>
            <pubDate>Tue, 13 Jun 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Configure gpg-agent as your SSH agent on Windows with Gpg4win: export the key, set SSH_AUTH_SOCK, register the keygrip, and keep it running after reboot.]]></description>
            <content:encoded><![CDATA[
                <p>You are viewing this post as a preview for RSS readers and it may be slightly different from the original version due to markdown formatting issues. <a href='https://ozelot.fyi/blog/how-to-use-gpg-key-for-ssh-authentication/'>Click here</a> to see full post.</p> 
                <h2 id="intro">Intro</h2><p>You might already know about using public key authentication for SSH. But did you know that you can also use a GNU Privacy Guard (GPG) keypair for SSH authentication? In this tutorial, we'll look at configuring this on a Windows client. This guide assumes you already have a <strong>basic understanding of GPG and SSH</strong>.</p><p>Why bother, if a plain SSH key already works? You end up with <strong>one keypair for everything</strong> - signing commits, encrypting files and logging into servers - instead of a separate key sitting in <code>~/.ssh</code> for each purpose. And if your GPG key lives on a <strong>hardware token</strong> like a YubiKey or Nitrokey, the private key never leaves the device, so it can't be copied off your machine even if the machine is compromised.</p><h2 id="install-gpg4win">Install Gpg4win</h2><p><a href="https://www.gpg4win.org/download.html" rel="nofollow">Download Gpg4win</a> and install it. Then, create or import your key if you haven't already. Make sure that your key allows authorization or has a subkey that permits it.</p><h2 id="export-ssh-key-and-put-it-on-the-server">Export SSH key and put it on the server</h2><p>To export the SSH key from your GPG key, open the command line and run:</p><pre className="language-text" code="gpg --export-ssh-key KEY<br>" language="text"><code __ignoreMap="">gpg --export-ssh-key KEY<br></code></pre><p>Where <code>KEY</code> is your GPG key name, email or ID. You can find it with:</p><pre className="language-text" code="gpg --list-keys<br>" language="text"><code __ignoreMap="">gpg --list-keys<br></code></pre><p>The output should look something like this:</p><pre className="language-text" code="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDbDzdth1SOQ793iDV1bhSEabDXj52r4Q6bYpGo27a72VSaqzBEaL7mllL2qteIInWe4yHQxX5kwKxEsB4w657GIkWzTAMMu+tCCRf3EgiZawuICXSpCcCjWu4NjQm6i1N2wWu7LHyKHTKkmyz6F48ti0vWIGv3h78am9aCZSgWyXERPyov9VxVAFKZYJMxNpTpgDx4gCoqKYlTbUqEBYabsk5atpxtRxsS27vsEpaB1Ib2KKWmKhFpLdv0EAkKQrSZlJf15gexnt8o7agGKB2c+kdHhYWYkkTTpZPlaIRejDLFpB6JTFppmgaMaKrN65+dH7Q2FXasZp0Shret17IZ9xmPdRUELSPwDb4OceapjtvHBMB3Ot+NXnSMKFT6/mJa/TB+/nZ9h7gv9vy59wE+LLdNONicWxH9EZyCloIMfM9OVXWgDE+7St18dr4Pd9QIX8bNGWSpGnad5LPCFRV5RdHWfQL4Ag4P63X5qfxrqWFynrfzDlHiXV/hnBykO7yihVOy590wnTkSvsgFT6+6wdVztmX2BJfDOp4EnYDoNDXrKxG4muizHJ79CjvE9Fx+5hU9aCC4WwDsLRa1YNtLDfCJByOZWmcyldHfu21MMgZV6JdkmSj8v8ojQf1myx4rMPIEENNeurGK/iQ6ObJtp92pwOIRP6Zsxnm1WChxbQ== openpgp:0x900AF684<br>" language="text"><code __ignoreMap="">ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDbDzdth1SOQ793iDV1bhSEabDXj52r4Q6bYpGo27a72VSaqzBEaL7mllL2qteIInWe4yHQxX5kwKxEsB4w657GIkWzTAMMu+tCCRf3EgiZawuICXSpCcCjWu4NjQm6i1N2wWu7LHyKHTKkmyz6F48ti0vWIGv3h78am9aCZSgWyXERPyov9VxVAFKZYJMxNpTpgDx4gCoqKYlTbUqEBYabsk5atpxtRxsS27vsEpaB1Ib2KKWmKhFpLdv0EAkKQrSZlJf15gexnt8o7agGKB2c+kdHhYWYkkTTpZPlaIRejDLFpB6JTFppmgaMaKrN65+dH7Q2FXasZp0Shret17IZ9xmPdRUELSPwDb4OceapjtvHBMB3Ot+NXnSMKFT6/mJa/TB+/nZ9h7gv9vy59wE+LLdNONicWxH9EZyCloIMfM9OVXWgDE+7St18dr4Pd9QIX8bNGWSpGnad5LPCFRV5RdHWfQL4Ag4P63X5qfxrqWFynrfzDlHiXV/hnBykO7yihVOy590wnTkSvsgFT6+6wdVztmX2BJfDOp4EnYDoNDXrKxG4muizHJ79CjvE9Fx+5hU9aCC4WwDsLRa1YNtLDfCJByOZWmcyldHfu21MMgZV6JdkmSj8v8ojQf1myx4rMPIEENNeurGK/iQ6ObJtp92pwOIRP6Zsxnm1WChxbQ== openpgp:0x900AF684<br></code></pre><p>Then copy the entire output (including ssh-rsa and the openpgp:0xXXXXXXXX at the end) and add it to the <code>~/.ssh/authorized_keys</code> file on your server.</p><h2 id="set-up-client-configuration">Set up client configuration</h2><h3 id="make-sure-openssh-authentication-agent-is-disabled">Make sure OpenSSH Authentication Agent is disabled</h3><p>Open <code>services.msc</code>, find <strong>OpenSSH Authentication Agent</strong> and make sure it is turned off, if not then turn it off. If Windows ssh-agent is left running it will conflict with gpg-agent.</p><h3 id="create-environmental-variable">Create environmental variable</h3><p>Run <code>SystemPropertiesAdvanced</code> and go to <code>Environmental variables</code>. Next create new user variable:</p><p>Name: <code>SSH_AUTH_SOCK</code><br>Value: <code>//./pipe/openssh-ssh-agent</code></p><p>Then close both windows to save.</p><h3 id="set-up-gpg-agent-config">Set up gpg-agent config</h3><p>Go to <code>%appdata%\gnupg</code>, create file <code>gpg-agent.conf</code> with following content:</p><pre className="language-text" code="enable-ssh-support<br>enable-putty-support<br>enable-win32-openssh-support<br>" language="text"><code __ignoreMap="">enable-ssh-support<br>enable-putty-support<br>enable-win32-openssh-support<br></code></pre><h3 id="set-up-keygrip">Set up keygrip</h3><p>Execute:</p><pre className="language-text" code="gpg --list-keys --with-keygrip<br>" language="text"><code __ignoreMap="">gpg --list-keys --with-keygrip<br></code></pre><p>Copy the keygrip for your key, then go to <code>%appdata%\gnupg</code>, create file named <code>sshcontrol</code> and paste your keygrip in it. Leave one blank line at the end.</p><h2 id="prepare-for-run">Prepare for run</h2><p>Close all your command line/powershell tasks. Go to <strong>Task manager</strong> and kill app processes from GnuPG and <strong>ssh-agent.exe</strong>. Then open new terminal and run <code>gpg-connect-agent /bye</code>. Wait for it to turn on.</p><p>Next run <code>ssh-add -L</code>. If it returned your key, that means it's working.</p><p>Now you can login to your SSH server via <code>ssh user@example.com</code>. Instead of traditionally asking you for your password you will be asked to authenticate with a key (such as pinentry).</p><h3 id="make-gpg-agent-run-automatically">Make gpg-agent run automatically</h3><p>To avoid running <code>gpg-connect-agent /bye</code> after every system startup, add it to the autostart.</p><p>Go to <code>C:\Program Files (x86)\GnuPG\bin</code> (or where you have GnuPG installed), find <code>gpg-connect-agent.exe</code>, create a shortcut and move it to <code>%appdata%\Microsoft\Windows\Start Menu\Programs\Startup</code>.</p><h2 id="troubleshooting">Troubleshooting</h2><h3 id="ssh-add-l-returns-nothing">ssh-add -L returns nothing</h3><p>Almost always the <code>sshcontrol</code> file. Check that it contains the keygrip of a subkey with the <strong>authentication</strong> capability (<code>[A]</code> in <code>gpg --list-keys --with-keygrip</code>), not the signing or encryption one, and that the file ends with a blank line — without it the last entry is ignored.</p><p>If the file looks right, the agent is probably still running with the old configuration.<br>Kill it and start again:</p><pre className="language-text" code="gpgconf --kill gpg-agent<br>gpg-connect-agent /bye<br>" language="text"><code __ignoreMap="">gpgconf --kill gpg-agent<br>gpg-connect-agent /bye<br></code></pre><h3 id="sign_and_send_pubkey-signing-failed-agent-refused-operation">sign_and_send_pubkey: signing failed: agent refused operation</h3><p>The agent found the key but refused to use it. Usually the subkey has no authentication capability, or the keygrip in <code>sshcontrol</code> belongs to a different subkey. Verify with <code>gpg --list-keys --with-keygrip</code> and look for the line marked <code>[A]</code>.</p><h3 id="windows-ssh-agent-keeps-taking-over">Windows ssh-agent keeps taking over</h3><p>Two things to check. <strong>OpenSSH Authentication Agent</strong> in <code>services.msc</code> has to be stopped <em>and</em> set to disabled - otherwise Windows restarts it on the next boot. And <code>SSH_AUTH_SOCK</code> only applies to processes started after the variable was created, so close every terminal (and your IDE) and open a fresh one.</p><h3 id="pinentry-never-appears">pinentry never appears</h3><p>The agent was started from a different session than the one you're authenticating from (or isn't running at all). Kill it with <code>gpgconf --kill gpg-agent</code>, then run <code>gpg-connect-agent /bye</code> from the same terminal you use for SSH.</p>
                <p>For more articles, visit <a href="https://ozelot.fyi/">ozelot.fyi</a> or subscribe this RSS channel - <a href="https://ozelot.fyi/rss.xml">ozelot.fyi/rss.xml</a>.</p>
            ]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to set up Fail2ban for Portainer via Cloudflare]]></title>
            <link>https://ozelot.fyi/blog/how-to-set-up-fail2ban-for-portainer-via-cloudflare/</link>
            <guid>https://ozelot.fyi/blog/how-to-set-up-fail2ban-for-portainer-via-cloudflare/</guid>
            <pubDate>Thu, 27 Oct 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[Secure your Portainer instance with Fail2ban rule via Cloudflare. Don't let any pirate ships enter your port!]]></description>
            <content:encoded><![CDATA[
                <p>You are viewing this post as a preview for RSS readers and it may be slightly different from the original version due to markdown formatting issues. <a href='https://ozelot.fyi/blog/how-to-set-up-fail2ban-for-portainer-via-cloudflare/'>Click here</a> to see full post.</p> 
                <h2 id="what-is-portainer-and-why-you-need-to-ensure-its-security">What is Portainer and why you need to ensure its security</h2><p>Portainer is a tool that can manage Docker containers and images with GUI. It is a web application and can be exposed to the world, which, may raise doubts about its security. After all, with Portainer you can create, delete or modify containers and images.</p><h3 id="notice">Notice</h3><p>To be clear, you need to be aware of the danger posed by exposing such important services open to the world. The following guide <strong>will not make your instance completely safe</strong>, but it will reduce the risk of unauthorized access using dictonary attacks by bots.</p><h2 id="integration-between-portainer-and-fail2ban">Integration between Portainer and Fail2ban</h2><p>Portainer collects application logs, including information about unsuccessful authorization, but it does not contain the IP address from which the request was sent <a href="https://github.com/portainer/portainer/issues/4300" rel="nofollow">and does not look like it will change</a>, so it cannot be used. This is what it looks like:</p><pre className="language-text" code="time="2022-10-15T15:49:14Z" level=info msg="2022/10/15 15:49:14 http error: Invalid credentials (err=Unauthorized) (code=422)"<br>" language="text"><code __ignoreMap="">time="2022-10-15T15:49:14Z" level=info msg="2022/10/15 15:49:14 http error: Invalid credentials (err=Unauthorized) (code=422)"<br></code></pre><p>There is another solution. We assume that you are using Portainer Community Edition 2.15.1 exposed via Nginx reverse proxy and  you <a href="https://github.com/oasisfleeting/cloudflare-whitelister-nginx-real-ip" rel="nofollow">see real query IP</a> if using Cloudflare. So you can use <code>access.log</code> file instead. This is what the query fragment with incorrect login details looks like:</p><pre className="language-text" code="127.0.0.1 - - [26/Oct/2022:20:55:11 +0200] "POST /api/auth HTTP/2.0" 422 59 "https://portainer.example.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36"<br>" language="text"><code __ignoreMap="">127.0.0.1 - - [26/Oct/2022:20:55:11 +0200] "POST /api/auth HTTP/2.0" 422 59 "https://portainer.example.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36"<br></code></pre><p><code>127.0.0.1</code> is a real request IP, and <code>https://portainer.example.com/</code> is URL of your Portainer instance.</p><h2 id="configuration">Configuration</h2><p>Go to <code>/etc/fail2ban/filter.d</code> and create <code>portainer.conf</code> file. Paste the following content into it:</p><pre className="language-text" code="[Definition]<br>failregex = ^<HOST>.*POST /api/auth HTTP/2.0" 422.*."https://portainer.example.com/"<br>" language="text"><code __ignoreMap="">[Definition]<br>failregex = ^<HOST>.*POST /api/auth HTTP/2.0" 422.*."https://portainer.example.com/"<br></code></pre><p>Be sure to replace <code>https://portainer.example.com</code> with the address of your Portainer instance.</p><p>Next, go to <code>/etc/fail2ban/jail.d</code> and create <code>portainer.local</code> file with such content:</p><pre className="language-text" code="[portainer]<br>backend = auto<br>enabled = true<br>port = 80,443<br>protocol = tcp<br>filter = portainer<br>action = cloudflare<br>maxretry = 3<br>bantime = 1800<br>findtime = 1800<br>logpath = /var/log/nginx/access.log<br>" language="text"><code __ignoreMap="">[portainer]<br>backend = auto<br>enabled = true<br>port = 80,443<br>protocol = tcp<br>filter = portainer<br>action = cloudflare<br>maxretry = 3<br>bantime = 1800<br>findtime = 1800<br>logpath = /var/log/nginx/access.log<br></code></pre><p>If you are exposing Portainer to a port other than the default, set your port there. Configure <code>maxretry</code>, <code>bantime</code> and <code>findtime</code> as you want. Make sure that the path given in <code>logpath</code> leads to the <code>access.log</code> file with the Portainer webserver logs.</p><p>As you may have noticed, there is a information about the <code>cloudflare</code> action in the above file. Now we need to set up it.</p><p>You have to block requests using Cloudflare instead of iptables, as only Nginx can see the real IP of users - iptables cannot.</p><p>Go to <code>/etc/fail2ban/action.d</code>. Find <code>cloudflare.conf</code> file there. If for some reason there is no such file, get it from <a href="https://github.com/fail2ban/fail2ban/blob/master/config/action.d/cloudflare.conf" rel="nofollow">here</a>.</p><pre className="language-text" code="#<br># Author: Mike Rushton<br>#<br># IMPORTANT<br>#<br># Please set jail.local's permission to 640 because it contains your CF API key.<br>#<br># This action depends on curl (and optionally jq).<br># Referenced from http://www.normyee.net/blog/2012/02/02/adding-cloudflare-support-to-fail2ban by NORM YEE<br>#<br># To get your CloudFlare API Key: https://www.cloudflare.com/a/account/my-account<br>#<br># CloudFlare API error codes: https://www.cloudflare.com/docs/host-api.html#s4.2<br><br>[Definition]<br><br># Option:  actionstart<br># Notes.:  command executed on demand at the first ban (or at the start of Fail2Ban if actionstart_on_demand is set to false).<br># Values:  CMD<br>#<br>actionstart =<br><br># Option:  actionstop<br># Notes.:  command executed at the stop of jail (or at the end of Fail2Ban)<br># Values:  CMD<br>#<br>actionstop =<br><br># Option:  actioncheck<br># Notes.:  command executed once before each actionban command<br># Values:  CMD<br>#<br>actioncheck =<br><br># Option:  actionban<br># Notes.:  command executed when banning an IP. Take care that the<br>#          command is executed with Fail2Ban user rights.<br># Tags:    <ip>  IP address<br>#          <failures>  number of failures<br>#          <time>  unix timestamp of the ban time<br># Values:  CMD<br>#<br># API v1<br>#actionban = curl -s -o /dev/null https://www.cloudflare.com/api_json.html -d 'a=ban' -d 'tkn=<cftoken>' -d 'email=<cfuser>' -d 'key=<ip>'<br># API v4<br>actionban = curl -s -o /dev/null -X POST <_cf_api_prms> \<br>            -d '{"mode":"block","configuration":{"target":"ip","value":"<ip>"},"notes":"Fail2Ban <name>"}' \<br>            <_cf_api_url><br><br># Option:  actionunban<br># Notes.:  command executed when unbanning an IP. Take care that the<br>#          command is executed with Fail2Ban user rights.<br># Tags:    <ip>  IP address<br>#          <failures>  number of failures<br>#          <time>  unix timestamp of the ban time<br># Values:  CMD<br>#<br># API v1<br>#actionunban = curl -s -o /dev/null https://www.cloudflare.com/api_json.html -d 'a=nul' -d 'tkn=<cftoken>' -d 'email=<cfuser>' -d 'key=<ip>'<br># API v4<br>actionunban = id=$(curl -s -X GET <_cf_api_prms> \<br>                   "<_cf_api_url>?mode=block&configuration_target=ip&configuration_value=<ip>&page=1&per_page=1&notes=Fail2Ban%%20<name>" \<br>                   | { jq -r '.result[0].id' 2>/dev/null || tr -d '\n' | sed -nE 's/^.*"result"\s*:\s*\[\s*\{\s*"id"\s*:\s*"([^"]+)".*$/\1/p'; })<br>              if [ -z "$id" ]; then echo "<name>: id for <ip> cannot be found"; exit 0; fi;<br>              curl -s -o /dev/null -X DELETE <_cf_api_prms> "<_cf_api_url>/$id"<br><br>_cf_api_url = https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules<br>_cf_api_prms = -H 'X-Auth-Email: <cfuser>' -H 'X-Auth-Key: <cftoken>' -H 'Content-Type: application/json'<br><br>[Init]<br><br># If you like to use this action with mailing whois lines, you could use the composite action<br># action_cf_mwl predefined in jail.conf, just define in your jail:<br>#<br># action = %(action_cf_mwl)s<br># # Your CF account e-mail<br># cfemail  = <br># # Your CF API Key<br># cfapikey = <br><br>cftoken = YOUR_CLOUDFLARE_API_KEY<br><br>cfuser = YOUR_CLOUDFLARE_EMAIL<br>" language="text"><code __ignoreMap="">#<br># Author: Mike Rushton<br>#<br># IMPORTANT<br>#<br># Please set jail.local's permission to 640 because it contains your CF API key.<br>#<br># This action depends on curl (and optionally jq).<br># Referenced from http://www.normyee.net/blog/2012/02/02/adding-cloudflare-support-to-fail2ban by NORM YEE<br>#<br># To get your CloudFlare API Key: https://www.cloudflare.com/a/account/my-account<br>#<br># CloudFlare API error codes: https://www.cloudflare.com/docs/host-api.html#s4.2<br><br>[Definition]<br><br># Option:  actionstart<br># Notes.:  command executed on demand at the first ban (or at the start of Fail2Ban if actionstart_on_demand is set to false).<br># Values:  CMD<br>#<br>actionstart =<br><br># Option:  actionstop<br># Notes.:  command executed at the stop of jail (or at the end of Fail2Ban)<br># Values:  CMD<br>#<br>actionstop =<br><br># Option:  actioncheck<br># Notes.:  command executed once before each actionban command<br># Values:  CMD<br>#<br>actioncheck =<br><br># Option:  actionban<br># Notes.:  command executed when banning an IP. Take care that the<br>#          command is executed with Fail2Ban user rights.<br># Tags:    <ip>  IP address<br>#          <failures>  number of failures<br>#          <time>  unix timestamp of the ban time<br># Values:  CMD<br>#<br># API v1<br>#actionban = curl -s -o /dev/null https://www.cloudflare.com/api_json.html -d 'a=ban' -d 'tkn=<cftoken>' -d 'email=<cfuser>' -d 'key=<ip>'<br># API v4<br>actionban = curl -s -o /dev/null -X POST <_cf_api_prms> \<br>            -d '{"mode":"block","configuration":{"target":"ip","value":"<ip>"},"notes":"Fail2Ban <name>"}' \<br>            <_cf_api_url><br><br># Option:  actionunban<br># Notes.:  command executed when unbanning an IP. Take care that the<br>#          command is executed with Fail2Ban user rights.<br># Tags:    <ip>  IP address<br>#          <failures>  number of failures<br>#          <time>  unix timestamp of the ban time<br># Values:  CMD<br>#<br># API v1<br>#actionunban = curl -s -o /dev/null https://www.cloudflare.com/api_json.html -d 'a=nul' -d 'tkn=<cftoken>' -d 'email=<cfuser>' -d 'key=<ip>'<br># API v4<br>actionunban = id=$(curl -s -X GET <_cf_api_prms> \<br>                   "<_cf_api_url>?mode=block&configuration_target=ip&configuration_value=<ip>&page=1&per_page=1&notes=Fail2Ban%%20<name>" \<br>                   | { jq -r '.result[0].id' 2>/dev/null || tr -d '\n' | sed -nE 's/^.*"result"\s*:\s*\[\s*\{\s*"id"\s*:\s*"([^"]+)".*$/\1/p'; })<br>              if [ -z "$id" ]; then echo "<name>: id for <ip> cannot be found"; exit 0; fi;<br>              curl -s -o /dev/null -X DELETE <_cf_api_prms> "<_cf_api_url>/$id"<br><br>_cf_api_url = https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules<br>_cf_api_prms = -H 'X-Auth-Email: <cfuser>' -H 'X-Auth-Key: <cftoken>' -H 'Content-Type: application/json'<br><br>[Init]<br><br># If you like to use this action with mailing whois lines, you could use the composite action<br># action_cf_mwl predefined in jail.conf, just define in your jail:<br>#<br># action = %(action_cf_mwl)s<br># # Your CF account e-mail<br># cfemail  = <br># # Your CF API Key<br># cfapikey = <br><br>cftoken = YOUR_CLOUDFLARE_API_KEY<br><br>cfuser = YOUR_CLOUDFLARE_EMAIL<br></code></pre><p>Set up your Cloudflare API Key and Cloudflare Account Email.</p><p>After completing the configuration, restart Fail2ban using <code>systemctl restart fail2ban</code> and be sure to check if the restart was successful with <code>systemctl status fail2ban</code>. If something is wrong, the restart will not return this information and fail2ban will not work.</p><p>Then check the status of the newly created jail using <code>fail2ban-client status portainer</code>. It should look something like this:</p><pre className="language-text" code="Status for the jail: portainer<br>|- Filter<br>|  |- Currently failed: 3<br>|  |- Total failed:     22<br>|  `- File list:        /var/log/nginx/access.log<br>`- Actions<br>   |- Currently banned: 0<br>   |- Total banned:     2<br>   `- Banned IP list:<br>" language="text"><code __ignoreMap="">Status for the jail: portainer<br>|- Filter<br>|  |- Currently failed: 3<br>|  |- Total failed:     22<br>|  `- File list:        /var/log/nginx/access.log<br>`- Actions<br>   |- Currently banned: 0<br>   |- Total banned:     2<br>   `- Banned IP list:<br></code></pre><h2 id="conclusion">Conclusion</h2><p>Fail2ban should now secure your Portainer instance and temporarily ban the address after several invalid login attempts. Sometimes you have to wait a few seconds before the cloudflare rule takes effect.</p><p><img alt="Cloudflare blocking page" src="/media/cloudflare-error-1006.webp" title="Cloudflare blocking page"></img></p>
                <p>For more articles, visit <a href="https://ozelot.fyi/">ozelot.fyi</a> or subscribe this RSS channel - <a href="https://ozelot.fyi/rss.xml">ozelot.fyi/rss.xml</a>.</p>
            ]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[About this website]]></title>
            <link>https://ozelot.fyi/blog/about-this-website/</link>
            <guid>https://ozelot.fyi/blog/about-this-website/</guid>
            <pubDate>Wed, 24 Aug 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[The first blog post describing how this website was built, how it works, and where it is hosted.]]></description>
            <content:encoded><![CDATA[
                <p>You are viewing this post as a preview for RSS readers and it may be slightly different from the original version due to markdown formatting issues. <a href='https://ozelot.fyi/blog/about-this-website/'>Click here</a> to see full post.</p> 
                <h2 id="whats-this-website-about">What's this website about?</h2><p>This site isn't about showing off impressive projects or boasting about skills. It's about foxes. A quiet, cozy fox den where a fox can shyly leave useful guides, experiences, and curious thoughts. The goal is to create a place where others can wander in, find something helpful, and maybe discover something useful rather than stare at a list of achievements. While you won't find much about me here, curious foxes may still discover a hidden little <a href="/ctf/">Capture The Flag</a> while exploring.</p><p>The weird little creatures you see throughout the website are all illustrations of the author's original character.</p><h2 id="what-technologies-did-you-use">What technologies did you use?</h2><h3 id="nuxtjs">Nuxt.js</h3><p>This website is built with <del><a href="https://v3.nuxtjs.org/" rel="nofollow">Nuxt3</a></del> <a href="https://nuxt.com/blog/v4" rel="nofollow">Nuxt4</a>, a framework based on Vue.js.</p><p>It's generated as a static site using <a href="https://nuxt.com/docs/getting-started/deployment#static-hosting" rel="nofollow">Static Site Generation</a>, while requiring fewer resources than SSR and providing much better SEO than a SPA. Static generation also ensures that every page has proper metadata, making search engines and social media previews work as expected.</p><p>It also makes it easy to automatically generate files such as <a href="https://ozelot.fyi/sitemap.xml" rel="nofollow">sitemap.xml</a> or <a href="https://ozelot.fyi/rss.xml" rel="nofollow">RSS Feed</a>.</p><h3 id="nuxt-content">Nuxt Content</h3><p><a href="https://content.nuxt.com" rel="nofollow">Nuxt Content</a> lets blog posts be written in Markdown without the need for a traditional CMS. Every article lives alongside the website's source code, making everything easy to manage and version using Git.</p><p>During the build process, the content is converted into static pages that can be served efficiently from a CDN. The result is a lightweight blog with fast loading times and SEO-friendly pages.</p><h3 id="comment-system">Comment system</h3><p>The blog includes a custom comment system built with Fastify.</p><p>Key features:</p><ul><li>GitHub OAuth login</li><li>Comment threads and replies</li><li>Rate limiting</li><li>Email notifications for new comments</li></ul><p>It uses session-based authentication with cookies, stores comments in a SQLite database using Prisma, and sends email notifications via SMTP. The entire system was built specifically for this blog instead of relying on a <a href="https://www.trustpilot.com/review/disqus.com" rel="nofollow">popular third-party service</a>.</p><p>The backend source code is available <a href="https://github.com/0zelot/blog-comments-backend" rel="nofollow">on GitHub</a>.</p><h3 id="tailwind-css">Tailwind CSS</h3><p>This website uses <a href="https://tailwindcss.com" rel="nofollow">Tailwind CSS</a> with <a href="https://flowbite.com" rel="nofollow">Flowbite</a>.</p><p>Tailwind makes styling the website fast and keeps the design consistent without writing much custom CSS. It also removes unused styles during the build process, helping keep the final CSS bundle small.</p><p>Flowbite provides a solid set of components that fit naturally into the project and save time when building common interface elements. Since everything is still based on Tailwind utilities, every component can be tweaked to match the look of this fox den without much effort.</p><h2 id="web-hosting">Web hosting</h2><p>Except for the comment system API, this website is hosted on <a href="https://pages.cloudflare.com/" rel="nofollow">Cloudflare Pages</a>.</p><p>Since the website is generated as a static site, Cloudflare Pages turned out to be a perfect fit. It offers free hosting, automatic deployments, a global CDN, DNS services and Cloudflare's protection with almost no configuration required.</p><p>I chose Cloudflare Pages over hosting the website on my server for two reasons:</p><ul><li><strong>Automatic deployments</strong> - Every push to GitHub automatically starts a new deployment, and within a minute or two the updated website is live.</li><li><strong>Reliability</strong> - Keeping the website separate from the rest of my infrastructure means it stays online even if my server decides to have a bad day. Since it's just a static website, there isn't much reason to host it myself. There's a good chance it will outlive the server it's managed from.</li></ul><p>And it just works <code>¯\_(ツ)_/¯</code></p><h2 id="credits">Credits</h2><ul><li>Nuxt documentation</li><li>Tailwind CSS documentation</li><li><del>Unresolved</del> GitHub issues</li><li>Stack Overflow</li><li>ChatGPT and Claude (proofreading, brainstorming, no vibe coding)</li></ul><p>Remember the CTF? Here is your flag - <code>FLAG-FIRSTBLOGPOST</code>.</p>
                <p>For more articles, visit <a href="https://ozelot.fyi/">ozelot.fyi</a> or subscribe this RSS channel - <a href="https://ozelot.fyi/rss.xml">ozelot.fyi/rss.xml</a>.</p>
            ]]></content:encoded>
        </item>
    </channel>
</rss>