<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Nirjas Jakilim on Medium]]></title>
        <description><![CDATA[Stories by Nirjas Jakilim on Medium]]></description>
        <link>https://medium.com/@nirzak?source=rss-5c3352923df3------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*3Pa7sDX1uc4CXPwqNU7OBg.jpeg</url>
            <title>Stories by Nirjas Jakilim on Medium</title>
            <link>https://medium.com/@nirzak?source=rss-5c3352923df3------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Mon, 25 May 2026 21:16:01 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@nirzak/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Setting Up CrowdSec on your Linux Server: A Complete Guide]]></title>
            <link>https://nirzak.medium.com/setting-up-crowdsec-on-your-linux-server-a-complete-guide-57f2e5a48279?source=rss-5c3352923df3------2</link>
            <guid isPermaLink="false">https://medium.com/p/57f2e5a48279</guid>
            <category><![CDATA[cyber-security-awareness]]></category>
            <category><![CDATA[security]]></category>
            <category><![CDATA[cybersecurity]]></category>
            <category><![CDATA[crowdsec]]></category>
            <category><![CDATA[linux]]></category>
            <dc:creator><![CDATA[Nirjas Jakilim]]></dc:creator>
            <pubDate>Sat, 11 Apr 2026 18:46:37 GMT</pubDate>
            <atom:updated>2026-04-11T18:46:37.030Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*76vNRKS8IA7d23mCsXJ6Lg.png" /><figcaption>Setting Up CrowdSec on your Linux Server: A Complete Guide</figcaption></figure><p>Previously, I wrote about setting up fail2ban to protect your server from malicious attacks and brute-force attempts, which works quite well. However, there is a better alternative: CrowdSec. CrowdSec is an open-source, collaborative security engine that detects and blocks malicious behavior using shared threat intelligence. Unlike traditional fail2ban-style tools, it crowdsources attack patterns across its entire user base, which means your server benefits from bans triggered by attacks on other machines worldwide.</p><p>This guide walks through a full production setup on the Ubuntu 22.04 LTS operating system, which should also work on other systems.</p><h3>Prerequisites</h3><ul><li>A server running Ubuntu or any other operating system</li><li>sudo access</li><li>nftables installed (sudo apt install nftables -y)</li><li>Grafana Prometheus for monitoring section</li><li>A Telegram account (for the optional notification section)</li></ul><h3>1. Installing CrowdSec</h3><p>CrowdSec provides an official repository script that handles APT source registration:</p><pre># Add the CrowdSec repository<br>curl -s https://install.crowdsec.net | sudo bash</pre><pre># Install the agent<br>sudo apt update &amp;&amp; sudo apt install crowdsec -y</pre><p>Verify the service is running:</p><pre>sudo systemctl status crowdsec<br>sudo cscli version</pre><blockquote><strong><em>Note:</em></strong><em> On first install, CrowdSec runs a detection wizard (</em><em>wizard.sh) that scans your running services and pre-populates the log acquisition config. You can find the log acquisition config from this location and configure it as per your use case </em>/etc/crowdsec/acquis.yaml</blockquote><h3>2. Enrolling into the CrowdSec Security Engine</h3><p>Enrollment links your local agent to the <a href="https://app.crowdsec.net/">CrowdSec Console</a>, giving you access to the shared blocklist network, centralized alert management and remote decision control.</p><p><strong>Steps:</strong></p><ol><li>Create a free account at <a href="https://app.crowdsec.net/">app.crowdsec.net</a></li><li>Navigate to <strong>Engines → Enroll</strong> and copy your enrollment key</li><li>Run on your server:</li></ol><pre>sudo cscli console enroll &lt;YOUR_ENROLLMENT_KEY&gt;</pre><ol><li>Restart CrowdSec:</li></ol><pre>sudo systemctl restart crowdsec</pre><p>Go back to the Console and <strong>accept</strong> the engine from the UI.</p><p>Once accepted, your instance appears under <strong>Engines</strong> with a green status. You can now install their blocklists from the UI and manage decisions remotely.</p><h3>3. Installing the Firewall Bouncer</h3><p>Now that you have your security engine running, it’s time to install the remediation component. The remediation component is primarily responsible for blocking threat actors. One great thing about CrowdSec is that you can choose which type of remediation component you want. For example, if you want to ban an IP from your firewall, you can use cs-firewall-bouncer. If you want to restrict the IP from your Nginx server, then you will install cs-nginx-bouncer, and there are many more options. Here, we will use the nftables-compatible cs-firewall-bouncer.</p><pre>sudo apt install crowdsec-firewall-bouncer-nftables -y</pre><p>Enable and start it:</p><pre>sudo systemctl enable --now crowdsec-firewall-bouncer</pre><p>Confirm the bouncer registered with the agent:</p><pre>sudo cscli bouncers list</pre><blockquote><strong><em>Warning:</em></strong><em> Ensure the </em><em>nf_tables kernel module is loaded before starting the bouncer: </em><em>sudo modprobe nf_tables</em></blockquote><h3>4. Installing Collections</h3><p>Collections bundle parsers and detection scenarios for specific services. Install the ones matching your environment:</p><pre># Core service collections<br>sudo cscli collections install crowdsecurity/nginx<br>sudo cscli collections install crowdsecurity/sshd<br>sudo cscli collections install crowdsecurity/mysql<br>sudo cscli collections install crowdsecurity/linux</pre><pre># Optional but recommended<br>sudo cscli collections install crowdsecurity/http-cve<br>sudo cscli collections install crowdsecurity/iptables</pre><p>Activate the configuration change:</p><pre># Reload to activate new parsers and scenarios<br>sudo systemctl reload crowdsec</pre><p>Verify what’s installed:</p><pre>sudo cscli collections list</pre><p>The output should show something like below:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/989/1*f0wrn2CX922LlfqqVRt-Iw.png" /><figcaption>Crowdsec collection list example</figcaption></figure><p><strong>Tip:</strong> There’s a lot of already developed collections by their community out there. You can search the available collections on their console and then install it with the mentioned command.</p><h3>5. Configuring Log Acquisition</h3><p>CrowdSec reads logs via /etc/crowdsec/acquis.yaml. While the wizard generates this automatically, you should review and modify it based on your specific use cases. For example, if your Nginx logs are stored in a custom location rather than /var/log/nginx/error.log, you must update the configuration accordingly. Below is a sample illustrating the structure of an acquis.yaml file:</p><pre># /etc/crowdsec/acquis.yamlfilenames:<br>  - /var/log/nginx/access.log<br>  - /var/log/nginx/error.log<br>labels:<br>  type: nginx<br>---<br># SSH auth logs<br>filenames:<br>  - /var/log/auth.log<br>labels:<br>  type: syslog<br>---<br># MySQL error log<br>filenames:<br>  - /var/log/mysql/error.log<br>labels:<br>  type: mysql<br>---<br># General syslog and kernel log<br>filenames:<br>  - /var/log/syslog<br>  - /var/log/kern.log<br>labels:<br>  type: syslog</pre><p>After any changes don’t forget to reload crowdsec:</p><pre>sudo systemctl reload crowdsec</pre><p>Use sudo cscli metrics to confirm log files are being tailed. Zero parsed lines typically means a missing collection or wrong type label.</p><h3>6. Whitelisting IPs with cscli allowlist</h3><p>CrowdSec newer versions include a native allowlist command to permanently exempt IPs or CIDRs from bans. It is useful for your own infrastructure, monitoring tools or trusted partners. My recommendation is always whitelist your own IPs so that you don’t get accidentally banned and blocked out of your own server.</p><p><strong>Create an allowlist:</strong></p><pre>sudo cscli allowlists create my_allowlist \<br>--description &quot;Trusted internal and monitoring IPs&quot;</pre><p><strong>Add IPs or CIDRs:</strong></p><pre># Single IP example<br>sudo cscli allowlists add my_allowlist 192.168.1.10 \<br>  --comment &quot;Internal monitoring&quot;<br><br># CIDR range example<br>sudo cscli allowlists add my_allowlist 10.0.0.0/8 \<br>  --comment &quot;Private network range&quot;<br><br># Multiple IPs at once example<br>sudo cscli allowlists add my_allowlist \<br>  203.0.113.5 203.0.113.10 \<br>  --comment &quot;Partner IPs&quot;</pre><p><strong>Manage allowlists:</strong></p><pre>sudo cscli allowlists list<br>sudo cscli allowlists inspect my_allowlist<br>sudo cscli allowlists remove my_allowlist 203.0.113<br>sudo cscli allowlists delete my_allowlist</pre><blockquote><em>Allowlisted IPs are evaluated before any decision is applied and they will never be banned even if they trigger a scenario.</em></blockquote><h3>7. Configuring nftables as the Bouncer Backend</h3><p>Edit the bouncer config at /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml:</p><pre>mode: nftables<br>update_frequency: 10s<br>log_mode: file<br>log_dir: /var/log/<br>log_level: info<br>log_compression: true<br>log_max_size: 100<br>log_max_backups: 3<br>log_max_age: 30<br>api_url: http://127.0.0.1:8080/<br>api_key: &lt;BOUNCER_API_KEY&gt;<br>insecure_skip_verify: false<br>disable_ipv6: false<br>deny_action: DROP<br>deny_log: false<br>supported_decisions_types:<br>  - ban<br>blacklists_ipv4: crowdsec-blacklists<br>blacklists_ipv6: crowdsec6-blacklists<br>ipset_type: nethash<br><br>## nftables<br>nftables:<br>  ipv4:<br>    enabled: true<br>    set-only: false<br>    table: crowdsec<br>    chain: crowdsec-chain<br>    priority: -10<br>  ipv6:<br>    enabled: true<br>    set-only: false<br>    table: crowdsec6<br>    chain: crowdsec6-chain<br>    priority: -10<br><br>nftables_hooks:<br>  - input<br>  - forward<br><br># packet filter<br>pf:<br>  # an empty string disables the anchor<br>  anchor_name: &quot;&quot;</pre><p>The bouncer API key is auto-generated on install. To retrieve or regenerate it:</p><pre>sudo cscli bouncers list</pre><p>Regenerate it if needed:</p><pre># Regenerate if needed<br>sudo cscli bouncers delete crowdsec-firewall-bouncer<br>sudo cscli bouncers add crowdsec-firewall-bouncer</pre><p>Restart the bouncer and verify nftables rules:</p><pre>sudo systemctl restart crowdsec-firewall-bouncer<br>sudo nft list ruleset | grep crowdsec</pre><blockquote><strong><em>Warning:</em></strong><em> If you’re running both </em><em>iptables and </em><em>nftables, use the </em><em>iptables-nft backend to avoid rule conflicts: </em><em>sudo update-alternatives --set iptables /usr/sbin/iptables-nft</em></blockquote><h3>8. Prometheus + Grafana Integration</h3><p>CrowdSec exposes a Prometheus metrics endpoint out of the box with no extra plugins.</p><h3>Enable Prometheus in CrowdSec</h3><p>Confirm or add the following in /etc/crowdsec/config.yaml:</p><pre>prometheus:<br>  enabled: true<br>  level: full<br>  listen_addr: 127.0.0.1<br>  listen_port: 6060</pre><p>Reload and test:</p><pre>sudo systemctl reload crowdsec<br>curl -s http://127.0.0.1:6060/metrics | head -20</pre><h3>Configure Prometheus to Scrape CrowdSec</h3><p>Add a scrape job to your prometheus.yml:</p><pre>scrape_configs:<br>  - job_name: &#39;crowdsec&#39;<br>    static_configs:<br>      - targets: [&#39;localhost:6060&#39;]<br>    scrape_interval: 30s</pre><p>Reload Prometheus:</p><pre>sudo systemctl reload prometheus<br># Or via the HTTP API if enabled:<br>curl -X POST http://localhost:9090/-/reload</pre><h3>Import the Grafana Dashboard</h3><ol><li>In Grafana, go to <strong>Dashboards → Import</strong></li><li>Use dashboard ID <strong>21419 </strong>(Crowdsec Metrics)</li><li>Select your Prometheus datasource and click <strong>Import</strong></li><li>You can also import dashboards from their official github repository here: <a href="https://github.com/crowdsecurity/grafana-dashboards/tree/master/dashboards_v5">Crowdsec Grafana Dashboards</a></li></ol><p><strong>Key metrics to monitor:</strong></p><p>cs_active_decisions Current active bans</p><p>cs_alerts_total Total scenarios triggered</p><p>cs_lapi_requests_total Bouncer polling activity</p><p>cs_parser_hits_total Log lines processed</p><h3>9. Telegram Notification Integration</h3><p>CrowdSec’s HTTP notification plugin can POST alerts to any webhook, including Telegram’s Bot API. Alerts include inline buttons linking the offending IP to Shodan and the CrowdSec CTI.</p><h3>Step 1: Create a Telegram Bot</h3><ol><li>Open Telegram and search for <strong>@BotFather</strong></li><li>Send /newbot and follow the prompts — save your <strong>bot token</strong></li><li>Add the bot to your target group or channel</li><li>Get the <strong>chat ID</strong>:</li></ol><pre>curl -s https://api.telegram.org/bot&lt;BOT_TOKEN&gt;/getUpdates \<br>  | python3 -m json.tool | grep &#39;&quot;id&quot;&#39;</pre><blockquote><em>Group chat IDs are </em><strong><em>negative integers</em></strong><em> (e.g., </em><em>-123456789).</em></blockquote><h3>Step 2: Configure the Plugin</h3><p>Create /etc/crowdsec/notifications/http_tg.yaml:</p><pre># Author: Nirjas Jakilim<br>type: http<br>name: http_tg<br>log_level: info<br><br>format: |<br>  {<br>   &quot;chat_id&quot;: &quot;-YOUR_CHAT_ID&quot;,<br>   &quot;text&quot;: &quot;<br>     {{range . -}}<br>     {{$alert := . -}}<br>     {{range .Decisions -}}<br>     {{if $alert.Source.Cn -}}<br>  Scenario: {{.Scenario}}<br>  IP: {{.Value}}<br>  Ban Duration: {{.Duration}}<br>  Country: {{$alert.Source.Cn}}<br>  AS Name: {{$alert.Source.AsName}}<br>    {{end}}<br>     {{if not $alert.Source.Cn -}}<br>  Scenario: {{.Scenario}}<br>  IP: {{.Value}}<br>  Ban Duration: {{.Duration}}<br>  Country: Unknown<br>    {{end}}<br>     {{end -}}<br>     {{end -}}<br>   &quot;,<br>   &quot;reply_markup&quot;: {<br>      &quot;inline_keyboard&quot;: [<br>          {{ $arrLength := len . -}}<br>          {{ range $i, $value := . -}}<br>          {{ $V := $value.Source.Value -}}<br>          [<br>              {<br>                  &quot;text&quot;: &quot;See {{ $V }} on shodan.io&quot;,<br>                  &quot;url&quot;: &quot;https://www.shodan.io/host/{{ $V }}&quot;<br>              },<br>              {<br>                  &quot;text&quot;: &quot;See {{ $V }} on crowdsec.net&quot;,<br>                  &quot;url&quot;: &quot;https://app.crowdsec.net/cti/{{ $V }}&quot;<br>              }<br>          ]{{if lt $i ( sub $arrLength 1) }},{{end }}<br>          {{end -}}<br>      ]<br>    }<br>  }<br>url: https://api.telegram.org/bot&lt;BOT_TOKEN&gt;/sendMessage<br>method: POST<br>headers:<br>  Content-Type: &quot;application/json&quot;</pre><h3>Step 3: Add Notifications in profiles.yaml</h3><p>Edit /etc/crowdsec/profiles.yaml to attach http_tg to your remediation profiles:</p><pre># Skip re-banning IPs that already have an active decision<br>name: silence_ip_remediation<br>filters:<br>  - Alert.Remediation == true &amp;&amp; Alert.GetScope() == &quot;Ip&quot; &amp;&amp; GetActiveDecisionsCount(Alert.GetValue()) &gt; 0<br>on_success: break<br>---<br># Default IP ban — 500h with Telegram notification<br>name: default_ip_remediation<br>filters:<br>  - Alert.Remediation == true &amp;&amp; Alert.GetScope() == &quot;Ip&quot;<br>decisions:<br>  - type: ban<br>    duration: 500h<br>notifications:<br>  - http_tg<br>on_success: break<br>---<br># Range ban — 500h with Telegram notification<br>name: default_range_remediation<br>filters:<br>  - Alert.Remediation == true &amp;&amp; Alert.GetScope() == &quot;Range&quot;<br>decisions:<br>  - type: ban<br>    duration: 500h<br>notifications:<br>  - http_tg<br>on_success: break</pre><h3>Step 4: Reload and Test</h3><pre>sudo systemctl reload crowdsec</pre><p>Send a test notification:</p><pre># Send a test notification<br>sudo cscli notifications test http_tg</pre><p>If the test fails, check /var/log/crowdsec/crowdsec.log for plugin errors. <strong>Common issues:</strong> wrong chat ID format, bot not added to the group, or malformed JSON in the format template.</p><p>If everything is alright the alert notifications will be like as below</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/519/1*3P2ZD9payo5y1LFBwWoFXg.png" /><figcaption>Crowdsec telegram notification example</figcaption></figure><h3>Verifying the Full Stack</h3><p>Once everything is configured, run these commands to confirm each layer is working:</p><pre># Agent status and active decisions<br>sudo cscli decisions list<br>sudo cscli alerts list<br><br># Bouncer is enforcing bans<br>sudo nft list ruleset | grep crowdsec<br><br># Metrics endpoint alive<br>curl -s http://127.0.0.1:6060/metrics | grep cs_active<br><br># All services healthy<br>sudo systemctl status crowdsec crowdsec-firewall-bouncer</pre><h3>Conclusion</h3><p>You now have a fully operational CrowdSec stack: the agent parsing logs from nginx, SSH, MySQL, and syslog, the nftables bouncer enforcing bans at the kernel level, IP allowlists protecting trusted sources, Prometheus and Grafana providing observability and Telegram delivering real-time alerts with one-click threat investigation links.</p><p>Because CrowdSec is collaborative, every ban your instance issues contributes back to the shared blocklist making the network stronger for everyone.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=57f2e5a48279" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Top SSHD Hardening Tricks I Use to Keep My Server Safe from Brute Force Attacks]]></title>
            <link>https://nirzak.medium.com/top-sshd-hardening-tricks-i-use-to-keep-my-server-safe-from-brute-force-attacks-73a7bbff0542?source=rss-5c3352923df3------2</link>
            <guid isPermaLink="false">https://medium.com/p/73a7bbff0542</guid>
            <category><![CDATA[security]]></category>
            <category><![CDATA[linux-security]]></category>
            <category><![CDATA[ssh]]></category>
            <category><![CDATA[hardening]]></category>
            <dc:creator><![CDATA[Nirjas Jakilim]]></dc:creator>
            <pubDate>Mon, 30 Mar 2026 16:56:08 GMT</pubDate>
            <atom:updated>2026-03-30T17:12:25.547Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*skYU4M73SlzPbHB7aEN8LQ.png" /><figcaption>SSHD Hardening methods to mitigate brute force attacks</figcaption></figure><p><em>The moment you expose your server to the internet with a public IP, if you check the </em><strong><em>/var/log/auth.log</em></strong><em> file, you will be in a complete panic within a moment. Numerous bots and attackers will start brute-forcing your server to gain access. This is very alarming if your server is not protected with proper SSHD hardening, as attackers might compromise your system at any moment. Fortunately, there is an exact </em><strong><em>sshd_config</em></strong><em> hardening process you can apply to every new server to make brute-force attacks a non-issue. Here are nine straightforward steps to lock down your server.</em></p><p><strong>Warning</strong>: Always keep a backup SSH window open for your server. Never forget to test the configuration with the sshd -t command before applying changes to avoid getting locked out.</p><h3>1. Change the Default Port</h3><p>The single biggest drop in noise. Automated scanners and attackers almost exclusively target port 22. So, if you move to a high, non-standard port that will eliminate almost ~99% of bot traffic overnight.</p><pre># /etc/ssh/sshd_config<br>Port 2222 # Pick anything from 1024–65535</pre><p>Remember to update your firewall rules and use <strong>ssh -p &lt;your_port&gt; user@host</strong> going forward.</p><h3>2. Disable Root Login</h3><p>Never allow direct root SSH access. That’s like opening your server’s main door to the attackers. If an attacker gets in, they can do almost anything on your servers. You may want them to at least need to escalate privileges separately. Always log in as a regular user and then use sudo.</p><pre>PermitRootLogin no</pre><p>If you still really need direct root ssh login then you can use the config mentioned below to strictly enforce key based login for the root user</p><pre>PermitRootLogin prohibit-password</pre><p><strong>Tip:</strong> Always confirm you have sudo access before disabling root login.</p><h3>3. Enforce Key-Based Authentication (No Passwords)</h3><p>Password auth is a significant attack surface. So, you should kill it entirely. SSH keys are the minimum bar. Then you can also pair with a strong passphrase on the key itself. You can apply the following settings to enforce strict key based access on your server and restrict password based logins.</p><pre>PasswordAuthentication no<br>PubkeyAuthentication yes<br>AuthenticationMethods publickey<br>ChallengeResponseAuthentication no<br>UsePAM yes</pre><p><strong>What is does?</strong></p><p>PasswordAuthentication no completely disable password based authentication</p><p>PubkeyAuthentication yes only allowed key based authentication</p><p>AuthenticationMethods publickey enforce key based authentication method only</p><p>ChallengeResponseAuthentication no disables keyboard interactive authentication</p><p>UsePAM yes enables pluggable authentication module</p><h3>4. Restrict Which Users Can SSH</h3><p>Whitelist the exact users (or groups) that need SSH access. It can significantly reduce the attack surface since only users you explicitly allow can now login. Everyone else is denied at the sshd level even if they have valid credentials.</p><pre>AllowUsers &lt;your_username_here&gt; &lt;anotheruser&gt;</pre><p>Or by group:</p><pre>AllowGroups sshusers</pre><p>If you use the AllowGroups directive then you can add any user to the sshusers group</p><pre>sudo usermod -aG sshusers &lt;youruser&gt;</pre><h3>5. Limit Authentication Attempts per Connection</h3><p>Reduce how many tries an attacker gets before the connection is dropped.</p><pre>MaxAuthTries 3<br>LoginGraceTime 20<br>MaxStartups 10:30:60</pre><p><strong>What it does?</strong></p><p>MaxAuthTries 3 will drop the connection after the failure attempt count reach to 3</p><p>LoginGraceTime 20 will disconnect if not authenticated within 20 seconds</p><p>MaxStartups will throttle unauthenticated connection attempts</p><h3>6. Disable Features You Don’t Use</h3><p>Every enabled feature is a potential attack vector. Disable anything you don’t actively need.</p><pre>X11Forwarding no<br>AllowTcpForwarding no # unless you need SSH tunneling<br>AllowAgentForwarding no<br>PermitTunnel no<br>GatewayPorts no<br>PermitEmptyPassword no<br>IgnoreRhosts yes</pre><h3>7. Enforce Strong Ciphers and Key Exchange Algorithms</h3><p>Strip out legacy ciphers. Attackers can downgrade your connection to weak crypto if you leave old algorithms enabled.</p><pre>KexAlgorithms curve25519-sha256,diffie-hellman-group16-sha512<br>Ciphers aes256-gcm@openssh.com,chacha20-poly1305@openssh.com<br>MACs hmac-sha2–256-etm@openssh.com,hmac-sha2–512-etm@openssh.com<br>HostKeyAlgorithms ssh-ed25519,rsa-sha2–512</pre><h3>8. Use tools like fail2ban or Crowdsec</h3><p>Even with all the above, bots will keep trying. fail2ban watches your auth logs and auto-bans IPs after repeated failures. Pair it with the custom port from tip #1.</p><p>If you need to setup fail2ban then I have covered it in a separate article here: <a href="https://nirzak.medium.com/guide-to-secure-your-self-hosted-stacks-like-nginx-ssh-vaultwarden-with-fail2ban-6e1a0e65e891">Guide to Secure Your Self-Hosted Stacks like Nginx, SSH, &amp; Vaultwarden with Fail2ban</a></p><pre># /etc/fail2ban/jail.local<br>[sshd]<br>enabled = true<br>port = 2222    # Mention your ssh port here<br>filter   = sshd<br>logpath  = %(sshd_log)s<br>backend  = %(sshd_backend)s<br>maxretry = 3<br>bantime = 3600 # ban for 1 hour. You can also increase it<br>findtime = 600</pre><p>About crowdsec I will cover it on a separate article.</p><h3>9. Restrict SSH Access at the Firewall Level</h3><p>If you connect from a static IP or a VPN, whitelist that IP in your firewall. This is the hardest gate of all, the port never even responds to unauthorized sources.</p><pre># UFW example<br>ufw allow from 203.0.113.10 to any port 2222<br><br># iptables example<br>iptables -A INPUT -p tcp — dport 2222 -s 203.0.113.10 -j ACCEPT<br>iptables -A INPUT -p tcp — dport 2222 -j DROP</pre><h3>Apply the Changes</h3><p>Always test your config before reloading because one syntax error locks you out:</p><pre>sudo sshd -t</pre><p>If the above command didn’t show any error then run the following command to reload your sshd config</p><pre>sudo systemctl reload sshd</pre><h3>Final Thoughts</h3><p>None of these tricks is a silver bullet on its own, but defense in depth is the main goal. My standard stack for any new VPS:</p><ul><li>Move SSH off port 22</li><li>Keys only, no passwords</li><li>Whitelist users</li><li>fail2ban or crowdsec watching auth logs</li><li>Firewall IP allowlist if feasible</li></ul><p>With all of this in place, brute-force attacks become background noise that you never have to think about again.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=73a7bbff0542" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Configuring a Cluster File System on OCI using OCFS2]]></title>
            <link>https://nirzak.medium.com/configuring-a-cluster-file-system-on-oci-using-ocfs2-ef959420bc56?source=rss-5c3352923df3------2</link>
            <guid isPermaLink="false">https://medium.com/p/ef959420bc56</guid>
            <category><![CDATA[oracle-cloud]]></category>
            <category><![CDATA[ocfs2]]></category>
            <category><![CDATA[cluster]]></category>
            <category><![CDATA[linux-file-system]]></category>
            <category><![CDATA[filesystem]]></category>
            <dc:creator><![CDATA[Nirjas Jakilim]]></dc:creator>
            <pubDate>Sat, 28 Mar 2026 18:11:00 GMT</pubDate>
            <atom:updated>2026-03-28T18:43:00.088Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*om9Y011RalUeu9-vmChLKA.png" /></figure><p>Setting up a shared file system across multiple virtual machines in the cloud can be tricky, but Oracle Cluster File System Version 2 (OCFS2) makes it straightforward. If you are running instances on Oracle Cloud Infrastructure (OCI) and need multiple VMs to read and write to the same block volume simultaneously, this guide will walk you through the process on Ubuntu.</p><h3>Prerequisites</h3><p>Before we begin the configuration, ensure you have the following infrastructure in place:</p><ul><li><strong>Two Ubuntu VMs</strong> provisioned. For this guide, we will use server1 (IP: 10.0.1.85) and server2 (IP: 10.0.1.235) and server2 (IP: 10.0.1.235)</li><li><strong>A Block Volume</strong> created and attached to both VMs.</li><li>The access type for the block volume must be set to <strong>Read/write — shareable</strong>.</li></ul><p>Once these resources are ready, you can proceed to configure the cluster file system.</p><h3>Step 1: Configure Security Rules</h3><p>OCFS2 nodes need to communicate with each other over specific ports. You must update your OCI Virtual Cloud Network (VCN) security lists to allow internal traffic.</p><p>Allow TCP communication on ports <strong>7777</strong> and <strong>3260</strong>. Add the following ingress rules for your private subnet (e.g., 10.0.1.0/24):</p><p><strong>Source:</strong> 10.0.1.0/24 | <strong>IP Protocol:</strong> TCP | <strong>Destination Port:</strong> 7777</p><p><strong>Source:</strong> 10.0.1.0/24 | <strong>IP Protocol:</strong> TCP | <strong>Destination Port:</strong> 3260</p><p>To verify the connectivity between the VMs, run the following netcat commands from one VM to the other:</p><pre>nc -vz 10.0.1.85 3260;<br>nc -vz 10.0.1.85 7777</pre><blockquote><strong><em>Note:</em></strong><em> If your security rules are correct, the connection will show as “refused” because the cluster services are not running yet. This is expected. Do the same thing from the other VM with your server2 ip.</em></blockquote><h3>Step 2: Install OCFS2 Packages</h3><p>Log into both VMs and install the necessary OCFS2 tools. Run the following command on both machines:</p><pre>sudo apt update &amp;&amp; sudo apt upgrade -y;<br>sudo apt install ocfs2-tools-dev ocfs2-tools -y</pre><h3>Step 3: Define the Cluster</h3><p>Next, we need to create the cluster definition using the o2cb utility. This will generate the /etc/ocfs2/cluster.conf file if it doesn&#39;t already exist.</p><p>Run the following command on <strong>both VMs</strong> to create a cluster named ociocfs2:</p><pre>sudo o2cb add-cluster ociocfs2</pre><p>Now, add your nodes to the cluster. Run these commands on <strong>both VMs</strong>:</p><pre>sudo o2cb add-node ociocfs2 server1 --ip 10.0.1.85;<br>sudo o2cb add-node ociocfs2 server2 --ip 10.0.1.235</pre><p>You can verify the configuration by checking the .conf file:</p><pre>sudo cat /etc/ocfs2/cluster.conf</pre><p>The output should list your cluster name, the heartbeat mode, and the details for both nodes.</p><pre>cluster:<br>        name = ociocfs2<br>        heartbeat_mode = local<br>        node_count = 2<br><br>node:<br>        cluster = ociocfs2<br>        number = 0<br>        ip_port = 7777<br>        ip_address = 10.0.1.85<br>        name = server1<br><br>node:<br>        cluster = ociocfs2<br>        number = 1<br>        ip_port = 7777<br>        ip_address = 10.0.1.235<br>        name = server2</pre><h3>Step 4: Attach the Block Volume via iSCSI</h3><p>You need to run the iSCSI commands provided in your OCI console to attach the block volume at the OS level. To get the commands, go to you cloud console’s storage&gt;block volume and then click on your block volume name. There’s on the attached instances click on the 3 dot menu of your each instance’s section. You can get there the attach and detach commands.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0F5_WBwkBuU-pl3BacI68A.png" /><figcaption>iSCSI command option page</figcaption></figure><p><strong>On server1:</strong></p><pre>sudo iscsiadm -m node -o new -T iqn.2015-12.com.oracleiaas:5f2a1b82-9c40-4e1a-8b3d-72f6a5b9c1e4 -p 169.254.2.2:3260;<br>sudo iscsiadm -m node -o update -T iqn.2015-12.com.oracleiaas:5f2a1b82-9c40-4e1a-8b3d-72f6a5b9c1e4 -n node.startup -v automatic;<br>sudo iscsiadm -m node -T iqn.2015-12.com.oracleiaas:5f2a1b82-9c40-4e1a-8b3d-72f6a5b9c1e4 -p 169.254.2.2:3260 -l</pre><p><strong>On server2:</strong></p><pre>sudo iscsiadm -m node -o new -T iqn.2015-12.com.oracleiaas:5f2a1b82-9c40-4e1a-8b3d-72f6a5b9c1e4 -p 169.254.2.3:3260;<br>sudo iscsiadm -m node -o update -T iqn.2015-12.com.oracleiaas:5f2a1b82-9c40-4e1a-8b3d-72f6a5b9c1e4 -n node.startup -v automatic;<br>sudo iscsiadm -m node -T iqn.2015-12.com.oracleiaas:5f2a1b82-9c40-4e1a-8b3d-72f6a5b9c1e4 -p 169.254.2.3:3260 -l</pre><p>Once attached, identify the device path of your new volume:</p><pre>sudo fdisk -l</pre><p>Look for a pattern like /dev/sdb. whose size exactly match your created block volume’s size. That should be your target device. For the remainder of this guide, we will assume the device is /dev/sdb.</p><h3>Step 5: Format and Sync the Partition</h3><p>Start the cluster services before creating the partition:</p><pre>sudo systemctl start ocfs2;<br>sudo systemctl start o2cb</pre><p>Now, format the partition on <strong>only one machine</strong>:</p><pre>sudo mkfs.ocfs2 -L &quot;ocfs2&quot; /dev/sdb</pre><p>This will initialize the superblock, format the journals, and establish the cluster stack. Once it says mkfs.ocfs2 successful, the formatting is complete.</p><p>To ensure the partition table is updated across your environment, run the following sync command on <strong>both VMs</strong>:</p><pre>sudo blockdev --rereadpt /dev/sdb</pre><h3>Step 6: Final Configuration and Mounting</h3><p>Reconfigure the cluster tools on <strong>both VMs</strong> by running:</p><pre>sudo dpkg-reconfigure ocfs2-tools</pre><p>When prompted, provide the exact cluster name you defined earlier (ociocfs2).</p><p>To ensure the drive mounts automatically on boot, add the following line to your /etc/fstab file:</p><p>Plaintext</p><pre>/dev/sdb /data ocfs2 _netdev,defaults 0 0</pre><p>You also need to configure the kernel for cluster operation so the changes persist across reboots. Add these entries to your /etc/sysctl.conf file:</p><pre># Define panic and panic_on_oops for cluster operation <br>kernel.panic=30<br>kernel.panic_on_oops=1</pre><p>Ensure the /data target directory exists on both VMs. Then, run the mount command on <strong>only one node</strong>:</p><pre>sudo mount -a</pre><p>If everything is configured correctly, the volume should now be mounted on both nodes automatically.</p><p>You can verify the mount point on your instances using df -hT:</p><p>Bash</p><pre>nirzak@server1:~$ df -hT | grep data<br>/dev/sdb      ocfs2  106G  2.2G  104G   3% /data<br><br>nirzak@server2:~$ df -hT | grep /data<br>/dev/sdb      ocfs2  106G  2.2G  104G   3% /data</pre><h3>Troubleshooting &amp; Expanding the Cluster</h3><ul><li><strong>Module Errors:</strong> If you encounter a module error during setup, you may need to install the Oracle-specific Linux modules. Run: sudo apt install linux-modules-extra-$(uname -r)</li><li><strong>Adding New Nodes:</strong> If you need to make changes to your nodes or add new ones to the cluster configuration later, you must unregister and re-register the cluster. Use the following commands:</li></ul><pre>sudo o2cb unregister-cluster &lt;cluster_name&gt;;<br>sudo o2cb register-cluster &lt;cluster_name&gt;</pre><p>The restart both services,</p><pre>sudo systemctl restart ocfs2 &amp;&amp; sudo systemctl restart o2cb</pre><p>After restarting the services, you can mount the file system to the new node with:</p><pre>sudo mount.ocfs2 /dev/sdb /data</pre><p>Don’t forget to replace /dev/sdb with your devices’s path and /data with your mountpoint’s path. That’s all for now. Your cluster file system should now be online. Try to copy file on one server’s directory and check if it appears on the second server or not. If it appears yourc cluster should be working fine.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ef959420bc56" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Guide to Secure Your Self-Hosted Stacks like Nginx, SSH, & Vaultwarden with Fail2ban]]></title>
            <link>https://nirzak.medium.com/guide-to-secure-your-self-hosted-stacks-like-nginx-ssh-vaultwarden-with-fail2ban-6e1a0e65e891?source=rss-5c3352923df3------2</link>
            <guid isPermaLink="false">https://medium.com/p/6e1a0e65e891</guid>
            <category><![CDATA[security]]></category>
            <category><![CDATA[ssh]]></category>
            <category><![CDATA[vaultwarden]]></category>
            <category><![CDATA[fail2ban]]></category>
            <category><![CDATA[nginx]]></category>
            <dc:creator><![CDATA[Nirjas Jakilim]]></dc:creator>
            <pubDate>Thu, 26 Mar 2026 18:47:56 GMT</pubDate>
            <atom:updated>2026-03-26T18:47:56.835Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="Secure your server with fail2ban" src="https://cdn-images-1.medium.com/max/1024/1*4CNbD5jGuwxkVxz7lqW-8A.png" /></figure><p>If you are self-hosting services and expose them to public interfaces then you already know the anxiety of watching your server logs. The moment you expose port 22 (SSH) or 443 (HTTPS) to the internet, botnets and automated scripts begin knocking on your door. Whether they are brute-forcing your SSH credentials, probing Nginx for vulnerabilities, or trying to break into your Vaultwarden password manager, the noise is endless.</p><p>You can’t sit there and manually block IPs all day. You need an automated bouncer.</p><p>Enter <strong>Fail2ban</strong>.</p><p>Fail2ban is an intrusion prevention software framework that protects computer servers from brute-force attacks. It works by scanning log files (like /var/log/auth.log or Nginx access logs) and dynamically updating your firewall rules to ban IPs that show malicious signs.</p><p>In this guide, we’ll walk through setting up a robust Fail2ban configuration tailored for a modern self-hosted stack featuring SSH, Nginx, and Vaultwarden.</p><h3>Step 1: Installation</h3><p>First, let’s get Fail2ban installed. On a Debian/Ubuntu-based system, you can install it via apt:</p><pre>sudo apt update<br>sudo apt install fail2ban<br>sudo systemctl enable fail2ban</pre><p><strong>Tip:</strong> Never edit the default /etc/fail2ban/jail.conf file directly. Package updates will overwrite it. Instead, we create a .local file, which Fail2ban reads to override the defaults.</p><h3>Step 2: Defining the Jails (jail.local)</h3><p>A “jail” in Fail2ban is essentially a rule. It tells the service which log file to watch, which filter (regex) to use, and what the punishment should be.</p><p>Create a new file at /etc/fail2ban/jail.local and paste in the following configuration:</p><pre>[sshd]<br># To use more aggressive sshd modes set filter parameter &quot;mode&quot; in jail.local:<br># normal (default), ddos, extra or aggressive (combines all).<br>enabled  = true<br>port     = ssh<br>filter   = sshd<br>logpath  = %(sshd_log)s<br>backend  = %(sshd_backend)s<br><br>[vaultwarden]<br>enabled  = true<br>port     = 80,443,8081<br>backend  = pyinotify<br>filter   = vaultwarden<br>banaction = %(banaction_allports)s<br>logpath  = /opt/vaultwarden/vw-logs/access.log<br>maxretry = 3<br>bantime  = 1h<br>findtime = 10m<br><br>[nginx-http-auth]<br>enabled  = true<br>port     = http,https<br>logpath  = %(nginx_error_log)s<br>filter   = nginx-http-auth<br>maxretry = 3<br>bantime  = 1h<br>findtime = 10m<br><br>[nginx-bad-status]<br>enabled  = true<br>port     = http,https<br>logpath  = %(nginx_access_log)s<br>filter   = nginx-bad-status<br>maxretry = 15<br>bantime  = 10w<br>findtime = 10m<br><br>[nginx-botsearch]<br>enabled  = true<br>port     = http,https<br>logpath  = %(nginx_error_log)s<br>filter   = nginx-botsearch<br>maxretry = 2<br><br>[recidive]<br>enabled  = true<br>logpath  = /var/log/fail2ban.log<br>banaction = %(banaction_allports)s<br>bantime  = 4w<br>findtime = 1d</pre><p><strong>Note</strong>: Don’t forget to change the vaultwarden logpath <strong>/opt/vaultwarden/vw-logs/access.log</strong> according to your server’s log path.</p><h4>What’s happening here?</h4><ul><li><strong>[sshd]</strong>: The standard SSH protection. We are keeping it enabled to prevent basic brute-force ssh attacks.</li><li><strong>[vaultwarden]</strong>: Protects our password manager. It watches all relevant web ports and uses banaction_allports to completely lock out an offending IP across all services. If someone fails a login 3 times in 10 minutes, they are banned for an hour.</li><li><strong>[nginx-*]</strong>: These jails protect the web server. They handle failed HTTP authentications, aggressive bot searches and excessive &quot;bad&quot; HTTP status codes (like 404s or 403s).</li><li><strong>[recidive]</strong>: The ultimate ban-hammer. This jail actually monitors Fail2ban&#39;s <em>own</em> log file. If an IP gets banned multiple times by other jails within a day, the recidive jail steps in and slaps them with a massive 4-week ban.</li></ul><h3>Step 3: Creating Custom Filters</h3><p>Jails rely on filters to know what a “failed” attempt looks like. While Fail2ban comes with default filters for SSH and basic Nginx auth, we need to create custom filters for our specific Vaultwarden setup and our Nginx bad-status rules.</p><p>Filters are stored in /etc/fail2ban/filter.d/.</p><h4>1. The Vaultwarden Filter</h4><p>Create a file at /etc/fail2ban/filter.d/vaultwarden.local:</p><pre>[INCLUDES]<br>before = common.conf<br><br>[Definition]<br>failregex = ^.*?Username or password is incorrect\. Try again\. IP: &lt;ADDR&gt;\. Username:.*$<br>ignoreregex =</pre><p><em>This simple regex specifically targets the exact string Vaultwarden outputs to its log file when a login fails, capturing the offender’s IP address (</em><em>&lt;ADDR&gt;).</em></p><h4>2. The Nginx Bad Status Filter</h4><p>Create a file at /etc/fail2ban/filter.d/nginx-bad-status.conf:</p><pre>[Definition]<br># Match standard log format - handles both normal HTTP requests and malformed requests (hex)<br>failregex = ^&lt;HOST&gt; .* &quot;\S+ [^&quot;]*&quot; (?:400|401|403|404|405|444) \d+ &quot;.*&quot; &quot;.*&quot;$<br>            ^&lt;HOST&gt; .* &quot;.*&quot; (?:400|401|403|404|405|444) \d+ &quot;.*&quot; &quot;.*&quot;$<br><br># Ignore common legitimate 404s<br>ignoreregex = ^&lt;HOST&gt; .* &quot;GET (?:/favicon\.ico|/robots\.txt|/sitemap\.xml).* 404 \d+ &quot;.*&quot; &quot;.*&quot;$<br><br># Define the timestamp pattern in your logs<br>datepattern = %%d/%%b/%%Y:%%H:%%M:%%S %%z<br></pre><p><em>This is an incredibly useful filter. Scanners constantly hit servers with malformed requests or search for hidden directories, generating 4xx errors. This catches them while safely ignoring harmless requests for missing favicons or robots.txt files.</em></p><h3>Step 4: Start the Engine</h3><p>With your configurations and filters in place, it’s time to start Fail2ban and let it do its job.</p><p>Restart the service to apply the new configurations:</p><pre>sudo systemctl restart fail2ban</pre><p>You can check the status of your jails at any time using:</p><pre>sudo fail2ban-client status</pre><p>To see the details of a specific jail (like who is currently banned):</p><pre>sudo fail2ban-client status vaultwarden</pre><h3>Conclusion</h3><p>Fail2ban is a lightweight, incredibly effective tool for keeping the noise out of your server. By combining service-specific jails like Vaultwarden with broad-stroke protections like Nginx bad-status and the long-term recidive ban, you drastically reduce the attack surface of your self-hosted infrastructure.</p><p>Set it, forget it and enjoy the peace of mind. There’s also another great tool to use as a complete firewall solution for your servers and that is crowdsec. I will write a separate guideline for that one later.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6e1a0e65e891" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Hardening Nginx: A Practical Guide to Modular Security Configuration]]></title>
            <link>https://nirzak.medium.com/hardening-nginx-a-practical-guide-to-modular-security-configuration-33c9621021d2?source=rss-5c3352923df3------2</link>
            <guid isPermaLink="false">https://medium.com/p/33c9621021d2</guid>
            <category><![CDATA[nginx]]></category>
            <category><![CDATA[security-hardening]]></category>
            <category><![CDATA[security]]></category>
            <category><![CDATA[nginx-security]]></category>
            <category><![CDATA[http-security-headers]]></category>
            <dc:creator><![CDATA[Nirjas Jakilim]]></dc:creator>
            <pubDate>Thu, 26 Mar 2026 17:47:17 GMT</pubDate>
            <atom:updated>2026-03-26T18:07:29.731Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Wvl2EwXqSBeR4xMqQhZv-g.png" /></figure><p>Out of the box, Nginx is incredibly fast and efficient but it isn’t inherently secure against modern automated attacks like scanners, scraping bots and most sophisticated brute force attacks. Over time, I’ve set up a modular approach to hardening my Nginx setups. By splitting the security configurations into multiple logical files, it becomes much easier later to maintain, audit, and apply them across multiple virtual hosts.</p><p>In this guide, I’ll walk you through the essential configurations that will significantly improve your server’s security. Please note before jumping to the main article. You will need to install nginx-module-headers-more module for the more_set_headers directive to work.</p><h3>1. Global Security Settings</h3><p>This configurations will cover server-wide settings, masking the server identity and filtering out malicious traffic before it even reaches your application.</p><p>By leveraging Nginx’s map module, we can identify path traversal attempts, unauthorized bot scanners, and cloud metadata SSRF attempts efficiently without cluttering our server blocks with heavy conditional arguments.</p><pre># Hide Nginx version<br>server_tokens off;<br><br># Mask the server name (Note: requires the headers-more-nginx-module)<br>more_set_headers &quot;Server: AnyNameHere&quot;;<br><br># Identify sesitive paths URIs<br>map $request_uri $bad_request {<br>    default 0;<br>    &quot;~*wp-admin&quot; 1;<br>    &quot;~*(?:phpunit|phpinfo|phpmyadmin|php-?my-?admin|administrator|wp|wordpress)&quot; 1;<br>    &quot;~*(?:/\.env|\.git|\.svn|\.hg|\.DS_Store|\.idea|\.htaccess|\.htpasswd)&quot; 1;<br>    &quot;~*(?:etc/passwd|proc|\/\/)+&quot; 1;<br>    # Path traversal attempts (e.g., ../../etc/passwd)<br>    &quot;~*(?:\.\./|\.\.\\x5c)&quot; 1;<br>    # Cloud Metadata SSRF attempts<br>    &quot;~*(?:169\.254\.169\.254)&quot; 1;<br>    # Block direct access to common script/backup extensions remove it if you actually want to host this extension files on your server<br>    &quot;~*(?:\.sh|\.bash|\.bak|\.sql|\.tar|\.gz|\.zip)$&quot; 1;<br>}<br># Identify malicious automated scanners and bots<br>map $http_user_agent $bad_user_agent {<br>    default 0;<br>    &quot;~*havij|nikto|scan|loader|fetch|acunetix|sonic|nessus|sqlmap|zgrab|masscan|nmap|dirb|gobuster|shodan|censys|wpscan&quot; 1;<br>}<br># Combine the conditions<br>map &quot;$bad_request$bad_user_agent&quot; $is_bad_request {<br>    default 0;<br>    &quot;~*1&quot; 1;<br>}<br><br># Global rate limiting settings for DDos Protection. Change the limits according to your needs<br>limit_req_zone $binary_remote_addr zone=ratelimit:10m rate=20r/s;</pre><p><strong>How to apply it:</strong> Save the above configuration as security-global.conf file inside /etc/nginx/snippets directory. Include this file inside your main http {} block in nginx.conf. Like as below,</p><pre>include /etc/nginx/snippets/security-global.conf;</pre><p>Then, to actually drop the malicious traffic, add this simple check inside your server {} blocks of your main nginx configuration or on any blocks where you want to apply them.</p><pre>if ($is_bad_request) {<br>    return 444; # Closes the connection immediately without a response<br>}</pre><p>To apply the rate limiting setting add the following line on any of your server {} block of the main nginx configuration,</p><pre>limit_req zone=ratelimit burst=20 nodelay;</pre><h3>2. Location-Specific Rules</h3><p>Follow this section only if you want to restrict all the hidden files from being served through nginx cause they can be accidentally end up in your web root. Also, Text editors often leave backup files ending in ~ and version control systems leave hidden directories like .git. This file ensures they are never served to the public.</p><pre># Deny access to all hidden files and directories (starting with a dot)<br>location ~ /\. {<br>    deny all;<br>    access_log off;<br>    log_not_found off;<br>}<br># Deny access to temporary editor files<br>location ~ ~$ {<br>    deny all;<br>}</pre><p><strong>How to apply it:</strong></p><p>Save the above code block as security-location.conf file inside your /etc/nginx/snippets directory.</p><p>Then include the file inside your server {} block like below,</p><pre>include /etc/nginx/snippets/security-location.conf;</pre><p>Setting access_log off and log_not_found off can prevent your access log from being spammed. but still if you want them on your access log for security purposes then you can set it as access_log on.</p><h3>3. HTTP Security Headers</h3><p>This is the most crucial part. Injecting the right HTTP headers protects your users from Cross-Site Scripting (XSS), clickjacking, and MIME-type sniffing or various attacks. It reduces attack surfaces greatly. I have covered all the most important security headers except the content security policy header. Since that header need to be configured according to your application.</p><pre># Prevent clickjacking by restricting who can embed your site in an iframe<br>add_header X-Frame-Options &quot;SAMEORIGIN&quot; always;<br><br># Force the browser&#39;s native XSS filter<br>add_header X-XSS-Protection &quot;1; mode=block&quot; always;<br><br># Prevent MIME-sniffing (forces the browser to respect the declared content type)<br>add_header X-Content-Type-Options &quot;nosniff&quot; always;<br><br># Control how much referrer information is passed to external sites<br>add_header Referrer-Policy &quot;same-origin&quot; always;<br><br># Restrict access to device hardware (camera, microphone, location)<br>add_header Permissions-Policy &quot;geolocation=(), microphone=(), camera=()&quot; always;<br><br># Enforce HTTPS connections for a full year, including subdomains<br>add_header Strict-Transport-Security &quot;max-age=31536000; includeSubDomains; preload&quot; always;</pre><p><strong>How to apply it:</strong></p><p>Save the file as /etc/nginx/snippets/security-headers.conf file. You can then include this file in your http {}, server {}, or even specific location {} blocks depending on which application you will need to apply those headers.</p><h3>Wrapping Up</h3><p>By splitting your security configurations into security-global.conf, security-location.conf and security-headers.conf, you can keep your main site configurations incredibly clean. Your typical virtual host file now only needs three simple include statements to achieve a robust baseline of security.</p><p>Note: Always remember to test your configuration before reloading the web server</p><pre>nginx -t<br>systemctl reload nginx</pre><p>Security is layered. While Nginx hardeing won’t fix vulnerable application code but dropping malicious traffic at the edge saves your backend resources and mitigates entire classes of common automated attacks.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=33c9621021d2" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to monitor Hazelcast Metrics using JMX exporter]]></title>
            <link>https://nirzak.medium.com/how-to-monitor-hazelcast-metrics-using-jmx-exporter-50e335435c60?source=rss-5c3352923df3------2</link>
            <guid isPermaLink="false">https://medium.com/p/50e335435c60</guid>
            <category><![CDATA[jmx-exporter]]></category>
            <category><![CDATA[hazelcast]]></category>
            <category><![CDATA[prometheus]]></category>
            <category><![CDATA[jmx]]></category>
            <dc:creator><![CDATA[Nirjas Jakilim]]></dc:creator>
            <pubDate>Sat, 19 Jul 2025 17:20:03 GMT</pubDate>
            <atom:updated>2025-07-19T17:20:03.287Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3uev0entm76-hLJpnjVGMQ.png" /></figure><p>As distributed systems become more complex, monitoring becomes crucial for maintaining application performance and reliability. If you’re using Hazelcast as your in-memory data grid, you’ll want to keep a close eye on its performance metrics. Today, I’ll walk you through setting up JMX exporter to monitor Hazelcast metrics effectively.</p><h3>Why Monitor Hazelcast Metrics?</h3><p>Before diving into the setup, let’s understand why monitoring Hazelcast is essential. Hazelcast provides valuable insights into:</p><ul><li>Memory usage and distribution</li><li>Network communication patterns</li><li>Cache hit/miss ratios</li><li>Queue sizes and processing times</li><li>Connection health and latency</li></ul><p>These metrics help you optimize performance, troubleshoot issues, and make informed scaling decisions.</p><h3>Prerequisites</h3><p>Before we start, make sure you have:</p><ul><li>Hazelcast instance (embedded or standalone)</li><li>JMX Prometheus Exporter JAR file</li><li>Basic understanding of JVM options and YAML configuration</li></ul><h3>Step 1: Enabling JMX API</h3><p>The first step is enabling JMX (Java Management Extensions) on your Hazelcast instance. The process differs slightly between embedded and standalone deployments.</p><h3>For Embedded Hazelcast</h3><p>When running Hazelcast embedded within your application, add these JVM arguments to your application’s startup command:</p><pre>-Dcom.sun.management.jmxremote<br>-Dcom.sun.management.jmxremote.port=&lt;portNo&gt;<br>-Dcom.sun.management.jmxremote.authenticate=false<br>-Dhazelcast.jmx=true<br>-javaagent:path_to_jmx_exporter_jar_file=&lt;port&gt;:/path_to_jmx_exporter_config/config.yaml</pre><p><strong>Important Security Note</strong>: Set -Dcom.sun.management.jmxremote.authenticate=true in production environments and configure proper authentication.</p><h3>For Standalone Hazelcast</h3><p>For standalone Hazelcast instances, navigate to your Hazelcast installation directory and edit the config/jvm.options file:</p><pre>cd $HAZELCAST_HOME<br>vim config/jvm.options</pre><p>Add the same JVM arguments mentioned above to this file.</p><h3>Step 2: Configuring JMX Exporter</h3><p>Create a configuration file (typically named config.yaml) for the JMX exporter. This file defines how JMX metrics are transformed into Prometheus format:</p><pre>#---<br>attrNameSnakeCase: true<br>lowercaseOutputName: true<br>lowercaseOutputLabelNames: true<br>whitelistObjectNames:<br>  - &quot;com.hazelcast:type=Metrics,*&quot;<br><br>rules:<br>  - pattern: &quot;^com.hazelcast&lt;type=Metrics, instance=(.*), prefix=(.*), tag([0-9]+)=(.*)&gt;&lt;&gt;(.+):&quot;<br>    name: hazelcast_$5<br>    attrNameSnakeCase: true<br>    labels:<br>      instance: $1<br>      prefix: $2<br>      tag$3: $4<br><br>  - pattern: &quot;^com.hazelcast&lt;type=Metrics, instance=(.*), prefix=(.*)&gt;&lt;&gt;(.+):&quot;<br>    name: hazelcast_$3<br>    attrNameSnakeCase: true<br>    labels:<br>      instance: $1<br>      prefix: $2</pre><ul><li><strong>attrNameSnakeCase</strong>: Converts attribute names to snake_case format</li><li><strong>lowercaseOutputName</strong>: Ensures metric names are lowercase</li><li><strong>whitelistObjectNames</strong>: Filters JMX objects to include only Hazelcast metrics</li><li><strong>rules</strong>: Define patterns for transforming JMX metric names into Prometheus format</li></ul><h3>Step 3: Restart and Verify</h3><p>After configuring everything, restart your Java application or Hazelcast instance. The metrics should become available at:</p><pre>http://&lt;server_ip&gt;:&lt;jmx_exporter_port&gt;/metrics</pre><h3>Understanding the Metrics Format</h3><p>Once everything is running, you’ll see metrics in Prometheus format. Here are some examples of the metrics:</p><pre>hazelcast_total_max_get_latency{instance=&quot;hz-instance&quot;,prefix=&quot;map&quot;,tag0=&quot;name=request-cache&quot;} 1.0<br>hazelcast_priority_queue_size{instance=&quot;hz-instance&quot;,prefix=&quot;operation&quot;} 0.0<br>hazelcast_connection_type{instance=&quot;hazelcastInstance&quot;,prefix=&quot;tcp.connection&quot;,tag0=&quot;endpoint=[localhost]:5000&quot;,tag1=&quot;bindAddress=[testserver]:5000&quot;} 1.0</pre><p>Each metric includes:</p><ul><li><strong>Metric name</strong>: Descriptive name of what’s being measured</li><li><strong>Labels</strong>: Key-value pairs like instance, prefix and tags</li><li><strong>Value</strong>: The actual measurement</li></ul><h3>Common Troubleshooting Tips</h3><p>Based on my experience implementing this setup, here are some issues you might encounter:</p><p><strong>Port Conflicts</strong>: Ensure your JMX and exporter ports don’t conflict with other services.</p><p><strong>Configuration Path Issues</strong>: Double-check that the path to your config.yaml file is absolute and accessible.</p><p><strong>Firewall Restrictions</strong>: Make sure the specified ports are open and accessible from your monitoring systems.</p><p><strong>Memory Overhead</strong>: JMX monitoring adds some overhead, so monitor your application’s memory usage after enabling it.</p><h3>Best Practices</h3><ol><li><strong>Security First</strong>: Always enable authentication in production environments</li><li><strong>Resource Monitoring</strong>: Keep an eye on the additional memory and CPU overhead</li><li><strong>Selective Metrics</strong>: Use whitelist patterns to export only the metrics you need</li><li><strong>Regular Updates</strong>: Keep your JMX exporter version updated for better performance and security</li></ol><h3>Integration with Monitoring Stack</h3><p>Once you have metrics exposed, you can integrate them with your existing monitoring stack:</p><ul><li><strong>Prometheus</strong>: Configure Prometheus to scrape the metrics endpoint</li><li><strong>Grafana</strong>: Create dashboards to visualize Hazelcast performance</li><li><strong>AlertManager</strong>: Set up alerts for critical thresholds</li></ul><h3>Conclusion</h3><p>Monitoring Hazelcast with JMX exporter provides valuable insights into your distributed cache performance. While the initial setup requires some configuration, the visibility you gain into your system’s behavior is invaluable for maintaining robust applications.</p><p>The key is starting simple and gradually expanding your monitoring scope as you understand which metrics matter most for your specific use case. Remember to balance comprehensive monitoring with system performance, and always prioritize security in production environments.</p><p>If you have already implemented this on your environment then I’d love to hear about your experiences and any additional tips you’ve discovered along the way! Thanks for your time!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=50e335435c60" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Jenkins Plugin Update Workaround for Offline or Proxy-Restricted Environments]]></title>
            <link>https://nirzak.medium.com/jenkins-plugin-update-workaround-for-offline-or-proxy-restricted-environments-312a2caba10a?source=rss-5c3352923df3------2</link>
            <guid isPermaLink="false">https://medium.com/p/312a2caba10a</guid>
            <category><![CDATA[proxy]]></category>
            <category><![CDATA[jenkins]]></category>
            <category><![CDATA[firewall]]></category>
            <category><![CDATA[ci-cd-tools]]></category>
            <dc:creator><![CDATA[Nirjas Jakilim]]></dc:creator>
            <pubDate>Mon, 14 Jul 2025 15:49:47 GMT</pubDate>
            <atom:updated>2025-07-14T15:52:52.463Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*IcfHi4plPfZynj4RQ6EE6A.png" /></figure><p>When deploying Jenkins in a secure environment — like behind a corporate firewall or proxy, it’s often creates problems in plugin installations or updates. Because Jenkins fetches plugin metadata and files from dynamic CDN URLs, which makes it hard to whitelist exact endpoints on your proxy or firewall.</p><p>In this guide, I’ll walk you through how to faciliates jenkins plugin updates using a static URL, making it possible to fetch and install plugins without opening up your network to all of Jenkins’ CDNs.</p><h3>The Problem</h3><p>Jenkins plugin updates rely on URLs that resolve to CDNs or change frequently. In a restricted environment:</p><ul><li>Your Jenkins host may have <strong>no direct internet access</strong>.</li><li>All outbound traffic must go through a <strong>proxy or firewall</strong>.</li><li>Whitelisting all possible cdn endpoints is not feasible.</li></ul><p><strong>Solution:</strong> Generate your own update-center.json file, sign it with your own certificate, and serve it from a local or approved static host. Then configure Jenkins to use this static URL as the update site.</p><h3>Step-by-Step Solution</h3><h4>Step 1: Prepare the update-center.json on Any Internet-Connected Machine</h4><ol><li>Clone the <a href="https://github.com/Nirzak/jenkins-update-center">Nirzak/jenkins-update-center</a> repo: (Thanks to lewark)</li></ol><pre>git clone https://github.com/Nirzak/jenkins-update-center.git<br>cd jenkins-update-center</pre><p>2. Remove any existing certificates: (Skip this step if you want to use the repo certificate)</p><pre>rm -rf rootCA/update-center.crt rootCA/update-center.key</pre><p>3. Edit the mirrors.json file and replace the content with:</p><pre>{   &quot;jenkins&quot;: &quot;https://archives.jenkins.io/&quot; }</pre><p>Here, I used archives.jenkins.io as the mirror. you can also use any other mirror url if you want. <strong>You need to allow the mirror url from your proxy or firewall.</strong></p><p>4. Generate a new self-signed cert and key: (Not needed if you skipped step 2)</p><pre>openssl genrsa -out rootCA/update-center.key 2048</pre><pre>openssl req -new -x509 -days 3650 -key rootCA/update-center.key -out rootCA/update-center.crt</pre><p>5. Install required Python packages:</p><pre>pip3 install -r requirements.txt</pre><p>6. Generate the custom update-center.json file:</p><pre>python3 generator.py</pre><p>Your generated update-center.json will be available in the &lt;current_directory&gt;/updates/jenkins directory.</p><h3>Step 2: Setup on Jenkins Host</h3><ol><li>Copy the generated update-center.json and update-center.crt to your Jenkins host.</li><li>Create the required directory:</li></ol><pre>mkdir -p ${JENKINS_HOME}/update-center-rootCAs</pre><p>3. Move the cert file:</p><pre>cp rootCA/update-center.crt ${JENKINS_HOME}/update-center-rootCAs</pre><h3>Step 3: Host the update-center.json</h3><p>Use Python’s built-in HTTP server (or any simple web server to serve the file):</p><pre>cd path/to/update-center.json<br>nohup python3 -m http.server 8000 &amp;</pre><p>This will serve the file at:</p><pre>http://your-host:8000/update-center.json</pre><h3>Step 4: Configure Jenkins to Use the New Update Site</h3><ol><li>Open Jenkins UI.</li><li>Navigate to: <strong>Manage Jenkins → Manage Plugins → Advanced</strong>.</li><li>In the <strong>Update Site URL</strong> field, enter:</li></ol><pre>http://your-host:8000/update-center.json</pre><p>Click <strong>Submit</strong> to save.</p><h3>All done!</h3><p>Now go to the <strong>Available</strong> or <strong>Updates</strong> tab under <strong>Manage Plugins</strong>, and you should be able to install or update any plugin if you allow the mirror url from your proxy or firewall. Now you don’t need to allow all the other cdn mirror urls from your proxy since it’s only using a static url.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=312a2caba10a" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Getting Started with GitHub Actions]]></title>
            <link>https://nirzak.medium.com/getting-started-with-github-actions-ff3a0fd7005f?source=rss-5c3352923df3------2</link>
            <guid isPermaLink="false">https://medium.com/p/ff3a0fd7005f</guid>
            <category><![CDATA[continuous-delivery]]></category>
            <category><![CDATA[continuous-integration]]></category>
            <category><![CDATA[github-actions]]></category>
            <category><![CDATA[cicd-tools]]></category>
            <category><![CDATA[github]]></category>
            <dc:creator><![CDATA[Nirjas Jakilim]]></dc:creator>
            <pubDate>Thu, 27 Jul 2023 17:50:55 GMT</pubDate>
            <atom:updated>2023-07-30T16:15:54.789Z</atom:updated>
            <content:encoded><![CDATA[<p>Every software industry nowadays is adapting continuous integration and continuous delivery (CI/CD) platforms to reduce time and effort in the software release process. One such CI/CD platform is Github’s Github Actions. Before starting, if you don’t know what CI/CD is, then let me tell you, CI (Continuous Integration) basically means automating a build and test phase of a software release cycle. And CD (Continuous Delivery) means to deliver or release it to the customer continuously. So, together they complete a whole release cycle.</p><p>Now, let’s get back to Github Action which itselft is a CI/CD solution. Github Actions allows a developer to automate worflows like issues, pull requests, commits, merging almost eveything within github. Now it can be creating a pull request, merging it, assigning someone into the pull request. In fact you can also set organization rules, for example, assign developer permissions etc using this feature.</p><p>Now let’s take a dive into the details of Github Action. Every Github Action is a workflow that consists of some core components like events, jobs, steps, actions, and runners. Let’s explore them in detail.</p><p><strong>Workflow:</strong></p><p>A workflow is a configurable automated process consisting of one or multiple jobs running parallelly triggered by events. They are typically declared in YAML files and reside inside .github/workflows directory of a GitHub repository.</p><p><strong>Events:</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*mODkFseBVPXCf5Hb.png" /></figure><p>Events are basically the defined triggers that start a workflow. For example: Creating a pull request, Pushing a commit into the repository, A defined scheduled cron time, Merging Pull requests etc can be an example of events.</p><p><strong>Jobs:</strong></p><p>Jobs are the executed tasks in a workflow that runs after an event is triggered. A workflow can have one or multiple jobs running in parallel.</p><p><strong>Actions:</strong></p><p>These are the most interesting things in a GitHub workflow. Actions are basically some particular tasks bound as a single package that you can use in any action workflow or you can also make your own.</p><p><strong>Runner:</strong></p><p>A runner is a series of tasks in a workflow that are executed in a single job. Every runner is responsible for executing a single job.</p><p><strong>Setting Up A Basic Action Workflow:</strong></p><p>Now let’s set up a basic action workflow to see how it works. To do that, let’s create a demo repository on GitHub.</p><ol><li>Now inside that repository create a directory named .github and inside that .gihub directory create another directory workflow. So, basically it will be .github/workflows</li><li>Inside that .github/workflows directory create a yaml file named demo-workflow.yml</li><li>Copy the following yaml config into that file. (Courtesy of Github Docs)</li></ol><pre>name: GitHub Actions Demo<br>on: [push]<br>jobs:<br> Explore-GitHub-Actions:<br> runs-on: ubuntu-latest<br> steps:<br> — run: echo “🎉 The job was automatically triggered by a ${{ github.event_name }} event.”<br> — run: echo “🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!”<br> — run: echo “🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}.”<br> — name: Check out repository code<br> uses: actions/checkout@v3<br> — run: echo “💡 The ${{ github.repository }} repository has been cloned to the runner.”<br> — run: echo “🖥️ The workflow is now ready to test your code on the runner.”<br> — name: List files in the repository<br> run: |<br> ls ${{ github.workspace }}<br> — run: echo “🍏 This job’s status is ${{ job.status }}.”</pre><p>4. Save it and then just push a random commit into that repository and see the workflow in action. Here, We have used [push] as a trigger, so it will trigger for every commit that has been pushed into that repository.</p><p>Now let’s elaborate on the above workflow in detail.</p><ol><li>Here on the first line “<strong>name:”</strong> is basically the workflow name. You can set this to anything that resembles that workflow. If you omit “<strong>name:”</strong>, GitHub will set it to the workflow file path relative to the root of the repository.</li><li>In the next line “<strong>on:”</strong> is basically used for the trigger purpose. This is our event trigger. Here, we can use more event triggers like fork, created, and deleted and also particular events like pull_requests, issues etc. for example:</li></ol><pre>on:<br>  pull_request:<br>    types: [opened, reopened]</pre><p>This will trigger an event whenever a pull request is created or reopened. Also, you can create multiple custom labels and use them as a trigger using the label’s name. For more action triggers take a look at this article: <a href="https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows">https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows</a></p><p>3. The third line is the jobs part. Here we will define our tasks that will run after the event gets triggered. Inside jobs, there’s a section titled “<strong>runs-on:</strong>” this is basically the environment where the tasks will be executed. In the above case, this is “<strong>ubuntu-latest”</strong>.</p><p>4. In the steps section, we have defined an “<strong>uses:</strong>” section. This is basically the action’s part. Here, you can use any pre-defined action workflow or make your own action workflow and then can use it by just mentioning the action name like this one actions/checkout@v3. Here, we are using checkout action workflow which basically clones our repo to a runner, so that we can further run tests on it, deploy it or perform any other tasks on the code.</p><p>So, guys that’s all for now. To learn more about github actions take a look into github’s official documentation about Github Actions here: <a href="https://docs.github.com/en/actions/learn-github-actions">https://docs.github.com/en/actions/learn-github-actions</a><br>Happy Automating!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ff3a0fd7005f" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>