<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Ivan Salloum</title><description>Practical Linux server administration and security tutorials, plus ordered written courses, by Ivan Salloum.</description><link>https://ivansalloum.com/</link><item><title>Your First Authoritative DNS Server with CoreDNS</title><link>https://ivansalloum.com/your-first-authoritative-dns-server-with-coredns/</link><guid isPermaLink="true">https://ivansalloum.com/your-first-authoritative-dns-server-with-coredns/</guid><description>Learn how to set up your own authoritative DNS server for full DNS control and easy custom record management.</description><pubDate>Sat, 06 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;If you’re like me, you love self-hosting your own projects. But as the number of services you run grows, so does the list of DNS records.&lt;/p&gt;&lt;p&gt;I used to log into my Cloudflare dashboard constantly, adding &lt;code&gt;A&lt;/code&gt; record after &lt;code&gt;A&lt;/code&gt; record. It felt messy, and I ended up with dozens of entries for my main domain.&lt;/p&gt;&lt;p&gt;I wanted a cleaner solution – a way to isolate my self-hosted projects (like &lt;a href=&quot;https://ivansalloum.com/how-to-run-beszel-for-server-monitoring/&quot;&gt;Beszel&lt;/a&gt;) from my main site, gain full control over their DNS, and maybe even learn a thing or two about how DNS really works under the hood.&lt;/p&gt;&lt;p&gt;That’s where CoreDNS comes in. It’s a tiny, incredibly fast, and easy-to-configure DNS server that’s perfect for this. With a single configuration file and a lightweight Docker container, you can build your own &lt;strong&gt;authoritative DNS server&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;In this guide, I&apos;ll walk you through the exact steps I took to set up my own authoritative DNS server for a subdomain.&lt;/p&gt;&lt;p&gt;We’re going to build the official source of truth for a dedicated zone, like &lt;code&gt;lab.yourdomain.com&lt;/code&gt;. It’s a game-changer for managing a growing collection of self-hosted services.&lt;/p&gt;&lt;p&gt;Any public DNS query for &lt;code&gt;*.lab.yourdomain.com&lt;/code&gt; will be correctly delegated and answered by &lt;em&gt;your&lt;/em&gt; CoreDNS server, giving you full, instant control over your records.&lt;/p&gt;&lt;p&gt;Don’t worry if this is new – I&apos;ll walk you through it, step by step.&lt;/p&gt;&lt;h2&gt;The Big Picture: What We&apos;re Building (and What We&apos;re Not)&lt;/h2&gt;&lt;p&gt;Before we get our hands dirty, let’s zoom out.&lt;/p&gt;&lt;p&gt;Our goal is to build our very own &lt;strong&gt;authoritative DNS server&lt;/strong&gt;. Think of it as creating a definitive, private address book for our own projects.&lt;/p&gt;&lt;p&gt;This guide focuses on using a &lt;strong&gt;subdomain&lt;/strong&gt; (like &lt;code&gt;lab.yourdomain.com&lt;/code&gt;) for this project, with our CoreDNS server living at &lt;code&gt;ns1.lab.yourdomain.com&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;This approach is known as using an &lt;strong&gt;&amp;quot;in-bailiwick&amp;quot; nameserver&lt;/strong&gt; , a fancy way of saying the nameserver (&lt;code&gt;ns1.lab...&lt;/code&gt;) resides within the same subdomain (&lt;code&gt;lab...&lt;/code&gt;) that it manages.&lt;/p&gt;&lt;p&gt;It’s a clean and scalable way to structure your DNS. I chose this method to neatly separate my lab services from my main site without needing to buy a new domain, and it&apos;s a fantastic, low-cost way to learn if you already own one.&lt;/p&gt;&lt;p&gt;If you decide to create another DNS environment later (like &lt;code&gt;dev.yourdomain.com&lt;/code&gt;), you can repeat the pattern without everything getting tangled up.&lt;/p&gt;&lt;p&gt;However, the principles here can also be adapted if you want to use a dedicated root domain (e.g., &lt;code&gt;your-new-cool-project.com&lt;/code&gt;).&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;_&lt;strong&gt;A quick note on SEO:&lt;/strong&gt; _A common worry is whether using a subdomain will hurt your main site’s search engine ranking. Don’t worry, it won’t. Google and other search engines treat subdomains as separate properties. As long as your projects don’t create security issues, your main domain’s SEO will be completely unaffected.&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;Authoritative vs. Recursive: A Crucial Distinction&lt;/h3&gt;&lt;p&gt;It is vital to understand the type of server we are building.&lt;/p&gt;&lt;p&gt;A DNS server can play two main roles:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Authoritative Server (What we are building):&lt;/strong&gt; This server holds the &amp;quot;master copy&amp;quot; of records for a specific zone. It only answers questions about domains it controls. When asked about &lt;code&gt;beszel.lab.ivansalloum.com&lt;/code&gt;, it confidently says, &amp;quot;I know the answer! It&apos;s &lt;code&gt;192.168.1.100&lt;/code&gt;.&amp;quot; It is the source of truth.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Recursive Resolver (What we are NOT building):&lt;/strong&gt; This is the type of DNS server you typically use for your computer or home router (like Cloudflare&apos;s &lt;code&gt;1.1.1.1&lt;/code&gt; or Google&apos;s &lt;code&gt;8.8.8.8&lt;/code&gt;). Its job is to go out and find the answer to &lt;em&gt;any&lt;/em&gt; query. When you ask it for &lt;code&gt;google.com&lt;/code&gt;, it recursively talks to other DNS servers across the internet to find the correct IP address for you.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Our configuration is &lt;strong&gt;purely authoritative&lt;/strong&gt;. It knows &lt;em&gt;nothing&lt;/em&gt; about the outside world.&lt;/p&gt;&lt;p&gt;If we allow our server to answer generic queries for anyone on the internet, it would become an &lt;strong&gt;&amp;quot;Open Resolver.&amp;quot;&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Hackers actively hunt for these unsecured servers to launch massive DDoS attacks (called Amplification Attacks) against victims. By keeping our server strictly Authoritative, we keep our infrastructure – and the internet – safer.&lt;/p&gt;&lt;p&gt;❗&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Do NOT set this server as the primary DNS on your computer or router.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;If you point your laptop&apos;s DNS settings only to your new CoreDNS server and then try to visit &lt;code&gt;google.com&lt;/code&gt;, this is what happens:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Your server looks at its own &lt;code&gt;Corefile&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;It sees it&apos;s only responsible for &lt;code&gt;lab.yourdomain.com&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;It essentially replies, &amp;quot;I don&apos;t know who &lt;code&gt;google.com&lt;/code&gt; is, and it&apos;s not my job to find out.&amp;quot;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The request will fail, and for you, the internet will appear &amp;quot;broken.&amp;quot;&lt;/p&gt;&lt;p&gt;Our server is only meant to be found by public resolvers (like Cloudflare or Google) that are looking for answers about our specific subdomain.&lt;/p&gt;&lt;h2&gt;Preparing Your Server&lt;/h2&gt;&lt;p&gt;Before we dive into CoreDNS specifics, we need to get your server ready.&lt;/p&gt;&lt;p&gt;I’m writing this guide based on my experience with an &lt;strong&gt;Ubuntu 24.04 LTS&lt;/strong&gt; server hosted on a &lt;strong&gt;Hetzner VPS&lt;/strong&gt; , but the steps should be broadly applicable to most Linux distributions.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;I run everything on Hetzner because it’s cost-effective and reliable – if you’d like to try them, here’s my &lt;a href=&quot;https://hetzner.cloud/?ref=MC4Yy318xX5X&quot;&gt;referral link&lt;/a&gt;.&lt;/p&gt;&lt;h3&gt;Set Your Server&apos;s Hostname&lt;/h3&gt;&lt;p&gt;It’s good practice to give your server a meaningful hostname.&lt;/p&gt;&lt;p&gt;For this setup, it makes sense to use the nameserver&apos;s own FQDN (Fully Qualified Domain Name):&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo hostnamectl set-hostname ns1.lab.ivansalloum.com
&lt;/pre&gt;&lt;p&gt;This command updates your server&apos;s hostname to reflect its role as the nameserver for your lab environment.&lt;/p&gt;&lt;p&gt;Remember to replace &lt;code&gt;ivansalloum.com&lt;/code&gt; with your actual domain!&lt;/p&gt;&lt;h3&gt;Register Your Nameserver&apos;s IP (Glue Records)&lt;/h3&gt;&lt;p&gt;It&apos;s absolutely crucial to register the IP addresses for your nameserver with your domain registrar or DNS provider. These are often called &amp;quot;glue records&amp;quot; and they tell the internet where &lt;code&gt;ns1.lab.ivansalloum.com&lt;/code&gt; actually lives.&lt;/p&gt;&lt;p&gt;Without them, no one will be able to find your custom DNS server.&lt;/p&gt;&lt;p&gt;You&apos;ll need to create &lt;strong&gt;A&lt;/strong&gt; and optionally &lt;strong&gt;AAAA&lt;/strong&gt; records for your nameserver&apos;s hostname (&lt;code&gt;ns1.lab.ivansalloum.com&lt;/code&gt;) at your DNS provider.&lt;/p&gt;&lt;p&gt;Ensure these records are set up to directly point to your server&apos;s IP addresses and are &lt;strong&gt;not proxied&lt;/strong&gt; if your provider offers that option (e.g., Cloudflare&apos;s orange cloud). DNS traffic for nameservers must hit your server directly.&lt;/p&gt;&lt;p&gt;Also, do &lt;em&gt;not&lt;/em&gt; add an &lt;code&gt;A&lt;/code&gt; record for just &lt;code&gt;lab&lt;/code&gt; at this stage; we are only creating records for &lt;code&gt;ns1.lab&lt;/code&gt; itself. The &lt;code&gt;lab&lt;/code&gt; subdomain will be delegated later.&lt;/p&gt;&lt;h3&gt;Free Up Port 53 (Stop &lt;code&gt;systemd-resolved&lt;/code&gt;)&lt;/h3&gt;&lt;p&gt;DNS services communicate primarily over port 53.&lt;/p&gt;&lt;p&gt;&lt;code&gt;systemd-resolved&lt;/code&gt; (Ubuntu’s local DNS stub) might already be listening on this port, preventing CoreDNS from binding to it. We need to disable its stub listener.&lt;/p&gt;&lt;p&gt;You can check if port 53 is in use with:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo lsof -i :53
&lt;/pre&gt;&lt;p&gt;If you see &lt;code&gt;systemd-resolved&lt;/code&gt; listed, follow these steps to disable it.&lt;/p&gt;&lt;p&gt;First, create the directory for &lt;code&gt;systemd-resolved&lt;/code&gt; overrides:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo mkdir -p /etc/systemd/resolved.conf.d/
&lt;/pre&gt;&lt;p&gt;Then, set appropriate permissions for the newly created directory:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo chmod 755 /etc/systemd/resolved.conf.d/
&lt;/pre&gt;&lt;p&gt;Next, disable the &lt;code&gt;DNSStubListener&lt;/code&gt; by writing the configuration to a file:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;printf &amp;quot;[Resolve]\nDNSStubListener=no\n&amp;quot; | sudo tee /etc/systemd/resolved.conf.d/noresolved.conf
&lt;/pre&gt;&lt;p&gt;Finally, restart &lt;code&gt;systemd-resolved&lt;/code&gt; to apply the changes:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo systemctl restart systemd-resolved.service
&lt;/pre&gt;&lt;p&gt;After restarting, &lt;code&gt;sudo lsof -i :53&lt;/code&gt; should no longer show &lt;code&gt;systemd-resolved&lt;/code&gt; listening.&lt;/p&gt;&lt;h3&gt;Configure Your Firewall (UFW)&lt;/h3&gt;&lt;p&gt;For your CoreDNS server to receive queries, you&apos;ll need to open the necessary ports in your firewall.&lt;/p&gt;&lt;p&gt;DNS traffic primarily uses UDP port 53, but TCP port 53 is also essential:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw allow 53/udp sudo ufw allow 53/tcp sudo ufw enable # If UFW is not already enabled
&lt;/pre&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Why both UDP and TCP?&lt;/strong&gt; Almost all DNS queries use UDP because it&apos;s fast and lightweight. However, if a DNS response is too large, or for critical operations like zone transfers (especially important if you ever add a secondary DNS server), TCP is used.&lt;/p&gt;&lt;h3&gt;Install Docker&lt;/h3&gt;&lt;p&gt;We&apos;ll be running CoreDNS inside a Docker container for ease of deployment and management.&lt;/p&gt;&lt;p&gt;It&apos;s crucial to use up-to-date Docker Engine and Docker Compose versions because Ubuntu&apos;s default repositories often contain outdated packages.&lt;/p&gt;&lt;p&gt;For optimal performance and the latest features, always rely on Docker&apos;s official repositories.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;If you need a detailed walkthrough, I&apos;ve got a dedicated guide: &lt;a href=&quot;https://ivansalloum.com/installing-docker-and-docker-compose-on-ubuntu/&quot;&gt;Installing Docker and Docker Compose on Ubuntu (Using Docker’s Official Repository)&lt;/a&gt;.&lt;/p&gt;&lt;h3&gt;Create CoreDNS Directories&lt;/h3&gt;&lt;p&gt;We need a dedicated place for CoreDNS&apos;s configuration and zone files:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo mkdir -p /opt/coredns/{config,zones} sudo chmod -R 755 /opt/coredns
&lt;/pre&gt;&lt;p&gt;With these preparations done, your server is ready to host your authoritative CoreDNS instance!&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;&lt;strong&gt;My Full Server Preparation Checklist&lt;/strong&gt; We&apos;ve just done the necessary prep for CoreDNS. If you&apos;d like to see my complete checklist for setting up a new Ubuntu server from scratch for optimal security and performance, I&apos;ve detailed everything in this guide: &lt;a href=&quot;https://ivansalloum.com/preparing-your-ubuntu-server-for-first-use/&quot;&gt;Preparing Your Ubuntu Server for First Use&lt;/a&gt;&lt;/p&gt;&lt;h2&gt;Creating Your First Zone File&lt;/h2&gt;&lt;p&gt;Now for the fun part. We need to create a &lt;strong&gt;zone file&lt;/strong&gt;. This is a plain text file that acts as the database for your subdomain.&lt;/p&gt;&lt;p&gt;It contains all the DNS records – like &lt;code&gt;A&lt;/code&gt;, &lt;code&gt;AAAA&lt;/code&gt;, &lt;code&gt;CNAME&lt;/code&gt;, etc. – that tell the world where to direct traffic for hosts within your zone.&lt;/p&gt;&lt;p&gt;First, navigate to the &lt;code&gt;zones&lt;/code&gt; directory we created earlier:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;cd /opt/coredns/zones
&lt;/pre&gt;&lt;p&gt;Next, create and open the zone file. It&apos;s a common convention to name this file &lt;code&gt;db.&lt;/code&gt; followed by your zone name. In my case, it&apos;s &lt;code&gt;db.lab.ivansalloum.com&lt;/code&gt;:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo vim db.lab.ivansalloum.com
&lt;/pre&gt;&lt;p&gt;Now, paste the following template into the file. This is the heart of your DNS setup:&lt;/p&gt;&lt;pre data-language=&quot;ini&quot;&gt;$ORIGIN lab.ivansalloum.com. $TTL 3600 @ IN SOA ns1.lab.ivansalloum.com. admin.ivansalloum.com. ( 2025120601 ; Serial (YYYYMMDDnn) - CHANGE THIS ON EVERY EDIT! 7200 ; Refresh (2 hours) 120 ; Retry (2 minutes) 2419200 ; Expire (28 days) 3600 ; Negative Cache TTL (1 hour) ) IN NS ns1.lab.ivansalloum.com. ns1 IN A YOUR_SERVER_IPV4 ns1 IN AAAA YOUR_SERVER_IPV6 ; --- Your Custom Records Go Here --- beszel IN A 192.168.1.100 hestia IN A 192.168.1.1 ; --- Optional Wildcard For Instant Testing --- * IN A 192.168.0.10
&lt;/pre&gt;&lt;p&gt;Once you&apos;ve pasted the template, here are the three critical edits you must make before saving:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Go through the template and replace&lt;ol&gt;&lt;li&gt;&lt;code&gt;lab.ivansalloum.com.&lt;/code&gt;,&lt;/li&gt;&lt;li&gt;&lt;code&gt;ns1.lab.ivansalloum.com.&lt;/code&gt;,&lt;/li&gt;&lt;li&gt;and &lt;code&gt;admin.ivansalloum.com.&lt;/code&gt;&lt;/li&gt;&lt;li&gt;with your actual subdomain, nameserver FQDN, and admin email.&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;li&gt;Replace &lt;code&gt;YOUR_SERVER_IPV4&lt;/code&gt; and &lt;code&gt;YOUR_SERVER_IPV6&lt;/code&gt; with the actual public IP addresses of your CoreDNS server. If you don&apos;t use IPv6, simply delete that line entirely.&lt;/li&gt;&lt;li&gt;The line &lt;code&gt;2025120601&lt;/code&gt; is the zone&apos;s version number. It&apos;s crucial that you &lt;strong&gt;increment this number every time you edit the file&lt;/strong&gt;. A common format is &lt;code&gt;YYYYMMDDnn&lt;/code&gt;, where &lt;code&gt;nn&lt;/code&gt; is the revision number for the day. For this first setup, I recommend setting it to today&apos;s date in &lt;code&gt;YYYYMMDDnn&lt;/code&gt; format. This makes it easy to track your initial zone version.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Save the file and exit the editor (&lt;code&gt;:wq&lt;/code&gt; in vim).&lt;/p&gt;&lt;h3&gt;A Note on Zone File Naming&lt;/h3&gt;&lt;p&gt;You might be wondering why we named our zone file &lt;code&gt;db.lab.ivansalloum.com&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;db.&lt;/code&gt; prefix is purely for convention and readability; it&apos;s a holdover from BIND, one of the oldest DNS servers, where &lt;code&gt;db&lt;/code&gt; stood for &amp;quot;database.&amp;quot;&lt;/p&gt;&lt;p&gt;You could name your file &lt;code&gt;records.txt&lt;/code&gt; if you wanted, as long as you point to it correctly in your &lt;code&gt;Corefile&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Sticking to convention, however, makes your setup instantly familiar to other sysadmins (and your future self).&lt;/p&gt;&lt;h3&gt;What Did We Just Create?&lt;/h3&gt;&lt;p&gt;After saving your new zone file, you&apos;ve essentially created the instruction manual for your &lt;code&gt;lab.ivansalloum.com&lt;/code&gt; subdomain. In short, it does two main things:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Declares Authority:&lt;/strong&gt; The &lt;code&gt;SOA&lt;/code&gt; and &lt;code&gt;NS&lt;/code&gt; records act like official stamps, announcing that your server is in charge of this zone.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Builds an Address Book:&lt;/strong&gt; The &lt;code&gt;A&lt;/code&gt;, &lt;code&gt;AAAA&lt;/code&gt;, and other records map your service names (like &lt;code&gt;beszel&lt;/code&gt;) to their IP addresses.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The most important &amp;quot;address book&amp;quot; entries are the &lt;code&gt;A&lt;/code&gt; and &lt;code&gt;AAAA&lt;/code&gt; records for &lt;code&gt;ns1&lt;/code&gt; itself. These are called &lt;strong&gt;glue records&lt;/strong&gt; , and they&apos;re critical because they tell the internet how to find your nameserver in the first place.&lt;/p&gt;&lt;p&gt;Think of it as setting up the front desk (SOA, NS) and then filling out the directory (A, AAAA, etc.) for your new DNS office. Pretty neat, right?&lt;/p&gt;&lt;h2&gt;Configuring CoreDNS with the &lt;code&gt;Corefile&lt;/code&gt;&lt;/h2&gt;&lt;p&gt;Our zone file is ready, holding all our DNS records.&lt;/p&gt;&lt;p&gt;But how do we tell CoreDNS to actually &lt;em&gt;use&lt;/em&gt;  it? This is where the &lt;code&gt;Corefile&lt;/code&gt; comes in, and honestly, its simplicity is the main reason I fell in love with CoreDNS.&lt;/p&gt;&lt;p&gt;Instead of wrestling with complex, multi-file setups, CoreDNS uses one single, human-readable file to manage everything. It’s where we tell CoreDNS what zones to serve, what plugins to use, and how to behave.&lt;/p&gt;&lt;p&gt;Let&apos;s get it set up.&lt;/p&gt;&lt;p&gt;Navigate to the &lt;code&gt;config&lt;/code&gt; directory you created earlier:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;cd /opt/coredns/config
&lt;/pre&gt;&lt;p&gt;Now, create and open the &lt;code&gt;Corefile&lt;/code&gt; itself:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo vim Corefile
&lt;/pre&gt;&lt;p&gt;Paste this small block of text in. This is all you need to get a basic authoritative server running:&lt;/p&gt;&lt;pre data-language=&quot;ini&quot;&gt;lab.ivansalloum.com:53 { log errors file /zones/db.lab.ivansalloum.com }
&lt;/pre&gt;&lt;p&gt;Save and close the file.&lt;/p&gt;&lt;p&gt;This configuration is elegantly powerful. The first line, &lt;code&gt;lab.ivansalloum.com:53&lt;/code&gt;, tells CoreDNS to listen for queries for our zone on port 53.&lt;/p&gt;&lt;p&gt;Inside the block, we enable two incredibly useful plugins, &lt;code&gt;log&lt;/code&gt; and &lt;code&gt;errors&lt;/code&gt;, which are lifesavers for debugging and seeing your server in action.&lt;/p&gt;&lt;p&gt;Finally, the &lt;code&gt;file /zones/db.lab.ivansalloum.com&lt;/code&gt; line is the heart of our setup. This plugin points CoreDNS to our zone file, making it the official, authoritative source for all the records within it. The path &lt;code&gt;/zones/db.lab.ivansalloum.com&lt;/code&gt; is relative to the inside of our future Docker container.&lt;/p&gt;&lt;p&gt;And that&apos;s it. We&apos;ve defined our records and told CoreDNS how to serve them.&lt;/p&gt;&lt;h2&gt;Docker Compose Configuration&lt;/h2&gt;&lt;p&gt;Now that we have our zone file and &lt;code&gt;Corefile&lt;/code&gt; configured, it&apos;s time to bring our CoreDNS server to life. For this, I absolutely love using Docker.&lt;/p&gt;&lt;p&gt;We&apos;ll use &lt;code&gt;docker compose&lt;/code&gt; to define and run our CoreDNS service. This allows us to set up all the necessary parameters – like ports, volumes, and restart policies – in a single, easy-to-read file.&lt;/p&gt;&lt;p&gt;Let&apos;s create our &lt;code&gt;docker-compose.yml&lt;/code&gt; file in the main &lt;code&gt;/opt/coredns&lt;/code&gt; directory:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo vim /opt/coredns/docker-compose.yml
&lt;/pre&gt;&lt;p&gt;Paste the following content into the file:&lt;/p&gt;&lt;pre data-language=&quot;yaml&quot;&gt;services: coredns: image: coredns/coredns:latest container_name: coredns restart: unless-stopped ports: \- &amp;quot;53:53/udp&amp;quot; \- &amp;quot;53:53/tcp&amp;quot; volumes: \- ./config/Corefile:/Corefile:ro \- ./zones:/zones:ro
&lt;/pre&gt;&lt;p&gt;Save and close the file.&lt;/p&gt;&lt;p&gt;This &lt;code&gt;docker-compose&lt;/code&gt; file defines how our CoreDNS server will run. In short, it:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Pulls the official CoreDNS image&lt;/strong&gt;  and names the container &lt;code&gt;coredns&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Sets the  &lt;code&gt;restart&lt;/code&gt; policy to &lt;code&gt;unless-stopped&lt;/code&gt;&lt;/strong&gt;, which is a must-have for any self-hosted service to ensure it automatically starts up after a reboot.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Maps the DNS ports&lt;/strong&gt;  (&lt;code&gt;53/udp&lt;/code&gt; and &lt;code&gt;53/tcp&lt;/code&gt;) from your host server to the container.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Mounts our configuration files&lt;/strong&gt;  (the &lt;code&gt;Corefile&lt;/code&gt; and our &lt;code&gt;zones&lt;/code&gt; directory) into the container in read-only mode, so CoreDNS can use them.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;With this &lt;code&gt;docker-compose.yml&lt;/code&gt; in place, we&apos;re just one command away from launching our very own authoritative DNS server!&lt;/p&gt;&lt;h2&gt;Launching CoreDNS and Local Testing&lt;/h2&gt;&lt;p&gt;We&apos;ve prepared our server, crafted our zone file, and configured CoreDNS. Now, let&apos;s fire up our container and make sure everything is working as expected.&lt;/p&gt;&lt;p&gt;First, navigate to the main &lt;code&gt;/opt/coredns&lt;/code&gt; directory where your &lt;code&gt;docker-compose.yml&lt;/code&gt; file is located:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;cd /opt/coredns
&lt;/pre&gt;&lt;p&gt;Now, launch CoreDNS with Docker Compose in detached mode (&lt;code&gt;-d&lt;/code&gt;), meaning it will run in the background:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo docker compose up -d
&lt;/pre&gt;&lt;p&gt;You should see output indicating that the &lt;code&gt;coredns&lt;/code&gt; container has been created and started.&lt;/p&gt;&lt;p&gt;With our CoreDNS server now running, let&apos;s immediately verify it&apos;s answering queries locally before we go live. This local verification step is crucial as it confirms our setup works before we expose it to the internet.&lt;/p&gt;&lt;p&gt;We&apos;ll use the &lt;code&gt;dig&lt;/code&gt; command-line tool to do this. The trick is to query &lt;em&gt;your own server&apos;s IP address directly&lt;/em&gt; , bypassing any other DNS resolvers. When we query our server directly, we&apos;re bypassing public resolvers to test our server in isolation.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;&lt;strong&gt;What&apos;s a DNS Resolver?&lt;/strong&gt; A DNS resolver is a server (like &lt;code&gt;1.1.1.1&lt;/code&gt; or &lt;code&gt;8.8.8.8&lt;/code&gt;) that clients (your computer) query to find the IP address of a domain name. It handles the entire process of finding the authoritative nameserver and returning the answer to you.&lt;/p&gt;&lt;p&gt;Remember to replace &lt;code&gt;YOUR_SERVER_IPV4&lt;/code&gt; with your server&apos;s actual public IPv4 address.&lt;/p&gt;&lt;p&gt;First, let&apos;s confirm our server is authoritative for the zone by querying the SOA record:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;dig @YOUR_SERVER_IPV4 lab.ivansalloum.com SOA
&lt;/pre&gt;&lt;p&gt;You should see your &lt;code&gt;SOA&lt;/code&gt; record returned, matching the details you put in &lt;code&gt;db.lab.ivansalloum.com&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Next, let&apos;s query one of the specific test records we added, like &lt;code&gt;beszel&lt;/code&gt;:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;dig @YOUR_SERVER_IPV4 beszel.lab.ivansalloum.com
&lt;/pre&gt;&lt;p&gt;This should return the &lt;code&gt;A&lt;/code&gt; record for &lt;code&gt;beszel&lt;/code&gt; (e.g., &lt;code&gt;192.168.1.100&lt;/code&gt;).&lt;/p&gt;&lt;p&gt;Finally, if you included the optional wildcard record, test it:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;dig @YOUR_SERVER_IPV4 anything.lab.ivansalloum.com
&lt;/pre&gt;&lt;p&gt;This should resolve to the IP address defined in your wildcard entry.&lt;/p&gt;&lt;p&gt;If all these &lt;code&gt;dig&lt;/code&gt; commands return the expected answers, congratulations! Your authoritative DNS server is fully functional and ready to serve requests for your subdomain.&lt;/p&gt;&lt;p&gt;The next step is to tell the rest of the internet to start asking &lt;em&gt;your&lt;/em&gt; server for these records.&lt;/p&gt;&lt;p&gt;We&apos;re almost there!&lt;/p&gt;&lt;h2&gt;Delegating Your Subdomain&lt;/h2&gt;&lt;p&gt;Our CoreDNS server is humming along locally.&lt;/p&gt;&lt;p&gt;The final, exhilarating step is to tell the world – specifically, through your domain registrar or DNS provider (in my case, Cloudflare) – that for your chosen subdomain, &lt;em&gt;your&lt;/em&gt; CoreDNS server is now the one in charge. This is called &lt;strong&gt;delegation&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;It&apos;s a powerful moment, making your self-hosted DNS server officially responsible for &lt;code&gt;lab.ivansalloum.com&lt;/code&gt; (or whatever subdomain you chose).&lt;/p&gt;&lt;p&gt;Head over to your domain registrar or DNS provider and add a new &lt;strong&gt;NS (Nameserver)&lt;/strong&gt; record for your subdomain with the following details::&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Type&lt;/strong&gt; : &lt;code&gt;NS&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Name&lt;/strong&gt; : &lt;code&gt;lab&lt;/code&gt; (or your chosen subdomain part, e.g., &lt;code&gt;lab.ivansalloum.com&lt;/code&gt;)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Value&lt;/strong&gt; : &lt;code&gt;ns1.lab.ivansalloum.com.&lt;/code&gt; (your nameserver&apos;s FQDN, with a trailing dot!)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;TTL&lt;/strong&gt; : &lt;code&gt;Auto&lt;/code&gt; is usually fine.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Before you save this record, there are a few crucial sanity checks to perform:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Don&apos;t Forget Your Glue Records:&lt;/strong&gt; This entire process will only work if you&apos;ve already created the &lt;code&gt;A&lt;/code&gt; and &lt;code&gt;AAAA&lt;/code&gt; records for &lt;code&gt;ns1.lab.ivansalloum.com&lt;/code&gt; as we did in the &amp;quot;Preparing Your Server&amp;quot; section. These &amp;quot;glue records&amp;quot; are what allow the internet to find your nameserver in the first place.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Ensure &amp;quot;DNS Only&amp;quot; Mode:&lt;/strong&gt; If your provider (like Cloudflare) offers a proxy service, make sure it is &lt;strong&gt;disabled&lt;/strong&gt; for this NS record (e.g., a &amp;quot;grey cloud&amp;quot; in Cloudflare). The internet needs to be able to reach your nameserver directly.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Avoid Conflicting Records:&lt;/strong&gt; Make sure you do NOT have an &lt;code&gt;A&lt;/code&gt; record for &lt;code&gt;lab&lt;/code&gt; itself. A subdomain can either be delegated (with an &lt;code&gt;NS&lt;/code&gt; record) or point to an IP (with an &lt;code&gt;A&lt;/code&gt; record), but it can&apos;t do both.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Once you&apos;ve confirmed these points and saved your new &lt;code&gt;NS&lt;/code&gt; record, you&apos;re ready for the final validation.&lt;/p&gt;&lt;h2&gt;Public Validation: Confirming Your Delegation&lt;/h2&gt;&lt;p&gt;The CoreDNS server is running, the zone file is configured, and your DNS provider has been updated to delegate your subdomain.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Now comes the exciting part:&lt;/strong&gt; confirming that the entire internet sees your CoreDNS server as the authoritative source for your subdomain.&lt;/p&gt;&lt;p&gt;While DNS changes can sometimes take a while to propagate, subdomain delegation is usually quite fast – I&apos;ve often seen it go live within &lt;strong&gt;1 to 10 minutes&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Once you&apos;ve given it a moment, you can verify your setup from any machine &lt;em&gt;other than your CoreDNS server&lt;/em&gt;. This ensures you&apos;re querying external DNS resolvers, just like the rest of the world. We&apos;ll also use &lt;code&gt;dig&lt;/code&gt; for this.&lt;/p&gt;&lt;p&gt;First, let&apos;s ask a public resolver (like &lt;code&gt;1.1.1.1&lt;/code&gt;) to confirm that our new nameserver is correctly set up for the subdomain:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;dig lab.ivansalloum.com NS @1.1.1.1
&lt;/pre&gt;&lt;p&gt;You should see your nameserver (&lt;code&gt;ns1.lab.ivansalloum.com.&lt;/code&gt;) in the answer section, which confirms the delegation is visible.&lt;/p&gt;&lt;p&gt;Next, let&apos;s test that your CoreDNS server is serving the actual records. Try querying one of the custom records you created in your zone file:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;dig beszel.lab.ivansalloum.com @1.1.1.1
&lt;/pre&gt;&lt;p&gt;This should return the &lt;code&gt;A&lt;/code&gt; record for &lt;code&gt;beszel&lt;/code&gt; that you defined earlier.&lt;/p&gt;&lt;p&gt;If both of these commands work, your authoritative DNS server is officially live and serving records to the internet!&lt;/p&gt;&lt;h2&gt;Managing Your DNS Records&lt;/h2&gt;&lt;p&gt;Now that your authoritative DNS server is live, the best part is how easy it is to manage your records. The process is simple and gives you full control.&lt;/p&gt;&lt;p&gt;Here’s the step-by-step workflow to update or add new records.&lt;/p&gt;&lt;h3&gt;Step 1: Edit Your Zone File&lt;/h3&gt;&lt;p&gt;First, open your zone file:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo vim /opt/coredns/zones/db.lab.ivansalloum.com
&lt;/pre&gt;&lt;p&gt;Now, you can either change an existing record or add new ones.&lt;/p&gt;&lt;p&gt;CoreDNS supports all standard DNS record types, including &lt;code&gt;A&lt;/code&gt;, &lt;code&gt;AAAA&lt;/code&gt;, &lt;code&gt;CNAME&lt;/code&gt;, &lt;code&gt;TXT&lt;/code&gt;, and more.&lt;/p&gt;&lt;p&gt;For example, to add new &lt;code&gt;A&lt;/code&gt;, &lt;code&gt;CNAME&lt;/code&gt;, and &lt;code&gt;TXT&lt;/code&gt; records, you would add lines similar to these:&lt;/p&gt;&lt;pre data-language=&quot;ini&quot;&gt;; --- Your Custom Records Go Here --- beszel IN A 192.168.1.100 hestia IN A 192.168.1.1 wordpress IN A 192.168.1.105 ; A record for our new WordPress instance www.beszel IN CNAME beszel.lab.ivansalloum.com. ; CNAME for www.beszel pointing to beszel @ IN TXT &amp;quot;v=spf1 include:_spf.example.com ~all&amp;quot; ; TXT record for SPF
&lt;/pre&gt;&lt;p&gt;Remember to replace the example values with your own service names and data.&lt;/p&gt;&lt;h3&gt;Step 2: Increment the Serial Number (Crucial!)&lt;/h3&gt;&lt;p&gt;This is a critical step, primarily for &lt;strong&gt;secondary DNS servers&lt;/strong&gt; (if you ever add them) to know your zone file has changed and needs updating.&lt;/p&gt;&lt;p&gt;The only strict rule is that the serial number &lt;strong&gt;must be a single number that strictly increases&lt;/strong&gt; every time you make a change. However, using the &lt;code&gt;YYYYMMDDnn&lt;/code&gt; format (Year, Month, Day, and a two-digit revision for changes on that day) is a widely-adopted best practice for human readability and sanity.&lt;/p&gt;&lt;p&gt;Here&apos;s how to manage it:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;First change of the day:&lt;/strong&gt; Update the date part to today&apos;s date and set the two-digit counter to &lt;code&gt;01&lt;/code&gt;. For example, if your old serial was &lt;code&gt;2025120202&lt;/code&gt; and today is December 7th, 2025, your new serial would be &lt;code&gt;2025120701&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Subsequent changes on the &lt;em&gt;same&lt;/em&gt; day:&lt;/strong&gt; Only increment the last two digits. For example, a second change on December 7th would make it &lt;code&gt;2025120702&lt;/code&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Always remember to increment this number whenever you modify your zone file.&lt;/p&gt;&lt;h3&gt;Step 3: Restart the CoreDNS Container&lt;/h3&gt;&lt;p&gt;After saving and closing the file, you need to restart the CoreDNS container so it reloads your updated zone file.&lt;/p&gt;&lt;p&gt;Navigate to the directory where your &lt;code&gt;docker-compose.yml&lt;/code&gt; file is located:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;cd /opt/coredns
&lt;/pre&gt;&lt;p&gt;Then, simply run the restart command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo docker compose restart
&lt;/pre&gt;&lt;p&gt;This command will gracefully restart the &lt;code&gt;coredns&lt;/code&gt; service. It&apos;s very fast, usually taking just a second or two.&lt;/p&gt;&lt;p&gt;Once it restarts, CoreDNS will have loaded your new records.&lt;/p&gt;&lt;h3&gt;Step 4: Verify Your Changes&lt;/h3&gt;&lt;p&gt;Finally, to be sure everything worked, test your new record with &lt;code&gt;dig&lt;/code&gt; from your local machine:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;dig wordpress.lab.ivansalloum.com @1.1.1.1
&lt;/pre&gt;&lt;p&gt;You should see the new IP address (&lt;code&gt;192.168.1.105&lt;/code&gt;) reflected in the answer.&lt;/p&gt;&lt;p&gt;That&apos;s it! You can repeat this process any time you need to add, update, or remove a DNS record.&lt;/p&gt;&lt;h2&gt;Automating Zone Reloads (The Pro Way)&lt;/h2&gt;&lt;p&gt;Manually restarting the CoreDNS container after every zone file change is simple and effective, but there’s an even better, more professional way: using the &lt;code&gt;reload&lt;/code&gt; plugin.&lt;/p&gt;&lt;p&gt;This plugin watches your zone file for changes and, if it detects any, gracefully reloads the zone in the background with zero downtime. You never have to restart the container for simple record updates again!&lt;/p&gt;&lt;p&gt;Let&apos;s enable it.&lt;/p&gt;&lt;p&gt;Edit your &lt;code&gt;Corefile&lt;/code&gt; one more time:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo vim /opt/coredns/config/Corefile
&lt;/pre&gt;&lt;p&gt;Add the &lt;code&gt;reload&lt;/code&gt; plugin to the top of your server block. It should look like this:&lt;/p&gt;&lt;pre data-language=&quot;ini&quot;&gt;lab.ivansalloum.com:53 { reload log errors file /zones/db.lab.ivansalloum.com }
&lt;/pre&gt;&lt;p&gt;Save the file, and then restart the container &lt;strong&gt;one last time&lt;/strong&gt; to activate the &lt;code&gt;reload&lt;/code&gt; plugin itself:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;cd /opt/coredns sudo docker compose restart
&lt;/pre&gt;&lt;p&gt;From now on, you can simply edit your &lt;code&gt;db.lab.ivansalloum.com&lt;/code&gt; file, and CoreDNS will automatically apply the changes within about 30 seconds (the default check interval).&lt;/p&gt;&lt;p&gt;This small change makes managing your DNS records an even more seamless experience.&lt;/p&gt;&lt;p&gt;❗&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Don&apos;t forget to increase your serial number!&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The &lt;code&gt;reload&lt;/code&gt; plugin is smart. It only reloads the zone if it detects that the &lt;strong&gt;serial number has increased&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;If you forget to increment the serial number, the plugin will assume nothing has changed and won&apos;t apply your updates.&lt;/p&gt;&lt;p&gt;So, the workflow remains:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Edit your records,&lt;/li&gt;&lt;li&gt;increment the serial,&lt;/li&gt;&lt;li&gt;and save the file.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;The restart step is now automated for you.&lt;/p&gt;&lt;h2&gt;Seeing the Difference: &lt;code&gt;dig&lt;/code&gt; in Action&lt;/h2&gt;&lt;p&gt;We&apos;ve talked about the difference between an authoritative server and a recursive resolver, but let&apos;s see it live using &lt;code&gt;dig&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;This is the best way to truly understand the roles they play.&lt;/p&gt;&lt;h3&gt;Query 1: A Standard Recursive Query&lt;/h3&gt;&lt;p&gt;When you run &lt;code&gt;dig&lt;/code&gt; for a public domain, your computer uses the default DNS resolver provided by your router or ISP:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;dig ivansalloum.com
&lt;/pre&gt;&lt;p&gt;This is a &lt;strong&gt;recursive resolver&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Notice the &lt;code&gt;flags&lt;/code&gt; in the header of the response: &lt;code&gt;qr rd ra&lt;/code&gt;.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;qr&lt;/code&gt; = Query Response&lt;/li&gt;&lt;li&gt;&lt;code&gt;rd&lt;/code&gt; = Recursion Desired (You asked the resolver to find the answer for you)&lt;/li&gt;&lt;li&gt;&lt;code&gt;ra&lt;/code&gt; = Recursion Available (The resolver is confirming it can do this)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The resolver went out and found the answer for you.&lt;/p&gt;&lt;h3&gt;Query 2: Specifying a Public Resolver&lt;/h3&gt;&lt;p&gt;We can get the same kind of recursive answer by specifying a different public resolver, like Cloudflare&apos;s &lt;code&gt;1.1.1.1&lt;/code&gt;:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;dig ivansalloum.com @1.1.1.1
&lt;/pre&gt;&lt;p&gt;Again, you&apos;ll see the &lt;code&gt;ra&lt;/code&gt; flag.&lt;/p&gt;&lt;p&gt;&lt;code&gt;1.1.1.1&lt;/code&gt; is a recursive resolver, and it happily found the answer.&lt;/p&gt;&lt;h3&gt;Query 3: Asking Our Authoritative Server for a Public Domain (The &amp;quot;Broken&amp;quot; Test)&lt;/h3&gt;&lt;p&gt;Now, let&apos;s ask &lt;em&gt;our&lt;/em&gt; new CoreDNS server to find &lt;code&gt;ivansalloum.com&lt;/code&gt;:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;dig ivansalloum.com @YOUR_SERVER_IPV4
&lt;/pre&gt;&lt;p&gt;The result is completely different.&lt;/p&gt;&lt;p&gt;The status is &lt;code&gt;REFUSED&lt;/code&gt;, and you might see a warning like &lt;code&gt;recursion requested but not available&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Our server correctly refused the query because it&apos;s not a public resolver and has no idea who &lt;code&gt;ivansalloum.com&lt;/code&gt; is.&lt;/p&gt;&lt;h3&gt;Query 4: Asking Our Authoritative Server for a Zone It Controls&lt;/h3&gt;&lt;p&gt;This is where our server shines.&lt;/p&gt;&lt;p&gt;Let&apos;s ask it for a record from the zone it&apos;s responsible for:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;dig beszel.lab.ivansalloum.com @YOUR_SERVER_IPV4
&lt;/pre&gt;&lt;p&gt;This query succeeds beautifully! Now, look very closely at the flags in the header: &lt;code&gt;qr aa rd&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;That &lt;code&gt;aa&lt;/code&gt; flag is the key. It stands for &lt;strong&gt;Authoritative Answer&lt;/strong&gt;. This is the DNS system&apos;s way of confirming that the answer came directly from the server that is the ultimate source of truth for that domain.&lt;/p&gt;&lt;p&gt;You will &lt;em&gt;not&lt;/em&gt; see this &lt;code&gt;aa&lt;/code&gt; flag when you query for &lt;code&gt;beszel.lab.ivansalloum.com&lt;/code&gt; using a public resolver like &lt;code&gt;1.1.1.1&lt;/code&gt;, because &lt;code&gt;1.1.1.1&lt;/code&gt; is just relaying the answer it found from your server.&lt;/p&gt;&lt;p&gt;This &lt;code&gt;aa&lt;/code&gt; flag is your proof that you have successfully built and queried a true authoritative DNS server.&lt;/p&gt;&lt;h2&gt;Backup and Recovery with Hetzner Snapshots&lt;/h2&gt;&lt;p&gt;Even with a stable DNS server, having a disaster recovery plan is essential for peace of mind.&lt;/p&gt;&lt;p&gt;For my setup on Hetzner, their snapshot feature combined with IP protection has proven to be a robust and reliable solution for disaster recovery.&lt;/p&gt;&lt;p&gt;The goal was to simulate a total server failure and see if I could bring my DNS server back to life from a backup.&lt;/p&gt;&lt;p&gt;Here&apos;s the strategy I used, combining two powerful Hetzner features.&lt;/p&gt;&lt;h3&gt;The Strategy: Protected IPs + Snapshots&lt;/h3&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Enable IP Protection:&lt;/strong&gt; Before anything else, I went to my Hetzner Cloud Console and enabled &amp;quot;IP protection&amp;quot; on my server&apos;s public IPv4 and IPv6 addresses. This is a critical feature that prevents your IPs from being lost if the server is deleted. It turns them into floating IPs that you can re-assign to a new server.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Take a Snapshot:&lt;/strong&gt; With my CoreDNS server running and serving records for a test service (&lt;code&gt;beszel.lab.ivansalloum.com&lt;/code&gt;), I took a manual snapshot of the server.&lt;/li&gt;&lt;/ol&gt;&lt;h3&gt;My Disaster Recovery Test&lt;/h3&gt;&lt;p&gt;With the snapshot created and the IPs protected, it was time for the real test: I deleted the server entirely.&lt;/p&gt;&lt;p&gt;An interesting thing happened right after. On my home WiFi, &lt;code&gt;beszel.lab.ivansalloum.com&lt;/code&gt; still worked for a few minutes. This is DNS caching in action! My local router or ISP&apos;s resolver had cached the record and didn&apos;t need to ask for it again.&lt;/p&gt;&lt;p&gt;However, when I switched to my phone&apos;s 5G network (a &amp;quot;fresh&amp;quot; resolver), the domain failed to resolve immediately. This perfectly demonstrates why DNS resilience is so important – cached records will eventually expire, and a single server outage will be felt.&lt;/p&gt;&lt;p&gt;Next, I provisioned a new server from the snapshot I had created. During the creation process, I re-attached my protected IPv4 and IPv6 addresses to the new instance.&lt;/p&gt;&lt;p&gt;The result? Within moments of the new server booting up, CoreDNS was back online.&lt;/p&gt;&lt;p&gt;My &lt;code&gt;beszel.lab.ivansalloum.com&lt;/code&gt; domain started resolving correctly on all networks.&lt;/p&gt;&lt;p&gt;It just worked.&lt;/p&gt;&lt;h3&gt;Automated Backups &amp;amp; Protection&lt;/h3&gt;&lt;p&gt;While manual snapshots are great for point-in-time recovery, Hetzner offers additional layers of protection:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Automated Backups:&lt;/strong&gt; For more frequent and automated restore points, consider enabling Hetzner&apos;s backup add-on. For a small additional cost (typically 20% of the server cost), this service provides daily backups that are kept for a week. A key advantage is that you can convert any automated backup into a manual snapshot before performing a restore or server deletion.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Deletion Protection:&lt;/strong&gt; To prevent accidental deletion of your server (and thus, your DNS service), always enable deletion protection in the Hetzner Cloud Console. It&apos;s a simple toggle that can save you a lot of headache.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This comprehensive approach ensures you&apos;re well-covered for any eventuality.&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;Congratulations! You&apos;ve successfully deployed your very own authoritative DNS server with CoreDNS, giving you unparalleled control over your subdomains.&lt;/p&gt;&lt;p&gt;This setup empowers you to:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Isolate your self-hosted projects:&lt;/strong&gt; Keep your main domain&apos;s DNS clean and separate.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Enjoy instant control:&lt;/strong&gt; Add, update, or remove records in seconds, without waiting for third-party providers.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Deepen your DNS understanding:&lt;/strong&gt; You&apos;ve walked through the core concepts of DNS delegation, zone files, and record management.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This journey into self-hosting your DNS is a fantastic step towards greater independence and control in your personal infrastructure.&lt;/p&gt;&lt;p&gt;Keep experimenting, keep learning, and enjoy the power you now have at your fingertips.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;If you run into any issues or need further help, feel free to revisit this guide or &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;reach out&lt;/a&gt; for assistance.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Servers</category></item><item><title>How to Run Beszel for Server Monitoring</title><link>https://ivansalloum.com/how-to-run-beszel-for-server-monitoring/</link><guid isPermaLink="true">https://ivansalloum.com/how-to-run-beszel-for-server-monitoring/</guid><description>Step-by-step guide to deploy Beszel for self-hosted server monitoring – everything you need to run it confidently.</description><pubDate>Sun, 09 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;I used to keep an eye on my Hetzner servers the old-fashioned way: SSH in, run a few commands, hop to the next box, and hope nothing was falling apart in the background.&lt;/p&gt;&lt;p&gt;Every monitoring tool I tried felt heavier than the services I was protecting – bloated dashboards, high CPU usage, and no way to see everything at a glance.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://beszel.dev&quot;&gt;&lt;strong&gt;Beszel&lt;/strong&gt;&lt;/a&gt; finally fixed that for me.&lt;/p&gt;&lt;h2&gt;Why Beszel? (Context &amp;amp; Goals)&lt;/h2&gt;&lt;p&gt;Beszel is an open-source, self-hosted server monitoring tool written in Go with a lightweight PocketBase dashboard.&lt;/p&gt;&lt;p&gt;It’s designed for simplicity, efficiency, and clarity - no unnecessary bloat, no agents eating your RAM.&lt;/p&gt;&lt;p&gt;Here’s what makes it stand out for me:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Lightweight design&lt;/strong&gt; – The hub is a PocketBase app with a Go backend, and each server runs a tiny agent. Even with the hub and five agents, CPU usage stays under 2% on the smallest Hetzner instance.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Real metrics with real usability&lt;/strong&gt; – The web dashboard is fast, mobile-friendly, and customizable. I can hide metrics I don’t care about (like GPU and temp), pin the ones I do, hit ⌘K to jump to any server, and even dig into Docker container stats and logs without SSH.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Alerts that replace my uptime tools&lt;/strong&gt; – Beszel watches for downtime, CPU spikes, disks filling up, Docker containers misbehaving, and it emails me when something crosses a threshold. I don’t need separate uptime and resource monitors anymore.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Under the hood, Beszel is a hub-and-agent design. Each agent keeps a WebSocket connection to the hub, but if that ever drops (say, the proxy restarts), it automatically exposes a backup SSH tunnel on port 45876 so the hub can keep pulling metrics. That dual-path connection, plus mutual authentication and fingerprinting, makes me comfortable running it on every production server.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;The rest of this guide walks through how I run Beszel in production:&lt;/strong&gt; preparing the server and DNS, shipping the hub with Docker, securing it with Caddy, onboarding more systems, enabling health checks and alerts, and planning for disaster recovery with snapshots and backups.&lt;/p&gt;&lt;h2&gt;Prep the Server and DNS&lt;/h2&gt;&lt;p&gt;Before deploying Beszel, I prepare the base server – just like I do for every new self-hosted project:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Baseline hardening&lt;/strong&gt; – I follow the &lt;a href=&quot;https://ivansalloum.com/preparing-your-ubuntu-server-for-first-use/&quot;&gt;&amp;quot;Preparing Your Ubuntu Server for First Use&amp;quot;&lt;/a&gt; guide I already published: create a non-root user, lock down SSH, enable UFW, and keep the packages up to date.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Install Docker Engine + Docker Compose&lt;/strong&gt; – Beszel’s deployment runs on Docker, so I install Docker Engine and the Docker Compose plugin from Docker’s official repository using my guide: &lt;a href=&quot;https://ivansalloum.com/installing-docker-and-docker-compose-on-ubuntu/&quot;&gt;Installing Docker and Docker Compose on Ubuntu&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Hetzner IP protection&lt;/strong&gt; – In the Hetzner Cloud console, I enable protection on the server &lt;em&gt;and&lt;/em&gt; on its primary IPv4/IPv6 addresses. That way if the server gets deleted (accidentally or during a disaster), the IPs stay with me and I can reassign them later without touching DNS.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;I run everything on Hetzner because it’s cost-effective and reliable – if you’d like to try them, here’s my &lt;a href=&quot;https://hetzner.cloud/?ref=MC4Yy318xX5X&quot;&gt;referral link&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Next comes DNS:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;I create &lt;code&gt;A&lt;/code&gt; and &lt;code&gt;AAAA&lt;/code&gt; records for the hostname I’ll use (e.g., &lt;code&gt;beszel.yourdomain.com&lt;/code&gt;). When I’m using Cloudflare, I keep the proxy toggled off while I’m setting up the reverse proxy – plain DNS means no cached certificates or blocked ACME requests while Caddy fetches SSL certs.&lt;/li&gt;&lt;/ul&gt;&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;I recommend running each self-hosted project on its&lt;strong&gt;own main hostname&lt;/strong&gt; and letting that server do just one job. Beszel can technically share a box with other services, but I keep it isolated so troubleshooting stays simple.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;With the base server hardened, DNS pointing to the host, Docker/Compose installed, and IP protection turned on, I’m ready to deploy the Beszel stack.&lt;/p&gt;&lt;h2&gt;Install Beszel Hub + Local Agent with Docker&lt;/h2&gt;&lt;p&gt;Beszel ships as two components: the hub (PocketBase + Beszel&apos;s web dashboard) and the agent that runs on every server you want to monitor.&lt;/p&gt;&lt;p&gt;You can install Beszel either using &lt;strong&gt;Docker&lt;/strong&gt;  or by using a &lt;strong&gt;single binary file&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;It’s written in pure Go and can be easily compiled on the server using an &lt;a href=&quot;https://beszel.dev/guide/agent-installation#binary&quot;&gt;install script&lt;/a&gt; that Beszel provides – but &lt;strong&gt;I still prefer Docker&lt;/strong&gt; because it keeps everything in one compose file and makes backups, upgrades, and restores painless.&lt;/p&gt;&lt;p&gt;Beszel’s documentation shows two install flows: deploy the hub first and add agents later, or launch both hub and local agent in one go.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;I prefer the second option so everything is up and monitored at the same time. That way, if I ever need to scale the hub’s resources as more agents come online, I already have baseline metrics for the hub itself.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;Create the Compose File&lt;/h3&gt;&lt;p&gt;I start by creating a working directory and moving into it:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;mkdir ~/beszel cd ~/beszel
&lt;/pre&gt;&lt;p&gt;Then I create a &lt;code&gt;docker-compose.yml&lt;/code&gt; file and drop in the official stack:&lt;/p&gt;&lt;pre data-language=&quot;yaml&quot;&gt;services: beszel: image: henrygd/beszel:latest container_name: beszel restart: unless-stopped ports: \- 8090:8090 volumes: \- ./beszel_data:/beszel_data \- ./beszel_socket:/beszel_socket beszel-agent: image: henrygd/beszel-agent:latest container_name: beszel-agent restart: unless-stopped network_mode: host volumes: \- ./beszel_agent_data:/var/lib/beszel-agent \- ./beszel_socket:/beszel_socket \- /var/run/docker.sock:/var/run/docker.sock:ro environment: LISTEN: /beszel_socket/beszel.sock HUB_URL: http://localhost:8090 TOKEN: KEY: &amp;quot;&amp;quot;
&lt;/pre&gt;&lt;p&gt;This is straight from Beszel’s docs – no edits beyond plugging in my token/key later.&lt;/p&gt;&lt;p&gt;The local agent writes metrics to &lt;code&gt;/beszel_socket/beszel.sock&lt;/code&gt;, so the hub mounts the same path under &lt;code&gt;volumes&lt;/code&gt; to read the data locally. Because the containers run in separate networks, the Unix socket is what lets them talk to each other without exposing anything to the outside.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;TOKEN&lt;/code&gt; and &lt;code&gt;KEY&lt;/code&gt; placeholders stay put for now – we’ll grab the real values from the dashboard right after the stack starts.&lt;/p&gt;&lt;p&gt;With the file saved, I launch the stack:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo docker compose up -d
&lt;/pre&gt;&lt;p&gt;That brings both services online so I can finish wiring the local agent from the dashboard.&lt;/p&gt;&lt;h3&gt;Wire the Local Agent in the Dashboard&lt;/h3&gt;&lt;p&gt;Once the containers are running, I open &lt;code&gt;http://&amp;lt;server-ip&amp;gt;:8090&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Beszel immediately asks me to create the first account – I type my email, pick a password, and seconds later I’m at the dashboard.&lt;/p&gt;&lt;p&gt;From there I add a system named &lt;code&gt;Hub&lt;/code&gt;, copy the token/key Beszel shows, drop those values into the compose file (&lt;code&gt;TOKEN&lt;/code&gt;/&lt;code&gt;KEY&lt;/code&gt;), and rerun &lt;code&gt;sudo docker compose up -d&lt;/code&gt;. Because both containers share &lt;code&gt;/beszel_socket/beszel.sock&lt;/code&gt;, I set that socket path as the Host/IP when creating the agent so the connection lights up immediately.&lt;/p&gt;&lt;p&gt;You might notice the dashboard is reachable even if UFW doesn’t allow port 8090. That’s Docker at work – publishing ports through its own iptables rules (&lt;code&gt;DOCKER&lt;/code&gt; chain) before UFW ever sees the traffic. &lt;em&gt;We’ll tuck the hub behind Caddy and bind it to&lt;code&gt;127.0.0.1&lt;/code&gt; shortly so only HTTPS requests hit it.&lt;/em&gt;&lt;/p&gt;&lt;h3&gt;Hub-Only Variant&lt;/h3&gt;&lt;p&gt;If you’d rather bootstrap just the hub and attach agents later, you can start with a trimmed compose file:&lt;/p&gt;&lt;pre data-language=&quot;yaml&quot;&gt;services: beszel: image: henrygd/beszel container_name: beszel restart: unless-stopped ports: \- 8090:8090 volumes: \- ./beszel_data:/beszel_data
&lt;/pre&gt;&lt;p&gt;Notice there’s no &lt;code&gt;./beszel_socket:/beszel_socket&lt;/code&gt; line – that socket is only needed when a local agent is present.&lt;/p&gt;&lt;p&gt;Once you’re ready to monitor the hub itself, add the agent block back in and redeploy.&lt;/p&gt;&lt;h2&gt;Secure Access with Caddy&lt;/h2&gt;&lt;p&gt;A reverse proxy sits in front of your service, terminates HTTPS, and forwards clean traffic to the backend.&lt;/p&gt;&lt;p&gt;I don’t like exposing port 8090 over plain HTTP, so before I do anything serious in the dashboard I put Beszel behind Caddy.&lt;/p&gt;&lt;p&gt;I’ve used NGINX for years, but for single-app setups like this I switched to Caddy because it’s lightweight, handles Let’s Encrypt automatically, and takes minutes to configure.&lt;/p&gt;&lt;p&gt;I install Caddy from Ubuntu’s repo:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt install caddy
&lt;/pre&gt;&lt;p&gt;Then I back up the default config and overwrite &lt;code&gt;/etc/caddy/Caddyfile&lt;/code&gt; with the snippet from Beszel’s docs:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;beszel.example.com { request_body { max_size 10MB } reverse_proxy 127.0.0.1:8090 { transport http { read_timeout 360s } } }
&lt;/pre&gt;&lt;p&gt;Replace &lt;code&gt;beszel.example.com&lt;/code&gt; with your hostname, save, and run:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo systemctl restart caddy
&lt;/pre&gt;&lt;p&gt;Caddy immediately requests a Let’s Encrypt certificate for that hostname and starts proxying traffic to &lt;code&gt;127.0.0.1:8090&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;If ports 80/443 aren’t allowed in UFW, Caddy can’t complete the ACME challenge.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;I forgot to open them the first time and hit Let’s Encrypt’s rate limit, so now I always double-check before restarting. When something looked off,&lt;code&gt;sudo journalctl -u caddy --no-pager -n 100&lt;/code&gt; helped me identify the issue right away.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Once Caddy is proxying to &lt;code&gt;127.0.0.1:8090&lt;/code&gt;, I tighten the Docker port mapping so Beszel only listens on localhost:&lt;/p&gt;&lt;pre data-language=&quot;yaml&quot;&gt;ports: \- 127.0.0.1:8090:8090
&lt;/pre&gt;&lt;p&gt;After updating the port mapping, I redeploy with &lt;code&gt;sudo docker compose up -d&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;From that point on, the dashboard is only reachable through HTTPS on my hostname.&lt;/p&gt;&lt;p&gt;If you’re exposing the dashboard to a whole team or running Beszel for clients, also harden the public ports that Caddy serves. My &lt;a href=&quot;https://ivansalloum.com/how-i-built-an-adaptive-firewall-setup-with-ufw-and-fail2ban/&quot;&gt;UFW+ adaptive firewall stack&lt;/a&gt; sits in front of ports 80 and 443, detects abusive connection spikes, and keeps the reverse proxy responsive even when lots of people are logged in.&lt;/p&gt;&lt;p&gt;It’s overkill for a single-user lab, but for business use it gives me the comfort that HTTPS stays protected while everyone pounds the UI.&lt;/p&gt;&lt;h3&gt;Set &lt;code&gt;APP_URL&lt;/code&gt;&lt;/h3&gt;&lt;p&gt;Beszel recommends setting &lt;code&gt;APP_URL&lt;/code&gt; when you’re behind a reverse proxy so notification links and agent configs use the correct URL.&lt;/p&gt;&lt;p&gt;Set it to the same hostname you specified in Caddy’s config. In the compose file, under the hub service, add:&lt;/p&gt;&lt;pre data-language=&quot;yaml&quot;&gt;environment: \- APP_URL=https://beszel.example.com
&lt;/pre&gt;&lt;p&gt;Then redeploy with &lt;code&gt;sudo docker compose up -d&lt;/code&gt;.&lt;/p&gt;&lt;h2&gt;Add More Servers (Systems)&lt;/h2&gt;&lt;p&gt;Beszel calls each monitored server a “System.”&lt;/p&gt;&lt;p&gt;To add one, I open the dashboard, click &lt;strong&gt;Add System&lt;/strong&gt; , choose the Docker option, and name it after the server’s hostname so everything stays consistent.&lt;/p&gt;&lt;p&gt;Beszel asks for the server’s IP and the port; I point it to the remote server I’m onboarding and leave the port at the default.&lt;/p&gt;&lt;p&gt;Beszel then shows a Docker compose snippet tailored for that system. I copy it to the target server, keep the same directory structure, and run &lt;code&gt;sudo docker compose up -d&lt;/code&gt;. Because all of my servers already run Docker, I stick with the Docker agent (there’s a binary option if you prefer).&lt;/p&gt;&lt;p&gt;Within seconds the new agent connects and the hub starts collecting metrics – it really is that simple: &lt;strong&gt;click, copy, paste, deploy, done.&lt;/strong&gt;&lt;/p&gt;&lt;h2&gt;How Agents Stay Connected&lt;/h2&gt;&lt;p&gt;Here’s what actually happens after you add a new system: the agent maintains two network paths, falls back automatically if the main one fails, and locks the connection down with mutual authentication.&lt;/p&gt;&lt;p&gt;The next few sections break that down so you know what’s happening behind the scenes.&lt;/p&gt;&lt;h3&gt;Communication Paths (WebSocket and SSH)&lt;/h3&gt;&lt;p&gt;Every agent maintains two ways to talk to the hub: a WebSocket that dials out to whatever you set in &lt;code&gt;HUB_URL&lt;/code&gt; (typically your dashboard hostname) and an SSH-like tunnel on port 45876.&lt;/p&gt;&lt;p&gt;The WebSocket is the default; if the hub URL isn’t reachable – say, Caddy is reloading – the agent automatically spins up the SSH listener and the hub dials in to keep metrics flowing.&lt;/p&gt;&lt;h3&gt;Port 45876 and Firewall Rules&lt;/h3&gt;&lt;p&gt;Because the agent runs in &lt;code&gt;network_mode: host&lt;/code&gt; so it can read the real &lt;strong&gt;NIC counters&lt;/strong&gt; , Docker can’t publish port 45876 the way it does for 8090.&lt;/p&gt;&lt;p&gt;On each server running an agent, I open the port explicitly in UFW but only from the hub’s IPs:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw allow in proto tcp from to any port 45876 sudo ufw allow in proto tcp from to any port 45876
&lt;/pre&gt;&lt;p&gt;That way, when the agent falls back to SSH, the hub can still reach it without exposing the port to the world.&lt;/p&gt;&lt;h3&gt;Logs Showing the Fallback in Action&lt;/h3&gt;&lt;p&gt;To see the switchover, I tail the agent logs:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo docker logs --tail=200 beszel-agent | egrep -i &amp;quot;connect(ed)? to hub|websocket|connected&amp;quot;
&lt;/pre&gt;&lt;p&gt;Example output:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;2025/11/06 18:23:00 INFO WebSocket connected host=beszel.example.com 2025/11/07 09:26:17 WARN Connection closed err=EOF 2025/11/07 09:26:17 WARN Disconnected from hub 2025/11/07 09:26:17 WARN WebSocket connection failed err=&amp;quot;unexpected status code: 502&amp;quot; 2025/11/07 09:26:17 INFO Starting SSH server addr=:45876 network=tcp 2025/11/07 09:26:50 INFO SSH connected addr=:56882 2025/11/07 09:26:50 INFO SSH connection established
&lt;/pre&gt;&lt;p&gt;When you see &lt;code&gt;WebSocket connected&lt;/code&gt;, the agent is in dial-out mode.&lt;/p&gt;&lt;p&gt;If the WebSocket drops, the agent starts the SSH server on &lt;code&gt;:45876&lt;/code&gt;, the hub reconnects through that path, and metrics keep flowing even if the dashboard itself is unreachable.&lt;/p&gt;&lt;p&gt;When the WebSocket comes back, the agent switches automatically.&lt;/p&gt;&lt;h3&gt;Security Notes (From Beszel’s Docs)&lt;/h3&gt;&lt;p&gt;The two communication paths are locked down even though they’re automatic:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;When the hub starts, it generates an ED25519 keypair. The SSH fallback only accepts that key, offers no shell, and doesn’t execute commands. Even if the key leaked, attackers couldn’t run arbitrary commands on the agent.&lt;/li&gt;&lt;li&gt;Every WebSocket session starts with mutual auth: the agent sends its registration token, the hub signs a challenge, and the agent verifies it against the hub’s public key. Then the agent sends a fingerprint tied to the monitored server, and the hub makes sure it matches the original system record before letting metrics flow.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Together those safeguards keep the link private – only the original hub and the original agent can talk to each other.&lt;/p&gt;&lt;h2&gt;Health Checks&lt;/h2&gt;&lt;p&gt;Beszel ships with health commands for both the hub and agent.&lt;/p&gt;&lt;p&gt;Docker can run those commands on a schedule (healthchecks) to ask “are you OK?” and mark the container &lt;code&gt;healthy&lt;/code&gt; or &lt;code&gt;unhealthy&lt;/code&gt;.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The hub command is &lt;code&gt;/beszel health --url http://localhost:8090&lt;/code&gt;, which does an HTTP GET to &lt;code&gt;/api/health&lt;/code&gt; from inside the container and only returns success if it sees a 200 OK.&lt;/li&gt;&lt;li&gt;The agent command is &lt;code&gt;/agent health&lt;/code&gt;, which verifies the agent process is running (but doesn’t guarantee the WebSocket or SSH path is live).&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Healthchecks aren’t free – they run a command every time and add a bit of CPU overhead – so I balance Beszel’s “60s or more” guidance with my own experience and stick to the 60–120s range.&lt;/p&gt;&lt;p&gt;Docker doesn’t restart containers automatically on failure; it just updates their health status.&lt;/p&gt;&lt;p&gt;I use that status for two things: &lt;code&gt;depends_on&lt;/code&gt; waits for the hub to start returning &lt;code&gt;/api/health&lt;/code&gt; before the agent launches, and &lt;code&gt;docker ps&lt;/code&gt; instantly shows me which container is misbehaving without digging through logs.&lt;/p&gt;&lt;p&gt;Before adding any healthchecks, the &lt;code&gt;Hub&lt;/code&gt; system page in the web dashboard (or whatever you named our local agent) shows “Health: None” for both containers running on the hub server (&lt;code&gt;beszel&lt;/code&gt; and &lt;code&gt;beszel-agent&lt;/code&gt;). Scroll to the bottom and you’ll see the empty indicators.&lt;/p&gt;&lt;p&gt;On the hub server, I edit &lt;code&gt;docker-compose.yml&lt;/code&gt; and add these blocks:&lt;/p&gt;&lt;pre data-language=&quot;yaml&quot;&gt;beszel: # ...existing config... healthcheck: test: [&amp;quot;CMD&amp;quot;, &amp;quot;/beszel&amp;quot;, &amp;quot;health&amp;quot;, &amp;quot;--url&amp;quot;, &amp;quot;http://localhost:8090&amp;quot;] interval: 120s timeout: 5s retries: 3 start_period: 5s beszel-agent: # ...existing config... healthcheck: test: [&amp;quot;CMD&amp;quot;, &amp;quot;/agent&amp;quot;, &amp;quot;health&amp;quot;] interval: 120s timeout: 5s retries: 3 start_period: 15s depends_on: beszel: condition: service_healthy
&lt;/pre&gt;&lt;p&gt;&lt;code&gt;http://localhost:8090&lt;/code&gt; works because it runs inside the hub container (not through Caddy). The &lt;code&gt;depends_on&lt;/code&gt; block makes sure the local agent doesn’t start dialing until the hub is actually serving &lt;code&gt;/api/health&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Beszel’s docs provide baseline healthchecks, but I tweak them slightly: &lt;code&gt;start_period&lt;/code&gt; is 5 seconds for the hub and 15 seconds for the agent, and I add explicit &lt;code&gt;timeout&lt;/code&gt;/&lt;code&gt;retries&lt;/code&gt; so the status is meaningful. That &lt;code&gt;depends_on&lt;/code&gt; block only belongs on the hub server – remote agents shouldn’t wait on anything.&lt;/p&gt;&lt;p&gt;After saving the file, I redeploy with &lt;code&gt;sudo docker compose up -d&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;From now on &lt;code&gt;docker ps&lt;/code&gt; shows &lt;code&gt;beszel ... (healthy)&lt;/code&gt; and &lt;code&gt;beszel-agent ... (healthy)&lt;/code&gt;, and if the hub ever stops answering 200 OK, I see &lt;code&gt;unhealthy&lt;/code&gt; immediately.&lt;/p&gt;&lt;p&gt;Back in the dashboard, those “Health: None” labels are replaced with green “Healthy” badges for both containers.&lt;/p&gt;&lt;p&gt;If you want to add a healthcheck to a remote agent (on the server you’re monitoring), just add the agent block (without &lt;code&gt;depends_on&lt;/code&gt;) to that system’s compose file:&lt;/p&gt;&lt;pre data-language=&quot;yaml&quot;&gt;healthcheck: test: [&amp;quot;CMD&amp;quot;, &amp;quot;/agent&amp;quot;, &amp;quot;health&amp;quot;] interval: 120s timeout: 5s retries: 3 start_period: 15s
&lt;/pre&gt;&lt;p&gt;That keeps Docker honest on each monitored server: the agent container reports &lt;code&gt;healthy&lt;/code&gt; only if the process is running.&lt;/p&gt;&lt;h2&gt;SMTP Configuration&lt;/h2&gt;&lt;p&gt;If you want email alerts (server down/up, login notifications), Beszel needs an SMTP server.&lt;/p&gt;&lt;p&gt;In the &lt;strong&gt;Settings → Notifications tab&lt;/strong&gt; you’ll see “Please configure an SMTP server.” Click it and you’re taken to the PocketBase dashboard where you can enable “Use SMTP mail server,” enter your SMTP details, and send a test email.&lt;/p&gt;&lt;p&gt;I use Proton Mail’s SMTP server (part of the Unlimited plan). It’s privacy-focused and reliable. If you want a free alternative, &lt;a href=&quot;https://get.smtp2go.com/r3moeahf98qf&quot;&gt;SMTP2GO&lt;/a&gt; is easy to set up and works well.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;_I’ve used Proton products for years –  if you try _&lt;a href=&quot;https://proton.me/mail&quot;&gt;&lt;em&gt;Proton Mail&lt;/em&gt;&lt;/a&gt; &lt;em&gt;for free and later upgrade to&lt;/em&gt;&lt;a href=&quot;https://go.getproton.me/aff_c?offer_id=26&amp;amp;aff_id=12648&amp;amp;url_id=1198&quot;&gt; &lt;em&gt;Unlimited&lt;/em&gt;&lt;/a&gt; &lt;em&gt;, it’s worth it.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;After the test email succeeds, go back to the main Beszel dashboard. Each system added has a bell icon at the right – click it to open Alert settings.&lt;/p&gt;&lt;p&gt;I enable the Status alert (uptime monitoring) and set it to 1 minute. Hetzner servers reboot in seconds, so I don’t get spammed during kernel reboots, but I know right away if a server goes offline.&lt;/p&gt;&lt;p&gt;Once SMTP is configured, Beszel also emails you when someone logs in from a new location.&lt;/p&gt;&lt;h2&gt;PocketBase Passwords&lt;/h2&gt;&lt;p&gt;After the initial setup (when Beszel prompts you for your first password), I immediately change the PocketBase superuser password from the hub server. This command is also your go-to if you ever lose dashboard access:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo docker exec beszel /beszel superuser update name@example.com newpassword
&lt;/pre&gt;&lt;p&gt;That command logs everyone out of the PocketBase admin UI.&lt;/p&gt;&lt;p&gt;It’s good practice to use a different password for PocketBase and Beszel, so after running the CLI command I also log into the PocketBase Users view and update the Beszel dashboard user there. Follow the same steps later whenever you want to rotate the Beszel password.&lt;/p&gt;&lt;p&gt;For now we do those updates separately; a single command would be nicer after an incident.&lt;/p&gt;&lt;h2&gt;MFA OTP&lt;/h2&gt;&lt;p&gt;Beszel also supports email-based MFA. In the hub’s compose file add:&lt;/p&gt;&lt;pre data-language=&quot;yaml&quot;&gt;environment: \- MFA_OTP=true
&lt;/pre&gt;&lt;p&gt;Redeploy with &lt;code&gt;sudo docker compose up -d&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;The next time you log out and back in, Beszel prompts for the one-time code it emails you.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;This only works if SMTP is rock solid, so use a reliable provider.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;Container Metrics&lt;/h2&gt;&lt;p&gt;When I first tried Beszel, it couldn’t track Docker containers. Now it does – and even captures logs.&lt;/p&gt;&lt;p&gt;Open any system that runs Docker and scroll down: you’ll see each container listed with CPU/memory usage, network I/O, image, status, uptime, and a health indicator.&lt;/p&gt;&lt;p&gt;Above the list, Beszel plots the top CPU and memory consumers plus a Docker network I/O chart, which makes it obvious which container is spiking.&lt;/p&gt;&lt;p&gt;Clicking a container shows its logs (with a refresh button) and configuration details.&lt;/p&gt;&lt;p&gt;I also skim the &lt;code&gt;beszel-agent&lt;/code&gt; container logs directly from the UI so I can tell if the agent fell back to SSH overnight without SSHing into the box.&lt;/p&gt;&lt;h2&gt;CPU Metrics&lt;/h2&gt;&lt;p&gt;The CPU charts are my favorite quick-check.&lt;/p&gt;&lt;p&gt;If I see a spike, I click the three dots in the CPU widget and open the CPU Time Breakdown chart.&lt;/p&gt;&lt;p&gt;The first thing I check is I/O wait – it’s often the culprit when disks are slow or a query is misbehaving.&lt;/p&gt;&lt;p&gt;Beszel also plots average utilization across cores and a per-core chart, which is great for single-threaded apps like WordPress because you can see which core is pegged.&lt;/p&gt;&lt;h2&gt;Disaster Recovery&lt;/h2&gt;&lt;p&gt;The final piece of the stack is disaster recovery.&lt;/p&gt;&lt;p&gt;Think about what happens if you misconfigure something, install a bad update, or your provider has a fire – how do you bring Beszel back online quickly?&lt;/p&gt;&lt;p&gt;Hetzner’s primary IP protection keeps your IPv4/IPv6 addresses even if you delete the server, but you still need a fresh server image.&lt;/p&gt;&lt;p&gt;Snapshots are the easiest way: take one in the Snapshots tab, and if disaster strikes you can spin up a new server from it, reassign the primary IPs, and you’re back without touching DNS or Caddy.&lt;/p&gt;&lt;p&gt;Snapshots are manual. If you want automated restore points, Hetzner’s backup add-on (20% of the server cost) keeps daily backups for a week.&lt;/p&gt;&lt;p&gt;You can convert any backup into a snapshot before deleting a server. Also enable deletion protection so you don’t nuke anything by accident.&lt;/p&gt;&lt;h3&gt;My Recovery Checklist&lt;/h3&gt;&lt;p&gt;Here’s the exact sequence I follow when something goes sideways:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Enable Hetzner backups and turn on deletion protection for both the server and its primary IPs.&lt;/li&gt;&lt;li&gt;When something goes wrong, convert the latest backup (and one or two earlier ones) into snapshots so I have multiple restore points.&lt;/li&gt;&lt;li&gt;Once the conversions finish, verify the snapshots exist, then disable deletion protection on the server only (leave IP protection enabled).&lt;/li&gt;&lt;li&gt;Delete the broken server, restore from the chosen snapshot, and assign the same primary IPv4/IPv6 addresses.&lt;/li&gt;&lt;li&gt;Bring Beszel back up, confirm Caddy works over HTTPS, agents reconnect, and metrics start flowing.&lt;/li&gt;&lt;li&gt;After everything is stable, remove extra snapshots to keep storage costs down.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;I’ve tested the process: I created a snapshot, deleted the hub server, restored to a new box with the same primary IPs, and Beszel came back without any issues.&lt;/p&gt;&lt;p&gt;Snapshots work flawlessly for this setup.&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;You’ve seen how I run Beszel end to end: prep the server, deploy the hub + agent, lock it down behind Caddy, onboard new server (systems), keep the hub healthy with healthchecks, wire up email/MFA, and plan for disaster recovery.&lt;/p&gt;&lt;p&gt;The payoff is huge – I can glance at the dashboard, see which containers or cores are spiking, know when a system goes down, and have confidence I can rebuild it quickly if something goes wrong.&lt;/p&gt;&lt;p&gt;If you follow the same steps, you’ll have a monitoring stack that’s fast, reliable, and easy to manage.&lt;/p&gt;&lt;p&gt;Thanks for following along; let me know how your own setup goes.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;If you run into any issues or need further help, feel free to revisit this guide or &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;reach out&lt;/a&gt; for assistance.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Servers</category></item><item><title>How I Built an Adaptive Firewall Setup with UFW and Fail2ban (UFW+)</title><link>https://ivansalloum.com/how-i-built-an-adaptive-firewall-setup-with-ufw-and-fail2ban/</link><guid isPermaLink="true">https://ivansalloum.com/how-i-built-an-adaptive-firewall-setup-with-ufw-and-fail2ban/</guid><description>Learn how I built UFW+, an adaptive firewall setup that combines UFW, Fail2ban, and nftables to block floods, scans, and spoofed traffic efficiently.</description><pubDate>Tue, 14 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;In my &lt;a href=&quot;https://ivansalloum.com/preventing-syn-flood-attacks-on-your-linux-server/&quot;&gt;earlier guide&lt;/a&gt;, I showed how to prevent SYN flood attacks using &lt;strong&gt;kernel tuning&lt;/strong&gt; , a few &lt;strong&gt;UFW rules&lt;/strong&gt; , and some &lt;strong&gt;basic Fail2ban jails&lt;/strong&gt;  to analyze UFW’s logs and block attackers.&lt;/p&gt;&lt;p&gt;That setup worked – it could stop simple SYN floods – but it had limits. It was reactive, narrow in scope, and mainly focused on controlling SYN packet rates.&lt;/p&gt;&lt;p&gt;Since then, I’ve built something much more powerful – a project I call &lt;strong&gt;UFW+&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;UFW+ takes the same foundation and pushes it to the next level: an &lt;strong&gt;adaptive firewall setup&lt;/strong&gt; that merges &lt;strong&gt;UFW&lt;/strong&gt; , &lt;strong&gt;Fail2ban&lt;/strong&gt; (the log-watching tool that blocks bad actors automatically), and the performance of &lt;strong&gt;nftables&lt;/strong&gt; (the modern Linux packet filtering engine) into a cohesive, modern defense layer.&lt;/p&gt;&lt;p&gt;By moving away from iptables and leveraging nftables’ native &lt;em&gt;sets&lt;/em&gt;  feature, it blocks abusive IPs more efficiently – without adding thousands of individual &lt;code&gt;DROP&lt;/code&gt; rules – while Fail2ban maintains real-time ban lists directly inside nftables.&lt;/p&gt;&lt;p&gt;It doesn’t just rate-limit SYN packets anymore – it detects scans, spoofed traffic, and abusive connection floods in real time, then automatically bans the sources with smarter, escalating Fail2ban rules.&lt;/p&gt;&lt;p&gt;This guide walks you through how I built it, the reasoning behind every rule, and how you can deploy it to harden your own &lt;strong&gt;Linux server&lt;/strong&gt; against modern network attacks.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;Don’t worry if this stack is new – I&apos;ll walk you through each piece step by step so you can follow along at your own pace.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;❗&lt;/p&gt;&lt;p&gt;This setup has been tested exclusively on &lt;strong&gt;Ubuntu 24.04 LTS&lt;/strong&gt;. Other distributions or earlier Ubuntu versions may require adjustments.&lt;/p&gt;&lt;h2&gt;Author’s Note&lt;/h2&gt;&lt;p&gt;This guide – or rather, &lt;strong&gt;UFW+&lt;/strong&gt;  – focuses on protecting &lt;strong&gt;HTTP (port 80)&lt;/strong&gt;  and &lt;strong&gt;HTTPS (port 443)&lt;/strong&gt;  traffic, since those are the most common public entry points.&lt;/p&gt;&lt;p&gt;However, the same logic and rule patterns can be applied to &lt;strong&gt;any service port&lt;/strong&gt;  – the principles are universal.&lt;/p&gt;&lt;p&gt;I plan to extend UFW+ to cover additional ports and the &lt;strong&gt;UDP-based version of port 443 (QUIC)&lt;/strong&gt;  in the future.&lt;/p&gt;&lt;p&gt;QUIC matters because most modern browsers prefer it for HTTPS traffic, so covering UDP-based port 443 keeps this setup aligned with how people actually reach your sites.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;When those improvements are implemented, this guide will be updated accordingly to include the new rules and configuration examples.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;It’s worth noting that while &lt;strong&gt;smaller attacks&lt;/strong&gt;  can often be handled with the right firewall setup, large, &lt;strong&gt;distributed attacks&lt;/strong&gt;  are much harder to stop completely.&lt;/p&gt;&lt;p&gt;In short, a &lt;strong&gt;Denial-of-Service (DoS)&lt;/strong&gt;  attack usually comes from one source, while a &lt;strong&gt;Distributed Denial-of-Service (DDoS)&lt;/strong&gt;  attack comes from many – making it far more difficult to block all of it at once.&lt;/p&gt;&lt;p&gt;That said, even basic protections can make your server a much harder target.&lt;/p&gt;&lt;p&gt;Even if your server provider (for example, &lt;strong&gt;Hetzner&lt;/strong&gt;) offers some level of built-in DDoS protection, it often won’t catch the smaller, low-rate &lt;strong&gt;SYN floods&lt;/strong&gt;  that directly target your VPS.&lt;/p&gt;&lt;p&gt;That’s where &lt;strong&gt;UFW+&lt;/strong&gt;  comes in – the setup you’ll learn in this guide focuses on &lt;strong&gt;server-side defenses&lt;/strong&gt;  that block and slow down those targeted attacks before they can cause real damage.&lt;/p&gt;&lt;p&gt;These layers don’t make you invincible, but they make your server resilient – and that’s the goal of everything you’ll build here.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;New to Hetzner? Their basic DDoS filtering and generous bandwidth make it a solid test bed – &lt;a href=&quot;https://hetzner.cloud/?ref=MC4Yy318xX5X&quot;&gt;use my link&lt;/a&gt; to grab some free credits if you want to try it.&lt;/p&gt;&lt;h2&gt;Background: The Old Setup and Its Limits&lt;/h2&gt;&lt;p&gt;In my previous setup, the goal was simple: stop SYN flood attacks.&lt;/p&gt;&lt;p&gt;At the time, I focused on the most common issue many people were struggling with – massive bursts of half-open TCP connections that could bring a production server down almost instantly.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;&lt;strong&gt;What’s a SYN flood attack?&lt;/strong&gt; A SYN flood is a type of &lt;strong&gt;DoS or DDoS attack&lt;/strong&gt; where an attacker sends a flood of fake TCP connection requests (SYN packets) but never completes the handshake.&lt;/p&gt;&lt;p&gt;The old firewall setup worked: it used a few &lt;strong&gt;mangle table&lt;/strong&gt; rules to drop suspicious packets, some UFW rate limits to slow down floods, and a basic Fail2ban jail that parsed UFW logs and temporarily blocked offending IPs.&lt;/p&gt;&lt;p&gt;That setup definitely helped – it could stop simple SYN floods and keep the server stable under light attack – but it also had &lt;strong&gt;limitations and design issues&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;The rate limits were too strict, which sometimes blocked legitimate users. Logging wasn’t rate-limited either, so during heavy attacks the server logs could quickly fill up.&lt;/p&gt;&lt;p&gt;Fail2ban relied on adding one &lt;code&gt;DROP&lt;/code&gt; rule per banned IP through iptables, which didn’t scale well when you’re dealing with hundreds of attackers.&lt;/p&gt;&lt;p&gt;It also became clear that SYN floods weren’t the only problem. Some attackers avoided the SYN limit altogether by opening a large number of concurrent connections and keeping them idle – a form of &lt;strong&gt;connection flood&lt;/strong&gt;  that slowly consumed resources.&lt;/p&gt;&lt;p&gt;And because I didn’t yet whitelist Cloudflare’s edge IP ranges, &lt;strong&gt;some Cloudflare addresses were being blocked as well&lt;/strong&gt; , which caused legitimate traffic to get filtered out.&lt;/p&gt;&lt;p&gt;Overall, the old setup worked, but it had rough edges that needed to be fixed. It was time to redesign it into something more efficient, scalable, and aware of real-world traffic patterns.&lt;/p&gt;&lt;h2&gt;Design Goals and Architecture&lt;/h2&gt;&lt;p&gt;When I started redesigning my old setup into &lt;strong&gt;UFW+&lt;/strong&gt; , I wanted to move away from static, reactive defenses and toward an &lt;strong&gt;adaptive, scalable firewall setup&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;The earlier approach worked for simple SYN floods, but it wasn’t sustainable under heavier or more diverse attacks.&lt;/p&gt;&lt;p&gt;So, I built UFW+ around three main design goals.&lt;/p&gt;&lt;h3&gt;1. Smarter Packet Filtering at the Earliest Stage&lt;/h3&gt;&lt;p&gt;The first step was improving &lt;strong&gt;packet sanity checks&lt;/strong&gt; before traffic even reached higher firewall chains.&lt;/p&gt;&lt;p&gt;In the original setup, this stage only dropped invalid and non-SYN TCP packets:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;-A PREROUTING -m conntrack --ctstate INVALID -j DROP -A PREROUTING -p tcp -m tcp ! --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j DROP
&lt;/pre&gt;&lt;p&gt;That worked, but left plenty of malformed packets untouched – so I expanded it into a full set of protocol sanity checks.&lt;/p&gt;&lt;p&gt;The idea was to make this stage act like a &lt;strong&gt;first-line scrubber&lt;/strong&gt; , eliminating garbage packets such as:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;TCP packets with impossible or illegal flag combinations (like SYN+FIN or SYN+RST)&lt;/li&gt;&lt;li&gt;NULL and Xmas scans&lt;/li&gt;&lt;li&gt;Spoofed packets claiming private, loopback, or reserved IP sources&lt;/li&gt;&lt;li&gt;TCP handshakes with invalid MSS values, which often signal malformed or probing traffic&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;By handling this at the &lt;code&gt;PREROUTING&lt;/code&gt; stage, the kernel drops malicious or malformed packets before they can consume connection-tracking resources – a lightweight way to protect the server from protocol abuse and spoofing.&lt;/p&gt;&lt;h3&gt;2. Adaptive Connection and Rate Controls&lt;/h3&gt;&lt;p&gt;In the earlier configuration, I used a fixed limit of 10 SYN packets per second per IP, with a small burst of 20.&lt;/p&gt;&lt;p&gt;There were no limits on total concurrent connections, so the firewall could only handle fast SYN floods – it didn’t recognize slower connection floods where attackers opened hundreds of idle sessions.&lt;/p&gt;&lt;p&gt;In short, it wasn’t adaptive. It applied the same rule to everyone, regardless of behavior, and it didn’t log efficiently either, which caused log spam during heavy attacks.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;UFW+&lt;/strong&gt; changes that with three key improvements:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Dynamic per-source SYN rate limiting:&lt;/strong&gt; Each source IP can open up to 30 new TCP connections per second (with a 60-packet burst). This allows for real traffic spikes while still throttling attacks.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Connection concurrency caps:&lt;/strong&gt; To counter &lt;em&gt;slow&lt;/em&gt; connection floods – where attackers open many idle sessions instead of rapid SYN bursts – each IP is capped at 100 concurrent connections.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Rate-limited logging (1 log per minute per IP):&lt;/strong&gt; Both the connection-limit and SYN flood rules log at most once per minute per source. This prevents log flooding under attack and gives Fail2ban a clean, steady signal to react to.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;These controls are layered: &lt;strong&gt;connection limits are evaluated first&lt;/strong&gt; , then &lt;strong&gt;SYN flood checks&lt;/strong&gt;  apply only to new handshakes that pass through.&lt;/p&gt;&lt;p&gt;Cloudflare edge IPs are whitelisted early so legitimate proxied traffic bypasses the analysis entirely.&lt;/p&gt;&lt;h3&gt;3. Fail2ban as an Adaptive Enforcement Layer&lt;/h3&gt;&lt;p&gt;The biggest shift in &lt;strong&gt;UFW+&lt;/strong&gt; is how bans are applied and managed.&lt;/p&gt;&lt;p&gt;Previously, I used the classic &lt;code&gt;iptables&lt;/code&gt; action in Fail2ban:&lt;/p&gt;&lt;pre data-language=&quot;ini&quot;&gt;action = iptables[type=allports, name=synflood, chain=fail2ban, protocol=tcp]
&lt;/pre&gt;&lt;p&gt;Each ban inserted a new &lt;code&gt;DROP&lt;/code&gt; rule directly into the firewall – fine for a few attackers, but not for hundreds.&lt;/p&gt;&lt;p&gt;Instead of inserting one &lt;code&gt;DROP&lt;/code&gt; rule per IP (which doesn’t scale well in iptables), Fail2ban now interacts directly with &lt;strong&gt;nftables sets&lt;/strong&gt;  – a native and efficient way to maintain large lists of banned IPs.&lt;/p&gt;&lt;p&gt;Each ban action simply adds the offending IP address to an nftables set referenced by a single rule, keeping the ruleset compact and performant.&lt;/p&gt;&lt;p&gt;The Fail2ban configuration itself became smarter and more dynamic:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;It now uses the &lt;strong&gt;systemd journal backend&lt;/strong&gt;  to read kernel-level UFW logs directly, improving accuracy and avoiding duplicate log parsing.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Two specialized jails&lt;/strong&gt;  monitor different log patterns:&lt;ul&gt;&lt;li&gt;&lt;code&gt;synflood&lt;/code&gt; for excessive handshake attempts&lt;/li&gt;&lt;li&gt;&lt;code&gt;connlimit&lt;/code&gt; for clients exceeding concurrent connection caps&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;Both jails share the same &lt;strong&gt;adaptive banning policy&lt;/strong&gt; : 3 strikes within 5 minutes triggers a 5-minute ban, which doubles on each repeat offense (up to 1 hour). This gives real users a margin of error while escalating punishment for persistent attackers.&lt;/li&gt;&lt;li&gt;A &lt;strong&gt;recidive jail&lt;/strong&gt;  tracks repeat offenders across days, applying bans that grow up to 3 weeks for chronic abuse.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;✅&lt;/p&gt;&lt;p&gt;The Fail2ban database retention was extended to &lt;strong&gt;30 days&lt;/strong&gt;  to support long-term tracking for recidive detection.&lt;/p&gt;&lt;h2&gt;Architecture Overview&lt;/h2&gt;&lt;p&gt;To understand how all the pieces of &lt;strong&gt;UFW+&lt;/strong&gt; fit together, it helps to look at the overall flow of packet handling and rule evaluation.&lt;/p&gt;&lt;p&gt;Because &lt;strong&gt;nftables&lt;/strong&gt;  processes packets based on &lt;strong&gt;hook priorities&lt;/strong&gt;  (lower numbers are evaluated first), Fail2ban’s drop rule chain is inserted with a priority such that banned IPs are dropped &lt;em&gt;before&lt;/em&gt;  any UFW-based chain.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Think of it like airport security:&lt;/strong&gt; the ID check happens before the boarding gate, so someone flagged early never reaches the later stations.&lt;/p&gt;&lt;p&gt;In practice, this happens even before the &lt;code&gt;before.rules&lt;/code&gt; file is evaluated, ensuring that once an IP is banned, it never reaches our UFW logic at all.&lt;/p&gt;&lt;p&gt;Here’s how the full traffic flow works:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;┌───────────────────────────────────────────┐ │ Kernel (nftables mangle) │ │ → Drops invalid/spoofed/malformed packets │ └───────────────────────────────────────────┘ ↓ ┌───────────────────────────────────────────┐ │ Fail2ban nftables drop sets │ │ → Banned IPs are dropped immediately │ │ via high-priority hook (before UFW) │ └───────────────────────────────────────────┘ ↓ ┌───────────────────────────────────────────┐ │ UFW (nftables backend) │ │ → Cloudflare whitelist (first) │ │ → Per-IP connection concurrency limits │ │ → SYN flood rate limiting │ └───────────────────────────────────────────┘ ↓ ┌───────────────────────────────────────────┐ │ Fail2ban adaptive layer │ │ → Monitors kernel logs (systemd backend) │ │ → Updates nftables ban sets in real time │ │ → Escalates bans on repeat offenses │ └───────────────────────────────────────────┘
&lt;/pre&gt;&lt;p&gt;This order ensures that:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Cloudflare traffic&lt;/strong&gt;  is trusted and bypasses analysis entirely.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Connection caps&lt;/strong&gt;  are evaluated before SYN flood checks (since they act on already-open sessions).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;SYN flood limits&lt;/strong&gt;  handle new connection bursts.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Fail2ban bans&lt;/strong&gt;  cut off attackers before UFW even runs.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Everything above the UFW layer (the mangle table and Fail2ban drop chain) happens early in packet processing, which keeps the firewall fast and efficient even under heavy attack.&lt;/p&gt;&lt;h2&gt;Configuration Walkthrough&lt;/h2&gt;&lt;p&gt;Now that you understand the reasoning and design behind &lt;strong&gt;UFW+&lt;/strong&gt; , it’s time to put it into action.&lt;/p&gt;&lt;p&gt;This walkthrough takes you through every step of building the setup – from preparing your server and defining early packet filters to configuring &lt;strong&gt;adaptive UFW and Fail2ban rules&lt;/strong&gt;  and integrating them with &lt;strong&gt;nftables&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;You can follow along on a fresh &lt;strong&gt;Ubuntu 24.04 LTS&lt;/strong&gt;  server (recommended), or adapt the steps for another Debian-based distribution.&lt;/p&gt;&lt;p&gt;Each section includes both the commands and the reasoning behind them – so you’ll know &lt;em&gt;why&lt;/em&gt;  every rule exists, not just how to copy it.&lt;/p&gt;&lt;h3&gt;Server Preparation&lt;/h3&gt;&lt;p&gt;Before adding any firewall rules, it’s important to start with a clean and secure foundation.&lt;/p&gt;&lt;p&gt;These steps prepare your Ubuntu 24.04 LTS server for the UFW+ setup, ensuring consistency, proper logging, and a safe configuration environment.&lt;/p&gt;&lt;h4&gt;Update and upgrade your server&lt;/h4&gt;&lt;p&gt;Always begin by bringing your server up to date.&lt;/p&gt;&lt;p&gt;This ensures you have the latest kernel, security patches, and package versions:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt update &amp;amp;&amp;amp; sudo apt dist-upgrade -y
&lt;/pre&gt;&lt;p&gt;If the server reports that a reboot is required to apply a new kernel, you can reboot later after completing all preparation steps.&lt;/p&gt;&lt;h4&gt;Create a non-root user&lt;/h4&gt;&lt;p&gt;As a good security practice, avoid working directly as root.&lt;/p&gt;&lt;p&gt;Instead, create a new user with &lt;code&gt;sudo&lt;/code&gt; privileges and use it for all management tasks:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo adduser yourusername sudo usermod -aG sudo yourusername
&lt;/pre&gt;&lt;p&gt;Then exit the SSH session and reconnect using your new user.&lt;/p&gt;&lt;h4&gt;Set your hostname and timezone&lt;/h4&gt;&lt;p&gt;Setting a proper hostname and timezone helps keep your logs consistent and easy to read:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo hostnamectl set-hostname yourservername.yourdomain.com sudo timedatectl set-timezone Your/Timezone
&lt;/pre&gt;&lt;p&gt;Always set a valid FQDN (Fully Qualified Domain Name) for the hostname.&lt;/p&gt;&lt;h4&gt;Enable UFW and protect SSH access&lt;/h4&gt;&lt;p&gt;Start by enabling UFW and applying a rate limit to your SSH port:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw limit 22/tcp sudo ufw enable
&lt;/pre&gt;&lt;p&gt;This allows SSH connections but slows down repeated failed attempts, reducing the chance of brute-force attacks.&lt;/p&gt;&lt;h4&gt;Allow HTTP and HTTPS traffic&lt;/h4&gt;&lt;p&gt;Next, open your web ports (80 and 443) – these should always be accessible to the public:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw allow 80/tcp sudo ufw allow 443/tcp
&lt;/pre&gt;&lt;p&gt;At this stage, you’re simply allowing web traffic – &lt;em&gt;UFW+ will later analyze and protect these ports.&lt;/em&gt;&lt;/p&gt;&lt;h4&gt;Install a web server for testing (optional)&lt;/h4&gt;&lt;p&gt;If you’re working on a clean test server, install &lt;strong&gt;Nginx&lt;/strong&gt;  to generate normal web traffic for testing:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt install nginx -y
&lt;/pre&gt;&lt;p&gt;If you’re running this setup on a production server, you can use your existing web server instead.&lt;/p&gt;&lt;h4&gt;Install and prepare Fail2ban&lt;/h4&gt;&lt;p&gt;Finally, install &lt;strong&gt;Fail2ban&lt;/strong&gt;  – it will serve as the &lt;strong&gt;adaptive banning layer&lt;/strong&gt; in your &lt;strong&gt;UFW+&lt;/strong&gt; setup:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt install fail2ban -y
&lt;/pre&gt;&lt;p&gt;Then copy the default configuration files to their editable &lt;code&gt;.local&lt;/code&gt; versions:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local sudo cp /etc/fail2ban/fail2ban.conf /etc/fail2ban/fail2ban.local
&lt;/pre&gt;&lt;p&gt;Editing &lt;code&gt;.local&lt;/code&gt; files ensures that your custom settings won’t be overwritten during server or package updates.&lt;/p&gt;&lt;h4&gt;Reboot your server&lt;/h4&gt;&lt;p&gt;Once everything is done, reboot the server:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo reboot
&lt;/pre&gt;&lt;p&gt;This ensures that all changes are properly applied and any pending updates or configurations take full effect.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;For a complete walkthrough on preparing a fresh server – including disabling root access, using SSH keys, and other best practices – check out my full guide: &lt;a href=&quot;https://ivansalloum.com/preparing-your-ubuntu-server-for-first-use/&quot;&gt;Preparing Your Ubuntu Server for First Use&lt;/a&gt;&lt;/p&gt;&lt;h3&gt;UFW and nftables on Modern Ubuntu&lt;/h3&gt;&lt;p&gt;Starting with &lt;strong&gt;Ubuntu 22.04 and later (including 24.04)&lt;/strong&gt; , UFW uses &lt;strong&gt;nftables&lt;/strong&gt; under the hood by default – even though it still looks like it’s using &lt;strong&gt;iptables&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;When you run UFW commands, they’re translated through the &lt;code&gt;iptables-nft&lt;/code&gt; compatibility layer, which means the actual packet filtering happens inside nftables.&lt;/p&gt;&lt;p&gt;You can confirm this by running:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo update-alternatives --display iptables
&lt;/pre&gt;&lt;p&gt;And you’ll see something like:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;link currently points to /usr/sbin/iptables-nft
&lt;/pre&gt;&lt;p&gt;To view the live nftables rules created by UFW, run:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo nft list ruleset
&lt;/pre&gt;&lt;p&gt;In short, &lt;strong&gt;UFW on modern Ubuntu already uses nftables by default&lt;/strong&gt;  – UFW+ simply takes advantage of that.&lt;/p&gt;&lt;h3&gt;Base Mangle Table Rules (Packet Sanity Filtering)&lt;/h3&gt;&lt;p&gt;The first real line of defense in &lt;strong&gt;UFW+&lt;/strong&gt; starts at the &lt;strong&gt;mangle table&lt;/strong&gt;  – before UFW’s normal chains even run.&lt;/p&gt;&lt;p&gt;The mangle table handles packets very early – in the &lt;code&gt;PREROUTING&lt;/code&gt; stage – before they reach normal firewall rules.&lt;/p&gt;&lt;p&gt;These rules act as &lt;strong&gt;packet sanity checks&lt;/strong&gt; , filtering out clearly invalid, spoofed, or malformed packets &lt;em&gt;as early as possible&lt;/em&gt; to prevent them from consuming server resources.&lt;/p&gt;&lt;h4&gt;Edit the UFW &lt;code&gt;before.rules&lt;/code&gt; file (IPv4)&lt;/h4&gt;&lt;p&gt;Open the file using your preferred text editor:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo vim /etc/ufw/before.rules
&lt;/pre&gt;&lt;p&gt;I use &lt;code&gt;vim&lt;/code&gt;, but you can use &lt;code&gt;nano&lt;/code&gt;, &lt;code&gt;vi&lt;/code&gt;, or anything you like.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;&lt;strong&gt;If you’re using Vim:&lt;/strong&gt; Press &lt;code&gt;i&lt;/code&gt; to enter &lt;strong&gt;insert&lt;/strong&gt;  mode and make your edits. When you’re done, press &lt;code&gt;Esc&lt;/code&gt;, then type &lt;code&gt;:wq&lt;/code&gt; and hit &lt;code&gt;Enter&lt;/code&gt; to save and exit.&lt;/p&gt;&lt;p&gt;Scroll to the very bottom of the file (after the last &lt;code&gt;COMMIT&lt;/code&gt; line), and append the following block:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;*mangle :PREROUTING ACCEPT [0:0] # Drop all packets flagged as INVALID by connection tracking -A PREROUTING -m conntrack --ctstate INVALID -j DROP # Drop NEW incoming TCP packets that do not have the SYN flag (possible scans/attacks) -A PREROUTING -p tcp ! --syn -m conntrack --ctstate NEW -j DROP # Drop packets with all TCP flags set (Xmas tree scan) -A PREROUTING -p tcp --tcp-flags ALL ALL -j DROP # Drop packets with no TCP flags set (NULL scan) -A PREROUTING -p tcp --tcp-flags ALL NONE -j DROP # Drop TCP packets with illegal flag combinations -A PREROUTING -p tcp --tcp-flags SYN,FIN SYN,FIN -j DROP -A PREROUTING -p tcp --tcp-flags SYN,RST SYN,RST -j DROP # Drop packets with invalid or spoofed source addresses -A PREROUTING -s 0.0.0.0/8 -j DROP -A PREROUTING -s 10.0.0.0/8 -j DROP -A PREROUTING -s 172.16.0.0/12 -j DROP -A PREROUTING -s 192.168.0.0/16 -j DROP -A PREROUTING -s 127.0.0.0/8 ! -i lo -j DROP # Drop TCP packets with an invalid MSS (Maximum Segment Size) -A PREROUTING -p tcp -m conntrack --ctstate NEW -m tcpmss ! --mss 536:65535 -j DROP COMMIT
&lt;/pre&gt;&lt;p&gt;Save and exit.&lt;/p&gt;&lt;h4&gt;Edit the UFW &lt;code&gt;before6.rules&lt;/code&gt; file (IPv6)&lt;/h4&gt;&lt;p&gt;Open:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo vim /etc/ufw/before6.rules
&lt;/pre&gt;&lt;p&gt;Scroll to the bottom (again, after the last &lt;code&gt;COMMIT&lt;/code&gt; line), and append this block:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;*mangle :PREROUTING ACCEPT [0:0] # Drop all packets flagged as INVALID by connection tracking -A PREROUTING -m conntrack --ctstate INVALID -j DROP # Drop NEW incoming TCP packets that do not have the SYN flag (possible scans/attacks) -A PREROUTING -p tcp ! --syn -m conntrack --ctstate NEW -j DROP # Drop packets with all TCP flags set (Xmas tree scan) -A PREROUTING -p tcp --tcp-flags ALL ALL -j DROP # Drop packets with no TCP flags set (NULL scan) -A PREROUTING -p tcp --tcp-flags ALL NONE -j DROP # Drop TCP packets with illegal flag combinations -A PREROUTING -p tcp --tcp-flags SYN,FIN SYN,FIN -j DROP -A PREROUTING -p tcp --tcp-flags SYN,RST SYN,RST -j DROP # Drop IPv6 ULA (Unique Local Addresses) and spoofed loopback -A PREROUTING -s fc00::/7 -j DROP -A PREROUTING ! -i lo -s ::1/128 -j DROP # Drop TCP packets with an invalid MSS (Maximum Segment Size) -A PREROUTING -p tcp -m conntrack --ctstate NEW -m tcpmss ! --mss 1220:65535 -j DROP COMMIT
&lt;/pre&gt;&lt;p&gt;Save and exit.&lt;/p&gt;&lt;h4&gt;Reload UFW&lt;/h4&gt;&lt;p&gt;Apply the changes:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw reload
&lt;/pre&gt;&lt;p&gt;This reloads UFW and applies the new mangle table rules immediately – there’s no need to reboot or restart any services.&lt;/p&gt;&lt;h4&gt;Verify the new rules&lt;/h4&gt;&lt;p&gt;To confirm that your new mangle table rules are active:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo nft list ruleset | grep mangle -A 20
&lt;/pre&gt;&lt;p&gt;You should see your &lt;code&gt;PREROUTING&lt;/code&gt; entries under the mangle table.&lt;/p&gt;&lt;h4&gt;What these rules do&lt;/h4&gt;&lt;p&gt;Each rule in the mangle table serves a specific purpose.&lt;/p&gt;&lt;p&gt;Together, they form a &lt;strong&gt;packet sanity layer&lt;/strong&gt;  that keeps invalid or malicious traffic from even entering your firewall.&lt;/p&gt;&lt;h5&gt;Drop &lt;code&gt;INVALID&lt;/code&gt; packets&lt;/h5&gt;&lt;p&gt;These are packets flagged by the Linux &lt;strong&gt;connection tracker&lt;/strong&gt;  as inconsistent – for example:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;A reply to a connection that doesn’t exist,&lt;/li&gt;&lt;li&gt;Fragments missing from a previous packet, or&lt;/li&gt;&lt;li&gt;Packets with mismatched headers.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;They’re useless for normal communication and often appear during floods or malformed traffic bursts.&lt;/p&gt;&lt;h5&gt;Drop &lt;code&gt;NEW&lt;/code&gt; TCP packets without the SYN flag&lt;/h5&gt;&lt;p&gt;A valid TCP connection always starts with a &lt;strong&gt;SYN&lt;/strong&gt;  packet.&lt;/p&gt;&lt;p&gt;If a packet claims to start a new connection (&lt;code&gt;NEW&lt;/code&gt; state) but has &lt;strong&gt;no SYN flag&lt;/strong&gt; , it’s almost certainly part of a &lt;strong&gt;scan&lt;/strong&gt;  or &lt;strong&gt;spoofed&lt;/strong&gt; attempt.&lt;/p&gt;&lt;h5&gt;Block Xmas and NULL scans&lt;/h5&gt;&lt;p&gt;Attackers sometimes send packets with:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;All flags set&lt;/strong&gt;  (Xmas tree scan), or&lt;/li&gt;&lt;li&gt;&lt;strong&gt;No flags at all&lt;/strong&gt;  (NULL scan).&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;These are classic reconnaissance techniques – often used by tools like &lt;strong&gt;Nmap&lt;/strong&gt; –  to map open ports or trigger unexpected responses.&lt;/p&gt;&lt;p&gt;They’re never part of normal TCP traffic.&lt;/p&gt;&lt;h5&gt;Drop illegal TCP flag combinations&lt;/h5&gt;&lt;p&gt;Packets that combine &lt;strong&gt;SYN+FIN&lt;/strong&gt;  or &lt;strong&gt;SYN+RST&lt;/strong&gt;  flags violate the TCP standard – a connection can’t be both starting and finishing (or resetting) at once.&lt;/p&gt;&lt;p&gt;They often indicate broken or malicious network stacks.&lt;/p&gt;&lt;h5&gt;Drop spoofed source IP addresses&lt;/h5&gt;&lt;p&gt;These rules block packets pretending to come from:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Private networks&lt;/strong&gt;  (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Reserved addresses&lt;/strong&gt;  (0.0.0.0/8)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Localhost spoofing&lt;/strong&gt;  (127.0.0.0/8 arriving on a non-loopback interface)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Traffic from these ranges should &lt;em&gt;never&lt;/em&gt;  appear on a public WAN interface.&lt;/p&gt;&lt;p&gt;If it does, it’s almost always a &lt;strong&gt;spoofed&lt;/strong&gt;  packet – someone faking source IPs to hide their identity or poison routing tables.&lt;/p&gt;&lt;h5&gt;Drop packets with invalid MSS values&lt;/h5&gt;&lt;p&gt;This rule checks the &lt;strong&gt;Maximum Segment Size (MSS)&lt;/strong&gt;  in new TCP handshakes and drops any packet outside the legal range:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;536 bytes&lt;/strong&gt;  – minimum allowed for IPv4 (&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc879&quot;&gt;RFC 879&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;1220 bytes&lt;/strong&gt; – minimum derived for IPv6 (based on &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc8200&quot;&gt;RFC 8200&apos;s&lt;/a&gt; 1280 Bytes MTU)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;65535 bytes&lt;/strong&gt;  – theoretical maximum&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;If a SYN packet advertises a value smaller or larger than these limits, it’s likely malformed or intentionally crafted.&lt;/p&gt;&lt;h3&gt;Whitelisting Cloudflare IP Ranges&lt;/h3&gt;&lt;p&gt;If your website is behind &lt;strong&gt;Cloudflare&lt;/strong&gt; , most incoming connections to your server actually come from Cloudflare’s &lt;strong&gt;edge proxy IPs&lt;/strong&gt; , not directly from your visitors.&lt;/p&gt;&lt;p&gt;To avoid rate-limiting or blocking Cloudflare itself, you need to &lt;strong&gt;whitelist their IP ranges&lt;/strong&gt;  before your other firewall chains (like connection or SYN flood limits) are evaluated.&lt;/p&gt;&lt;p&gt;This ensures that all legitimate traffic passed through Cloudflare reaches your server without restriction.&lt;/p&gt;&lt;h4&gt;Edit the UFW &lt;code&gt;before.rules&lt;/code&gt; file (IPv4)&lt;/h4&gt;&lt;p&gt;Open the file:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo vim /etc/ufw/before.rules
&lt;/pre&gt;&lt;p&gt;Scroll until just &lt;strong&gt;before&lt;/strong&gt;  UFW’s final &lt;code&gt;# don&apos;t delete the &apos;COMMIT&apos; line&lt;/code&gt; in the &lt;code&gt;*filter&lt;/code&gt; section, and insert the following block:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;:CF-EDGE - [0:0] # Send only web traffic to the CF-EDGE chain -A ufw-before-input -p tcp --dport 80 -j CF-EDGE -A ufw-before-input -p tcp --dport 443 -j CF-EDGE # Allow from Cloudflare edge networks -A CF-EDGE -s 173.245.48.0/20 -j ACCEPT -A CF-EDGE -s 103.21.244.0/22 -j ACCEPT -A CF-EDGE -s 103.22.200.0/22 -j ACCEPT -A CF-EDGE -s 103.31.4.0/22 -j ACCEPT -A CF-EDGE -s 141.101.64.0/18 -j ACCEPT -A CF-EDGE -s 108.162.192.0/18 -j ACCEPT -A CF-EDGE -s 190.93.240.0/20 -j ACCEPT -A CF-EDGE -s 188.114.96.0/20 -j ACCEPT -A CF-EDGE -s 197.234.240.0/22 -j ACCEPT -A CF-EDGE -s 198.41.128.0/17 -j ACCEPT -A CF-EDGE -s 162.158.0.0/15 -j ACCEPT -A CF-EDGE -s 104.16.0.0/13 -j ACCEPT -A CF-EDGE -s 104.24.0.0/14 -j ACCEPT -A CF-EDGE -s 172.64.0.0/13 -j ACCEPT -A CF-EDGE -s 131.0.72.0/22 -j ACCEPT # Anything else continues through normal UFW processing -A CF-EDGE -j RETURN
&lt;/pre&gt;&lt;p&gt;Then leave UFW’s own &lt;code&gt;COMMIT&lt;/code&gt; line in place, and below it you’ll still have your &lt;code&gt;*mangle&lt;/code&gt; section and its own &lt;code&gt;COMMIT&lt;/code&gt; – keep both.&lt;/p&gt;&lt;p&gt;These rules &lt;strong&gt;redirect web traffic (ports 80 and 443)&lt;/strong&gt;  from the main &lt;code&gt;ufw-before-input&lt;/code&gt; chain into our custom &lt;code&gt;CF-EDGE&lt;/code&gt; chain. That allows UFW to quickly check if a packet comes from one of Cloudflare’s edge IPs.&lt;/p&gt;&lt;p&gt;If it does, it’s accepted immediately. If not, it continues through the normal UFW rule flow.&lt;/p&gt;&lt;h4&gt;Edit the UFW &lt;code&gt;before6.rules&lt;/code&gt; file (IPv6)&lt;/h4&gt;&lt;p&gt;Open the file:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo vim /etc/ufw/before6.rules
&lt;/pre&gt;&lt;p&gt;Add this block &lt;strong&gt;before&lt;/strong&gt;  the final &lt;code&gt;COMMIT&lt;/code&gt; in the &lt;code&gt;*filter&lt;/code&gt; section:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;:CF6-EDGE - [0:0] # Send only web traffic to the CF6-EDGE chain -A ufw6-before-input -p tcp --dport 80 -j CF6-EDGE -A ufw6-before-input -p tcp --dport 443 -j CF6-EDGE # Allow from Cloudflare edge networks -A CF6-EDGE -s 2400:cb00::/32 -j ACCEPT -A CF6-EDGE -s 2606:4700::/32 -j ACCEPT -A CF6-EDGE -s 2803:f800::/32 -j ACCEPT -A CF6-EDGE -s 2405:b500::/32 -j ACCEPT -A CF6-EDGE -s 2405:8100::/32 -j ACCEPT -A CF6-EDGE -s 2a06:98c0::/29 -j ACCEPT -A CF6-EDGE -s 2c0f:f248::/32 -j ACCEPT # Anything else continues through normal UFW processing -A CF6-EDGE -j RETURN
&lt;/pre&gt;&lt;p&gt;These rules do the same as the IPv4 ones – they allow Cloudflare’s IPv6 edge IPs to pass through without restriction.&lt;/p&gt;&lt;h4&gt;Reload UFW&lt;/h4&gt;&lt;p&gt;Apply the changes:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw reload
&lt;/pre&gt;&lt;p&gt;This applies your updated rules immediately.&lt;/p&gt;&lt;p&gt;❗&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Cloudflare’s IP ranges sometimes change. You can always find the latest list here: &lt;a href=&quot;https://www.cloudflare.com/ips/&quot;&gt;https://www.cloudflare.com/ips/&lt;/a&gt; Consider writing a small script or cron job that checks this list regularly and updates your rules automatically when Cloudflare adds or removes IP ranges.&lt;/p&gt;&lt;h3&gt;Connection Limits &amp;amp; SYN Flood Protection&lt;/h3&gt;&lt;p&gt;After filtering packets and whitelisting Cloudflare, it’s time to add the &lt;strong&gt;connection and rate control rules&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;These will become the core of your firewall – keeping traffic balanced and preventing abusive patterns from overwhelming the server.&lt;/p&gt;&lt;h4&gt;Edit the UFW &lt;code&gt;before.rules&lt;/code&gt; file (IPv4)&lt;/h4&gt;&lt;p&gt;Open the file:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo vim /etc/ufw/before.rules
&lt;/pre&gt;&lt;p&gt;Then add these blocks &lt;strong&gt;after&lt;/strong&gt;  your Cloudflare rules and before UFW’s final &lt;code&gt;COMMIT&lt;/code&gt; line:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;:CONNLIMIT - [0:0] # Send only new SYN packets for web ports to the CONNLIMIT chain -A ufw-before-input -p tcp --syn --dport 80 -j CONNLIMIT -A ufw-before-input -p tcp --syn --dport 443 -j CONNLIMIT # connlimit checks how many concurrent connections each source IP already holds # If a single IP opens more than 100 concurrent connections: # - Log the event (limited to once per minute to avoid log spam) # - Drop further connections -A CONNLIMIT -m conntrack --ctstate NEW -m connlimit --connlimit-saddr --connlimit-above 100 --connlimit-mask 32 -m limit --limit 1/min --limit-burst 1 -j LOG --log-prefix &amp;quot;[UFW Connlimit] &amp;quot; -A CONNLIMIT -m conntrack --ctstate NEW -m connlimit --connlimit-saddr --connlimit-above 100 --connlimit-mask 32 -j DROP # Below the limit → continue to SYN_FLOOD_PROTECTION -A CONNLIMIT -j RETURN :SYN_FLOOD_PROTECTION - [0:0] # Send only new SYN packets for web ports to the SYN_FLOOD_PROTECTION chain -A ufw-before-input -p tcp --syn --dport 80 -j SYN_FLOOD_PROTECTION -A ufw-before-input -p tcp --syn --dport 443 -j SYN_FLOOD_PROTECTION # Normal traffic (≤30 new connections/sec, 60 burst) → allow immediately # hashlimit keeps track of per-IP connection rates so we can allow normal visitors -A SYN_FLOOD_PROTECTION -m conntrack --ctstate NEW -m hashlimit --hashlimit-name synlimit4 --hashlimit-upto 30/second --hashlimit-burst 60 --hashlimit-mode srcip --hashlimit-srcmask 32 -j RETURN # If rate exceeded → log once per minute, then drop -A SYN_FLOOD_PROTECTION -m conntrack --ctstate NEW -m hashlimit --hashlimit-name synlimit4 --hashlimit-above 30/second --hashlimit-burst 60 --hashlimit-mode srcip --hashlimit-srcmask 32 -m limit --limit 1/minute --limit-burst 1 -j LOG --log-prefix &amp;quot;[UFW SYN Flood Detected] &amp;quot; -A SYN_FLOOD_PROTECTION -m conntrack --ctstate NEW -m hashlimit --hashlimit-name synlimit4 --hashlimit-above 30/second --hashlimit-burst 60 --hashlimit-mode srcip --hashlimit-srcmask 32 -j DROP # Return to normal UFW processing -A SYN_FLOOD_PROTECTION -j RETURN
&lt;/pre&gt;&lt;p&gt;Let’s quickly break down what’s actually happening behind these rules.&lt;/p&gt;&lt;p&gt;They use a few important &lt;strong&gt;firewall modules&lt;/strong&gt;  that make rate control possible:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;connlimit&lt;/code&gt;&lt;/strong&gt;  – counts how many concurrent connections each IP address has open.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;hashlimit&lt;/code&gt;&lt;/strong&gt;  – tracks packet rates per IP address. It limits how many new TCP handshakes an IP can start each second.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;limit&lt;/code&gt;&lt;/strong&gt;  – rate-limits how often a rule can log messages. This keeps your logs clean and prevents a flood of entries during an attack.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Together, these modules make the firewall &lt;em&gt;adaptive&lt;/em&gt; – it reacts to each IP individually instead of applying one static limit to everyone.&lt;/p&gt;&lt;h4&gt;Edit the UFW &lt;code&gt;before6.rules&lt;/code&gt; file (IPv6)&lt;/h4&gt;&lt;p&gt;Open the file:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo vim /etc/ufw/before6.rules
&lt;/pre&gt;&lt;p&gt;Add the equivalent IPv6 version:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;:CONNLIMIT - [0:0] # Route new SYN packets on web ports to the CONNLIMIT chain -A ufw6-before-input -p tcp --syn --dport 80 -j CONNLIMIT -A ufw6-before-input -p tcp --syn --dport 443 -j CONNLIMIT # connlimit checks how many concurrent connections each IPv6 address holds # If a single IPv6 address opens more than 100 concurrent connections: # - Log once per minute # - Drop further connections -A CONNLIMIT -m conntrack --ctstate NEW -m connlimit --connlimit-saddr --connlimit-above 100 --connlimit-mask 128 -m limit --limit 1/min --limit-burst 1 -j LOG --log-prefix &amp;quot;[UFW Connlimit] &amp;quot; -A CONNLIMIT -m conntrack --ctstate NEW -m connlimit --connlimit-saddr --connlimit-above 100 --connlimit-mask 128 -j DROP # Below the limit → continue to SYN_FLOOD_PROTECTION -A CONNLIMIT -j RETURN :SYN_FLOOD_PROTECTION - [0:0] # Route new SYN packets on web ports to the SYN_FLOOD_PROTECTION chain -A ufw6-before-input -p tcp --syn --dport 80 -j SYN_FLOOD_PROTECTION -A ufw6-before-input -p tcp --syn --dport 443 -j SYN_FLOOD_PROTECTION # Normal traffic (≤30 new connections/sec, 60 burst) → allow immediately # Same hashlimit module handles IPv6 sources, just with a /128 mask # hashlimit keeps track of per-IP connection rates so we can allow normal visitors -A SYN_FLOOD_PROTECTION -m conntrack --ctstate NEW -m hashlimit --hashlimit-name synlimit6 --hashlimit-upto 30/second --hashlimit-burst 60 --hashlimit-mode srcip --hashlimit-srcmask 128 -j RETURN # Over the rate limit → log once per minute, then drop -A SYN_FLOOD_PROTECTION -m conntrack --ctstate NEW -m hashlimit --hashlimit-name synlimit6 --hashlimit-above 30/second --hashlimit-burst 60 --hashlimit-mode srcip --hashlimit-srcmask 128 -m limit --limit 1/minute --limit-burst 1 -j LOG --log-prefix &amp;quot;[UFW SYN Flood Detected] &amp;quot; -A SYN_FLOOD_PROTECTION -m conntrack --ctstate NEW -m hashlimit --hashlimit-name synlimit6 --hashlimit-above 30/second --hashlimit-burst 60 --hashlimit-mode srcip --hashlimit-srcmask 128 -j DROP # Return to normal UFW processing -A SYN_FLOOD_PROTECTION -j RETURN
&lt;/pre&gt;&lt;p&gt;The IPv6 rules work exactly the same way, using the same modules and logic – the only difference is the &lt;strong&gt;address mask&lt;/strong&gt; :&lt;/p&gt;&lt;ul&gt;&lt;li&gt;IPv4 uses &lt;code&gt;/32&lt;/code&gt; (one full address).&lt;/li&gt;&lt;li&gt;IPv6 uses &lt;code&gt;/128&lt;/code&gt; to match a single host, since IPv6 addresses are much longer.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Other than that, both rule sets behave identically: each monitors connections and SYN rates per IP, logs suspicious activity once per minute, and drops abusive traffic early.&lt;/p&gt;&lt;h4&gt;Reload UFW&lt;/h4&gt;&lt;p&gt;Apply the changes:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw reload
&lt;/pre&gt;&lt;p&gt;This applies your updated rules immediately.&lt;/p&gt;&lt;h4&gt;How it works&lt;/h4&gt;&lt;p&gt;The two custom chains – &lt;code&gt;CONNLIMIT&lt;/code&gt; and &lt;code&gt;SYN_FLOOD_PROTECTION&lt;/code&gt; – each handle a different kind of abuse pattern:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;CONNLIMIT&lt;/code&gt;:&lt;/strong&gt; Prevents &lt;strong&gt;slow connection floods&lt;/strong&gt; – when an attacker opens many concurrent connections and leaves them idle to consume memory and sockets. If a single IP holds more than 100 active connections at once, it’s logged (once per minute) and then dropped. All other IPs pass through normally.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;SYN_FLOOD_PROTECTION&lt;/code&gt;:&lt;/strong&gt; Prevents &lt;strong&gt;fast SYN floods&lt;/strong&gt; – large bursts of new TCP handshake attempts designed to overwhelm the kernel’s connection table. Allows up to 30 new SYN handshakes per second (with a burst buffer of 60). Anything above that is logged once per minute and dropped.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The two chains complement each other by handling &lt;em&gt;different time scales&lt;/em&gt;  of abuse:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;CONNLIMIT&lt;/code&gt; stops &lt;strong&gt;long-lived, slow-drip floods&lt;/strong&gt;  (hundreds of half-open or idle sessions).&lt;/li&gt;&lt;li&gt;&lt;code&gt;SYN_FLOOD_PROTECTION&lt;/code&gt; stops &lt;strong&gt;short, intense bursts&lt;/strong&gt;  of new handshakes.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Both chains work quietly in the background.&lt;/p&gt;&lt;h4&gt;Tuning the limits&lt;/h4&gt;&lt;p&gt;You can safely adjust these numbers depending on your traffic patterns:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Increase &lt;code&gt;--connlimit-above 100&lt;/code&gt; if your app legitimately uses many concurrent connections (for example, websockets or APIs).&lt;/li&gt;&lt;li&gt;Adjust &lt;code&gt;--hashlimit-upto 30/second&lt;/code&gt; and &lt;code&gt;--hashlimit-burst 60&lt;/code&gt; if your users trigger false positives under heavy load (for example, during peak visits).&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Start &lt;strong&gt;conservative&lt;/strong&gt; and monitor logs to see what works best for your environment.&lt;/p&gt;&lt;h3&gt;Fail2ban Integration with nftables&lt;/h3&gt;&lt;p&gt;With our UFW rules in place, it’s time to make the setup &lt;em&gt;adaptive&lt;/em&gt; – this is where &lt;strong&gt;Fail2ban&lt;/strong&gt;  comes in.&lt;/p&gt;&lt;p&gt;Fail2ban watches for repeated bad behavior in your logs and automatically blocks those IPs using &lt;strong&gt;nftables sets&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Think of it as the &lt;em&gt;memory&lt;/em&gt; layer of your firewall – it notices patterns and reacts.&lt;/p&gt;&lt;h4&gt;Disable the Default SSH Jail (Optional)&lt;/h4&gt;&lt;p&gt;On Ubuntu, Fail2ban comes with the SSH jail enabled out of the box.&lt;/p&gt;&lt;p&gt;You can confirm this by running:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo fail2ban-client status
&lt;/pre&gt;&lt;p&gt;You’ll see the &lt;code&gt;sshd&lt;/code&gt; jail already running. That’s controlled by this file:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;/etc/fail2ban/jail.d/defaults-debian.conf
&lt;/pre&gt;&lt;p&gt;It includes:&lt;/p&gt;&lt;pre data-language=&quot;ini&quot;&gt;[sshd] enabled = true
&lt;/pre&gt;&lt;p&gt;Personally, I don’t need this one – because SSH is already limited by UFW (&lt;code&gt;sudo ufw limit 22/tcp&lt;/code&gt;), and I only use SSH keys with root login and password authentication disabled.&lt;/p&gt;&lt;p&gt;If you’re in the same situation (which is recommended), you can safely disable the jail. Just don’t edit the default file – create a small override instead:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo vim /etc/fail2ban/jail.d/override.local
&lt;/pre&gt;&lt;p&gt;Add:&lt;/p&gt;&lt;pre data-language=&quot;ini&quot;&gt;[sshd] enabled = false
&lt;/pre&gt;&lt;p&gt;That’s cleaner and future-proof – your settings won’t be overwritten by package updates.&lt;/p&gt;&lt;h4&gt;Create Filters for UFW Log Events&lt;/h4&gt;&lt;p&gt;Next, we’ll teach Fail2ban how to recognize our custom UFW log messages.&lt;/p&gt;&lt;p&gt;We’ll create two filters: one for SYN flood logs and one for connection limit logs.&lt;/p&gt;&lt;p&gt;Go to the &lt;code&gt;/etc/fail2ban/filter.d/&lt;/code&gt; directory and create two new filter files:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;cd /etc/fail2ban/filter.d sudo touch synflood.conf connlimit.conf
&lt;/pre&gt;&lt;p&gt;Open &lt;code&gt;synflood.conf&lt;/code&gt; and add:&lt;/p&gt;&lt;pre data-language=&quot;ini&quot;&gt;[Definition] failregex = .*UFW SYN Flood Detected.*SRC=.*DPT=\d+.* ignoreregex =
&lt;/pre&gt;&lt;p&gt;Then open &lt;code&gt;connlimit.conf&lt;/code&gt; and add:&lt;/p&gt;&lt;pre data-language=&quot;ini&quot;&gt;[Definition] failregex = .*UFW Connlimit.*SRC=.*DPT=\d+.* ignoreregex =
&lt;/pre&gt;&lt;p&gt;That’s it.&lt;/p&gt;&lt;p&gt;Now whenever UFW logs a &lt;code&gt;[UFW SYN Flood Detected]&lt;/code&gt; or &lt;code&gt;[UFW Connlimit]&lt;/code&gt; message, Fail2ban will now recognize it instantly and take action.&lt;/p&gt;&lt;h4&gt;Extend Database Retention&lt;/h4&gt;&lt;p&gt;By default, Fail2ban forgets everything after a day. That’s not ideal for catching repeat offenders&lt;/p&gt;&lt;p&gt;For long-term tracking (especially for the recidive jail), we extend Fail2ban’s database retention period to 30 days.&lt;/p&gt;&lt;p&gt;Open the &lt;code&gt;/etc/fail2ban/fail2ban.local&lt;/code&gt; file and update:&lt;/p&gt;&lt;pre data-language=&quot;ini&quot;&gt;dbpurgeage = 30d
&lt;/pre&gt;&lt;p&gt;This makes sure Fail2ban remembers old bans, which is essential for the recidive jail we’ll set up next.&lt;/p&gt;&lt;h4&gt;Create the Jails&lt;/h4&gt;&lt;p&gt;Now for the main part – the jails that actually watch our logs and block bad IPs.&lt;/p&gt;&lt;p&gt;Go to the &lt;code&gt;/etc/fail2ban/jail.d/&lt;/code&gt; directory and create the files:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;cd /etc/fail2ban/jail.d sudo touch synflood.local connlimit.local recidive.local
&lt;/pre&gt;&lt;p&gt;Open &lt;code&gt;synflood.local&lt;/code&gt; and add:&lt;/p&gt;&lt;pre data-language=&quot;ini&quot;&gt;[synflood] enabled = true backend = systemd journalmatch = _TRANSPORT=kernel usedns = no filter = synflood banaction = nftables[type=allports, blocktype=drop] maxretry = 3 findtime = 5m bantime = 5m bantime.increment = true bantime.factor = 2 bantime.maxtime = 1h bantime.rndtime = 1m
&lt;/pre&gt;&lt;p&gt;Then open &lt;code&gt;connlimit.local&lt;/code&gt; and add:&lt;/p&gt;&lt;pre data-language=&quot;ini&quot;&gt;[connlimit] enabled = true backend = systemd journalmatch = _TRANSPORT=kernel usedns = no filter = connlimit banaction = nftables[type=allports, blocktype=drop] maxretry = 3 findtime = 5m bantime = 5m bantime.increment = true bantime.factor = 2 bantime.maxtime = 1h bantime.rndtime = 1m
&lt;/pre&gt;&lt;p&gt;Finally, open &lt;code&gt;recidive.local&lt;/code&gt; and add:&lt;/p&gt;&lt;pre data-language=&quot;ini&quot;&gt;[recidive] enabled = true usedns = no filter = recidive banaction = nftables[type=allports, blocktype=drop] logpath = /var/log/fail2ban.log maxretry = 3 findtime = 1d bantime = 7d bantime.increment = true bantime.factor = 2 bantime.maxtime = 21d bantime.rndtime = 1m
&lt;/pre&gt;&lt;p&gt;These three jails work together to handle short-term attacks and long-term offenders.&lt;/p&gt;&lt;p&gt;Each listens for the UFW log messages we defined earlier and bans abusive IPs automatically using &lt;strong&gt;nftables sets&lt;/strong&gt; – fast, clean, and scalable even with hundreds (or thousands) of bans.&lt;/p&gt;&lt;h4&gt;How the Jails Work&lt;/h4&gt;&lt;p&gt;Now that the jails are in place, here’s what’s actually happening behind the scenes.&lt;/p&gt;&lt;p&gt;Each jail watches for specific log messages coming from UFW.&lt;/p&gt;&lt;p&gt;When one of your firewall rules logs an event – like &lt;code&gt;[UFW SYN Flood Detected]&lt;/code&gt; or &lt;code&gt;[UFW Connlimit]&lt;/code&gt; – Fail2ban picks it up, checks how often it happens for that IP, and decides what to do next.&lt;/p&gt;&lt;p&gt;If the same IP keeps showing up too frequently within a certain time window (&lt;code&gt;findtime&lt;/code&gt;), Fail2ban considers it abusive and bans it for the amount of time defined by &lt;code&gt;bantime&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Why these settings:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;backend = systemd&lt;/code&gt; with** &lt;code&gt;journalmatch = _TRANSPORT=kernel&lt;/code&gt;** – tells Fail2ban to read the &lt;strong&gt;systemd journal&lt;/strong&gt; directly instead of scanning text logs. This is faster and more reliable on modern Ubuntu servers.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;usedns = no&lt;/code&gt;&lt;/strong&gt; – skips reverse DNS lookups to keep bans fast and predictable.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;banaction = nftables[type=allports, blocktype=drop]&lt;/code&gt;&lt;/strong&gt; – drops offenders through an nftables set so one rule blocks every port.&lt;/li&gt;&lt;li&gt;&lt;code&gt;**maxretry = 3**&lt;/code&gt; – three strikes inside &lt;code&gt;findtime&lt;/code&gt; triggers a ban.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;findtime = 5m&lt;/code&gt; (or &lt;code&gt;1d&lt;/code&gt; for recidive)&lt;/strong&gt; – the window Fail2ban uses to count those strikes. For example, if an IP triggers the rule three times in five minutes, it’s banned.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;bantime = 5m&lt;/code&gt;&lt;/strong&gt; – short first ban so real users recover quickly from mistakes.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;bantime.increment = true&lt;/code&gt; + &lt;code&gt;bantime.factor = 2&lt;/code&gt;&lt;/strong&gt; – makes bans smarter. If an IP keeps getting caught again, its ban time doubles each time (up to &lt;code&gt;bantime.maxtime&lt;/code&gt;).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;bantime.rndtime = 1m&lt;/code&gt;&lt;/strong&gt; – adds a random offset so bans expire at slightly different times instead of all at once.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The &lt;code&gt;recidive&lt;/code&gt; jail functions as long-term memory – monitoring Fail2ban’s own logs to identify IPs that have been banned multiple times across days or weeks.&lt;/p&gt;&lt;h4&gt;Apply the Changes&lt;/h4&gt;&lt;p&gt;Once everything’s ready, restart Fail2ban:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo systemctl restart fail2ban sudo fail2ban-client status
&lt;/pre&gt;&lt;p&gt;You should now see your three new jails (&lt;code&gt;synflood&lt;/code&gt;, &lt;code&gt;connlimit&lt;/code&gt;, and &lt;code&gt;recidive&lt;/code&gt;) active and running.&lt;/p&gt;&lt;h2&gt;Why I Use &lt;code&gt;DROP&lt;/code&gt; (silent) instead of &lt;code&gt;REJECT&lt;/code&gt; / &lt;code&gt;TCP RST&lt;/code&gt;&lt;/h2&gt;&lt;p&gt;I decided early on that I wanted this setup to be &lt;strong&gt;quiet&lt;/strong&gt;. No messages, no hints, no &lt;em&gt;you’re blocked&lt;/em&gt; replies – just silence.&lt;/p&gt;&lt;p&gt;That’s why every rule in &lt;strong&gt;UFW+&lt;/strong&gt; uses &lt;code&gt;DROP&lt;/code&gt;, not &lt;code&gt;REJECT&lt;/code&gt; or &lt;code&gt;REJECT with tcp-reset&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;When you &lt;code&gt;DROP&lt;/code&gt; a packet, it just disappears. The sender gets no answer at all.&lt;/p&gt;&lt;p&gt;When you &lt;code&gt;REJECT&lt;/code&gt; it, you actually send something back – an error or reset – which tells whoever’s on the other side that you’re alive and blocking them.&lt;/p&gt;&lt;p&gt;For normal users, that kind of feedback is nice and clean. But for attackers, it’s valuable information.&lt;/p&gt;&lt;p&gt;A fast &lt;em&gt;connection refused&lt;/em&gt; or &lt;em&gt;port unreachable&lt;/em&gt; tells their scanner exactly what’s happening – and that’s the opposite of what I want.&lt;/p&gt;&lt;p&gt;With &lt;code&gt;DROP&lt;/code&gt;, everything goes dark. Scanners hang, tools time out, and automated floods get slower because they have no signal to adjust their timing or rate.&lt;/p&gt;&lt;p&gt;Sure, it’s not perfect – some legitimate tools might just sit there waiting until they time out – but for a public-facing web server, that tradeoff is totally worth it.&lt;/p&gt;&lt;p&gt;So, I use &lt;code&gt;DROP&lt;/code&gt; because I want the firewall to act like a black hole. If someone’s attacking my server, I don’t want them to know they hit a wall – I want them to wonder if they ever even reached me.&lt;/p&gt;&lt;h2&gt;How It All Flows Together&lt;/h2&gt;&lt;p&gt;Now that everything’s in place – the mangle table filters, Cloudflare whitelist, firewall rules, and Fail2ban jails – it’s worth seeing how the whole setup actually behaves.&lt;/p&gt;&lt;p&gt;Think of this as a quick &lt;em&gt;packet journey&lt;/em&gt; through &lt;strong&gt;UFW+&lt;/strong&gt;.&lt;/p&gt;&lt;h3&gt;Normal Visitor (Direct Access)&lt;/h3&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Mangle (&lt;code&gt;PREROUTING&lt;/code&gt;)&lt;/strong&gt; drops any junk or malformed packets.&lt;/li&gt;&lt;li&gt;Passes to &lt;strong&gt;Fail2ban&lt;/strong&gt;  (no ban, clean IP).&lt;/li&gt;&lt;li&gt;Reaches &lt;strong&gt;&lt;code&gt;CF-EDGE&lt;/code&gt;&lt;/strong&gt;  → returns normally.&lt;/li&gt;&lt;li&gt;Hits &lt;strong&gt;&lt;code&gt;CONNLIMIT&lt;/code&gt;&lt;/strong&gt;  → under 100 connections, returns.&lt;/li&gt;&lt;li&gt;Hits &lt;strong&gt;&lt;code&gt;SYN_FLOOD_PROTECTION&lt;/code&gt;&lt;/strong&gt;  → under 30 SYNs/sec, returns.&lt;/li&gt;&lt;li&gt;Finally, it reaches your web server – fast and clean.&lt;/li&gt;&lt;/ol&gt;&lt;h3&gt;Cloudflare Traffic&lt;/h3&gt;&lt;ol&gt;&lt;li&gt;Same initial mangle and Fail2ban checks.&lt;/li&gt;&lt;li&gt;Hits &lt;strong&gt;&lt;code&gt;CF-EDGE&lt;/code&gt;&lt;/strong&gt;  and gets accepted** immediately**.** It skips the &lt;strong&gt;&lt;code&gt;CONNLIMIT&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;SYN_FLOOD_PROTECTION&lt;/code&gt;&lt;/strong&gt; limit stages entirely (by design).&lt;/li&gt;&lt;/ol&gt;&lt;h3&gt;Slow Connection Hoarder&lt;/h3&gt;&lt;ol&gt;&lt;li&gt;Opens hundreds of idle sockets – say, 150 at once.&lt;/li&gt;&lt;li&gt;Passes the SYN rate limit (only 5 SYNs/sec).&lt;/li&gt;&lt;li&gt;Hits &lt;strong&gt;&lt;code&gt;CONNLIMIT&lt;/code&gt;&lt;/strong&gt;  → logs the event, drops anything over 100 connections. That IP starts failing silently after its 100th connection.&lt;/li&gt;&lt;li&gt;After repeated detections, &lt;strong&gt;Fail2ban&lt;/strong&gt;  steps in, adds the IP to the nftables ban set, and future packets die immediately.&lt;/li&gt;&lt;/ol&gt;&lt;h3&gt;SYN Flood (Fast Burst)&lt;/h3&gt;&lt;ol&gt;&lt;li&gt;Sends a wave – say 200 SYN packets per second.&lt;/li&gt;&lt;li&gt;Trips the &lt;strong&gt;&lt;code&gt;SYN_FLOOD_PROTECTION&lt;/code&gt;&lt;/strong&gt;  hashlimit.&lt;/li&gt;&lt;li&gt;Logs once per minute and drops the rest (silent).&lt;/li&gt;&lt;li&gt;After repeated detections, &lt;strong&gt;Fail2ban&lt;/strong&gt;  steps in, adds the IP to the nftables ban set, and future packets die immediately.&lt;/li&gt;&lt;/ol&gt;&lt;h2&gt;Kernel Hardening (Complementary to UFW+)&lt;/h2&gt;&lt;p&gt;Your firewall takes care of the traffic that tries to &lt;em&gt;reach&lt;/em&gt;  your server – but what about the packets that actually &lt;em&gt;make it through&lt;/em&gt;?&lt;/p&gt;&lt;p&gt;You can go one step further by &lt;strong&gt;hardening the kernel itself&lt;/strong&gt; , so that even if bad packets slip through, the server’s networking behavior remains safe and predictable.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;UFW+&lt;/strong&gt; filters the noise: it drops invalid, spoofed, or suspicious packets before they ever touch your services.&lt;/li&gt;&lt;li&gt;Kernel hardening (via &lt;code&gt;sysctl&lt;/code&gt;) tells the kernel how to behave – what to ignore, what to drop, and how to react when things look weird at the protocol level.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Together, they make a perfect pair:&lt;/strong&gt; your firewall cleans the input, and your kernel stays disciplined about what it accepts.&lt;/p&gt;&lt;h3&gt;Adding Kernel Hardening Settings&lt;/h3&gt;&lt;p&gt;UFW&apos;s &lt;code&gt;/etc/ufw/sysctl.conf&lt;/code&gt; already includes a few sensible defaults – for example, it disables &lt;strong&gt;ICMP redirects&lt;/strong&gt; and ignores &lt;strong&gt;bogus ICMP errors&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;You’ll simply &lt;strong&gt;add&lt;/strong&gt;  extra security parameters below them to enhance protection further.&lt;/p&gt;&lt;p&gt;Open the file:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo vim /etc/ufw/sysctl.conf
&lt;/pre&gt;&lt;p&gt;Append these settings at the end:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;# Enable SYN cookies (mitigates SYN floods at TCP level) net.ipv4.tcp_syncookies = 1 # Drop invalid packets at routing level net.ipv4.conf.all.rp_filter = 1 net.ipv4.conf.default.rp_filter = 1 # Don’t accept source-routed packets net.ipv4.conf.all.accept_source_route = 0 net.ipv6.conf.all.accept_source_route = 0 # Protect against TIME-WAIT assassination net.ipv4.tcp_rfc1337 = 1
&lt;/pre&gt;&lt;p&gt;Save and apply the changes:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw reload
&lt;/pre&gt;&lt;p&gt;That’s it.&lt;/p&gt;&lt;p&gt;UFW automatically loads its own sysctl file on startup, so these rules will take effect right away – no separate &lt;code&gt;sysctl -p&lt;/code&gt; needed.&lt;/p&gt;&lt;p&gt;Keeping your hardening settings here also keeps everything tidy – firewall logic and kernel behavior in one place, under UFW’s control.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Want to go deeper?&lt;/strong&gt; For a full breakdown of Linux kernel hardening – including detailed explanations and additional &lt;code&gt;sysctl&lt;/code&gt; options – check out my complete guide: &lt;a href=&quot;https://ivansalloum.com/kernel-hardening-securing-your-linux-server/&quot;&gt;Kernel Hardening: Securing Your Linux Server&lt;/a&gt;&lt;/p&gt;&lt;h2&gt;Testing and Verification&lt;/h2&gt;&lt;p&gt;I test &lt;strong&gt;UFW+&lt;/strong&gt; with two Hetzner VMs: one runs the UFW+ setup (the &lt;strong&gt;victim&lt;/strong&gt;) and the other is my &lt;strong&gt;attacker&lt;/strong&gt;  box with &lt;code&gt;apache-utils&lt;/code&gt; installed so I can use the &lt;code&gt;ab&lt;/code&gt; command.&lt;/p&gt;&lt;p&gt;Below I show the exact commands I ran, the logs I saw, and what each result means.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;New to Hetzner? &lt;a href=&quot;https://hetzner.cloud/?ref=MC4Yy318xX5X&quot;&gt;Use my link&lt;/a&gt; to get free credits!&lt;/p&gt;&lt;h3&gt;Quick SYN-Flood Smoke Test&lt;/h3&gt;&lt;p&gt;From the attacker I run this command, which opens a lot of new connections quickly (no keep-alive):&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ab -n 500 -c 20 http://victim-ipv4/
&lt;/pre&gt;&lt;p&gt;I watch the logs on the victim in real time:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo tail -f /var/log/syslog | grep -E &apos;UFW Connlimit|UFW SYN Flood Detected&apos;
&lt;/pre&gt;&lt;p&gt;What happened to me:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The &lt;code&gt;SYN_FLOOD_PROTECTION&lt;/code&gt; chain started logging &lt;code&gt;[UFW SYN Flood Detected]&lt;/code&gt; messages.&lt;/li&gt;&lt;li&gt;After three log entries inside five minutes, Fail2ban banned the attacker IP and added it to an nftables set.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I verified the ban like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo fail2ban-client status synflood sudo nft list table inet f2b-table
&lt;/pre&gt;&lt;p&gt;You should see the attacker’s IP in the &lt;code&gt;addr-set-synflood&lt;/code&gt; (or similar) set.&lt;/p&gt;&lt;p&gt;The reason this triggered is that &lt;code&gt;ab -n 500 -c 20&lt;/code&gt; (without &lt;code&gt;-k&lt;/code&gt;) opens one new TCP connection per request. On a fast link, that translated to thousands of SYNs/sec, which trips a 30 SYN/s hashlimit very quickly.&lt;/p&gt;&lt;h3&gt;The Keep-alive Difference (Important)&lt;/h3&gt;&lt;p&gt;I reran the same test but with keep-alive enabled:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ab -k -n 500 -c 20 http://victim-ipv4/
&lt;/pre&gt;&lt;p&gt;This test did not trigger blocking.&lt;/p&gt;&lt;p&gt;With &lt;code&gt;-k&lt;/code&gt;, &lt;code&gt;ab&lt;/code&gt; reuses the 20 connections (you’ll see &lt;code&gt;Keep-Alive requests: 500&lt;/code&gt; in the output), so only about 20 SYNs are sent rather than 500.&lt;/p&gt;&lt;p&gt;Browsers reuse connections the same way, so this mode is a better approximation of real-world traffic.&lt;/p&gt;&lt;h3&gt;Testing &lt;code&gt;CONNLIMIT&lt;/code&gt; (concurrent connections)&lt;/h3&gt;&lt;p&gt;To hit &lt;code&gt;CONNLIMIT&lt;/code&gt; you must keep many &lt;strong&gt;simultaneous&lt;/strong&gt;  connections open.&lt;/p&gt;&lt;p&gt;My first attempt was:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ab -k -n 2000 -c 101 http://victim-ipv4/
&lt;/pre&gt;&lt;p&gt;That didn’t hit &lt;code&gt;CONNLIMIT&lt;/code&gt; because &lt;code&gt;SYN_FLOOD_PROTECTION&lt;/code&gt; blocked the test first — opening 101 connections quickly trips the SYN limit rules.&lt;/p&gt;&lt;p&gt;To test &lt;code&gt;CONNLIMIT&lt;/code&gt; properly I temporarily bypassed &lt;code&gt;SYN_FLOOD_PROTECTION&lt;/code&gt; for my attacker IP and ran the keep-alive test again:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo iptables -I SYN_FLOOD_PROTECTION 1 -s attacker-ipv4 -p tcp -m multiport --dports 80,443 -j ACCEPT sudo ab -k -n 2000 -c 101 http://victim-ipv4/
&lt;/pre&gt;&lt;p&gt;With that bypass in place, &lt;code&gt;CONNLIMIT&lt;/code&gt; logged hits and began dropping connections above 100, which confirmed the connection-cap behavior works as designed.&lt;/p&gt;&lt;p&gt;At the end, I used the &lt;code&gt;sudo ufw reload&lt;/code&gt; command to remove the bypass.&lt;/p&gt;&lt;h3&gt;IPv4 vs IPv6 behavior&lt;/h3&gt;&lt;p&gt;Bans are per address family.&lt;/p&gt;&lt;p&gt;When the attacker IPv4 address was banned, the victim’s IPv6 address still responded normally.&lt;/p&gt;&lt;p&gt;For example, after banning IPv4 I got the default Nginx page when curling the victim’s IPv6:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;curl http://[victim-ipv6]/ # -&amp;gt; returns the Nginx index.html content
&lt;/pre&gt;&lt;p&gt;But curling the victim’s IPv4 hung with no response:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;curl http://victim-ipv4/ # -&amp;gt; hangs / no output
&lt;/pre&gt;&lt;p&gt;If your web server serves both families, you can test both.&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;If you’ve followed everything up to this point – congratulations.&lt;/p&gt;&lt;p&gt;You’ve built a layered firewall that’s far more capable than a stock UFW setup.&lt;/p&gt;&lt;p&gt;Your server now:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Filters malformed or spoofed packets right at the &lt;strong&gt;&lt;code&gt;PREROUTING&lt;/code&gt;&lt;/strong&gt; stage.&lt;/li&gt;&lt;li&gt;Whitelists &lt;strong&gt;Cloudflare&lt;/strong&gt;  edge IPs for clean traffic prioritization.&lt;/li&gt;&lt;li&gt;Enforces &lt;strong&gt;per-IP connection and SYN limits&lt;/strong&gt; that adapt to activity, not static thresholds.&lt;/li&gt;&lt;li&gt;Reacts to repeated abuse through &lt;strong&gt;Fail2ban&lt;/strong&gt; , which adds offenders to &lt;strong&gt;nftables sets&lt;/strong&gt; instantly.&lt;/li&gt;&lt;li&gt;Escalates bans for repeat offenders (short initial bans that grow with repeat offenses via the &lt;strong&gt;recidive&lt;/strong&gt;  logic).&lt;/li&gt;&lt;li&gt;Adds an extra layer of resilience with &lt;strong&gt;kernel hardening&lt;/strong&gt; , so even low-level packet tricks don’t slip through.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;All of these layers work together silently.&lt;/p&gt;&lt;p&gt;In testing, the difference is immediate. You can hammer the server with raw SYN floods, or try slow connection hoarding, and still see the web server stay responsive.&lt;/p&gt;&lt;p&gt;At the same time, real visitors and search bots browse normally without ever noticing what’s happening behind the scenes.&lt;/p&gt;&lt;p&gt;I’ll continue extending &lt;strong&gt;UFW+&lt;/strong&gt;  to cover more ports and add UDP/QUIC protections (port 443/UDP) in future versions.&lt;/p&gt;&lt;p&gt;For more comprehensive Linux server security resources, be sure to check out the full collection of detailed guides &lt;a href=&quot;https://ivansalloum.com/collections/linux-server-security/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Thanks for following along – and if you’ve built your own variation of this setup or tested it in your own environment, I’d love to hear how it performed.&lt;/p&gt;&lt;p&gt;If you run into any issues or need further help, feel free to revisit this guide or &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;reach out&lt;/a&gt; for assistance.&lt;/p&gt;&lt;p&gt;And if you found this useful, or have thoughts and feedback, drop them in the &lt;strong&gt;discussion section&lt;/strong&gt;  – I always enjoy seeing how others build on this.&lt;/p&gt;&lt;p&gt;Until then, keep your packets clean and your logs quiet. See you in the terminal.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Security</category></item><item><title>How to Use IP Pools for Better Deliverability in Postal</title><link>https://ivansalloum.com/how-to-use-ip-pools-in-postal/</link><guid isPermaLink="true">https://ivansalloum.com/how-to-use-ip-pools-in-postal/</guid><description>Learn how to configure IP pools in Postal using Floating IPs for better email deliverability, reputation management, and sending flexibility.</description><pubDate>Thu, 05 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;In my &lt;a href=&quot;https://ivansalloum.com/setting-up-postal-as-an-smtp-server/&quot;&gt;Postal SMTP server setup guide&lt;/a&gt;, we built a fully functioning &lt;strong&gt;send-only&lt;/strong&gt; mail server and successfully sent our first email using the server’s primary IP address.&lt;/p&gt;&lt;p&gt;Now it’s time to take things further.&lt;/p&gt;&lt;p&gt;In this guide, we’ll learn how to configure &lt;strong&gt;IP pools in Postal&lt;/strong&gt; to send emails from &lt;strong&gt;multiple IP addresses&lt;/strong&gt; instead of relying solely on your server’s main IP.&lt;/p&gt;&lt;p&gt;This setup helps improve deliverability, isolate sender reputation, and add flexibility – especially when working with multiple clients or high-volume sending.&lt;/p&gt;&lt;p&gt;To make this work, we’ll use &lt;strong&gt;Floating IPs&lt;/strong&gt; from Hetzner, which allow you to attach extra IP addresses to your server without losing them if the server is ever rebuilt.&lt;/p&gt;&lt;p&gt;❗&lt;/p&gt;&lt;p&gt;If you’re using a different server provider, make sure they &lt;strong&gt;support&lt;/strong&gt; similar functionality.&lt;/p&gt;&lt;p&gt;We’ll cover everything from assigning and configuring Floating IPs to setting up DNS records, building your IP pools in Postal, and fine-tuning priorities for smart sending behavior.&lt;/p&gt;&lt;p&gt;I’m In!&lt;/p&gt;&lt;h2&gt;Why Use IP Pools and Floating IPs?&lt;/h2&gt;&lt;p&gt;Using &lt;strong&gt;IP pools&lt;/strong&gt; in Postal along with &lt;strong&gt;Floating IPs&lt;/strong&gt; gives you greater control over how your emails are sent – and boosts &lt;strong&gt;deliverability&lt;/strong&gt; , &lt;strong&gt;flexibility&lt;/strong&gt; , and &lt;strong&gt;reputation management&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Here’s why IP pools and Floating IPs are worth setting up:&lt;/p&gt;&lt;p&gt;Combined, they let you build a more robust and professional email sending setup.&lt;/p&gt;&lt;h2&gt;Setting Up Floating IPs&lt;/h2&gt;&lt;p&gt;To send emails from multiple IP addresses in Postal, you’ll need to set up &lt;strong&gt;Floating IPs&lt;/strong&gt; on your server.&lt;/p&gt;&lt;p&gt;This example walks you through creating and configuring &lt;strong&gt;two Floating IP sets&lt;/strong&gt; (IPv4 + IPv6 pairs) using &lt;strong&gt;Hetzner&lt;/strong&gt; , though the process is similar with other providers that support floating or additional IPs.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;New to Hetzner? &lt;a href=&quot;https://hetzner.cloud/?ref=MC4Yy318xX5X&quot;&gt;Use my link&lt;/a&gt; to get free credits!&lt;/p&gt;&lt;h3&gt;Create Floating IPs in Hetzner&lt;/h3&gt;&lt;ol&gt;&lt;li&gt;In the Hetzner dashboard, go to &lt;strong&gt;Floating IPs&lt;/strong&gt; from the left menu&lt;/li&gt;&lt;li&gt;Click &lt;strong&gt;Add Floating IP&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;Choose the same &lt;strong&gt;location&lt;/strong&gt; as your &lt;a href=&quot;https://ivansalloum.com/setting-up-postal-as-an-smtp-server/&quot;&gt;Postal server&lt;/a&gt;&lt;/li&gt;&lt;li&gt;Add &lt;strong&gt;two IP pairs&lt;/strong&gt; :&lt;ol&gt;&lt;li&gt;Two &lt;strong&gt;IPv4 addresses&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;One &lt;strong&gt;IPv6 block&lt;/strong&gt; (&lt;code&gt;/64&lt;/code&gt;) – you’ll create multiple usable IPv6 addresses from it (e.g. &lt;code&gt;::1&lt;/code&gt;, &lt;code&gt;::2&lt;/code&gt;)&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;li&gt;Give each IP set a clear name to stay organized&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;You can create as many floating IPs as needed and group them however you like. Just make sure to track which IPv4 is paired with which IPv6.&lt;/p&gt;&lt;h3&gt;Assign Floating IPs to Your Server&lt;/h3&gt;&lt;p&gt;For each IP address:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Click the &lt;strong&gt;three-dot menu&lt;/strong&gt; next to the IP&lt;/li&gt;&lt;li&gt;Choose &lt;strong&gt;Assign&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;Select the server where Postal is installed&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;❗&lt;/p&gt;&lt;p&gt;This links the IPs to your server at the network level, but they’re not active yet.&lt;/p&gt;&lt;h3&gt;Configure the IPs on Your Server&lt;/h3&gt;&lt;p&gt;Although the IPs are now assigned, your server still doesn&apos;t know how to use them.&lt;/p&gt;&lt;p&gt;You need to manually configure the IPs in your network settings to make them active and routable.&lt;/p&gt;&lt;p&gt;To do this, access the server and create a configuration file inside the &lt;code&gt;/etc/netplan/&lt;/code&gt; directory. You can do this by running:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo vim /etc/netplan/60-floating-ips.yaml
&lt;/pre&gt;&lt;p&gt;Then add your IPs. Replace the addresses below with your actual ones:&lt;/p&gt;&lt;pre data-language=&quot;yaml&quot;&gt;network: version: 2 renderer: networkd ethernets: eth0: addresses: \- 89.107.50.13/32 # Floating IPv4 - ip-set-1 \- 2a04:2f8:1c0c:a0a2::1/64 # Floating IPv6 - ip-set-1 \- 90.92.38.139/32 # Floating IPv4 - ip-set-2 \- 2a04:2f8:1c0c:a0a2::2/64 # Floating IPv6 - ip-set-2
&lt;/pre&gt;&lt;p&gt;Use &lt;code&gt;/32&lt;/code&gt; for IPv4 and &lt;code&gt;/64&lt;/code&gt; for IPv6.&lt;/p&gt;&lt;p&gt;You can create multiple IPv6 addresses from your block by simply appending different values (like &lt;code&gt;::3&lt;/code&gt;, &lt;code&gt;::4&lt;/code&gt;, etc.).&lt;/p&gt;&lt;h3&gt;Apply and Verify&lt;/h3&gt;&lt;p&gt;Set the proper permissions to avoid receiving a warning:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo chmod 600 /etc/netplan/60-floating-ips.yaml
&lt;/pre&gt;&lt;p&gt;Apply the configuration:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo netplan apply
&lt;/pre&gt;&lt;p&gt;Check that your new IPs are active:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ip addr show
&lt;/pre&gt;&lt;p&gt;You should see all assigned IPs listed under your network interface (&lt;code&gt;eth0&lt;/code&gt;).&lt;/p&gt;&lt;p&gt;🧠&lt;/p&gt;&lt;p&gt;You can repeat this process to add more floating IPs in the future. Just extend the same Netplan file (or use separate ones) and follow the same pattern.&lt;/p&gt;&lt;h2&gt;Creating an IP Pool&lt;/h2&gt;&lt;p&gt;Before you can use multiple IPs for sending, you need to enable IP pool support in Postal.&lt;/p&gt;&lt;p&gt;Open the Postal configuration file:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo vim /opt/postal/config/postal.yml
&lt;/pre&gt;&lt;p&gt;Under the &lt;code&gt;postal:&lt;/code&gt; section, add the following line:&lt;/p&gt;&lt;pre data-language=&quot;yaml&quot;&gt;postal: use_ip_pools: true
&lt;/pre&gt;&lt;p&gt;Save and close the file, then restart Postal:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo postal restart
&lt;/pre&gt;&lt;p&gt;Once Postal is back up, head to the web interface:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;From the top menu, click on &lt;strong&gt;IP Pools&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;Click &lt;strong&gt;Create the first IP pool&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;Give your pool a descriptive name (e.g. dedi, client-a, transactional) and click &lt;strong&gt;Create IP pool&lt;/strong&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;You’ll now see an empty table where you can start adding IP addresses to this pool.&lt;/p&gt;&lt;h3&gt;Adding Two IP Pairs to the Pool&lt;/h3&gt;&lt;p&gt;Now that you&apos;ve configured two Floating IP pairs on your server, it’s time to add them to your IP pool.&lt;/p&gt;&lt;p&gt;If you name your pool &lt;code&gt;dedi&lt;/code&gt;** like me, here’s how you can add each IP pair to it:&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;strong&gt;IP Pair&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;IPv4&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;IPv6&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Hostname&lt;/strong&gt;&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;First IP Pair&lt;/td&gt;&lt;td&gt;&lt;code&gt;89.107.50.13&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;2a04:2f8:1c0c:a0a2::1&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;dedi-1.example.com&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Second IP Pair&lt;/td&gt;&lt;td&gt;&lt;code&gt;90.92.38.139&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;2a04:2f8:1c0c:a0a2::2&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;dedi-2.example.com&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;To add them:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Click &lt;strong&gt;Add an IP address to pool&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;Enter the IPv4, IPv6, and Hostname for the first pair&lt;/li&gt;&lt;li&gt;Enter a priority value&lt;/li&gt;&lt;li&gt;Click &lt;strong&gt;Create IP address&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;Repeat for the second pair&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;If you want to use just &lt;strong&gt;one specific Floating IP set&lt;/strong&gt;  for sending, make sure only that set is in the IP pool. If you add multiple IP sets to the same pool, Postal will use them all – based on their &lt;strong&gt;priority values&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Each IP set in a pool can have a &lt;strong&gt;priority&lt;/strong&gt;  between 1 and 100. This priority determines the likelihood that a specific IP will be chosen when sending an email. The higher the number, the more likely it is to be used.&lt;/p&gt;&lt;p&gt;By default, the priority is set to 100.&lt;/p&gt;&lt;p&gt;✅&lt;/p&gt;&lt;p&gt;Once your pool is set up and IPs are added, you&apos;re ready to move on to DNS setup.&lt;/p&gt;&lt;h2&gt;DNS Setup&lt;/h2&gt;&lt;p&gt;Now that you&apos;ve added your IP pairs to the pool, it&apos;s time to configure the DNS records for each one.&lt;/p&gt;&lt;p&gt;This step is crucial – without proper DNS records, your emails might never make it to inboxes or could be flagged as spam.&lt;/p&gt;&lt;h3&gt;A and AAAA Records&lt;/h3&gt;&lt;p&gt;For each IP pair, you’ll need to create both A and AAAA records that point to the respective IPv4 and IPv6 addresses.&lt;/p&gt;&lt;p&gt;Here’s how you would configure them based on the example IPs and hostnames:&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;strong&gt;Record Type&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Host&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Value (IP Address)&lt;/strong&gt;&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;A&lt;/td&gt;&lt;td&gt;&lt;code&gt;dedi-1.example.com&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;89.107.50.13&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;AAAA&lt;/td&gt;&lt;td&gt;&lt;code&gt;dedi-1.example.com&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;2a04:2f8:1c0c:a0a2::1&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;A&lt;/td&gt;&lt;td&gt;&lt;code&gt;dedi-2.example.com&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;90.92.38.139&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;AAAA&lt;/td&gt;&lt;td&gt;&lt;code&gt;dedi-2.example.com&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;2a04:2f8:1c0c:a0a2::1&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;Each hostname must resolve to the exact IPs you&apos;ve assigned in the pool.&lt;/p&gt;&lt;h3&gt;PTR (Reverse DNS)&lt;/h3&gt;&lt;p&gt;Once you&apos;ve configured your A and AAAA records, the next step is to set the &lt;strong&gt;PTR (Reverse DNS)&lt;/strong&gt; records for your Floating IPs.&lt;/p&gt;&lt;p&gt;This is important because many mail servers check if your IP address points back to the same domain name you&apos;re sending from. If it doesn&apos;t match, your email might get flagged as spam or blocked entirely.&lt;/p&gt;&lt;p&gt;Here’s how to do it in Hetzner:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Log in&lt;/strong&gt; to the &lt;a href=&quot;https://console.hetzner.cloud/projects&quot;&gt;Hetzner Cloud Console&lt;/a&gt; and navigate to your project.&lt;/li&gt;&lt;li&gt;From the left-hand menu, click &lt;strong&gt;Floating IPs&lt;/strong&gt;.&lt;/li&gt;&lt;li&gt;You’ll see your assigned IPv4 and IPv6 addresses listed.&lt;/li&gt;&lt;li&gt;For each IP:&lt;ol&gt;&lt;li&gt;Click the &lt;strong&gt;three-dot menu&lt;/strong&gt; on the right.&lt;/li&gt;&lt;li&gt;Choose &lt;strong&gt;Edit Reverse DNS&lt;/strong&gt;.&lt;/li&gt;&lt;li&gt;Enter the matching hostname for that IP.&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Make sure your &lt;strong&gt;PTR record matches the A/AAAA record exactly&lt;/strong&gt;. This reverse DNS match is a key requirement for being accepted by many recipient mail servers.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;For &lt;strong&gt;IPv6 addresses&lt;/strong&gt; , append a &lt;code&gt;1&lt;/code&gt;, &lt;code&gt;2&lt;/code&gt;, etc., after the &lt;code&gt;::&lt;/code&gt; (based on the IP pair).&lt;/p&gt;&lt;h3&gt;SPF Record&lt;/h3&gt;&lt;p&gt;To improve your email deliverability and avoid getting flagged as spam, you need to configure &lt;strong&gt;SPF (Sender Policy Framework)&lt;/strong&gt; records in your DNS.&lt;/p&gt;&lt;p&gt;SPF helps receiving mail servers verify that your server is authorized to send emails for your domain.&lt;/p&gt;&lt;p&gt;If you followed my &lt;a href=&quot;https://ivansalloum.com/setting-up-postal-as-an-smtp-server/&quot;&gt;Postal setup guide&lt;/a&gt;, you should already have a global SPF record for your server’s &lt;strong&gt;primary IPs&lt;/strong&gt; that looks something like this:&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;strong&gt;Record Type&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Host&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Value&lt;/strong&gt;&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;TXT&lt;/td&gt;&lt;td&gt;&lt;code&gt;spf.mail.example.com&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;&amp;quot;v=spf1 ip4:70.130.219.212 ip6:643c:ac1f:3f7c:bb5c::1 ~all&amp;quot;&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;Now that we’ve created a dedicated IP pool using Floating IPs, we’ll need a separate SPF record for them:&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;strong&gt;Record Type&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Host&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Value&lt;/strong&gt;&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;TXT&lt;/td&gt;&lt;td&gt;&lt;code&gt;spf.dedi.example.com&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;&amp;quot;v=spf1 ip4:89.107.50.13 ip6:2a04:2f8:1c0c:a0a2::1 ip4:90.92.38.139 ip6:2a04:2f8:1c0c:a0a2::2 ~all&amp;quot;&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;Finally, &lt;strong&gt;update your global SPF record&lt;/strong&gt; to include the new one:&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;strong&gt;Record Type&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Host&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Value&lt;/strong&gt;&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;TXT&lt;/td&gt;&lt;td&gt;&lt;code&gt;spf.mail.example.com&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;&amp;quot;v=spf1 ip4:70.130.219.212 ip6:643c:ac1f:3f7c:bb5c::1 include:spf.dedi.example.com ~all&amp;quot;&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;This setup ensures that all IPs in your &lt;code&gt;dedi&lt;/code&gt; pool are authorized to send emails for any domains you add in Postal.&lt;/p&gt;&lt;h2&gt;Why SPF Can’t Fully Isolate IP Pools&lt;/h2&gt;&lt;p&gt;If you include both your &lt;code&gt;dedi&lt;/code&gt; pool IPs and your primary server IPs in the same SPF, receiving mail servers will treat all of those IPs as valid senders for &lt;code&gt;@example.com&lt;/code&gt; – even if, at the Postal level, you’ve told Postal to use only the &lt;code&gt;dedi&lt;/code&gt; pool.&lt;/p&gt;&lt;p&gt;In other words, including both in one SPF means you cannot restrict the domain to just the &lt;code&gt;dedi&lt;/code&gt; pool.&lt;/p&gt;&lt;p&gt;This means:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;No True Isolation:&lt;/strong&gt; Even if you plan to send some mail only from the &lt;code&gt;dedi&lt;/code&gt; pool, the SPF record still authorizes your primary server IPs (and later, any other pools you include). As a result, you cannot stop a hard-coded domain from accidentally using the &lt;strong&gt;wrong&lt;/strong&gt; IP if Postal ever sends from it.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;The Return-Path Problem:&lt;/strong&gt; Postal always uses a single &lt;strong&gt;return-path&lt;/strong&gt; (MAIL FROM) hostname – something like &lt;code&gt;rp.mail.example.com&lt;/code&gt; – for every email it sends. The receiving server checks SPF against that return-path hostname. If that hostname’s SPF record includes both your primary and &lt;code&gt;dedi&lt;/code&gt; IPs, there’s no way to tell Postal &amp;quot;when sending from the dedi pool, use a different return-path&amp;quot;. Postal simply doesn’t let you override the return-path per domain or per pool. As a result, even if you add a separate &lt;code&gt;spf.dedi.example.com&lt;/code&gt; record, Postal will still send everything as &lt;code&gt;@rp.mail.example.com&lt;/code&gt;, so the only SPF that actually &lt;strong&gt;matters&lt;/strong&gt; is the one on &lt;code&gt;rp.mail.example.com&lt;/code&gt;, and it will include all your IPs.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;You have two choices:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Live With It:&lt;/strong&gt; Accept that your SPF lists both (or all) of your IPs under the single return-path hostname. Any sending domain pointing at that SPF can egress on any of them. You get simplicity and fewer records, but you lose the guarantee that &lt;strong&gt;Domain A&lt;/strong&gt; will only ever use the &lt;code&gt;dedi&lt;/code&gt; pool.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Run One Postal Instance Per Pool:&lt;/strong&gt; If you truly need &lt;strong&gt;Domain A&lt;/strong&gt; to use only the &lt;code&gt;dedi&lt;/code&gt; IPs and &lt;strong&gt;Domain B&lt;/strong&gt; to use another pool IPs, you must run two separate Postal servers – each with its own hostname and its own &lt;code&gt;rp.*&lt;/code&gt; return-path. That way, Postal’s built-in return-path SPF is tied to just one pool, and you regain full separation at the cost of extra infrastructure.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;If you’re not running Postal in a mission-critical environment where strict IP separation is absolutely required, it’s perfectly acceptable to &lt;strong&gt;live with&lt;/strong&gt; a combined SPF record.&lt;/p&gt;&lt;p&gt;In practice, the pool-level routing that Postal provides has been reliable, and I’ve never experienced &lt;strong&gt;misrouted or mis-authorized mail&lt;/strong&gt; because of this setup.&lt;/p&gt;&lt;p&gt;If you ever find you need airtight isolation, you can think about the two-instance approach – but for most use cases, trusting Postal’s pool assignment and a single SPF record is simple and works just fine.&lt;/p&gt;&lt;h2&gt;Put Your IP Pool Into Action&lt;/h2&gt;&lt;p&gt;Now that your IP pool is set up and your DNS is configured, it’s time to put it to use.&lt;/p&gt;&lt;p&gt;Go back to your &lt;strong&gt;organization&lt;/strong&gt; in Postal, and you’ll see a new tab called &lt;strong&gt;IPs&lt;/strong&gt;. Open it and select the IP pool you just created (e.g. &lt;code&gt;dedi&lt;/code&gt;). This enables your organization to use that pool for sending across &lt;strong&gt;all mail servers&lt;/strong&gt; associated with it.&lt;/p&gt;&lt;p&gt;If you assign &lt;strong&gt;multiple IP pools&lt;/strong&gt; , you can choose which one to use for each mail server from the &lt;strong&gt;Server Settings&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;Once you assign an IP pool to an organization, any new mail server you create will &lt;strong&gt;no longer use your server’s primary IPs&lt;/strong&gt;. All sending will happen through the IPs in the assigned pool.&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;That’s it – you’ve successfully set up and configured IP pools in Postal using Floating IPs!&lt;/p&gt;&lt;p&gt;This setup gives you more control over your sending infrastructure, helps protect your IP reputation, and improves deliverability by isolating different types of email traffic.&lt;/p&gt;&lt;p&gt;If you ever need to add more IPs or create additional IP pools (for example, for new clients or projects), just repeat the same process:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Add new Floating IPs&lt;/li&gt;&lt;li&gt;Configure them on your server&lt;/li&gt;&lt;li&gt;Update DNS records (A, AAAA, PTR, SPF)&lt;/li&gt;&lt;li&gt;Add them to an IP pool in Postal&lt;/li&gt;&lt;li&gt;Assign the pool to the appropriate organization&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;If you run into any issues or need further help, feel free to revisit this guide or &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;reach out&lt;/a&gt; for assistance.&lt;/p&gt;&lt;p&gt;If you found value in this guide or have any questions or feedback, please don&apos;t hesitate to share your thoughts in the &lt;strong&gt;discussion&lt;/strong&gt; section.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Servers</category></item><item><title>Installing Docker and Docker Compose on Ubuntu (Using Docker’s Official Repository)</title><link>https://ivansalloum.com/installing-docker-and-docker-compose-on-ubuntu/</link><guid isPermaLink="true">https://ivansalloum.com/installing-docker-and-docker-compose-on-ubuntu/</guid><description>Remove old Docker packages and install Docker Engine and the Compose plugin on Ubuntu Server using Docker’s official repository.</description><pubDate>Thu, 01 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;When I first started with Docker, I used to just run &lt;code&gt;apt install docker.io&lt;/code&gt; and call it a day. It worked – but not always well.&lt;/p&gt;&lt;p&gt;The Docker and Compose versions from Ubuntu&apos;s default repositories were &lt;strong&gt;often outdated&lt;/strong&gt; , and I quickly ran into missing features and annoying compatibility issues.&lt;/p&gt;&lt;p&gt;One example: the newer Docker Compose v2 plugin (which integrates with the &lt;code&gt;docker&lt;/code&gt; command) wasn’t available at all – Ubuntu’s repo still had the old, now-deprecated &lt;code&gt;docker-compose&lt;/code&gt; v1 binary.&lt;/p&gt;&lt;p&gt;After a bit of digging, I found the better way: installing &lt;strong&gt;Docker Engine&lt;/strong&gt; and the &lt;strong&gt;Compose plugin&lt;/strong&gt; straight from Docker’s official repository. It’s what the Docker team recommends, and it ensures you&apos;re always running the latest stable versions.&lt;/p&gt;&lt;p&gt;In this guide, I’ll walk you through the steps I now use – a clean setup that keeps things up-to-date and future-proof.&lt;/p&gt;&lt;p&gt;I’m In!&lt;/p&gt;&lt;h2&gt;Uninstall Old Packages&lt;/h2&gt;&lt;p&gt;Before installing Docker from the official repository, it’s a good idea to &lt;strong&gt;remove any old or conflicting packages&lt;/strong&gt;  that might be lingering from a previous setup.&lt;/p&gt;&lt;p&gt;You can uninstall them all in one go with this loop:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt remove -y $pkg done
&lt;/pre&gt;&lt;p&gt;This command iterates through common legacy Docker and container packages – such as &lt;code&gt;docker.io&lt;/code&gt;, &lt;code&gt;docker-compose&lt;/code&gt;, &lt;code&gt;containerd&lt;/code&gt;, and more – and uninstalls each one.&lt;/p&gt;&lt;p&gt;🙆‍♂️&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Don’t worry:&lt;/strong&gt; This &lt;strong&gt;won’t delete your Docker images or containers&lt;/strong&gt;. It just clears out the old binaries so you&apos;re starting fresh.&lt;/p&gt;&lt;p&gt;Once that’s done, clean up any leftover dependencies:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt autoremove -y
&lt;/pre&gt;&lt;p&gt;This step ensures any libraries or packages that were only needed by the old Docker binaries are removed, leaving you with a clean slate for the new installation.&lt;/p&gt;&lt;h2&gt;Install Docker from Official Docker Repository&lt;/h2&gt;&lt;p&gt;Now it’s time to install &lt;strong&gt;Docker Engine&lt;/strong&gt;  and the &lt;strong&gt;Docker Compose plugin&lt;/strong&gt;  using Docker’s official APT repository – not the outdated packages from Ubuntu’s default sources.&lt;/p&gt;&lt;p&gt;This ensures you get the latest version of &lt;strong&gt;Docker CE (Community Edition)&lt;/strong&gt;  along with the &lt;strong&gt;integrated Compose v2 plugin&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Run the following commands to add Docker’s GPG key and repository:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;# Update your package list and install tools needed for the setup sudo apt update sudo apt install -y ca-certificates curl gnupg # Create a keyring directory (best practice for modern APT key management) sudo install -m 0755 -d /etc/apt/keyrings # Download Docker’s official GPG key curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ | sudo tee /etc/apt/keyrings/docker.asc &amp;gt; /dev/null # Set correct permissions sudo chmod a+r /etc/apt/keyrings/docker.asc # Add the Docker repository to APT sources echo \ &amp;quot;deb [arch=$(dpkg --print-architecture) \ signed-by=/etc/apt/keyrings/docker.asc] \ https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable&amp;quot; \ | sudo tee /etc/apt/sources.list.d/docker.list &amp;gt; /dev/null # Refresh package list again sudo apt update
&lt;/pre&gt;&lt;p&gt;Let’s break down what this does:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Installs required packages like &lt;code&gt;curl&lt;/code&gt; and &lt;code&gt;gnupg&lt;/code&gt; (if not already present) for handling HTTPS and GPG keys.&lt;/li&gt;&lt;li&gt;Downloads Docker’s official GPG key and saves it in &lt;code&gt;/etc/apt/keyrings/docker.asc&lt;/code&gt; (a secure location for APT keys).&lt;/li&gt;&lt;li&gt;Adds Docker’s APT repository to your server’s software sources (pointing to your Ubuntu release codename).&lt;/li&gt;&lt;li&gt;Updates the package list to include Docker’s repository.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Once the repo is set up, install Docker and its core components:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt install -y docker-ce docker-ce-cli containerd.io \ docker-buildx-plugin docker-compose-plugin
&lt;/pre&gt;&lt;p&gt;This installs the Docker daemon (&lt;code&gt;docker-ce&lt;/code&gt;) along with:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;docker-ce-cli&lt;/code&gt;:&lt;/strong&gt; The command-line interface for Docker.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;containerd.io&lt;/code&gt;:&lt;/strong&gt; The container runtime Docker uses under the hood.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;docker-buildx-plugin&lt;/code&gt;:&lt;/strong&gt; Docker’s Buildx plugin (for extended build capabilities).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;docker-compose-plugin&lt;/code&gt;:&lt;/strong&gt; The Docker Compose v2 plugin, which adds the &lt;code&gt;docker compose&lt;/code&gt; subcommand.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;✅&lt;/p&gt;&lt;p&gt;With this, &lt;strong&gt;Docker Engine&lt;/strong&gt; is installed as a service on your server.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;Sometimes Docker doesn’t start automatically after installation. To ensure it’s running, start the service and check its status:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo systemctl start docker.service sudo systemctl status docker.service
&lt;/pre&gt;&lt;p&gt;If the service is listed as &lt;code&gt;active (running)&lt;/code&gt;, Docker is ready to use – otherwise, you can inspect its logs with &lt;code&gt;sudo journalctl -u docker.service --no-pager&lt;/code&gt; to diagnose any startup issues.&lt;/p&gt;&lt;h2&gt;Run Docker Hello-World&lt;/h2&gt;&lt;p&gt;To verify that Docker Engine is installed and working properly, run the classic &lt;code&gt;hello-world&lt;/code&gt; container:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo docker run hello-world
&lt;/pre&gt;&lt;p&gt;This command downloads a test image and runs it in a container. When the container runs, it prints a &lt;code&gt;Hello from Docker!&lt;/code&gt; confirmation message and then exits.&lt;/p&gt;&lt;p&gt;If you see this message, congrats – your Docker installation is successful.&lt;/p&gt;&lt;h2&gt;Manage Docker as a non-root user&lt;/h2&gt;&lt;p&gt;By default, you need to use &lt;code&gt;sudo&lt;/code&gt; for all Docker commands. If you’d prefer to run Docker as your normal user (without &lt;code&gt;sudo&lt;/code&gt;):&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo usermod -aG docker $USER
&lt;/pre&gt;&lt;p&gt;After running this, &lt;strong&gt;log out and SSH back in&lt;/strong&gt;  (or reboot) for the group change to take effect.&lt;/p&gt;&lt;p&gt;Once you’ve &lt;strong&gt;SSHed in&lt;/strong&gt; again, test it by running &lt;code&gt;docker info&lt;/code&gt; or &lt;code&gt;docker run hello-world&lt;/code&gt; again – this time &lt;strong&gt;without&lt;/strong&gt; the &lt;code&gt;sudo&lt;/code&gt; prefix.&lt;/p&gt;&lt;h2&gt;Verify Docker Compose Plugin&lt;/h2&gt;&lt;p&gt;Now that Docker and the Compose plugin are installed, let’s confirm that &lt;strong&gt;Docker Compose v2&lt;/strong&gt;  is working properly.&lt;/p&gt;&lt;p&gt;Run the following command to see if Docker Compose is recognized:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo docker compose version
&lt;/pre&gt;&lt;p&gt;If everything was set up correctly, this will display the Compose plugin’s version, for example:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;Docker Compose version v2.x.x
&lt;/pre&gt;&lt;p&gt;The important part is that you see a version starting with &lt;code&gt;v2&lt;/code&gt;, confirming that you have Docker Compose V2 installed as a plugin.&lt;/p&gt;&lt;p&gt;If you see an error:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Double-check that you installed the &lt;code&gt;docker-compose-plugin&lt;/code&gt; package.&lt;/li&gt;&lt;li&gt;Make sure you&apos;re running the command as &lt;code&gt;sudo&lt;/code&gt; or that your user is part of the &lt;code&gt;docker&lt;/code&gt; group.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Now, rerun the previous command and verify whether Docker Compose is recognized.&lt;/p&gt;&lt;h3&gt;Run a Quick Docker Compose Test&lt;/h3&gt;&lt;p&gt;Let’s test Docker Compose by launching a simple container.&lt;/p&gt;&lt;p&gt;Create a test directory:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;mkdir docker-compose-test &amp;amp;&amp;amp; cd docker-compose-test
&lt;/pre&gt;&lt;p&gt;Create a &lt;code&gt;docker-compose.yml&lt;/code&gt; file with the following content:&lt;/p&gt;&lt;pre data-language=&quot;yaml&quot;&gt;services: hello: image: hello-world
&lt;/pre&gt;&lt;p&gt;This defines a Compose app with one service named &lt;code&gt;hello&lt;/code&gt;, using Docker’s tiny &lt;code&gt;hello-world&lt;/code&gt; image.&lt;/p&gt;&lt;p&gt;Run the application:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo docker compose up
&lt;/pre&gt;&lt;p&gt;Docker (via the Compose plugin) will pull the &lt;code&gt;hello-world&lt;/code&gt; image (if not already pulled) and start the container.&lt;/p&gt;&lt;p&gt;You should see output indicating that Docker is creating and running the container, and then the &lt;code&gt;Hello from Docker!&lt;/code&gt;** message from the &lt;code&gt;hello-world&lt;/code&gt; container.&lt;/p&gt;&lt;p&gt;Compose will manage the container’s lifecycle. Since &lt;code&gt;hello-world&lt;/code&gt; exits after printing its message, you’ll likely see Compose stop the container once it’s done.&lt;/p&gt;&lt;p&gt;For example, the output may look like:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;[+] Running 2/2 ✔ Network test_default Created 0.1s ✔ Container test-hello-1 Created 0.1s Attaching to hello-1 hello-1 | hello-1 | Hello from Docker! hello-1 | This message shows that your installation appears to be working correctly. ... hello-1 exited with code 0
&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Success!&lt;/strong&gt; This confirms that &lt;code&gt;docker compose&lt;/code&gt; is working, and the Compose plugin is running correctly.&lt;/p&gt;&lt;p&gt;In a real-world app, your containers would typically stay running – like a web service using NGINX or a database – but for testing, &lt;code&gt;hello-world&lt;/code&gt; is a simple and safe way to verify the setup.&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;By installing Docker from &lt;strong&gt;Docker’s official APT repository&lt;/strong&gt; , you’ve set yourself up with:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The &lt;strong&gt;latest Docker Engine&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;The &lt;strong&gt;official Docker Compose v2 plugin&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;A setup that follows &lt;strong&gt;current best practices&lt;/strong&gt;  for Ubuntu servers&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This approach not only gives you access to the newest features (like &lt;code&gt;docker compose&lt;/code&gt; instead of the old &lt;code&gt;docker-compose&lt;/code&gt; binary), but also ensures smoother upgrades moving forward.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;💬  Found this guide helpful?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;I’d love to hear your thoughts, questions, or suggestions in the discussion section below. Your feedback helps improve future guides and supports others on their server journey.&lt;/p&gt;&lt;p&gt;Prefer a more direct conversation? Feel free to &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;contact me&lt;/a&gt; anytime.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Servers</category></item><item><title>Self-Host Private and Lightweight Analytics with Umami</title><link>https://ivansalloum.com/self-host-private-and-lightweight-analytics-with-umami/</link><guid isPermaLink="true">https://ivansalloum.com/self-host-private-and-lightweight-analytics-with-umami/</guid><description>A step-by-step guide to self-hosting Umami – a lightweight, privacy-friendly analytics tool with no cookies or third-party tracking.</description><pubDate>Thu, 17 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;I&apos;ve always looked for a web analytics solution that respects user privacy and doesn’t slow down my website. The default option for most people is Google Analytics – it’s popular, but in my opinion, it’s bloated and far from privacy-friendly.&lt;/p&gt;&lt;p&gt;Many analytics platforms aren’t GDPR-compliant and often require cookie consent banners, which can hurt the user experience. On top of that, relying on third-party services can negatively affect your site’s performance – just like Google Analytics does.&lt;/p&gt;&lt;p&gt;So why do we keep using third-party tools to collect visitor data when we can host our own analytics and truly own our data?&lt;/p&gt;&lt;p&gt;Sure, some platforms offer deep insights, but unless your business relies heavily on those insights, all that detail isn’t always necessary.&lt;/p&gt;&lt;p&gt;In this guide, I’ll show you how to self-host &lt;a href=&quot;https://umami.is/&quot;&gt;&lt;strong&gt;Umami&lt;/strong&gt;&lt;/a&gt; – an open-source analytics tool that’s fast, privacy-friendly, and easy to host.&lt;/p&gt;&lt;h2&gt;Umami No Longer Supports MySQL&lt;/h2&gt;&lt;p&gt;As of &lt;strong&gt;Umami v3&lt;/strong&gt; , &lt;strong&gt;MySQL is no longer supported&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;In previous versions of this guide, I used &lt;strong&gt;MySQL&lt;/strong&gt;  as the database backend.&lt;/p&gt;&lt;p&gt;If you installed Umami following those steps, don’t worry – you can migrate your existing data to &lt;strong&gt;PostgreSQL&lt;/strong&gt; , which is now the only supported database engine.&lt;/p&gt;&lt;p&gt;👇&lt;/p&gt;&lt;p&gt;Follow the new &lt;strong&gt;“Migrating from MySQL to PostgreSQL&amp;quot;&lt;/strong&gt; section to safely switch over before updating to the latest Umami version.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;If you’re new to Umami&lt;/strong&gt; , simply follow this guide from the beginning – it now covers installation &lt;strong&gt;with PostgreSQL&lt;/strong&gt;  (the only supported database going forward).&lt;/p&gt;&lt;h2&gt;Why Choose Umami?&lt;/h2&gt;&lt;p&gt;After using Google Analytics for a couple of months to track my website’s traffic, I just stopped. I said to myself, &lt;em&gt;nah, there has to be a better solution.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;It made my website feel heavy and sluggish. I had to add a cookie banner just to stay compliant, and honestly, that hurt the user experience. People would land on my site, see the banner, and bounce – probably never to return.&lt;/p&gt;&lt;p&gt;I’ve always wanted to build a site that feels fast, clean, and respectful. So why should I ruin the experience just to collect some traffic stats?&lt;/p&gt;&lt;p&gt;Eventually, I came across &lt;a href=&quot;https://pirsch.io/&quot;&gt;&lt;strong&gt;Pirsch.io&lt;/strong&gt;&lt;/a&gt; – a solid, privacy-friendly tool that doesn’t use cookies and doesn’t require consent banners. I used it for a while and liked it.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;But if you know me…&lt;/strong&gt; I’m the self-host guy. I wanted full control – &lt;em&gt;my server, my data, my rules.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;That’s when I found &lt;strong&gt;Umami&lt;/strong&gt;. It anonymizes visitor data to protect privacy, doesn’t use cookies (so no annoying cookie banners), and since it’s self-hosted on your own infrastructure, &lt;em&gt;your data stays entirely in your hands.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;The best part?&lt;/strong&gt; Its tracking script is only &lt;strong&gt;2KB&lt;/strong&gt; in size – practically nothing! Google Analytics used to slow down my site’s page load time by around 500ms.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;On top of that&lt;/strong&gt; , it has a fantastic UI – easy to navigate, simple to understand, and quick to find exactly what you need. It also comes packed with features like reporting, comparison tools, filtering, custom events, team collaboration, and much more.&lt;/p&gt;&lt;h2&gt;Requirements Before You Start&lt;/h2&gt;&lt;p&gt;To follow this guide, you’ll need a server running &lt;strong&gt;Ubuntu 24.04 LTS&lt;/strong&gt; , &lt;a href=&quot;https://ivansalloum.com/preparing-your-ubuntu-server-for-first-use/&quot;&gt;prepared&lt;/a&gt; for installing &lt;strong&gt;Umami&lt;/strong&gt;. I personally recommend using &lt;strong&gt;Hetzner&lt;/strong&gt;  – it&apos;s reliable and affordable.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;New to Hetzner? &lt;a href=&quot;https://hetzner.cloud/?ref=MC4Yy318xX5X&quot;&gt;Use my link&lt;/a&gt; to get free credits!&lt;/p&gt;&lt;p&gt;Make sure you’ve set up &lt;strong&gt;DNS records&lt;/strong&gt;  for the domain or subdomain you plan to use for accessing Umami’s web interface:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Add an &lt;strong&gt;A record&lt;/strong&gt;  pointing to your server’s IPv4 address.&lt;/li&gt;&lt;li&gt;If you’re using IPv6, also add an &lt;strong&gt;AAAA record&lt;/strong&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I always recommend running self-hosted projects on the &lt;strong&gt;main hostname&lt;/strong&gt;  (like &lt;code&gt;umami.yourdomain.com&lt;/code&gt;) if that project is the only thing hosted on the server – which is the case here. I don’t recommend running Umami alongside other services on the same server. Instead, always opt for &lt;strong&gt;one server per project&lt;/strong&gt;  when possible. It keeps things clean, reduces conflicts, and makes troubleshooting easier.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;You can enable protection for your server’s primary IP addresses in the Hetzner dashboard to ensure they’re preserved even if the server is deleted.&lt;/p&gt;&lt;p&gt;This way, when restoring Umami on a new server, you can reassign the same IPs – avoiding the need to update any DNS settings.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;The only major requirement for installing Umami is having &lt;strong&gt;Docker Engine&lt;/strong&gt; and the &lt;strong&gt;Docker Compose plugin&lt;/strong&gt; installed on your server.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;For step-by-step instructions, see my &lt;a href=&quot;https://ivansalloum.com/installing-docker-and-docker-compose-on-ubuntu/&quot;&gt;Docker Engine &amp;amp; Compose Plugin installation tutorial&lt;/a&gt; on Ubuntu Server.&lt;/p&gt;&lt;p&gt;Umami doesn’t come with an automatic installation script – instead, we’ll create a &lt;code&gt;docker-compose.yml&lt;/code&gt; file and define the necessary configuration ourselves.&lt;/p&gt;&lt;p&gt;The official repository ships an example PostgreSQL stack, and I use that as a baseline before layering on my own tweaks.&lt;/p&gt;&lt;h2&gt;Installing Umami&lt;/h2&gt;&lt;p&gt;Installing &lt;strong&gt;Umami&lt;/strong&gt; is a straightforward process.&lt;/p&gt;&lt;p&gt;First, navigate to your home directory and create a new directory for Umami:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;mkdir ~/umami cd ~/umami
&lt;/pre&gt;&lt;p&gt;Next, create a &lt;code&gt;docker-compose.yml&lt;/code&gt; file inside that directory and add the following configuration:&lt;/p&gt;&lt;pre data-language=&quot;yaml&quot;&gt;services: umami: image: docker.umami.is/umami-software/umami:postgresql-latest ports: \- &amp;quot;3000:3000&amp;quot; environment: DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@umami-postgres:5432/${POSTGRES_DB} DATABASE_TYPE: postgresql APP_SECRET: ${APP_SECRET} depends_on: umami-postgres: condition: service_healthy restart: unless-stopped healthcheck: test: [&amp;quot;CMD-SHELL&amp;quot;, &amp;quot;curl -f http://localhost:3000/api/heartbeat || exit 1&amp;quot;] interval: 5s timeout: 5s retries: 5 umami-postgres: image: postgres:15 environment: POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} volumes: \- ./umami-postgres-data:/var/lib/postgresql/data restart: unless-stopped healthcheck: test: [&amp;quot;CMD-SHELL&amp;quot;, &amp;quot;pg_isready -d ${POSTGRES_DB} -U ${POSTGRES_USER}&amp;quot;] interval: 5s timeout: 5s retries: 5
&lt;/pre&gt;&lt;p&gt;This &lt;code&gt;docker-compose.yml&lt;/code&gt; file defines two services:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;umami&lt;/code&gt;&lt;/strong&gt; : the actual Umami app.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;umami-postgres&lt;/code&gt;&lt;/strong&gt; : the PostgreSQL database that Umami will use to store analytics data.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I mount the database under &lt;code&gt;./umami-postgres-data&lt;/code&gt;. Feel free to rename it, just stay consistent throughout your environment.&lt;/p&gt;&lt;p&gt;You’ll notice the Postgres image is pinned to &lt;code&gt;postgres:15&lt;/code&gt;; that matches Umami’s upstream example compose file and keeps the base install aligned with what the maintainers test against. Once Umami officially bumps their reference stack, you can update this version in lockstep.&lt;/p&gt;&lt;p&gt;The services are connected internally by Docker, and &lt;code&gt;depends_on&lt;/code&gt; ensures the database is healthy before Umami starts. All sensitive values like database credentials and secret keys are pulled from environment variables using the &lt;code&gt;${VARIABLE_NAME}&lt;/code&gt; format.&lt;/p&gt;&lt;p&gt;You need to create a &lt;code&gt;.env&lt;/code&gt; file in the same directory as your &lt;code&gt;docker-compose.yml&lt;/code&gt; with the following variables:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;POSTGRES_DB=umami POSTGRES_USER=umami POSTGRES_PASSWORD=userpassword APP_SECRET=yourgeneratedsecretkey REDIS_PASS=redispassword REDIS_URL=redis://:${REDIS_PASS}@redis:6379
&lt;/pre&gt;&lt;p&gt;That covers both the PostgreSQL credentials and the Redis settings we’ll enable later – feel free to pick stronger values right away so you don’t have to revisit the file.&lt;/p&gt;&lt;p&gt;To generate a secure &lt;code&gt;APP_SECRET&lt;/code&gt; value, you can run:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;openssl rand -base64 32
&lt;/pre&gt;&lt;p&gt;Copy the output and paste it as the value for &lt;code&gt;APP_SECRET&lt;/code&gt; in your &lt;code&gt;.env&lt;/code&gt; file.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Avoid using special characters&lt;/strong&gt;  like &lt;code&gt;!&lt;/code&gt;, &lt;code&gt;@&lt;/code&gt;, &lt;code&gt;#&lt;/code&gt;, or &lt;code&gt;&amp;amp;&lt;/code&gt; in your PostgreSQL password, as it can sometimes cause issues when parsed inside Docker or PostgreSQL.&lt;/p&gt;&lt;p&gt;Once your &lt;code&gt;docker-compose.yml&lt;/code&gt; and &lt;code&gt;.env&lt;/code&gt; files are ready, you can start &lt;strong&gt;Umami&lt;/strong&gt; by running the following command inside the same directory:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo docker compose up -d
&lt;/pre&gt;&lt;p&gt;This will pull the necessary images, start the containers in the background, and get Umami up and running on port &lt;code&gt;3000&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;You can now check if everything is running properly with:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo docker ps
&lt;/pre&gt;&lt;p&gt;This will list all running containers. You should see both the &lt;code&gt;umami&lt;/code&gt; and &lt;code&gt;umami-postgres&lt;/code&gt; services listed and marked as &lt;code&gt;Up&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Note that the container names may look different from the service names you used in the &lt;code&gt;docker-compose.yml&lt;/code&gt;. That’s because the &lt;strong&gt;Docker Compose&lt;/strong&gt; plugin** generates container names using this pattern:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;\--
&lt;/pre&gt;&lt;p&gt;Here’s a quick breakdown:&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;strong&gt;Part&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Value&lt;/strong&gt;&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;project-name&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;umami&lt;/code&gt; (folder name)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;service-name&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;umami&lt;/code&gt;, &lt;code&gt;umami-postgres&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;index&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;1&lt;/code&gt; (first container)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;So if your project folder is named &lt;code&gt;umami&lt;/code&gt;, your container names will likely be:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;umami-umami-1&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;code&gt;umami-umami-postgres-1&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;If something isn’t working or you want to check what’s going on behind the scenes, use:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo docker logs
&lt;/pre&gt;&lt;p&gt;You can use either the container&apos;s name or its ID to check logs for errors or issues.&lt;/p&gt;&lt;p&gt;You can now access Umami’s web interface by visiting:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;http://your_server_ip:3000
&lt;/pre&gt;&lt;p&gt;Log in using the default credentials:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Username&lt;/strong&gt; : &lt;code&gt;admin&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Password&lt;/strong&gt; : &lt;code&gt;umami&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Before doing anything else, I strongly recommend creating a new admin user with a unique username and strong password – then delete the default one. This helps keep your dashboard secure.&lt;/p&gt;&lt;h2&gt;Reverse Proxy Setup&lt;/h2&gt;&lt;p&gt;We definitely don’t want to access our web interface using a non-secure connection and our server’s IP address. Instead, we want to use our server’s &lt;strong&gt;hostname&lt;/strong&gt;  over a &lt;strong&gt;secure HTTPS connection&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;To achieve that, we’ll set up a &lt;strong&gt;reverse proxy&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;You can use any reverse proxy you&apos;re comfortable with (like NGINX), but in this guide, we’ll use &lt;strong&gt;Caddy&lt;/strong&gt;. It&apos;s a lightweight, modern web server that&apos;s easy to configure – and best of all, it handles SSL certificates &lt;strong&gt;automatically&lt;/strong&gt;  using Let&apos;s Encrypt.&lt;/p&gt;&lt;p&gt;Install Caddy with:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt install caddy
&lt;/pre&gt;&lt;p&gt;Caddy’s config is deceptively simple. Start by opening the config file:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo vim /etc/caddy/Caddyfile
&lt;/pre&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;Make a backup of the file before editing, just in case.&lt;/p&gt;&lt;p&gt;Clear out the default contents and paste in the following config (replace &lt;code&gt;umami.yourdomain.com&lt;/code&gt; with your actual domain):&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;umami.yourdomain.com { reverse_proxy localhost:3000 }
&lt;/pre&gt;&lt;p&gt;Before restarting Caddy, make sure your firewall actually lets ports &lt;strong&gt;80/443&lt;/strong&gt; through; otherwise, Let’s Encrypt can’t validate the certificate:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw allow 80/tcp sudo ufw allow 443/tcp
&lt;/pre&gt;&lt;p&gt;With the rules in place, restart Caddy:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo systemctl restart caddy
&lt;/pre&gt;&lt;p&gt;Caddy will automatically fetch an SSL certificate for your domain and start proxying traffic to your Umami container running on port &lt;code&gt;3000&lt;/code&gt;.&lt;/p&gt;&lt;h2&gt;Restrict Direct Access Port 3000&lt;/h2&gt;&lt;p&gt;By default, Docker exposes &lt;strong&gt;Umami&lt;/strong&gt; on port &lt;code&gt;3000&lt;/code&gt;, which means it can be accessed directly via your server&apos;s IP (&lt;code&gt;http://your_server_ip:3000&lt;/code&gt;).&lt;/p&gt;&lt;p&gt;To enhance security and ensure all traffic goes through your reverse proxy (&lt;strong&gt;Caddy&lt;/strong&gt;), you should restrict Umami to listen only on &lt;code&gt;localhost&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;In your &lt;code&gt;docker-compose.yml&lt;/code&gt;, change the port binding from:&lt;/p&gt;&lt;pre data-language=&quot;yaml&quot;&gt;ports: \- &amp;quot;3000:3000&amp;quot;
&lt;/pre&gt;&lt;p&gt;To:&lt;/p&gt;&lt;pre data-language=&quot;yaml&quot;&gt;ports: \- &amp;quot;127.0.0.1:3000:3000&amp;quot;
&lt;/pre&gt;&lt;p&gt;This ensures Umami is only accessible from inside the server (by Caddy), and &lt;strong&gt;blocks external access&lt;/strong&gt;  on port &lt;code&gt;3000&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Then, from inside your Umami project directory, restart your Docker containers:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo docker compose down sudo docker compose up -d
&lt;/pre&gt;&lt;p&gt;Umami is now securely hidden behind your reverse proxy.&lt;/p&gt;&lt;h2&gt;Enabling Redis&lt;/h2&gt;&lt;p&gt;Umami supports &lt;strong&gt;Redis&lt;/strong&gt;  as a &lt;strong&gt;caching layer&lt;/strong&gt;  to improve performance and handle login authentication.&lt;/p&gt;&lt;p&gt;When enabled, Redis caches frequently accessed data like &lt;strong&gt;website lookups&lt;/strong&gt; , reducing database load and speeding up responses. It also replaces &lt;strong&gt;JWT tokens&lt;/strong&gt;  with &lt;strong&gt;Redis-based session management&lt;/strong&gt; , providing a more efficient and secure way to handle &lt;strong&gt;user sessions&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;To enable Redis, we need to edit our &lt;code&gt;docker-compose.yml&lt;/code&gt; file to include Redis as a service and connect Umami to it.&lt;/p&gt;&lt;p&gt;First, add a new environment variable under the &lt;code&gt;APP_SECRET&lt;/code&gt; variable:&lt;/p&gt;&lt;pre data-language=&quot;yaml&quot;&gt;REDIS_URL: ${REDIS_URL}
&lt;/pre&gt;&lt;p&gt;This tells Umami where to find Redis.&lt;/p&gt;&lt;p&gt;If you followed the earlier &lt;code&gt;.env&lt;/code&gt; snippet, you already have the Redis variables in place. Otherwise, add the following lines now (and replace &lt;code&gt;redispassword&lt;/code&gt; with something strong):&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;REDIS_PASS=redispassword REDIS_URL=redis://:${REDIS_PASS}@redis:6379
&lt;/pre&gt;&lt;p&gt;&lt;code&gt;REDIS_PASS&lt;/code&gt; is the single source of truth for both the container and Umami’s connection string, so you only have to set the password once.&lt;/p&gt;&lt;p&gt;Now, add Redis as a service to your &lt;code&gt;docker-compose.yml&lt;/code&gt; file. This will run Redis as a container alongside Umami. Place this Redis service definition at the end of the file:&lt;/p&gt;&lt;pre data-language=&quot;yaml&quot;&gt;redis: image: redis:latest command: redis-server --requirepass ${REDIS_PASS} volumes: \- ./redis-data:/data restart: unless-stopped healthcheck: test: [&amp;quot;CMD&amp;quot;, &amp;quot;redis-cli&amp;quot;, &amp;quot;-a&amp;quot;, &amp;quot;${REDIS_PASS}&amp;quot;, &amp;quot;ping&amp;quot;] interval: 5s timeout: 5s retries: 5
&lt;/pre&gt;&lt;p&gt;Also, in your Umami service section, add Redis as a dependency to ensure Umami waits for Redis to be ready before starting. Under &lt;code&gt;depends_on&lt;/code&gt;, include:&lt;/p&gt;&lt;pre data-language=&quot;yaml&quot;&gt;redis: condition: service_healthy
&lt;/pre&gt;&lt;p&gt;After updating the &lt;code&gt;docker-compose.yml&lt;/code&gt; file, apply the changes by restarting your containers:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo docker compose down sudo docker compose up -d
&lt;/pre&gt;&lt;p&gt;Verify that all containers are running:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo docker ps
&lt;/pre&gt;&lt;p&gt;To check that Redis is working and accepting connections, enter the Redis container (substitute the same password you set in &lt;code&gt;REDIS_PASS&lt;/code&gt;):&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo docker exec -it umami-redis-1 redis-cli -a &amp;quot;${REDIS_PASS}&amp;quot;
&lt;/pre&gt;&lt;p&gt;Inside Redis CLI, test the connection:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ping
&lt;/pre&gt;&lt;p&gt;If Redis is working, it will respond with:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;PONG
&lt;/pre&gt;&lt;p&gt;Finally, to confirm Umami is connecting to Redis, you can monitor Redis activity:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo docker exec -it umami-redis-1 redis-cli -a &amp;quot;${REDIS_PASS}&amp;quot; monitor
&lt;/pre&gt;&lt;p&gt;Then visit your Umami web interface, and you should see Redis commands appear, confirming that Redis is handling caching and session management.&lt;/p&gt;&lt;h2&gt;Installing Updates&lt;/h2&gt;&lt;p&gt;To upgrade to the latest version, simply pull the updated image and restart your containers.&lt;/p&gt;&lt;p&gt;Navigate to your Umami project directory and pull the latest image for the PostgreSQL build:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo docker pull docker.umami.is/umami-software/umami:postgresql-latest
&lt;/pre&gt;&lt;p&gt;Recreate your containers using the updated image:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo docker compose down sudo docker compose up -d
&lt;/pre&gt;&lt;p&gt;Umami will apply any necessary database migrations automatically on startup.&lt;/p&gt;&lt;p&gt;If you’re still running the legacy MySQL stack, skip down to the migration section before pulling any new images.&lt;/p&gt;&lt;p&gt;Your data will remain safe, and the update process typically takes just a few seconds.&lt;/p&gt;&lt;p&gt;Always take a snapshot first if you&apos;re running in production.&lt;/p&gt;&lt;h2&gt;Disaster Recovery&lt;/h2&gt;&lt;p&gt;The final step in setting up your self-hosted analytics platform is planning for disaster recovery.&lt;/p&gt;&lt;p&gt;Think about what you’d do if something goes wrong – like a server breach, a misconfiguration, a bad update, or even something out of your control, like a fire in your provider’s data center. You need a way to quickly bring &lt;strong&gt;Umami&lt;/strong&gt; back online without losing your analytics data.&lt;/p&gt;&lt;p&gt;We already have our primary IPs secured (assuming you have enabled protection for them) – they stay with us no matter what, which is great. Now, we need a reliable way to restore the server and reassign those same IPs. The best way to do this is by using &lt;strong&gt;snapshots&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;If you&apos;re using Hetzner, you can go to your server’s &lt;strong&gt;Snapshots&lt;/strong&gt;  tab and create one. A snapshot is a full backup of your server’s disk at that point in time.&lt;/p&gt;&lt;p&gt;If disaster strikes, you can spin up a new server from that snapshot, assign the primary IPs, and it should work right away – no need to change any DNS settings or reconfigure Caddy.&lt;/p&gt;&lt;p&gt;Make sure to test this recovery process at least once so you’re confident it works.&lt;/p&gt;&lt;p&gt;Snapshots are created manually. If you want automatic backups, you can enable Hetzner&apos;s backup feature. It costs 20% extra, but it keeps daily backups for a week. Once the week is over, old backups are replaced by new ones. You can convert any of these backups into a snapshot, which you can then use to restore your server.&lt;/p&gt;&lt;p&gt;If you delete the server, all its backups are deleted too. Always convert at least one backup into a snapshot before deleting anything. Also, enable &lt;strong&gt;deletion protection&lt;/strong&gt;  on your server to avoid losing everything by mistake.&lt;/p&gt;&lt;h2&gt;&lt;strong&gt;Migrating from MySQL to PostgreSQL&lt;/strong&gt;&lt;/h2&gt;&lt;p&gt;It took me two days to finish this section!&lt;/p&gt;&lt;p&gt;Umami made it unnecessarily hard for Docker users to migrate – just because they didn’t want to spend some time writing a proper walkthrough.&lt;/p&gt;&lt;p&gt;Their official page barely covers the migration and even that doesn’t work out of the box.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;What you’ll do (high level):&lt;/strong&gt;&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Spin up a &lt;strong&gt;new&lt;/strong&gt;  Umami stack on &lt;strong&gt;PostgreSQL&lt;/strong&gt;  using Umami &lt;strong&gt;v2.19.0&lt;/strong&gt;  (the last v2 release).&lt;/li&gt;&lt;li&gt;Import your data from the old MySQL instance.&lt;/li&gt;&lt;li&gt;Verify everything on a temporary port (&lt;strong&gt;3001&lt;/strong&gt;).&lt;/li&gt;&lt;li&gt;Switch the image to &lt;strong&gt;v3&lt;/strong&gt;  and cut over to port &lt;strong&gt;3000&lt;/strong&gt;.&lt;/li&gt;&lt;/ol&gt;&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;&lt;strong&gt;Why v2.19.0 first?&lt;/strong&gt; You migrate the data into a clean v2 database, verify, and &lt;strong&gt;then&lt;/strong&gt;  upgrade the app to v3. This avoids schema surprises during the import.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;First, create a new directory for the PostgreSQL setup:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;mkdir umami-postgresql cd umami-postgresql
&lt;/pre&gt;&lt;p&gt;Inside that directory, create two files: &lt;code&gt;.env&lt;/code&gt; and &lt;code&gt;docker-compose.yml&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Open &lt;code&gt;.env&lt;/code&gt; and add the following:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;POSTGRES_DB=umami POSTGRES_USER=umami POSTGRES_PASSWORD=password APP_SECRET=yourgeneratedsecretkey REDIS_PASS=redispassword REDIS_URL=redis://:${REDIS_PASS}@redis:6379
&lt;/pre&gt;&lt;p&gt;Open &lt;code&gt;docker-compose.yml&lt;/code&gt; and add the following:&lt;/p&gt;&lt;pre data-language=&quot;yaml&quot;&gt;services: umami: image: docker.umami.is/umami-software/umami:postgresql-v2.19.0 ports: \- &amp;quot;3001:3000&amp;quot; environment: DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@umami-postgres:5432/${POSTGRES_DB} APP_SECRET: ${APP_SECRET} REDIS_URL: ${REDIS_URL} depends_on: umami-postgres: condition: service_healthy redis: condition: service_healthy restart: unless-stopped healthcheck: test: [&amp;quot;CMD-SHELL&amp;quot;, &amp;quot;curl -fsS http://localhost:3000/api/heartbeat || exit 1&amp;quot;] interval: 5s timeout: 5s retries: 5 umami-postgres: image: postgres:15 environment: POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} volumes: \- ./umami-pg-data:/var/lib/postgresql/data restart: unless-stopped healthcheck: test: [&amp;quot;CMD-SHELL&amp;quot;, &amp;quot;pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}&amp;quot;] interval: 5s timeout: 5s retries: 5 redis: image: redis:latest command: redis-server --requirepass ${REDIS_PASS} volumes: \- ./redis-data:/data restart: unless-stopped healthcheck: test: [&amp;quot;CMD&amp;quot;, &amp;quot;redis-cli&amp;quot;, &amp;quot;-a&amp;quot;, &amp;quot;${REDIS_PASS}&amp;quot;, &amp;quot;ping&amp;quot;] interval: 5s timeout: 5s retries: 5
&lt;/pre&gt;&lt;p&gt;We’re spinning up a new Umami instance running on &lt;strong&gt;port 3001&lt;/strong&gt;  so we can check the migration before replacing the old one.&lt;/p&gt;&lt;p&gt;This setup uses the &lt;strong&gt;last MySQL-compatible Umami version (v2.19.0)&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Run the containers:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo docker compose up -d
&lt;/pre&gt;&lt;p&gt;Then open your browser and go to:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;http://yourserverip:3001
&lt;/pre&gt;&lt;p&gt;If it loads, you’re good.&lt;/p&gt;&lt;p&gt;Now stop both Umami (app) containers (not PostgreSQL or MySQL or Redis):&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo docker ps sudo docker stop
&lt;/pre&gt;&lt;p&gt;From the &lt;strong&gt;old MySQL&lt;/strong&gt;  docker directory, run:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;docker exec -i \ mysqldump -h 127.0.0.1 -u root -p&apos;YOUR_OLD_ROOT_PASSWORD&apos; \ \--no-create-info --default-character-set=utf8mb4 --quick --single-transaction --skip-add-locks \ umami &amp;gt; umami_mysql_dump.sql
&lt;/pre&gt;&lt;p&gt;Make the dump Postgres-friendly:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;# 1) MySQL backticks -&amp;gt; Postgres double quotes sed -i &apos;s/`/&amp;quot;/g&apos; umami_mysql_dump.sql # 2) MySQL-style escaped quotes -&amp;gt; Postgres-style sed -i &amp;quot;s/\\\\\\\&apos;/&apos;&apos;/g&amp;quot; umami_mysql_dump.sql
&lt;/pre&gt;&lt;p&gt;Copy the dump file into the &lt;strong&gt;new&lt;/strong&gt;  Postgres container:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;# back in the new umami-postgresql directory sudo docker cp umami_mysql_dump.sql :/tmp/dump.sql
&lt;/pre&gt;&lt;p&gt;Clear two internal tables (per Umami’s guidance):&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo docker exec -it \ psql -U -d umami \ -c &apos;TRUNCATE TABLE &amp;quot;_prisma_migrations&amp;quot;, &amp;quot;user&amp;quot;;&apos;
&lt;/pre&gt;&lt;p&gt;Import (temporarily relaxing foreign keys during the load):&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;
```bash sudo docker start ```

Open `http://yourserverip:3001` and log in with your **existing**  Umami credentials from MySQL. If websites don’t appear, make sure you’re logged in as the user who owns them.

Now, edit your `docker-compose.yml` to use the latest image and restore the local binding:

```yaml
image: docker.umami.is/umami-software/umami:postgresql-latest ports: \- &amp;quot;127.0.0.1:3000:3000&amp;quot;
&lt;/pre&gt;&lt;p&gt;Back in your old Umami directory:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo docker compose down -v --rmi local --remove-orphans sudo rm -rf ./umami-db-data ./redis-data
&lt;/pre&gt;&lt;p&gt;Those folder names match the original MySQL-era guide (&lt;code&gt;umami-db-data&lt;/code&gt;). If your old stack used a different directory – say, &lt;code&gt;./umami-mysql-data&lt;/code&gt; – swap the paths accordingly so you don’t delete the wrong data.&lt;/p&gt;&lt;p&gt;Then, back in your new Umami directory:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo docker compose up -d
&lt;/pre&gt;&lt;p&gt;Your Umami instance is now fully running on PostgreSQL – finally, the way it should’ve been documented in the first place.&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;With your Umami instance now fully deployed and secured behind a reverse proxy, you have complete control over your analytics.&lt;/p&gt;&lt;p&gt;From here, you can now start adding websites, creating new teams, setting up custom events, and exploring your analytics dashboard – all from your own infrastructure, fully in your control.&lt;/p&gt;&lt;p&gt;You’ve built a powerful, privacy-friendly analytics platform – no third-party trackers, no compromises.&lt;/p&gt;&lt;p&gt;And just as important – &lt;strong&gt;test your disaster recovery plan regularly&lt;/strong&gt;  to make sure everything still works as expected. It’s better to catch issues during a test than during a real outage.&lt;/p&gt;&lt;p&gt;If you run into any issues or need further help, feel free to revisit this guide or &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;reach out&lt;/a&gt; for assistance.&lt;/p&gt;&lt;p&gt;If you found value in this guide or have any questions or feedback, please don&apos;t hesitate to share your thoughts in the &lt;strong&gt;discussion&lt;/strong&gt; section.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Servers</category></item><item><title>Setting Up Postal as an SMTP Server</title><link>https://ivansalloum.com/setting-up-postal-as-an-smtp-server/</link><guid isPermaLink="true">https://ivansalloum.com/setting-up-postal-as-an-smtp-server/</guid><description>Set up a secure and efficient SMTP server with Postal, from installation to advanced configuration.</description><pubDate>Mon, 07 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;Over the past few weeks, I’ve been exploring the idea of running my own mail server. While I had a basic understanding of how email servers work, I’d never actually set one up from scratch.&lt;/p&gt;&lt;p&gt;To keep things simple (and sane), I decided to focus on just one piece of the puzzle: &lt;strong&gt;sending email&lt;/strong&gt;. No inboxes, no IMAP or POP – just a streamlined, &lt;strong&gt;send-only SMTP server&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Get updates from my mail server journey – tips, lessons, and discoveries along the way.&lt;/p&gt;&lt;p&gt;I’m In!&lt;/p&gt;&lt;h2&gt;Why Postal?&lt;/h2&gt;&lt;p&gt;If you know me, you know that from my collection of &lt;a href=&quot;https://ivansalloum.com/collections/linux-server-security/&quot;&gt;server security&lt;/a&gt; guides, I’ve always tried to limit the use of third-party security software and prefer to build my own solutions.&lt;/p&gt;&lt;p&gt;I’ve always leaned towards the &lt;strong&gt;old school&lt;/strong&gt; way of doing things, avoiding extra software and instead using the available server packages to suit my needs.&lt;/p&gt;&lt;p&gt;So why didn’t I just go with &lt;strong&gt;Postfix&lt;/strong&gt; for this SMTP setup? I seriously considered it – but &lt;strong&gt;Postal&lt;/strong&gt; offered too many advantages to ignore.&lt;/p&gt;&lt;p&gt;Unlike Postfix, Postal comes with a suite of modern features out of the box. And since this is a fairly complex project, I didn’t want to spend hours wiring together manual configs for every little piece.&lt;/p&gt;&lt;p&gt;Postal simplifies a lot of that – and has an active community (&lt;a href=&quot;https://github.com/orgs/postalserver/discussions&quot;&gt;GitHub Discussions&lt;/a&gt;) where you can get support if you hit a wall.&lt;/p&gt;&lt;p&gt;🙆‍♂️&lt;/p&gt;&lt;p&gt;One feature that really sold me: &lt;strong&gt;IP pool support&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Postal makes it easy to create multiple IP pools, so you can send emails from different IP addresses instead of being stuck with just the server’s main IP.&lt;/p&gt;&lt;p&gt;This is huge for:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Separating email traffic across clients or apps.&lt;/li&gt;&lt;li&gt;Managing deliverability and sender reputation.&lt;/li&gt;&lt;li&gt;Assigning clean IPs to new users without affecting your existing ones.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;If you&apos;re running multiple projects or managing email for clients, this kind of flexibility is a must-have.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;And that’s just scratching the surface.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Throughout this guide, you’ll discover more powerful features&lt;/strong&gt; , such as its clean and intuitive web interface, its built-in integration with &lt;strong&gt;SpamAssassin&lt;/strong&gt;  to scan outgoing emails for spam, and even the ability to &lt;strong&gt;receive emails through  routes&lt;/strong&gt; (yep, we’ll cover that too).&lt;/p&gt;&lt;h2&gt;Author&apos;s Note&lt;/h2&gt;&lt;p&gt;Now that you know why I chose &lt;strong&gt;Postal&lt;/strong&gt; as my SMTP server, I want to highlight a few key points that can make or break your setup – especially when it comes to deliverability.&lt;/p&gt;&lt;p&gt;❗&lt;/p&gt;&lt;p&gt;Your IP reputation is everything.&lt;/p&gt;&lt;p&gt;A clean, trusted IP is crucial – not just for your main server, but for &lt;strong&gt;every IP you use in a pool&lt;/strong&gt;. A poor reputation can lead to your emails being flagged as spam or blocked entirely.&lt;/p&gt;&lt;p&gt;That’s why choosing the right server provider matters. In my experience, &lt;strong&gt;Hetzner&lt;/strong&gt; has the strictest email-sending policies.&lt;/p&gt;&lt;p&gt;Most providers block port &lt;strong&gt;25&lt;/strong&gt;  by default to combat spam, and while many will unblock it upon request, &lt;strong&gt;Hetzner is by far the most stringent in this regard&lt;/strong&gt;. Their strict policies ensure that their IPs remain clean and trusted, making them an excellent choice for an SMTP server.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;New to Hetzner? &lt;a href=&quot;https://hetzner.cloud/?ref=MC4Yy318xX5X&quot;&gt;Use my link&lt;/a&gt; to get free credits!&lt;/p&gt;&lt;p&gt;But even with clean IPs, don’t expect inbox success on day one. If you&apos;re using a brand-new IP, mail servers may treat it as suspicious. You’ll need to warm up the IP – sending gradually and building trust over time.&lt;/p&gt;&lt;p&gt;🔁&lt;/p&gt;&lt;p&gt;Are you planning to use &lt;strong&gt;multiple IPs&lt;/strong&gt; or create &lt;strong&gt;IP pools&lt;/strong&gt;? Choose a provider that supports it.&lt;/p&gt;&lt;p&gt;Hetzner, again, is great here. They offer &lt;strong&gt;Floating IPs&lt;/strong&gt; –** extra public IP addresses you can attach to your VPS in addition to the main IP.&lt;/p&gt;&lt;p&gt;What makes Floating IPs useful:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;They’re &lt;strong&gt;detached from the server&lt;/strong&gt; itself, so if you delete or replace the server, you can &lt;strong&gt;reassign the same IPs&lt;/strong&gt; to a new one.&lt;/li&gt;&lt;li&gt;This helps you &lt;strong&gt;preserve your sending reputation&lt;/strong&gt; and avoid restarting from scratch.&lt;/li&gt;&lt;li&gt;Perfect for &lt;strong&gt;IP pools&lt;/strong&gt; and advanced sending setups.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;If you&apos;re using a provider other than Hetzner, be sure to check if they offer something similar.&lt;/p&gt;&lt;p&gt;✅&lt;/p&gt;&lt;p&gt;&lt;strong&gt;And don’t forget:&lt;/strong&gt; Make sure port 25 is open. Without it, your SMTP server won’t be able to send anything.&lt;/p&gt;&lt;h3&gt;IP Pools&lt;/h3&gt;&lt;p&gt;If you&apos;re interested in using IP pools with Postal, I&apos;ve written a separate guide that walks through how to set them up and use them effectively.&lt;/p&gt;&lt;p&gt;But don’t worry about that just yet – &lt;strong&gt;I recommend completing this setup guide first&lt;/strong&gt; , then coming back to the IP pools once everything is up and running.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://ivansalloum.com/how-to-use-ip-pools-in-postal/&quot;&gt;How to Use IP Pools for Better Deliverability in Postal&lt;/a&gt;&lt;/p&gt;&lt;h2&gt;Server Preparation&lt;/h2&gt;&lt;p&gt;Before preparing our server to run Postal, we need to deploy it first.&lt;/p&gt;&lt;p&gt;Choose &lt;strong&gt;Ubuntu 24.04 LTS&lt;/strong&gt; as the server image – it’s stable, well-supported, and perfect for this setup.&lt;/p&gt;&lt;p&gt;If you&apos;re using Hetzner, you’ll notice they offer two types of servers:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Shared CPU&lt;/strong&gt; (resources are shared with other customers)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Dedicated CPU&lt;/strong&gt; (resources are reserved for you)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;❗&lt;/p&gt;&lt;p&gt;You don’t want your outbound queue throttled because of a noisy neighbor.&lt;/p&gt;&lt;p&gt;I highly recommend opting for the servers with &lt;strong&gt;dedicated CPUs&lt;/strong&gt; , as an SMTP server is mission-critical and should never face resource limitations.&lt;/p&gt;&lt;p&gt;It’s also essential to &lt;strong&gt;enable IPv6&lt;/strong&gt; on your server (many modern mail servers require it) and &lt;strong&gt;monitor server resources&lt;/strong&gt; to avoid bottlenecks in production.&lt;/p&gt;&lt;h3&gt;Initial Setup&lt;/h3&gt;&lt;p&gt;After your server is deployed, &lt;strong&gt;SSH&lt;/strong&gt; into it and begin by running the following commands to perform a full update:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;apt update apt dist-upgrade
&lt;/pre&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;Assuming you’re accessing the server as &lt;code&gt;root&lt;/code&gt; for the first time, remember to change your root password to something stronger afterward.&lt;/p&gt;&lt;p&gt;Next, make sure to set the correct timezone for your server. This isn’t just for convenience – it’s important for Postal’s internal logging and email timestamps.&lt;/p&gt;&lt;p&gt;For example, to set the timezone to Berlin, you can run:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;timedatectl set-timezone Europe/Berlin
&lt;/pre&gt;&lt;p&gt;With the timezone in place, the next step is to configure your server’s hostname.&lt;/p&gt;&lt;p&gt;A typical hostname consists of two parts: the &lt;strong&gt;server name&lt;/strong&gt; and the &lt;strong&gt;domain name&lt;/strong&gt;. Since we’re building an SMTP server, a clear and descriptive hostname helps other mail servers recognize your server’s role.&lt;/p&gt;&lt;p&gt;A recommended format is:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;hostnamectl set-hostname mailout.example.com
&lt;/pre&gt;&lt;p&gt;Using &lt;code&gt;mailout&lt;/code&gt; as the server name is a good practice, as it clearly indicates to other mail servers that this server is handling SMTP functions.&lt;/p&gt;&lt;p&gt;For better security:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Create a new user with &lt;strong&gt;sudo privileges&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Disable&lt;/strong&gt; the root user&lt;/li&gt;&lt;li&gt;Set up &lt;strong&gt;SSH keys&lt;/strong&gt; for the new user&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Disable&lt;/strong&gt; password authentication&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;I’ve covered all of this in detail in my &lt;a href=&quot;https://ivansalloum.com/preparing-your-ubuntu-server-for-first-use/&quot;&gt;Server Preparation guide&lt;/a&gt; – check it out if you need help.&lt;/p&gt;&lt;h3&gt;Install Dependencies and Docker&lt;/h3&gt;&lt;p&gt;Now that your server is updated and configured, let’s install the essential tools &lt;strong&gt;Postal&lt;/strong&gt; relies on.&lt;/p&gt;&lt;p&gt;Start by installing the required packages:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt install git curl jq
&lt;/pre&gt;&lt;p&gt;This ensures that &lt;code&gt;git&lt;/code&gt; is available on your server, which we’ll use to clone the Postal installation helper repository:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo git clone https://github.com/postalserver/install /opt/postal/install
&lt;/pre&gt;&lt;p&gt;Then, create a symbolic link so you can run the &lt;code&gt;postal&lt;/code&gt; command from anywhere on the server:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ln -s /opt/postal/install/bin/postal /usr/bin/postal
&lt;/pre&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;The &lt;code&gt;postal&lt;/code&gt; command needs to be run with &lt;code&gt;sudo&lt;/code&gt; privileges or as the root user.&lt;/p&gt;&lt;h4&gt;Use a Proper Docker Setup (Not Ubuntu’s Built-in Version)&lt;/h4&gt;&lt;p&gt;Postal runs entirely inside containers, so installing &lt;strong&gt;Docker Engine&lt;/strong&gt; and the &lt;strong&gt;Docker Compose plugin&lt;/strong&gt; is a &lt;strong&gt;major prerequisite&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;But, the Docker packages included in Ubuntu’s default repositories are often outdated and may cause issues.&lt;/p&gt;&lt;p&gt;To avoid problems, follow my &lt;a href=&quot;https://ivansalloum.com/installing-docker-and-docker-compose-on-ubuntu/&quot;&gt;Docker installation tutorial&lt;/a&gt;, where I walk through installing the latest version of Docker and Docker Compose the right way – fully compatible with Postal.&lt;/p&gt;&lt;h3&gt;Set Up MariaDB&lt;/h3&gt;&lt;p&gt;Postal requires a database engine to store all emails and other essential configuration data, and it will automatically provision a database for each mail server you create.&lt;/p&gt;&lt;p&gt;We’ll run &lt;strong&gt;MariaDB&lt;/strong&gt; in a container, which is the easiest option:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo docker run -d \ \--name postal-mariadb \ -p 127.0.0.1:3306:3306 \ \--restart always \ -e MARIADB_DATABASE=postal \ -e MARIADB_ROOT_PASSWORD=postal \ mariadb
&lt;/pre&gt;&lt;p&gt;This will start a MariaDB instance, listening on port &lt;code&gt;3306&lt;/code&gt;. Make sure to change the root password to a strong one and store it securely, as you’ll need it later during the Postal configuration.&lt;/p&gt;&lt;p&gt;You can verify that MariaDB is running with:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo docker ps
&lt;/pre&gt;&lt;p&gt;You should see the &lt;code&gt;postal-mariadb&lt;/code&gt; container listed with its status as &lt;code&gt;Up&lt;/code&gt;.&lt;/p&gt;&lt;h3&gt;Final Prep Steps&lt;/h3&gt;&lt;p&gt;Once everything is in place, it&apos;s a good idea to &lt;code&gt;sudo reboot&lt;/code&gt; your server to make sure all changes are fully applied.&lt;/p&gt;&lt;p&gt;If you&apos;re using &lt;strong&gt;Hetzner&lt;/strong&gt; , visit the &lt;strong&gt;Primary IPs&lt;/strong&gt; tab and enable protection for your assigned IPv4 and IPv6 addresses.&lt;/p&gt;&lt;p&gt;✅&lt;/p&gt;&lt;p&gt;IP protection allows you to &lt;strong&gt;reuse your existing IPs&lt;/strong&gt; if you ever need to redeploy the server.&lt;/p&gt;&lt;h2&gt;Generating Configuration Files&lt;/h2&gt;&lt;p&gt;Before starting the installation, Postal provides a tool that automatically generates the initial configuration files required for setup.&lt;/p&gt;&lt;p&gt;Run the following command, replacing &lt;code&gt;mailout.example.com&lt;/code&gt; with the actual hostname you set earlier – the one you plan to use to access the Postal web interface:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo postal bootstrap mailout.example.com
&lt;/pre&gt;&lt;p&gt;The command will generate three important files inside the &lt;code&gt;/opt/postal/config/&lt;/code&gt; directory:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;postal.yml&lt;/code&gt;&lt;/strong&gt; : The main configuration file for Postal.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;signing.key&lt;/code&gt;&lt;/strong&gt; : A private key used by Postal to sign various internal components.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;Caddyfile&lt;/code&gt;&lt;/strong&gt; : The configuration file for the &lt;strong&gt;Caddy&lt;/strong&gt; web server, which Postal uses to serve its web interface.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The only file that matters to us at this point is the &lt;code&gt;postal.yml&lt;/code&gt; file, which we’ll use to tweak and customize Postal’s configuration.&lt;/p&gt;&lt;p&gt;Once the files are generated, open the &lt;code&gt;postal.yml&lt;/code&gt; file and update the passwords for both the &lt;code&gt;main_db&lt;/code&gt; and &lt;code&gt;message_db&lt;/code&gt; sections.&lt;/p&gt;&lt;p&gt;Use the password you specified earlier when creating the MariaDB instance with Docker:&lt;/p&gt;&lt;pre data-language=&quot;yaml&quot;&gt;main_db: host: 127.0.0.1 username: root password: here_comes_the_password database: postal message_db: host: 127.0.0.1 username: root password: here_comes_the_password prefix: postal
&lt;/pre&gt;&lt;p&gt;Save and close the file – that’s all we need to do for now.&lt;/p&gt;&lt;p&gt;❗&lt;/p&gt;&lt;p&gt;Since the Docker setup mounts &lt;code&gt;/opt/postal/config&lt;/code&gt; as &lt;code&gt;/config&lt;/code&gt;, any file paths referenced inside &lt;code&gt;postal.yml&lt;/code&gt; should start with &lt;code&gt;/config&lt;/code&gt; instead of &lt;code&gt;/opt/postal/config&lt;/code&gt;.&lt;/p&gt;&lt;h2&gt;DNS Setup&lt;/h2&gt;&lt;p&gt;Correct DNS configuration is &lt;strong&gt;critical&lt;/strong&gt;. If your DNS isn’t properly set up, your emails may end up in spam – or not be delivered at all.&lt;/p&gt;&lt;p&gt;Postal requires several DNS records to function properly. Let’s walk through each type.&lt;/p&gt;&lt;h3&gt;A &amp;amp; AAAA&lt;/h3&gt;&lt;p&gt;We’ll begin by creating two essential DNS records: an A record and an AAAA record:&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;strong&gt;Record Type&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Host&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Value (IP Address)&lt;/strong&gt;&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;A&lt;/td&gt;&lt;td&gt;mailout.example.com&lt;/td&gt;&lt;td&gt;Primary IPv4&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;AAAA&lt;/td&gt;&lt;td&gt;mailout.example.com&lt;/td&gt;&lt;td&gt;Primary IPv6&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;Then, add A and AAAA records for the MX hostname that we will use later for receiving emails through routes:&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;strong&gt;Record Type&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Host&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Value (IP Address)&lt;/strong&gt;&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;A&lt;/td&gt;&lt;td&gt;mx.mailout.example.com&lt;/td&gt;&lt;td&gt;Primary IPv4&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;AAAA&lt;/td&gt;&lt;td&gt;mx.mailout.example.com&lt;/td&gt;&lt;td&gt;Primary IPv6&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;All records should point to your server’s primary IPs&lt;/p&gt;&lt;h3&gt;PTR (Reverse DNS)&lt;/h3&gt;&lt;p&gt;PTR (Reverse DNS) records are used by receiving mail servers to verify that your server’s IP address maps back to its hostname – this is a key check for spam prevention and essential for passing many email reputation filters.&lt;/p&gt;&lt;p&gt;Now, set up reverse DNS (PTR) for both IPv4 and IPv6:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Go to &lt;strong&gt;Hetzner dashboard&lt;/strong&gt; →&lt;strong&gt;Server Details&lt;/strong&gt; →&lt;strong&gt;Networking&lt;/strong&gt; tab&lt;/li&gt;&lt;li&gt;Find the IP you want to edit, click the &lt;strong&gt;three dots&lt;/strong&gt; →&lt;strong&gt;Edit Reverse DNS&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;Set the reverse DNS to match your hostname (&lt;code&gt;mailout.example.com&lt;/code&gt;)&lt;/li&gt;&lt;li&gt;For IPv6, append &lt;code&gt;::1&lt;/code&gt; and update the hostname accordingly&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;✅&lt;/p&gt;&lt;p&gt;Your PTR record must match your server&apos;s hostname exactly.&lt;/p&gt;&lt;h3&gt;SPF Record&lt;/h3&gt;&lt;p&gt;It&apos;s crucial to add an SPF (Sender Policy Framework) record to your DNS setup.&lt;/p&gt;&lt;p&gt;This record helps prevent your emails from being marked as spam by verifying that your server is authorized to send emails on behalf of your domain.&lt;/p&gt;&lt;p&gt;Add a global SPF record that includes your primary IPs:&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;strong&gt;Record Type&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Host&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Value&lt;/strong&gt;&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;TXT&lt;/td&gt;&lt;td&gt;spf.mailout.example.com&lt;/td&gt;&lt;td&gt;&amp;quot;v=spf1 ip4:70.130.219.212 ip6:643c:ac1f:3f7c:bb5c::1 ~all&amp;quot;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;This is a global SPF record you will use for any domain you add to Postal.&lt;/p&gt;&lt;h3&gt;Return Path (MX + SPF + DKIM)&lt;/h3&gt;&lt;p&gt;The &lt;strong&gt;Return Path&lt;/strong&gt;  is the email address that gets used to handle &lt;strong&gt;bounces&lt;/strong&gt;  (rejected or undelivered emails) for any email you send.&lt;/p&gt;&lt;p&gt;It tells the receiving mail server where to &lt;strong&gt;send error messages&lt;/strong&gt;  if it can&apos;t deliver an email (like if the recipient&apos;s email address doesn&apos;t exist, or the mailbox is full).&lt;/p&gt;&lt;p&gt;It&apos;s different from the &lt;strong&gt;From&lt;/strong&gt; address and is usually hidden from end users – but it’s essential for proper bounce handling.&lt;/p&gt;&lt;p&gt;Think of it like this:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;You send an email to someone.&lt;/li&gt;&lt;li&gt;If the email can&apos;t be delivered, a &lt;strong&gt;bounce email&lt;/strong&gt;  will be sent back to the &lt;strong&gt;return path&lt;/strong&gt;  address, not your &lt;strong&gt;From&lt;/strong&gt;  address.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;To configure a return path domain in Postal, we need to add three DNS records:&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;strong&gt;Record Type&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Host&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Mail Server&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Priority&lt;/strong&gt;&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;MX&lt;/td&gt;&lt;td&gt;rp.mailout.example.com&lt;/td&gt;&lt;td&gt;mailout.example.com&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;This tells the world where to send &lt;strong&gt;bounce emails&lt;/strong&gt;  for the return path.&lt;/p&gt;&lt;p&gt;It means &lt;strong&gt;bounces&lt;/strong&gt;  for emails sent from &lt;code&gt;mailout.example.com&lt;/code&gt; will go to &lt;code&gt;rp.mailout.example.com&lt;/code&gt; (your return path domain), which will then point to your Postal server to handle them&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;strong&gt;Record Type&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Host&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Value&lt;/strong&gt;&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;TXT&lt;/td&gt;&lt;td&gt;rp.mailout.example.com&lt;/td&gt;&lt;td&gt;&amp;quot;v=spf1 a mx include:spf.mailout.example.com ~all&amp;quot;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;This authorizes your return path domain to send emails and helps prevent bounce messages from being flagged as spam.&lt;/p&gt;&lt;p&gt;The last DNS record we need to add is the &lt;strong&gt;DKIM&lt;/strong&gt; (DomainKeys Identified Mail) record. It is an email authentication method that uses a digital signature to verify that the email was sent and authorized by the domain owner.&lt;/p&gt;&lt;p&gt;✅&lt;/p&gt;&lt;p&gt;This digitally signs emails from the return path domain, proving they haven’t been tampered with and are authorized by your server.&lt;/p&gt;&lt;p&gt;Postal provides a command that generates a DKIM record for us, which begins with the &lt;code&gt;postal&lt;/code&gt; DKIM identifier. If you&apos;d like to customize this identifier, open the &lt;code&gt;/opt/postal/config/postal.yml&lt;/code&gt; file and add the following to the &lt;code&gt;dns:&lt;/code&gt; section:&lt;/p&gt;&lt;pre data-language=&quot;yaml&quot;&gt;dns: dkim_identifier: custom
&lt;/pre&gt;&lt;p&gt;Afterward, run this command to generate the DKIM record:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo postal default-dkim-record
&lt;/pre&gt;&lt;p&gt;In my case, I am adding a TXT record with the host value of:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;custom._domainkey.rp.mailout.example.com
&lt;/pre&gt;&lt;p&gt;The value for this record will be the output generated by the command.&lt;/p&gt;&lt;h3&gt;Route Domain&lt;/h3&gt;&lt;p&gt;If you wish to receive incoming emails by forwarding them directly to routes in Postal, you&apos;ll need to configure a domain and point it to your server using an MX record, like this:&lt;/p&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;strong&gt;Record Type&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Host&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Mail Server&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;Priority&lt;/strong&gt;&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;MX&lt;/td&gt;&lt;td&gt;routes.mailout.example.com&lt;/td&gt;&lt;td&gt;mailout.example.com&lt;/td&gt;&lt;td&gt;10&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;This lets incoming emails sent to &lt;code&gt;routes.mailout.example.com&lt;/code&gt; reach Postal for routing.&lt;/p&gt;&lt;h3&gt;DMARC&lt;/h3&gt;&lt;p&gt;DMARC (Domain-based Message Authentication, Reporting, and Conformance) is an email authentication protocol that helps prevent email spoofing and phishing by allowing domain owners to specify how incoming emails should be handled if it fails SPF or DKIM checks.&lt;/p&gt;&lt;p&gt;The DMARC record specifies:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Whether to accept, quarantine, or reject emails that fail authentication.&lt;/li&gt;&lt;li&gt;How the domain owner wants to receive reports about these emails.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;For this, add a TXT record with a host value starting with &lt;code&gt;_dmarc&lt;/code&gt; for your domain and containing the following:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;&amp;quot;v=DMARC1; p=quarantine; rua=mailto:postmaster@example.com&amp;quot;
&lt;/pre&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;p=quarantine&lt;/code&gt;&lt;/strong&gt; : This policy tells receiving mail servers to treat emails that fail DMARC checks as suspicious and place them in the recipient&apos;s spam or junk folder.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;rua=mailto:postmaster@example.com&lt;/code&gt;&lt;/strong&gt; : This specifies the email address where aggregate reports about DMARC failures will be sent, allowing the domain owner to monitor and analyze the performance of their DMARC policy.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Adding a DMARC record improves both your domain&apos;s security and email deliverability.&lt;/p&gt;&lt;p&gt;❗&lt;/p&gt;&lt;p&gt;Postal doesn&apos;t auto-suggest a DMARC record – make sure to add this manually per domain.&lt;/p&gt;&lt;h3&gt;Verify DNS Configuration&lt;/h3&gt;&lt;p&gt;If you open the &lt;code&gt;/opt/postal/config/postal.yml&lt;/code&gt; file, you will see DNS records specified under the &lt;code&gt;dns:&lt;/code&gt; section.&lt;/p&gt;&lt;p&gt;This section essentially tells Postal which DNS records you’ve added and how Postal should recognize them.&lt;/p&gt;&lt;p&gt;By default, it includes all the necessary records – &lt;strong&gt;except for the Track Domain&lt;/strong&gt; , which is used for click and open tracking. Since that’s outside the scope of this guide, you can safely comment it out.&lt;/p&gt;&lt;p&gt;✅&lt;/p&gt;&lt;p&gt;Make sure the listed records match your DNS settings. Once they do, your DNS setup is complete.&lt;/p&gt;&lt;p&gt;Postal also supports a couple of optional DNS-related settings you can customize in the same file:&lt;/p&gt;&lt;pre data-language=&quot;yaml&quot;&gt;dns: domain_verify_prefix: custom-verification custom_return_path_prefix: customrp
&lt;/pre&gt;&lt;p&gt;By default, &lt;code&gt;domain_verify_prefix&lt;/code&gt; is set to &lt;code&gt;postal-verification&lt;/code&gt; and &lt;code&gt;custom_return_path_prefix&lt;/code&gt; to &lt;code&gt;psrp&lt;/code&gt;. You can personalize these values to reflect your domain or brand.&lt;/p&gt;&lt;h2&gt;Starting Postal&lt;/h2&gt;&lt;p&gt;With everything configured, it’s time to bring Postal to life.&lt;/p&gt;&lt;p&gt;First, initialize the database and create your admin user:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo postal initialize sudo postal make-user
&lt;/pre&gt;&lt;p&gt;Once that’s done, start Postal:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo postal start
&lt;/pre&gt;&lt;p&gt;This command launches all the necessary components using Docker containers.&lt;/p&gt;&lt;p&gt;You can verify that everything is running smoothly by checking the service status:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo postal status
&lt;/pre&gt;&lt;p&gt;If you run the &lt;code&gt;sudo docker ps&lt;/code&gt; command, you’ll notice several new containers up and running – these are the components that power your Postal installation.&lt;/p&gt;&lt;h2&gt;Web Access with Caddy&lt;/h2&gt;&lt;p&gt;To access Postal’s web interface, you’ll need to set up a web proxy.&lt;/p&gt;&lt;p&gt;You can use any reverse proxy you&apos;re comfortable with, but in this guide, we’ll use &lt;strong&gt;Caddy&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;It is a lightweight, modern web server that’s easy to configure and automatically handles SSL certificates via Let&apos;s Encrypt.&lt;/p&gt;&lt;p&gt;Run the following command to start the Caddy container:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo docker run -d \ \--name postal-caddy \ \--restart always \ \--network host \ -v /opt/postal/config/Caddyfile:/etc/caddy/Caddyfile \ -v /opt/postal/caddy-data:/data \ caddy
&lt;/pre&gt;&lt;p&gt;Once it’s running, Caddy will automatically request and install an SSL certificate for your domain.&lt;/p&gt;&lt;p&gt;You should now be able to access the Postal web interface using your hostname and log in with the admin user you created earlier.&lt;/p&gt;&lt;h2&gt;Securing the Web Interface&lt;/h2&gt;&lt;p&gt;Someone asked about having a kind of 2FA for the web interface, since Postal doesn&apos;t provide any 2FA feature for now, and brought up a really good point.&lt;/p&gt;&lt;p&gt;You can restrict access to ports 80 and 443 to a static IP (like a VPN), but this doesn&apos;t work for everyone.&lt;/p&gt;&lt;p&gt;So, to add an extra layer of security to the normal username and password authentication method that Postal provides, we can enable &lt;strong&gt;Basic Authentication&lt;/strong&gt;  through Caddy, which is easy to configure.&lt;/p&gt;&lt;p&gt;Inside the &lt;code&gt;/opt/postal/config/&lt;/code&gt; directory, you&apos;ll find the &lt;code&gt;Caddyfile&lt;/code&gt; file. Open it and add the following, so the configuration looks like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;mailout.examle.com { reverse_proxy 127.0.0.1:5000 basicauth { username $2a$14$dp6WI0ldDiY/lFUL5I7q7ug/29DbwHYe5oFO6Z6mN6SRDIz/1/lgK } }
&lt;/pre&gt;&lt;p&gt;Replace &lt;code&gt;username&lt;/code&gt; with the username you want to use, and what follows the username is the password, but hashed using &lt;strong&gt;bcrypt&lt;/strong&gt; , which is what Caddy uses.&lt;/p&gt;&lt;p&gt;While the &lt;code&gt;caddy hash-password&lt;/code&gt; command works normally, it won’t work in this case since we run Caddy in a container, and the command is not available. As a quick alternative, you can use this &lt;a href=&quot;https://bcrypt-generator.com/?ref=ivansalloum.com&quot;&gt;online tool&lt;/a&gt; to hash your password.&lt;/p&gt;&lt;p&gt;After generating the hash, replace the hash in the example above with your own, then save and close the file.&lt;/p&gt;&lt;p&gt;Now, run the following two commands to stop and start Caddy again:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo docker stop postal-caddy sudo docker start postal-caddy
&lt;/pre&gt;&lt;p&gt;Finally, try to access the web interface, and you should not be able to do so unless you enter the username and password you just added to the &lt;code&gt;Caddyfile&lt;/code&gt; file.&lt;/p&gt;&lt;p&gt;✅&lt;/p&gt;&lt;p&gt;I hope this helps solve the issue of not having 2FA. Thanks to &lt;strong&gt;Martin Kurz&lt;/strong&gt;  for raising this point!&lt;/p&gt;&lt;h2&gt;Time to Mail It!&lt;/h2&gt;&lt;p&gt;With Postal installed and running, it’s time to test your setup.&lt;/p&gt;&lt;p&gt;Start by creating an organization. Give it a name and click &lt;strong&gt;Create Organization&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Once inside:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Go to the &lt;strong&gt;Settings&lt;/strong&gt; tab&lt;/li&gt;&lt;li&gt;Update the &lt;strong&gt;timezone&lt;/strong&gt; to match your local time (this keeps log timestamps accurate)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Simple but important.&lt;/p&gt;&lt;h3&gt;A Quick Note on Domains&lt;/h3&gt;&lt;p&gt;Before adding your first mail server, there&apos;s something &lt;strong&gt;important&lt;/strong&gt; to note.&lt;/p&gt;&lt;p&gt;You can add a domain:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;At the organization level&lt;/strong&gt; : applies to all mail servers in that organization.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Inside a specific mail server&lt;/strong&gt; : applies only to that one server.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;✅&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Best practice:&lt;/strong&gt; Add domains inside each mail server. This keeps things organized and avoids DNS conflicts.&lt;/p&gt;&lt;p&gt;DNS can get complicated and busy when the domain is linked to all mail servers created within the organization.&lt;em&gt;This is something I was advised by a member of the Postal team on GitHub.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;If that ever changes and I find a good use case for organization-level domains, I’ll &lt;strong&gt;update&lt;/strong&gt; this guide. But for now, let’s keep it tidy.&lt;/p&gt;&lt;h3&gt;Create a Mail Server&lt;/h3&gt;&lt;p&gt;Go ahead and create your first mail server. When you do:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Set the Mode to &lt;code&gt;Live&lt;/code&gt;&lt;/li&gt;&lt;li&gt;Once it’s created, head to the &lt;strong&gt;Domains&lt;/strong&gt; tab&lt;/li&gt;&lt;li&gt;Add your sending domain (usually the same one you used to set up Postal)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I always add my main domain here and set up a &lt;strong&gt;route&lt;/strong&gt; for &lt;code&gt;postmaster@example.com&lt;/code&gt; – that way, I can receive &lt;strong&gt;DMARC reports&lt;/strong&gt; as mentioned earlier in the DNS section.&lt;/p&gt;&lt;p&gt;Inside your mail server, go to &lt;strong&gt;Settings&lt;/strong&gt; → &lt;strong&gt;Server Settings&lt;/strong&gt; , and you’ll find a field called &lt;strong&gt;POSTMASTER&lt;/strong&gt; :&lt;/p&gt;&lt;ul&gt;&lt;li&gt;This is the contact address that shows up in bounce messages if someone sends email to a nonexistent address.&lt;/li&gt;&lt;li&gt;It defaults to &lt;code&gt;postmaster@example.com&lt;/code&gt;, and that’s exactly what most servers expect – so I recommend keeping it as is.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;For example, if someone tries to email an address you haven’t set a route for, it will bounce, and the bounce message will mention the &lt;strong&gt;POSTMASTER&lt;/strong&gt;  address as the contact point.&lt;/p&gt;&lt;p&gt;👆&lt;/p&gt;&lt;p&gt;What mentioned above is only relevant if you want Postal to handle &lt;strong&gt;incoming emails&lt;/strong&gt; and forward them to another address, like your &lt;strong&gt;Gmail&lt;/strong&gt; , using &lt;strong&gt;routes&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;That brings us to the &lt;strong&gt;MX record&lt;/strong&gt; :&lt;/p&gt;&lt;ul&gt;&lt;li&gt;If you&apos;re &lt;em&gt;not&lt;/em&gt; using Postal to receive mail: you can skip adding the MX record Postal suggests when adding a new domain.&lt;/li&gt;&lt;li&gt;If you&apos;re &lt;em&gt;not&lt;/em&gt; using another service for incoming mail: &lt;strong&gt;I still recommend&lt;/strong&gt; adding the MX record.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;It helps make your sending email address appear more trustworthy and legitimate.&lt;/p&gt;&lt;h3&gt;Send a Test Email&lt;/h3&gt;&lt;p&gt;Let’s make sure it actually works.&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Go to the &lt;strong&gt;Messages&lt;/strong&gt; tab → &lt;strong&gt;Send Message&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;In the &lt;strong&gt;FROM&lt;/strong&gt; field, use something like &lt;code&gt;hi@example.com&lt;/code&gt; (make sure that domain has been added)&lt;/li&gt;&lt;li&gt;For the &lt;strong&gt;TO&lt;/strong&gt; field, use a spam-checking tool like &lt;a href=&quot;https://www.mail-tester.com&quot;&gt;mail-tester.com&lt;/a&gt;&lt;ol&gt;&lt;li&gt;It’ll give you a temporary email address&lt;/li&gt;&lt;li&gt;Paste that into the &lt;strong&gt;TO&lt;/strong&gt; field and click &lt;strong&gt;Send Message&lt;/strong&gt;&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Then head back to the test tool and check your score.&lt;/p&gt;&lt;p&gt;🥳&lt;/p&gt;&lt;p&gt;I got a &lt;strong&gt;10/10&lt;/strong&gt;. If your score is lower, don’t worry – the tool gives you a breakdown of what went wrong and how to fix it.&lt;/p&gt;&lt;h2&gt;Configuring Routes&lt;/h2&gt;&lt;p&gt;It&apos;s time to set up &lt;strong&gt;routes&lt;/strong&gt;  so you can receive incoming mail.&lt;/p&gt;&lt;p&gt;Inside your mail server, go to the &lt;strong&gt;Routing&lt;/strong&gt; tab. You’ll see three endpoint types available:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;HTTP&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;SMTP&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Address&lt;/strong&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;For now, we’ll focus on the &lt;strong&gt;Address&lt;/strong&gt; endpoint – it’s the simplest and most practical option for forwarding emails to your inbox.&lt;/p&gt;&lt;p&gt;🧪&lt;/p&gt;&lt;p&gt;Feel free to experiment with HTTP or SMTP endpoints later. If you discover something cool, share it in the discussion section at the end. I may update this guide after deeper testing.&lt;/p&gt;&lt;p&gt;Create an address endpoint with the email address you&apos;d like incoming emails forwarded to – this can be your personal or business inbox.&lt;/p&gt;&lt;h3&gt;Create a Route&lt;/h3&gt;&lt;p&gt;Now go to the &lt;strong&gt;Routes&lt;/strong&gt; sub-tab and click &lt;strong&gt;Add your first route&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Fill in the following fields:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Name&lt;/strong&gt; : The email address you want to receive emails at (e.g. &lt;code&gt;postmaster&lt;/code&gt;). If you&apos;ve already added your domain, just select it from the dropdown.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Endpoint&lt;/strong&gt; : Select the address endpoint you just created.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Spam Mode&lt;/strong&gt; : Choose your preferred option (we’ll configure spam filtering later).&lt;/li&gt;&lt;li&gt;Click &lt;strong&gt;Create Route&lt;/strong&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;&lt;code&gt;postmaster@yourdomain.com&lt;/code&gt; is useful for receiving &lt;strong&gt;DMARC reports&lt;/strong&gt; and &lt;strong&gt;bounce notifications&lt;/strong&gt;.&lt;/p&gt;&lt;h3&gt;Test Your Route&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Try sending an email&lt;/strong&gt; to the address you configured – e.g. &lt;code&gt;postmaster@example.com&lt;/code&gt; and check if it lands in the inbox of the address you configured as the endpoint.&lt;/p&gt;&lt;p&gt;You can also view incoming mail in Postal:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Go to the &lt;strong&gt;Messages&lt;/strong&gt; tab → &lt;strong&gt;Incoming Messages&lt;/strong&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;If you open a mail, you’ll first see a summary – like whether it&apos;s marked as spam.&lt;/p&gt;&lt;h3&gt;A Quick Security Check&lt;/h3&gt;&lt;p&gt;Now head to the &lt;strong&gt;Outgoing Messages&lt;/strong&gt; tab and open any mail you’ve sent.&lt;/p&gt;&lt;p&gt;In the &lt;strong&gt;Properties&lt;/strong&gt; tab, you’ll see delivery info – like whether it was sent over a secure (SSL) connection.&lt;/p&gt;&lt;p&gt;But here&apos;s the catch: &lt;strong&gt;incoming&lt;/strong&gt; mail &lt;em&gt;don’t&lt;/em&gt; show this same detail.&lt;/p&gt;&lt;p&gt;😬&lt;/p&gt;&lt;p&gt;That means your server isn’t receiving mail over a secure connection yet.&lt;/p&gt;&lt;p&gt;We’ll fix that next.&lt;/p&gt;&lt;h2&gt;SMTP TLS&lt;/h2&gt;&lt;p&gt;By default, Postal&lt;strong&gt;does not have TLS enabled for incoming mail&lt;/strong&gt; , which means your server isn’t currently receiving emails securely.&lt;/p&gt;&lt;p&gt;You can confirm this using the &lt;a href=&quot;https://www.checktls.com/TestReceiver&quot;&gt;&lt;strong&gt;CheckTLS&lt;/strong&gt;&lt;/a&gt; tool. Just enter your domain in the &lt;strong&gt;eMail Target&lt;/strong&gt;  field and click the &lt;strong&gt;Run Test&lt;/strong&gt; button.&lt;/p&gt;&lt;p&gt;The test should return an error like:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;TLS is not an option on this server
&lt;/pre&gt;&lt;p&gt;You can also use this command to confirm that no SSL is configured and TLS isn’t working:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;openssl s_client -connect mailout.example.com:25 -starttls smtp
&lt;/pre&gt;&lt;p&gt;This command connects to your mail server on port 25 and tries to upgrade the connection to a secure one using TLS.&lt;/p&gt;&lt;p&gt;If TLS isn’t enabled yet, you’ll see an error like:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;Didn&apos;t find STARTTLS in server response, trying anyway...
&lt;/pre&gt;&lt;p&gt;This output means your server isn’t advertising TLS as an option, so incoming connections can’t be encrypted – exactly what we’re trying to fix next.&lt;/p&gt;&lt;h3&gt;Why This Happens&lt;/h3&gt;&lt;p&gt;I spent three days debugging this exact issue, only to realize it was something simple – and &lt;strong&gt;poorly documented in Postal’s official docs&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Here’s what you need to know:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;When &lt;strong&gt;sending&lt;/strong&gt; email, Postal doesn’t need an SSL certificate – the &lt;strong&gt;receiving server&lt;/strong&gt; handles encryption.&lt;/li&gt;&lt;li&gt;But when &lt;strong&gt;receiving&lt;/strong&gt; email, your Postal server &lt;em&gt;is&lt;/em&gt; the receiving server – so &lt;strong&gt;you&lt;/strong&gt; must provide a certificate to support TLS.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;That’s why outgoing emails show &lt;strong&gt;Received over an SSL connection&lt;/strong&gt; in the &lt;strong&gt;Properties&lt;/strong&gt; tab, but incoming ones don’t.&lt;/p&gt;&lt;p&gt;When using routes to receive emails, Postal first receives the message, then forwards it to the address endpoint you specified. This forwarding process is encrypted using an SSL certificate, because – as mentioned above – during forwarding, &lt;strong&gt;Postal acts as the sending server&lt;/strong&gt; , and the receiving server (e.g. Gmail) handles the encryption. However, the &lt;strong&gt;initial connection between the original sender and Postal is not secure&lt;/strong&gt; unless SMTP TLS is enabled.&lt;/p&gt;&lt;p&gt;Also important: without TLS, &lt;strong&gt;apps like WordPress won’t be able to submit email to Postal securely&lt;/strong&gt; – so enabling it is strongly recommended, even if you&apos;re not receiving mail directly. Because WordPress first submits the email to Postal, which then forwards it to the final recipient.&lt;/p&gt;&lt;h3&gt;Issue an SSL Certificate&lt;/h3&gt;&lt;p&gt;We need an SSL certificate for both the hostname and the MX record. To obtain one, we’ll use &lt;strong&gt;Certbot&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Start by installing it:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt install certbot
&lt;/pre&gt;&lt;p&gt;When issuing a certificate, Certbot needs to use port 80 for domain verification. However, that port is currently in use by the &lt;strong&gt;Caddy&lt;/strong&gt;  web server, which is handling web traffic for Postal&apos;s web interface.&lt;/p&gt;&lt;p&gt;To work around this, we’ll temporarily stop the Caddy container, issue the certificate, and then start the container again – all in one command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo docker stop postal-caddy &amp;amp;&amp;amp; sudo certbot certonly --standalone -d mailout.example.com -d mx.mailout.example.com --agree-tos --email postmaster@example.com --non-interactive &amp;amp;&amp;amp; sudo docker start postal-caddy
&lt;/pre&gt;&lt;p&gt;Once that completes, your certificate will be successfully issued.&lt;/p&gt;&lt;p&gt;✅&lt;/p&gt;&lt;p&gt;You can check the certificate and obtain the certificate and key paths along with other relevant details by running the &lt;code&gt;sudo certbot certificates&lt;/code&gt; command.&lt;/p&gt;&lt;h3&gt;Configure Postal to Use TLS&lt;/h3&gt;&lt;p&gt;Now that we have our SSL certificate, we need to copy it to the &lt;code&gt;/opt/postal/config/&lt;/code&gt; directory and rename its certificate and key files for Postal to use:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo cp /etc/letsencrypt/live/mailout.example.com/fullchain.pem /opt/postal/config/smtp.crt sudo cp /etc/letsencrypt/live/mailout.example.com/privkey.pem /opt/postal/config/smtp.key
&lt;/pre&gt;&lt;p&gt;Next, change the file permissions for the certificate files:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo chmod 644 /opt/postal/config/smtp.*
&lt;/pre&gt;&lt;p&gt;Then, open the &lt;code&gt;/opt/postal/config/postal.yml&lt;/code&gt; file and add the following:&lt;/p&gt;&lt;pre data-language=&quot;yaml&quot;&gt;smtp_server: tls_enabled: true tls_certificate_path: /config/smtp.crt tls_private_key_path: /config/smtp.key
&lt;/pre&gt;&lt;p&gt;Finally, restart Postal to apply the changes:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo postal restart
&lt;/pre&gt;&lt;p&gt;To verify that SMTP TLS is working, run this command again:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;openssl s_client -connect mailout.example.com:25 -starttls smtp
&lt;/pre&gt;&lt;p&gt;You should now see a successful TLS connection.&lt;/p&gt;&lt;p&gt;Next, run the &lt;a href=&quot;https://www.checktls.com/TestReceiver&quot;&gt;&lt;strong&gt;CheckTLS&lt;/strong&gt;&lt;/a&gt; test again by entering your domain as the email target and clicking &lt;strong&gt;Run Test&lt;/strong&gt;. Check the results to confirm that TLS is properly enabled and functioning.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;Try your route again, and now you should see an indicator under the &lt;strong&gt;Properties&lt;/strong&gt;  tab that the email was &lt;strong&gt;received over an SSL connection&lt;/strong&gt;.&lt;/p&gt;&lt;h3&gt;Handle Auto-Renewal with Certbot&lt;/h3&gt;&lt;p&gt;Since Certbot uses &lt;strong&gt;port 80&lt;/strong&gt; and Caddy is running, the auto-renewal will fail unless you automate stopping/starting Caddy.&lt;/p&gt;&lt;p&gt;If you&apos;re curious, the configuration for auto-renewal can be found inside the &lt;code&gt;/etc/letsencrypt/renewal&lt;/code&gt; directory.&lt;/p&gt;&lt;p&gt;Certbot handles auto-renewals using systemd timers instead of cron jobs. This timer run twice per day and can be checked using the following command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo systemctl list-timers
&lt;/pre&gt;&lt;p&gt;You can examine the content of the timer with the following command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo cat /lib/systemd/system/certbot.timer
&lt;/pre&gt;&lt;p&gt;Now, to solve this problem, we can use something called &lt;strong&gt;Renewal Hooks&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Inside the &lt;code&gt;/etc/letsencrypt/renewal-hooks&lt;/code&gt; directory, you’ll find three different subdirectories:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;deploy&lt;/code&gt;&lt;/strong&gt; : This directory is for scripts that run &lt;strong&gt;after&lt;/strong&gt;  a certificate is successfully &lt;strong&gt;renewed&lt;/strong&gt;.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;post&lt;/code&gt;&lt;/strong&gt; : Scripts in this directory run &lt;strong&gt;after&lt;/strong&gt;  the certificate is &lt;strong&gt;renewed and deployed&lt;/strong&gt;.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;pre&lt;/code&gt;&lt;/strong&gt; : These are scripts that run &lt;strong&gt;before&lt;/strong&gt;  Certbot &lt;strong&gt;tries to renew&lt;/strong&gt; the certificate.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;✅&lt;/p&gt;&lt;p&gt;We use &lt;code&gt;pre&lt;/code&gt; to stop the Caddy container, &lt;code&gt;deploy&lt;/code&gt; to copy the renewed certificate files, change their permissions, and restart Postal, and &lt;code&gt;post&lt;/code&gt; for starting the Caddy container again.&lt;/p&gt;&lt;p&gt;Inside the &lt;code&gt;pre&lt;/code&gt; directory, create a script called &lt;code&gt;stop_caddy.sh&lt;/code&gt; with the following content:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;#!/bin/bash docker stop postal-caddy
&lt;/pre&gt;&lt;p&gt;Inside the &lt;code&gt;deploy&lt;/code&gt; directory, create a script called &lt;code&gt;copy_ssl_cert.sh&lt;/code&gt; with the following content:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;#!/bin/bash # Copy new certificate files cp /etc/letsencrypt/live/mailout.example.com/fullchain.pem /opt/postal/config/smtp.crt cp /etc/letsencrypt/live/mailout.example.com/privkey.pem /opt/postal/config/smtp.key # Set correct permissions chmod 644 /opt/postal/config/smtp.* # Restart Postal postal restart
&lt;/pre&gt;&lt;p&gt;Finally, inside the &lt;code&gt;post&lt;/code&gt; directory, create a script called &lt;code&gt;start_caddy.sh&lt;/code&gt; with the following content:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;#!/bin/bash docker start postal-caddy
&lt;/pre&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;Don&apos;t forget to give each script execute permissions but make sure to restrict them to root only.&lt;/p&gt;&lt;p&gt;You can test if everything is working by first checking the expiry date using the following command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo certbot certificates
&lt;/pre&gt;&lt;p&gt;This should match the expiry date of the copied certificate. To check the expiry date of the certificate in Postal, run:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;openssl x509 -enddate -noout -in /opt/postal/config/smtp.crt
&lt;/pre&gt;&lt;p&gt;Then, force the renewal using the following command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo certbot renew --force-renewal
&lt;/pre&gt;&lt;p&gt;Lastly, check the expiry date again to make sure it matches. If it does, the setup is working correctly.&lt;/p&gt;&lt;p&gt;❗&lt;/p&gt;&lt;p&gt;Don’t test this on the same day you issued the SSL certificate. Wait a few days or you won’t see a change in expiry, even if the copy succeeded.&lt;/p&gt;&lt;h2&gt;Installing SpamAssassin&lt;/h2&gt;&lt;p&gt;For filtering spam emails, we will install and use &lt;strong&gt;SpamAssassin&lt;/strong&gt; , a powerful and widely-used spam filtering tool.&lt;/p&gt;&lt;p&gt;By default, Postal will communicate with SpamAssassin&apos;s &lt;code&gt;spamd&lt;/code&gt; service using a TCP socket connection (port 783).&lt;/p&gt;&lt;p&gt;You’ll need to install SpamAssassin on your server and enable it within Postal to begin filtering spam.&lt;/p&gt;&lt;p&gt;For installing SpamAssassin, use the following command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt install spamassassin
&lt;/pre&gt;&lt;p&gt;Next, you will need to enable the SpamAssassin timer:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo systemctl enable --now spamassassin-maintenance.timer
&lt;/pre&gt;&lt;p&gt;It is used to update the spam rules automatically.&lt;/p&gt;&lt;p&gt;To enable spam checking, we need to add the following to the end of the &lt;code&gt;/opt/postal/config/postal.yml&lt;/code&gt; file:&lt;/p&gt;&lt;pre data-language=&quot;yaml&quot;&gt;spamd: enabled: true host: 127.0.0.1 port: 783
&lt;/pre&gt;&lt;p&gt;Finally, restart Postal to apply your changes:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo postal restart
&lt;/pre&gt;&lt;p&gt;Inside Postal web interface:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Go to your &lt;strong&gt;Organization Settings&lt;/strong&gt; → &lt;strong&gt;Spam&lt;/strong&gt; tab&lt;/li&gt;&lt;li&gt;You’ll see the &lt;strong&gt;Spam Threshold&lt;/strong&gt; setting (default: 5)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;A score &lt;em&gt;above&lt;/em&gt; the threshold triggers the &lt;strong&gt;SPAM MODE&lt;/strong&gt; set in your route config. A score &lt;em&gt;below&lt;/em&gt; the threshold means the message is treated as non-spam.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;Remember when you created your first route and selected a &lt;strong&gt;SPAM MODE&lt;/strong&gt;? That setting now takes effect.&lt;/p&gt;&lt;p&gt;There’s also a &lt;strong&gt;Spam Failure Threshold&lt;/strong&gt; (default: 20). If a mail scores &lt;em&gt;above&lt;/em&gt; 20, it is immediately dropped.&lt;/p&gt;&lt;p&gt;❗&lt;/p&gt;&lt;p&gt;By default, Postal doesn&apos;t filter outgoing emails, only incoming ones, after you install and enable SpamAssassin.&lt;/p&gt;&lt;h3&gt;What Do Spam Scores Mean?&lt;/h3&gt;&lt;p&gt;Now, let&apos;s talk about what spam scores are before we enable spam checking for outgoing emails.&lt;/p&gt;&lt;p&gt;SpamAssassin works by analyzing an email and giving it a spam score.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The lower the score, the better the chances of the email getting delivered successfully.&lt;/li&gt;&lt;li&gt;Conversely, the higher the score, the higher the likelihood the message is labeled spam/junk.&lt;/li&gt;&lt;li&gt;A score below 5 is considered decent, while anything above 5 indicates a higher chance of being filtered out.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;SpamAssassin uses over 700 tests and a variety of analytical techniques to detect spam. Each attribute it checks contributes to the overall score.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Negative scores&lt;/strong&gt; indicate the email is less likely to be spam, while positive scores suggest possible spam.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;A score of 0 is neutral, meaning the factor has little impact.&lt;/p&gt;&lt;p&gt;When setting up SpamAssassin, the score threshold can be adjusted, with 5 being the default.&lt;/p&gt;&lt;p&gt;To ensure successful email delivery, aim for a spam score below 5, with scores between 0-2 being optimal. Negative scores are even better, as they indicate the email is very unlikely to be marked as spam, though they can be difficult to achieve.&lt;/p&gt;&lt;p&gt;I recommend starting with a threshold of 5 and gradually lowering it based on your spam checking history. For instance, if you notice your outgoing emails consistently have a score of 3, consider lowering your threshold to 4.&lt;/p&gt;&lt;p&gt;Over time, work on improving your score and address the factors contributing to higher scores.&lt;/p&gt;&lt;h3&gt;Enable Spam Checking for Outgoing Mail&lt;/h3&gt;&lt;p&gt;To filter outgoing mail:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Go to your mail server&lt;strong&gt;Settings&lt;/strong&gt; → &lt;strong&gt;Advanced Settings&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;Find the &lt;strong&gt;Outbound Spam Threshold&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;Set it to &lt;code&gt;5&lt;/code&gt;&lt;/li&gt;&lt;li&gt;Click &lt;strong&gt;Save Server&lt;/strong&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Now outgoing mail will also be checked using SpamAssassin before it’s sent out.&lt;/p&gt;&lt;p&gt;✅&lt;/p&gt;&lt;p&gt;This is helpful for catching mistakes early, like misconfigured headers, bad links, or overly promotional content.&lt;/p&gt;&lt;h2&gt;System Email Configuration&lt;/h2&gt;&lt;p&gt;To enable system-generated emails (such as password reset links) in Postal, you&apos;ll need to configure the &lt;code&gt;smtp:&lt;/code&gt; section in the Postal configuration file.&lt;/p&gt;&lt;p&gt;Open the &lt;code&gt;/opt/postal/config/postal.yml&lt;/code&gt; file and scroll to the bottom of it, and you’ll find the &lt;code&gt;smtp:&lt;/code&gt; section. This is where you&apos;ll define the SMTP server Postal should use for sending system emails.&lt;/p&gt;&lt;p&gt;Although you could use an external SMTP provider, we’re going to use &lt;strong&gt;Postal&lt;/strong&gt; itself to keep everything self-hosted.&lt;/p&gt;&lt;p&gt;Inside your Postal web interface:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Go to your &lt;strong&gt;Mail Server&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;Click the &lt;strong&gt;Credentials&lt;/strong&gt; tab&lt;/li&gt;&lt;li&gt;Click &lt;strong&gt;Add Credential&lt;/strong&gt;&lt;ol&gt;&lt;li&gt;Leave the &lt;strong&gt;TYPE&lt;/strong&gt; set to &lt;strong&gt;SMTP&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;Name it something like &lt;code&gt;System&lt;/code&gt; to keep things organized&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;li&gt;Click &lt;strong&gt;Create Credential&lt;/strong&gt; – a password will be generated automatically&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Now go to the &lt;strong&gt;Help&lt;/strong&gt; tab → &lt;strong&gt;Sending Email&lt;/strong&gt; sub-tab. You’ll see the SMTP settings needed to complete the &lt;code&gt;smtp:&lt;/code&gt; section in your &lt;code&gt;postal.yml&lt;/code&gt; file.&lt;/p&gt;&lt;p&gt;Back in &lt;code&gt;/opt/postal/config/postal.yml&lt;/code&gt;, fill out the &lt;code&gt;smtp:&lt;/code&gt; section with the info you just retrieved. For the &lt;code&gt;from_address&lt;/code&gt;, it&apos;s a good idea to use something like &lt;code&gt;system@example.com&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Make sure to also change the port from &lt;code&gt;2525&lt;/code&gt; to &lt;code&gt;25&lt;/code&gt;, since our SMTP server uses port 25 for sending emails.&lt;/p&gt;&lt;p&gt;Finally, restart Postal to apply your changes:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo postal restart
&lt;/pre&gt;&lt;p&gt;Now, try logging out of the Postal web interface and resetting your password. You should receive an email with a link to set a new one.&lt;/p&gt;&lt;h2&gt;Enabling UFW Firewall&lt;/h2&gt;&lt;p&gt;We finally want to enable the UFW firewall on our server, and I&apos;ve left this to the end so you can check if everything still works perfectly after enabling it.&lt;/p&gt;&lt;p&gt;We only need to allow incoming connections, as outgoing traffic is allowed by default with UFW.&lt;/p&gt;&lt;p&gt;To configure this, we need to allow incoming traffic on the following ports: 22 (SSH), 25 (SMTP), 80 (HTTP), and 443 (HTTPS).&lt;/p&gt;&lt;p&gt;Use the following commands to allow these connections and enable the firewall:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw limit 22/tcp sudo ufw allow 25/tcp sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw enable
&lt;/pre&gt;&lt;p&gt;Sometimes, UFW may require a reboot to work correctly. So, if you encounter any issues, try rebooting the server.&lt;/p&gt;&lt;h2&gt;Disaster Recovery&lt;/h2&gt;&lt;p&gt;The final step in setting up your SMTP server is planning for disaster recovery. Think about what you’d do if something goes wrong – like a server breach, a misconfiguration, a bad update, or even something out of your control, like a fire in your provider’s data center. You need a way to quickly bring everything back online.&lt;/p&gt;&lt;p&gt;We already have our primary IPs secured – they stay with us no matter what, which is great. Now, we need a reliable way to restore the server and assign those same IPs to it. The best way to do this is by using &lt;strong&gt;snapshots&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;In Hetzner, you can go to your server’s &lt;strong&gt;Snapshots&lt;/strong&gt;  tab and create a snapshot, which is basically a copy of your server&apos;s disk at that moment.&lt;/p&gt;&lt;p&gt;If something happens, you can spin up a new server from that snapshot, assign the primary IPs, and it should work immediately – no need to change any DNS settings.&lt;/p&gt;&lt;p&gt;Make sure to test this process a few times so you’re comfortable with it.&lt;/p&gt;&lt;p&gt;Snapshots are created manually. If you want automatic backups, you can enable Hetzner&apos;s backup feature. It costs 20% extra, but it keeps daily backups for a week. Once the week is over, old backups are replaced by new ones. You can convert any of these backups into a snapshot, which you can then use to restore your server.&lt;/p&gt;&lt;p&gt;If you delete the server, all its backups are deleted too. Always convert at least one backup into a snapshot before deleting anything. Also, enable &lt;strong&gt;deletion protection&lt;/strong&gt;  on your server to avoid losing everything by mistake.&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;Congratulations on reaching the end!&lt;/p&gt;&lt;p&gt;Remember to monitor your setup, fine-tune your spam settings, and keep your SSL certificate renewed.&lt;/p&gt;&lt;p&gt;And just as important – &lt;strong&gt;test your disaster recovery plan regularly&lt;/strong&gt;  to make sure everything still works as expected. It’s better to catch issues during a test than during a real outage.&lt;/p&gt;&lt;p&gt;If you run into any issues or need further help, feel free to revisit this guide or &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;reach out&lt;/a&gt; for assistance. Happy emailing!&lt;/p&gt;&lt;p&gt;If you found value in this guide or have any questions or feedback, please don&apos;t hesitate to share your thoughts in the &lt;strong&gt;discussion&lt;/strong&gt; section.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Servers</category></item><item><title>Stop Using APT! Switch to Nala</title><link>https://ivansalloum.com/stop-using-apt-switch-to-nala/</link><guid isPermaLink="true">https://ivansalloum.com/stop-using-apt-switch-to-nala/</guid><description>A faster, cleaner alternative to APT for managing packages on Debian-based systems.</description><pubDate>Thu, 20 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;I recently started using the Nala package manager, and I was immediately impressed.&lt;/p&gt;&lt;p&gt;As a longtime Ubuntu fan, I was used to managing packages with APT, but Nala completely changed the game for me.&lt;/p&gt;&lt;p&gt;It acts as a new frontend for APT – just as APT itself is a frontend for the underlying package manager – but with a much cleaner, more readable, and user-friendly experience.&lt;/p&gt;&lt;p&gt;The way it simplifies package management while staying fully compatible with APT, all while adding new functionalities, makes it a must-try for any Linux user.&lt;/p&gt;&lt;h2&gt;Why Nala&lt;/h2&gt;&lt;p&gt;For newer Linux users, APT can often be confusing when it comes to installing or upgrading packages.&lt;/p&gt;&lt;p&gt;Even for me, someone who has been using Linux – specifically Ubuntu – every day for a long time, I find APT&apos;s output frustrating and unreadable.&lt;/p&gt;&lt;p&gt;Nala improves this experience by eliminating unnecessary messages, enhancing the formatting of package details, and using color coding to clearly indicate the actions being taken – whether it&apos;s installing, removing, or upgrading a package.&lt;/p&gt;&lt;p&gt;But that&apos;s not all. Nala also brings exciting features like &lt;strong&gt;parallel downloads&lt;/strong&gt; , speeding up package installations, and &lt;strong&gt;Nala Fetch&lt;/strong&gt; , which lets you pick the fastest mirrors.&lt;/p&gt;&lt;p&gt;It’s just great!&lt;/p&gt;&lt;h2&gt;Installation&lt;/h2&gt;&lt;p&gt;We install Nala using APT. Yeah, you heard that right – APT!&lt;/p&gt;&lt;p&gt;When I first installed it, I couldn’t help but wonder, &lt;em&gt;Would APT be sad?&lt;/em&gt; It knows I&apos;m about to replace it... but don&apos;t worry, APT’s still here to stay.&lt;/p&gt;&lt;p&gt;Use the following command to install Nala:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt install nala
&lt;/pre&gt;&lt;p&gt;Once Nala was installed, my next step was to explore its functionality.&lt;/p&gt;&lt;p&gt;The first thing I do after installing any new package (especially one I haven&apos;t used before) is check its man page or use the &lt;code&gt;-h&lt;/code&gt; option to see what options are available and get a sense of how to use it.&lt;/p&gt;&lt;p&gt;So, of course, I went ahead and tried the &lt;code&gt;-h&lt;/code&gt; option with Nala, and I was seriously impressed.&lt;/p&gt;&lt;p&gt;See for yourself:&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://ivansalloum.com/content/images/2025/02/nala-s-help-output.png&quot; alt=&quot;Nala&apos;s help output&quot;&gt;Nala&apos;s help output&lt;/p&gt;&lt;p&gt;It is organized, put inside its own box, colored, and tells you exactly how to use it.&lt;/p&gt;&lt;h2&gt;Nala Fetch&lt;/h2&gt;&lt;p&gt;Before you start using Nala, I want to show you one of its best features to speed up package management: the &lt;code&gt;fetch&lt;/code&gt; command.&lt;/p&gt;&lt;p&gt;Just run this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo nala fetch
&lt;/pre&gt;&lt;p&gt;This basically tests all available mirrors, provides you with a list of them, tests the latency, and scores each mirror.&lt;/p&gt;&lt;p&gt;It then lets you pick the fastest ones to use and writes them to the &lt;code&gt;nala-sources.list&lt;/code&gt; file inside the &lt;code&gt;/etc/apt/sources.list.d/&lt;/code&gt; directory.&lt;/p&gt;&lt;p&gt;And that&apos;s it! Now you are using the fastest mirrors.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;At the moment, the &lt;code&gt;fetch&lt;/code&gt; command works only on Debian, Ubuntu, and derivatives still tied to the main repositories.&lt;/p&gt;&lt;h2&gt;Parallel Downloads&lt;/h2&gt;&lt;p&gt;Unlike APT, Nala supports parallel downloads, meaning it can download up to three packages at once from each mirror. This speeds up package installations significantly, especially for many small packages.&lt;/p&gt;&lt;p&gt;Nala also alternates between mirrors, boosting download speeds by choosing the fastest available server. If one mirror fails, Nala automatically switches to the next one, ensuring your downloads continue without interruption.&lt;/p&gt;&lt;p&gt;I love how Nala handles these tasks on its own, without relying on APT for downloading or verification, making it both faster and more reliable.&lt;/p&gt;&lt;h2&gt;Running Updates&lt;/h2&gt;&lt;p&gt;Nala provides commands to update your server, just like APT, but with an additional useful option.&lt;/p&gt;&lt;p&gt;Instead of running &lt;code&gt;apt update&lt;/code&gt; followed by either &lt;code&gt;apt dist-upgrade&lt;/code&gt; or &lt;code&gt;apt full-upgrade&lt;/code&gt;, Nala simplifies the process with a single command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo nala full-upgrade
&lt;/pre&gt;&lt;p&gt;This command updates your package lists and upgrades the server by removing, installing, and upgrading packages – all in one step.&lt;/p&gt;&lt;p&gt;However, if you prefer the traditional APT workflow, Nala still provides equivalent commands.&lt;/p&gt;&lt;p&gt;You can use these two commands to update package lists and upgrade installed packages:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo nala update sudo nala upgrade
&lt;/pre&gt;&lt;p&gt;These work exactly like &lt;code&gt;apt update&lt;/code&gt; and &lt;code&gt;apt upgrade&lt;/code&gt;, giving you the same functionality with Nala’s improved readability.&lt;/p&gt;&lt;h2&gt;History Tracking&lt;/h2&gt;&lt;p&gt;One of the commands Nala provides that I love is the &lt;code&gt;history&lt;/code&gt; command!&lt;/p&gt;&lt;p&gt;It gives me a clear log of past package operations, like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ivan@vm1:~$ nala history ID Command Date and Time Altered Requested-By 1 upgrade base-files bind9-dnsutils bind9-host bind9… 2025-02-20 08:03:23 UTC 65 ivan (1000) 2 full-upgrade 2025-02-20 08:04:41 UTC 3 ivan (1000) 3 install bpytop 2025-02-20 08:50:01 UTC 2 ivan (1000)
&lt;/pre&gt;&lt;p&gt;Here’s what each column means:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;ID:&lt;/strong&gt; The operation number in chronological order.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Command:&lt;/strong&gt; The exact package management command that was run.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Date and Time:&lt;/strong&gt; When the command was executed.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Altered:&lt;/strong&gt; The number of packages that were installed, upgraded, or removed.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Requested-By:&lt;/strong&gt; The user who ran the command.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This makes it easy to track package changes on your server, ensuring you always know what was installed, upgraded, or removed.&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;With its clean and readable output, faster downloads through parallel connections, and convenient features like &lt;strong&gt;Nala Fetch&lt;/strong&gt; and &lt;strong&gt;History Tracking&lt;/strong&gt; , Nala offers a smoother experience compared to APT.&lt;/p&gt;&lt;p&gt;The rest of the commands remain the same as APT – such as &lt;code&gt;install&lt;/code&gt; for installing packages or &lt;code&gt;autoremove&lt;/code&gt; to remove unneeded ones – so there’s little to no learning curve for existing users.&lt;/p&gt;&lt;p&gt;If you found value in this reading or have any questions or feedback, please don&apos;t hesitate to share your thoughts in the &lt;strong&gt;discussion&lt;/strong&gt; section.&lt;/p&gt;&lt;p&gt;Your input is greatly appreciated, and you can also &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;contact me&lt;/a&gt; directly if you prefer.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Servers</category></item><item><title>Real-Time Linux Server Monitoring with dstat</title><link>https://ivansalloum.com/real-time-linux-server-monitoring-with-dstat/</link><guid isPermaLink="true">https://ivansalloum.com/real-time-linux-server-monitoring-with-dstat/</guid><description>Monitor real-time server performance with dstat to track CPU, disk, memory, and network activity.</description><pubDate>Wed, 12 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;Whether you&apos;re debugging an issue or optimizing server resources, real-time data on CPU usage, memory usage, disk activity, and network traffic is crucial.&lt;/p&gt;&lt;p&gt;This is where &lt;code&gt;dstat&lt;/code&gt; shines – a powerful tool that provides detailed, real-time server performance insights.&lt;/p&gt;&lt;p&gt;In this guide, we’ll explore &lt;code&gt;dstat&lt;/code&gt; and how you can use it to monitor and analyze your Linux server.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;_I assume you&apos;re working on a properly set-up Ubuntu server. If not, check out my guide  on &lt;em&gt;&lt;a href=&quot;https://ivansalloum.com/preparing-your-ubuntu-server-for-first-use/&quot;&gt;&lt;em&gt;preparing  Ubuntu servers&lt;/em&gt;&lt;/a&gt; _  to get started.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;What is &lt;code&gt;dstat&lt;/code&gt;?&lt;/h2&gt;&lt;p&gt;&lt;code&gt;dstat&lt;/code&gt; is a real-time performance monitoring tool that provides a comprehensive view of server statistics, including CPU usage, memory usage, disk I/O, and network traffic.&lt;/p&gt;&lt;p&gt;Unlike tools like &lt;code&gt;vmstat&lt;/code&gt;, &lt;code&gt;iostat&lt;/code&gt;, and &lt;code&gt;netstat&lt;/code&gt;, which focus on specific resources, &lt;code&gt;dstat&lt;/code&gt; combines them all into a single command, making performance analysis easier.&lt;/p&gt;&lt;h2&gt;Installation&lt;/h2&gt;&lt;p&gt;&lt;code&gt;dstat&lt;/code&gt; is not installed by default. To install it, use the following command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt install dstat
&lt;/pre&gt;&lt;p&gt;Once installed, you can verify the installation by running:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;dstat --version
&lt;/pre&gt;&lt;p&gt;Now that &lt;code&gt;dstat&lt;/code&gt; is installed, let’s explore how to use it for monitoring your Linux server.&lt;/p&gt;&lt;h2&gt;Basic Usage&lt;/h2&gt;&lt;p&gt;Running &lt;code&gt;dstat&lt;/code&gt; without any options defaults to the &lt;code&gt;-a&lt;/code&gt; (all) option, which is equivalent to:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;dstat -cdngy
&lt;/pre&gt;&lt;p&gt;This command displays real-time statistics for:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;CPU usage&lt;/strong&gt; (&lt;code&gt;-c&lt;/code&gt;)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Disk activity&lt;/strong&gt; (&lt;code&gt;-d&lt;/code&gt;)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Network traffic&lt;/strong&gt; (&lt;code&gt;-n&lt;/code&gt;)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Paging activity&lt;/strong&gt; (&lt;code&gt;-g&lt;/code&gt;)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;System statistics&lt;/strong&gt; (&lt;code&gt;-y&lt;/code&gt;)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;To start monitoring, simply run:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;dstat
&lt;/pre&gt;&lt;p&gt;This will update the server’s performance metrics every second in real time. You can stop it anytime by pressing &lt;strong&gt;Ctrl + C&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Here&apos;s an example of the output:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;\----total-usage---- -dsk/total- -net/total- ---paging-- ---system-- usr sys idl wai stl| read writ| recv send| in out | int csw 0 0 100 0 0| 0 0 |7160B 4953B| 0 0 | 167 145 0 0 100 0 0| 0 0 |1038B 812B| 0 0 | 108 102 0 0 99 0 0| 0 0 | 66B 310B| 0 0 | 33 52 ^C
&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;dstat&lt;/code&gt; output provides a quick overview of key performance metrics to help monitor server activity in real time.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;CPU usage&lt;/strong&gt; (&lt;code&gt;usr sys idl wai stl&lt;/code&gt;) displays user and system CPU usage, idle time, I/O wait time, and stolen CPU cycles, because it is being used by another virtual machine (VM).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Disk activity&lt;/strong&gt; (&lt;code&gt;read writ&lt;/code&gt;) shows the total disk read and write operations.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Network traffic&lt;/strong&gt; (&lt;code&gt;recv send&lt;/code&gt;) indicates the amount of data received and sent over the network.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Paging&lt;/strong&gt; (&lt;code&gt;in out&lt;/code&gt;) represents memory pages swapped in and out.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;System metrics&lt;/strong&gt; (&lt;code&gt;int csw&lt;/code&gt;) track system interrupts and context switches.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Additionally, you can enable time/date output with the &lt;code&gt;-t&lt;/code&gt; option:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;dstat -t
&lt;/pre&gt;&lt;p&gt;This will display the time and date alongside the performance metrics, giving you a timestamp for each data point. You can also use &lt;code&gt;--time-adv&lt;/code&gt; for millisecond precision in the time output.&lt;/p&gt;&lt;h2&gt;Monitoring CPU Usage&lt;/h2&gt;&lt;p&gt;To get detailed insights into CPU performance, &lt;code&gt;dstat&lt;/code&gt; provides options to track overall and per-core usage in real time.&lt;/p&gt;&lt;p&gt;To monitor CPU statistics, use the &lt;code&gt;-c&lt;/code&gt; option:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;dstat -c
&lt;/pre&gt;&lt;p&gt;This will display real-time CPU usage, including user (&lt;code&gt;usr&lt;/code&gt;), system (&lt;code&gt;sys&lt;/code&gt;), idle (&lt;code&gt;idl&lt;/code&gt;), wait (&lt;code&gt;wai&lt;/code&gt;), and stolen (&lt;code&gt;stl&lt;/code&gt;) CPU percentages.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;usr&lt;/code&gt; value represents the CPU usage for running user processes (outside the kernel), while the &lt;code&gt;sys&lt;/code&gt; value represents the CPU usage for running kernel processes. The &lt;code&gt;idl&lt;/code&gt; value indicates the percentage of CPU that is idle. A high &lt;code&gt;wai&lt;/code&gt; value indicates that processes are waiting on disk I/O, and &lt;code&gt;stl&lt;/code&gt; is relevant in virtualized environments, showing CPU cycles stolen by the hypervisor.&lt;/p&gt;&lt;p&gt;If you want to monitor all available CPU cores dynamically, you can use:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;dstat -c -C 0,1,2,3
&lt;/pre&gt;&lt;p&gt;This will display usage for all CPU cores individually, providing a detailed breakdown of CPU activity.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;-c&lt;/code&gt; option will ensure that only CPU performance metrics are shown, while the &lt;code&gt;-C&lt;/code&gt; option allows you to specify individual CPU cores by their numbers. The number &lt;code&gt;0&lt;/code&gt; refers to the first core, as Linux starts counting cores from zero.&lt;/p&gt;&lt;p&gt;You can also use the &lt;code&gt;-f&lt;/code&gt; option for a full display of all available CPU cores, instead of specifying each core individually with the &lt;code&gt;-C&lt;/code&gt; option.&lt;/p&gt;&lt;p&gt;Additionally, the &lt;code&gt;--cpu-use&lt;/code&gt; option focuses only on CPU usage stats and provides a per-core usage breakdown:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;dstat --cpu-use
&lt;/pre&gt;&lt;p&gt;This will show per-core CPU usage in a simplified format, helping you track load distribution across different cores.&lt;/p&gt;&lt;p&gt;To monitor the server load, use the &lt;code&gt;-l&lt;/code&gt; option:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;dstat -l
&lt;/pre&gt;&lt;p&gt;This will display a load average over different time intervals (1 min, 5 mins, and 15 mins).&lt;/p&gt;&lt;p&gt;You can also track process-related statistics using the &lt;code&gt;-p&lt;/code&gt; option:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;dstat -p
&lt;/pre&gt;&lt;p&gt;This displays the number of &lt;strong&gt;runnable&lt;/strong&gt; , &lt;strong&gt;uninterruptible&lt;/strong&gt; , and &lt;strong&gt;newly created&lt;/strong&gt; processes, helping you analyze CPU load and responsiveness.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Runnable&lt;/strong&gt; are processes ready to execute.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Uninterruptible&lt;/strong&gt; are processes stuck waiting for I/O operations to finish. If too many processes remain in this state, it may indicate disk slowness or other bottlenecks.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;For a total count of running processes, use the &lt;code&gt;--proc-count&lt;/code&gt; option:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;dstat --proc-count
&lt;/pre&gt;&lt;p&gt;This displays the number of processes, with &lt;code&gt;scal&lt;/code&gt; showing the runnable processes and &lt;code&gt;tota&lt;/code&gt; showing the total number of processes.&lt;/p&gt;&lt;h2&gt;Monitoring Disk Usage&lt;/h2&gt;&lt;p&gt;To monitor disk activity, &lt;code&gt;dstat&lt;/code&gt; provides options that allow you to track real-time disk read and write operations. You can monitor overall disk usage or focus on specific devices and partitions to analyze disk performance more effectively.&lt;/p&gt;&lt;p&gt;To monitor disk activity in real time, use the &lt;code&gt;-d&lt;/code&gt; option:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;dstat -d
&lt;/pre&gt;&lt;p&gt;This will display the total read and write operations on your disk, helping you track how much data is being transferred.&lt;/p&gt;&lt;p&gt;If you have multiple storage devices or partitions, you can specify which one to monitor by using the &lt;code&gt;-D&lt;/code&gt; option, followed by the device name, like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;dstat -d -D sda
&lt;/pre&gt;&lt;p&gt;This command will display disk activity specifically for the &lt;code&gt;sda&lt;/code&gt; device.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;-f&lt;/code&gt; option works here as well, providing a full display of all disks, so you don&apos;t need to specify them individually.&lt;/p&gt;&lt;p&gt;If you want to go deeper into I/O performance, you can use the &lt;code&gt;-r&lt;/code&gt; option to track the number of read and write requests made to the disk. For example, if the server reads 100 files and writes 50 files in one second, this will show the number of I/O operations being initiated:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;dstat -r
&lt;/pre&gt;&lt;p&gt;This will show the number of read and write requests, helping you analyze I/O activity in detail.&lt;/p&gt;&lt;p&gt;Additionally, the &lt;code&gt;--aio&lt;/code&gt; option tracks &lt;strong&gt;asynchronous I/O operations&lt;/strong&gt; (non-blocking operations). For example, instead of waiting for one read operation to finish before starting the next, asynchronous I/O allows multiple read operations to happen simultaneously, making the disk more efficient:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;dstat --aio
&lt;/pre&gt;&lt;p&gt;These options help you monitor the frequency and efficiency of disk I/O operations in real time.&lt;/p&gt;&lt;h2&gt;Monitoring Memory Usage&lt;/h2&gt;&lt;p&gt;To monitor memory usage in real time, &lt;code&gt;dstat&lt;/code&gt; provides options to track both physical memory (RAM) and swap space.&lt;/p&gt;&lt;p&gt;To monitor memory usage in real time, use the &lt;code&gt;-m&lt;/code&gt; option:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;dstat -m
&lt;/pre&gt;&lt;p&gt;This will display memory usage details, including used, free, buffer, and cache memory.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Buffers&lt;/strong&gt; temporarily store data being read from or written to disk, improving disk I/O performance by reducing direct disk access. &lt;strong&gt;Cache&lt;/strong&gt; holds frequently accessed files and directory data in RAM, allowing for faster retrieval without needing to read from disk repeatedly. The server dynamically manages this memory, freeing it when needed for active processes.&lt;/p&gt;&lt;p&gt;To monitor swap space usage, use the &lt;code&gt;-s&lt;/code&gt; option:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;dstat -s
&lt;/pre&gt;&lt;p&gt;This will display real-time swap usage, showing the amount of swap space that is free and used.&lt;/p&gt;&lt;p&gt;You can also use the &lt;code&gt;-g&lt;/code&gt; option to monitor paging activity, which shows how much data is being swapped in and out of memory.&lt;/p&gt;&lt;p&gt;Paging refers to the process of swapping memory pages between physical memory (RAM) and disk storage (swap space) when the server is under memory pressure. This process helps manage memory when the available physical RAM is full.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Page in:&lt;/strong&gt; When data is moved from disk (swap space) back into RAM.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Page out:&lt;/strong&gt; When data is moved from RAM to disk (swap space) to free up memory for other processes.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This is helpful to understand memory management and identify potential performance issues.&lt;/p&gt;&lt;h2&gt;Monitoring Network Traffic&lt;/h2&gt;&lt;p&gt;To monitor network traffic in real time, &lt;code&gt;dstat&lt;/code&gt; provides options to track incoming and outgoing data on your network interfaces.&lt;/p&gt;&lt;p&gt;You can easily track network statistics, such as the amount of data received and sent, and use various options to monitor specific interfaces or the overall network activity.&lt;/p&gt;&lt;p&gt;To monitor network traffic, use the &lt;code&gt;-n&lt;/code&gt; option:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;dstat -n
&lt;/pre&gt;&lt;p&gt;This will display real-time statistics for data received and sent over the network interfaces.&lt;/p&gt;&lt;p&gt;If you want to monitor specific network interfaces, such as &lt;code&gt;eth0&lt;/code&gt;, or include a summary of all interfaces, you can use the &lt;code&gt;-N&lt;/code&gt; option:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;dstat -n -N eth0,total
&lt;/pre&gt;&lt;p&gt;This command will show network statistics for &lt;code&gt;eth0&lt;/code&gt; as well as a total summary for all interfaces combined. This helps you track both individual and overall network performance.&lt;/p&gt;&lt;p&gt;Additionally, to monitor the number of network packets received and transmitted instead of data volume, use the &lt;code&gt;--net-packets&lt;/code&gt; option:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;dstat --net-packets
&lt;/pre&gt;&lt;p&gt;This provides a count of packets sent and received, which is useful for analyzing network load and diagnosing packet-related issues.&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;In conclusion, &lt;code&gt;dstat&lt;/code&gt; is a versatile tool that helps you monitor real-time server performance by tracking key metrics like CPU, disk, memory, and network activity It’s a great resource for identifying and addressing performance issues.&lt;/p&gt;&lt;p&gt;For more options and advanced usage, check the &lt;code&gt;dstat&lt;/code&gt; man page:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;man dstat
&lt;/pre&gt;&lt;p&gt;This will provide additional details to customize the tool for your needs.&lt;/p&gt;&lt;p&gt;If you found value in this guide or have any questions or feedback, please don&apos;t hesitate to share your thoughts in the &lt;strong&gt;discussion&lt;/strong&gt; section.&lt;/p&gt;&lt;p&gt;Your input is greatly appreciated, and you can also &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;contact me&lt;/a&gt; directly if you prefer.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Servers</category></item><item><title>Benchmarking Disk Performance on a Linux Server</title><link>https://ivansalloum.com/benchmarking-disk-performance-on-a-linux-server/</link><guid isPermaLink="true">https://ivansalloum.com/benchmarking-disk-performance-on-a-linux-server/</guid><description>Learn how to benchmark disk performance on a Linux server using various tools and metrics, to assess IOPS, throughput, and latency.</description><pubDate>Fri, 07 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;Disk performance is a critical factor that impacts everything from boot times to database queries.&lt;/p&gt;&lt;p&gt;I learned this the hard way when a slow disk caused high &lt;a href=&quot;https://ivansalloum.com/linux-server-resource-monitoring-made-easy/#context-switching-and-io-wait&quot;&gt;I/O wait&lt;/a&gt; on my server, bringing everything to a crawl. Web pages loaded slowly, database queries took forever, and the entire server felt unresponsive.&lt;/p&gt;&lt;p&gt;In high-performance environments such as web servers and databases, where thousands of I/O operations occur every second, disk speed becomes a non-negotiable priority.&lt;/p&gt;&lt;p&gt;That&apos;s why in this guide, we&apos;ll explore how to benchmark disk performance on a Linux server using various tools and metrics, helping you assess throughput, latency, and IOPS effectively.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;_I assume you&apos;re working on a properly set-up Ubuntu server. If not, check out my guide  on &lt;em&gt;&lt;a href=&quot;https://ivansalloum.com/preparing-your-ubuntu-server-for-first-use/&quot;&gt;&lt;em&gt;preparing  Ubuntu servers&lt;/em&gt;&lt;/a&gt; _  to get started.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;Author&apos;s Note&lt;/h2&gt;&lt;p&gt;Before we jump into benchmarking, here’s an important disclaimer: &lt;strong&gt;&lt;em&gt;Never run these tests on a production environment.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;On one hand, you risk breaking things, and on the other hand, the results won’t be accurate.&lt;/p&gt;&lt;p&gt;For the most accurate results, follow these best practices:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Run tests on an inactive server with no active I/O operations or memory usage.&lt;/li&gt;&lt;li&gt;Repeat each test 2–3 times to account for variability.&lt;/li&gt;&lt;li&gt;Calculate the average value of your results for better accuracy.&lt;/li&gt;&lt;/ul&gt;&lt;h2&gt;Understanding Disk Basics&lt;/h2&gt;&lt;p&gt;For example, let&apos;s look at hosting a WordPress website to help understand how disk performance affects website speed.&lt;/p&gt;&lt;p&gt;Disks are responsible for storing your website files, databases, and logs, so their speed directly influences the user experience.&lt;/p&gt;&lt;p&gt;Key factors that affect disk speed include the &lt;strong&gt;type of disk&lt;/strong&gt; and various&lt;strong&gt;disk speed metrics&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Type of disk:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;HDD (Hard Disk Drive):&lt;/strong&gt; Slower performance due to the use of spinning mechanical disks.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;SSD (Solid State Drive):&lt;/strong&gt; Much faster than HDDs, as they have no moving parts.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;NVMe SSD:&lt;/strong&gt; The fastest type of SSD, offering superior speed compared to standard SSDs.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Disk speed metrics:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Latency:&lt;/strong&gt; The time it takes to access data on the disk. Lower latency translates to faster data access and a quicker website.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Throughput:&lt;/strong&gt; The amount of data that can be transferred per second, measured in MB/s. Higher throughput means faster data transfer.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;IOPS (Input/Output Operations Per Second):&lt;/strong&gt; This metric measures how many small read/write (I/O) operations can occur per second. Higher IOPS is crucial for databases, as it allows for faster processing of multiple requests.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;HDDs are good at sequential reads/writes but slow at handling small random requests. SSDs and NVMe disks excel at random IOPS and low latency.&lt;/p&gt;&lt;h2&gt;Types of Disk Operations&lt;/h2&gt;&lt;p&gt;When benchmarking, you&apos;ll typically test two different types of read/write operations:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Sequential I/O:&lt;/strong&gt; Large continuous files, like downloading a big backup or saving a large video.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Random I/O:&lt;/strong&gt; Small, scattered files, like database queries or WordPress files.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;For example, WordPress websites mainly depend on random read/write performance, especially for database and PHP file reads.&lt;/p&gt;&lt;h2&gt;Basic Read/Write Tests&lt;/h2&gt;&lt;p&gt;In the following, I’ll show you how to test your disk’s sequential read/write performance.&lt;/p&gt;&lt;p&gt;Sequential I/O tests measure how quickly data can be written to or read from the disk in a &lt;strong&gt;continuous, linear order&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;These tests are commonly used for tasks like large file transfers or backups and simulate real-world scenarios where data is written continuously, without the disk needing to jump to different parts.&lt;/p&gt;&lt;h3&gt;Sequential Read Speed Performance&lt;/h3&gt;&lt;p&gt;When deploying a new Linux server, I always begin with a quick sequential read speed test using the &lt;code&gt;hdparm&lt;/code&gt; command.&lt;/p&gt;&lt;p&gt;&lt;code&gt;hdparm&lt;/code&gt; measures both &lt;strong&gt;cached&lt;/strong&gt; and &lt;strong&gt;buffered&lt;/strong&gt; read speeds, giving you an idea of how quickly your server can access data from memory and directly from the disk.&lt;/p&gt;&lt;p&gt;It ensures accurate measurements by flushing the &lt;strong&gt;buffer cache&lt;/strong&gt; before running the test since we want to know how fast data is read from the disk itself without any buffers.&lt;/p&gt;&lt;p&gt;Now, to run the read speed test, use the following:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo hdparm -Tt /dev/sda
&lt;/pre&gt;&lt;p&gt;Replace &lt;code&gt;/dev/sda&lt;/code&gt; with the actual device name of your target disk.&lt;/p&gt;&lt;p&gt;Once the test completes, &lt;code&gt;hdparm&lt;/code&gt; will display output like this:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;/dev/sda: Timing cached reads: 28238 MB in 1.99 seconds = 14180.88 MB/sec Timing buffered disk reads: 6434 MB in 3.00 seconds = 2144.09 MB/sec
&lt;/pre&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;Timing cached reads&lt;/code&gt;&lt;/strong&gt; : Measure memory read speed, indicating how fast data is accessed from memory.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;Timing buffered disk reads&lt;/code&gt;&lt;/strong&gt; : Reflect the actual disk read speed, showing how fast your server can read data directly from the disk.&lt;/li&gt;&lt;/ul&gt;&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;It’s a simple yet powerful way to get an initial sense of disk performance. I love this command!&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h3&gt;Sequential Write Speed Performance&lt;/h3&gt;&lt;p&gt;To measure sequential write speed, you can use the following &lt;code&gt;dd&lt;/code&gt; command to write data to a test file:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;dd if=/dev/zero of=test_file bs=64k count=16k conv=fdatasync
&lt;/pre&gt;&lt;p&gt;Here’s what each part of the command does:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;if=/dev/zero&lt;/code&gt;&lt;/strong&gt; : Uses a special file that generates zeroes, so no actual data needs to be read, focusing on write performance.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;of=test_file&lt;/code&gt;&lt;/strong&gt; : Specifies the file where data is written.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;bs=64k&lt;/code&gt;&lt;/strong&gt; : Sets the block size to 64KB, a common block size for disk transfers in real-world scenarios.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;count=16k&lt;/code&gt;&lt;/strong&gt; : Writes 1GB of data (64KB * 16,000).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;conv=fdatasync&lt;/code&gt;&lt;/strong&gt; : Ensures that all data is flushed to the disk immediately. Without this flag, some data might remain in memory (cached) instead of being written to the disk, which could lead to inaccurate results.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Once the test completes, &lt;code&gt;dd&lt;/code&gt; will display output like this:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;16384+0 records in 16384+0 records out 1073741824 bytes (1.1 GB, 1.0 GiB) copied, 1.36814 s, 785 MB/s
&lt;/pre&gt;&lt;p&gt;&lt;code&gt;785 MB/s&lt;/code&gt; is the &lt;strong&gt;sequential write speed&lt;/strong&gt; of the disk, indicating how fast the disk can write data in this test scenario.&lt;/p&gt;&lt;p&gt;Feel free to change the &lt;code&gt;bs&lt;/code&gt; and &lt;code&gt;count&lt;/code&gt; values based on your specific test scenario. For example, using &lt;code&gt;bs=1M&lt;/code&gt; and &lt;code&gt;count=1024&lt;/code&gt; would write a 1GB file but with a larger block size.&lt;/p&gt;&lt;h2&gt;Advanced Disk Performance Benchmarking&lt;/h2&gt;&lt;p&gt;&lt;code&gt;fio&lt;/code&gt; is one of the most powerful and flexible tools for benchmarking disk performance, particularly because it can simulate &lt;strong&gt;real-world workloads&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;To evaluate how well your disk can handle random I/O loads, we can run a benchmark that simulates both &lt;strong&gt;random read&lt;/strong&gt; and &lt;strong&gt;random write&lt;/strong&gt; operations.&lt;/p&gt;&lt;p&gt;This is especially important for websites that serve dynamic content and rely on database access like WordPress for example.&lt;/p&gt;&lt;p&gt;&lt;code&gt;fio&lt;/code&gt; is not usually installed by default. To install it, run the following command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt install fio
&lt;/pre&gt;&lt;p&gt;Here’s how you can benchmark your disk’s random I/O performance:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;fio --name=random_rw --ioengine=libaio --rw=randrw --direct=1 --bs=4k --numjobs=4 --size=1G --runtime=30 --time_based --group_reporting --unlink=1
&lt;/pre&gt;&lt;p&gt;Breakdown of the command:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;--name=random_rw&lt;/code&gt;: The name of the test job.&lt;/li&gt;&lt;li&gt;&lt;code&gt;--ioengine=libaio&lt;/code&gt;: &lt;code&gt;libaio&lt;/code&gt; is the I/O engine that enables asynchronous operations. It&apos;s optimal for testing random I/O because it simulates a real-world server where multiple I/O operations happen in parallel.&lt;/li&gt;&lt;li&gt;&lt;code&gt;--rw=randrw&lt;/code&gt;: This tells &lt;code&gt;fio&lt;/code&gt; to perform random read/write operations.&lt;/li&gt;&lt;li&gt;&lt;code&gt;--direct=1&lt;/code&gt;: This forces direct I/O, bypassing the server&apos;s cache, ensuring that the test measures actual disk performance without being influenced by cached data.&lt;/li&gt;&lt;li&gt;&lt;code&gt;--bs=4k&lt;/code&gt;: Block size of 4KB is common for database workloads.&lt;/li&gt;&lt;li&gt;&lt;code&gt;--numjobs=4&lt;/code&gt;: This creates 4 parallel jobs, which simulates multiple users or processes accessing the disk concurrently. More jobs will help simulate a higher load and better represent the demands on a real server.&lt;/li&gt;&lt;li&gt;&lt;code&gt;--size=1G&lt;/code&gt;: Each of the 4 jobs will use 1 GB of data.&lt;/li&gt;&lt;li&gt;&lt;code&gt;--runtime=30&lt;/code&gt;: This runs the benchmark for 30 seconds, allowing enough time to get a solid measurement of disk performance without excessive overhead.&lt;/li&gt;&lt;li&gt;&lt;code&gt;--time_based&lt;/code&gt;: This will run the test for the duration set by &lt;code&gt;--runtime&lt;/code&gt;, instead of running it until &lt;code&gt;fio&lt;/code&gt; finishes reading/writing the data, even if the data isn&apos;t fully processed. This ensures that the benchmark will always run for the specified period (30 seconds in this case), regardless of whether all data has been read or written, which is particularly useful for simulating time-based loads in real-world environments.&lt;/li&gt;&lt;li&gt;&lt;code&gt;--group_reporting&lt;/code&gt;: This will display the results from all jobs into a single output for easier analysis.&lt;/li&gt;&lt;li&gt;&lt;code&gt;--unlink=1&lt;/code&gt;: After the benchmark, &lt;code&gt;unlink&lt;/code&gt; deletes the test files to clean up.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Once the test completes, &lt;code&gt;fio&lt;/code&gt; will generate a large amount of output. Pay close attention to these specific lines in the output:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;read: IOPS=27.8k, BW=109MiB/s (114MB/s)(3263MiB/30001msec) lat (usec): min=55, max=5530, avg=142.94, stdev=50.93 write: IOPS=22.2k, BW=86.5MiB/s (90.8MB/s)(2597MiB/30001msec) lat (usec): min=4, max=3448, avg= 6.81, stdev= 8.16
&lt;/pre&gt;&lt;p&gt;Read performance:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;IOPS&lt;/strong&gt; : 27.8k operations per second (higher is better).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Bandwidth&lt;/strong&gt; : 114MB/s throughput (how fast data is read).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Latency&lt;/strong&gt; : Average time per read (142.94 µs), lower is better.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Write performance:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;IOPS&lt;/strong&gt; : 22.2k write operations per second (higher is better).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Bandwidth&lt;/strong&gt; : 90.8MB/s throughput (how fast data is written).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Latency&lt;/strong&gt; : Average time per write (6.81 µs), lower is better.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The disk performs well with high IOPS (27.8k for reads, 22.2k for writes), meaning it can handle many operations per second. The throughput (109 MiB/s for reads, 86.5 MiB/s for writes) indicates good data transfer speeds, which is important for handling website traffic. The latency is low, especially for writes, meaning the disk responds quickly to requests.&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;In this guide, we’ve explored the essential tools for benchmarking disk performance on a Linux server.&lt;/p&gt;&lt;p&gt;By measuring IOPS, throughput, and latency, you can gain a clearer understanding of how your server’s disk is performing.&lt;/p&gt;&lt;p&gt;Remember, disk speed plays a crucial role in server responsiveness, and investing time in disk performance analysis can lead to significant improvements in your server’s stability.&lt;/p&gt;&lt;p&gt;If you found value in this guide or have any questions or feedback, please don&apos;t hesitate to share your thoughts in the &lt;strong&gt;discussion&lt;/strong&gt; section.&lt;/p&gt;&lt;p&gt;Your input is greatly appreciated, and you can also &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;contact me&lt;/a&gt; directly if you prefer.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Servers</category></item><item><title>Linux Server Resource Monitoring Made Easy</title><link>https://ivansalloum.com/linux-server-resource-monitoring-made-easy/</link><guid isPermaLink="true">https://ivansalloum.com/linux-server-resource-monitoring-made-easy/</guid><description>Learn essential commands and techniques to monitor your Linux server&apos;s resource usage and fix performance bottlenecks.</description><pubDate>Sun, 02 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;Resource monitoring is a critical aspect of maintaining the health and performance of your Linux servers.&lt;/p&gt;&lt;p&gt;Whether you&apos;re managing a personal project or a large-scale production environment, monitoring ensures that your server runs as expected, with minimal downtime and maximum efficiency.&lt;/p&gt;&lt;p&gt;In this guide, we’ll explore essential commands and techniques to help you monitor your server&apos;s resource usage and fix performance bottlenecks effectively.&lt;/p&gt;&lt;h2&gt;Core Concepts to Understand&lt;/h2&gt;&lt;p&gt;Before diving into specific commands and techniques, it’s important to understand the key areas of Linux server resource monitoring.&lt;/p&gt;&lt;p&gt;Memory usage, for instance, should be closely observed to ensure there is enough available RAM. When the server runs out of RAM, it starts using swap space, which is much slower and can significantly degrade performance. While some swap usage is normal, excessive swapping indicates a need for more memory or better optimization.&lt;/p&gt;&lt;p&gt;Storage is another key resource to monitor. Running out of disk space can bring a server to a halt, preventing new data from being written. Common causes include growing log files or unmonitored backups, which need to be managed proactively.&lt;/p&gt;&lt;p&gt;CPU performance also requires attention, as an overloaded CPU can’t keep up with processing demands, leading to high load averages and overall performance issues.&lt;/p&gt;&lt;p&gt;Input/output (I/O) monitoring is equally vital, as excessive read/write operations or traffic flow can create bottlenecks, affecting how efficiently the server handles operations.&lt;/p&gt;&lt;p&gt;By monitoring these core areas, you can identify and fix bottlenecks, enabling you to focus on efficiently restoring and optimizing server performance.&lt;/p&gt;&lt;h2&gt;&lt;code&gt;top&lt;/code&gt; Command&lt;/h2&gt;&lt;p&gt;&lt;code&gt;top&lt;/code&gt; is one of the most widely used and long-standing commands for monitoring server resource usage in real-time. It provides a dynamic, continuously updated view of system processes and their resource usage.&lt;/p&gt;&lt;p&gt;Unlike static commands like &lt;code&gt;ps&lt;/code&gt;, &lt;code&gt;top&lt;/code&gt; is interactive, allowing users to scroll through the list of processes, filter results, and even terminate processes directly from the interface.&lt;/p&gt;&lt;p&gt;Simply type &lt;code&gt;top&lt;/code&gt; in the terminal, and it will display server statistics in a structured format. You can use the &lt;strong&gt;arrow keys&lt;/strong&gt; to navigate through the list. To exit, simply press &lt;code&gt;q&lt;/code&gt; on your keyboard.&lt;/p&gt;&lt;p&gt;The upper half of the output presents an overview of resource usage, including server uptime, the number of active tasks, CPU usage breakdown, and memory statistics.&lt;/p&gt;&lt;p&gt;The lower half of the output displays a detailed list of processes in different states, sorted by default in descending order of CPU usage.&lt;/p&gt;&lt;h3&gt;Time, Uptime, User Sessions, and Load Average&lt;/h3&gt;&lt;p&gt;The first line in &lt;code&gt;top&lt;/code&gt; shows the current time, followed by the server&apos;s uptime, indicating how long the server has been running since its last reboot.&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;top - 08:59:53 up 34 min, 2 users, load average: 0.61, 0.16, 0.05
&lt;/pre&gt;&lt;p&gt;For example, the time is &lt;code&gt;08:59:53&lt;/code&gt;, and the server has been up for &lt;strong&gt;34 minutes&lt;/strong&gt;. It also displays the number of active user sessions (in my case, two).&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;If you want to get more details about the active user sessions, use the &lt;code&gt;who&lt;/code&gt; command.&lt;/p&gt;&lt;p&gt;The &lt;strong&gt;load average&lt;/strong&gt; at the end shows the server&apos;s workload over the last 1, 5, and 15 minutes. It&apos;s essentially a measure of how many processes are actively using the CPU or waiting for it to become available.&lt;/p&gt;&lt;p&gt;While a load of &lt;code&gt;1.0&lt;/code&gt; would represent 100% CPU usage on single-core servers, most servers today are multi-core.&lt;/p&gt;&lt;p&gt;For example:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;On a dual-core server, a load of &lt;code&gt;1.0&lt;/code&gt; means that one core is fully utilized, leaving the other core idle, which equals around 50% CPU usage.&lt;/li&gt;&lt;li&gt;On a quad-core server, a load of &lt;code&gt;1.0&lt;/code&gt; represents about 25% CPU usage, since only one of the four cores is in use.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;It’s important to note that the load average in Linux accounts for both running and waiting processes, not just those currently executing. It represents an average value, not an instantaneous measurement.&lt;/p&gt;&lt;p&gt;To get a rough idea of CPU utilization, divide the load average by the number of CPU cores. While this isn’t an exact measure, it can still be quite useful.&lt;/p&gt;&lt;h3&gt;Task Summary&lt;/h3&gt;&lt;p&gt;The second line provides information on &lt;strong&gt;total tasks&lt;/strong&gt; , as well as how many are &lt;strong&gt;running&lt;/strong&gt; , &lt;strong&gt;sleeping&lt;/strong&gt; , &lt;strong&gt;stopped&lt;/strong&gt; , or in a &lt;strong&gt;zombie&lt;/strong&gt; state.&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;Tasks: 105 total, 2 running, 103 sleeping, 0 stopped, 0 zombie
&lt;/pre&gt;&lt;p&gt;In my case, I have 105 total processes, with &lt;code&gt;2 running&lt;/code&gt; and &lt;code&gt;103 sleeping&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;The server tracks the &lt;strong&gt;state&lt;/strong&gt; of each process, which can be one of the following:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Runnable (R)&lt;/strong&gt; : The process is executing on the CPU or ready to run.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Interruptible sleep (S)&lt;/strong&gt; : The process is waiting for an event to complete.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Uninterruptible sleep (D)&lt;/strong&gt; : The process is waiting for an I/O operation to finish.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Stopped (T)&lt;/strong&gt; : The process is stopped by a signal or is being traced.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Zombie (Z)&lt;/strong&gt; : A terminated process whose data structures are still in memory because the parent process hasn&apos;t collected its status yet.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Processes in the &lt;strong&gt;R&lt;/strong&gt; state are shown as &lt;code&gt;running&lt;/code&gt;, those in the &lt;strong&gt;D&lt;/strong&gt; and &lt;strong&gt;S&lt;/strong&gt; states are shown as &lt;code&gt;sleeping&lt;/code&gt;, and processes in the &lt;strong&gt;T&lt;/strong&gt; state are shown as &lt;code&gt;stopped&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;zombie&lt;/code&gt; value shows the number of processes in the &lt;strong&gt;Z&lt;/strong&gt; state.&lt;/p&gt;&lt;h3&gt;CPU Usage&lt;/h3&gt;&lt;p&gt;The third line shows the breakdown of &lt;strong&gt;CPU usage&lt;/strong&gt; , indicating the percentage of CPU used for different tasks.&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;%Cpu(s): 13.6 us, 0.3 sy, 0.0 ni, 86.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;us&lt;/code&gt; value represents the CPU usage for running user processes (outside the kernel), while the &lt;code&gt;sy&lt;/code&gt; value represents the CPU usage for running kernel processes.&lt;/p&gt;&lt;p&gt;Linux servers uses a &lt;strong&gt;nice&lt;/strong&gt; value to set the priority of a process. A higher nice value means lower priority, and a lower nice value means higher priority. The &lt;code&gt;ni&lt;/code&gt; value shows the CPU usage for processes with a manually set nice value.&lt;/p&gt;&lt;p&gt;Next is &lt;code&gt;id&lt;/code&gt;, which represents the percentage of CPU that is idle. Then comes &lt;code&gt;wa&lt;/code&gt;, which shows the percentage of CPU spent waiting for I/O to complete (more on this later).&lt;/p&gt;&lt;p&gt;&lt;code&gt;hi&lt;/code&gt; (hardware interrupts) and &lt;code&gt;si&lt;/code&gt; (software interrupts) refer to interrupts, which are signals that request the CPU&apos;s immediate attention.&lt;/p&gt;&lt;p&gt;In a virtualized environment, the &lt;code&gt;st&lt;/code&gt; (steal time) shows the percentage of CPU that is unavailable because it is being used by another virtual machine (VM).&lt;/p&gt;&lt;h3&gt;Memory Usage&lt;/h3&gt;&lt;p&gt;The fourth and fifth lines display the server&apos;s &lt;strong&gt;memory and swap usage&lt;/strong&gt; , showing how RAM and swap space are allocated.&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;MiB Mem : 7941.3 total, 7485.9 free, 383.6 used, 301.6 buff/cache MiB Swap: 512.0 total, 512.0 free, 0.0 used. 7557.8 avail Mem
&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;total&lt;/code&gt; value represents the total available memory, while &lt;code&gt;free&lt;/code&gt; indicates the amount of unused memory. The &lt;code&gt;used&lt;/code&gt; value shows the memory currently in use by active processes.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;buff/cache&lt;/code&gt; value refers to memory used by the server for buffers and caching.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Buffers&lt;/strong&gt; temporarily store data being read from or written to disk, improving disk I/O performance by reducing direct disk access. &lt;strong&gt;Cache&lt;/strong&gt; holds frequently accessed files and directory data in RAM, allowing for faster retrieval without needing to read from disk repeatedly. The server dynamically manages this memory, freeing it when needed for active processes.&lt;/p&gt;&lt;p&gt;The fifth line provides &lt;strong&gt;swap space&lt;/strong&gt; details, showing the &lt;code&gt;total&lt;/code&gt;, &lt;code&gt;free&lt;/code&gt;, and &lt;code&gt;used&lt;/code&gt; swap memory. Swap is disk space used as virtual memory when RAM is full. Ideally, swap usage should be low, as frequent swapping can slow down performance.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;avail Mem&lt;/code&gt; value represents the amount of memory that can be allocated to processes &lt;strong&gt;without causing more swapping&lt;/strong&gt;. It’s the most important value to check when determining how much RAM is available for new processes.&lt;/p&gt;&lt;p&gt;Unlike the &lt;code&gt;free&lt;/code&gt; value, which only shows completely unused memory, &lt;code&gt;avail Mem&lt;/code&gt; also considers memory that can be quickly reclaimed from &lt;code&gt;buff/cache&lt;/code&gt;. Since Linux dynamically manages buffers and cache, this memory is available for new processes when needed.&lt;/p&gt;&lt;p&gt;In short, while &lt;code&gt;free&lt;/code&gt; memory might appear low, a large &lt;code&gt;buff/cache&lt;/code&gt; means that memory can still be used efficiently. That’s why &lt;code&gt;avail Mem&lt;/code&gt; is the best indicator of how much RAM is actually available.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;If you want to know the available RAM as a percentage, use the following command: &lt;code&gt;free | grep Mem | awk &apos;{print $7/$2 * 100 }&apos;&lt;/code&gt;&lt;/p&gt;&lt;h3&gt;Process List&lt;/h3&gt;&lt;p&gt;Now, let&apos;s dive into the lower half of the &lt;code&gt;top&lt;/code&gt; output, which displays a list of processes in different states, such as running or stopped, along with important details about each one.&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;PID&lt;/code&gt; is the process ID, a unique number that identifies each process. The &lt;code&gt;USER&lt;/code&gt; field displays the username of the user who started the process.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;PR&lt;/code&gt; and &lt;code&gt;NI&lt;/code&gt; fields represent the priority of the process. &lt;code&gt;NI&lt;/code&gt; shows the nice value, which influences the process’s priority, while &lt;code&gt;PR&lt;/code&gt; indicates the process’s priority from the kernel’s perspective.&lt;/p&gt;&lt;p&gt;&lt;code&gt;VIRT&lt;/code&gt; represents the total virtual memory used by a process, including code, data, and swapped-out memory. &lt;code&gt;RES&lt;/code&gt; shows the amount of physical RAM used by the process, giving a more accurate picture of actual memory consumption, excluding swapped-out memory but including shared libraries if they are loaded. &lt;code&gt;SHR&lt;/code&gt; indicates the amount of shared memory used by the process, which can be accessed by other processes (shareable), and is a part of &lt;code&gt;RES&lt;/code&gt;. &lt;code&gt;%MEM&lt;/code&gt; shows the process&apos;s memory usage as a percentage of the total available RAM.&lt;/p&gt;&lt;p&gt;In terms of comparison:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;VIRT&lt;/code&gt; is always greater than or equal to &lt;code&gt;RES&lt;/code&gt; since &lt;code&gt;VIRT&lt;/code&gt; includes potential memory usage, while &lt;code&gt;RES&lt;/code&gt; shows actual physical usage.&lt;/li&gt;&lt;li&gt;&lt;code&gt;RES&lt;/code&gt; includes &lt;code&gt;SHR&lt;/code&gt;, so to calculate the exclusive memory usage of a process, you subtract &lt;code&gt;SHR&lt;/code&gt; from &lt;code&gt;RES&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;&lt;code&gt;%MEM&lt;/code&gt; is simply &lt;code&gt;RES&lt;/code&gt; expressed as a percentage of total available RAM.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;By understanding these metrics, you can better analyze a process’s memory usage. For example, high &lt;code&gt;VIRT&lt;/code&gt; with low &lt;code&gt;RES&lt;/code&gt; might suggest a process with a large address space but efficient memory use. High &lt;code&gt;RES&lt;/code&gt; with high &lt;code&gt;SHR&lt;/code&gt; indicates heavy reliance on shared libraries, and a high &lt;code&gt;%MEM&lt;/code&gt; highlights processes consuming significant memory.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;S&lt;/code&gt; field shows the process&apos;s status, indicating whether it&apos;s running (R), sleeping (S), stopped (T), or a zombie (Z). Additionally, there is a less common state, the idle kernel thread (I), which represents a process that is not currently doing any work.&lt;/p&gt;&lt;p&gt;&lt;code&gt;%CPU&lt;/code&gt; shows the percentage of CPU usage consumed by the process, while &lt;code&gt;TIME+&lt;/code&gt; indicates the total CPU time the process has been running since it started.&lt;/p&gt;&lt;p&gt;Finally, the &lt;code&gt;COMMAND&lt;/code&gt; field displays the name or command of the process, allowing you to easily identify it.&lt;/p&gt;&lt;h3&gt;Usage Examples&lt;/h3&gt;&lt;p&gt;So far, I have only talked about the &lt;code&gt;top&lt;/code&gt; output and explained what it provides. However, there&apos;s much more to do.&lt;/p&gt;&lt;p&gt;You can manage processes and make changes directly while &lt;code&gt;top&lt;/code&gt; is running, or you can use it with various options to customize the output and make it more suited to your needs.&lt;/p&gt;&lt;p&gt;For example, while &lt;code&gt;top&lt;/code&gt; is running, you can kill a process simply by pressing &lt;strong&gt;k&lt;/strong&gt; , typing the process ID, and pressing the &lt;strong&gt;ENTER&lt;/strong&gt; key. You will then be asked to specify the signal with which to kill the process. If you leave this blank, &lt;code&gt;top&lt;/code&gt; uses the default &lt;strong&gt;SIGTERM&lt;/strong&gt; signal, which allows processes to terminate gracefully. If you want to kill a process forcefully, you can type &lt;strong&gt;SIGKILL&lt;/strong&gt; or use its number (9). The number for &lt;strong&gt;SIGTERM&lt;/strong&gt; is 15.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;Keep in mind that keypresses are case sensitive, so pressing &lt;strong&gt;K&lt;/strong&gt; instead of &lt;strong&gt;k&lt;/strong&gt; will not work or will trigger a different action.&lt;/p&gt;&lt;p&gt;If you want to filter processes by user, you can use the &lt;code&gt;-u&lt;/code&gt; option followed by a specific username, like so:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;top -u
&lt;/pre&gt;&lt;p&gt;This will show you only the processes started by that user.&lt;/p&gt;&lt;p&gt;You can press the following keys to sort the list of processes &lt;code&gt;top&lt;/code&gt; displays:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;M&lt;/strong&gt; to sort by memory usage.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;P&lt;/strong&gt; to sort by CPU usage.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;N&lt;/strong&gt; to sort by process ID.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;T&lt;/strong&gt; to sort by the running time.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;By default, &lt;code&gt;top&lt;/code&gt; displays all results in descending order. However, you can switch to ascending order by pressing the &lt;strong&gt;R&lt;/strong&gt; key.&lt;/p&gt;&lt;p&gt;You can also sort the list using the &lt;code&gt;-o&lt;/code&gt; option from the command line before running &lt;code&gt;top&lt;/code&gt;, like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;top -o %CPU
&lt;/pre&gt;&lt;p&gt;Change &lt;code&gt;%CPU&lt;/code&gt; to &lt;code&gt;%MEM&lt;/code&gt;, &lt;code&gt;TIME+&lt;/code&gt;, or any other attribute you want to sort by.&lt;/p&gt;&lt;p&gt;By default, &lt;code&gt;top&lt;/code&gt; does not show the full path to the process running or make a distinction between kernelspace processes and userspace processes. If you need this information, press the &lt;strong&gt;c&lt;/strong&gt; key. Press &lt;strong&gt;c&lt;/strong&gt; again to go back to the default view. Kernelspace processes are marked with square brackets around them.&lt;/p&gt;&lt;p&gt;You can also display threads, not just processes, by pressing the &lt;strong&gt;H&lt;/strong&gt; key. Notice how the second line changes from saying &lt;code&gt;Tasks&lt;/code&gt; to &lt;code&gt;Threads&lt;/code&gt;, and it will now show the number of threads instead of processes.&lt;/p&gt;&lt;p&gt;Lastly, pressing &lt;strong&gt;e&lt;/strong&gt; switches between kilobytes (default), megabytes, gigabytes, terabytes, and petabytes for process usage. After selecting a size, press &lt;strong&gt;W&lt;/strong&gt; to save your preference, so &lt;code&gt;top&lt;/code&gt; starts with your chosen size next time.&lt;/p&gt;&lt;h2&gt;Context Switching and I/O Wait&lt;/h2&gt;&lt;p&gt;It&apos;s important to understand how a Linux server manages processes and what &lt;strong&gt;I/O wait&lt;/strong&gt; is – specifically, what the &lt;code&gt;wa&lt;/code&gt; value in &lt;code&gt;top&lt;/code&gt; exactly represents.&lt;/p&gt;&lt;p&gt;Processes on a Linux server perform either &lt;strong&gt;I/O-bound work&lt;/strong&gt; or &lt;strong&gt;CPU-bound work&lt;/strong&gt; (like executing arithmetic operations).&lt;/p&gt;&lt;p&gt;I/O-bound work refers to operations where data is read from or written to storage devices. On a server running a WordPress website, common I/O operations include:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Reading files&lt;/strong&gt; such as PHP scripts, images, or HTML pages.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Writing logs&lt;/strong&gt; like access and error logs.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Database operations&lt;/strong&gt; , including reading from or writing to the site&apos;s database, especially for online stores.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;When a process is performing I/O tasks, it doesn&apos;t need the CPU for that part of the task. The CPU is idle while the process waits for the I/O operation to finish.&lt;/p&gt;&lt;p&gt;To make efficient use of the CPU, the server switches to another process that is ready to run, allowing the CPU to be used while the first process waits. This creates the illusion of &lt;strong&gt;multitasking&lt;/strong&gt; , even though the CPU is only running one process at a time. The server switches between processes so quickly that it appears as though multiple processes are running simultaneously. This process is often referred to as &lt;strong&gt;context switching&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;I/O wait is a measure of the time the CPU spends waiting for I/O operations to complete. While the CPU is capable of processing requests quickly, it may need to retrieve data from the disk. If the disk is slow or busy, the CPU remains idle until the I/O operation finishes. This waiting time is known as I/O wait, which is shown by the &lt;code&gt;wa&lt;/code&gt; value in &lt;code&gt;top&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;In a well-optimized server, I/O wait is typically low – around &lt;strong&gt;0-1%&lt;/strong&gt; – which indicates that the disk is keeping up with the CPU. However, if you see an &lt;code&gt;wa&lt;/code&gt; value of &lt;strong&gt;25%&lt;/strong&gt; , that’s a red flag. It means a &lt;strong&gt;quarter&lt;/strong&gt; of the CPU’s time is wasted waiting for I/O operations, making the disk a performance bottleneck.&lt;/p&gt;&lt;p&gt;For example, on a server running WordPress, high &lt;code&gt;wa&lt;/code&gt; value can lead to slower page loads or delays in handling requests because the disk can&apos;t retrieve data fast enough. If I/O wait reaches &lt;strong&gt;75%&lt;/strong&gt; , the impact is even more severe – three-quarters of the CPU’s time is spent waiting on disk operations, which can cripple server performance.&lt;/p&gt;&lt;p&gt;Now, back to the fact that the server switches between processes that are ready to run while others are waiting for I/O operations to finish. Even though the server can switch to another process when one is waiting on I/O, there are scenarios where you still end up with high I/O wait. This is important to clarify, as many people mistakenly think that context switching can eliminate I/O wait time, but that&apos;s not entirely true.&lt;/p&gt;&lt;p&gt;If many processes are I/O-bound (such as a busy web server), there might not be enough CPU-bound processes ready to run. In this case, even though the CPU is free, all available processes are blocked, waiting for I/O to finish. The CPU ends up idle during these periods, causing the &lt;code&gt;wa&lt;/code&gt; value to increase.&lt;/p&gt;&lt;p&gt;Another point is that the actual time required for an I/O operation to finish doesn’t change because of context switching. While the CPU might switch to another process, the I/O operation itself still takes a certain amount of time to complete. If this latency is high (because your disk is slow), more processes will be waiting for the I/O operation to finish, and the I/O wait will increase.&lt;/p&gt;&lt;p&gt;If you notice a high &lt;code&gt;wa&lt;/code&gt; value, you should &lt;strong&gt;immediately investigate&lt;/strong&gt; what&apos;s causing the issue and fix it. Identifying the bottleneck (whether it&apos;s a slow disk, high disk usage, or inefficient processes) will help restore server performance.&lt;/p&gt;&lt;h2&gt;&lt;code&gt;iostat&lt;/code&gt; Command&lt;/h2&gt;&lt;p&gt;&lt;code&gt;iostat&lt;/code&gt; is a powerful command that provides detailed insights into your server’s I/O performance. It offers a broader view of server-wide I/O statistics. It helps you understand the overall I/O behavior of your server.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;iostat&lt;/code&gt; command is part of the &lt;code&gt;sysstat&lt;/code&gt; package, which is installed on most Linux distributions.&lt;/p&gt;&lt;p&gt;Running the command without any options shows also CPU information, such as the number of cores, average CPU usage, and average I/O wait. Using the &lt;code&gt;-d&lt;/code&gt; option will display only the I/O statistics for the available disks.&lt;/p&gt;&lt;p&gt;I always use the command with the &lt;code&gt;-d&lt;/code&gt; and &lt;code&gt;-x&lt;/code&gt; options, which shows extended I/O statistics for more detailed information.&lt;/p&gt;&lt;p&gt;You can also specify a number after the options to set the interval in seconds at which the command will be rerun. For example, running &lt;code&gt;iostat -d -x 1&lt;/code&gt; will execute the command every second and show updated I/O statistics.&lt;/p&gt;&lt;p&gt;When investigating an I/O problem, such as high I/O wait, I always pay attention to a few key metrics in the command&apos;s output.&lt;/p&gt;&lt;p&gt;The first important metric I look at is &lt;code&gt;%util&lt;/code&gt;, which shows how busy the disk is. If &lt;code&gt;%util&lt;/code&gt; is close to &lt;strong&gt;100%&lt;/strong&gt; , it means the disk is nearly full and handling as many I/O requests as possible, which can cause performance issues. If you see &lt;code&gt;%util&lt;/code&gt; consistently near 100%, it’s a clear sign that your disk is fully utilized, and you may need to investigate the cause of the high I/O usage or consider performance upgrades.&lt;/p&gt;&lt;p&gt;Next, I look at these metrics, each of which provides a different view of your disk’s performance:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;r/s&lt;/code&gt; and &lt;code&gt;w/s&lt;/code&gt;:&lt;/strong&gt; These show the number of read and write requests per second. They help you understand the intensity of I/O operations (IOPS).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;rkB/s&lt;/code&gt; and &lt;code&gt;wkB/s&lt;/code&gt;:&lt;/strong&gt; These measure the amount of data in kilobytes being read from or written to the disk per second. They give you an idea of throughput.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;r_await&lt;/code&gt; and &lt;code&gt;w_await&lt;/code&gt;:&lt;/strong&gt; These represent the average time (usually in milliseconds) that read and write requests take to complete.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Now, let’s discuss what you can learn from these metrics:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;If &lt;code&gt;%util&lt;/code&gt; is high but the &lt;code&gt;await&lt;/code&gt; values are low, your disk might be very efficient even under heavy load.&lt;/li&gt;&lt;li&gt;Conversely, if &lt;code&gt;%util&lt;/code&gt; is moderate but &lt;code&gt;r_await&lt;/code&gt; or &lt;code&gt;w_await&lt;/code&gt; are high, it could be that the disk is having trouble completing each operation quickly, suggesting latency problems.&lt;/li&gt;&lt;li&gt;If your disk shows high &lt;code&gt;rkB/s&lt;/code&gt; 0r &lt;code&gt;wkB/s&lt;/code&gt; values, it means large volumes of data are being transferred, which could be normal for a heavy database server but might be a performance concern in other scenarios.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Lastly, to view the metrics in megabytes instead of kilobytes, use the &lt;code&gt;-m&lt;/code&gt; option.&lt;/p&gt;&lt;h2&gt;&lt;code&gt;iotop&lt;/code&gt; Command&lt;/h2&gt;&lt;p&gt;&lt;code&gt;iotop&lt;/code&gt; is a powerful command for monitoring disk I/O usage in real-time, helping you investigate the causes of high I/O wait, such as inefficient processes that are consuming excessive I/O resources.&lt;/p&gt;&lt;p&gt;Similar to how &lt;code&gt;top&lt;/code&gt; displays a list of processes along with their CPU and memory usage, &lt;code&gt;iotop&lt;/code&gt; provides detailed insights into threads consuming the most disk operations.&lt;/p&gt;&lt;p&gt;Sometimes, &lt;code&gt;iotop&lt;/code&gt; is not installed by default. If that&apos;s the case, you can install it using the following command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt install iotop
&lt;/pre&gt;&lt;p&gt;Once installed, running &lt;code&gt;iotop&lt;/code&gt; without any options displays a real-time list of threads along with their I/O usage.&lt;/p&gt;&lt;p&gt;To filter and display only active threads performing disk operations, use the &lt;code&gt;-o&lt;/code&gt; option, like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ivan@vm1:~$ sudo iotop -o Total DISK READ: 1530.64 M/s | Total DISK WRITE: 11.92 K/s Current DISK READ: 1530.64 M/s | Current DISK WRITE: 154.98 K/s TID PRIO USER DISK READ DISK WRITE SWAPIN IO&amp;gt; COMMAND 9655 be/4 root 1530.64 M/s 0.00 B/s 0.00 % 0.05 % hdparm -Tt /dev/sda ...
&lt;/pre&gt;&lt;p&gt;&lt;code&gt;Total DISK READ&lt;/code&gt; and &lt;code&gt;Total DISK WRITE&lt;/code&gt; show how much data processes are asking the server to read or write. &lt;code&gt;Current DISK READ&lt;/code&gt; and &lt;code&gt;Current DISK WRITE&lt;/code&gt; reflect the real-time data flow to and from the disk.&lt;/p&gt;&lt;p&gt;These values may differ for two reasons:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;The Linux kernel uses caching to speed up I/O, so data might be temporarily stored in memory instead of being immediately written to or read from the disk.&lt;/li&gt;&lt;li&gt;The kernel may reorder I/O operations for better efficiency.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Because of this, even if processes request a lot of I/O (shown in the &lt;code&gt;Total&lt;/code&gt; values), the actual I/O happening at any moment (shown in the &lt;code&gt;Current&lt;/code&gt; values) might be different.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;TID&lt;/code&gt; (Thread ID) is a unique identifier for the specific thread within a process that is handling I/O operations. The &lt;code&gt;PRIO&lt;/code&gt; (priority) indicates how much priority the server assigns to that thread for execution. The &lt;code&gt;USER&lt;/code&gt; column shows the user who owns the process.&lt;/p&gt;&lt;p&gt;&lt;code&gt;DISK READ&lt;/code&gt; shows the amount of data a process is reading from the disk in real-time. Similarly, &lt;code&gt;DISK WRITE&lt;/code&gt; displays the amount of data being written to the disk by the process at that moment. These values help you identify which processes are performing the most disk operations.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;IO&amp;gt;&lt;/code&gt; column shows the total I/O activity of a process, including both disk reads/writes and swap operations. The &lt;code&gt;SWAPIN&lt;/code&gt; column shows the percentage of swap space the process is using, which can indicate memory pressure. Monitoring both columns helps you understand if performance issues are caused by heavy disk usage, swapping, or both.&lt;/p&gt;&lt;p&gt;Finally, two options I always use with the command are the &lt;code&gt;-P&lt;/code&gt; option, which lists processes instead of threads along with their PIDs, and the &lt;code&gt;-a&lt;/code&gt; option, which shows the accumulated I/O rate.&lt;/p&gt;&lt;p&gt;The accumulated value reflects the total amount of data a process has read or written since monitoring started, averaged over that entire period, instead of just showing the current moment-by-moment I/O rate.&lt;/p&gt;&lt;p&gt;For example, rather than displaying a spike of 10 MB/s for a few seconds followed by a drop to 2 MB/s, the accumulated value averages all the data over time, giving you a clearer, overall I/O rate.&lt;/p&gt;&lt;h2&gt;Slow Disk Causing High I/O Wait&lt;/h2&gt;&lt;p&gt;I want to share an experience I had with a server running a WordPress website that had a very slow disk. This was when I self-hosted WordPress on a server running the LEMP stack. The server was performing very slowly, and I needed to figure out the cause.&lt;/p&gt;&lt;p&gt;The first thing I did was run the &lt;code&gt;top&lt;/code&gt; command. I noticed high load average values, even though the &lt;code&gt;id&lt;/code&gt; value was around &lt;strong&gt;80%&lt;/strong&gt; , indicating that most of the CPU was idle. This left me confused. Why was there high load when the CPU wasn’t being heavily used?&lt;/p&gt;&lt;p&gt;It was a challenge to pinpoint the issue, but then I noticed that the &lt;code&gt;wa&lt;/code&gt; (I/O wait) value was high. I can&apos;t remember the exact value, but it was around &lt;strong&gt;8%&lt;/strong&gt;. Typically, the &lt;code&gt;wa&lt;/code&gt; value should be close to &lt;strong&gt;0%&lt;/strong&gt; , and a consistent value above &lt;strong&gt;1%&lt;/strong&gt; often indicates an I/O problem.&lt;/p&gt;&lt;p&gt;The server had many cores, so the &lt;code&gt;top&lt;/code&gt; command only showed average values for the &lt;code&gt;%Cpu(s)&lt;/code&gt; line. I pressed &lt;strong&gt;1&lt;/strong&gt; to expand the view and see the &lt;code&gt;wa&lt;/code&gt; value for each individual core. Some of them had I/O wait values around &lt;strong&gt;30%&lt;/strong&gt; and &lt;strong&gt;50%,&lt;/strong&gt; which clearly pointed to an I/O issue.&lt;/p&gt;&lt;p&gt;To investigate further, I used the &lt;code&gt;iostat&lt;/code&gt; command to check the I/O statistics and noticed that the &lt;code&gt;%util&lt;/code&gt; value was at &lt;strong&gt;100%&lt;/strong&gt; , meaning my disk was fully utilized.&lt;/p&gt;&lt;p&gt;Next, I ran the &lt;code&gt;iotop&lt;/code&gt; command to see if any processes were causing the problem, but there was no indication of a specific process to blame. I ran &lt;code&gt;iotop&lt;/code&gt; with the &lt;code&gt;-a&lt;/code&gt; option to get accumulated values over time.&lt;/p&gt;&lt;p&gt;After a while, I observed that the &lt;code&gt;Total DISK WRITE&lt;/code&gt; value was quite low, even though the disk was fully utilized. This told me that the disk itself was slow and needed to be replaced.&lt;/p&gt;&lt;p&gt;From this experience, I also learned an important lesson: never save website cache to disk. Instead, use cache on RAM, which is faster and reduces I/O operations, especially if you have enough RAM. In my case, the FastCGI cache stored on the disk was the major contributor to the issue, as my disk was too slow. This was a key factor in causing the performance problem.&lt;/p&gt;&lt;h2&gt;&lt;code&gt;df&lt;/code&gt; Command&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;df&lt;/code&gt; command is used to display the available and used disk space on filesystems. It provides an overview of how storage is allocated across the different mounted partitions of your server.&lt;/p&gt;&lt;p&gt;Disk space is shown in 1K blocks by default. If you want to run the &lt;code&gt;df&lt;/code&gt; command in its human-readable format, use the &lt;code&gt;-h&lt;/code&gt; option. This will display the sizes in a more understandable format (KB, MB, GB), making it easier to interpret.&lt;/p&gt;&lt;p&gt;If you don’t include a specific mount point, the command shows information on all currently mounted filesystems, with output like this when using the &lt;code&gt;-h&lt;/code&gt; option:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;Filesystem Size Used Avail Use% Mounted on tmpfs 795M 984K 794M 1% /run /dev/sda 157G 3.0G 146G 3% / tmpfs 3.9G 0 3.9G 0% /dev/shm tmpfs 5.0M 0 5.0M 0% /run/lock tmpfs 795M 12K 795M 1% /run/user/0
&lt;/pre&gt;&lt;p&gt;In the output of the &lt;code&gt;df -h&lt;/code&gt; command, each column represents important information about the server’s mounted filesystems.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;Filesystem&lt;/code&gt; column lists the mounted devices or partitions, like &lt;code&gt;/dev/sda&lt;/code&gt; for physical disks and &lt;code&gt;tmpfs&lt;/code&gt; for temporary memory-based storage.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;Size&lt;/code&gt; column shows the total size of each filesystem, while the &lt;code&gt;Used&lt;/code&gt; column indicates how much space is currently used. The &lt;code&gt;Avail&lt;/code&gt; column shows the remaining available space, and the &lt;code&gt;Use%&lt;/code&gt; column displays the percentage of space used. The &lt;code&gt;Mounted on&lt;/code&gt; column tells you where each filesystem is mounted, like &lt;code&gt;/dev/sda&lt;/code&gt; at the root (&lt;code&gt;/&lt;/code&gt;) or &lt;code&gt;tmpfs&lt;/code&gt; at &lt;code&gt;/run&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;We can use the &lt;code&gt;-i&lt;/code&gt; option to check inode (index node) usage. This will display the number of inodes used and available on each mounted filesystem. You can combine it with the &lt;code&gt;-h&lt;/code&gt; option to make the output more human-readable, like this:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;Filesystem Inodes IUsed IFree IUse% Mounted on tmpfs 993K 646 993K 1% /run /dev/sda 9.9M 133K 9.8M 2% / tmpfs 993K 1 993K 1% /dev/shm tmpfs 993K 3 993K 1% /run/lock tmpfs 199K 33 199K 1% /run/user/0
&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;Inodes&lt;/code&gt; column shows the total number of inodes available on each filesystem. &lt;code&gt;IUsed&lt;/code&gt; indicates how many inodes are currently in use, while &lt;code&gt;IFree&lt;/code&gt; displays the remaining available inodes. &lt;code&gt;IUse%&lt;/code&gt; represents the percentage of inodes used.&lt;/p&gt;&lt;p&gt;For example, &lt;code&gt;/dev/sda&lt;/code&gt; has 9.9 million inodes, with 133K used (2%), leaving 9.8 million free. &lt;code&gt;tmpfs&lt;/code&gt; filesystems have significantly fewer inodes in use, as they primarily store temporary files.&lt;/p&gt;&lt;h2&gt;&lt;code&gt;tmpfs&lt;/code&gt; Filesystems&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;tmpfs&lt;/code&gt; filesystems you see in the &lt;code&gt;df -h&lt;/code&gt; output are created automatically by the Linux server during boot. These are virtual filesystems that reside in RAM and are used for various purposes.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;/run&lt;/code&gt; is used for storing runtime data for processes, such as PID files and socket files. It’s similar to &lt;code&gt;/tmp&lt;/code&gt;, but the data in &lt;code&gt;/run&lt;/code&gt; doesn&apos;t survive a reboot.&lt;/li&gt;&lt;li&gt;&lt;code&gt;/dev/shm&lt;/code&gt; is used for &lt;strong&gt;Inter-Process Communication (IPC)&lt;/strong&gt; , allowing multiple processes to share data in memory. This is faster than writing to disk.&lt;/li&gt;&lt;li&gt;&lt;code&gt;/run/lock&lt;/code&gt; is used for lock files, ensuring that certain resources are accessed by only one process at a time.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The size of &lt;code&gt;/dev/shm&lt;/code&gt; is usually set to half of the server’s available RAM. You might think this is a waste, especially if it’s not used. However, the memory for &lt;code&gt;/dev/shm&lt;/code&gt; isn’t pre-allocated, and processes can use it if needed. The size of &lt;code&gt;/dev/shm&lt;/code&gt; represents the maximum memory it can use, not what’s reserved. The server gives priority to active processes, so if other processes use &lt;strong&gt;80%&lt;/strong&gt; of the RAM, &lt;code&gt;/dev/shm&lt;/code&gt; will only use the remaining &lt;strong&gt;20%&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Sometimes, even though the server gives priority to active processes, a poorly behaving or unexpectedly resource-consuming process might end up filling up &lt;code&gt;/dev/shm&lt;/code&gt;, leaving other processes waiting for memory to become available. In such situations, the server will start using swap memory to compensate for the lack of available RAM.&lt;/p&gt;&lt;p&gt;Once the swap is fully used, the &lt;strong&gt;Out-of-Memory (OOM) Killer&lt;/strong&gt; is invoked to free up memory by terminating one or more processes. It doesn’t prioritize cleaning out &lt;code&gt;/dev/shm&lt;/code&gt;, so important processes may be killed, disrupting services.&lt;/p&gt;&lt;p&gt;We want to avoid triggering the OOM Killer whenever possible. It doesn&apos;t happen often, but it&apos;s good to keep it in mind.&lt;/p&gt;&lt;p&gt;For this reason, it&apos;s a good idea to keep an eye on how much space (RAM) &lt;code&gt;/dev/shm&lt;/code&gt; is using.&lt;/p&gt;&lt;h2&gt;&lt;code&gt;du&lt;/code&gt; Command&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;du&lt;/code&gt; command provides information about how much space individual files and directories consume. Unlike the &lt;code&gt;df&lt;/code&gt; command, which summarizes entire filesystems, &lt;code&gt;du&lt;/code&gt; is useful for drilling down to find large files or directories consuming excessive space.&lt;/p&gt;&lt;p&gt;Running the command alone will produce this output:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;4 ./.config/procps 8 ./.config 4 ./.cache 36 .
&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;du&lt;/code&gt; command displays the size of each directory and subdirectory, with the numbers representing disk usage in kilobytes. The final line provides the total disk usage for the directory where the command was executed, which in this case is 36 KB.&lt;/p&gt;&lt;p&gt;However, the standard &lt;code&gt;du&lt;/code&gt; command does not specify the unit of measurement, which can make the output more difficult to interpret. Additionally, it does not list the size of individual files inside the directory by default, and only includes their total size in the summary at the final line.&lt;/p&gt;&lt;p&gt;We can use the &lt;code&gt;-h&lt;/code&gt; and &lt;code&gt;-a&lt;/code&gt; options to make the output human-readable and list the sizes of all files, like this:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;4.0K ./.config/procps 8.0K ./.config 4.0K ./.bashrc 4.0K ./.profile 4.0K ./.bash_logout 4.0K ./.bash_history 4.0K ./.lesshst 0 ./.cache/motd.legal-displayed 4.0K ./.cache 36K .
&lt;/pre&gt;&lt;p&gt;In this output, the &lt;code&gt;-h&lt;/code&gt; option makes the sizes readable (KB, MB, GB), and the &lt;code&gt;-a&lt;/code&gt; option lists the size of every file, not just the directories.&lt;/p&gt;&lt;p&gt;If you only want to see the total disk usage of a directory, use the &lt;code&gt;-s&lt;/code&gt; option along with the &lt;code&gt;-h&lt;/code&gt; option, like this:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;36K .
&lt;/pre&gt;&lt;p&gt;A particularly useful flag that I especially love is the &lt;code&gt;--time&lt;/code&gt; flag. It shows the time of the last modification for any file within the directory or subdirectory. I often use this flag in conjunction with the &lt;code&gt;-a&lt;/code&gt; and &lt;code&gt;-h&lt;/code&gt; options.&lt;/p&gt;&lt;p&gt;The last option I want to talk about is the &lt;code&gt;-d&lt;/code&gt; option. This option allows you to specify the depth of directories to display, making it useful when you only want to see the sizes of top-level directories without drilling down into every subdirectory.&lt;/p&gt;&lt;p&gt;For example, using &lt;code&gt;du -d 1 -h&lt;/code&gt; will show the total size of each directory at the first level, providing a clearer overview of disk usage without excessive detail.&lt;/p&gt;&lt;h2&gt;&lt;code&gt;free&lt;/code&gt; Command&lt;/h2&gt;&lt;p&gt;The free command is one of the most widely used command to display server memory statistics. The information it presents is similar to what the &lt;code&gt;top&lt;/code&gt; command shows about memory usage, but in a more concise format.&lt;/p&gt;&lt;p&gt;Running the &lt;code&gt;free&lt;/code&gt; command with the &lt;code&gt;-h&lt;/code&gt; option provides the output in a human-readable format, making it easier to understand by displaying memory sizes in KB, MB, or GB, like this:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;total used free shared buff/cache available Mem: 7.8Gi 384Mi 7.3Gi 988Ki 301Mi 7.4Gi Swap: 511Mi 0B 511Mi
&lt;/pre&gt;&lt;p&gt;The server has a total of 7.8 GB of RAM. Out of that, 384 MB is in use, and 7.3 GB is free. It has 301 MB in cache or buffers that can be freed if needed, and 7.4 GB of memory is still available for use by new processes. The &lt;code&gt;Swap&lt;/code&gt; section shows 511 MB of swap space, which is completely free since no swap has been used.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;One important clarification:&lt;/strong&gt; the &lt;code&gt;shared&lt;/code&gt; value in the &lt;code&gt;free&lt;/code&gt; command output is &lt;strong&gt;not&lt;/strong&gt;  the same as the &lt;code&gt;SHR&lt;/code&gt; value in &lt;code&gt;top&lt;/code&gt;. In &lt;code&gt;free&lt;/code&gt;, &lt;code&gt;shared&lt;/code&gt; refers to the total memory used by all &lt;code&gt;tmpfs&lt;/code&gt; filesystems (such as &lt;code&gt;/dev/shm&lt;/code&gt;). In contrast, &lt;code&gt;SHR&lt;/code&gt; in &lt;code&gt;top&lt;/code&gt; represents the amount of &lt;strong&gt;potentially shareable&lt;/strong&gt;  memory a process is using, such as shared libraries or memory-mapped files. This does &lt;strong&gt;not&lt;/strong&gt;  mean the memory is actively shared – only that it &lt;strong&gt;could be&lt;/strong&gt;  if other processes use the same resources.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;Remember when I mentioned keeping an eye on&lt;code&gt;/dev/shm&lt;/code&gt; to monitor how much space (RAM) it’s using to prevent the OOM Killer from being invoked? You can use the &lt;code&gt;free&lt;/code&gt; command and check the &lt;code&gt;shared&lt;/code&gt; value instead of using the &lt;code&gt;df&lt;/code&gt; command for this.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;&lt;code&gt;uptime&lt;/code&gt; Command&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;uptime&lt;/code&gt; command displays how long the server has been running since its last reboot. It also shows the current time, the number of logged-in users, and the load average over the past 1, 5, and 15 minutes.&lt;/p&gt;&lt;p&gt;I previously talked about load average, which indicates how much work the CPU is handling and whether the server is under heavy load.&lt;/p&gt;&lt;p&gt;Running the &lt;code&gt;uptime&lt;/code&gt; command alone will produce output similar to this:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;12:34:56 up 10 days, 4:22, 2 users, load average: 0.45, 0.30, 0.25
&lt;/pre&gt;&lt;p&gt;Here, &lt;code&gt;12:34:56&lt;/code&gt; represents the current time, &lt;code&gt;up 10 days, 4:22&lt;/code&gt; means the server has been running for 10 days and 4 hours 22 minutes since its last reboot, &lt;code&gt;2 users&lt;/code&gt; shows the number of logged-in users, and the &lt;code&gt;load average&lt;/code&gt; values represent the server load.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;-p&lt;/code&gt; option displays the uptime in a simpler, more human-readable format:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;up 10 days, 4 hours, 22 minutes
&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;-s&lt;/code&gt; option shows the exact date and time when the server was last booted:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;2025-01-20 08:12:34
&lt;/pre&gt;&lt;p&gt;These options provide more flexibility when checking server uptime.&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;In this guide, we&apos;ve explored various commands to monitor and troubleshoot resource usage on Linux servers, with a focus on CPU, memory, storage, and I/O.&lt;/p&gt;&lt;p&gt;Understanding how your server manages resources and identifying potential bottlenecks is crucial for maintaining good performance.&lt;/p&gt;&lt;p&gt;If you found value in this guide or have any questions or feedback, please don&apos;t hesitate to share your thoughts in the &lt;strong&gt;discussion&lt;/strong&gt; section.&lt;/p&gt;&lt;p&gt;Your input is greatly appreciated, and you can also &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;contact me&lt;/a&gt; directly if you prefer.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Servers</category></item><item><title>Why Linux Servers Need Reboots and How to Avoid Downtime</title><link>https://ivansalloum.com/why-linux-servers-need-reboots-and-how-to-avoid-downtime/</link><guid isPermaLink="true">https://ivansalloum.com/why-linux-servers-need-reboots-and-how-to-avoid-downtime/</guid><description>Learn why Linux servers need reboots and how to safely reboot your production environment with minimal downtime.</description><pubDate>Fri, 17 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;Rebooting a Linux server might seem straightforward, but in a production environment, it requires careful planning to avoid downtime or disruptions.&lt;/p&gt;&lt;p&gt;Technologies like &lt;a href=&quot;https://ivansalloum.com/kernel-live-patching-for-high-availability-linux-servers/&quot;&gt;live patching&lt;/a&gt; can delay the need for a reboot and minimize downtime, but reboots are unavoidable at times.&lt;/p&gt;&lt;p&gt;While I can’t promise you’ll never need to reboot again after reading this, I will help you do so with confidence and minimal impact when necessary.&lt;/p&gt;&lt;p&gt;This guide explains why reboots are sometimes required, how to handle them correctly, and how to avoid them whenever possible.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;_I assume you&apos;re working on a properly set-up Ubuntu server. If not, check out my guide  on &lt;em&gt;&lt;a href=&quot;https://ivansalloum.com/preparing-your-ubuntu-server-for-first-use/&quot;&gt;&lt;em&gt;preparing  Ubuntu servers&lt;/em&gt;&lt;/a&gt; _  to get started.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;Why Does Linux Ask for a Reboot?&lt;/h2&gt;&lt;p&gt;While Linux is known for its stability and can run for extended periods without any issues, there are specific scenarios where a reboot becomes necessary.&lt;/p&gt;&lt;p&gt;Here are the most common reasons:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;New Kernel&lt;/strong&gt;&lt;ol&gt;&lt;li&gt;Running updates often involves installing a new kernel version, which requires a reboot to load. This is one of the most common reasons for needing a reboot.&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Critical Updates&lt;/strong&gt;&lt;ol&gt;&lt;li&gt;Some security updates are not kernel-related but still require a reboot to be fully applied.&lt;/li&gt;&lt;li&gt;Essential components, such as the glibc library or critical drivers, often require a reboot to take full effect.&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Filesystem Changes&lt;/strong&gt;&lt;ol&gt;&lt;li&gt;Significant changes to the filesystem, such as modifying the &lt;code&gt;/etc/fstab&lt;/code&gt; file, sometimes necessitate a reboot to ensure the server applies the new configurations correctly.&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Hardware Changes&lt;/strong&gt;&lt;ol&gt;&lt;li&gt;Adding or replacing hardware components, such as CPUs, GPUs, or memory, usually requires a reboot to take effect.&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Configuration Changes&lt;/strong&gt;&lt;ol&gt;&lt;li&gt;Modifications to key system settings, such as bootloader configurations (GRUB) or kernel parameters, often require a reboot to apply.&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Unexpected Crashes or Freezes&lt;/strong&gt;&lt;ol&gt;&lt;li&gt;Issues like kernel panics, hardware faults, or other unexpected failures may make a reboot necessary to restore functionality.&lt;/li&gt;&lt;/ol&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;These are some of the most common reasons, but you may face other situations as well that require a reboot while managing Linux servers.&lt;/p&gt;&lt;h2&gt;Checking If a Reboot Is Necessary&lt;/h2&gt;&lt;p&gt;Determining whether the server requires a reboot or not is your first step.&lt;/p&gt;&lt;p&gt;One simple way to check is by looking for the presence of the &lt;code&gt;/var/run/reboot-required&lt;/code&gt; file using the following command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo cat /var/run/reboot-required
&lt;/pre&gt;&lt;p&gt;This file is a signal used on Debian-based distributions, like Ubuntu, to indicate that a reboot is needed after certain package updates. If the file does not exist, it typically means no reboot is currently required.&lt;/p&gt;&lt;p&gt;Another method is to use the &lt;code&gt;needrestart&lt;/code&gt; command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ivan@vm1:~$ sudo needrestart [sudo] password for ivan: Scanning processes... Scanning linux images... Running kernel seems to be up-to-date. No services need to be restarted. No containers need to be restarted. No user sessions are running outdated binaries. No VM guests are running outdated hypervisor (qemu) binaries on this host. ivan@vm1:~$
&lt;/pre&gt;&lt;p&gt;This command examines running services, libraries, and kernel modules to determine if any of them need to be restarted or if a reboot is required.&lt;/p&gt;&lt;p&gt;If your server needs a reboot, you may also see a message every time you SSH into the server, notifying you that a reboot is required.&lt;/p&gt;&lt;h2&gt;Avoiding Downtime&lt;/h2&gt;&lt;p&gt;In the following sections, I’ll show you some ways to avoid downtime and accomplish tasks without a reboot.&lt;/p&gt;&lt;p&gt;These are just examples of how I would handle these situations, but there may be cases where a reboot is unavoidable or where these methods might not work as expected.&lt;/p&gt;&lt;h3&gt;Skipping Kernel Reboots&lt;/h3&gt;&lt;p&gt;As I mentioned earlier, some updates involve installing a new kernel version, which often includes important fixes for vulnerabilities in the older version.&lt;/p&gt;&lt;p&gt;To load the new kernel and eliminate the security risks, a reboot is required. Without it, your server would continue using the older, vulnerable kernel.&lt;/p&gt;&lt;p&gt;However, you can use a &lt;a href=&quot;https://ivansalloum.com/kernel-live-patching-for-high-availability-linux-servers/&quot;&gt;live patching service&lt;/a&gt;, which allows you to patch the running kernel in real-time without needing a reboot. While the kernel image stored on the disk (the one used during boot) remains unchanged, the running kernel is patched, closing the vulnerability.&lt;/p&gt;&lt;p&gt;This can buy you time and help you avoid an immediate reboot, but eventually, you’ll still need to reboot to update the kernel image on the disk.&lt;/p&gt;&lt;p&gt;It’s also important to note that new kernel releases often come with new features and enhancements, which you won’t benefit from until a reboot is performed.&lt;/p&gt;&lt;h3&gt;Managing Critical Updates&lt;/h3&gt;&lt;p&gt;Some security updates are not related to the kernel, and updates to essential components, such as the glibc library, may still require a reboot to be fully applied.&lt;/p&gt;&lt;p&gt;In some cases, a reboot is needed because certain services must be restarted to reflect the updates. However, instead of rebooting, you can identify and restart only the affected services manually.&lt;/p&gt;&lt;p&gt;You can use the &lt;code&gt;needrestart&lt;/code&gt; command, which I covered earlier, to identify which services need a restart after an update.&lt;/p&gt;&lt;p&gt;Once identified, you can restart these services individually, avoiding the need for a full reboot.&lt;/p&gt;&lt;h3&gt;Filesystem Changes&lt;/h3&gt;&lt;p&gt;If you make any filesystem changes, you could try remounting instead of rebooting.&lt;/p&gt;&lt;p&gt;Remounting is the process of making a filesystem or partition accessible again after it has been modified (for example, after changes to its mount options). This can be done without requiring a reboot.&lt;/p&gt;&lt;p&gt;Let me demonstrate that by changing a mount option for the root &lt;code&gt;/&lt;/code&gt; partition.&lt;/p&gt;&lt;p&gt;First, let me check which mount options are specified for the root partition in the &lt;code&gt;/etc/fstab&lt;/code&gt; file:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ivan@vm1:~$ sudo cat /etc/fstab [sudo] password for ivan: ... # UUID=93050c6c-d74b-9257-75fb-27312cd730f2 / ext4 errors=remount-ro 0 1 ... ivan@vm1:~$
&lt;/pre&gt;&lt;p&gt;As you can see, we have the &lt;code&gt;errors=remount-ro&lt;/code&gt; mount option specified for the root partition.&lt;/p&gt;&lt;p&gt;Now, I will add &lt;code&gt;noatime&lt;/code&gt; to it, which will prevent the access time of files from being updated on every read, like this:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;UUID=93050c6c-d74b-9257-75fb-27312cd730f2 / ext4 errors=remount-ro,noatime 0 1
&lt;/pre&gt;&lt;p&gt;Before remounting, let&apos;s run the &lt;code&gt;mount | grep &apos; / &apos;&lt;/code&gt; command to see the current mount options for the root partition:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;/dev/sda on / type ext4 (rw,relatime,errors=remount-ro)
&lt;/pre&gt;&lt;p&gt;As you can see, currently, we have the &lt;code&gt;relatime&lt;/code&gt; option enabled.&lt;/p&gt;&lt;p&gt;Next, let’s try remounting the root partition to apply the changes without rebooting:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ivan@vm1:~$ sudo systemctl daemon-reload ivan@vm1:~$ sudo mount -o remount /
&lt;/pre&gt;&lt;p&gt;After remounting, let&apos;s check the mount options again:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ivan@vm1:~$ sudo mount | grep &apos; / &apos; [sudo] password for ivan: /dev/sda on / type ext4 (rw,noatime,errors=remount-ro) ivan@vm1:~$
&lt;/pre&gt;&lt;p&gt;As you can see, the &lt;code&gt;noatime&lt;/code&gt; option has now been applied, and the &lt;code&gt;relatime&lt;/code&gt; option has been replaced.&lt;/p&gt;&lt;h3&gt;Tweaking Kernel Parameters&lt;/h3&gt;&lt;p&gt;Sometimes, we need to tweak kernel parameters, such as when &lt;a href=&quot;https://ivansalloum.com/kernel-hardening-securing-your-linux-server/&quot;&gt;hardening the Linux kernel&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;This typically involves adding the desired parameters with their modified values to the &lt;code&gt;/etc/sysctl.conf&lt;/code&gt; file and rebooting the server for the changes to take effect.&lt;/p&gt;&lt;p&gt;However, instead of rebooting, you can apply the changes in real-time by running:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo sysctl -p
&lt;/pre&gt;&lt;p&gt;This command reloads the parameters from the &lt;code&gt;sysctl.conf&lt;/code&gt; file without requiring a reboot.&lt;/p&gt;&lt;p&gt;Alternatively, you can use the following command to modify a parameter temporarily:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo sysctl -w [parameter]=[value]
&lt;/pre&gt;&lt;p&gt;Keep in mind that changes made using &lt;code&gt;sysctl -w&lt;/code&gt; will only persist for the current session and will be reset upon reboot.&lt;/p&gt;&lt;p&gt;That’s why it’s best to add your parameters to the &lt;code&gt;sysctl.conf&lt;/code&gt; file, apply the changes in real-time, and ensure they remain in effect even after the server reboots.&lt;/p&gt;&lt;h3&gt;Reboot After Kernel Panic&lt;/h3&gt;&lt;p&gt;Kernel panics can result in your server becoming unresponsive and require a reboot to recover.&lt;/p&gt;&lt;p&gt;To prevent downtime due to a kernel panic, you can configure the server to automatically reboot after a panic occurs. This can be done by adjusting the &lt;code&gt;kernel.panic&lt;/code&gt; kernel parameter.&lt;/p&gt;&lt;p&gt;Open the &lt;code&gt;/etc/sysctl.conf&lt;/code&gt; file and add the following line at the end of the file:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;kernel.panic = 10
&lt;/pre&gt;&lt;p&gt;This parameter ensures that the server will automatically reboot 10 seconds after a kernel panic occurs, minimizing downtime.&lt;/p&gt;&lt;p&gt;After making this change, you can apply it by running:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo sysctl -p
&lt;/pre&gt;&lt;p&gt;This way, if a kernel panic happens, the server will not stay down for too long and will automatically reboot, ensuring higher availability.&lt;/p&gt;&lt;h2&gt;Different Ways To Reboot&lt;/h2&gt;&lt;p&gt;In the following sections, I’ll walk you through several methods to reboot a Linux server, depending on the situation and the level of control you need.&lt;/p&gt;&lt;p&gt;These methods range from graceful reboots to more forceful options for when the server becomes unresponsive.&lt;/p&gt;&lt;p&gt;It’s important to choose the right method based on your server’s state and the tasks at hand.&lt;/p&gt;&lt;h3&gt;The &lt;code&gt;reboot&lt;/code&gt; Command&lt;/h3&gt;&lt;p&gt;The &lt;code&gt;reboot&lt;/code&gt; command is one of the simplest and most common ways to restart a Linux server.&lt;/p&gt;&lt;p&gt;Just type the command as is:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo reboot
&lt;/pre&gt;&lt;p&gt;It performs a graceful reboot by sending signals to terminate all processes and safely restarting the server.&lt;/p&gt;&lt;p&gt;Use the &lt;code&gt;reboot&lt;/code&gt; command for routine restarts in a production environment or when you need a safe and straightforward reboot option.&lt;/p&gt;&lt;h3&gt;&lt;strong&gt;Reboot Using&lt;code&gt;shutdown&lt;/code&gt;&lt;/strong&gt;&lt;/h3&gt;&lt;p&gt;The &lt;code&gt;shutdown&lt;/code&gt; command can also be used to restart a Linux server. It allows you to schedule a reboot and notify users about the upcoming restart.&lt;/p&gt;&lt;p&gt;Use this command for an immediate reboot:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo shutdown -r now
&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;-r&lt;/code&gt; option is used to indicate a reboot.&lt;/p&gt;&lt;p&gt;You can also schedule a reboot with a delay, like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo shutdown -r +5 &amp;quot;Server will reboot in 5 minutes&amp;quot;
&lt;/pre&gt;&lt;p&gt;It performs a graceful shutdown by notifying users and stopping services before restarting the server.&lt;/p&gt;&lt;p&gt;Use the &lt;code&gt;shutdown&lt;/code&gt; command for scheduled reboots or when you want to give users a heads-up before the server restarts.&lt;/p&gt;&lt;h3&gt;Reboot Using &lt;code&gt;init&lt;/code&gt; or &lt;code&gt;systemctl&lt;/code&gt;&lt;/h3&gt;&lt;p&gt;You can reboot a Linux server using either &lt;code&gt;init&lt;/code&gt; or &lt;code&gt;systemctl&lt;/code&gt;, depending on the system&apos;s initialization method.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;init&lt;/code&gt; command is used to switch between runlevels, and runlevel 6 triggers a server reboot by reinitializing all processes. It’s mainly used on older systems with SysVinit, and while it can be useful for advanced users, it should be used cautiously in production environments.&lt;/p&gt;&lt;p&gt;The command for a reboot using &lt;code&gt;init&lt;/code&gt; is:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo init 6
&lt;/pre&gt;&lt;p&gt;When you use &lt;code&gt;init 6&lt;/code&gt;, all running processes are stopped and the server is restarted.&lt;/p&gt;&lt;p&gt;However, this approach might not always stop certain processes gracefully, making it a less ideal choice for production servers unless absolutely necessary.&lt;/p&gt;&lt;p&gt;For modern servers that use Systemd, the &lt;code&gt;systemctl&lt;/code&gt; command provides more control over the reboot process.&lt;/p&gt;&lt;p&gt;Using the &lt;code&gt;sudo systemctl reboot&lt;/code&gt; command ensures that systemd-managed services are stopped gracefully before the reboot, making it a safer and more reliable option for newer distributions.&lt;/p&gt;&lt;h3&gt;Reboot via SSH&lt;/h3&gt;&lt;p&gt;You can reboot a Linux server remotely using SSH without needing physical access.&lt;/p&gt;&lt;p&gt;This is especially useful in situations where the server becomes unresponsive, and a reboot is required to restore functionality.&lt;/p&gt;&lt;p&gt;To reboot a server via SSH, simply connect to the server using your SSH client and run the &lt;code&gt;reboot&lt;/code&gt; command, like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ssh [user]@[server-ip] &amp;quot;sudo reboot&amp;quot;
&lt;/pre&gt;&lt;p&gt;Instead of using the &lt;code&gt;reboot&lt;/code&gt; command, you could also run any other reboot command, such as &lt;code&gt;shutdown&lt;/code&gt; or &lt;code&gt;systemctl&lt;/code&gt;.&lt;/p&gt;&lt;h3&gt;Reboot Through Server Provider&lt;/h3&gt;&lt;p&gt;If you are managing a server from a server provider, like Hetzner, you should have an option to reboot the server from the cloud dashboard. Some providers offer two reboot options: a &lt;strong&gt;soft reboot&lt;/strong&gt; and a &lt;strong&gt;hard reboot&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;New to Hetzner? &lt;a href=&quot;https://hetzner.cloud/?ref=MC4Yy318xX5X&quot;&gt;Use my link&lt;/a&gt; to get free credits!&lt;/p&gt;&lt;p&gt;A &lt;strong&gt;soft reboot&lt;/strong&gt; initiates a restart through the system, allowing it to safely stop services and processes, similar to running the &lt;code&gt;reboot&lt;/code&gt; command.&lt;/p&gt;&lt;p&gt;In contrast, a &lt;strong&gt;hard reboot&lt;/strong&gt; is essentially a forced power cycle, which can be used when the server is unresponsive and cannot be rebooted gracefully.&lt;/p&gt;&lt;p&gt;A &lt;strong&gt;hard reboot&lt;/strong&gt; can sometimes result in data loss or file system corruption, so it should be used cautiously.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;Hard and soft reboots are more commonly available for dedicated physical servers. For virtual private servers (VPS), the options are typically limited to a soft reboot.&lt;/p&gt;&lt;h2&gt;How to Reboot Safely&lt;/h2&gt;&lt;p&gt;Rebooting a Linux server in a production environment requires careful preparation to minimize disruption.&lt;/p&gt;&lt;p&gt;The first and most critical step is to notify all stakeholders about the reboot. For logged-in users on the server, you can use the &lt;code&gt;wall&lt;/code&gt; command to broadcast a system-wide message, like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo wall &amp;quot;Server rebooting at 4 AM.&amp;quot;
&lt;/pre&gt;&lt;p&gt;This ensures that users are aware of the reboot and have time to save their work.&lt;/p&gt;&lt;p&gt;If you’re running a hosting business or managing services for clients, it’s equally important to notify customers and anyone else who could be affected by the downtime.&lt;/p&gt;&lt;p&gt;Next, it’s essential to verify the server’s health. Use commands like &lt;code&gt;top&lt;/code&gt;, &lt;code&gt;df -h&lt;/code&gt;, and &lt;code&gt;free -m&lt;/code&gt; to check CPU, memory usage, and disk space.&lt;/p&gt;&lt;p&gt;Tools like &lt;code&gt;htop&lt;/code&gt; provide a more detailed, real-time overview of the server, helping you identify any resource bottlenecks.&lt;/p&gt;&lt;p&gt;These checks provide an overview of the server’s performance and help identify any issues that might need attention before the reboot.&lt;/p&gt;&lt;p&gt;Before proceeding with the reboot, stop any critical services gracefully.&lt;/p&gt;&lt;p&gt;Services such as web servers (Apache or Nginx) and databases (MySQL) should be stopped manually to prevent data corruption or other issues. For example:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo systemctl stop nginx sudo systemctl stop mysql
&lt;/pre&gt;&lt;p&gt;Keep in mind, however, that some reboot commands we’ve covered – like &lt;code&gt;reboot&lt;/code&gt; and &lt;code&gt;shutdown&lt;/code&gt; – already stop services gracefully as part of the reboot process. If you’re using one of these commands, this step may not always be necessary, but it’s a good habit to verify and manually stop any especially critical services beforehand.&lt;/p&gt;&lt;p&gt;Step four is to ensure you have up-to-date backups available. Before rebooting, always verify that critical data and configurations are backed up in case anything goes wrong during the reboot process. This precaution will help you recover quickly if needed.&lt;/p&gt;&lt;p&gt;Finally, proceed with the reboot using your preferred method. Once the server restarts, verify that all services have come back online and are running as expected. Check the status of essential services such as Apache, MySQL, or Nginx, and inspect the logs for any errors that might have occurred during the reboot process.&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;Rebooting a Linux server, especially in production, is not a task to take lightly.&lt;/p&gt;&lt;p&gt;By understanding why reboots are necessary, planning thoroughly, and leveraging alternatives where possible, you can minimize downtime and ensure a smooth process.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;For more comprehensive Linux server security resources, be sure to check out the full collection of detailed guides &lt;a href=&quot;https://ivansalloum.com/collections/linux-server-security/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;If you found value in this guide or have any questions or feedback, please don&apos;t hesitate to share your thoughts in the &lt;strong&gt;discussion&lt;/strong&gt; section.&lt;/p&gt;&lt;p&gt;Your input is greatly appreciated, and you can also &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;contact me&lt;/a&gt; directly if you prefer.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Security</category><category>Servers</category></item><item><title>Auditing Linux Servers with Auditd</title><link>https://ivansalloum.com/auditing-linux-servers-with-auditd/</link><guid isPermaLink="true">https://ivansalloum.com/auditing-linux-servers-with-auditd/</guid><description>Explore how to use Auditd to monitor and audit activities on Linux servers for improved security and compliance</description><pubDate>Mon, 30 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;Monitoring and &lt;a href=&quot;https://ivansalloum.com/collections/linux-server-security/&quot;&gt;securing&lt;/a&gt; a Linux server is essential for administrators who want to protect their servers from unauthorized access, suspicious activities, or unintended changes.&lt;/p&gt;&lt;p&gt;This is where &lt;strong&gt;Auditd&lt;/strong&gt; , the Linux Audit Daemon, comes into play. It is a powerful tool that allows you to track system events, monitor file access, and keep a detailed record of what happens on your Linux server.&lt;/p&gt;&lt;p&gt;In this guide, you will learn what Auditd is, how it works, and how to use it effectively to audit and secure your Linux servers.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;_I assume you&apos;re working on a properly set-up Ubuntu server. If not, check out my guide  on &lt;em&gt;&lt;a href=&quot;https://ivansalloum.com/preparing-your-ubuntu-server-for-first-use/&quot;&gt;&lt;em&gt;preparing  Ubuntu servers&lt;/em&gt;&lt;/a&gt; _  to get started.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;What is Auditd?&lt;/h2&gt;&lt;p&gt;Auditd is the user-space part of the &lt;strong&gt;Linux Auditing System&lt;/strong&gt; , a built-in feature of the Linux kernel. It is a tool that helps you monitor and log events happening on your Linux server, making it easier to respond to and investigate security incidents.&lt;/p&gt;&lt;p&gt;Based on pre-defined rules, Auditd generates log entries to record as much information about the events that are happening on your server as possible.&lt;/p&gt;&lt;p&gt;Auditd can record important actions, like who accessed a file, which commands were run, or if someone made changes to critical system configuration files.&lt;/p&gt;&lt;p&gt;It works by listening for specific events triggered by the Linux kernel, such as:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;File access (read, write, execute).&lt;/li&gt;&lt;li&gt;System call activities (what processes are doing).&lt;/li&gt;&lt;li&gt;User logins and authentication attempts.&lt;/li&gt;&lt;li&gt;Changes to files, directories, or system configurations.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Once these events are detected, Auditd logs them into a file located at:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;/var/log/audit/audit.log
&lt;/pre&gt;&lt;p&gt;For example, if you want to know who modified the &lt;code&gt;/etc/passwd&lt;/code&gt; file, Auditd can log the &lt;strong&gt;who, what, and when&lt;/strong&gt; of that event.&lt;/p&gt;&lt;p&gt;In simple terms, Auditd gives you the ability to watch over your Linux server, record important activities, and keep track of changes to ensure everything is secure and under control.&lt;/p&gt;&lt;h2&gt;Auditd Architecture&lt;/h2&gt;&lt;p&gt;Linux distributions are divided into two key areas: &lt;strong&gt;kernel space&lt;/strong&gt; , which is the core of the operating system, and &lt;strong&gt;user space&lt;/strong&gt; , where user-level programs (like your web server, text editor, or any software you install) and services run.&lt;/p&gt;&lt;p&gt;The separation between the kernel and user-level programs helps ensure high security and stability, which is one of the main reasons Linux is so reliable.&lt;/p&gt;&lt;p&gt;This means that user-level programs cannot directly access system resources like hardware or kernel-controlled functions. Instead, they rely on &lt;strong&gt;system calls&lt;/strong&gt; (syscalls) to ask the kernel to perform tasks for them.&lt;/p&gt;&lt;p&gt;Auditd operates in user space, meaning it runs outside the kernel but works closely with kernel features.&lt;/p&gt;&lt;p&gt;The Linux Auditing System, integrated into the Linux kernel, provides a framework for tracking and logging various activities on the server, especially security-related ones.&lt;/p&gt;&lt;p&gt;The kernel generates auditing data and sends it to Auditd in user space for processing.&lt;/p&gt;&lt;p&gt;As the user-space component of the Linux Auditing System, Auditd collects and manages this data, allowing administrators to review and analyze security-related activities happening on the server.&lt;/p&gt;&lt;p&gt;This seamless interaction between the kernel and Auditd ensures robust monitoring and auditing capabilities built into the Linux server.&lt;/p&gt;&lt;h2&gt;Installing Auditd&lt;/h2&gt;&lt;p&gt;For Debian-based distributions, like Ubuntu, you can install the latest version of Auditd along with its relevant plugins by running:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt install auditd audispd-plugins
&lt;/pre&gt;&lt;p&gt;This command ensures that both the core auditing functionality and additional plugins are installed.&lt;/p&gt;&lt;p&gt;After installation, the &lt;code&gt;auditd&lt;/code&gt; service (daemon) is added. It is enabled and running by default, which you can verify using the &lt;code&gt;sudo systemctl status auditd.service&lt;/code&gt; command.&lt;/p&gt;&lt;p&gt;Now, let’s check whether any Auditd rules are in effect by using the &lt;code&gt;sudo auditctl -l&lt;/code&gt; command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ivan@vm1:~$ sudo auditctl -l [sudo] password for ivan: No Rules ivan@vm1:~$
&lt;/pre&gt;&lt;p&gt;As you see, we haven’t created any rules yet since we just installed Auditd.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;auditctl&lt;/code&gt; command is what we use to manage rules, and the &lt;code&gt;-l&lt;/code&gt; option lists the rules.&lt;/p&gt;&lt;h2&gt;Monitoring File Changes&lt;/h2&gt;&lt;p&gt;Let&apos;s say that I want to monitor the &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; file for any changes.&lt;/p&gt;&lt;p&gt;This file is critical because it contains the configuration settings for the SSH service, which controls remote access to our server. Unauthorized modifications to it could lead to a security breach or unauthorized access.&lt;/p&gt;&lt;p&gt;To monitor changes to the file, we use the &lt;code&gt;auditctl&lt;/code&gt; command to create a rule:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo auditctl -w /etc/ssh/sshd_config -p rwa -k sshd_changes
&lt;/pre&gt;&lt;p&gt;Here’s what each option does:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;-w&lt;/code&gt;&lt;/strong&gt; : This stands for &amp;quot;watch&amp;quot;, and it points to the object we want to monitor. In our case, it specifies the file to watch.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;-p rwa&lt;/code&gt;&lt;/strong&gt; : This indicates the object&apos;s permissions we want to monitor. In our case, these are:&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;r&lt;/code&gt;&lt;/strong&gt; : Read access to the file (when someone reads the contents).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;w&lt;/code&gt;&lt;/strong&gt; : Write permissions (modifications to the file content).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;a&lt;/code&gt;&lt;/strong&gt; : Attribute changes (metadata changes such as ownership or permissions).&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;-k sshd_changes&lt;/code&gt;&lt;/strong&gt; : Assigns a unique key (name) to the rule for easier searching in logs.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;To confirm the rule was created, we list our rules again using the &lt;code&gt;sudo auditctl -l&lt;/code&gt; command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ivan@vm1:~$ sudo auditctl -l [sudo] password for ivan: -w /etc/ssh/sshd_config -p rwa -k sshd_changes ivan@vm1:~$
&lt;/pre&gt;&lt;p&gt;Alright, our rule is there.&lt;/p&gt;&lt;p&gt;Now, let&apos;s test our rule to see how Auditd behaves when we interact with the &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; file.&lt;/p&gt;&lt;p&gt;To generate a read event, we&apos;ll use the &lt;code&gt;cat&lt;/code&gt; command to simply view the contents of the &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; file:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo cat /etc/ssh/sshd_config
&lt;/pre&gt;&lt;p&gt;Next, let&apos;s modify the file to generate a write event. This could be something simple, like adding a comment to the file:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo sh -c &apos;echo &amp;quot;# Comment&amp;quot; &amp;gt;&amp;gt; /etc/ssh/sshd_config&apos;
&lt;/pre&gt;&lt;p&gt;Finally, let&apos;s change the file&apos;s permissions to generate an attribute change** event:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo chmod 600 /etc/ssh/sshd_config
&lt;/pre&gt;&lt;p&gt;This command changes the file&apos;s permissions so that only the root user has read and write access.&lt;/p&gt;&lt;p&gt;Now that we&apos;ve interacted with the &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; file, we need to check the Auditd logs to verify that the events were captured correctly.&lt;/p&gt;&lt;h2&gt;Analyzing Rule Logs&lt;/h2&gt;&lt;p&gt;Let&apos;s analyze the logs that were generated by Auditd after we tested our rule.&lt;/p&gt;&lt;p&gt;We can use the &lt;code&gt;ausearch&lt;/code&gt; command with the &lt;code&gt;-i&lt;/code&gt; option to interpret the logs in a human-readable format and the &lt;code&gt;-k&lt;/code&gt; option to filter by the key we assigned like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ausearch -i -k sshd_changes
&lt;/pre&gt;&lt;p&gt;This will display all the events related to the &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; file that were triggered by the rule we created.&lt;/p&gt;&lt;p&gt;You can also use &lt;code&gt;aureport&lt;/code&gt; to generate a summary of the generated logs filtered by the key:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo aureport -i -k | grep &amp;quot;sshd_changes&amp;quot;
&lt;/pre&gt;&lt;p&gt;This will give you a summarized report of the events that were captured, making it easier to review the activity related to the file.&lt;/p&gt;&lt;p&gt;Let&apos;s start by using the &lt;code&gt;ausearch&lt;/code&gt; command. The first log entry you may see is something like this:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;type=PROCTITLE .... : proctitle=auditctl -w /etc/ssh/sshd_config -p rwa -k sshd_changes .... type=CONFIG_CHANGE .... : auid=ivan ses=345 subj=unconfined op=add_rule key=sshd_changes list=exit res=yes
&lt;/pre&gt;&lt;p&gt;This log entry indicates that the &lt;code&gt;auditctl&lt;/code&gt; command itself was executed, which created the rule we applied to monitor the &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; file. The &lt;code&gt;type=CONFIG_CHANGE&lt;/code&gt; line indicates that the rule was successfully added, and it also logs the rule&apos;s configuration.&lt;/p&gt;&lt;p&gt;Next, let&apos;s take a look at the second log entry, which corresponds to our test where we used the &lt;code&gt;cat&lt;/code&gt; command to read the contents of the file:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;type=PROCTITLE .... : proctitle=cat /etc/ssh/sshd_config type=PATH .... : item=0 name=/etc/ssh/sshd_config inode=66811 dev=08:00 mode=file,644 ouid=root ogid=root rdev=00:00 nametype=NORMAL cap_fp=none cap_fi=none cap_fe=0 cap_fver=0 cap_frootid=0 type=CWD .... : cwd=/home/ivan type=SYSCALL .... : arch=x86_64 syscall=openat success=yes exit=3 a0=AT_FDCWD a1=0x7ffeb3cb07b2 a2=O_RDONLY a3=0x0 items=1 ppid=34033 pid=34034 auid=ivan uid=root gid=root euid=root suid=root fsuid=root egid=root sgid=root fsgid=root tty=pts1 ses=345 comm=cat exe=/usr/bin/cat subj=unconfined key=sshd_changes
&lt;/pre&gt;&lt;p&gt;Let’s break it down:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;type=PROCTITLE&lt;/code&gt;&lt;/strong&gt; : This line shows the command that was executed.&lt;/li&gt;&lt;li&gt;&lt;code&gt;**type=PATH**&lt;/code&gt;: This indicates that the &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; file was accessed.&lt;/li&gt;&lt;li&gt;&lt;code&gt;**type=CWD**&lt;/code&gt;: This shows the current working directory when the command was run, which was &lt;code&gt;/home/ivan&lt;/code&gt; in this case.&lt;/li&gt;&lt;li&gt;&lt;code&gt;**type=SYSCALL**&lt;/code&gt;: This entry shows the system call made during the command execution. The &lt;code&gt;openat&lt;/code&gt; syscall was used to open the &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; file for reading (&lt;code&gt;0_RDONLY&lt;/code&gt;). It also contains details like the user (&lt;code&gt;auid=ivan&lt;/code&gt;), process ID, and command that was executed (&lt;code&gt;cat&lt;/code&gt; in this case), as well as the success status of the action (&lt;code&gt;success=yes&lt;/code&gt;).&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;There should be three additional log entries corresponding to the last two tests we performed.&lt;/p&gt;&lt;p&gt;For the write test, when we attempted to add a comment to the &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; file using the &lt;code&gt;sudo sh -c &apos;echo &amp;quot;# Comment&amp;quot; &amp;gt;&amp;gt; /etc/ssh/sshd_config&apos;&lt;/code&gt; command, two log entries were created:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The first log entry reflects the initial failed attempt. This entry shows the &lt;code&gt;-bash&lt;/code&gt; command and an &lt;code&gt;openat&lt;/code&gt; syscall with the &lt;code&gt;O_WRONLY|O_CREAT|O_APPEND&lt;/code&gt; flags, but the operation was unsuccessful (&lt;code&gt;exit=EACCES (Permission denied)&lt;/code&gt;) because the user Ivan (&lt;code&gt;auid=ivan uid=ivan&lt;/code&gt;) did not have permission to modify the file directly. This happens because the redirection (&lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;) is handled by the shell, and without &lt;code&gt;sudo&lt;/code&gt;, it runs as the unprivileged user.&lt;/li&gt;&lt;li&gt;The second log entry corresponds to the actual execution of the command using &lt;code&gt;sudo sh -c&lt;/code&gt;. Here, the command was executed with elevated privileges (&lt;code&gt;auid=ivan uid=root&lt;/code&gt;), and the &lt;code&gt;openat&lt;/code&gt; syscall succeeded (&lt;code&gt;exit=3&lt;/code&gt;), allowing the file modification to proceed.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This dual-entry behavior is a result of how the shell and redirection operate in conjunction with privilege elevation via the &lt;code&gt;sudo&lt;/code&gt; command.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;&lt;code&gt;auid&lt;/code&gt; (Audit User ID) represents the original user ID associated with the session, while &lt;code&gt;uid&lt;/code&gt; (User ID) represents the effective user ID of the process that generated the event.&lt;/p&gt;&lt;p&gt;Finally, for the attribute change test, when we modified the file&apos;s permissions using the &lt;code&gt;chmod&lt;/code&gt; command, you should expect a log entry that includes the &lt;code&gt;chmod&lt;/code&gt; command along with a &lt;code&gt;fchmodat&lt;/code&gt; syscall.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;If you&apos;re curious about the various syscalls and their details, you can check out a comprehensive list of syscalls &lt;a href=&quot;https://filippo.io/linux-syscall-table/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Now, let&apos;s take a look at the logs generated by the &lt;code&gt;aureport&lt;/code&gt; command to summarize the events we captured.&lt;/p&gt;&lt;p&gt;Here&apos;s a sample of the report output:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;1237\. 12/20/2024 11:18:49 sshd_changes yes /usr/sbin/auditctl ivan 45451 1238\. 12/20/2024 11:25:38 sshd_changes yes /usr/bin/cat ivan 45887 1239\. 12/20/2024 11:31:43 sshd_changes no /usr/bin/bash ivan 46283 1240\. 12/20/2024 11:31:45 sshd_changes yes /usr/bin/dash ivan 46284 1241\. 12/20/2024 11:31:49 sshd_changes yes /usr/bin/chmod ivan 46299
&lt;/pre&gt;&lt;p&gt;Each of these entries shows that the relevant actions (read, write, and attribute change) were successfully logged.&lt;/p&gt;&lt;p&gt;Let’s pipe the output of &lt;code&gt;aureport&lt;/code&gt; into &lt;code&gt;head&lt;/code&gt; instead of &lt;code&gt;grep&lt;/code&gt; so that we can view the column headers:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ivan@vm1:~$ sudo aureport -i -k | head [sudo] password for ivan: Key Report =============================================== # date time key success exe auid event =============================================== ... ivan@vm1:~$
&lt;/pre&gt;&lt;p&gt;The status in the &lt;code&gt;success&lt;/code&gt; column will be either &lt;code&gt;yes&lt;/code&gt; or &lt;code&gt;no&lt;/code&gt;, depending on whether the user successfully performed an action that triggered a rule violation.&lt;/p&gt;&lt;p&gt;If the event wasn’t caused by a rule being triggered, the status may appear as a question mark.&lt;/p&gt;&lt;p&gt;You may see multiple log events when opening or editing a file with a text editor because the various syscalls made by the editor are logged individually by Auditd.&lt;/p&gt;&lt;h2&gt;Monitoring Directory Changes&lt;/h2&gt;&lt;p&gt;In addition to monitoring individual files, Auditd allows you to monitor entire directories for changes. This is useful for tracking activities like the creation or deletion of files, changes to file attributes, and any modifications within sensitive directories.&lt;/p&gt;&lt;p&gt;Imagine a team of administrators needs a secure directory called &lt;code&gt;secureadmins&lt;/code&gt; to store critical configuration files that should only be accessible to the administrators Ivan and Elie. This directory must be highly secure to prevent unauthorized access.&lt;/p&gt;&lt;p&gt;The commands we are going to use ensure that only the &lt;code&gt;secureadmins&lt;/code&gt; group can access the directory.&lt;/p&gt;&lt;p&gt;First, create a group for the admins:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo groupadd secureadmins
&lt;/pre&gt;&lt;p&gt;After that, add users Ivan and Elie to the group:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo usermod -a -G secureadmins ivan sudo usermod -a -G secureadmins elie
&lt;/pre&gt;&lt;p&gt;Then, create the directory:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo mkdir /secureadmins
&lt;/pre&gt;&lt;p&gt;Next, set the owner and group for the directory:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo chown nobody:secureadmins /secureadmins
&lt;/pre&gt;&lt;p&gt;This assigns the ownership of the directory to:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Owner&lt;/strong&gt; : &lt;code&gt;nobody&lt;/code&gt;, which prevents any specific user from having special control.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Group&lt;/strong&gt; : &lt;code&gt;secureadmins&lt;/code&gt;, so group members (Ivan and Elie) can access it.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Finally, set directory permissions:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo chmod 3770 /secureadmins
&lt;/pre&gt;&lt;p&gt;These permissions ensure the following:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The SetGID ensures that files created in the directory inherit the group ownership.&lt;/li&gt;&lt;li&gt;The Sticky Bit prevents users from deleting or renaming files they don’t own.&lt;/li&gt;&lt;li&gt;Only members of &lt;code&gt;secureadmins&lt;/code&gt; can access, modify, or create files in the directory.&lt;/li&gt;&lt;li&gt;No access for anyone outside the group.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;You can confirm the directory’s permissions with this command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ls -ld /secureadmins/
&lt;/pre&gt;&lt;p&gt;The output should look like this:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;drwxrws--T 2 nobody secureadmins 4096 Dec 27 10:00 /secureadmins/
&lt;/pre&gt;&lt;p&gt;Only Ivan, Elie, and processes explicitly running as part of the &lt;code&gt;secureadmins&lt;/code&gt; group can access and modify files in the &lt;code&gt;/secureadmins&lt;/code&gt; directory.&lt;/p&gt;&lt;p&gt;Now, to monitor the &lt;code&gt;/secureadmins&lt;/code&gt; directory, run the following command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo auditctl -w /secureadmins/ -k secureadmins_monitor
&lt;/pre&gt;&lt;p&gt;This time, I left out the &lt;code&gt;-p&lt;/code&gt; option because I want track all actions (read, write, execute, and attribute changes) within the &lt;code&gt;/secureadmins&lt;/code&gt; directory.&lt;/p&gt;&lt;p&gt;However, if I only want to monitor for when someone tries to &lt;code&gt;cd&lt;/code&gt; into the directory, I would use the following command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo auditctl -w /secureadmins/ -p x -k secureadmins_monitor
&lt;/pre&gt;&lt;p&gt;Now, perform actions like creating, modifying, or deleting files in &lt;code&gt;/secureadmins/&lt;/code&gt; as one of the group members to generate audit events. You can also change the directory&apos;s attributes, like permissions, to test attribute change logging.&lt;/p&gt;&lt;p&gt;Use the &lt;code&gt;ausearch&lt;/code&gt; command to review logs for the &lt;code&gt;secureadmins_monitor&lt;/code&gt; key:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ausearch -i -k secureadmins_monitor
&lt;/pre&gt;&lt;p&gt;Or generate a summary of the logged events using the &lt;code&gt;aureport&lt;/code&gt; command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo aureport -i -k | grep &amp;quot;secureadmins_monitor&amp;quot;
&lt;/pre&gt;&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;If you see the root user when using the&lt;code&gt;aureport&lt;/code&gt; command, it&apos;s because you switched to the user using the &lt;code&gt;su&lt;/code&gt; command from root, instead of opening a new SSH session and logging in directly as that user.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;Auditing Syscalls&lt;/h2&gt;&lt;p&gt;Now, let&apos;s say I want to monitor when someone performs a certain action. To do this, I need to use syscalls.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;A syscall happens whenever a user issues a command that requests the Linux kernel to perform a task, as I mentioned in the &lt;strong&gt;Auditd Architecture&lt;/strong&gt; section.&lt;/p&gt;&lt;p&gt;This isn&apos;t difficult, but the syntax is a bit trickier compared to monitoring files or directories.&lt;/p&gt;&lt;p&gt;With this rule, I want to monitor whenever Ivan attempts to open or create a file:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo auditctl -a always,exit -F arch=b64 -F auid=1001 -S openat -k monitor_ivan
&lt;/pre&gt;&lt;p&gt;Here’s what each option does:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;**-a always,exit**&lt;/code&gt;: It tells Auditd to &lt;code&gt;always&lt;/code&gt; log the event when the specified syscall happens, and to do so on &lt;code&gt;exit&lt;/code&gt;, meaning when the syscall finishes.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;-F&lt;/code&gt;&lt;/strong&gt; : It is used to build a rule field, and we can see two rule fields in this command:&lt;ul&gt;&lt;li&gt;&lt;code&gt;**arch=b64**&lt;/code&gt;: This first rule field specifies the server&apos;s CPU architecture. &lt;code&gt;b64&lt;/code&gt; means that the server is running on a 64-bit x86_64 architecture. For 32-bit servers, use &lt;code&gt;b32&lt;/code&gt;, but &lt;code&gt;b64&lt;/code&gt; is what you will mostly see nowadays.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;auid=1001&lt;/code&gt;&lt;/strong&gt; : This second rule field specifies the user ID number of the user that we want to monitor.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;li&gt;&lt;code&gt;**-S openat**&lt;/code&gt;: This specifies the syscall to monitor. In this case, it is the &lt;code&gt;openat&lt;/code&gt; syscall, which is used to open or create a file.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;You can use the &lt;code&gt;id&lt;/code&gt; command to find the &lt;code&gt;auid&lt;/code&gt; of a specific user.&lt;/p&gt;&lt;p&gt;Now, I will access the server as the user Ivan and run the &lt;code&gt;cat&lt;/code&gt; command on the &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; file. Before doing so, I want to check if Auditd has already generated any logs. To do this, I use the command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ausearch -i -k monitor_ivan
&lt;/pre&gt;&lt;p&gt;To my surprise, I see numerous log entries, even though Ivan hasn’t done anything on the server yet.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;In the second log entry, I see that Ivan accessed the &lt;code&gt;/etc/passwd&lt;/code&gt; file.&lt;/li&gt;&lt;li&gt;In the third log entry, I see that Ivan accessed the &lt;code&gt;/etc/login.defs&lt;/code&gt; file.&lt;/li&gt;&lt;li&gt;Ivan also accessed the &lt;code&gt;/etc/group&lt;/code&gt; file.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;However, Ivan didn’t choose to access these files directly. These actions were performed automatically by the server during the normal login process.&lt;/p&gt;&lt;p&gt;If I now use the &lt;code&gt;cat&lt;/code&gt; command to see the content of the &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; file, it will generate even more log entries because running this command requires the server to load additional files in the background.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;This highlights an important point:&lt;/strong&gt; when creating Auditd rules, you need to carefully consider what you want to monitor. If your rules are too broad, you might end up with an overwhelming number of log entries, making it difficult to find the information you actually need.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;To effectively monitor someone’s actions, you should create more specific rules to avoid collecting excessive, irrelevant information.&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;Making Rules Persistent&lt;/h2&gt;&lt;p&gt;By default, any rule we add from the command line will only persist until we reboot the server. Once we reboot the server, these rules will be lost and the monitoring will stop.&lt;/p&gt;&lt;p&gt;This is because the &lt;code&gt;auditctl&lt;/code&gt; command modifies the runtime configuration of Auditd, meaning the changes are active for the current session but do not persist after a reboot.&lt;/p&gt;&lt;p&gt;To ensure rules persist after a reboot, we need to save them to a rule file inside the &lt;code&gt;/etc/audit/rules.d/&lt;/code&gt; directory. For example, create a file called &lt;code&gt;custom.rules&lt;/code&gt; and add our previous rule like this:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;-w /secureadmins/ -k secureadmins_monitor
&lt;/pre&gt;&lt;p&gt;Next, restart the &lt;code&gt;auditd&lt;/code&gt; service to apply the rule:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo systemctl restart auditd.service
&lt;/pre&gt;&lt;p&gt;Restarting the &lt;code&gt;auditd&lt;/code&gt; service inserts all rules into the &lt;code&gt;audit.rules&lt;/code&gt; file located in the &lt;code&gt;/etc/audit/&lt;/code&gt; directory. Whether the rules are in a single file or spread across multiple files in &lt;code&gt;/etc/audit/rules.d/&lt;/code&gt;, Auditd reads them all.&lt;/p&gt;&lt;p&gt;If you examine the &lt;code&gt;/etc/audit/audit.rules&lt;/code&gt; file after restarting, you&apos;ll find the rules appended at the end, like this:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;## This file is automatically generated from /etc/audit/rules.d -D -b 8192 -f 1 \--backlog_wait_time 60000 -w /secureadmins/ -k secureadmins_monitor
&lt;/pre&gt;&lt;p&gt;Here’s what each part of the file means:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;**-D**&lt;/code&gt;: Deletes all current rules to start with a clean slate. This means any rules added via the command line will be removed during the service restart (or reboot) since restarting it will read the &lt;code&gt;audit.rules&lt;/code&gt; file again.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;-b 8192&lt;/code&gt;&lt;/strong&gt; : Sets the size of the audit backlog buffer. A larger buffer prevents event loss during high log generation. If the buffers fill up, the server stops generating audit events.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;--backlog_wait_time 60000&lt;/code&gt;&lt;/strong&gt; : Sets the backlog wait time to 60 seconds. This determines how long a process waits for buffer space before dropping audit events.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;-f&lt;/code&gt;&lt;/strong&gt; : Defines the failure mode for critical errors:&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;0&lt;/code&gt;&lt;/strong&gt; (Silent): Ignores errors and logs them silently, prioritizing uptime.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;1&lt;/code&gt;&lt;/strong&gt; (Default): Logs errors and continues running, useful for general-purpose servers.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;2&lt;/code&gt;&lt;/strong&gt; (Panic): Halts the system for high-security environments, ensuring no operations proceed without auditing.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The last line in the file is our rule, added from the &lt;code&gt;/etc/audit/rules.d/custom.rules&lt;/code&gt; file.&lt;/p&gt;&lt;p&gt;The first four lines in &lt;code&gt;audit.rules&lt;/code&gt; come from the default &lt;code&gt;/etc/audit/rules.d/audit.rules&lt;/code&gt; file. To modify these settings, such as changing the failure mode to &lt;code&gt;-f 2&lt;/code&gt;, edit the &lt;code&gt;/etc/audit/rules.d/audit.rules&lt;/code&gt; file and restart the &lt;code&gt;auditd&lt;/code&gt; service.&lt;/p&gt;&lt;p&gt;Finally, I want to introduce another way to manage rules using the &lt;code&gt;augenrules&lt;/code&gt; command, which reads rules from the &lt;code&gt;/etc/audit/rules.d/&lt;/code&gt; directory.&lt;/p&gt;&lt;p&gt;Let’s say you’ve just installed Auditd and added your rules using rule files in the &lt;code&gt;/etc/audit/rules.d/&lt;/code&gt; directory. Once your rules are in place, you want to make them persistent across reboots and apply them.&lt;/p&gt;&lt;p&gt;Here’s how:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ivan@vm1:~$ sudo auditctl -l [sudo] password for ivan: No rules ivan@vm1:~$ sudo vim /etc/audit/rules.d/custom.rules ivan@vm1:~$ sudo augenrules --check /usr/sbin/augenrules: Rules have changed and should be updated ivan@vm1:~$ sudo augenrules --load ... ivan@vm1:~$
&lt;/pre&gt;&lt;p&gt;First, we check for any changes. If there are changes, the command will detect them. Then, we use the &lt;code&gt;--load&lt;/code&gt; option to apply the changes. This automatically appends any newly added rules to the end of the &lt;code&gt;/etc/audit/audit.rules&lt;/code&gt; file. If you remove a rule and rerun the commands, it will detect and apply the change as well.&lt;/p&gt;&lt;p&gt;The great thing about this approach is that you can check for changes and apply them without restarting the &lt;code&gt;auditd&lt;/code&gt; service.&lt;/p&gt;&lt;h2&gt;Using Predefined Rulesets&lt;/h2&gt;&lt;p&gt;In the &lt;code&gt;/usr/share/doc/auditd/examples/rules&lt;/code&gt; directory, you’ll find several predefined rulesets. To use any of these rulesets, simply copy the relevant &lt;code&gt;.rules&lt;/code&gt; file to the &lt;code&gt;/etc/audit/rules.d/&lt;/code&gt; directory.&lt;/p&gt;&lt;p&gt;For example:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo cp /usr/share/doc/auditd/examples/rules/30-pci-dss-v31.rules /etc/audit/rules.d/
&lt;/pre&gt;&lt;p&gt;Once copied, restart the &lt;code&gt;auditd&lt;/code&gt; service to apply the new rules, or use the &lt;code&gt;sudo augenrules --load&lt;/code&gt; command.&lt;/p&gt;&lt;p&gt;If you find that a particular rule doesn’t work for your setup or needs to be adjusted, you can easily modify the rule file by commenting out unnecessary lines.&lt;/p&gt;&lt;h2&gt;Deleting Rules&lt;/h2&gt;&lt;p&gt;Now that you know how to add rules using the &lt;code&gt;auditctl&lt;/code&gt; command or by creating custom rule files, let’s look at how to delete rules.&lt;/p&gt;&lt;p&gt;If you added a rule with the &lt;code&gt;auditctl&lt;/code&gt; command (which only lasts for the current session and disappears after a reboot), you can delete it using the same command but replacing &lt;code&gt;-w&lt;/code&gt; with &lt;code&gt;-W&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;For example, to add a rule:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo auditctl -w /secureadmins/ -k secureadmins_monitor
&lt;/pre&gt;&lt;p&gt;To delete the same rule:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo auditctl -W /secureadmins/ -k secureadmins_monitor
&lt;/pre&gt;&lt;p&gt;If the rule was added using a rule file in the &lt;code&gt;/etc/audit/rules.d/&lt;/code&gt; directory, using the &lt;code&gt;-W&lt;/code&gt; option will only delete the rule temporarily. It will reappear after a reboot because Auditd will read all rule files again.&lt;/p&gt;&lt;p&gt;To delete the rule permanently, remove the corresponding rule file from &lt;code&gt;/etc/audit/rules.d/&lt;/code&gt; and restart the &lt;code&gt;auditd&lt;/code&gt; service, or use the &lt;code&gt;sudo augenrules --load&lt;/code&gt; command.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;You can use the &lt;code&gt;sudo auditctl -D&lt;/code&gt; command to delete all existing rules temporarily.&lt;/p&gt;&lt;p&gt;If you want to delete a rule that monitors a specific syscall, use the &lt;code&gt;-d&lt;/code&gt; option as follows:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo auditctl -d always,exit -F arch=b64 -F auid=1001 -S openat -k monitor_ivan
&lt;/pre&gt;&lt;p&gt;This will delete the rule we previously created to monitor whenever Ivan tries to open or create a file.&lt;/p&gt;&lt;h2&gt;Generating Authentication Reports&lt;/h2&gt;&lt;p&gt;By default, Auditd logs authentication attempts and user logins, both failed and successful. This is part of its core functionality to provide a security audit trail for the server.&lt;/p&gt;&lt;p&gt;These logs help identify potential brute-force attacks or unauthorized access attempts.&lt;/p&gt;&lt;p&gt;To generate an authentication report, we simply use the &lt;code&gt;sudo aureport -au&lt;/code&gt; command. You may get a long list of authentication attempts. I will pipe the command into &lt;code&gt;tail -4&lt;/code&gt; to get the last 4 events:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;25462\. 12/30/2024 11:39:31 git 134.209.194.117 ssh /usr/sbin/sshd no 2468 25463\. 12/30/2024 11:40:11 esroot 65.21.109.67 ssh /usr/sbin/sshd no 2474 25464\. 12/30/2024 11:40:13 gitlab 65.21.109.67 ssh /usr/sbin/sshd no 2478 25465\. 12/30/2024 11:40:35 apache 65.21.109.67 ssh /usr/sbin/sshd no 2482
&lt;/pre&gt;&lt;p&gt;As you can see, all are failed authentication attempts that Auditd logged.&lt;/p&gt;&lt;p&gt;If you want to get more information about a particular event, use the &lt;code&gt;ausearch -a&lt;/code&gt; command followed by the event number, like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ivan@vm1:~$ sudo ausearch -a 2468 [sudo] password for ivan: \---- time-&amp;gt;Mon Dec 30 11:39:31 2024 type=USER_AUTH msg=audit(1735558771.846:2468): pid=2637 uid=0 auid=4294967295 ses=4294967295 subj=unconfined msg=&apos;op=PAM:authentication grantors=? acct=&amp;quot;git&amp;quot; exe=&amp;quot;/usr/sbin/sshd&amp;quot; hostname=134.209.194.117 addr=134.209.194.117 terminal=ssh res=failed&apos; ivan@vm1:~$
&lt;/pre&gt;&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;Ubuntu, by default, also logs authentication attempts in the&lt;code&gt;/var/log/auth.log&lt;/code&gt; file.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;Tracking User Login&lt;/h2&gt;&lt;p&gt;Besides generating authentication reports using &lt;code&gt;sudo aureport -au&lt;/code&gt;, you can also track user login activity with the &lt;code&gt;aulast&lt;/code&gt; and &lt;code&gt;aulastlog&lt;/code&gt; commands.&lt;/p&gt;&lt;p&gt;These utilities provide detailed insights into both historical and recent user logins, allowing you to effectively monitor and analyze login patterns on your server.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;aulast&lt;/code&gt; command displays a history of user login and logout events by searching through the Auditd logs.&lt;/p&gt;&lt;p&gt;Here&apos;s an example of the output:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ivan@vm1:~$ sudo aulast [sudo] password for ivan: ivan pts/1 92.75.35.67 Sat Jan 11 09:51 - 20:33 (10:42) ivan pts/1 92.75.35.67 Sun Jan 12 08:27 - 19:47 (11:20) reboot system boot 6.8.0-51-generic Sat Jan 11 09:17 ivan pts/1 92.75.35.67 Mon Jan 13 08:56 still logged in ivan@vm1:~$
&lt;/pre&gt;&lt;p&gt;From this output:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The user &lt;code&gt;ivan&lt;/code&gt; accessed the server multiple times from the same IP address (&lt;code&gt;92.75.35.67&lt;/code&gt;) on different days.&lt;/li&gt;&lt;li&gt;The session duration is shown in parentheses (&lt;code&gt;10:42&lt;/code&gt; means 10 hours and 42 minutes).&lt;/li&gt;&lt;li&gt;Server reboots are also logged.&lt;/li&gt;&lt;li&gt;A user currently logged in is marked with &lt;code&gt;still logged in&lt;/code&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The &lt;code&gt;aulastlog&lt;/code&gt; command shows the most recent login details for all users on the server.&lt;/p&gt;&lt;p&gt;Here&apos;s an example of the output:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ivan@vm1:~$ sudo aulastlog [sudo] password for ivan: Username Port From Latest root /dev/pts/0 92.75.35.67 01/13/2025 09:55:57 daemon **Never logged in** ivan /dev/pts/1 92.75.35.67 01/13/2025 10:18:21 ... ivan@vm1:~$
&lt;/pre&gt;&lt;p&gt;From this output:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;root&lt;/code&gt; last logged in on &lt;code&gt;01/13/2025&lt;/code&gt; at &lt;code&gt;09:55:57&lt;/code&gt; from the &lt;code&gt;92.75.35.67&lt;/code&gt; IP address.&lt;/li&gt;&lt;li&gt;&lt;code&gt;ivan&lt;/code&gt; last logged in shortly after on &lt;code&gt;01/13/2025&lt;/code&gt; at &lt;code&gt;10:18:21&lt;/code&gt; from the same IP address.&lt;/li&gt;&lt;li&gt;System users like &lt;code&gt;daemon&lt;/code&gt; have never logged in, as indicated by &lt;code&gt;**Never logged in**&lt;/code&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This utility complements &lt;code&gt;aulast&lt;/code&gt; by focusing on the latest activity rather than providing a full login history.&lt;/p&gt;&lt;h2&gt;Tracing a Process&lt;/h2&gt;&lt;p&gt;Something cool I want to show you is how to trace a process! This allows you to monitor the syscalls, file accesses, and other activities performed by a running program or command.&lt;/p&gt;&lt;p&gt;I absolutely love this feature because it gives me the ability to dive deep into what happens behind the scenes when a specific command is executed. You can really understand what’s going on at a low level, which is great for debugging or just satisfying your curiosity!&lt;/p&gt;&lt;p&gt;To do this, we’ll use the &lt;code&gt;autrace&lt;/code&gt; command, which temporarily adds Auditd rules to track the execution of a specific command.&lt;/p&gt;&lt;p&gt;Before we begin, make sure there are no existing Auditd rules. That’s right – you need to remove any previously created rules for &lt;code&gt;autrace&lt;/code&gt; to work, or else you&apos;ll encounter an error.&lt;/p&gt;&lt;p&gt;Here’s what I do: I keep my rule files saved and use the &lt;code&gt;augenrules&lt;/code&gt; command to load them. When I want to use the &lt;code&gt;autrace&lt;/code&gt; command, I first delete all the current rules with:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo auditctl -D
&lt;/pre&gt;&lt;p&gt;Once I&apos;m done using &lt;code&gt;autrace&lt;/code&gt;, I simply restart the &lt;code&gt;auditd&lt;/code&gt; service to apply my saved rules again, or use the &lt;code&gt;sudo augenrules --load&lt;/code&gt; command.&lt;/p&gt;&lt;p&gt;The reason this works is that deleting rules using &lt;code&gt;sudo auditctl -D&lt;/code&gt; only removes them temporarily, so it’s not an issue.&lt;/p&gt;&lt;p&gt;Now, after deleting all the rules, I want to trace the execution of the &lt;code&gt;cat&lt;/code&gt; command against the &lt;code&gt;sshd_config&lt;/code&gt; file. I do this by running the following command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo autrace /usr/bin/cat /etc/ssh/sshd_config
&lt;/pre&gt;&lt;p&gt;This will allow &lt;code&gt;autrace&lt;/code&gt; to track the execution of the &lt;code&gt;cat&lt;/code&gt; command. The command will execute normally, and you will see the content of the file.&lt;/p&gt;&lt;p&gt;However, at the last line, you will see something like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;Cleaning up... Trace complete. You can locate the records with &apos;ausearch -i -p 34358&apos;
&lt;/pre&gt;&lt;p&gt;This indicates that the trace has finished.&lt;/p&gt;&lt;p&gt;You can use &lt;code&gt;ausearch&lt;/code&gt; to review the detailed records of the traced execution, like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ausearch -i -p 34358
&lt;/pre&gt;&lt;p&gt;Once you&apos;re finished with the trace, simply run the &lt;code&gt;sudo augenrules --load&lt;/code&gt; command to apply your Auditd rules again.&lt;/p&gt;&lt;h2&gt;Auditd&apos;s Configuration File&lt;/h2&gt;&lt;p&gt;The primary configuration file for Auditd is &lt;code&gt;auditd.conf&lt;/code&gt;, located in the &lt;code&gt;/etc/audit/&lt;/code&gt; directory, which controls various settings related to how the &lt;code&gt;auditd&lt;/code&gt; service operates.&lt;/p&gt;&lt;p&gt;In most cases, it’s best to stick with the default settings and adjust them only when necessary.&lt;/p&gt;&lt;p&gt;While there are many settings available, I&apos;ll focus on a few important ones to give you a better understanding of how to configure Auditd for your needs.&lt;/p&gt;&lt;p&gt;The first setting you may want to change is the &lt;code&gt;log_file&lt;/code&gt; setting, which controls the location of the Auditd log file. By default, the logs are written to &lt;code&gt;/var/log/audit/audit.log&lt;/code&gt;. However, depending on your needs, you might want to change this path to a different location or storage device. The log file is where all the audit events are saved.&lt;/p&gt;&lt;p&gt;The next three settings you might want to adjust are &lt;code&gt;max_log_file&lt;/code&gt;, &lt;code&gt;max_log_file_action&lt;/code&gt; and &lt;code&gt;num_logs&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;max_log_file&lt;/code&gt; setting controls the maximum size of the log file in megabytes. Once the file reaches this limit, it triggers an action defined by the &lt;code&gt;max_log_file_action&lt;/code&gt; setting. By default, the action is set to &lt;code&gt;ROTATE&lt;/code&gt;, which means that when the log file reaches the maximum size, Auditd will create a new log file and keep rotating older logs. The log files are numbered, with lower numbers being older. The &lt;code&gt;num_logs&lt;/code&gt; setting controls how many rotated logs are kept before they are discarded.&lt;/p&gt;&lt;p&gt;By default, &lt;code&gt;max_log_file&lt;/code&gt; is set to 8 MB, and &lt;code&gt;num_logs&lt;/code&gt; is set to 5. For many setups, these defaults are sufficient to maintain an effective log history without consuming too much disk space. However, if your server generates a high volume of logs, you might need to adjust these values to suit your needs.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;For a detailed explanation of all settings, you can refer to the &lt;a href=&quot;https://man7.org/linux/man-pages/man5/auditd.conf.5.html&quot;&gt;Linux manual page&lt;/a&gt; for the &lt;code&gt;auditd.conf&lt;/code&gt; file.&lt;/p&gt;&lt;p&gt;To apply any changes made to the &lt;code&gt;auditd.conf&lt;/code&gt; file, remember to restart the &lt;code&gt;auditd&lt;/code&gt; service using the &lt;code&gt;sudo systemctl restart auditd&lt;/code&gt; command.&lt;/p&gt;&lt;h2&gt;More About &lt;code&gt;auditctl&lt;/code&gt;&lt;/h2&gt;&lt;p&gt;So far, we’ve used the &lt;code&gt;auditctl&lt;/code&gt; command to create, delete, and list rules. Now, let’s explore how you can use it to control Auditd itself.&lt;/p&gt;&lt;p&gt;With the &lt;code&gt;auditctl&lt;/code&gt; command, you can check the status of Auditd and change some of its configurations directly from the command line.&lt;/p&gt;&lt;p&gt;I want to begin with the &lt;code&gt;-s&lt;/code&gt; option, which displays the current status of Auditd, including its configuration details:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ivan@vm1:~$ sudo auditctl -s [sudo] password for ivan: enabled 1 failure 2 pid 75821 rate_limit 0 backlog_limit 8192 lost 0 backlog 0 backlog_wait_time 60000 backlog_wait_time_actual 0 loginuid_immutable 0 unlocked ivan@vm1:~$
&lt;/pre&gt;&lt;p&gt;As you can see, Auditd is currently enabled. You can stop Auditd by running the &lt;code&gt;sudo auditctl -e 0&lt;/code&gt; command and re-enable it using &lt;code&gt;sudo auditctl -e 1&lt;/code&gt;. This provides a quick way to control Auditd.&lt;/p&gt;&lt;p&gt;However, keep in mind that these changes are temporary. If you reboot the server, Auditd will start again. As mentioned earlier, the &lt;code&gt;auditctl&lt;/code&gt; command modifies the runtime configuration of Auditd, so the changes are only active for the current session.&lt;/p&gt;&lt;p&gt;If you want to stop Auditd permanently, you can use the &lt;code&gt;systemctl&lt;/code&gt; command to stop and disable the &lt;code&gt;auditd&lt;/code&gt; service.&lt;/p&gt;&lt;p&gt;You can also use the &lt;code&gt;auditctl&lt;/code&gt; command to change the failure mode, buffer size, and backlog wait time.&lt;/p&gt;&lt;p&gt;For example, you can change the failure mode with:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo auditctl -f 2
&lt;/pre&gt;&lt;p&gt;Change the buffer size with:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo auditctl -b 4096
&lt;/pre&gt;&lt;p&gt;And set the backlog wait time with:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo auditctl --backlog_wait_time 30000
&lt;/pre&gt;&lt;p&gt;However, for these changes to persist after a reboot, you need to modify the &lt;code&gt;/etc/audit/rules.d/audit.rules&lt;/code&gt; file and restart the &lt;code&gt;auditd&lt;/code&gt; service.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;-s&lt;/code&gt; option also provides additional information:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The &lt;code&gt;pid&lt;/code&gt; value shows the process number of the &lt;code&gt;auditd&lt;/code&gt; service. If the &lt;code&gt;pid&lt;/code&gt; is &lt;code&gt;0&lt;/code&gt;, it indicates that the &lt;code&gt;auditd&lt;/code&gt; service is not running.&lt;/li&gt;&lt;li&gt;The &lt;code&gt;lost&lt;/code&gt; entry tells you how many events have been discarded.&lt;/li&gt;&lt;li&gt;The &lt;code&gt;backlog&lt;/code&gt; field indicates how many events are currently queued, waiting for Auditd to read them.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;You can use the &lt;code&gt;-v&lt;/code&gt; option to check the version of Auditd:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ivan@vm1:~$ sudo auditctl -v [sudo] password for ivan: auditctl version 3.1.2 ivan@vm1:~$
&lt;/pre&gt;&lt;p&gt;Finally, the &lt;code&gt;-h&lt;/code&gt; option provides help about the usage of the &lt;code&gt;auditctl&lt;/code&gt; command. It gives you a quick summary of the available options.&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;By now, you should have a solid understanding of how to configure and manage Auditd to meet your security and auditing needs.&lt;/p&gt;&lt;p&gt;However, keep in mind that while Auditd is a powerful tool for alerting you about potential security breaches, it doesn&apos;t actively harden the server against them. It’s important to combine Auditd with other security measures.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;For more comprehensive Linux server security resources, be sure to check out the full collection of detailed guides &lt;a href=&quot;https://ivansalloum.com/collections/linux-server-security/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;If you found value in this guide or have any questions or feedback, please don&apos;t hesitate to share your thoughts in the discussion section.&lt;/p&gt;&lt;p&gt;Your input is greatly appreciated, and you can also &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;contact me&lt;/a&gt; directly if you prefer.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Security</category></item><item><title>Kernel Live Patching for High-Availability Linux Servers</title><link>https://ivansalloum.com/kernel-live-patching-for-high-availability-linux-servers/</link><guid isPermaLink="true">https://ivansalloum.com/kernel-live-patching-for-high-availability-linux-servers/</guid><description>Discover how kernel live patching boosts security and uptime for high-availability Linux servers without the need for reboots</description><pubDate>Mon, 09 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;Kernel live patching is a popular solution for Linux servers, allowing critical kernel security patches to be applied without rebooting. This is especially useful in environments where uptime is essential.&lt;/p&gt;&lt;p&gt;However, the concept of live patching is often oversimplified in many guides, leaving out important details.&lt;/p&gt;&lt;p&gt;In this guide, I’ll take a deeper look at what kernel live patching is and how to install and use a kernel live patching service.&lt;/p&gt;&lt;p&gt;I’ll also highlight important considerations you should keep in mind.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;_I assume you&apos;re working on a properly set-up Ubuntu server. If not, check out my guide  on &lt;em&gt;&lt;a href=&quot;https://ivansalloum.com/preparing-your-ubuntu-server-for-first-use/&quot;&gt;&lt;em&gt;preparing  Ubuntu servers&lt;/em&gt;&lt;/a&gt; _  to get started.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;Author&apos;s Note&lt;/h2&gt;&lt;p&gt;Before diving into the details, I want to share a few thoughts.&lt;/p&gt;&lt;p&gt;While exploring various guides online about live patching the Linux kernel, I noticed a common theme: many of them focus on promoting the concept as a way to keep Linux servers running without reboots, often emphasizing phrases like &amp;quot;reboot-free kernel updates&amp;quot; or &amp;quot;reboot-free security updates&amp;quot;.&lt;/p&gt;&lt;p&gt;While this is partially true, it’s not the complete picture.&lt;/p&gt;&lt;p&gt;Installing a kernel live patching service doesn’t mean you can ignore installing new security updates or rebooting your server, nor does it replace the need for &lt;a href=&quot;https://ivansalloum.com/automating-security-updates-on-linux-servers/&quot;&gt;regular security updates&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;It&apos;s important to understand that live patching provides only a temporary solution by patching the running kernel.&lt;/p&gt;&lt;p&gt;To stay fully protected, you&apos;ll still need to update and reboot your server, and I will explain why in this guide.&lt;/p&gt;&lt;h2&gt;What is Kernel Live Patching?&lt;/h2&gt;&lt;p&gt;Kernel live patching is a method of applying small, targeted patches directly to the running kernel&apos;s memory without replacing the kernel binary. These patches are applied in real-time, focusing on fixing specific vulnerabilities in critical areas of the kernel.&lt;/p&gt;&lt;p&gt;For example, if a vulnerability is discovered in the kernel version you’re currently using, a new version with the fix will be released. Normally, you’d need to update your server and then reboot to load the new kernel. However, if you delay the reboot, your server remains vulnerable, leaving it exposed until the new kernel is loaded.&lt;/p&gt;&lt;p&gt;Live patching addresses this issue by patching the vulnerability directly in the running kernel, avoiding the need for an immediate update and reboot.&lt;/p&gt;&lt;p&gt;These patches are designed to close critical security gaps, such as those leading to privilege escalation or remote code execution. However, live patching does not include non-critical fixes, new features, or driver updates.&lt;/p&gt;&lt;p&gt;In simple terms, live patching is like fixing a hole in a door by covering it up without replacing the door itself. This approach keeps your server secure while avoiding the downtime caused by a reboot.&lt;/p&gt;&lt;h2&gt;Traditional Updates&lt;/h2&gt;&lt;p&gt;Let me start by discussing traditional updates and how you would normally update your server without kernel live patching.&lt;/p&gt;&lt;p&gt;When deploying a new server from a cloud provider like Hetzner, the first step after accessing the server is to run updates. This is typically done by first running the &lt;code&gt;sudo apt update&lt;/code&gt; command, followed by either the &lt;code&gt;sudo apt upgrade&lt;/code&gt; or &lt;code&gt;sudo apt dist-upgrade&lt;/code&gt; command.&lt;/p&gt;&lt;p&gt;Using &lt;code&gt;apt upgrade&lt;/code&gt; won’t install a new kernel version. It upgrades existing packages, but doesn’t install new packages or remove unneeded ones. This means that your current kernel will receive security patches and bug fixes, but you won’t get a newer kernel version.&lt;/p&gt;&lt;p&gt;If a patch is released for the current version of the kernel, running &lt;code&gt;apt upgrade&lt;/code&gt; will handle it. But if the kernel update involves installing a newer version or additional dependencies, it won&apos;t handle it.&lt;/p&gt;&lt;p&gt;On the other hand, using &lt;code&gt;apt dist-upgrade&lt;/code&gt; can install a new kernel version. It intelligently handles changes in package dependencies, including installing new packages or removing existing ones if necessary.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;New to Hetzner? &lt;a href=&quot;https://hetzner.cloud/?ref=MC4Yy318xX5X&quot;&gt;Use my link&lt;/a&gt; to get free credits!&lt;/p&gt;&lt;p&gt;Now, let me show you how to check for security-related updates. I&apos;ll use the following command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt -s dist-upgrade | grep &amp;quot;^Inst&amp;quot; | grep -i security
&lt;/pre&gt;&lt;p&gt;This command provides a list of available security updates.&lt;/p&gt;&lt;p&gt;Some updates, such as kernel-related updates, require a reboot after installation. Others, which pertain to various packages, take effect immediately without needing a reboot.&lt;/p&gt;&lt;p&gt;For example, on a Hetzner server running Ubuntu 24.04 version, the command returns the following kernel-related security updates:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;Inst linux-modules-6.8.0-49-generic (6.8.0-49.49 Ubuntu:24.04/noble-updates, Ubuntu:24.04/noble-security [amd64]) Inst linux-image-6.8.0-49-generic (6.8.0-49.49 Ubuntu:24.04/noble-updates, Ubuntu:24.04/noble-security [amd64]) Inst linux-image-virtual [6.8.0-45.45] (6.8.0-49.49 Ubuntu:24.04/noble-updates, Ubuntu:24.04/noble-security [amd64]) Inst linux-tools-common [6.8.0-45.45] (6.8.0-49.49 Ubuntu:24.04/noble-updates, Ubuntu:24.04/noble-security [all])
&lt;/pre&gt;&lt;p&gt;Next, I&apos;ll check my current kernel version using the &lt;code&gt;uname -a&lt;/code&gt; command:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;Linux ubuntu-4gb-fsn1-2 6.8.0-45-generic #45-Ubuntu SMP PREEMPT_DYNAMIC Fri Aug 30 12:02:04 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux
&lt;/pre&gt;&lt;p&gt;This indicates I&apos;m running kernel version &lt;strong&gt;6.8.0-45&lt;/strong&gt; , while the updates are for &lt;strong&gt;6.8.0-49&lt;/strong&gt; , a newer version in the same kernel series (6.8.0). It&apos;s an incremental update, not a major version change.&lt;/p&gt;&lt;p&gt;Because the older version (6.8.0-45) is already installed, both &lt;code&gt;sudo apt upgrade&lt;/code&gt; and &lt;code&gt;sudo apt dist-upgrade&lt;/code&gt; can apply the update.&lt;/p&gt;&lt;p&gt;The command &lt;code&gt;sudo apt upgrade&lt;/code&gt; typically updates packages that are already installed, replacing the older version with the newer one. In this case, it will update the kernel packages to &lt;strong&gt;6.8.0-49&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;However, since these updates are kernel-related, a reboot is required for the changes to take effect.&lt;/p&gt;&lt;p&gt;For now, I will update all available packages. After the update process was finished, I saw these lines:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;Pending kernel upgrade! Running kernel version: 6.8.0-45-generic Diagnostics: The currently running kernel version is not the expected kernel version 6.8.0-49-generic.
&lt;/pre&gt;&lt;p&gt;I need to reboot the server for the new kernel to take effect, but I won&apos;t do that right now, as I want to show you what a kernel live patching service can do.&lt;/p&gt;&lt;h2&gt;Kernel Live Patching Service&lt;/h2&gt;&lt;p&gt;I will be using &lt;a href=&quot;https://tuxcare.com/buy/kce/&quot;&gt;KernelCare Simple Patch&lt;/a&gt; from TuxCare, a kernel live patching service that costs only $2.95 per month. It provides security patches for a range of popular Linux kernels, which can be applied without rebooting the server.&lt;/p&gt;&lt;p&gt;When a new vulnerability is found in the Linux kernel, TuxCare creates a live patch to fix it. The patch is first tested in TuxCare’s internal server farm and then gradually moved through various testing tiers to ensure it&apos;s been thoroughly tested.&lt;/p&gt;&lt;p&gt;Once the patch is ready, servers using KernelCare will receive and apply the patch live.&lt;/p&gt;&lt;p&gt;You have other options, such as the &lt;a href=&quot;https://ubuntu.com/security/livepatch&quot;&gt;Ubuntu Livepatch Service&lt;/a&gt;, but I personally prefer KernelCare. It is more affordable and works reliably. While the Ubuntu Livepatch Service is available for personal use for free, it is significantly more expensive than KernelCare for commercial purposes.&lt;/p&gt;&lt;p&gt;Now, access your Ubuntu server and run the following command as root to install KernelCare:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;curl -s -L https://kernelcare.com/installer | bash
&lt;/pre&gt;&lt;p&gt;Then, register your key (which you received by email after purchasing and can also find in your customer dashboard) using the command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;kcarectl --register [KEY]
&lt;/pre&gt;&lt;p&gt;KernelCare will automatically check for new patches every 4 hours. For now, I&apos;ll manually check and update with this command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;kcarectl --update
&lt;/pre&gt;&lt;p&gt;I received the following output:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;Downloading updates Patch level 4 applied. Effective kernel version 6.8.0-47.47 Kernel is safe
&lt;/pre&gt;&lt;p&gt;As you can see, a vulnerability was fixed in kernel version &lt;strong&gt;6.8.0-47.47&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Remember, I haven&apos;t rebooted yet, so my kernel version is still &lt;strong&gt;6.8.0-45&lt;/strong&gt; if I run the &lt;code&gt;uname -a&lt;/code&gt; command. Although I updated to &lt;strong&gt;6.8.0-49&lt;/strong&gt; , I didn&apos;t reboot, so the underlying kernel remains the old one. This means the update hasn&apos;t fixed the vulnerability yet.&lt;/p&gt;&lt;p&gt;Now, let me run the &lt;code&gt;kcarectl --patch-info&lt;/code&gt; command:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;OS: ubuntu-noble kernel: kernel-6.8.0-45.45 time: 2024-10-24 07:30:13 kpatch-name: ubuntu-noble/6.8.0-47.47/CVE-2024-45016-netem-fix-return-value-if-duplicate-enqueue-fails.patch kpatch-description: netem: fix return value if duplicate enqueue fails kpatch-kernel: 6.8.0-47.47 kpatch-cve: CVE-2024-45016 kpatch-cvss: 5.5 kpatch-cve-url: https://ubuntu.com/security/CVE-2024-45016 kpatch-patch-url: https://git.launchpad.net/~ubuntu-kernel/ubuntu/+source/linux/+git/noble/commit/?id=5017a6a30cd43240c688ed996b81a5daff2a7dc9 uname: 6.8.0-47.47
&lt;/pre&gt;&lt;p&gt;KernelCare patched our kernel live against &lt;a href=&quot;https://ubuntu.com/security/CVE-2024-45016&quot;&gt;CVE-2024-45016&lt;/a&gt;, which was fixed in kernel version &lt;strong&gt;6.8.0-47.47&lt;/strong&gt; for Ubuntu 22.04 servers. Since I haven&apos;t rebooted, KernelCare has protected my server live without requiring a reboot. Great!&lt;/p&gt;&lt;p&gt;If I had rebooted after updating all packages, the new kernel (&lt;strong&gt;6.8.0-49&lt;/strong&gt;) would have been loaded, and KernelCare would not have found any CVEs because the new kernel already includes the fix for the CVE.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;For more information about KernelCare, check its&lt;/em&gt;&lt;a href=&quot;https://docs.tuxcare.com/live-patching-services/&quot;&gt; &lt;em&gt;documentation&lt;/em&gt;&lt;/a&gt; &lt;em&gt;.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;Considerations&lt;/h2&gt;&lt;p&gt;It’s true that KernelCare helped fix the vulnerability live without requiring a reboot, but the underlying kernel hasn&apos;t been updated yet. Everything is virtual, so to speak.&lt;/p&gt;&lt;p&gt;If I run the &lt;code&gt;uname -a&lt;/code&gt; command, I&apos;ll still see the old kernel version in the output. However, if I run the &lt;code&gt;kcarectl --uname&lt;/code&gt; command, I&apos;ll see kernel version &lt;strong&gt;6.8.0-47.47&lt;/strong&gt; , which has the CVE fixed. KernelCare refers to this as the &amp;quot;effective kernel&amp;quot;.&lt;/p&gt;&lt;p&gt;After live patching, the kernel image stored on the disk (the one used during boot) and the kernel running remain unchanged, meaning we are not fully protected. And we don&apos;t benefit from any other enhancements, such as new features or non-critical fixes.&lt;/p&gt;&lt;p&gt;Another thing to mention is that there are some security updates that are not kernel-related but still require a reboot to be fully applied.&lt;/p&gt;&lt;p&gt;For these reasons, I recommend updating the server regularly to apply both non-kernel security updates and any new kernel updates. This ensures that the kernel image stored on the disk is also updated, so when you reboot, the new kernel is loaded.&lt;/p&gt;&lt;p&gt;I will reboot the server and check the running kernel, as well as what KernelCare has to say.&lt;/p&gt;&lt;p&gt;After the reboot, I ran the &lt;code&gt;uname -a&lt;/code&gt; command:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;Linux ubuntu-4gb-nbg1-6 6.8.0-49-generic #49-Ubuntu SMP PREEMPT_DYNAMIC Mon Nov 4 02:06:24 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux
&lt;/pre&gt;&lt;p&gt;As you can see, the server is now running the new kernel.&lt;/p&gt;&lt;p&gt;If I run the &lt;code&gt;kcarectl --update&lt;/code&gt; command again, I get the following output:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;Updates already downloaded No updates are needed for this kernel Kernel is safe
&lt;/pre&gt;&lt;p&gt;This means there is no live patching needed because we are now running the new kernel, and there are no vulnerabilities to fix.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;It is wise and important to reboot from time to time.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;The real value of live patching is that it buys you time. It allows you to decide when to reboot while still protecting your Linux server instantly and without intervention if vulnerabilities are discovered in the kernel.&lt;/p&gt;&lt;p&gt;It’s a powerful tool, but one that should be used as part of a broader update and maintenance strategy.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;For more comprehensive Linux server security resources, be sure to check out the full collection of detailed guides &lt;a href=&quot;https://ivansalloum.com/collections/linux-server-security/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;If you found value in this guide or have any questions or feedback, please don&apos;t hesitate to share your thoughts in the discussion section.&lt;/p&gt;&lt;p&gt;Your input is greatly appreciated, and you can also &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;contact me&lt;/a&gt; directly if you prefer.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Security</category></item><item><title>Preventing SYN Flood Attacks on Your Linux Server</title><link>https://ivansalloum.com/preventing-syn-flood-attacks-on-your-linux-server/</link><guid isPermaLink="true">https://ivansalloum.com/preventing-syn-flood-attacks-on-your-linux-server/</guid><description>Learn how to protect your Linux server from SYN flood attacks with firewall rules, kernel tweaks, and Fail2ban.</description><pubDate>Wed, 20 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;SYN flood attacks are one of those frustrating things that can quietly – or not so quietly – take your server down, fast. They work by flooding your server with a ton of fake connection requests, leaving it too busy to handle the real ones from actual users.&lt;/p&gt;&lt;p&gt;I ran into one of these attacks a while back while hosting a small WordPress site on a VPS. One day, the site started loading really slowly, and soon after, it became completely unreachable. Even SSH was lagging. When I checked the logs, there were tons of strange connection attempts. That’s when I realized it was a SYN flood.&lt;/p&gt;&lt;p&gt;In this guide, I’ll walk you through the steps I now use to prevent SYN flood attacks on my own servers. We’ll cover:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Kernel tweaks to help the server handle floods more efficiently&lt;/li&gt;&lt;li&gt;Firewall rules to drop suspicious connections early&lt;/li&gt;&lt;li&gt;How to use &lt;strong&gt;Fail2ban&lt;/strong&gt;  to automatically block abusive IP addresses&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;These steps are lightweight, effective, and won’t interfere with normal traffic. By the end, you’ll have a solid setup that helps keep your server responsive and secure.&lt;/p&gt;&lt;h2&gt;Update (October 2025)&lt;/h2&gt;&lt;p&gt;Since writing this guide, I’ve taken these ideas much further.&lt;/p&gt;&lt;p&gt;I’ve built a new, modern firewall setup – a project I call &lt;strong&gt;UFW+&lt;/strong&gt;  – which expands on the same SYN flood protection concepts covered here.&lt;/p&gt;&lt;p&gt;The new version combines &lt;strong&gt;UFW&lt;/strong&gt; , &lt;strong&gt;Fail2ban&lt;/strong&gt; , and &lt;strong&gt;nftables&lt;/strong&gt;  into a single adaptive firewall setup that can automatically detect and block &lt;strong&gt;floods, scans, and spoofed traffic&lt;/strong&gt;  in real time.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Read the new guide&lt;/strong&gt; : &lt;a href=&quot;https://ivansalloum.com/how-i-built-an-adaptive-firewall-setup-with-ufw-and-fail2ban/&quot;&gt;How I Built an Adaptive Firewall Setup with UFW and Fail2ban (UFW+)&lt;/a&gt;&lt;/p&gt;&lt;h2&gt;Author&apos;s Note&lt;/h2&gt;&lt;p&gt;Before we jump into the technical stuff, I wanted to share a bit of the &amp;quot;why&amp;quot; behind this guide.&lt;/p&gt;&lt;p&gt;I’ve always been into Linux server security, but SYN flood attacks pushed me to dig deeper. When I first started looking into ways to prevent them, I found tons of guides online – but most were either vague, overly simplified, or just plain outdated. Some tossed out a few &lt;strong&gt;iptables&lt;/strong&gt; rules and called it a day. Others mentioned tools like Fail2ban but didn’t explain how to set them up properly.&lt;/p&gt;&lt;p&gt;That just didn’t cut it for me.&lt;/p&gt;&lt;p&gt;So I decided to piece things together myself – test different approaches, break things, fix them again, and eventually land on a setup that actually works.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;What you’re reading now is the guide I wish I had when I started.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;To really block these kinds of attacks, you need to understand the basics of how TCP works – especially the &lt;a href=&quot;https://ivansalloum.com/understanding-tcp-and-the-three-way-handshake/&quot;&gt;&lt;strong&gt;Three-Way Handshake&lt;/strong&gt;&lt;/a&gt; – and how SYN floods try to break that process.&lt;/p&gt;&lt;p&gt;😅&lt;/p&gt;&lt;p&gt;Don’t worry if that sounds technical. I’ll explain it simply as we go.&lt;/p&gt;&lt;p&gt;Also, it’s worth noting that while &lt;strong&gt;DoS attacks&lt;/strong&gt; can be handled with the right setup, large-scale &lt;strong&gt;DDoS attacks&lt;/strong&gt;  are tougher to stop completely. That said, even basic protections can make your server a much harder target.&lt;/p&gt;&lt;p&gt;Finally, even if your hosting provider (like &lt;strong&gt;Hetzner&lt;/strong&gt;) has some built-in DDoS protection, it often doesn’t catch the smaller, sneakier SYN floods aimed at your VPS.&lt;/p&gt;&lt;p&gt;That’s why it’s on you to add extra layers of security to your servers – to make it as difficult as possible for attackers to cause trouble.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;By the way, if you’re new to Hetzner, you can &lt;a href=&quot;https://hetzner.cloud/?ref=MC4Yy318xX5X&quot;&gt;use my link&lt;/a&gt; to get some free credits and try their services risk-free!&lt;/p&gt;&lt;h2&gt;What is a SYN Flood Attack?&lt;/h2&gt;&lt;p&gt;To understand a SYN flood attack, it helps to first know how a normal TCP connection starts. It begins with something called a &lt;strong&gt;Three-Way Handshake&lt;/strong&gt; :&lt;/p&gt;&lt;ol&gt;&lt;li&gt;The client sends a &lt;strong&gt;SYN&lt;/strong&gt;  packet to the server, signaling a request to connect.&lt;/li&gt;&lt;li&gt;The server replies with a &lt;strong&gt;SYN-ACK&lt;/strong&gt;  packet, acknowledging the request.&lt;/li&gt;&lt;li&gt;The client then sends an &lt;strong&gt;ACK&lt;/strong&gt;  packet to complete the handshake.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Once this handshake is complete, the connection is established, and data can start flowing.&lt;/p&gt;&lt;p&gt;A &lt;strong&gt;SYN flood attack&lt;/strong&gt;  takes advantage of this handshake process by sending many SYN packets to the server – but the attacker never sends back the final ACK. The server responds with SYN-ACKs and waits, leaving these connections &amp;quot;half-open&amp;quot;.&lt;/p&gt;&lt;p&gt;Each half-open connection uses up server resources. When too many pile up, the server gets overwhelmed and can’t handle new, legitimate connections. In extreme cases, this can cause the server to crash or become completely unresponsive.&lt;/p&gt;&lt;p&gt;SYN flood attacks can come from a single device (a DoS attack) or from many devices at once (a DDoS attack), often coordinated through a botnet.&lt;/p&gt;&lt;p&gt;Because these connections never fully open, this type of attack is sometimes called a &amp;quot;half-open attack&amp;quot;.&lt;/p&gt;&lt;h2&gt;How I Tested Everything&lt;/h2&gt;&lt;p&gt;To experiment safely, I set up two Ubuntu 24.04 virtual machines on my MacBook – one to play the role of the &amp;quot;attacker&amp;quot;. and the other as the &amp;quot;victim&amp;quot;. Both were barebones servers, perfect for simulating what might happen on a real VPS.&lt;/p&gt;&lt;p&gt;I installed &lt;strong&gt;NGINX&lt;/strong&gt;  on the victim machine to keep port 80 open, then used tools like &lt;strong&gt;hping3&lt;/strong&gt;  and &lt;strong&gt;ApacheBench&lt;/strong&gt;  to simulate traffic. While ApacheBench is typically a web server benchmarking tool, it can also flood a server with requests to mimic what happens during an attack.&lt;/p&gt;&lt;p&gt;The real star here was &lt;code&gt;hping3&lt;/code&gt;. With the command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;hping3 -S -p 80 --flood victim.ip
&lt;/pre&gt;&lt;p&gt;I was able to launch a SYN flood. The &lt;code&gt;--flood&lt;/code&gt; flag sends a constant stream of SYN packets without completing the TCP handshake – and sure enough, it didn’t take long before the victim server started lagging, then completely stopped responding. Even my SSH connection dropped.&lt;/p&gt;&lt;p&gt;To observe what was happening, I ran:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;tcpdump -nn port 80
&lt;/pre&gt;&lt;p&gt;Watching those packets pile up in real time really showed how quickly a SYN flood can choke a server.&lt;/p&gt;&lt;p&gt;I also ran ApacheBench with a high number of requests to simulate excessive legitimate-looking traffic. The server&apos;s CPU and memory spiked fast. Obviously, no real user would hit the server that hard – so it was a good baseline to test defenses.&lt;/p&gt;&lt;h3&gt;What Didn&apos;t Work&lt;/h3&gt;&lt;p&gt;At this point, I hadn’t applied any protection – just raw, open NGINX on port 80.&lt;/p&gt;&lt;p&gt;I tried enabling &lt;strong&gt;UFW&lt;/strong&gt;  &lt;strong&gt;(Uncomplicated Firewall)&lt;/strong&gt; , but it didn’t do much. Its built-in rate limiting (&lt;code&gt;ufw limit&lt;/code&gt;) works okay for things like SSH, but it’s far too aggressive for a public web server. You don’t want to accidentally block real users just because they refreshed a few times.&lt;/p&gt;&lt;p&gt;I quickly realized I needed a better solution – something smarter, more flexible, and ideally automated.&lt;/p&gt;&lt;h3&gt;Finding What Worked&lt;/h3&gt;&lt;p&gt;After a lot of digging and testing, I built a setup that allowed a reasonable number of SYN packets per IP in a short window – and logged or dropped any excess traffic. This gave me the control I needed to separate real users from potential attackers.&lt;/p&gt;&lt;p&gt;Rather than just copy-pasting firewall rules from random websites, I treated them like building blocks. I tweaked, tested, broke things, fixed them, and eventually landed on a rule set that actually worked under load.&lt;/p&gt;&lt;p&gt;Then came &lt;strong&gt;Fail2ban&lt;/strong&gt;. I configured it to monitor logs and ban IPs that triggered too many half-open connections. With a few tweaks, it worked well alongside UFW, automatically adding malicious IPs to the block list.&lt;/p&gt;&lt;p&gt;Once everything looked good locally, I tested the same setup on two real VPS servers (hosted on Hetzner). The difference was night and day – even under stress, the servers stayed responsive.&lt;/p&gt;&lt;p&gt;❗&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;  Tools like hping3 and ApacheBench should &lt;strong&gt;only&lt;/strong&gt;  be used for testing in safe, isolated environments. Don’t use them on servers you don’t own or control.&lt;/p&gt;&lt;h2&gt;Kernel Settings: Tweaks That Help (a Bit)&lt;/h2&gt;&lt;p&gt;Before jumping into firewall rules, I wanted to see if some kernel-level tuning could help – and while it doesn’t stop a SYN flood on its own, it definitely improves how the server handles things under pressure.&lt;/p&gt;&lt;h3&gt;Enable Syncookies (If Not Already)&lt;/h3&gt;&lt;p&gt;This one’s usually enabled by default, but it’s worth double-checking:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo sysctl net.ipv4.tcp_syncookies
&lt;/pre&gt;&lt;p&gt;If the result isn’t &lt;code&gt;1&lt;/code&gt;, just open your &lt;code&gt;sysctl.conf&lt;/code&gt; file:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo vim /etc/sysctl.conf
&lt;/pre&gt;&lt;p&gt;Uncomment or add:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;net.ipv4.tcp_syncookies = 1
&lt;/pre&gt;&lt;p&gt;Syncookies help the server manage half-open connections more efficiently – not bulletproof, but useful.&lt;/p&gt;&lt;h3&gt;Tighten Reverse Path Filtering&lt;/h3&gt;&lt;p&gt;By default, reverse path filtering is set to loose mode (&lt;code&gt;2&lt;/code&gt;), but switching to strict mode (&lt;code&gt;1&lt;/code&gt;) offers better protection against IP spoofing.&lt;/p&gt;&lt;p&gt;In &lt;code&gt;/etc/sysctl.conf&lt;/code&gt;, find or add:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;net.ipv4.conf.default.rp_filter = 1 net.ipv4.conf.all.rp_filter = 1
&lt;/pre&gt;&lt;p&gt;This tells your server to drop packets that don’t come from reachable IPs, which helps filter out bogus traffic.&lt;/p&gt;&lt;h3&gt;Optimize Connection Handling&lt;/h3&gt;&lt;p&gt;These two extra parameters can make your server more resilient under load:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;net.ipv4.tcp_max_syn_backlog = 4096 net.ipv4.tcp_synack_retries = 3
&lt;/pre&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;tcp_max_syn_backlog&lt;/code&gt; – Increases the size of the queue that stores half-open connections. &lt;code&gt;4096&lt;/code&gt; is a good starting point, but for high-memory servers, you can safely go higher – up to &lt;code&gt;16384&lt;/code&gt; – to handle heavier traffic.&lt;/li&gt;&lt;li&gt;&lt;code&gt;tcp_synack_retries&lt;/code&gt; – Reduces how long the server keeps waiting for an ACK response. Lowering this to &lt;code&gt;3&lt;/code&gt; (from the default &lt;code&gt;5&lt;/code&gt;) helps free up resources faster when connections don’t complete.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;These tweaks help the kernel respond faster and stay available when the connection queue starts filling up.&lt;/p&gt;&lt;h3&gt;Apply the Changes&lt;/h3&gt;&lt;p&gt;Once you&apos;re done editing &lt;code&gt;/etc/sysctl.conf&lt;/code&gt;, save the file and reload the settings:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo sysctl -p
&lt;/pre&gt;&lt;p&gt;A quick reboot after that ensures everything takes effect properly.&lt;/p&gt;&lt;h2&gt;Understanding UFW Rule Files&lt;/h2&gt;&lt;p&gt;Since we’ll be using &lt;strong&gt;UFW (Uncomplicated Firewall)&lt;/strong&gt;  to help protect the server from SYN flood attacks, it’s important to understand how UFW handles rules under the hood.&lt;/p&gt;&lt;p&gt;Inside the &lt;code&gt;/etc/ufw/&lt;/code&gt; directory, you’ll find three key files:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;before.rules&lt;/code&gt;&lt;/strong&gt;  – for IPv4 traffic&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;before6.rules&lt;/code&gt;&lt;/strong&gt;  – for IPv6 traffic&lt;/li&gt;&lt;li&gt;&lt;code&gt;**user.rules**&lt;/code&gt; – for rules you add with UFW commands like &lt;code&gt;ufw allow&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Here’s what matters:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The &lt;code&gt;before.rules&lt;/code&gt; and &lt;code&gt;before6.rules&lt;/code&gt; files are &lt;strong&gt;processed first&lt;/strong&gt; , before anything you manually allow via command line. That means rules here take &lt;strong&gt;priority&lt;/strong&gt; , which makes them the best place to add protections like SYN flood filtering.&lt;/li&gt;&lt;li&gt;The &lt;code&gt;user.rules&lt;/code&gt; file stores your typical &lt;code&gt;allow&lt;/code&gt;/&lt;code&gt;deny&lt;/code&gt; commands but should &lt;strong&gt;not&lt;/strong&gt;  be edited directly – UFW manages this file automatically.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;By understanding the order UFW uses to load rules, you’ll be better equipped to insert effective protections exactly where they’ll have the most impact.&lt;/p&gt;&lt;h2&gt;Blocking Invalid Packets (Prepping for SYN Flood Protection)&lt;/h2&gt;&lt;p&gt;Let’s lay the groundwork for defending against SYN flood attacks by blocking &lt;strong&gt;invalid TCP packets&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;First, make sure UFW is enabled and that you won’t lock yourself out:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw limit 22/tcp sudo ufw enable
&lt;/pre&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Heads up:&lt;/strong&gt; The &lt;code&gt;ufw limit&lt;/code&gt; rule helps protect SSH by rate-limiting connections – it blocks IPs that try to connect too many times in a short period, which is useful against brute-force and flood attempts.&lt;/p&gt;&lt;p&gt;Next, we’ll filter out shady packets – like those that don’t follow the standard TCP handshake (e.g. from &lt;strong&gt;Nmap&lt;/strong&gt; or other scanners). Every legit TCP connection should start with just the &lt;strong&gt;SYN&lt;/strong&gt;  flag.&lt;/p&gt;&lt;p&gt;Here’s how to block invalid stuff early:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Open both &lt;code&gt;before.rules&lt;/code&gt; and &lt;code&gt;before6.rules&lt;/code&gt;&lt;/li&gt;&lt;li&gt;Add this section &lt;strong&gt;after the last  &lt;code&gt;COMMIT&lt;/code&gt; line&lt;/strong&gt;:&lt;/li&gt;&lt;/ol&gt;&lt;pre data-language=&quot;text&quot;&gt;*mangle :PREROUTING ACCEPT [0:0] -A PREROUTING -m conntrack --ctstate INVALID -j DROP -A PREROUTING -p tcp -m tcp ! --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j DROP COMMIT
&lt;/pre&gt;&lt;p&gt;Then reload UFW:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw reload
&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;What this does:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The &lt;strong&gt;first rule&lt;/strong&gt;  drops anything marked as &lt;code&gt;INVALID&lt;/code&gt; by the kernel’s connection tracker.&lt;/li&gt;&lt;li&gt;The &lt;strong&gt;second rule&lt;/strong&gt;  drops TCP packets pretending to start a connection but missing the proper SYN flag.&lt;/li&gt;&lt;li&gt;We add this to the &lt;strong&gt;mangle table&lt;/strong&gt;  and the &lt;strong&gt;PREROUTING chain&lt;/strong&gt; , so these packets are filtered &lt;strong&gt;as soon as they arrive&lt;/strong&gt;  – before they waste any more server resources.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This is a lightweight but powerful way to cut down on noise and bad traffic before it becomes a problem.&lt;/p&gt;&lt;h2&gt;Exploring SYN Flood Mitigation Approaches with UFW&lt;/h2&gt;&lt;p&gt;Now that we’ve covered the basics – including blocking obviously bad traffic – it’s time to put up a solid line of defense against SYN flood attacks.&lt;/p&gt;&lt;p&gt;We’ll use &lt;strong&gt;UFW&lt;/strong&gt; , Ubuntu’s built-in firewall, which you’ve already enabled and configured to allow SSH traffic. Plus, dropping invalid packets in the previous step gave us a strong foundation to build on.&lt;/p&gt;&lt;p&gt;Since we’re using &lt;code&gt;ufw limit 22/tcp&lt;/code&gt; for SSH, it’s already got some protection built in. That said, if you want a more custom approach, the rules we’re about to go over can apply to &lt;strong&gt;any TCP service&lt;/strong&gt; , including SSH.&lt;/p&gt;&lt;p&gt;Before settling on a robust, all-in-one solution, I experimented with a few different ways to protect my server from SYN flood attacks using UFW.&lt;/p&gt;&lt;p&gt;Each approach had its strengths but also some limitations – whether it was overly broad limits, excessive logging, or lack of persistence. These trials helped me understand what worked, what didn’t, and why a more refined solution was necessary.&lt;/p&gt;&lt;p&gt;Below, I’ll walk you through the three strategies I tested, starting with simple rate limiting and moving toward a dynamic blacklist configuration.&lt;/p&gt;&lt;p&gt;After that, I’ll share the final solution that combines the best of all three.&lt;/p&gt;&lt;h3&gt;Approach 1: Basic SYN Flood Mitigation Using &lt;code&gt;limit&lt;/code&gt;&lt;/h3&gt;&lt;p&gt;In this first attempt, I used the &lt;code&gt;limit&lt;/code&gt; module to rate-limit incoming TCP SYN packets to port 80, protecting against SYN flood attacks.&lt;/p&gt;&lt;p&gt;The rules were added to the end of the &lt;code&gt;/etc/ufw/before.rules&lt;/code&gt; file, just before the final &lt;code&gt;COMMIT&lt;/code&gt; line:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;-A ufw-before-input -p tcp --syn --dport 80 -m conntrack --ctstate NEW -m limit --limit 10/second --limit-burst 20 -j ACCEPT -A ufw-before-input -p tcp --syn --dport 80 -m conntrack --ctstate NEW -m limit --limit 10/second --limit-burst 20 -j LOG --log-prefix &amp;quot;[UFW SYN Flood Detected] &amp;quot; -A ufw-before-input -p tcp --syn --dport 80 -j DROP
&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;What these rules do:&lt;/strong&gt;&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Accept Rule:&lt;/strong&gt;  Allows up to 10 new SYN packets per second (with a burst of 20) to port 80.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Log Rule:&lt;/strong&gt;  Logs any excess packets beyond this rate for monitoring.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Drop Rule:&lt;/strong&gt;  Drops any packets that exceed the rate limit to prevent abuse.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;These rules were added directly to the &lt;code&gt;ufw-before-input&lt;/code&gt; chain, which is processed before any user-defined rules.&lt;/p&gt;&lt;p&gt;This chain is part of the early stages of UFW’s firewall processing, meaning these rules are applied before incoming traffic reaches any services or applications running on the server. This ensures potentially harmful connections are filtered out as early as possible.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Heads up:&lt;/strong&gt; Since these rules handle port 80 traffic, there&apos;s no need to allow HTTP explicitly via &lt;code&gt;sudo ufw allow 80/tcp&lt;/code&gt;. The &lt;code&gt;before.rules&lt;/code&gt; rules take precedence.&lt;/p&gt;&lt;p&gt;For IPv6 support, add the same rules to &lt;code&gt;/etc/ufw/before6.rules&lt;/code&gt;, replacing &lt;code&gt;ufw-before-input&lt;/code&gt; with the &lt;code&gt;ufw6-before-input&lt;/code&gt; chain.&lt;/p&gt;&lt;p&gt;✅&lt;strong&gt;Result:&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;After saving the changes, I reloaded UFW using &lt;code&gt;sudo ufw reload&lt;/code&gt;. I then simulated a SYN flood using &lt;code&gt;hping3&lt;/code&gt;, and the rules worked as expected.&lt;/p&gt;&lt;p&gt;Excess packets were dropped, and logs filled with the defined prefix showed clear evidence of blocked attack attempts. Despite the test flood, CPU usage remained stable, with only a slight increase.&lt;/p&gt;&lt;p&gt;⚠️&lt;strong&gt;Limitation:&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;This approach was simple and effective, but it came with a major limitation. The &lt;code&gt;limit&lt;/code&gt; module enforces a global rate limit, meaning if one client exceeds the threshold, all others are affected.&lt;/p&gt;&lt;p&gt;This isn’t ideal for production environments with multiple users, so &lt;strong&gt;I moved on to a more flexible solution&lt;/strong&gt;.&lt;/p&gt;&lt;h3&gt;Approach 2: Per-IP Rate Limiting with &lt;code&gt;hashlimit&lt;/code&gt;&lt;/h3&gt;&lt;p&gt;While the first method successfully blocked SYN flood attacks, it introduced a serious limitation: the &lt;code&gt;limit&lt;/code&gt; module applies a &lt;strong&gt;global rate limit&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;That means if a single IP address triggers the limit, &lt;strong&gt;all clients&lt;/strong&gt;  are affected – even legitimate ones. This clearly isn’t suitable for a production environment with many concurrent users.&lt;/p&gt;&lt;p&gt;To solve this, I replaced the &lt;code&gt;limit&lt;/code&gt; module with the more flexible &lt;code&gt;hashlimit&lt;/code&gt; module, which lets us apply rate limits on a &lt;strong&gt;per-IP basis&lt;/strong&gt;. That way, only abusive IPs are throttled, while others continue to access the service normally.&lt;/p&gt;&lt;p&gt;Here are the updated rules added to the end of the &lt;code&gt;before.rules&lt;/code&gt; file, just before the final &lt;code&gt;COMMIT&lt;/code&gt; line:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;-A ufw-before-input -p tcp --syn --dport 80 -m conntrack --ctstate NEW -m hashlimit --hashlimit-name http_limit --hashlimit-above 10/second --hashlimit-burst 20 --hashlimit-mode srcip --hashlimit-srcmask 32 -j DROP -A ufw-before-input -p tcp --syn --dport 80 -m conntrack --ctstate NEW -m hashlimit --hashlimit-name http_limit --hashlimit-above 10/second --hashlimit-burst 20 --hashlimit-mode srcip --hashlimit-srcmask 32 -j LOG --log-prefix &amp;quot;[UFW SYN Flood Detected] &amp;quot; -A ufw-before-input -p tcp --syn --dport 80 -m conntrack --ctstate NEW -j ACCEPT
&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;What these rules do:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Drop Rule&lt;/strong&gt; : Drops SYN packets from a single IP if they exceed 10 per second (with a burst of 20).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Log Rule&lt;/strong&gt; : Logs these excess packets using the prefix &lt;code&gt;[UFW SYN Flood Detected]&lt;/code&gt; for monitoring.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Accept Rule&lt;/strong&gt; : Allows new SYN packets that haven’t exceeded the limit to reach the server.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Key Parameters Explained:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;--hashlimit-mode srcip&lt;/code&gt;: Applies limits individually for each &lt;strong&gt;source IP address&lt;/strong&gt;.&lt;/li&gt;&lt;li&gt;&lt;code&gt;--hashlimit-srcmask 32&lt;/code&gt;: Ensures the limit applies to &lt;strong&gt;each unique IP&lt;/strong&gt;. If needed, this can be changed (like &lt;code&gt;/24&lt;/code&gt;) to group IPs by subnet.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;✅&lt;strong&gt;Result:&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;The server successfully blocked SYN flood attempts while &lt;strong&gt;allowing normal traffic&lt;/strong&gt;  to pass through. During testing with &lt;code&gt;hping3&lt;/code&gt;, I could still access the NGINX default page via &lt;code&gt;curl&lt;/code&gt;, confirming the server was not overwhelmed and legitimate traffic was unaffected.&lt;/p&gt;&lt;p&gt;⚠️&lt;strong&gt;Limitation:&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;This approach does &lt;strong&gt;not track or remember attackers&lt;/strong&gt;. If an IP backs off for a moment and resumes the attack, it will be treated as a fresh connection attempt.&lt;/p&gt;&lt;p&gt;There&apos;s no persistence or blacklist – just real-time rate limiting.&lt;/p&gt;&lt;h3&gt;Approach 3: IP Blacklisting with &lt;code&gt;recent&lt;/code&gt; + &lt;code&gt;hashlimit&lt;/code&gt;&lt;/h3&gt;&lt;p&gt;After implementing per-IP rate limiting, I wanted to go one step further – by &lt;strong&gt;blacklisting&lt;/strong&gt;  IPs that continuously misbehaved.&lt;/p&gt;&lt;p&gt;To do this, I combined the &lt;code&gt;hashlimit&lt;/code&gt; module with the &lt;code&gt;recent&lt;/code&gt; module. This setup lets the firewall &lt;strong&gt;track and remember&lt;/strong&gt; IPs that exceed allowed limits and temporarily &lt;strong&gt;block them&lt;/strong&gt;  using a dynamic blacklist.&lt;/p&gt;&lt;p&gt;Here are the rules I added to the end of the &lt;code&gt;before.rules&lt;/code&gt; file, just before the final &lt;code&gt;COMMIT&lt;/code&gt; line:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;-A ufw-before-input -p tcp --syn --dport 80 -m conntrack --ctstate NEW -m hashlimit --hashlimit-name http_limit --hashlimit-above 10/second --hashlimit-burst 20 --hashlimit-mode srcip --hashlimit-srcmask 32 -m recent --name blacklist --set --rsource -j DROP -A ufw-before-input -p tcp --syn --dport 80 -m conntrack --ctstate NEW -m hashlimit --hashlimit-name http_limit --hashlimit-above 10/second --hashlimit-burst 20 --hashlimit-mode srcip --hashlimit-srcmask 32 -j LOG --log-prefix &amp;quot;[UFW SYN Flood Detected] &amp;quot; -A ufw-before-input -m recent --name blacklist --rcheck --seconds 300 --hitcount 1 --rsource -j DROP -A ufw-before-input -p tcp --syn --dport 80 -m conntrack --ctstate NEW -j ACCEPT
&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;What these rules do:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Blacklist &amp;amp; Drop Rule&lt;/strong&gt;: Drops packets from any IP that exceeds the rate limit and adds that IP to a temporary blacklist.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Log Rule&lt;/strong&gt; : Logs excess SYN packets for monitoring.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Blacklist Check Rule&lt;/strong&gt; : Drops packets from IPs already on the blacklist for 5 minutes.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Accept Rule&lt;/strong&gt; : Allows new, valid connections to port 80 that haven’t exceeded the threshold.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;✅&lt;strong&gt;Result:&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;During testing, my IP was automatically blacklisted after simulating a SYN flood using &lt;code&gt;hping3&lt;/code&gt;. While blacklisted, all packets from my IP were dropped, and I couldn&apos;t access the web server.&lt;/p&gt;&lt;p&gt;After 5 minutes of inactivity, access was restored – confirming that &lt;strong&gt;blacklist enforcement and timeout both worked correctly&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;You can check which IPs are currently blacklisted using:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo cat /proc/net/xt_recent/blacklist
&lt;/pre&gt;&lt;p&gt;⚠️&lt;strong&gt;Limitation:&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;While this setup worked better than global limits, I quickly ran into a few issues.&lt;/p&gt;&lt;p&gt;Since the blacklist created by the &lt;code&gt;recent&lt;/code&gt; module lives only in memory, all entries are &lt;strong&gt;wiped after a reboot&lt;/strong&gt;. That means persistent attackers could just wait it out or come back with new IPs.&lt;/p&gt;&lt;p&gt;Also, because IPs are &lt;strong&gt;blacklisted immediately&lt;/strong&gt; after crossing the threshold, there’s a chance of blocking legitimate users who briefly spike above the limit.&lt;/p&gt;&lt;p&gt;This approach definitely offered more control than before, but it &lt;strong&gt;didn’t scale well&lt;/strong&gt; for handling more distributed or high-volume SYN flood attacks. It felt like a solid step forward, but not something I’d rely on as a long-term or standalone solution.&lt;/p&gt;&lt;h2&gt;Final Solution: Combining Rate Limiting and Fail2ban&lt;/h2&gt;&lt;p&gt;In this final solution, we combine &lt;strong&gt;per-IP SYN flood protection&lt;/strong&gt;  with &lt;strong&gt;persistent blacklisting and broader access control using Fail2ban&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;recent&lt;/code&gt; module we used earlier only stores blacklisted IPs in memory, so all entries are lost upon reboot. While this isn&apos;t a major issue for many setups, &lt;strong&gt;Fail2ban&lt;/strong&gt;  offers a cleaner and more powerful alternative by:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Persistently block&lt;/strong&gt;  IPs across reboots.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Ban IPs only after repeated violations&lt;/strong&gt;  (e.g., 5 times), rather than on the first offense.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Unban&lt;/strong&gt;  IPs manually or automatically after a set duration.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This hybrid setup gives us the best of both worlds: real-time traffic control through UFW, and intelligent, manageable banning via Fail2ban.&lt;/p&gt;&lt;p&gt;By configuring Fail2ban to watch &lt;code&gt;/var/log/syslog&lt;/code&gt; for entries matching our custom log prefix, we can automatically block any IP that triggers our rules repeatedly.&lt;/p&gt;&lt;p&gt;This approach avoids prematurely banning legitimate users who may accidentally trigger rate limits, while still stopping persistent attackers effectively.&lt;/p&gt;&lt;h3&gt;Install and Configure Fail2ban&lt;/h3&gt;&lt;p&gt;Install Fail2ban with:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt install fail2ban
&lt;/pre&gt;&lt;p&gt;Now create a &lt;strong&gt;custom filter&lt;/strong&gt;  so that Fail2ban knows what to look for in your logs. The log prefix we used in our UFW rules earlier was:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;[UFW SYN Flood Detected]
&lt;/pre&gt;&lt;p&gt;So we’ll create a filter to catch that. Go to &lt;code&gt;/etc/fail2ban/filter.d/&lt;/code&gt; and create a file called &lt;code&gt;synflood.conf&lt;/code&gt;:&lt;/p&gt;&lt;pre data-language=&quot;ini&quot;&gt;[Definition] failregex = .*UFW SYN Flood Detected.*SRC=.*DPT=\d+.* ignoreregex =
&lt;/pre&gt;&lt;p&gt;This regex will match log entries that look like:&lt;/p&gt;&lt;pre data-language=&quot;ini&quot;&gt;[UFW SYN Flood Detected] IN=... SRC=192.168.0.1 DPT=80 ...
&lt;/pre&gt;&lt;p&gt;This works for both port 80 and 443, so you don’t need to create separate filters.&lt;/p&gt;&lt;p&gt;Fail2ban uses &amp;quot;jails&amp;quot; to define what to monitor, how many times to allow it, and what to do when the threshold is exceeded.&lt;/p&gt;&lt;p&gt;Create a local copy of the jail configuration:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
&lt;/pre&gt;&lt;p&gt;This is important because you should not modify the &lt;code&gt;jail.conf&lt;/code&gt; file directly, as it may be overwritten during an update.&lt;/p&gt;&lt;p&gt;Edit &lt;code&gt;jail.local&lt;/code&gt;, and under the &lt;code&gt;# JAILS&lt;/code&gt; section, add:&lt;/p&gt;&lt;pre data-language=&quot;ini&quot;&gt;[synflood] enabled = true filter = synflood action = iptables[type=allports, name=synflood, chain=fail2ban, protocol=tcp] logpath = /var/log/syslog maxretry = 5 findtime = 600 bantime = 86400
&lt;/pre&gt;&lt;p&gt;We are creating a new jail called &lt;code&gt;synflood&lt;/code&gt; that will block IPs from accessing all ports on the server if they appear five times in the &lt;code&gt;/var/log/syslog&lt;/code&gt; file within ten minutes, for a duration of one day.&lt;/p&gt;&lt;p&gt;As you may have noticed, I’m using &lt;code&gt;iptables&lt;/code&gt; to block IPs instead of UFW, by adding them to a new chain called &lt;code&gt;fail2ban&lt;/code&gt;. The reason for this is that if we use UFW, it will block IPs at the user level by adding them to the &lt;code&gt;user.rules&lt;/code&gt; file. However, this won’t take effect because the &lt;code&gt;before.rules&lt;/code&gt; file is processed first.&lt;/p&gt;&lt;p&gt;You can also block traffic only on ports 80 and 443 by using the &lt;code&gt;multiport&lt;/code&gt; option, like this:&lt;/p&gt;&lt;pre data-language=&quot;ini&quot;&gt;action = iptables[type=multiport, name=synflood, chain=fail2ban, port=&amp;quot;http,https&amp;quot;, protocol=tcp]
&lt;/pre&gt;&lt;p&gt;This is useful if you only want to restrict web traffic while leaving other services like SSH or FTP unaffected by the ban.&lt;/p&gt;&lt;h3&gt;Update Firewall Rules&lt;/h3&gt;&lt;p&gt;Next, we modify UFW’s &lt;code&gt;before.rules&lt;/code&gt; file so that:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Fail2ban can inject blocking rules early in the firewall chain.&lt;/li&gt;&lt;li&gt;We can isolate SYN flood protection into its own chain.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Edit &lt;code&gt;/etc/ufw/before.rules&lt;/code&gt; and create two new chains placing them under the &lt;code&gt;# End required lines&lt;/code&gt; line:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;:SYN_FLOOD_PROTECTION - [0:0] :fail2ban - [0:0]
&lt;/pre&gt;&lt;p&gt;Then replace/add these rules:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;# Direct traffic to fail2ban first -A ufw-before-input -j fail2ban # fail2ban will insert IP block rules above this line -A fail2ban -j RETURN # Now send relevant traffic to our SYN_FLOOD_PROTECTION chain -A ufw-before-input -p tcp --syn --dport 80 -j SYN_FLOOD_PROTECTION -A ufw-before-input -p tcp --syn --dport 443 -j SYN_FLOOD_PROTECTION # SYN rate-limiting and logging (HTTP) -A SYN_FLOOD_PROTECTION -p tcp --syn --dport 80 -m conntrack --ctstate NEW -m hashlimit --hashlimit-name http_limit --hashlimit-above 10/second --hashlimit-burst 20 --hashlimit-mode srcip --hashlimit-srcmask 32 -j DROP -A SYN_FLOOD_PROTECTION -p tcp --syn --dport 80 -m conntrack --ctstate NEW -m hashlimit --hashlimit-name http_limit --hashlimit-above 10/second --hashlimit-burst 20 --hashlimit-mode srcip --hashlimit-srcmask 32 -j LOG --log-prefix &amp;quot;[UFW SYN Flood Detected] &amp;quot; # SYN rate-limiting and logging (HTTPS) -A SYN_FLOOD_PROTECTION -p tcp --syn --dport 443 -m conntrack --ctstate NEW -m hashlimit --hashlimit-name http_limit --hashlimit-above 10/second --hashlimit-burst 20 --hashlimit-mode srcip --hashlimit-srcmask 32 -j DROP -A SYN_FLOOD_PROTECTION -p tcp --syn --dport 443 -m conntrack --ctstate NEW -m hashlimit --hashlimit-name http_limit --hashlimit-above 10/second --hashlimit-burst 20 --hashlimit-mode srcip --hashlimit-srcmask 32 -j LOG --log-prefix &amp;quot;[UFW SYN Flood Detected] &amp;quot; # Allow remaining SYN packets through -A SYN_FLOOD_PROTECTION -p tcp --syn --dport 80 -m conntrack --ctstate NEW -j ACCEPT -A SYN_FLOOD_PROTECTION -p tcp --syn --dport 443 -m conntrack --ctstate NEW -j ACCEPT
&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;🔍 What’s Happening Here?&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;First&lt;/strong&gt; , we redirect traffic from the &lt;code&gt;ufw-before-input&lt;/code&gt; chain to the &lt;code&gt;fail2ban&lt;/code&gt; chain.&lt;/li&gt;&lt;li&gt;The rule &lt;code&gt;-A fail2ban -j RETURN&lt;/code&gt; exits the &lt;code&gt;fail2ban&lt;/code&gt; chain and moves traffic to the next rule in &lt;code&gt;ufw-before-input&lt;/code&gt;, which sends it to our &lt;code&gt;SYN_FLOOD_PROTECTION&lt;/code&gt; chain.&lt;/li&gt;&lt;li&gt;In the &lt;code&gt;fail2ban&lt;/code&gt; chain, &lt;strong&gt;Fail2ban dynamically inserts IP block rules&lt;/strong&gt;  when it detects malicious activity.&lt;/li&gt;&lt;li&gt;So, &lt;strong&gt;UFW checks the  &lt;code&gt;fail2ban&lt;/code&gt; chain first&lt;/strong&gt;, and if the source IP is blacklisted, it’s immediately denied access.&lt;/li&gt;&lt;li&gt;Once it reaches the &lt;code&gt;RETURN&lt;/code&gt; rule, traffic is passed along to the &lt;code&gt;SYN_FLOOD_PROTECTION&lt;/code&gt; chain, which:&lt;ul&gt;&lt;li&gt;&lt;strong&gt;drops&lt;/strong&gt;  traffic that exceeds our rate limits,&lt;/li&gt;&lt;li&gt;&lt;strong&gt;logs&lt;/strong&gt;  it for Fail2ban to analyze,&lt;/li&gt;&lt;li&gt;and &lt;strong&gt;allows&lt;/strong&gt;  traffic from IPs that are behaving normally.&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;Don’t forget to update &lt;code&gt;before6.rules&lt;/code&gt; the same way for IPv6 support – replacing &lt;code&gt;ufw-before-input&lt;/code&gt; with the &lt;code&gt;ufw6-before-input&lt;/code&gt; chain.&lt;/p&gt;&lt;p&gt;This layered approach ensures blocked IPs are filtered before they hit your rate limit logic, making your firewall more efficient and more secure.&lt;/p&gt;&lt;h3&gt;Reload UFW and Restart Fail2ban&lt;/h3&gt;&lt;p&gt;After replacing/adding your firewall rules and setting up Fail2ban, apply the changes by reloading UFW and restarting Fail2ban:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw reload sudo systemctl restart fail2ban
&lt;/pre&gt;&lt;p&gt;This ensures both your new firewall chains and Fail2ban configurations are actively enforced.&lt;/p&gt;&lt;h3&gt;Monitor and Test Fail2ban Protection&lt;/h3&gt;&lt;p&gt;Check the status of your &lt;code&gt;synflood&lt;/code&gt; jail:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo fail2ban-client status synflood
&lt;/pre&gt;&lt;p&gt;This shows how many IPs are currently banned, total bans, and the list of blocked IPs.&lt;/p&gt;&lt;p&gt;Let&apos;s inspect the content of the custom chain we created:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo iptables -L fail2ban -v -n
&lt;/pre&gt;&lt;p&gt;You will notice two rules inside this chain:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The first redirects traffic to a new chain called &lt;code&gt;f2b-synflood&lt;/code&gt;, which Fail2ban automatically creates to manage and organize firewall rules. This ensures that if the same chain is used for another jail, the rules remain separate.&lt;/li&gt;&lt;li&gt;The second rule allows any traffic not handled by the &lt;code&gt;f2b-synflood&lt;/code&gt; chain to proceed.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;❗&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; You may not see the first rule if no IPs have been blocked yet.&lt;/p&gt;&lt;p&gt;To view the block rules that Fail2ban has added, examine the contents of the &lt;code&gt;f2b-synflood&lt;/code&gt; chain using the command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo iptables -L f2b-synflood -v -n
&lt;/pre&gt;&lt;p&gt;Now, if I initiate an attack using &lt;code&gt;hping3&lt;/code&gt;, Fail2ban will detect the attack and add a rule to block my IP from accessing all ports:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;Chain f2b-synflood (1 references) pkts bytes target prot opt in out source destination 1657K 66M REJECT 0 -- * * 192.168.64.7 0.0.0.0/0 reject-with icmp-port-unreachable 445K 18M RETURN 0 -- * * 0.0.0.0/0 0.0.0.0/0
&lt;/pre&gt;&lt;p&gt;And indeed, Fail2ban successfully caught me.&lt;/p&gt;&lt;p&gt;If I want to unblock my IP, I can simply use the command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo fail2ban-client set synflood unbanip 192.168.64.7
&lt;/pre&gt;&lt;p&gt;It&apos;s simpler compared to using the &lt;code&gt;recent&lt;/code&gt; module.&lt;/p&gt;&lt;p&gt;You can also check the IPs that Fail2ban has identified in the log file but has not yet blocked (because they are still under the limit) by using the &lt;code&gt;sudo grep synflood /var/log/fail2ban.log&lt;/code&gt; command.&lt;/p&gt;&lt;h2&gt;Key Consideration &amp;amp; Recommended Safeguards&lt;/h2&gt;&lt;p&gt;Before wrapping up, I&apos;d like to highlight an important point I mentioned earlier in this guide.&lt;/p&gt;&lt;p&gt;Our current setup – which limits each IP to 10 SYN packets per second with an initial burst of 20 – works well for &lt;strong&gt;blocking small to medium-sized SYN flood attacks&lt;/strong&gt;  coming from a few sources.&lt;/p&gt;&lt;p&gt;🧠&lt;/p&gt;&lt;p&gt;&lt;strong&gt;However:&lt;/strong&gt; What happens if an attacker launches a &lt;strong&gt;DDoS attack&lt;/strong&gt; using thousands of IPs, each sending packets below our limit?&lt;/p&gt;&lt;p&gt;Since our rate-limiting rules apply &lt;strong&gt;per IP address&lt;/strong&gt; , such an attack might &lt;strong&gt;bypass the firewall entirely&lt;/strong&gt;  – because each IP appears &amp;quot;well-behaved&amp;quot; individually. The &lt;strong&gt;total volume&lt;/strong&gt; , however, could still overwhelm your server.&lt;/p&gt;&lt;p&gt;I haven’t tested this large-scale scenario myself, so I can&apos;t say exactly how it would behave – but it&apos;s definitely something to consider in your threat model.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;What You Can Do:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Use a server provider with built-in DDoS protection:&lt;/strong&gt; Providers like &lt;strong&gt;Hetzner&lt;/strong&gt;  block malicious traffic at the network level – before it hits your server.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Enable rate limiting in NGINX:&lt;/strong&gt; Use modules like &lt;code&gt;limit_req&lt;/code&gt; and &lt;code&gt;limit_conn&lt;/code&gt; to throttle request rates and concurrent connections per IP.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Use a CDN like Cloudflare:&lt;/strong&gt; Cloudflare offers &lt;strong&gt;DDoS protection&lt;/strong&gt; , &lt;strong&gt;WAF&lt;/strong&gt; , and &lt;strong&gt;edge caching&lt;/strong&gt; , filtering harmful traffic before it reaches your origin server.**&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;New to Hetzner? &lt;a href=&quot;https://hetzner.cloud/?ref=MC4Yy318xX5X&quot;&gt;Use my link&lt;/a&gt; to get free credits!&lt;/p&gt;&lt;p&gt;Combining these external protections with your current UFW and Fail2ban setup creates a &lt;strong&gt;multi-layered defense&lt;/strong&gt;  strategy that can handle both targeted and large-scale attacks effectively.&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;In this guide, we walked through multiple approaches to protect your server against SYN flood attacks – from simple firewall rules using &lt;code&gt;limit&lt;/code&gt;, to more advanced setups using &lt;code&gt;hashlimit&lt;/code&gt;, &lt;code&gt;recent&lt;/code&gt;, and finally integrating &lt;strong&gt;Fail2ban&lt;/strong&gt;  for persistent, dynamic IP blocking.&lt;/p&gt;&lt;p&gt;While the process involved several steps and configurations, the end result is a much more secure server, capable of mitigating SYN flood attacks while still allowing legitimate traffic through.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Looking for more?&lt;/strong&gt; Check out my &lt;a href=&quot;https://ivansalloum.com/collections/linux-server-security/&quot;&gt;full collection&lt;/a&gt; of in-depth Linux server security guides.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;💬  Found this guide helpful?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;I&apos;d love to hear about your experiences, questions, or ideas in the discussion section below. Your feedback not only helps improve future guides – it helps fellow admins on their own journey.&lt;/p&gt;&lt;p&gt;Prefer a more direct conversation? Feel free to &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;contact me&lt;/a&gt; anytime.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Security</category></item><item><title>Understanding TCP and the Three-Way Handshake</title><link>https://ivansalloum.com/understanding-tcp-and-the-three-way-handshake/</link><guid isPermaLink="true">https://ivansalloum.com/understanding-tcp-and-the-three-way-handshake/</guid><description>Learn the basics of TCP and the Three-Way Handshake, exploring how data is transmitted, with real-world HTTP traffic example.</description><pubDate>Tue, 05 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;The Transmission Control Protocol (TCP) is a set of rules that helps devices talk to each other over the internet.&lt;/p&gt;&lt;p&gt;It’s designed to reliably send data across networks, ensuring they reach their destination successfully.&lt;/p&gt;&lt;p&gt;A key part of this reliability is the TCP Three-Way Handshake, a process that establishes a secure connection between two devices before data exchange begins.&lt;/p&gt;&lt;p&gt;In this reading, we will explore the role of TCP and the Three-Way Handshake in ensuring accurate and secure data transmission.&lt;/p&gt;&lt;p&gt;Additionally, we will analyze an example of HTTP traffic to illustrate these concepts in action.&lt;/p&gt;&lt;h2&gt;The TCP/IP Model&lt;/h2&gt;&lt;p&gt;Let’s start with the TCP/IP model, as it’s essential for understanding how TCP operates.&lt;/p&gt;&lt;p&gt;TCP/IP stands for Transmission Control Protocol and Internet Protocol, and it lays the foundational for network communication by explaining how data moves across networks.&lt;/p&gt;&lt;p&gt;The TCP/IP model is made up of four layers, each with its own role in the data transmission process: the application layer, transport layer, internet layer, and network access layer.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://ivansalloum.com/content/images/2024/11/TCP-IP.png&quot; alt=&quot;TCP/IP Model&quot;&gt;Source: Google&apos;s Cybersecurity Course on Coursera&lt;/p&gt;&lt;p&gt;TCP is responsible for establishing a connection between two devices and ensuring the reliable transmission of data, while IP manages the routing and addressing of packets.&lt;/p&gt;&lt;h3&gt;Where TCP Fits In&lt;/h3&gt;&lt;p&gt;In the TCP/IP model, TCP works at the Transport Layer.&lt;/p&gt;&lt;p&gt;Here’s how the process works from start to finish:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The Application Layer passes data, such as web requests, emails, or file transfers, to TCP, which divides it into smaller units called &lt;strong&gt;segments&lt;/strong&gt;.&lt;/li&gt;&lt;li&gt;TCP prepares these segments for reliable delivery by adding headers with essential information, like sequence numbers, to ensure they can be reassembled in the correct order on the receiving end.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;A segment is the basic unit of data transmission in TCP, consisting of a header and a payload (the actual data being sent).&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Once the data reaches the Internet Layer, IP takes over, adding its own headers for addressing and routing, turning these segments into &lt;strong&gt;packets&lt;/strong&gt;  that can be transmitted across networks.&lt;/li&gt;&lt;li&gt;Finally, packets reach the Network Access Layer. Here, they are wrapped with additional headers and trailers, transforming them into &lt;strong&gt;frames&lt;/strong&gt;. These frames are the actual units sent over the network, whether by wired, wireless, or other means.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This layered process, from segments to packets to frames, ensures that data moves smoothly and accurately across networks, with each layer handling a specific part of the journey.&lt;/p&gt;&lt;h3&gt;Example: Visiting a Website&lt;/h3&gt;&lt;p&gt;Let’s say you want to visit a website from your computer, which is connected to a network set up like this: your computer connects to a switch, then to a router, followed by another switch, and finally to a server that hosts the website.&lt;/p&gt;&lt;p&gt;You type the website&apos;s URL into your browser and click Enter. Your computer first uses the DNS server within your network to determine the IP address associated with this URL, which is typically the server&apos;s IP address where the website is hosted. After that, your computer sends a GET request to retrieve the content of the website.&lt;/p&gt;&lt;p&gt;Now, a process called encapsulation occurs. The application protocol we are using is HTTPS. Within the Application Layer, the GET request is encapsulated inside an HTTPS header that informs the destination server about the type of request being made.&lt;/p&gt;&lt;p&gt;Let’s imagine the data (the GET request) as a piece of letter that is encapsulated inside an envelope. Each time we move down the TCP/IP model, the envelope is wrapped in another envelope with additional data.&lt;/p&gt;&lt;p&gt;The request moves down to the Transport Layer. Here, it is wrapped in a TCP header, which includes important information like source and destination port numbers, sequence numbers, and other control data. At this stage, the data is divided into segments.&lt;/p&gt;&lt;p&gt;Next, we move to the Internet Layer, where a new header is added with the source and destination IP addresses, transforming the segments into packets.&lt;/p&gt;&lt;p&gt;Finally, the packets arrive at the Network Access Layer, where they are further encapsulated with additional headers and trailers, turning them into frames. These frames are then sent over Ethernet cables to the switch.&lt;/p&gt;&lt;p&gt;The switch opens the first envelope and sees that the data should be sent to a specific MAC address. It consults its MAC address table to determine which router it should send the frames to.&lt;/p&gt;&lt;p&gt;Now, the process of opening envelopes is called decapsulation – it’s like opening the envelopes to check their contents.&lt;/p&gt;&lt;p&gt;The router receives the frames and opens the second envelope, which contains the destination IP. It examines its routing tables to determine where the data should go.&lt;/p&gt;&lt;p&gt;Since the switch can only see MAC addresses, the router must inform the second switch of the MAC address corresponding to the destination IP – the server hosting the website.&lt;/p&gt;&lt;p&gt;To find out this MAC address, the router sends an ARP request. Once it obtains the MAC address, the router re-encapsulates the packets with the new MAC addresses into frames and sends them to the switch.&lt;/p&gt;&lt;p&gt;The data is then transmitted through the second switch to the server. The server decapsulates the frames, removing each layer until it reveals the HTTPS message. It recognizes your GET request and prepares to send back the contents of the website.&lt;/p&gt;&lt;p&gt;The server then repeats the same process to send the webpage data back to your computer, encapsulating it in layers as it goes.&lt;/p&gt;&lt;h2&gt;Why TCP is Reliable&lt;/h2&gt;&lt;p&gt;TCP begins with a Three-Way Handshake to establish a connection between two devices:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;The client sends a &lt;strong&gt;SYN (synchronize)&lt;/strong&gt; packet to the server.&lt;/li&gt;&lt;li&gt;The server responds with a &lt;strong&gt;SYN-ACK (synchronize-acknowledge)&lt;/strong&gt; packet.&lt;/li&gt;&lt;li&gt;The client sends an &lt;strong&gt;ACK (acknowledge)&lt;/strong&gt; packet back to the server.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;This process ensures both devices are ready to communicate.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;SYN and ACK are called flags, which are used to signify the state of a connection.&lt;/p&gt;&lt;p&gt;Once the connection is established, TCP begins breaking down the data into smaller segments.&lt;/p&gt;&lt;p&gt;Segmenting the data enables smoother transmission and prevents large chunks from overwhelming the network.&lt;/p&gt;&lt;p&gt;Each segment is wrapped in a TCP header that holds essential details. This header includes information such as source and destination port numbers, sequence numbers, and flags.&lt;/p&gt;&lt;p&gt;TCP assigns each segment a sequence number. This allows the receiver to put all the data segments back together in the right order, even if they arrive out of sequence.&lt;/p&gt;&lt;p&gt;As the receiver gets each segment, it sends back an acknowledgment (ACK) to the sender. This confirms that each segment has been received successfully.&lt;/p&gt;&lt;p&gt;If an acknowledgment is missing, the sender knows that segment may have been lost and will retransmit it.&lt;/p&gt;&lt;p&gt;To further ensure data integrity, TCP includes a checksum in each segment’s header to verify that data wasn’t corrupted during transmission.&lt;/p&gt;&lt;p&gt;If any segment is corrupted, TCP will retransmit it until it is received correctly.&lt;/p&gt;&lt;p&gt;These features ensure that all data reaches its destination completely and accurately.&lt;/p&gt;&lt;p&gt;Because of its reliability, TCP is ideal for applications where accurate data delivery is essential, such as web browsing (HTTP &amp;amp; HTTPS), file sharing (FTP), and email (IMAP, POP, SMTP).&lt;/p&gt;&lt;h2&gt;The Three-Way Handshake&lt;/h2&gt;&lt;p&gt;TCP uses this handshake process to establish a reliable connection between a client (such as your browser) and a server (like a website).&lt;/p&gt;&lt;p&gt;Think of it as a way for both parties to agree on how they want to communicate before the actual data transfer begins.&lt;/p&gt;&lt;p&gt;&lt;em&gt;◆ The TCP three-way handshake: SYN → SYN-ACK → ACK — view on the site&lt;/em&gt;&lt;/p&gt;&lt;p&gt;Step-by-step breakdown when visiting a website:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Initiation (SYN)&lt;/strong&gt; :** The browser, acting as the client, initiates the connection by sending a &lt;strong&gt;SYN (synchronize)&lt;/strong&gt;  packet to the server hosting the website. This packet includes a starting sequence number (e.g., X), essentially signaling, “Hey, I want to start a connection, and here’s my starting sequence number: X.”&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Response (SYN-ACK)&lt;/strong&gt; : Upon receiving the SYN packet, the server responds with a &lt;strong&gt;SYN-ACK (synchronize-acknowledge)&lt;/strong&gt;  packet. This response includes its own starting sequence number (e.g., Y), along with an acknowledgment of the client’s sequence number incremented by 1 (X + 1). It’s like the server replying, “I’ve received your request and here’s my starting sequence number: Y. I acknowledge your number X + 1.”&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Acknowledgment (ACK)&lt;/strong&gt; : Finally, the client replies with an &lt;strong&gt;ACK (acknowledge)&lt;/strong&gt;  packet to confirm receipt of the server’s SYN-ACK packet. This packet contains the server’s sequence number incremented by 1 (Y + 1), signaling, “I got your response, and now we’re synchronized and ready to exchange data.”&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;The packet is called a SYN packet because, during the handshake, it is necessary to synchronize sequence numbers between the client and server. The starting sequence number for both sides is known as the &lt;strong&gt;Initial Sequence Number (ISN)&lt;/strong&gt; , a vital component in TCP communication.&lt;/p&gt;&lt;p&gt;When the client sends a SYN packet to the server, it’s essentially saying, “Hey, I’m going to start counting the data I send from this number.”&lt;/p&gt;&lt;p&gt;In response, the server needs to take an important action: it acknowledges the client’s sequence number by incrementing it by one in its reply. This lets the client know that the server has received its message and is ready to proceed with the connection.&lt;/p&gt;&lt;p&gt;Sequence numbers are chosen randomly instead of starting at zero. The client and server each select random numbers (X and Y) for the following reasons:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Preventing Confusion&lt;/strong&gt; : If the client and server have communicated previously and start a new session, using the same sequence numbers could lead to old messages being mistaken for new ones. Random starting points help prevent this overlap and confusion.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Security&lt;/strong&gt; : Randomizing helps protect against certain types of attacks, such as TCP spoofing, ensuring that only legitimate connections are recognized.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This synchronization is crucial because TCP ensures reliable data transfer by tracking and managing the order of packets.&lt;/p&gt;&lt;p&gt;By knowing the sequence numbers, both the client and server can reassemble data in the correct order and detect any missing or out-of-sequence segments.&lt;/p&gt;&lt;h2&gt;The Handshake in Action&lt;/h2&gt;&lt;p&gt;To illustrate the TCP Three-Way Handshake in action, let’s analyze the following &lt;code&gt;tcpdump&lt;/code&gt; output generated from a connection to a web server on port 80 (HTTP).&lt;/p&gt;&lt;p&gt;I used the &lt;code&gt;tcpdump -nn -S port 80&lt;/code&gt; command to monitor traffic on port 80, and then executed the &lt;code&gt;curl http://167.235.253.88&lt;/code&gt; command to initiate a request to the web server.&lt;/p&gt;&lt;p&gt;This output captures the sequence of packets exchanged during the handshake and the subsequent data transfer:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;13:34:12.344584 IP 92.72.34.89.26232 &amp;gt; 167.235.253.88.80: Flags [S], seq 3531231207, win 65535, options [mss 1412,nop,wscale 6,nop,nop,TS val 3019538245 ecr 0,sackOK,eol], length 0 13:34:12.344657 IP 167.235.253.88.80 &amp;gt; 92.72.34.89.26232: Flags [S.], seq 2227943007, ack 3531231208, win 65160, options [mss 1460,sackOK,TS val 1425199703 ecr 3019538245,nop,wscale 7], length 0 13:34:12.372629 IP 92.72.34.89.26232 &amp;gt; 167.235.253.88.80: Flags [.], ack 2227943008, win 2056, options [nop,nop,TS val 3019538274 ecr 1425199703], length 0 13:34:12.372629 IP 92.72.34.89.26232 &amp;gt; 167.235.253.88.80: Flags [P.], seq 3531231208:3531231285, ack 2227943008, win 2056, options [nop,nop,TS val 3019538274 ecr 1425199703], length 77: HTTP: GET / HTTP/1.1 13:34:12.372862 IP 167.235.253.88.80 &amp;gt; 92.72.34.89.26232: Flags [.], ack 3531231285, win 509, options [nop,nop,TS val 1425199732 ecr 3019538274], length 0 13:34:12.373363 IP 167.235.253.88.80 &amp;gt; 92.72.34.89.26232: Flags [P.], seq 2227943008:2227943870, ack 3531231285, win 509, options [nop,nop,TS val 1425199732 ecr 3019538274], length 862: HTTP: HTTP/1.1 200 OK 13:34:12.400041 IP 92.72.34.89.26232 &amp;gt; 167.235.253.88.80: Flags [.], ack 2227943870, win 2042, options [nop,nop,TS val 3019538302 ecr 1425199732], length 0
&lt;/pre&gt;&lt;p&gt;The flags are shown inside the square brackets, such as &lt;code&gt;[S]&lt;/code&gt; for SYN, &lt;code&gt;[S.]&lt;/code&gt; for SYN-ACK, and &lt;code&gt;[.]&lt;/code&gt; for ACK.&lt;/p&gt;&lt;p&gt;Let’s first analyze the first three packets, which represent the handshake:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;SYN Packet (Initiation)&lt;/strong&gt; : At &lt;strong&gt;13:34:12.344584&lt;/strong&gt; , the client (IP &lt;strong&gt;92.72.34.89&lt;/strong&gt;) initiates a connection to the server (IP &lt;strong&gt;167.235.253.88&lt;/strong&gt;) by sending a SYN packet from its ephemeral port &lt;strong&gt;26232&lt;/strong&gt;  to the server&apos;s port &lt;strong&gt;80&lt;/strong&gt;. This packet indicates that the client is ready to establish a connection and includes its initial sequence number (&lt;strong&gt;3531231207&lt;/strong&gt;).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;SYN-ACK Packet (Response)&lt;/strong&gt; : Just &lt;strong&gt;73 microseconds&lt;/strong&gt;  later, at &lt;strong&gt;13:34:12.344657&lt;/strong&gt; , the server responds to the client’s SYN packet with a SYN-ACK packet. This packet is sent from port &lt;strong&gt;80&lt;/strong&gt; of the server to the client&apos;s ephemeral port &lt;strong&gt;26232&lt;/strong&gt;. The server acknowledges the client&apos;s request by incrementing the client&apos;s sequence number to &lt;strong&gt;3531231208&lt;/strong&gt;  and introduces its own initial sequence number (&lt;strong&gt;2227943007&lt;/strong&gt;).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;ACK Packet (Acknowledgment)&lt;/strong&gt; : At &lt;strong&gt;13:34:12.372629&lt;/strong&gt; , the client sends an ACK packet back to the server to confirm receipt of the server’s SYN-ACK packet. This packet acknowledges the server&apos;s sequence number, incremented to &lt;strong&gt;2227943008&lt;/strong&gt;. This communication occurs between the same port numbers as before: the client’s port &lt;strong&gt;26232&lt;/strong&gt;  and the server’s port &lt;strong&gt;80&lt;/strong&gt;.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;At this point, the client and server are synchronized and ready to exchange data.&lt;/p&gt;&lt;p&gt;Note that the TCP handshake occurs before any actual data exchange takes place. The handshake is completed prior to TCP dividing the data into segments and passing them down to the Network Layer for encapsulation into packets. You can confirm this by observing that the length of the handshake packets is zero, indicating that no data is being transferred during the handshake process itself.&lt;/p&gt;&lt;p&gt;Sequence numbers play a crucial role, helping the sender determine whether the receiver has received the data by sending acknowledgments. When the receiver acknowledges the data, it adds the length of the received data to the sender’s sequence number.&lt;/p&gt;&lt;p&gt;For example, if the client sends &lt;strong&gt;100 bytes&lt;/strong&gt;  of data, the server adds &lt;strong&gt;100&lt;/strong&gt;  to the client’s sequence number in its acknowledgment.&lt;/p&gt;&lt;p&gt;During the handshake, only &lt;strong&gt;1&lt;/strong&gt;  is added to the sequence number because no actual data is being transferred – just a synchronization request. This increment of &lt;strong&gt;1&lt;/strong&gt;  is often referred to as a &lt;strong&gt;Ghost Byte&lt;/strong&gt; , as it does not correspond to any real data being sent. It simply indicates that the handshake was acknowledged.&lt;/p&gt;&lt;p&gt;Now, let’s analyze the last four packets in which data exchange begins:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Data Transmission Initiated by the Client&lt;/strong&gt; : In this packet, the client (IP &lt;strong&gt;92.72.34.89&lt;/strong&gt;) sends a &lt;strong&gt;push (P)&lt;/strong&gt;  packet with a sequence number ranging from &lt;strong&gt;3531231208&lt;/strong&gt; to&lt;strong&gt;3531231285&lt;/strong&gt; , indicating it is sending &lt;strong&gt;77 bytes&lt;/strong&gt;  of data. This packet contains an HTTP GET request.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Acknowledgment from the Server&lt;/strong&gt; : The server responds with an acknowledgment (ACK) for the data received from the client, indicating that it has successfully received the last byte of the client&apos;s data (up to sequence number &lt;strong&gt;3531231285&lt;/strong&gt;).&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Data Transmission from the Server&lt;/strong&gt; : The server now sends its response, which is a push packet containing an HTTP status code &lt;strong&gt;200 OK&lt;/strong&gt;  along with &lt;strong&gt;862 bytes&lt;/strong&gt; of data.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Final Acknowledgment from the Client&lt;/strong&gt; : Finally, the client sends another acknowledgment, confirming that it has received the entire response from the server up to the sequence number &lt;strong&gt;2227943870&lt;/strong&gt;.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;In summary, these packets show how data is exchanged after the TCP handshake. They demonstrate how the client requests data, the server responds, and how acknowledgments help keep the connection reliable.&lt;/p&gt;&lt;h2&gt;Troubleshooting Tip&lt;/h2&gt;&lt;p&gt;When encountering network issues, start by analyzing network traffic and focus on the transport layer within the TCP/IP model.&lt;/p&gt;&lt;p&gt;This approach allows for a more streamlined troubleshooting process:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;If TCP is functioning correctly:&lt;/strong&gt;  Move up the TCP/IP model to inspect the application layer.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;If TCP shows issues&lt;/strong&gt;  (e.g., retransmissions or out-of-order packets): Shift focus downward to diagnose the network layer, where underlying problems may be affecting traffic flow.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;This method helps isolate problems efficiently by using the TCP/IP model as a guide.&lt;/p&gt;&lt;p&gt;Starting at the transport layer allows you to quickly determine if the issue lies higher in the stack, such as application-level misconfigurations, or lower in the network layers, where physical connectivity or routing might be at fault.&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;In conclusion, understanding how TCP works and the Three-Way Handshake is essential for grasping how data is transmitted and troubleshooting network issues.&lt;/p&gt;&lt;p&gt;If you found value in this reading or have any questions or feedback, please don&apos;t hesitate to share your thoughts in the &lt;strong&gt;discussion&lt;/strong&gt; section.&lt;/p&gt;&lt;p&gt;Your input is greatly appreciated, and you can also &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;contact me&lt;/a&gt; directly if you prefer.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Networking</category></item><item><title>Linux Server Security</title><link>https://ivansalloum.com/linux-server-security/</link><guid isPermaLink="true">https://ivansalloum.com/linux-server-security/</guid><description>Comprehensive guides to Linux server security and best practices.</description><pubDate>Mon, 21 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;As a cybersecurity enthusiast with a passion for Linux security, I&apos;ve created a series of guides and tutorials focused on enhancing the security of Linux servers.&lt;/p&gt;&lt;p&gt;This collection is designed to help you learn about important security concepts and practices in a logical order.&lt;/p&gt;&lt;p&gt;I encourage you to explore each guide to deepen your understanding and effectively secure your Linux server.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;🔹 Preparing Your Ubuntu Server for First Use&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Prepare your Ubuntu server for first use with essential setup steps, ensuring safe management and optimal performance from the start.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://ivansalloum.com/preparing-your-ubuntu-server-for-first-use/&quot;&gt; Learn more! &lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;🔹 Managing Users on Linux Server Securely&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Guide to securing your Linux server by managing users, controlling access, and configuring sudo safely.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://ivansalloum.com/managing-users-on-linux-server-securely/&quot;&gt; Learn more! &lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;🔹 Securing SSH: Essential Steps for Linux Servers&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Learn key steps to secure SSH on Linux servers, including important configurations and best practices for enhanced security.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://ivansalloum.com/securing-ssh-essential-steps-for-linux-servers/&quot;&gt; Learn more! &lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;🔹 Securing Your Linux Server with Fail2ban&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Guide to installing and configuring Fail2ban to protect your server from unauthorized access and brute force attacks.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://ivansalloum.com/securing-your-linux-server-with-fail2ban/&quot;&gt; Learn more! &lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;🔹 Setting Up a Firewall using UFW: An In-Depth Guide&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Guide to securing your server with UFW, covering setup, advanced rules, and best practices for effective firewall management.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://ivansalloum.com/setting-up-a-firewall-using-ufw-an-in-depth-guide/&quot;&gt; Learn more! &lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;🔹 How to Block Invalid Packets with UFW&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Learn how to enhance your server&apos;s security by blocking INVALID packets with UFW in this step-by-step tutorial.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://ivansalloum.com/how-to-block-invalid-packets-with-ufw/&quot;&gt; Learn more! &lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;🔹 Linux Server Security: Cloud Firewall Setup&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Easy-to-follow guide to enhancing your Linux server security by setting up a cloud firewall.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://ivansalloum.com/linux-server-security-cloud-firewall-setup/&quot;&gt; Learn more! &lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;🔹 Kernel Hardening: Securing Your Linux Server&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Guide to improving Linux server security with kernel hardening, protecting against network attacks and information leaks.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://ivansalloum.com/kernel-hardening-securing-your-linux-server/&quot;&gt; Learn more! &lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;🔹 How to Keep Users&apos; Processes Private&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Learn to stop users from seeing each other&apos;s processes on your Linux server.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://ivansalloum.com/how-to-keep-users-processes-private/&quot;&gt; Learn more! &lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;🔹 Automating Security Updates on Linux Servers&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Essential steps to manually update your Linux server and set up automatic security updates.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://ivansalloum.com/automating-security-updates-on-linux-servers/&quot;&gt; Learn more! &lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;🔹 Kernel Live Patching for High-Availability Linux Servers&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Discover how kernel live patching boosts security and uptime for high-availability Linux servers without the need for reboots.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://ivansalloum.com/kernel-live-patching-for-high-availability-linux-servers/&quot;&gt; Learn more! &lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;🔹 Why Linux Servers Need Reboots and How to Avoid Downtime&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Learn why Linux servers need reboots and how to safely reboot your production environment with minimal downtime.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://ivansalloum.com/why-linux-servers-need-reboots-and-how-to-avoid-downtime/&quot;&gt; Learn more! &lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;🔹 Preventing SYN Flood Attacks on Your Linux Server&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Learn how to protect your Linux server from SYN flood attacks with firewall rules, kernel tweaks, and Fail2ban.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://ivansalloum.com/preventing-syn-flood-attacks-on-your-linux-server/&quot;&gt; Learn more! &lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;🔹 How to Protect Linux Servers from Malware&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Comprehensive guide to protecting your Linux server from malware using the Maldet and ClamAV combo.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://ivansalloum.com/how-to-protect-linux-servers-from-malware/&quot;&gt; Learn more! &lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;🔹 How to Scan for Rootkits on a Linux Server&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Easy-to-follow tutorial for scanning rootkits on your Linux server using Rootkit Hunter.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://ivansalloum.com/how-to-scan-for-rootkits-on-a-linux-server/&quot;&gt; Learn more! &lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;strong&gt;🔹 Auditing Linux Servers with Auditd&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Explore how to use Auditd to monitor and audit activities on Linux servers for improved security and compliance.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://ivansalloum.com/auditing-linux-servers-with-auditd/&quot;&gt; Learn more! &lt;/a&gt;&lt;/p&gt;&lt;/article&gt;</content:encoded></item><item><title>Preparing Your Ubuntu Server for First Use</title><link>https://ivansalloum.com/preparing-your-ubuntu-server-for-first-use/</link><guid isPermaLink="true">https://ivansalloum.com/preparing-your-ubuntu-server-for-first-use/</guid><description>Prepare your Ubuntu server for first use with essential setup steps, ensuring safe management and optimal performance from the start.</description><pubDate>Mon, 21 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;Spinning up a new Ubuntu server is exciting – but also a bit intimidating. The moment it&apos;s online, it’s visible to the internet, and that means it&apos;s vulnerable.&lt;/p&gt;&lt;p&gt;I remember my first time: within hours, bots were already knocking at my SSH door.&lt;/p&gt;&lt;p&gt;That’s why properly preparing your server from day one is so important. It’s not just about installing software – it’s about setting the stage for security, stability, and performance.&lt;/p&gt;&lt;p&gt;✅&lt;/p&gt;&lt;p&gt;A few smart tweaks early on can help your server run faster, stay safer, and avoid problems down the line.&lt;/p&gt;&lt;p&gt;In this guide, I’ll walk you through the exact steps I take right after deploying a fresh Ubuntu instance. Whether you&apos;re setting up a web server, mail server, or anything else, these initial steps will help you.&lt;/p&gt;&lt;p&gt;I’m In!&lt;/p&gt;&lt;h2&gt;Document Everything (Seriously)&lt;/h2&gt;&lt;p&gt;You might not realize it now, but keeping track of what you do – from the tiniest config tweak to major installs – is one of the most valuable habits you can build as a server admin.&lt;/p&gt;&lt;p&gt;I learned this the hard way. The first time I messed with SSH configs and accidentally locked myself out, I couldn’t remember what I had changed – or why.&lt;/p&gt;&lt;p&gt;Ever since then, I document everything: the exact commands I run, what I was trying to do, and the results.&lt;/p&gt;&lt;p&gt;Even small changes matter. Things like:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Editing &lt;code&gt;sshd_config&lt;/code&gt;&lt;/li&gt;&lt;li&gt;Installing packages or services&lt;/li&gt;&lt;li&gt;Adjusting firewall rules&lt;/li&gt;&lt;li&gt;Creating users or groups&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I also note the &lt;strong&gt;date, time&lt;/strong&gt; , and a quick summary of my intention (e.g., &lt;em&gt;&amp;quot;disabled password login for root&amp;quot;&lt;/em&gt;).&lt;/p&gt;&lt;p&gt;You don’t need a fancy setup to start – just open a plain text file on your local machine and jot things down. Later, you can explore more structured tools.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Pro Tip:&lt;/strong&gt; Good documentation isn’t just for debugging – it’s also a huge win if you ever rebuild, automate, or hand off your setup later.&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;Logging In as Root&lt;/h2&gt;&lt;p&gt;The first step is getting into your server – and for that, you&apos;ll need two things:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The server’s &lt;strong&gt;IP address&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;The &lt;strong&gt;root password&lt;/strong&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Many providers allow you to set the root password during server creation on their dashboard or send it to you via email with the server details, as &lt;strong&gt;Hetzner&lt;/strong&gt; does.&lt;/p&gt;&lt;p&gt;Keep in mind that the root password provided by providers like Hetzner is often temporary. Once you access your server, you&apos;ll need to change it.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;New to Hetzner? &lt;a href=&quot;https://hetzner.cloud/?ref=MC4Yy318xX5X&quot;&gt;Use my link&lt;/a&gt; to get free credits!&lt;/p&gt;&lt;p&gt;Once you’ve got your credentials, fire up your terminal and run:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ssh root@your.server.ip
&lt;/pre&gt;&lt;p&gt;You’ll be prompted for the root password. Type it in, and you’re in!&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Run &lt;code&gt;passwd&lt;/code&gt; right away to set a new root password (or update any user’s password later). It’s a quick, essential first move.&lt;/p&gt;&lt;h2&gt;Running Updates&lt;/h2&gt;&lt;p&gt;Once you&apos;re logged in, your server might greet you with a message like:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;X packages can be updated. Y of these updates are security updates.
&lt;/pre&gt;&lt;p&gt;Don&apos;t ignore it.&lt;/p&gt;&lt;p&gt;Running outdated software is one of the easiest ways to expose your server to known vulnerabilities. So, before you do anything else, let’s bring your server up to date.&lt;/p&gt;&lt;p&gt;Begin by updating the package list on your server with the following command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;apt update
&lt;/pre&gt;&lt;p&gt;It doesn’t install anything yet – it just tells your server what’s new in the package repositories.&lt;/p&gt;&lt;p&gt;Now, install the updates:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;apt upgrade
&lt;/pre&gt;&lt;p&gt;You’ll be prompted to confirm – just type &lt;code&gt;yes&lt;/code&gt; and hit &lt;strong&gt;ENTER&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;These updates aren’t just about bug fixes – they include &lt;strong&gt;critical security patches, performance improvements&lt;/strong&gt; , and stability fixes that keep your server healthy from the start.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Personal Tip:&lt;/strong&gt; Some immediately reboot after updating, but I prefer waiting until the server setup is complete to ensure all changes apply.&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;Adding a Non-Root User&lt;/h2&gt;&lt;p&gt;The &lt;strong&gt;root&lt;/strong&gt; user is powerful – it has full control over your server and can run &lt;em&gt;any&lt;/em&gt; command without restriction.&lt;/p&gt;&lt;p&gt;But with great power comes great risk: a single typo or wrong command run as root can break your server or open security holes.&lt;/p&gt;&lt;p&gt;That’s why it’s best practice to create a &lt;strong&gt;non-root user&lt;/strong&gt; for your daily work. This user will have limited permissions by default and will require you to prefix admin commands with &lt;code&gt;sudo&lt;/code&gt; – which adds a protective layer by prompting for your password before critical actions.&lt;/p&gt;&lt;p&gt;Run this command, replacing &lt;code&gt;username&lt;/code&gt; with your chosen username:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;adduser username
&lt;/pre&gt;&lt;p&gt;You’ll be asked to create a password and fill in some optional details.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Use strong passwords! A password manager can help you generate and store complex passwords safely.&lt;/p&gt;&lt;p&gt;To allow your new user to run admin commands, add them to the sudo group:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;usermod -aG sudo username
&lt;/pre&gt;&lt;p&gt;To check that the new user is good to go, log out with the &lt;code&gt;logout&lt;/code&gt; command and access your server again with the new user:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ssh your.new.user@your.server.ip
&lt;/pre&gt;&lt;p&gt;Try running the &lt;code&gt;apt update&lt;/code&gt; command. It should not work directly, as you need to run the command like this: &lt;code&gt;sudo apt update&lt;/code&gt; and then enter your password.&lt;/p&gt;&lt;p&gt;✅&lt;/p&gt;&lt;p&gt;That’s it! You’re now set up to work safely on your server without the risks of using root directly.&lt;/p&gt;&lt;h2&gt;SSH Keys &amp;amp; Hardening&lt;/h2&gt;&lt;p&gt;By default, you log into your server with a password – but there’s a safer method: &lt;strong&gt;SSH key authentication&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Instead of typing a password each time, you use a &lt;strong&gt;key pair&lt;/strong&gt; :&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The &lt;strong&gt;public key&lt;/strong&gt; goes on the server.&lt;/li&gt;&lt;li&gt;The &lt;strong&gt;private key&lt;/strong&gt; stays safely on your local machine.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;When you try to connect, the server checks if you have the matching private key. If you do, you&apos;re in.&lt;/p&gt;&lt;p&gt;This is far more secure than using passwords, and it protects against brute-force login attempts.&lt;/p&gt;&lt;h3&gt;Check for Existing Keys&lt;/h3&gt;&lt;p&gt;First, check if you already have an SSH key pair on your local machine:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ls -l ~/.ssh
&lt;/pre&gt;&lt;p&gt;If the directory is empty (or only contains &lt;code&gt;known_hosts&lt;/code&gt;), you&apos;re good to go. If you see existing keys like &lt;code&gt;id_rsa&lt;/code&gt; or &lt;code&gt;id_ed25519&lt;/code&gt;, consider backing them up before continuing.&lt;/p&gt;&lt;h3&gt;Generate a New SSH Key Pair&lt;/h3&gt;&lt;p&gt;To create a secure key pair, run:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ssh-keygen -b 4096
&lt;/pre&gt;&lt;p&gt;You’ll be prompted to choose a file path for saving the key – press &lt;strong&gt;ENTER&lt;/strong&gt; to accept the default or enter a custom path to avoid overwriting any existing key.&lt;/p&gt;&lt;p&gt;You’ll also be asked to enter a passphrase. While optional, using a passphrase adds another layer of security and is strongly recommended.&lt;/p&gt;&lt;p&gt;Once done, two files will be created in your &lt;code&gt;~/.ssh&lt;/code&gt; directory:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;id_ed25519&lt;/code&gt; – your &lt;strong&gt;private key&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;&lt;code&gt;id_ed25519.pub&lt;/code&gt; – your &lt;strong&gt;public key&lt;/strong&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;❗&lt;/p&gt;&lt;p&gt;Keep the private key safe and never share it.&lt;/p&gt;&lt;h3&gt;Copy the Public Key to the Non-Root User&lt;/h3&gt;&lt;p&gt;If you copy the public key for the root user, only root will be able to log in with it. If you copy it for another user, only that user can use key-based login.&lt;/p&gt;&lt;p&gt;Since we’re using a non-root user, we’ll copy the key for that user instead.&lt;/p&gt;&lt;p&gt;To enable key-based access for your &lt;strong&gt;non-root user&lt;/strong&gt; , copy the public key to the server using:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ssh-copy-id -i ~/.ssh/id_ed25519.pub your.non.root.user@your.server.ip
&lt;/pre&gt;&lt;p&gt;Replace &lt;code&gt;your.non.root.user&lt;/code&gt; and &lt;code&gt;your.server.ip&lt;/code&gt; with your actual values. Enter the user’s password when prompted.&lt;/p&gt;&lt;p&gt;After this, you’ll be able to log in without a password – your key will handle the authentication.&lt;/p&gt;&lt;h3&gt;Disable Password and Root Logins&lt;/h3&gt;&lt;p&gt;Now that key-based authentication is working, let’s lock things down even further.&lt;/p&gt;&lt;p&gt;We’ll disable password logins entirely and prevent root from logging in over SSH.&lt;/p&gt;&lt;p&gt;Open the SSH configuration file:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo vim /etc/ssh/sshd_config
&lt;/pre&gt;&lt;p&gt;Find and update the following lines. If they’re commented out (start with &lt;code&gt;#&lt;/code&gt;), remove the &lt;code&gt;#&lt;/code&gt; character:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;PasswordAuthentication no PermitRootLogin no
&lt;/pre&gt;&lt;p&gt;Once you’ve saved and exited the file, reload the SSH service to apply the changes:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo systemctl reload ssh
&lt;/pre&gt;&lt;p&gt;⚠️&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Heads up:&lt;/strong&gt;  Some server providers include additional SSH configuration files in &lt;code&gt;/etc/ssh/sshd_config.d/&lt;/code&gt;. Be sure to check them so your settings aren’t overridden.&lt;/p&gt;&lt;h3&gt;Test Your Setup&lt;/h3&gt;&lt;p&gt;Try logging in as root:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ssh root@your.server.ip
&lt;/pre&gt;&lt;p&gt;You should see:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;Permission denied (publickey).
&lt;/pre&gt;&lt;p&gt;Also, try logging in as your non-root user from a machine that doesn’t have the private key – you’ll be blocked there too.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;At this point, your server:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Rejects password-based logins&lt;/li&gt;&lt;li&gt;Blocks root SSH access&lt;/li&gt;&lt;li&gt;Only allows access for your non-root user with SSH key authentication&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This is a major win for your server’s security.&lt;/p&gt;&lt;h2&gt;Changing Server Hostname&lt;/h2&gt;&lt;p&gt;Giving your server a meaningful hostname is like putting a name tag on it – it helps you recognize it at a glance and makes your terminal sessions less error-prone.&lt;/p&gt;&lt;p&gt;Imagine managing multiple servers and accidentally running a destructive command on the wrong one just because they all look the same in the prompt. A clear, unique hostname minimizes that risk.&lt;/p&gt;&lt;p&gt;But beyond convenience, setting the hostname properly is also important for functionality:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Some software relies on the server’s hostname to work correctly – especially services that depend on networking or domain resolution.&lt;/li&gt;&lt;li&gt;If a server&apos;s hostname can&apos;t be resolved to an IP address, it can cause communication and networking issues. This may result in timeouts, connection errors, and other unexpected behavior.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;To set the hostname, use this command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo hostnamectl set-hostname yourservername.yourdomain.com
&lt;/pre&gt;&lt;p&gt;Replace &lt;code&gt;yourservername.yourdomain.com&lt;/code&gt; with the actual name you want to use. A typical hostname includes two parts:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Server name&lt;/strong&gt; , like &lt;code&gt;web&lt;/code&gt; or &lt;code&gt;mail&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Domain name&lt;/strong&gt; , like &lt;code&gt;example.com&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;For instance:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo hostnamectl set-hostname mail.example.com
&lt;/pre&gt;&lt;p&gt;That’s all it takes to update the server’s hostname.&lt;/p&gt;&lt;p&gt;❗&lt;/p&gt;&lt;p&gt;Make sure to add an A record for your hostname.&lt;/p&gt;&lt;h2&gt;Setting Up a Firewall&lt;/h2&gt;&lt;p&gt;Before adding services or hosting applications, it’s important to lock down your server to prevent unauthorized access. A basic firewall configuration that only allows SSH traffic is a great starting point for a secure setup.&lt;/p&gt;&lt;p&gt;Ubuntu comes with &lt;a href=&quot;https://ivansalloum.com/setting-up-a-firewall-using-ufw-an-in-depth-guide/&quot;&gt;&lt;strong&gt;UFW&lt;/strong&gt;&lt;/a&gt; (Uncomplicated Firewall), a user-friendly tool for managing firewall rules. On many cloud providers (like Vultr), it’s already installed – and sometimes even pre-configured to allow SSH.&lt;/p&gt;&lt;p&gt;UFW is typically included in Debian-based distributions, like Ubuntu, but if needed, install it with:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt install ufw
&lt;/pre&gt;&lt;p&gt;To check if UFW is already active:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw status
&lt;/pre&gt;&lt;p&gt;If it’s inactive, continue to the next step. If it’s active and you want a clean slate, reset it:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw disable sudo ufw reset
&lt;/pre&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;Resetting clears existing rules, which is useful if you’re unsure what’s already in place.&lt;/p&gt;&lt;p&gt;Before enabling UFW, make sure SSH (port 22) is allowed – otherwise, you could lock yourself out:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw allow 22/tcp
&lt;/pre&gt;&lt;p&gt;Now that SSH access is allowed, turn on the firewall:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw enable
&lt;/pre&gt;&lt;p&gt;You’ll see a warning that the connection may be disrupted – but since SSH is allowed, you’re safe to continue.&lt;/p&gt;&lt;p&gt;✅&lt;/p&gt;&lt;p&gt;You now have a basic firewall in place.&lt;/p&gt;&lt;p&gt;Only SSH traffic is allowed in, and all other incoming connections are blocked by default. Outbound traffic remains unrestricted, so your server can still fetch updates and reach the outside world.&lt;/p&gt;&lt;p&gt;When you later install a web server or other services, you can open additional ports as needed:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw allow 80/tcp sudo ufw allow 443/tcp
&lt;/pre&gt;&lt;p&gt;These commands allow traffic on the standard ports used for websites – &lt;strong&gt;port 80&lt;/strong&gt; for unencrypted web traffic and &lt;strong&gt;port 443&lt;/strong&gt; for secure HTTPS connections. Without opening these, your website won’t be accessible from a browser.&lt;/p&gt;&lt;h2&gt;Changing the Timezone&lt;/h2&gt;&lt;p&gt;Setting the correct timezone for your server is important because it ensures that all timestamps and scheduled tasks reflect your local time (or the time zone relevant to your server’s purpose).&lt;/p&gt;&lt;p&gt;An incorrect timezone can lead to confusing logs, misfired cron jobs, or mismatched timestamps in applications. Getting it right from the beginning saves you from future debugging headaches.&lt;/p&gt;&lt;p&gt;To see the full list of available timezones, run:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;timedatectl list-timezones
&lt;/pre&gt;&lt;p&gt;This will display all supported timezones (you can scroll or pipe it through &lt;code&gt;less&lt;/code&gt; for easier browsing).&lt;/p&gt;&lt;p&gt;Once you’ve found the correct timezone for your location (e.g. &lt;code&gt;Europe/Berlin&lt;/code&gt;, &lt;code&gt;America/New_York&lt;/code&gt;, &lt;code&gt;Asia/Tokyo&lt;/code&gt;), apply it using:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo timedatectl set-timezone Your/Timezone
&lt;/pre&gt;&lt;p&gt;For example:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo timedatectl set-timezone Europe/Berlin
&lt;/pre&gt;&lt;p&gt;Your server’s clock is now aligned with the correct timezone.&lt;/p&gt;&lt;h2&gt;Changing the Default Editor&lt;/h2&gt;&lt;p&gt;Your server uses a default text editor for system-level tasks like editing crontabs (&lt;code&gt;crontab -e&lt;/code&gt;) or secure files with &lt;code&gt;visudo&lt;/code&gt;. By default, this editor is often &lt;strong&gt;Nano&lt;/strong&gt; – a simple, beginner-friendly choice.&lt;/p&gt;&lt;p&gt;But if you prefer something more powerful, like &lt;strong&gt;Vim&lt;/strong&gt; , you can easily switch it.&lt;/p&gt;&lt;p&gt;Run the following command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo update-alternatives --config editor
&lt;/pre&gt;&lt;p&gt;You’ll see a list of installed editors:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;There are 4 choices for the alternative editor (providing /usr/bin/editor). Selection Path Priority Status \------------------------------------------------------------ * 0 /bin/nano 40 auto mode 1 /bin/ed -100 manual mode 2 /bin/nano 40 manual mode 3 /usr/bin/vim.basic 30 manual mode 4 /usr/bin/vim.tiny 15 manual mode Press to keep the current choice[*], or type selection number:
&lt;/pre&gt;&lt;p&gt;Enter the number next to your preferred editor and press &lt;strong&gt;ENTER&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;🙆‍♂️&lt;/p&gt;&lt;p&gt;I personally use &lt;strong&gt;Vim&lt;/strong&gt; because of its speed and power – but go with whatever makes you comfortable!&lt;/p&gt;&lt;h2&gt;Installing Essential Software&lt;/h2&gt;&lt;p&gt;Once your server is updated and secure, it’s a good idea to install some essential tools that make daily tasks easier and more efficient.&lt;/p&gt;&lt;p&gt;Depending on what your server is for (web hosting, backups, development, etc.), you might need different tools – but some are useful in almost every setup.&lt;/p&gt;&lt;p&gt;Here’s a go-to list I personally install on all my servers:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt install git htop curl wget zip unzip net-tools ncdu rsync
&lt;/pre&gt;&lt;p&gt;Let’s quickly break down what these do:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;git&lt;/code&gt; – Version control for managing code and config files&lt;/li&gt;&lt;li&gt;&lt;code&gt;htop&lt;/code&gt; – An interactive process viewer (better than &lt;code&gt;top&lt;/code&gt;)&lt;/li&gt;&lt;li&gt;&lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;wget&lt;/code&gt; – For downloading files and testing web endpoints&lt;/li&gt;&lt;li&gt;&lt;code&gt;zip&lt;/code&gt;, &lt;code&gt;unzip&lt;/code&gt; – For compressing and extracting archives&lt;/li&gt;&lt;li&gt;&lt;code&gt;net-tools&lt;/code&gt; – Includes legacy tools like &lt;code&gt;ifconfig&lt;/code&gt; and &lt;code&gt;netstat&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;code&gt;ncdu&lt;/code&gt; – Disk usage analyzer (great for cleaning up space)&lt;/li&gt;&lt;li&gt;&lt;code&gt;rsync&lt;/code&gt; – Fast, efficient file transfer and sync utility&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;These are lightweight but powerful tools that make server life a lot smoother. Feel free to expand this list with tools specific to your use case!&lt;/p&gt;&lt;h2&gt;Configuring Swap Space&lt;/h2&gt;&lt;p&gt;If your server runs out of RAM, it can crash processes or become unresponsive. One way to prevent this is by adding &lt;strong&gt;swap space&lt;/strong&gt; – a portion of disk that acts as backup memory.&lt;/p&gt;&lt;p&gt;The caveat is that it is slower than RAM since data is written to the disk. However, it provides valuable safety against out-of-memory situations.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;&lt;strong&gt;Personal Tip:&lt;/strong&gt; I usually configure swap space on servers with SSD or NVMe storage. On older machines, adding more RAM is often the better choice.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;First, check if your server already has existing swap space by running:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo swapon --show
&lt;/pre&gt;&lt;p&gt;If you receive no output, it means that swap space has not been added. You can confirm this by using the &lt;code&gt;free -h&lt;/code&gt; command.&lt;/p&gt;&lt;p&gt;A common rule of thumb is to allocate swap space equal to your RAM, or double it if you have disk space to spare.&lt;/p&gt;&lt;p&gt;Here&apos;s how to create a 1 GB swap file:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo fallocate -l 1G /swapfile
&lt;/pre&gt;&lt;p&gt;Swap files must only be accessible to root. Set the correct permissions:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo chmod 600 /swapfile
&lt;/pre&gt;&lt;p&gt;Next, mark the file for swap usage:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo mkswap /swapfile
&lt;/pre&gt;&lt;p&gt;Then enable it:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo swapon /swapfile
&lt;/pre&gt;&lt;p&gt;Use the &lt;code&gt;free -h&lt;/code&gt; command again to check that the swap space is now active.&lt;/p&gt;&lt;p&gt;To keep the swap space active after reboot, add it to the &lt;code&gt;/etc/fstab&lt;/code&gt; file:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;echo &apos;/swapfile none swap sw 0 0&apos; | sudo tee -a /etc/fstab
&lt;/pre&gt;&lt;p&gt;Your server now has a safety net when memory runs low.&lt;/p&gt;&lt;h2&gt;Configuring SMTP Relay&lt;/h2&gt;&lt;p&gt;To ensure your server can send important notifications – such as security alerts or upgrade failures – it’s crucial to install &lt;strong&gt;Postfix&lt;/strong&gt; and configure it to use an external SMTP relay.&lt;/p&gt;&lt;p&gt;By relaying email through a trusted SMTP provider, you avoid common pitfalls like blocked ports or blacklisted IPs that can prevent your server from delivering mail reliably.&lt;/p&gt;&lt;p&gt;These notifications are especially valuable for things like:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Failed auto-updates (e.g. from &lt;a href=&quot;https://ivansalloum.com/automating-security-updates-on-linux-servers/&quot;&gt;&lt;code&gt;unattended-upgrades&lt;/code&gt;&lt;/a&gt;)&lt;/li&gt;&lt;li&gt;Cron job errors&lt;/li&gt;&lt;li&gt;System alerts&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Without a working mail configuration, you might miss out on critical issues until it&apos;s too late.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;I’ve written a dedicated guide on &lt;a href=&quot;https://ivansalloum.com/how-to-configure-postfix-for-external-smtp-relay/&quot;&gt;configuring Postfix with SMTP2GO&lt;/a&gt;, a reliable free SMTP relay that works great with most server setups.&lt;/p&gt;&lt;p&gt;If you&apos;re looking for full control over email delivery and want to manage your own SMTP infrastructure, I also have a detailed guide on &lt;a href=&quot;https://ivansalloum.com/setting-up-postal-as-an-smtp-server/&quot;&gt;building your own SMTP server with Postal&lt;/a&gt;. You can use Postal as a &lt;strong&gt;smart host&lt;/strong&gt; , letting other servers send email through it securely.&lt;/p&gt;&lt;p&gt;Choose what fits your setup – whether it’s a third-party SMTP provider or a self-hosted solution – but don’t skip this step.&lt;/p&gt;&lt;p&gt;❗&lt;/p&gt;&lt;p&gt;A silent server is a dangerous one.&lt;/p&gt;&lt;h2&gt;Applying Changes&lt;/h2&gt;&lt;p&gt;Once you&apos;ve gone through all the setup steps – including updates, user configuration, SSH hardening, firewall rules, swap space, and more – it’s a good idea to reboot your server.&lt;/p&gt;&lt;p&gt;This ensures that all changes are properly applied and any pending updates or configurations take full effect.&lt;/p&gt;&lt;p&gt;Run this command to reboot your server:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo reboot
&lt;/pre&gt;&lt;p&gt;After a few moments, your server will come back online with all configurations in place.&lt;/p&gt;&lt;p&gt;✅&lt;/p&gt;&lt;p&gt;You&apos;re now ready to move forward with whatever you’ve planned for your new server.&lt;/p&gt;&lt;h2&gt;Planning to Use Docker?&lt;/h2&gt;&lt;p&gt;If you&apos;re anything like me, Docker is probably one of the first tools you’ll want to install next. I use Docker on almost every server I manage, so I’ve made it a default part of my preparation routine.&lt;/p&gt;&lt;p&gt;Docker lets you run applications inside containers – lightweight, portable, and consistent environments that behave the same across development and production.&lt;/p&gt;&lt;p&gt;To get it installed properly, I recommend using Docker’s official repository rather than the default Ubuntu packages – that way, you get the latest stable releases and important security updates.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;I’ve written a dedicated guide that walks you through the entire process: &lt;a href=&quot;https://ivansalloum.com/installing-docker-and-docker-compose-on-ubuntu/&quot;&gt;Installing Docker and Docker Compose on Ubuntu&lt;/a&gt;&lt;/p&gt;&lt;p&gt;If Docker is part of your workflow – as it is in mine – this guide will help you set it up the right way.&lt;/p&gt;&lt;h2&gt;Using the SSH Config File&lt;/h2&gt;&lt;p&gt;When managing multiple servers, typing long SSH commands can get repetitive. That’s where the SSH config file comes in – it acts like an address book for your servers, making SSH connections easier and more organized.&lt;/p&gt;&lt;p&gt;Instead of typing the full SSH command every time, you can define shortcuts for each server.&lt;/p&gt;&lt;p&gt;Start by creating the config file on your &lt;strong&gt;local machine&lt;/strong&gt; (not on the server):&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;touch ~/.ssh/config
&lt;/pre&gt;&lt;p&gt;Now, open the file with your preferred editor and add a block like this:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;Host server1 HostName 81.41.156.93 User ivan Port 22 IdentityFile ~/.ssh/id_rsa
&lt;/pre&gt;&lt;p&gt;In this example:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;server1&lt;/code&gt; is a nickname for the server.&lt;/li&gt;&lt;li&gt;&lt;code&gt;HostName&lt;/code&gt; is the server’s IP address.&lt;/li&gt;&lt;li&gt;&lt;code&gt;User&lt;/code&gt; is the user you use to log in (in this case, &lt;code&gt;ivan&lt;/code&gt;).&lt;/li&gt;&lt;li&gt;&lt;code&gt;Port&lt;/code&gt; is the SSH port (22 is default, so you can skip it unless it’s custom).&lt;/li&gt;&lt;li&gt;&lt;code&gt;IdentityFile&lt;/code&gt; is the path to your private SSH key.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Without a config file, connecting looks like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ssh -p 22 -i ~/.ssh/id_rsa ivan@81.41.156.93
&lt;/pre&gt;&lt;p&gt;But with the config file set up, all you need is:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ssh server1
&lt;/pre&gt;&lt;p&gt;Simple, right?&lt;/p&gt;&lt;p&gt;Now, update the values to match your own server details. If you haven’t changed the SSH port, you can skip the &lt;code&gt;Port&lt;/code&gt; line entirely.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Managing multiple servers? Just add a new &lt;code&gt;Host&lt;/code&gt; block for each one inside the same config file.&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;🎉 Congratulations – you’ve reached the end of this guide!&lt;/p&gt;&lt;p&gt;By now, you’ve successfully prepared your Ubuntu server for its first use and laid a solid foundation for managing it safely and efficiently.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;It’s time to take the security of your server up a notch. You can find my full collection of detailed Linux server security guides &lt;a href=&quot;https://ivansalloum.com/collections/linux-server-security/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;💬  Found this guide helpful?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;I’d love to hear your thoughts, questions, or suggestions in the discussion section below. Your feedback helps improve future guides and supports others on their server journey.&lt;/p&gt;&lt;p&gt;Prefer a more direct conversation? Feel free to &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;contact me&lt;/a&gt; anytime.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Servers</category><category>Security</category></item><item><title>How to Install Releem on Enhance Control Panel</title><link>https://ivansalloum.com/how-to-install-releem-on-enhance-control-panel/</link><guid isPermaLink="true">https://ivansalloum.com/how-to-install-releem-on-enhance-control-panel/</guid><description>Learn how to easily install Releem on the Enhance Control Panel with this step-by-step tutorial.</description><pubDate>Fri, 31 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;Releem is a performance tuning tool for MySQL/MariaDB that recommends changes to your database configuration based on your current workload.&lt;/p&gt;&lt;p&gt;It collects metrics and suggests adjustments accordingly.&lt;/p&gt;&lt;p&gt;Releem offers both a free plan and a premium plan.&lt;/p&gt;&lt;p&gt;The free plan covers 10 essential MySQL/MariaDB variables and allows you to monitor the health status of the system, the InnoDB engine, memory, and queries through their dashboard.&lt;/p&gt;&lt;p&gt;You can also track system metrics and MySQL/MariaDB metrics such as latency, queries per second, slow queries, and IOPS r/w.&lt;/p&gt;&lt;p&gt;This tutorial will show you how to install Releem on the &lt;a href=&quot;https://enhance.com/&quot;&gt;Enhance&lt;/a&gt; control panel.&lt;/p&gt;&lt;h2&gt;Preparation&lt;/h2&gt;&lt;p&gt;First, &lt;a href=&quot;https://releem.com/&quot;&gt;sign up&lt;/a&gt; for a Releem account.&lt;/p&gt;&lt;p&gt;After creating your account, click the &lt;strong&gt;Add new server&lt;/strong&gt;  button and choose the &lt;strong&gt;MySQL on Linux Server: Manual Installation in Docker&lt;/strong&gt;  option.&lt;/p&gt;&lt;p&gt;Next, you need to add a read-only user for Releem to collect metrics.&lt;/p&gt;&lt;p&gt;Access the server where the database role is installed using Enhance as root.&lt;/p&gt;&lt;p&gt;If you are using a sudo user, switch to the root user and run the &lt;code&gt;mysql&lt;/code&gt; command to enter the MySQL CLI.&lt;/p&gt;&lt;p&gt;Run the following commands:&lt;/p&gt;&lt;pre data-language=&quot;sql&quot;&gt;CREATE USER &apos;releem&apos;@&apos;%&apos; identified by &apos;[Password]&apos;; GRANT PROCESS, REPLICATION CLIENT, SHOW VIEW ON *.* TO &apos;releem&apos;@&apos;%&apos;; GRANT SELECT ON performance_schema.events_statements_summary_by_digest TO &apos;releem&apos;@&apos;%&apos;; FLUSH PRIVILEGES;
&lt;/pre&gt;&lt;p&gt;Replace &lt;code&gt;[Password]&lt;/code&gt; in the first command with a strong, complex password.&lt;/p&gt;&lt;p&gt;These commands create a new user named “releem” with the specified password, grant the necessary privileges, and reload the grant tables to apply the changes immediately.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;The &lt;code&gt;%&lt;/code&gt; in the previous commands acts as a wildcard, granting the “releem” user access from any host. This is important because Docker may change IP addresses. By providing global access, we prevent any issues that could arise from these IP changes.&lt;/p&gt;&lt;p&gt;Exit the MySQL CLI by running the &lt;code&gt;exit&lt;/code&gt; command.&lt;/p&gt;&lt;p&gt;Before proceeding with the installation, we need to enable the performance schema and the slow query log.&lt;/p&gt;&lt;p&gt;To do this, access your Enhance panel, go to the &lt;strong&gt;Servers&lt;/strong&gt;  tab, and choose the server with the database role.&lt;/p&gt;&lt;p&gt;Under the &lt;strong&gt;Roles&lt;/strong&gt;  section, select the &lt;strong&gt;Database&lt;/strong&gt;  role and click the &lt;strong&gt;my.cnf&lt;/strong&gt;  button.&lt;/p&gt;&lt;p&gt;Then click the &lt;strong&gt;Edit configuration&lt;/strong&gt;  button and add the following lines to the end of the file:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;performance_schema=1 slow_query_log=1
&lt;/pre&gt;&lt;p&gt;Save the changes. Enhance will prompt you to restart the MySQL container; please do so.&lt;/p&gt;&lt;p&gt;Now you are ready to proceed with the installation.&lt;/p&gt;&lt;h2&gt;Installation&lt;/h2&gt;&lt;p&gt;Now, we can install the Releem agent on the server where the database role is installed.&lt;/p&gt;&lt;p&gt;After selecting the &lt;strong&gt;MySQL on Linux Server: Manual Installation in Docker&lt;/strong&gt;  option, you’ll see an example Docker command provided by Releem for installing the agent.&lt;/p&gt;&lt;p&gt;However, this command won’t work directly with Enhance; we need to make some modifications.&lt;/p&gt;&lt;p&gt;From the provided Docker command, you need to extract your API key and the agent version.&lt;/p&gt;&lt;p&gt;The API key is found in the &lt;code&gt;RELEEM_API_KEY&lt;/code&gt; variable, and the agent version is at the end of the command. As of the time of writing this tutorial, the latest agent version is 1.16.0.&lt;/p&gt;&lt;p&gt;Now, access the server where the database role is installed using Enhance as root. If you are using a sudo user, switch to the root user.&lt;/p&gt;&lt;p&gt;We need to use the following command to install the Releem agent:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;docker run -d -ti --restart unless-stopped --network=enhance-network --name &apos;releem-agent&apos; -e DB_HOST=&amp;quot;[server_public_ip]&amp;quot; -e DB_PORT=&amp;quot;3306&amp;quot; -e DB_PASSWORD=&amp;quot;[password]&amp;quot; -e DB_USER=&amp;quot;releem&amp;quot; -e RELEEM_API_KEY=&amp;quot;[your_api_key]&amp;quot; -e MEMORY_LIMIT=1000 -e RELEEM_HOSTNAME=&amp;quot;[server_hostname]&amp;quot; -v /etc/mysql/releem.conf.d:/etc/mysql/releem.conf.d -v /opt/releem/conf:/opt/releem/conf releem/releem-agent:1.16.0
&lt;/pre&gt;&lt;p&gt;Replace &lt;code&gt;[server_public_ip]&lt;/code&gt; with your server’s public IP address, &lt;code&gt;[password]&lt;/code&gt; with the password you set for the “releem” user, and &lt;code&gt;[your_api_key]&lt;/code&gt; with your API key.&lt;/p&gt;&lt;p&gt;Set the &lt;code&gt;MEMORY_LIMIT&lt;/code&gt; to the amount of memory you want to allocate for the database in megabytes. This means that Releem will tune the database to use this amount of memory instead of the total RAM.&lt;/p&gt;&lt;p&gt;If you are running both the App and Database roles on the same server, it is not recommended to allocate the total RAM to the database.&lt;/p&gt;&lt;p&gt;A good rule of thumb is to allocate around 60% of the available RAM.&lt;/p&gt;&lt;p&gt;Replace &lt;code&gt;[server_hostname]&lt;/code&gt; with your server’s hostname.&lt;/p&gt;&lt;p&gt;Finally, ensure the agent version at the end of the command matches the current version.&lt;/p&gt;&lt;p&gt;Now run the command.&lt;/p&gt;&lt;p&gt;After the command finishes, you should receive an email from Releem confirming that the server is now connected.&lt;/p&gt;&lt;p&gt;❗&lt;/p&gt;&lt;p&gt;Although Releem can apply changes automatically if desired, you should manually apply the recommended configurations due to the nature of Enhance.&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;I hope this tutorial has been helpful and that everything worked smoothly for you!&lt;/p&gt;&lt;p&gt;In this tutorial, you’ve learned how install Releem on Enhance control panel.&lt;/p&gt;&lt;p&gt;If you found value in this tutorial or have any questions or feedback, please don&apos;t hesitate to share your thoughts in the discussion section.&lt;/p&gt;&lt;p&gt;Your input is greatly appreciated, and you can also &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;contact me&lt;/a&gt; directly if you prefer.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Tools</category></item><item><title>Linux Server Security: Cloud Firewall Setup</title><link>https://ivansalloum.com/linux-server-security-cloud-firewall-setup/</link><guid isPermaLink="true">https://ivansalloum.com/linux-server-security-cloud-firewall-setup/</guid><description>Learn how to enhance your Linux server security with this easy-to-follow guide on setting up a cloud firewall.</description><pubDate>Thu, 21 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;Most server providers offer the option to set up a cloud firewall for your servers, allowing you to create a more advanced firewall setup.&lt;/p&gt;&lt;p&gt;With a cloud firewall, you’ll have both an OS-level firewall and a cloud-level firewall, effectively employing two firewalls working together.&lt;/p&gt;&lt;p&gt;This guide will discuss the benefits of using a cloud firewall and how to set one up, along with providing some tips about best practices.&lt;/p&gt;&lt;h2&gt;Benefits&lt;/h2&gt;&lt;p&gt;An advantage of having a configured and enabled cloud firewall is that if you accidentally add a rule that might lock you out from your server, you won’t be stranded.&lt;/p&gt;&lt;p&gt;The cloud firewall is accessible through the provider’s dashboard, allowing you to make changes as needed.&lt;/p&gt;&lt;p&gt;In my guide about &lt;a href=&quot;https://ivansalloum.com/setting-up-a-firewall-using-ufw-an-in-depth-guide/&quot;&gt;setting up a firewall using UFW&lt;/a&gt;, I discussed the challenge of restricting the SSH port to our home network IP, which can change due to DHCP.&lt;/p&gt;&lt;p&gt;That’s why I prefer using the cloud firewall for rules that might change in the future.&lt;/p&gt;&lt;p&gt;Here’s my approach: I configure the UFW firewall with no specific restrictions. For example, for SSH, I allow SSH traffic from all sources on the UFW firewall. However, on the cloud firewall, I specifically restrict SSH to my IP. This way, whenever my IP changes, I can access the provider’s dashboard, update the IP, and regain access to the server.&lt;/p&gt;&lt;p&gt;Another significant advantage is the ability to link the cloud firewall with multiple servers.&lt;/p&gt;&lt;p&gt;This means that configuring and enabling the cloud firewall simultaneously applies to multiple servers, providing more convenience than changing the same rules individually.&lt;/p&gt;&lt;p&gt;You would make the change once and push it to all linked servers.&lt;/p&gt;&lt;p&gt;Returning to my approach, if my IP changes, and I want to update it in the firewall to regain access, I do it once, and I regain access to all servers connected to the same firewall.&lt;/p&gt;&lt;p&gt;Now, let’s discuss the default policy of cloud firewalls.&lt;/p&gt;&lt;h2&gt;Default Policy&lt;/h2&gt;&lt;p&gt;Most cloud firewalls have a default policy of blocking all incoming traffic and allowing all outgoing traffic, similar to the UFW firewall.&lt;/p&gt;&lt;p&gt;Hetzner follows the same policy, and it is the server provider I use for all my projects.&lt;/p&gt;&lt;p&gt;If you are also using Hetzner, you are good to go.&lt;/p&gt;&lt;p&gt;If not, make sure that the policy aligns with your OS-level firewall.&lt;/p&gt;&lt;p&gt;If the default policy is different and you are unable to change it, please contact support or refer to the documentation of your provider for further information.&lt;/p&gt;&lt;p&gt;However, it’s worth noting that almost all server providers follow the same policy.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;New to Hetzner? &lt;a href=&quot;https://hetzner.cloud/?ref=MC4Yy318xX5X&quot;&gt;Use my link&lt;/a&gt; to get free credits!&lt;/p&gt;&lt;h2&gt;Configuration &amp;amp; Activation&lt;/h2&gt;&lt;p&gt;I will guide you through configuring and activating the cloud firewall with Hetzner.&lt;/p&gt;&lt;p&gt;If you are using another provider, the process should be similar.&lt;/p&gt;&lt;p&gt;Inside your project, go to the &lt;strong&gt;Firewalls&lt;/strong&gt;  tab, and click on the &lt;strong&gt;Create Firewall&lt;/strong&gt;  button. A new page will open where you can configure your firewall.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://ivansalloum.com/content/images/2024/10/hetzner-firewall-configuration.webp&quot; alt=&quot;Hetzner&apos;s Firewall Configuration&quot;&gt;&lt;/p&gt;&lt;p&gt;Add a description for each rule you add to ensure easy understanding of your configurations.&lt;/p&gt;&lt;p&gt;For the first rule, allowing traffic to port 22 (SSH), remove &lt;strong&gt;Any IPv4&lt;/strong&gt;  and &lt;strong&gt;Any IPv6&lt;/strong&gt; , and add only your current IP.&lt;/p&gt;&lt;p&gt;If your IP changes, you can always access the firewall from your dashboard and update it to the new one without any issues.&lt;/p&gt;&lt;p&gt;Now, for the second rule, which is allowing traffic to ICMP, it’s worth noting that many network administrators consider ICMP a security risk and opt to block it at the firewall.&lt;/p&gt;&lt;p&gt;While ICMP does have some security issues, it also serves important functions.&lt;/p&gt;&lt;p&gt;Some features are useful for troubleshooting, while others are essential for certain software to function correctly.&lt;/p&gt;&lt;p&gt;If you have configured a firewall using UFW on the OS level, it’s important to note that UFW has the ICMP protocol open by default. I prefer leaving it this way while controlling the ICMP protocol from the cloud firewall.&lt;/p&gt;&lt;p&gt;I have never experienced any issues denying traffic to ICMP, but your experience may vary. I recommend trying to deny traffic first. If a problem occurs, allow traffic again to ICMP.&lt;/p&gt;&lt;p&gt;If you want to deny traffic to ICMP, simply remove the second rule. If you want to allow traffic again, add the rule back.&lt;/p&gt;&lt;p&gt;Now, add the remaining rules according to your preferences, ensuring they align with your OS-level firewall ruleset to avoid conflicts.&lt;/p&gt;&lt;p&gt;I always leave the &lt;strong&gt;Outbound rules&lt;/strong&gt;  empty as there is nothing to add.&lt;/p&gt;&lt;p&gt;Once you’ve completed adding the rules, scroll down to the &lt;strong&gt;Apply to&lt;/strong&gt;  section, and select the servers to which you want this firewall to apply.&lt;/p&gt;&lt;p&gt;Scroll to the end, add a name to the firewall, and click on the &lt;strong&gt;Create Firewall&lt;/strong&gt;  button.&lt;/p&gt;&lt;p&gt;Now, you have two firewalls enabled and working together.&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;Great job reaching the end!&lt;/p&gt;&lt;p&gt;In this guide, you’ve learned how to set up a cloud firewall.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;You can find the full collection of detailed Linux server security guides &lt;a href=&quot;https://ivansalloum.com/collections/linux-server-security/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;If you found value in this guide or have any questions or feedback, please don&apos;t hesitate to share your thoughts in the &lt;strong&gt;discussion&lt;/strong&gt; section.&lt;/p&gt;&lt;p&gt;Your input is greatly appreciated, and you can also &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;contact me&lt;/a&gt; directly if you prefer.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Security</category></item><item><title>Setting Up a Firewall using UFW: An In-Depth Guide</title><link>https://ivansalloum.com/setting-up-a-firewall-using-ufw-an-in-depth-guide/</link><guid isPermaLink="true">https://ivansalloum.com/setting-up-a-firewall-using-ufw-an-in-depth-guide/</guid><description>Learn how to secure your server by setting up a firewall using UFW along with some best practices for effective firewall management.</description><pubDate>Wed, 20 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;Setting up a firewall is essential for securing your server, and UFW makes this process straightforward and user-friendly.&lt;/p&gt;&lt;p&gt;Think of it as your server&apos;s bouncer – it filters network traffic and only allows authorized access, keeping unwanted traffic out.&lt;/p&gt;&lt;p&gt;In this guide, I’ll walk you through setting up a firewall with UFW, as well as introduce you to advanced firewall rules that allow you to take your server&apos;s security to the next level.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;_I assume you&apos;re working on a properly set-up Ubuntu server. If not, check out my guide  on &lt;em&gt;&lt;a href=&quot;https://ivansalloum.com/preparing-your-ubuntu-server-for-first-use/&quot;&gt;&lt;em&gt;preparing  Ubuntu servers&lt;/em&gt;&lt;/a&gt; _  to get started.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;Author&apos;s Note&lt;/h2&gt;&lt;p&gt;Before we dive into the details, I’d like to highlight a few points.&lt;/p&gt;&lt;p&gt;Most guides online only cover the basics of UFW and how to add simple user-defined rules from the command line. It&apos;s hard to find a guide that goes beyond that.&lt;/p&gt;&lt;p&gt;This guide will take you beyond the basics and provide an example of how to implement advanced firewall rules. I&apos;ll explain some of the configuration and rule files, and where certain settings come from.&lt;/p&gt;&lt;p&gt;Understanding how UFW works, how to manage rules, and the order in which they take effect is essential, and I’ll do my best to make that clear in this guide.&lt;/p&gt;&lt;p&gt;If there’s anything you&apos;d like me to cover, feel free to &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;contact me&lt;/a&gt;, and I’ll either update this guide or publish a new one with the information you need, or help you if you encounter any issues.&lt;/p&gt;&lt;p&gt;I’m also in the process of writing a dedicated guide on Linux firewalls, focusing on the inner workings of UFW and how to develop advanced solutions with it.&lt;/p&gt;&lt;p&gt;On Debian-based distributions, like Ubuntu, UFW often comes pre-packaged.&lt;/p&gt;&lt;p&gt;You can check and install it using this command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt install ufw
&lt;/pre&gt;&lt;p&gt;Many server providers configure the UFW firewall upon deploying the server to allow only SSH connections, enabling you to connect to the server.&lt;/p&gt;&lt;p&gt;If you have a server from Vultr, UFW is likely to be enabled by default. In my case with Hetzner, UFW is not enabled.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;New to Hetzner? &lt;a href=&quot;https://hetzner.cloud/?ref=MC4Yy318xX5X&quot;&gt;Use my link&lt;/a&gt; to get free credits!&lt;/p&gt;&lt;p&gt;You can check the status of UFW and your current ruleset using this command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw status
&lt;/pre&gt;&lt;p&gt;The command’s output will either indicate that UFW is inactive, or that it is active with your current rule set.&lt;/p&gt;&lt;p&gt;If UFW is currently inactive, that’s fine, as we’ll proceed to configure it properly and enable it.&lt;/p&gt;&lt;p&gt;However, if UFW is already active, disable and reset it using the following commands:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw disable sudo ufw reset
&lt;/pre&gt;&lt;p&gt;You can re-enable it once you have added all the rules and finished configuring it.&lt;/p&gt;&lt;h2&gt;UFW’s Default Policy&lt;/h2&gt;&lt;p&gt;By default, UFW takes a secure approach by blocking all incoming traffic while allowing outgoing traffic from our server. This means our server can communicate externally, but it remains inaccessible to others.&lt;/p&gt;&lt;p&gt;Since there is no issue with our server reaching the outside world, there is no need to make any changes to that aspect.&lt;/p&gt;&lt;p&gt;However, to enable incoming traffic, it’s essential to selectively open only the required ports and authorize traffic through them.&lt;/p&gt;&lt;p&gt;You can find the default policy defined in the &lt;code&gt;/etc/default/ufw&lt;/code&gt; file:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;DEFAULT_INPUT_POLICY=&amp;quot;DROP&amp;quot; DEFAULT_OUTPUT_POLICY=&amp;quot;ACCEPT&amp;quot;
&lt;/pre&gt;&lt;p&gt;As you can see, the default policy for incoming traffic is set to &lt;code&gt;DROP&lt;/code&gt;, while the default policy for outgoing traffic is set to &lt;code&gt;ACCEPT&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;If UFW is enabled, you can also review the default policy using the following command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw status verbose
&lt;/pre&gt;&lt;p&gt;You can modify this default behavior of UFW either by directly editing the file or by using these two commands:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw default incoming sudo ufw default outgoing
&lt;/pre&gt;&lt;p&gt;Replace &lt;code&gt;&amp;lt;policy&amp;gt;&lt;/code&gt; with either &lt;code&gt;deny&lt;/code&gt;, &lt;code&gt;allow&lt;/code&gt; or &lt;code&gt;reject&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;&lt;code&gt;deny&lt;/code&gt; corresponds to DROP, &lt;code&gt;allow&lt;/code&gt; corresponds to ACCEPT, and &lt;code&gt;reject&lt;/code&gt; corresponds to REJECT.&lt;/p&gt;&lt;p&gt;Both DROP and REJECT policies prevent traffic from passing through the firewall, but they differ in their response messages.&lt;/p&gt;&lt;p&gt;With DROP, the traffic is silently discarded without any acknowledgment sent to the source. It neither forwards the packet nor responds to it.&lt;/p&gt;&lt;p&gt;On the other hand, REJECT sends an error message back to the source, signaling a connection failure.&lt;/p&gt;&lt;h2&gt;UFW&apos;s Configuration Files&lt;/h2&gt;&lt;p&gt;There are three files I’d like you to review.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;While you may not need to modify them when configuring UFW and adding your rules, it&apos;s helpful to be familiar with them.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;You may have already opened the &lt;code&gt;/etc/default/ufw&lt;/code&gt; file and examined the default policy of UFW, but there are other settings you might need to know about.&lt;/p&gt;&lt;p&gt;For example, you can disable IPv6 completely by changing the &lt;code&gt;IPV6&lt;/code&gt; variable from &lt;code&gt;yes&lt;/code&gt; to &lt;code&gt;no&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Additionally, you have the option to modify the default forward policy and the default application policy.&lt;/p&gt;&lt;p&gt;You can also enable UFW to manage the built-in chains by setting the &lt;code&gt;MANAGE_BUILTINS&lt;/code&gt; variable to &lt;code&gt;yes&lt;/code&gt;. Built-in chains are the default chains provided by Iptables, such as &lt;code&gt;INPUT&lt;/code&gt;, &lt;code&gt;OUTPUT&lt;/code&gt;, and &lt;code&gt;FORWARD&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;UFW creates its own chains to manage its rules, such as &lt;code&gt;ufw-user-input&lt;/code&gt; and &lt;code&gt;ufw-user-output&lt;/code&gt;. These UFW chains are linked to the built-in chains to apply UFW’s firewall rules.&lt;/p&gt;&lt;p&gt;When &lt;code&gt;MANAGE_BUILTINS&lt;/code&gt; is set to &lt;code&gt;yes&lt;/code&gt;, on stopping or reloading UFW, it will flush the built-in chains completely, removing all rules (both UFW-managed and non-UFW rules) from &lt;code&gt;INPUT&lt;/code&gt;, &lt;code&gt;OUTPUT&lt;/code&gt;, and &lt;code&gt;FORWARD&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Any non-UFW rules in the built-in chains will be lost. UFW will take full control of the firewall, which may conflict with other tools that manage these chains.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;For this reason, I don’t recommend enabling it unless you are fully aware of the implications.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;The only change I make in this file is to the &lt;code&gt;IPT_SYSCTL&lt;/code&gt; variable. There’s another file I’ll discuss next, which is &lt;code&gt;/etc/ufw/sysctl.conf&lt;/code&gt;. UFW uses this file to tweak certain kernel parameters.&lt;/p&gt;&lt;p&gt;However, the original file for changing kernel parameters is &lt;code&gt;/etc/sysctl.conf&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;While UFW uses its own version for this purpose, I prefer not to do that. When I &lt;a href=&quot;https://ivansalloum.com/kernel-hardening-securing-your-linux-server/&quot;&gt;harden the Linux kernel&lt;/a&gt;, I make my changes to kernel parameters directly in the &lt;code&gt;/etc/sysctl.conf&lt;/code&gt; file.&lt;/p&gt;&lt;p&gt;If UFW modifies the same parameters in its own file, my changes won’t take effect because UFW will override any corresponding parameters in the original &lt;code&gt;sysctl.conf&lt;/code&gt; file.&lt;/p&gt;&lt;p&gt;That’s why I configure UFW to use the original &lt;code&gt;sysctl.conf&lt;/code&gt; file like this:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;IPT_SYSCTL=/etc/sysctl.conf
&lt;/pre&gt;&lt;p&gt;This way, I avoid dealing with two files and can maintain a clearer overview of the changes in a single file.&lt;/p&gt;&lt;p&gt;If you open the &lt;code&gt;/etc/ufw/sysctl.conf&lt;/code&gt; file, you’ll find that UFW has tweaked several kernel parameters. Once UFW is enabled, the new values for these parameters will take effect.&lt;/p&gt;&lt;p&gt;By default, UFW disables the acceptance of ICMP redirects, which helps prevent man-in-the-middle attacks. It also ignores bogus (invalid) ICMP errors and disables the logging of Martian packets.&lt;/p&gt;&lt;p&gt;Since I prefer not to manage kernel parameters in two places, I configure UFW to use the &lt;code&gt;/etc/sysctl.conf&lt;/code&gt; file instead.&lt;/p&gt;&lt;p&gt;To incorporate the default changes that UFW makes, I can simply add these to the end of the &lt;code&gt;/etc/sysctl.conf&lt;/code&gt; file:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;net.ipv4.conf.all.accept_redirects = 0 net.ipv4.conf.default.accept_redirects = 0 net.ipv6.conf.all.accept_redirects = 0 net.ipv6.conf.default.accept_redirects = 0 net.ipv4.icmp_echo_ignore_broadcasts = 1 net.ipv4.icmp_ignore_bogus_error_responses = 1 net.ipv4.icmp_echo_ignore_all = 0 net.ipv4.conf.all.log_martians = 0 net.ipv4.conf.default.log_martians = 0
&lt;/pre&gt;&lt;p&gt;Now, simply reboot the server for the changes to take effect. There’s no need to enable UFW for the changes to apply, since we are using the original &lt;code&gt;sysctl.conf&lt;/code&gt; file.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;Check out my &lt;a href=&quot;https://ivansalloum.com/kernel-hardening-securing-your-linux-server/&quot;&gt;kernel hardening&lt;/a&gt; guide, where I list all the kernel parameters I tweak to improve the security of my Linux servers.&lt;/p&gt;&lt;p&gt;The last file I want you to review is the &lt;code&gt;/etc/ufw/ufw.conf&lt;/code&gt; file, which contains just two variables:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;ENABLED=no LOGLEVEL=low
&lt;/pre&gt;&lt;p&gt;The first variable controls whether UFW is enabled or disabled. There&apos;s no need to change it manually, as enabling or disabling UFW from the command line will automatically update this value.&lt;/p&gt;&lt;p&gt;The second variable controls the log level of UFW. It can be set to &lt;code&gt;off&lt;/code&gt;, &lt;code&gt;low&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;high&lt;/code&gt;, or &lt;code&gt;full&lt;/code&gt;, depending on how much logging detail you want.&lt;/p&gt;&lt;p&gt;You can also change the logging level directly from the command line using the following command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw logging
&lt;/pre&gt;&lt;p&gt;This will automatically update the value of the &lt;code&gt;LOGLEVEL&lt;/code&gt; variable.&lt;/p&gt;&lt;h2&gt;UFW&apos;s Rule Files&lt;/h2&gt;&lt;p&gt;In the &lt;code&gt;/etc/ufw/&lt;/code&gt; directory, you&apos;ll find files with the &lt;code&gt;.rules&lt;/code&gt; extension:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;after6.rules after.rules before6.rules before.rules user6.rules user.rules
&lt;/pre&gt;&lt;p&gt;These files control how UFW manages incoming, outgoing, and forwarded traffic. Files with the number &lt;code&gt;6&lt;/code&gt; handle IPv6 traffic, while files without it handle IPv4 traffic.&lt;/p&gt;&lt;p&gt;It&apos;s important to note that you should not modify the &lt;code&gt;user.rules&lt;/code&gt; or &lt;code&gt;user6.rules&lt;/code&gt; files directly, as any changes could be overwritten by UFW. Rules added by the user from the command line are saved to these files, which is why they are called &lt;code&gt;user.rules&lt;/code&gt; files.&lt;/p&gt;&lt;p&gt;You are free to add custom rules only to the &lt;code&gt;before.rules&lt;/code&gt; or &lt;code&gt;after.rules&lt;/code&gt; files.&lt;/p&gt;&lt;p&gt;The order in which UFW processes firewall rules is as follows: &lt;code&gt;before.rules&lt;/code&gt; first, &lt;code&gt;user.rules&lt;/code&gt; next, and &lt;code&gt;after.rules&lt;/code&gt; last.&lt;/p&gt;&lt;p&gt;For instance, if you place a rule to block HTTP traffic in &lt;code&gt;before.rules&lt;/code&gt;, it will be processed before any rule in the &lt;code&gt;user.rules&lt;/code&gt; file that might allow HTTP traffic.&lt;/p&gt;&lt;p&gt;The rules in the &lt;code&gt;before.rules&lt;/code&gt; files take priority, meaning they are executed first, allowing you to enforce more critical security measures, such as rules to &lt;a href=&quot;https://ivansalloum.com/preventing-syn-flood-attacks-on-your-linux-server/&quot;&gt;block SYN flood attacks&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;There are rare cases where I&apos;ve had to add custom rules to the &lt;code&gt;after.rules&lt;/code&gt; file. While I don&apos;t think you&apos;ll need to do this, it&apos;s good to at least know it exists – just in case!&lt;/p&gt;&lt;p&gt;And it’s important to mention that all of UFW&apos;s rule files primarily use the Iptables syntax.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;Understanding the order in which UFW processes these files and their respective roles is essential for effectively managing your firewall.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;Basic User-Defined Rules&lt;/h2&gt;&lt;p&gt;In the following, I&apos;ll cover the basic firewall rules that can be added from the command line.&lt;/p&gt;&lt;p&gt;UFW offers a set of commands for managing firewall rules directly, allowing you to quickly specify which services or ports are allowed or denied.&lt;/p&gt;&lt;p&gt;While these rules are designed for basic network access control and aren&apos;t intended for advanced use cases, they are perfect for setting up a firewall swiftly and efficiently.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;To review the rules you&apos;ve added when the firewall is disabled, use the command &lt;code&gt;sudo ufw show added&lt;/code&gt;, as &lt;code&gt;sudo ufw status&lt;/code&gt; won’t display the rules in that case.&lt;/p&gt;&lt;p&gt;Remember, every rule you add from the command line is saved to the &lt;code&gt;user.rules&lt;/code&gt; file, so feel free to check it as you add rules to understand how UFW translates the commands into the file.&lt;/p&gt;&lt;h3&gt;Allowing and Denying Traffic&lt;/h3&gt;&lt;p&gt;The core functionality of UFW is to allow or deny network traffic.&lt;/p&gt;&lt;p&gt;To allow or deny traffic for specific ports, you use the &lt;code&gt;allow&lt;/code&gt; or &lt;code&gt;deny&lt;/code&gt; rules, respectively.&lt;/p&gt;&lt;p&gt;To allow incoming traffic on port 22 (SSH):&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw allow 22
&lt;/pre&gt;&lt;p&gt;You can also specify a protocol (TCP or UDP):&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw allow 22/tcp
&lt;/pre&gt;&lt;p&gt;To deny traffic on port 80 (HTTP):&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw deny 80
&lt;/pre&gt;&lt;p&gt;If a range of ports is required, such as 5000-6000, use the following:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw deny 5000:6000/tcp
&lt;/pre&gt;&lt;p&gt;Similarly, to deny traffic for the same range:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw deny 5000:6000/tcp
&lt;/pre&gt;&lt;p&gt;You can also allow or deny traffic based on a service&apos;s name instead of specifying a port number. For example, to allow SSH traffic by using the service name, you can use:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw allow ssh
&lt;/pre&gt;&lt;p&gt;UFW will then automatically determine the correct port (port 22 for SSH) and the associated protocol (TCP) to apply the rule. This approach simplifies rule management, especially when dealing with well-known services.&lt;/p&gt;&lt;h3&gt;Application Profiles&lt;/h3&gt;&lt;p&gt;Applications (software or services installed) can register their profiles with UFW upon installation, enabling UFW to manage them by name.&lt;/p&gt;&lt;p&gt;To view the available profiles, you can use the following command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw app list
&lt;/pre&gt;&lt;p&gt;If your server is new, you are more likely to see only the OpenSSH profile, which is the service behind SSH.&lt;/p&gt;&lt;p&gt;When using application profiles, there’s no need to memorize specific ports. Instead, you use the profile name.&lt;/p&gt;&lt;p&gt;For instance, to allow traffic on port 443 (HTTPS), you can use the following commands:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw allow &amp;quot;NGINX HTTPS&amp;quot; sudo ufw allow &amp;quot;Apache Secure&amp;quot;
&lt;/pre&gt;&lt;p&gt;If you’re curious about the origins of these profiles, check the &lt;code&gt;/etc/ufw/applications.d/&lt;/code&gt; directory.&lt;/p&gt;&lt;h3&gt;The &lt;code&gt;limit&lt;/code&gt; Rule&lt;/h3&gt;&lt;p&gt;The &lt;code&gt;limit&lt;/code&gt; rule in UFW helps protect against brute-force attacks by restricting the number of connection attempts an IP can make in a short time.&lt;/p&gt;&lt;p&gt;For example, when securing SSH, this rule lets legitimate users connect but temporarily blocks any IP that makes too many failed attempts.&lt;/p&gt;&lt;p&gt;By default, the rule allows only 6 connections from the same IP within 30 seconds. If the limit is exceeded, the IP is blocked temporarily, which reduces the risk of brute-force attacks.&lt;/p&gt;&lt;p&gt;To apply this rule for SSH traffic:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw limit 22
&lt;/pre&gt;&lt;p&gt;This command enables SSH access while protecting the server from excessive connection attempts.&lt;/p&gt;&lt;h3&gt;Access Control by IP or Subnet&lt;/h3&gt;&lt;p&gt;UFW lets you control access based on IP addresses or subnets, which is useful for limiting access to specific clients or denying access from specific sources.&lt;/p&gt;&lt;p&gt;To allow all traffic from a specific IP to any port:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw allow from 192.168.1.100
&lt;/pre&gt;&lt;p&gt;To allow SSH traffic only from a specific IP:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw allow from 192.168.1.100 to any port 22
&lt;/pre&gt;&lt;p&gt;To allow traffic from a subnet:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw allow from 192.168.1.0/24
&lt;/pre&gt;&lt;p&gt;To deny traffic from a specific IP address:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw deny from 203.0.113.50
&lt;/pre&gt;&lt;p&gt;In some cases, you might want to specify not just the IP or subnet but also the protocol (TCP or UDP).&lt;/p&gt;&lt;p&gt;For example, to allow only TCP traffic from an IP address to port 80, you can specify the protocol as follows:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw allow from 192.168.1.100 to any port 80 proto tcp
&lt;/pre&gt;&lt;p&gt;By specifying protocols in your access control rules, you gain more control over the traffic flow, ensuring that only the desired traffic (TCP or UDP) is allowed from specific sources or networks.&lt;/p&gt;&lt;h3&gt;Enabling and Checking Status&lt;/h3&gt;&lt;p&gt;Before activating our firewall, it’s crucial to review the rules we’ve added so far to prevent any unexpected behavior.&lt;/p&gt;&lt;p&gt;As I mentioned earlier, if the firewall is disabled, we can&apos;t use the &lt;code&gt;sudo ufw status&lt;/code&gt; command to view our rules.&lt;/p&gt;&lt;p&gt;Instead, we use the &lt;code&gt;sudo ufw show added&lt;/code&gt; command. This command will list all the rules we have added.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;Always add the rules, review them, and then proceed to enable the firewall.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;To enable UFW and apply the rules you’ve configured, use the following command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw enable
&lt;/pre&gt;&lt;p&gt;Now, you can check the status of UFW and your current ruleset using the &lt;code&gt;sudo ufw status&lt;/code&gt; command.&lt;/p&gt;&lt;p&gt;For a more detailed view:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw status verbose
&lt;/pre&gt;&lt;p&gt;If you experience any issues, disable UFW using &lt;code&gt;sudo ufw disable&lt;/code&gt; and review your rules again.&lt;/p&gt;&lt;p&gt;If you need to reset UFW to its default state (removing all rules), you can use the &lt;code&gt;sudo ufw reset&lt;/code&gt; command.&lt;/p&gt;&lt;h3&gt;Deleting Rules&lt;/h3&gt;&lt;p&gt;If, for some reason, you want to delete a rule you have added, you can use the &lt;code&gt;sudo ufw delete&lt;/code&gt; command followed by the rule itself like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw delete deny from 111.111.111.111 to any port 80 proto tcp sudo ufw delete allow 80
&lt;/pre&gt;&lt;p&gt;There is another easier way to delete rules, but it requires the firewall to be enabled. This method involves using the rule number.&lt;/p&gt;&lt;p&gt;Once the firewall is enabled, you can use the &lt;code&gt;sudo ufw status numbered&lt;/code&gt; command to obtain a list of your rules and their corresponding numbers, like this:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;To Action From \-- ------ ---- [ 1] 22/tcp ALLOW IN Anywhere [ 2] 22/tcp (v6) ALLOW IN Anywhere (v6)
&lt;/pre&gt;&lt;p&gt;Now, to delete a rule, you can simply use the rule number:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw delete 1
&lt;/pre&gt;&lt;p&gt;This is a much simpler method.&lt;/p&gt;&lt;h2&gt;Best Practices&lt;/h2&gt;&lt;p&gt;I want to share some best practices with you from my experience&lt;/p&gt;&lt;p&gt;The first step before enabling a firewall is to allow SSH traffic to ensure access to the server. If you enable the firewall before adding this rule, you risk losing access to your server.&lt;/p&gt;&lt;p&gt;I showed you how to use the &lt;code&gt;allow&lt;/code&gt; or &lt;code&gt;limit&lt;/code&gt; rules. Using these rules will permit any IP address to access port 22, which means our SSH port is open to everyone. This is something I avoid on a production server.&lt;/p&gt;&lt;p&gt;If I have a static IP from which I can access the server, I restrict the SSH port to that IP. This provides an extra layer of security and reduces the risk of unauthorized access.&lt;/p&gt;&lt;p&gt;Even if you’ve generated an SSH key pair, implemented key authentication, and created a non-root user, hackers could still attempt to breach your server.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;If you’ve followed my guide on&lt;/em&gt;&lt;a href=&quot;https://ivansalloum.com/preparing-your-ubuntu-server-for-first-use/&quot;&gt; &lt;em&gt;preparing Ubuntu servers&lt;/em&gt;&lt;/a&gt; &lt;em&gt;, you should have already completed these security steps.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Only restrict the SSH port to your IP if it’s static – such as when using a VPN service or if your ISP has assigned you a static IP.&lt;/p&gt;&lt;p&gt;If you have a static IP, use the following command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw allow from proto tcp to any port 22
&lt;/pre&gt;&lt;p&gt;Now, the IP specified in the command is the only one that can access the server.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;When you restrict SSH access to a single IP, &lt;a href=&quot;https://ivansalloum.com/securing-your-linux-server-with-fail2ban/&quot;&gt;Fail2ban&lt;/a&gt; becomes irrelevant since there are no IPs to block. However, I still recommend keeping Fail2ban installed and enabled.&lt;/p&gt;&lt;p&gt;Another useful feature to implement is a &lt;a href=&quot;https://ivansalloum.com/linux-server-security-cloud-firewall-setup/&quot;&gt;cloud firewall&lt;/a&gt;. Most server providers offer a cloud firewall, which allows you to set up a basic firewall at the cloud level and apply it to your servers.&lt;/p&gt;&lt;p&gt;With this, you can restrict the SSH port to your IP address using the cloud firewall while leaving it open to everyone in UFW. If your IP address changes, you can easily access your provider&apos;s dashboard to update it.&lt;/p&gt;&lt;p&gt;Now, let&apos;s talk about HTTP and HTTPS traffic.&lt;/p&gt;&lt;p&gt;If you&apos;re hosting a website, you need to allow traffic on ports 80 and 443 for visitors to access it. There should be no restrictions here, as we want everyone to reach the site.&lt;/p&gt;&lt;p&gt;However, there are a couple of considerations. If you&apos;re using an SSL certificate (which you should) and redirecting traffic from HTTP to HTTPS, there&apos;s no need to allow traffic on port 80. While allowing traffic on both ports is generally fine, I wanted to mention this.&lt;/p&gt;&lt;p&gt;Additionally, if you&apos;re using a proxy service like Cloudflare, traffic will only come from their IPs. To enhance security, restrict HTTP and HTTPS traffic to only Cloudflare&apos;s IPs.&lt;/p&gt;&lt;p&gt;For example, use these commands:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw allow from to any port 80 proto tcp sudo ufw allow from to any port 443 proto tcp
&lt;/pre&gt;&lt;p&gt;Repeat these commands for all Cloudflare IPs, or automate it with a bash script.&lt;/p&gt;&lt;h2&gt;Advanced Firewall Rules&lt;/h2&gt;&lt;p&gt;Up until now, we’ve focused on basic user-defined rules that you can easily manage from the command line.&lt;/p&gt;&lt;p&gt;Now, I’ll introduce the idea of advanced rules, which allow you to control traffic at a deeper level by configuring UFW&apos;s &lt;code&gt;/etc/ufw/before.rules&lt;/code&gt; files.&lt;/p&gt;&lt;p&gt;These advanced rules let you filter traffic before it reaches your server’s services and before the firewall applies its standard rules.&lt;/p&gt;&lt;p&gt;Advanced rules are incredibly powerful and can be tailored to specific use cases, offering finer control over your network&apos;s security and performance.&lt;/p&gt;&lt;p&gt;And as I mentioned earlier, all of UFW&apos;s rule files primarily use the iptables syntax.&lt;/p&gt;&lt;h3&gt;Structure of &lt;code&gt;before.rules&lt;/code&gt; Files&lt;/h3&gt;&lt;p&gt;The &lt;code&gt;before.rules&lt;/code&gt; file begins with a declaration of the &lt;code&gt;*filter&lt;/code&gt; table and defines several custom chains:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;*filter :ufw-before-input - [0:0] :ufw-before-output - [0:0] :ufw-before-forward - [0:0] :ufw-not-local - [0:0]
&lt;/pre&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;:ufw-before-input&lt;/code&gt;: Processes incoming packets.&lt;/li&gt;&lt;li&gt;&lt;code&gt;:ufw-before-output&lt;/code&gt;: Processes outgoing packets.&lt;/li&gt;&lt;li&gt;&lt;code&gt;:ufw-before-forward&lt;/code&gt;: Handles packets forwarded through the server.&lt;/li&gt;&lt;li&gt;&lt;code&gt;:ufw-not-local&lt;/code&gt;: Deals with packets that are not addressed to or from the local system.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;It includes several default rules to handle fundamental network operations and improve security. These rules cover a variety of scenarios, such as allowing all traffic on loopback interfaces or dropping invalid packets. They are applied by default once you enable the firewall.&lt;/p&gt;&lt;p&gt;The file ends with the &lt;code&gt;COMMIT&lt;/code&gt; line, signaling the completion of the rules.&lt;/p&gt;&lt;p&gt;The structure in &lt;code&gt;before6.rules&lt;/code&gt; for IPv6 is nearly identical to that of &lt;code&gt;before.rules&lt;/code&gt; for IPv4.&lt;/p&gt;&lt;p&gt;The main difference is that the chains in &lt;code&gt;before6.rules&lt;/code&gt; all have the number &lt;code&gt;6&lt;/code&gt; added to their names, like this:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;*filter :ufw6-before-input - [0:0] :ufw6-before-output - [0:0] :ufw6-before-forward - [0:0]
&lt;/pre&gt;&lt;p&gt;However, unlike &lt;code&gt;before.rules&lt;/code&gt;, &lt;code&gt;before6.rules&lt;/code&gt; does not include a &lt;code&gt;ufw6-not-local&lt;/code&gt; chain.&lt;/p&gt;&lt;p&gt;It includes some of the default rules that &lt;code&gt;before.rules&lt;/code&gt; has, but it also contains additional rules that are applied specifically to IPv6 traffic.&lt;/p&gt;&lt;p&gt;The file also ends with the &lt;code&gt;COMMIT&lt;/code&gt; line, signaling the completion of the rules.&lt;/p&gt;&lt;h3&gt;Use Cases&lt;/h3&gt;&lt;p&gt;As I mentioned earlier, these files take priority, meaning they are executed first.&lt;/p&gt;&lt;p&gt;This allows us to define rules that will take effect before traffic reaches anything running on the server and before it travels further through the firewall.&lt;/p&gt;&lt;p&gt;You can, for example, implement a solution to &lt;a href=&quot;https://ivansalloum.com/preventing-syn-flood-attacks-on-your-linux-server/&quot;&gt;block SYN flood attacks&lt;/a&gt; by rate-limiting the number of SYN packets allowed through ports 80 and 443, and block IPs exceeding these limits. This is something that cannot be done using basic user-defined rules from the command line.&lt;/p&gt;&lt;p&gt;While you can use the &lt;code&gt;limit&lt;/code&gt; rule for ports 80 and 443, it’s not ideal for a web server since the limits may not be well-suited to handle the typical traffic patterns of a web server.&lt;/p&gt;&lt;p&gt;Using more advanced rules in &lt;code&gt;before.rules&lt;/code&gt; allows you to fine-tune the firewall for specific use cases like this.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;Check out my guide on &lt;a href=&quot;https://ivansalloum.com/preventing-syn-flood-attacks-on-your-linux-server/&quot;&gt;preventing SYN flood attacks&lt;/a&gt; on a Linux server, where I provide a solution that combines advanced UFW rules with Fail2ban.&lt;/p&gt;&lt;h3&gt;Example of Advanced Rules&lt;/h3&gt;&lt;p&gt;If you examine the contents of the &lt;code&gt;before.rules&lt;/code&gt; file, you will notice these two rules:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;-A ufw-before-input -m conntrack --ctstate INVALID -j ufw-logging-deny -A ufw-before-input -m conntrack --ctstate INVALID -j DROP
&lt;/pre&gt;&lt;p&gt;These two rules are designed to log and block any &lt;a href=&quot;https://ivansalloum.com/how-to-block-invalid-packets-with-ufw/&quot;&gt;invalid packets&lt;/a&gt;, and they are added by default by UFW to the &lt;code&gt;ufw-before-input&lt;/code&gt; chain, which filters incoming traffic before it reaches the server, ensuring that only legitimate connections are allowed.&lt;/p&gt;&lt;p&gt;UFW uses the &lt;code&gt;conntrack&lt;/code&gt; module (short for connection tracking) to monitor connections and identify those with &lt;code&gt;INVALID&lt;/code&gt; connection states. While these rules are effective, we can make them even better.&lt;/p&gt;&lt;p&gt;To further enhance the security of our server, we could add two additional rules to log and block any new connections that don’t have only the SYN flag set.&lt;/p&gt;&lt;p&gt;Add the following two rules below the ones added by default by UFW:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;-A ufw-before-input -p tcp -m tcp ! --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j ufw-logging-deny -A ufw-before-input -p tcp -m tcp ! --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j DROP
&lt;/pre&gt;&lt;p&gt;Don&apos;t forget to add them to the &lt;code&gt;before6.rules&lt;/code&gt; file as well:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;-A ufw6-before-input -p tcp -m tcp ! --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j ufw6-logging-deny -A ufw6-before-input -p tcp -m tcp ! --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j DROP
&lt;/pre&gt;&lt;ul&gt;&lt;li&gt;The first rule drops any packet that’s considered &lt;code&gt;INVALID&lt;/code&gt; by the &lt;code&gt;conntrack&lt;/code&gt; module.&lt;/li&gt;&lt;li&gt;The second rule blocks TCP packets that are flagged as &lt;code&gt;NEW&lt;/code&gt; (indicating new connection attempts) but don’t have the SYN flag set alone.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Now, reload UFW if it is already enabled:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw reload
&lt;/pre&gt;&lt;p&gt;These additional rules further enhance the firewall’s ability to filter out potentially malicious packets and protect your server from unwanted connection attempts.&lt;/p&gt;&lt;p&gt;Great job reaching the end!&lt;/p&gt;&lt;p&gt;I hope this guide has made setting up a firewall with UFW clear and straightforward.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;For more comprehensive Linux server security resources, be sure to check out the full collection of detailed guides &lt;a href=&quot;https://ivansalloum.com/collections/linux-server-security/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;If you found value in this guide or have any questions or feedback, please don&apos;t hesitate to share your thoughts in the &lt;strong&gt;discussion&lt;/strong&gt; section.&lt;/p&gt;&lt;p&gt;Your input is greatly appreciated, and you can also &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;contact me&lt;/a&gt; directly if you prefer.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Security</category></item><item><title>Securing Your Linux Server with Fail2ban</title><link>https://ivansalloum.com/securing-your-linux-server-with-fail2ban/</link><guid isPermaLink="true">https://ivansalloum.com/securing-your-linux-server-with-fail2ban/</guid><description>Learn how to install and configure Fail2ban to protect your server against unauthorized access attempts and brute force attacks.</description><pubDate>Tue, 19 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;Fail2Ban is a security tool that helps protect your server from unauthorized access attempts and brute force attacks by monitoring logs for suspicious activities and blocking the IP addresses of attackers.&lt;/p&gt;&lt;p&gt;This will allow your server to harden itself against these access attempts without intervention from you.&lt;/p&gt;&lt;p&gt;In this guide, I’ll walk you through installing and configuring Fail2Ban.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;_I assume you&apos;re working on a properly set-up Ubuntu server. If not, check out my guide  on &lt;em&gt;&lt;a href=&quot;https://ivansalloum.com/preparing-your-ubuntu-server-for-first-use/&quot;&gt;&lt;em&gt;preparing  Ubuntu servers&lt;/em&gt;&lt;/a&gt; _  to get started.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;Author&apos;s Note&lt;/h2&gt;&lt;p&gt;Before we dive into the technical details, I want to highlight a few important considerations regarding Fail2ban.&lt;/p&gt;&lt;p&gt;Firstly, it&apos;s crucial to understand that Fail2ban should not be your sole security measure. Some may mistakenly believe it can replace firewall rules, but that is not the case.&lt;/p&gt;&lt;p&gt;Additionally, disabling password authentication and relying solely on SSH key authentication may lessen the need for Fail2ban.&lt;/p&gt;&lt;p&gt;However, I still advocate for its use as an additional layer of security.&lt;/p&gt;&lt;h2&gt;Installation&lt;/h2&gt;&lt;p&gt;Fail2ban is available in Ubuntu’s repositories.&lt;/p&gt;&lt;p&gt;To install it, use this command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt install fail2ban
&lt;/pre&gt;&lt;p&gt;I noticed that Fail2ban disables its service upon installation on Ubuntu 22.04 due to some default settings that may cause unexpected behavior. In contrast, it is activated by default on Ubuntu 24.04.&lt;/p&gt;&lt;p&gt;You can verify this by using the following command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo systemctl status fail2ban
&lt;/pre&gt;&lt;p&gt;That&apos;s it for the installation.&lt;/p&gt;&lt;h2&gt;Pre-Configuration&lt;/h2&gt;&lt;p&gt;Fail2ban&apos;s configuration files are located in the &lt;code&gt;/etc/fail2ban/&lt;/code&gt; directory.&lt;/p&gt;&lt;p&gt;If you list the contents of this directory, you will find two important configuration files: &lt;code&gt;fail2ban.conf&lt;/code&gt; and &lt;code&gt;jail.conf&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;fail2ban.conf&lt;/code&gt; file contains Fail2Ban&apos;s global settings, which I don&apos;t recommend modifying.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;jail.conf&lt;/code&gt; file contains jails, filters with actions.&lt;/p&gt;&lt;p&gt;We shouldn&apos;t directly modify these files, as an update may override our changes.&lt;/p&gt;&lt;p&gt;That&apos;s why Fail2Ban recommends creating two local copies of these configuration files for us to modify.&lt;/p&gt;&lt;p&gt;Use the following commands to create a local copy of these two files:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo cp jail.conf jail.local sudo cp fail2ban.conf fail2ban.local
&lt;/pre&gt;&lt;p&gt;Now you can safely modify Fail2Ban&apos;s configuration.&lt;/p&gt;&lt;h2&gt;Configuration&lt;/h2&gt;&lt;p&gt;Open the &lt;code&gt;jail.local&lt;/code&gt; file with your preferred editor and examine its settings.&lt;/p&gt;&lt;p&gt;Under the &lt;code&gt;[DEFAULT]&lt;/code&gt; section, you will find settings that apply to all services protected by Fail2Ban.&lt;/p&gt;&lt;p&gt;Elsewhere in the file, there are sections like the &lt;code&gt;[sshd]&lt;/code&gt; section, which contains service-specific settings (or individual jails) that will override the defaults.&lt;/p&gt;&lt;p&gt;Under the &lt;code&gt;[DEFAULT]&lt;/code&gt; section, there are some variables that you may want to modify.&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;bantime
&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;bantime&lt;/code&gt; variable sets the duration for which an IP will be blocked from accessing the server after failing to authenticate correctly.&lt;/p&gt;&lt;p&gt;By default, this is set to 10 minutes.&lt;/p&gt;&lt;p&gt;You can change its value as you prefer, for instance, to &lt;code&gt;60m&lt;/code&gt; for one hour.&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;findtime maxretry
&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;findtime&lt;/code&gt; and &lt;code&gt;maxretry&lt;/code&gt; variables function together to determine the conditions under which an IP should be blocked from accessing the server.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;maxretry&lt;/code&gt; variable defines the number of authentication attempts an IP is allowed to make within a time period defined by &lt;code&gt;findtime&lt;/code&gt; before being blocked.&lt;/p&gt;&lt;p&gt;With the default settings, Fail2Ban will block an IP that unsuccessfully attempts to access the server more than 5 times within a 10-minute interval.&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;#ignoreip = 127.0.0.1/8 ::1
&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;ignoreip&lt;/code&gt; variable contains a list of IP addresses, CIDR masks, or DNS hosts that Fail2ban won&apos;t block.&lt;/p&gt;&lt;p&gt;By default, this variable is commented out.&lt;/p&gt;&lt;p&gt;I strongly advise uncommenting this line and appending your own IP address to the list. This ensures that you won&apos;t inadvertently block yourself from accessing the server.&lt;/p&gt;&lt;h3&gt;Email Alerts&lt;/h3&gt;&lt;p&gt;Receiving email alerts is a practice I consistently advocate for in all of my guides.&lt;/p&gt;&lt;p&gt;If you want to receive email alerts whenever Fail2ban blocks an IP, you should adjust these two variables inside the &lt;code&gt;jail.local&lt;/code&gt; file:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;destemail sender
&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;destemail&lt;/code&gt; variable defines the email address to which the alerts should be sent.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;sender&lt;/code&gt; variable defines the email address from which the alerts will be sent.&lt;/p&gt;&lt;p&gt;But there is something to pay attention to.&lt;/p&gt;&lt;p&gt;The sender variable should look like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sender = root@example.com
&lt;/pre&gt;&lt;p&gt;Don&apos;t use your hostname. This won&apos;t work:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sender = root@host.example.com
&lt;/pre&gt;&lt;p&gt;Lastly, there is the &lt;code&gt;mta&lt;/code&gt; variable, which specifies the mail agent that will be used to send the emails.&lt;/p&gt;&lt;p&gt;By default, Fail2ban uses Sendmail as its mail agent.&lt;/p&gt;&lt;p&gt;If you prefer to use Postfix, you can do so without changing the &lt;code&gt;mta&lt;/code&gt; variable, as Fail2ban will automatically use it if it is installed.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;Your server needs to be configured to send emails, and this can be done by setting up Postfix for &lt;a href=&quot;https://ivansalloum.com/how-to-configure-postfix-for-external-smtp-relay/&quot;&gt;external SMTP relay&lt;/a&gt; to ensure you receive alerts.&lt;/p&gt;&lt;p&gt;Now, there is one more step to take in order to receive alerts. Keep reading.&lt;/p&gt;&lt;h3&gt;Default Action&lt;/h3&gt;&lt;p&gt;If you scroll down a bit inside the &lt;code&gt;jail.local&lt;/code&gt; file, you&apos;ll see the &lt;code&gt;action&lt;/code&gt; variable:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;action = %(action_)s
&lt;/pre&gt;&lt;p&gt;This variable dictates the action Fail2ban should take when blocking an IP address.&lt;/p&gt;&lt;p&gt;The default action is to add a firewall rule that rejects traffic from the IP address, removing it after the specified &lt;code&gt;bantime&lt;/code&gt; elapses.&lt;/p&gt;&lt;p&gt;Aha, the default action doesn&apos;t send alerts, which is why I asked you to keep reading.&lt;/p&gt;&lt;p&gt;Above the action variable, you&apos;ll find various actions you can switch between.&lt;/p&gt;&lt;p&gt;For example, &lt;code&gt;action_mw&lt;/code&gt; sends an email when taking action, &lt;code&gt;action_mwl&lt;/code&gt; sends an email and includes logging, and &lt;code&gt;action_cf_mwl&lt;/code&gt; does all of the above, plus sends an update to the Cloudflare API associated with your account to ban the attacker there as well.&lt;/p&gt;&lt;p&gt;I always prefer using the &lt;code&gt;action_mwl&lt;/code&gt; action.&lt;/p&gt;&lt;p&gt;If you want to use it too, your &lt;code&gt;action&lt;/code&gt; variable should be set as follows:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;action = %(action_mwl)s
&lt;/pre&gt;&lt;p&gt;Now, whenever Fail2ban blocks an IP address, you&apos;ll receive an email alert, and Fail2ban logs the block.&lt;/p&gt;&lt;h3&gt;Individual Jails&lt;/h3&gt;&lt;p&gt;Now, it is time to examine the service-specific sections, also known as individual jails, such as the &lt;code&gt;[sshd]&lt;/code&gt; jail, which protects our server from unauthorized access attempts.&lt;/p&gt;&lt;p&gt;Each of these jails needs to be individually enabled by adding an &lt;code&gt;enabled = true&lt;/code&gt; line under the header, along with their other settings.&lt;/p&gt;&lt;p&gt;By default, only the &lt;code&gt;[sshd]&lt;/code&gt; jail is enabled, and all others are disabled.&lt;/p&gt;&lt;p&gt;You can verify this by opening the &lt;code&gt;/etc/fail2ban/jail.d/defaults-debian.conf&lt;/code&gt; file, which contains the line &lt;code&gt;enabled = true&lt;/code&gt; for the &lt;code&gt;[sshd]&lt;/code&gt; jail.&lt;/p&gt;&lt;p&gt;Scroll down the &lt;code&gt;jail.conf&lt;/code&gt; file until you find the &lt;code&gt;[sshd]&lt;/code&gt; jail, which should look similar to this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;port = ssh logpath = %(sshd_log)s backend = %(sshd_backend)s
&lt;/pre&gt;&lt;p&gt;If you&apos;ve changed the SSH port, ensure to update the value of the &lt;code&gt;port&lt;/code&gt; variable accordingly.&lt;/p&gt;&lt;p&gt;You can include variables defined in the &lt;code&gt;[DEFAULT]&lt;/code&gt; section, such as the &lt;code&gt;bantime&lt;/code&gt;, &lt;code&gt;maxretry&lt;/code&gt;, and &lt;code&gt;findtime&lt;/code&gt; variables, which will only apply to this jail.&lt;/p&gt;&lt;p&gt;If you scroll down further, you&apos;ll find other jails that are disabled, such as the &lt;code&gt;[nginx-http-auth]&lt;/code&gt; or &lt;code&gt;[apache-auth]&lt;/code&gt; jails.&lt;/p&gt;&lt;p&gt;If you&apos;re running NGINX and want to protect a page of your site using a password, you could enable the &lt;code&gt;[nginx-http-auth]&lt;/code&gt; jail to secure that page from unauthorized access attempts.&lt;/p&gt;&lt;h2&gt;Starting Fail2ban&lt;/h2&gt;&lt;p&gt;Since the &lt;code&gt;[sshd]&lt;/code&gt; jail, which protects SSH, is enabled, we can proceed to start and enable the &lt;code&gt;fail2ban&lt;/code&gt; service if it is disabled, depending on the version of Ubuntu you are using.&lt;/p&gt;&lt;p&gt;Use the following commands to start and enable Fail2ban:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo systemctl start fail2ban sudo systemctl enable fail2ban
&lt;/pre&gt;&lt;p&gt;You can use the &lt;code&gt;fail2ban-client&lt;/code&gt; command to check the active jails:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo fail2ban-client status
&lt;/pre&gt;&lt;p&gt;Output:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;Status |- Number of jail: 1 `- Jail list: sshd
&lt;/pre&gt;&lt;p&gt;To view the status and information regarding a specific jail like the &lt;code&gt;sshd&lt;/code&gt; jail, you can use the following command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo fail2ban-client status sshd
&lt;/pre&gt;&lt;p&gt;Output:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;Status for the jail: sshd |- Filter | |- Currently failed: 5 | |- Total failed: 21 | `- File list: /var/log/auth.log `- Actions |- Currently banned: 1 |- Total banned: 2 `- Banned IP list: 218.92.0.29
&lt;/pre&gt;&lt;p&gt;❗&lt;/p&gt;&lt;p&gt;If you have disabled password authentication for SSH, you may notice zero failed attempts.&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;Great job reaching the end!&lt;/p&gt;&lt;p&gt;There are many more things you can do with Fail2Ban, but for now, protecting SSH is the most important.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;You can find the full collection of detailed Linux server security guides &lt;a href=&quot;https://ivansalloum.com/collections/linux-server-security/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;If you found value in this guide or have any questions or feedback, please don&apos;t hesitate to share your thoughts in the &lt;strong&gt;discussion&lt;/strong&gt; section.&lt;/p&gt;&lt;p&gt;Your input is greatly appreciated, and you can also &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;contact me&lt;/a&gt; directly if you prefer.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Security</category></item><item><title>Automating Security Updates on Linux Servers</title><link>https://ivansalloum.com/automating-security-updates-on-linux-servers/</link><guid isPermaLink="true">https://ivansalloum.com/automating-security-updates-on-linux-servers/</guid><description>Discover the essential steps to manually update your Linux server and set up automatic security updates.</description><pubDate>Mon, 18 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;It is crucial to regularly update your servers.&lt;/p&gt;&lt;p&gt;Many hacks happen because servers aren&apos;t patched with the latest security updates.&lt;/p&gt;&lt;p&gt;In this guide, I&apos;ll walk you through both manual updates and setting up automatic security updates for your Linux server.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;_I assume you&apos;re working on a properly set-up Ubuntu server. If not, check out my guide  on &lt;em&gt;&lt;a href=&quot;https://ivansalloum.com/preparing-your-ubuntu-server-for-first-use/&quot;&gt;&lt;em&gt;preparing  Ubuntu servers&lt;/em&gt;&lt;/a&gt; _  to get started.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;Manual Updates&lt;/h2&gt;&lt;p&gt;First, let’s learn how to update manually.&lt;/p&gt;&lt;p&gt;It’s just two simple commands.&lt;/p&gt;&lt;p&gt;Begin by updating the package list on your server with the following command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt update
&lt;/pre&gt;&lt;p&gt;This command prompts the server to scan the server’s packages and identify those requiring updates, including security patches.&lt;/p&gt;&lt;p&gt;Once this is done, run the following command to update your server’s packages that need updating:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt upgrade
&lt;/pre&gt;&lt;p&gt;The server may ask for confirmation by displaying a prompt that requires a &lt;strong&gt;yes&lt;/strong&gt;  or &lt;strong&gt;no&lt;/strong&gt;  response. Make sure to type &lt;strong&gt;yes&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;The updating process may take a while, depending on the number of updates needed.&lt;/p&gt;&lt;h2&gt;Automatic Security Updates&lt;/h2&gt;&lt;p&gt;While updating manually is an option, it’s easy to forget or run out of time, which is why automatic security updates ensure your servers stay patched.&lt;/p&gt;&lt;p&gt;This is crucial for keeping your servers secure.&lt;/p&gt;&lt;h3&gt;Installing Unattended Upgrades&lt;/h3&gt;&lt;p&gt;Unattended Upgrades automatically installs security updates and patches without needing our input, so we need to install the &lt;code&gt;unattended-upgrades&lt;/code&gt; package.&lt;/p&gt;&lt;p&gt;Unattended Upgrades should be installed and enabled by default on Ubuntu servers. Use the following command to check and install it:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt install unattended-upgrades
&lt;/pre&gt;&lt;p&gt;Now, we need to run just one more command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo dpkg-reconfigure unattended-upgrades
&lt;/pre&gt;&lt;p&gt;A pop-up window will appear, asking you if you want to automatically download and install stable updates.&lt;/p&gt;&lt;p&gt;Choose &lt;strong&gt;&amp;lt; Yes&amp;gt;&lt;/strong&gt; and press the &lt;strong&gt;ENTER&lt;/strong&gt;  key.&lt;/p&gt;&lt;p&gt;When you do this, Unattended Upgrades changed the value from &lt;code&gt;0&lt;/code&gt; to &lt;code&gt;1&lt;/code&gt; in the &lt;code&gt;/etc/apt/apt.conf.d/20auto-upgrades&lt;/code&gt; file:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;APT::Periodic::Update-Package-Lists &amp;quot;1&amp;quot;; APT::Periodic::Unattended-Upgrade &amp;quot;1&amp;quot;;
&lt;/pre&gt;&lt;p&gt;The number indicates how often Unattended Upgrades will run in days.&lt;/p&gt;&lt;p&gt;A value of &lt;code&gt;1&lt;/code&gt; will run Unattended Upgrades every day, while a value of &lt;code&gt;0&lt;/code&gt; will disable Unattended Upgrades.&lt;/p&gt;&lt;h3&gt;Considerations&lt;/h3&gt;&lt;p&gt;Now that we have automatic security updates in place, there are some important considerations I’d like to share with you.&lt;/p&gt;&lt;p&gt;Firstly, Unattended Upgrades primarily deals with security updates.&lt;/p&gt;&lt;p&gt;You’ll need to manually check for other updates regularly, perhaps weekly.&lt;/p&gt;&lt;p&gt;Also, be aware that Unattended Upgrades might automatically reboot your server for certain updates, which could be disruptive on a production server.&lt;/p&gt;&lt;p&gt;I recommend to manually reboot during low-traffic periods or after notifying users of downtime.&lt;/p&gt;&lt;p&gt;You can customize Unattended Upgrades to either disable automatic reboots or reschedule them for less disruptive times.&lt;/p&gt;&lt;p&gt;Lastly, while Unattended Upgrades can be set to update all packages, not just security ones, I don’t recommend this option, as some updates may break your server.&lt;/p&gt;&lt;h3&gt;Configuration&lt;/h3&gt;&lt;p&gt;Unattended Upgrades has its settings in a file called &lt;code&gt;50unattended-upgrades&lt;/code&gt; under the &lt;code&gt;/etc/apt/apt.conf.d/&lt;/code&gt; directory.&lt;/p&gt;&lt;p&gt;Open this file in your preferred editor and take a look:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;Unattended-Upgrade::Allowed-Origins { &amp;quot;${distro_id}:${distro_codename}&amp;quot;; &amp;quot;${distro_id}:${distro_codename}-security&amp;quot;; &amp;quot;${distro_id}ESMApps:${distro_codename}-apps-security&amp;quot;; &amp;quot;${distro_id}ESM:${distro_codename}-infra-security&amp;quot;; // &amp;quot;${distro_id}:${distro_codename}-updates&amp;quot;; // &amp;quot;${distro_id}:${distro_codename}-proposed&amp;quot;; // &amp;quot;${distro_id}:${distro_codename}-backports&amp;quot;; };
&lt;/pre&gt;&lt;p&gt;As you can see from this block of code, Unattended Upgrades handles only security updates.&lt;/p&gt;&lt;p&gt;If you want it to handle non-security updates and update other installed packages, you can uncomment the &lt;code&gt;${distro_id}:${distro_codename}-updates&lt;/code&gt; line.&lt;/p&gt;&lt;p&gt;If you only want Unattended Upgrades to handle security updates, as I recommend, ensure that only security origins are allowed and that all others are commented out, like its default behavior.&lt;/p&gt;&lt;p&gt;You may also want to configure whether Unattended Upgrades should reboot the server if a security update requires a reboot to be applied.&lt;/p&gt;&lt;p&gt;You can specify a time to reboot or disable this feature completely.&lt;/p&gt;&lt;p&gt;To control if Unattended Upgrades should reboot your server automatically, look for the line &lt;code&gt;Unattended-Upgrade::Automatic-Reboot&lt;/code&gt; in the configuration file.&lt;/p&gt;&lt;p&gt;Set this to &lt;code&gt;&amp;quot;false&amp;quot;&lt;/code&gt; to prevent automatic reboots after updates.&lt;/p&gt;&lt;p&gt;If you prefer automatic reboots, change it to &lt;code&gt;&amp;quot;true&amp;quot;&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Additionally, you can schedule a specific time for these reboots.&lt;/p&gt;&lt;p&gt;For this, find the line &lt;code&gt;Unattended-Upgrade::Automatic-Reboot-Time&lt;/code&gt; and set it to your desired time, like &lt;code&gt;&amp;quot;04:00&amp;quot;&lt;/code&gt; for a reboot at 4 AM.&lt;/p&gt;&lt;h3&gt;Email Alerts&lt;/h3&gt;&lt;p&gt;There is one more thing to be aware of.&lt;/p&gt;&lt;p&gt;Sometimes, Unattended Upgrades fails to install a security update automatically, requiring a manual update.&lt;/p&gt;&lt;p&gt;You can specify an email address to which Unattended Upgrades should send an email in case this happens.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;Your server needs to be configured to send emails, and this can be done by setting up Postfix for &lt;a href=&quot;https://ivansalloum.com/how-to-configure-postfix-for-external-smtp-relay/&quot;&gt;external SMTP relay&lt;/a&gt; to ensure you receive alerts.&lt;/p&gt;&lt;p&gt;Scroll down the file until you find the line &lt;code&gt;Unattended-Upgrade::Mail &amp;quot;&amp;quot;;&lt;/code&gt; and add the email address you want to send a notification to inside the two double quotation marks.&lt;/p&gt;&lt;p&gt;Then, scroll down a little further until you find the line &lt;code&gt;Unattended-Upgrade::MailReport &amp;quot;on-change&amp;quot;;&lt;/code&gt; and change it from &lt;code&gt;&amp;quot;on-change&amp;quot;&lt;/code&gt; to &lt;code&gt;&amp;quot;only-on-error&amp;quot;&lt;/code&gt; to receive a notification only if a security update fails to be installed.&lt;/p&gt;&lt;p&gt;Don’t forget to uncomment these two lines. They should look like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;Unattended-Upgrade::Mail &amp;quot;hello@ivansalloum.com&amp;quot;; Unattended-Upgrade::MailReport &amp;quot;only-on-error&amp;quot;;
&lt;/pre&gt;&lt;p&gt;Once you&apos;re done configuring Unattended Upgrades, save and close the file.&lt;/p&gt;&lt;p&gt;Now, ensure that the &lt;code&gt;mailutils&lt;/code&gt; package is installed on your server, as it provides the &lt;code&gt;mail&lt;/code&gt; command used by Unattended Upgrades to send emails.&lt;/p&gt;&lt;p&gt;You can install it with the following command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt install mailutils
&lt;/pre&gt;&lt;p&gt;With that, we&apos;ve completed the configuration. Now, let&apos;s test our setup.&lt;/p&gt;&lt;h3&gt;Testing Our Setup&lt;/h3&gt;&lt;p&gt;To verify that your configuration is working correctly, you can manually trigger an update by executing the following command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo unattended-upgrade -d
&lt;/pre&gt;&lt;p&gt;You can also test your setup while setting the &lt;code&gt;Unattended-Upgrade::MailReport&lt;/code&gt; variable to &lt;code&gt;&amp;quot;always&amp;quot;&lt;/code&gt; to verify if your server can send emails.&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;Great job reaching the end!&lt;/p&gt;&lt;p&gt;In this guide, you&apos;ve learned how to manually update and set up automatic security updates.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;You can find the full collection of detailed Linux server security guides &lt;a href=&quot;https://ivansalloum.com/collections/linux-server-security/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;If you found value in this guide or have any questions or feedback, please don&apos;t hesitate to share your thoughts in the discussion section.&lt;/p&gt;&lt;p&gt;Your input is greatly appreciated, and you can also &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;contact me&lt;/a&gt; directly if you prefer.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Security</category></item><item><title>How to Scan for Rootkits on a Linux Server</title><link>https://ivansalloum.com/how-to-scan-for-rootkits-on-a-linux-server/</link><guid isPermaLink="true">https://ivansalloum.com/how-to-scan-for-rootkits-on-a-linux-server/</guid><description>Learn how to simply scan for rootkits on your Linux server with this easy-to-follow tutorial using Rootkit Hunter.</description><pubDate>Fri, 01 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;When it comes to keeping your Linux server safe, rootkits are a major concern.&lt;/p&gt;&lt;p&gt;They are sneaky malware that can infect your Linux server without being noticed, making them a nightmare for server administrators.&lt;/p&gt;&lt;p&gt;In this tutorial, I&apos;ll walk you through the steps to scan for rootkits on your Linux server.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;_I assume you&apos;re working on a properly set-up Ubuntu server. If not, check out my guide  on &lt;em&gt;&lt;a href=&quot;https://ivansalloum.com/preparing-your-ubuntu-server-for-first-use/&quot;&gt;&lt;em&gt;preparing  Ubuntu servers&lt;/em&gt;&lt;/a&gt; _  to get started.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;What are Rootkits?&lt;/h2&gt;&lt;p&gt;Rootkits are nasty pieces of malware that can deeply hide in your server, making it sometimes impossible to detect them.&lt;/p&gt;&lt;p&gt;Once a rootkit is on your server, it can do bad things like taking control, stealing important information, and letting hackers in.&lt;/p&gt;&lt;p&gt;Rootkits could replace commands like &lt;code&gt;ls&lt;/code&gt; or &lt;code&gt;ps&lt;/code&gt; with their own infected versions that work normally without you noticing.&lt;/p&gt;&lt;p&gt;They can infect any operating system, including Linux.&lt;/p&gt;&lt;h2&gt;Key Considerations&lt;/h2&gt;&lt;p&gt;Rootkits can only be planted after gaining administrative access to the server.&lt;/p&gt;&lt;p&gt;That&apos;s why it&apos;s crucial to prevent bad actors from gaining access to your server in the first place and planting malware.&lt;/p&gt;&lt;p&gt;Following &lt;a href=&quot;https://ivansalloum.com/preparing-your-ubuntu-server-for-first-use/&quot;&gt;essential security measures&lt;/a&gt; such as adding a non-root user and disabling root user access, &lt;a href=&quot;https://ivansalloum.com/setting-up-a-firewall-using-ufw-an-in-depth-guide/&quot;&gt;implementing a firewall&lt;/a&gt;, &lt;a href=&quot;https://ivansalloum.com/securing-ssh-essential-steps-for-linux-servers/&quot;&gt;securing SSH&lt;/a&gt;, and more could prevent rootkits from being installed on your server without the need for detection software.&lt;/p&gt;&lt;p&gt;Now, the last crucial point is that there is no software that is truly effective at detecting all rootkits. Rootkit Hunter is a good option but cannot detect every type of them.&lt;/p&gt;&lt;h2&gt;Installing Rootkit Hunter&lt;/h2&gt;&lt;p&gt;To install Rootkit Hunter, use the following command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt install rkhunter
&lt;/pre&gt;&lt;p&gt;Once it is installed, the first thing to do is to run this command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo rkhunter --propupd
&lt;/pre&gt;&lt;p&gt;This &lt;a href=&quot;https://sourceforge.net/p/rkhunter/wiki/propupd/&quot;&gt;command&lt;/a&gt; is used to update the local database file with the current properties of system files.&lt;/p&gt;&lt;p&gt;Then we need to update the rootkit signatures but before doing this we need to adjust three things in its main configuration file.&lt;/p&gt;&lt;p&gt;Open the &lt;code&gt;/etc/rkhunter.conf&lt;/code&gt; file and locate these three variables:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;UPDATE_MIRRORS MIRRORS_MODE WEB_CMD
&lt;/pre&gt;&lt;p&gt;Change their values to look like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;UPDATE_MIRRORS=1 MIRRORS_MODE=0 WEB_CMD=&amp;quot;&amp;quot;
&lt;/pre&gt;&lt;p&gt;Now, use the following command to update the rootkit signatures:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo rkhunter --update
&lt;/pre&gt;&lt;p&gt;And that&apos;s it for installing Rootkit Hunter.&lt;/p&gt;&lt;h3&gt;Running a Scan&lt;/h3&gt;&lt;p&gt;After successfully installing and updating Rootkit Hunter, it is time for a scan.&lt;/p&gt;&lt;p&gt;Use the following command to scan for rootkits:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo rkhunter -c
&lt;/pre&gt;&lt;p&gt;The scan will take some time and will consume server resources, so ensure you have sufficient resources available.&lt;/p&gt;&lt;p&gt;When running a scan using only the &lt;code&gt;-c&lt;/code&gt; option, Rootkit Hunter will continue to prompt you to press the &lt;strong&gt;ENTER&lt;/strong&gt;  key to proceed with scanning.&lt;/p&gt;&lt;p&gt;Use the following command instead:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo rkhunter -c --cronjob --rwo
&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;--cronjob&lt;/code&gt; option will make the program run as a cron job, causing it to run the entire scan without asking you to press the &lt;strong&gt;ENTER&lt;/strong&gt;  key.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;--rwo&lt;/code&gt; option will instruct the program to only log warnings instead of logging everything.&lt;/p&gt;&lt;p&gt;You can also use the &lt;code&gt;--sk&lt;/code&gt; option to prevent the program from prompting you to press the &lt;strong&gt;ENTER&lt;/strong&gt;  key.&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;Great job reaching the end!&lt;/p&gt;&lt;p&gt;I hope this tutorial has been helpful for you in protecting your Linux server from rootkits.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;You can find the full collection of detailed Linux server security guides &lt;a href=&quot;https://ivansalloum.com/collections/linux-server-security/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;If you found value in this tutorial or have any questions or feedback, please don&apos;t hesitate to share your thoughts in the &lt;strong&gt;discussion&lt;/strong&gt; section.&lt;/p&gt;&lt;p&gt;Your input is greatly appreciated, and you can also &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;contact me&lt;/a&gt; directly if you prefer.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Security</category></item><item><title>How to Protect Linux Servers from Malware</title><link>https://ivansalloum.com/how-to-protect-linux-servers-from-malware/</link><guid isPermaLink="true">https://ivansalloum.com/how-to-protect-linux-servers-from-malware/</guid><description>Learn how to protect your Linux server from malware using the Maldet and ClamAV combo with this comprehensive guide.</description><pubDate>Thu, 29 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;Many believe Linux servers are immune to malware, but that&apos;s not entirely accurate.&lt;/p&gt;&lt;p&gt;While infecting Linux servers itself is tough, sharing files with Windows users or hosting CMSs like WordPress can open doors to malware.&lt;/p&gt;&lt;p&gt;In this comprehensive guide, I&apos;ll walk you through the steps to protect your Linux server from Malware using ClamAV (Clam AntiVirus) and Maldet (Linux Malware Detect).&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;_I assume you&apos;re working on a properly set-up Ubuntu server. If not, check out my guide  on &lt;em&gt;&lt;a href=&quot;https://ivansalloum.com/preparing-your-ubuntu-server-for-first-use/&quot;&gt;&lt;em&gt;preparing  Ubuntu servers&lt;/em&gt;&lt;/a&gt; _  to get started.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;Maldet &amp;amp; ClamAV Combo&lt;/h2&gt;&lt;p&gt;I always prefer to provide context before delving into technical details, so I&apos;ll start by explaining what these two software are and how they combine to form a powerful combo.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.clamav.net/&quot;&gt;ClamAV&lt;/a&gt; is a free, powerful and efficient open-source software as it excels in detecting, quarantining, and removing various types of malware.&lt;/p&gt;&lt;p&gt;It includes the &lt;code&gt;freshclam&lt;/code&gt; utility, which ensures regular updates to the ClamAV database. This database is continuously refreshed with the latest threats as they are discovered.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://www.rfxn.com/projects/linux-malware-detect/&quot;&gt;Maldet&lt;/a&gt; is similar to ClamAV but is specifically tailored for hosting environments.&lt;/p&gt;&lt;p&gt;It includes a live monitoring feature that works with the Linux kernel&apos;s &lt;code&gt;inotify&lt;/code&gt; capability, detecting changes in files and scanning them promptly.&lt;/p&gt;&lt;p&gt;One of its great features is automatic creation of malware detection signatures when it detects malware on intrusion detection systems at the network&apos;s edge.&lt;/p&gt;&lt;p&gt;Maldet updates both its malware signatures and the program itself on a daily basis.&lt;/p&gt;&lt;p&gt;Now, the most interesting part is why using both of them together.&lt;/p&gt;&lt;p&gt;The ClamAV scan engine performs better than Maldet. When Maldet sees that ClamAV is installed, it uses ClamAV&apos;s scan engine automatically, making things run smoother.&lt;/p&gt;&lt;p&gt;Together, Maldet monitors specified paths for malware using ClamAV&apos;s scan engine and uses both malware signature databases.&lt;/p&gt;&lt;p&gt;This combined approach enhances their effectiveness compared to using either alone.&lt;/p&gt;&lt;h2&gt;Key Considerations&lt;/h2&gt;&lt;p&gt;Before we start, it’s crucial to be aware that ClamAV and Maldet can be resource-intensive.&lt;/p&gt;&lt;p&gt;Make sure your server has enough CPU and memory to handle them smoothly. Not having sufficient resources could slow down your server and affect its performance.&lt;/p&gt;&lt;p&gt;For a smoother experience, aim for a server with at least 2 dedicated CPU cores and 8 GB of memory.&lt;/p&gt;&lt;p&gt;It&apos;s a good idea to test ClamAV and Maldet on a separate server first to see how much resources they need and plan accordingly.&lt;/p&gt;&lt;p&gt;I&apos;ve had no performance issues with them on a dedicated vCPU server from Hetzner. Their CPUs are powerful, and their memory is high-speed.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;New to Hetzner? &lt;a href=&quot;https://hetzner.cloud/?ref=MC4Yy318xX5X&quot;&gt;Use my link&lt;/a&gt; to get free credits!&lt;/p&gt;&lt;p&gt;Lastly, it&apos;s crucial to understand that these two software aren&apos;t malware cleaners.&lt;/p&gt;&lt;p&gt;They detect malware, can quarantine infected files, and warn you, but they don&apos;t clean the files themselves.&lt;/p&gt;&lt;p&gt;Maldet will attempt to clean the files, but it&apos;s not something you can always count on. You&apos;ll need to handle file cleaning yourself.&lt;/p&gt;&lt;p&gt;We&apos;ll discuss this in more detail later on.&lt;/p&gt;&lt;h2&gt;Installing ClamAV&lt;/h2&gt;&lt;p&gt;We won&apos;t interact much with ClamAV directly when using both together.&lt;/p&gt;&lt;p&gt;Maldet automatically uses the ClamAV scan engine, so all we need to do is install ClamAV without any additional configuration.&lt;/p&gt;&lt;p&gt;However, I still want to demonstrate how to use ClamAV because there might be situations where you&apos;ll need to use it alone.&lt;/p&gt;&lt;p&gt;So, it&apos;s useful to know more than just how to install it.&lt;/p&gt;&lt;p&gt;Use the following command for installation:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt install clamav
&lt;/pre&gt;&lt;p&gt;The ClamAV database is updated automatically during the installation.&lt;/p&gt;&lt;p&gt;This installation adds one new service: the &lt;code&gt;clamav-freshclam&lt;/code&gt; service, which is enabled and running by default.&lt;/p&gt;&lt;p&gt;As I mentioned before, this service regularly checks for database updates.&lt;/p&gt;&lt;p&gt;🙆‍♂️&lt;/p&gt;&lt;p&gt;If you&apos;re not interested in learning more about ClamAV, feel free to skip ahead to the Maldet section. Since you&apos;ve already installed ClamAV, you&apos;re all set.&lt;/p&gt;&lt;h3&gt;Running a Scan&lt;/h3&gt;&lt;p&gt;To run a scan, use the &lt;code&gt;clamscan&lt;/code&gt; command on the directory you want to scan.&lt;/p&gt;&lt;p&gt;I will scan my WordPress files for any malware or viruses:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo clamscan -r /var/www/webapps/wordpress/public_html
&lt;/pre&gt;&lt;p&gt;Output:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;\----------- SCAN SUMMARY ----------- Known viruses: 8680618 Engine version: 0.103.9 Scanned directories: 1151 Scanned files: 6323 Infected files: 0 Data scanned: 361.83 MB Data read: 145.92 MB (ratio 2.48:1) Time: 69.349 sec (1 m 9 s) Start Date: 2024:02:28 13:33:46 End Date: 2024:02:28 13:34:55
&lt;/pre&gt;&lt;p&gt;ClamAV scanned my WordPress files and found no malware.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;-r&lt;/code&gt; option stands for recursive scan. When you include this option in your command, ClamAV will not only scan the specified directory but also all of its subdirectories.&lt;/p&gt;&lt;p&gt;This is particularly useful when you want to thoroughly check an entire directory tree for malware, ensuring that no part of the directory structure is overlooked.&lt;/p&gt;&lt;h3&gt;Resource Insights&lt;/h3&gt;&lt;p&gt;You&apos;ll notice that when you scan with ClamAV, it uses about a one GB of memory just to get started. This happens because it loads the database into memory.&lt;/p&gt;&lt;p&gt;Now, rerun the scan but keep an eye on your resource usage.&lt;/p&gt;&lt;p&gt;After running the &lt;code&gt;clamscan&lt;/code&gt; command, you’ll notice a short wait before the scan begins, and the memory usage on your server will increase by around one GB or more.&lt;/p&gt;&lt;p&gt;ClamAV needs this one GB of free memory for the database, plus additional resources for the scanning process itself.&lt;/p&gt;&lt;p&gt;That&apos;s why, as I mentioned earlier, I recommend having a server with sufficient resources, or only scanning when you have enough resources available.&lt;/p&gt;&lt;p&gt;Some people suggest stopping the &lt;code&gt;clamav-freshclam&lt;/code&gt; service and preventing it from starting on reboot to save resources. They recommend manually updating the database only when needed for a scan.&lt;/p&gt;&lt;p&gt;However, I don’t completely agree with this approach.&lt;/p&gt;&lt;p&gt;In my experience, the &lt;code&gt;clamav-freshclam&lt;/code&gt; service, which checks for database updates every hour, doesn’t use a lot of resources and helps keep the database updated.&lt;/p&gt;&lt;p&gt;While manually updating is an option and just involves running a command before scanning, it’s not necessary.&lt;/p&gt;&lt;p&gt;If you still choose to stop and disable the service, you can use these two commands to do so:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo systemctl stop clamav-freshclam.service sudo systemctl disable clamav-freshclam.service
&lt;/pre&gt;&lt;p&gt;Now, you’ll need to manually update the database each time before you scan.&lt;/p&gt;&lt;p&gt;Use this command for manual updates:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo freshclam
&lt;/pre&gt;&lt;p&gt;❗&lt;/p&gt;&lt;p&gt;When using ClamAV and Maldet together, you shouldn&apos;t stop and disable the &lt;code&gt;clamav-freshclam&lt;/code&gt; service, as it&apos;s needed for the monitoring feature. We want to make sure the database is always updated.&lt;/p&gt;&lt;h3&gt;Scanning the Root Filesystem&lt;/h3&gt;&lt;p&gt;You may consider scanning the entire root filesystem &lt;code&gt;/&lt;/code&gt; for malware, but there are critical aspects to consider.&lt;/p&gt;&lt;p&gt;If you’re using ClamAV on a production server, run a scan only if absolutely necessary.&lt;/p&gt;&lt;p&gt;Certain directories like &lt;code&gt;/proc&lt;/code&gt;, &lt;code&gt;/sys&lt;/code&gt;, &lt;code&gt;/run&lt;/code&gt;, &lt;code&gt;/dev&lt;/code&gt;, and &lt;code&gt;/snap&lt;/code&gt; are special and scanning them may lead to errors. Therefore, it’s essential to exclude these directories from the scan.&lt;/p&gt;&lt;p&gt;Creating a custom directory for moving infected files is advisable, and this directory should also be excluded from the scan.&lt;/p&gt;&lt;p&gt;To begin, create a directory for quarantining infected files:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo mkdir /root/quarantine
&lt;/pre&gt;&lt;p&gt;I created this directory within the root home directory.&lt;/p&gt;&lt;p&gt;To scan the entire root filesystem while excluding special directories, use the following command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo clamscan -r --log=/var/log/clamav/clamscan.log --move=/root/quarantine --exclude=&apos;^/(proc|sys|run|dev|snap)/&apos; /
&lt;/pre&gt;&lt;p&gt;This command performs a recursive scan of the root filesystem, excluding the specified special directories.&lt;/p&gt;&lt;p&gt;It logs the scan results in the &lt;code&gt;clamscan.log&lt;/code&gt; file located in the &lt;code&gt;/var/log/clamav/&lt;/code&gt; directory and moves any detected malware to the quarantine directory.&lt;/p&gt;&lt;p&gt;The final recommendation is to avoid setting up automated scans with ClamAV. It’s better to run scans manually.&lt;/p&gt;&lt;p&gt;This approach is due to the unpredictable nature of scan results. There’s a risk that ClamAV might mistakenly identify a normal file as malware and move it, which could break your server.&lt;/p&gt;&lt;p&gt;Therefore, it’s crucial to always run scans manually and carefully examine the results to maintain the integrity of your server.&lt;/p&gt;&lt;p&gt;I&apos;ve never had to scan the entire root filesystem because the likelihood of infection is extremely low.&lt;/p&gt;&lt;p&gt;However, as mentioned earlier, sharing files with Windows users or hosting CMSs like WordPress can expose your server to malware.&lt;/p&gt;&lt;p&gt;Therefore, it&apos;s essential to monitor these areas closely, &lt;em&gt;and&lt;/em&gt; &lt;em&gt;that&lt;/em&gt; &apos;s &lt;em&gt;where Maldet comes into play.&lt;/em&gt;&lt;/p&gt;&lt;h2&gt;Installing Maldet&lt;/h2&gt;&lt;p&gt;After installing ClamAV, we can proceed to install Maldet and begin configuring it.&lt;/p&gt;&lt;p&gt;But, make sure to install it using the root user.&lt;/p&gt;&lt;p&gt;If you install it using sudo, the Maldet files will be owned by the user who performed the installation, rather than by the root user. Installing it as root avoids the need to locate and modify the ownership of those files later on.&lt;/p&gt;&lt;p&gt;Maldet isn&apos;t available in the repositories of any Linux distribution, but it&apos;s simple to install.&lt;/p&gt;&lt;p&gt;After accessing the server using root, use this command to download the Maldet files:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;wget http://www.rfxn.com/downloads/maldetect-current.tar.gz
&lt;/pre&gt;&lt;p&gt;Extract the archive:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;tar xzvf maldetect-current.tar.gz
&lt;/pre&gt;&lt;p&gt;Now, navigate into the resulting directory and execute the installer like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;./install.sh
&lt;/pre&gt;&lt;p&gt;Once Maldet is installed, copy the &lt;code&gt;README&lt;/code&gt; file to your home directory so you can examine it if needed. It contains the documentation for Maldet.&lt;/p&gt;&lt;p&gt;The installer automatically creates the symbolic link for the &lt;code&gt;maldet&lt;/code&gt; service, and it also automatically downloads and installs the latest malware signatures.&lt;/p&gt;&lt;p&gt;Now, you can access the server again using your sudo user.&lt;/p&gt;&lt;p&gt;There&apos;s one more thing we need to install, which is the &lt;code&gt;inotify-tools&lt;/code&gt; package. This package is necessary for the monitoring feature and is required for the &lt;code&gt;maldet&lt;/code&gt; service to work.&lt;/p&gt;&lt;p&gt;Use this command to install it:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt install inotify-tools
&lt;/pre&gt;&lt;p&gt;If we check the status of the &lt;code&gt;maldet&lt;/code&gt; service, we&apos;ll notice that it&apos;s not active by default.&lt;/p&gt;&lt;p&gt;We&apos;ll start it after we finish configuring Maldet.&lt;/p&gt;&lt;h3&gt;Configuring Maldet&lt;/h3&gt;&lt;p&gt;Open the &lt;code&gt;/usr/local/maldetect/conf.maldet&lt;/code&gt; file with your favorite editor.&lt;/p&gt;&lt;p&gt;You need to configure Maldet to alert you by email whenever it detects any malware, which is highly recommended.&lt;/p&gt;&lt;p&gt;Find the &lt;code&gt;email_alert&lt;/code&gt; and &lt;code&gt;email_addr&lt;/code&gt; variables and change them accordingly:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;email_alert=&amp;quot;1&amp;quot; email_addr=&amp;quot;hello@ivansalloum.com&amp;quot;
&lt;/pre&gt;&lt;p&gt;There&apos;s an option to ignore email alerts for malware that has been cleaned, but I recommend disabling it.&lt;/p&gt;&lt;p&gt;It&apos;s enabled by default, so ensure to disable it like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;email_ignore_clean=&amp;quot;0&amp;quot;
&lt;/pre&gt;&lt;p&gt;It is always better to get notified about everything happening.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;Your server needs to be configured to send emails, and this can be done by setting up Postfix for &lt;a href=&quot;https://ivansalloum.com/how-to-configure-postfix-for-external-smtp-relay/&quot;&gt;external SMTP relay&lt;/a&gt; to ensure you receive alerts.&lt;/p&gt;&lt;p&gt;If you scroll down further, you&apos;ll notice the &lt;code&gt;autoupdate_signatures&lt;/code&gt; and &lt;code&gt;autoupdate_version&lt;/code&gt; variables, which are enabled by default and prompt Maldet to check for updates every day.&lt;/p&gt;&lt;p&gt;Now, we want Maldet to quarantine any infected files and attempt to clean them from any malware.&lt;/p&gt;&lt;p&gt;To do this, locate the &lt;code&gt;quarantine_hits&lt;/code&gt; and &lt;code&gt;quarantine_clean&lt;/code&gt; variables, and enable them:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;quarantine_hits=&amp;quot;1&amp;quot; quarantine_clean=&amp;quot;1&amp;quot;
&lt;/pre&gt;&lt;p&gt;There&apos;s an important consideration to keep in mind: quarantining infected files can potentially cause issues.&lt;/p&gt;&lt;p&gt;For instance, if you&apos;re hosting WordPress and Maldet moves an infected file to the &lt;code&gt;/usr/local/maldetect/quarantine&lt;/code&gt; directory, your site may stop working.&lt;/p&gt;&lt;p&gt;Therefore, you must decide between letting Maldet quarantine and attempt to clean the infected files, and then moving them back to the WordPress directory once you&apos;re certain they&apos;re safe, or not allowing Maldet to quarantine and clean infected files, and instead immediately cleaning them yourself after receiving an alert.&lt;/p&gt;&lt;p&gt;It&apos;s up to you.&lt;/p&gt;&lt;p&gt;Finally, we need to configure Maldet to monitor files in paths we specify.&lt;/p&gt;&lt;p&gt;By default, Maldet scans only files within the &lt;code&gt;/dev/shm/&lt;/code&gt;, &lt;code&gt;/var/tmp/&lt;/code&gt;, and &lt;code&gt;/tmp/&lt;/code&gt; directories.&lt;/p&gt;&lt;p&gt;To change this, locate the following two lines:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;default_monitor_mode=&amp;quot;users&amp;quot; # default_monitor_mode=&amp;quot;/usr/local/maldetect/monitor_paths&amp;quot;
&lt;/pre&gt;&lt;p&gt;Comment out the first line and uncomment the second line, like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;# default_monitor_mode=&amp;quot;users&amp;quot; default_monitor_mode=&amp;quot;/usr/local/maldetect/monitor_paths&amp;quot;
&lt;/pre&gt;&lt;p&gt;And that&apos;s it for the main configuration file. Save and close it.&lt;/p&gt;&lt;p&gt;Now, the final step before starting the &lt;code&gt;maldet&lt;/code&gt; service is to specify the paths we want to monitor.&lt;/p&gt;&lt;p&gt;Open the &lt;code&gt;/usr/local/maldetect/monitor_paths&lt;/code&gt; file with your favorite editor and add the directories you want to monitor, like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;/home /root /var/tmp /tmp
&lt;/pre&gt;&lt;p&gt;After this, it is time to start the &lt;code&gt;maldet&lt;/code&gt; service:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo systemctl start maldet
&lt;/pre&gt;&lt;p&gt;If you check the Maldet logs using the &lt;code&gt;sudo maldet --log&lt;/code&gt; command, you will see this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;maldet(33584): {mon} set inotify max_user_watches to 16384 maldet(33584): {mon} added /var/tmp to inotify monitoring array maldet(33584): {mon} added /tmp to inotify monitoring array maldet(33584): {mon} added /home to inotify monitoring array maldet(33584): {mon} added /root to inotify monitoring array maldet(33584): {mon} added /var/www to inotify monitoring array maldet(33584): {mon} starting inotify process on 5 paths, this might take awhile... maldet(33584): {mon} inotify startup successful (pid: 33716) maldet(33584): {mon} inotify monitoring log: /usr/local/maldetect/logs/inotify_log
&lt;/pre&gt;&lt;p&gt;This output is sourced from the &lt;code&gt;/usr/local/maldetect/logs/event_log&lt;/code&gt; file.&lt;/p&gt;&lt;p&gt;As you can see, Maldet has added the specified paths to the monitoring array, and the monitoring feature is now enabled.&lt;/p&gt;&lt;p&gt;You can also check the &lt;code&gt;/usr/local/maldetect/logs/inotify_log&lt;/code&gt; file, as it contains useful information about the changes occurring to the files.&lt;/p&gt;&lt;h3&gt;The ClamAV Daemon Warning&lt;/h3&gt;&lt;p&gt;If you examine the Maldet logs again, you&apos;ll notice this warning:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;maldet(33584): {mon} warning clamd service not running; force-set monitor mode file scanning to every 120s
&lt;/pre&gt;&lt;p&gt;This happens because we only installed the &lt;code&gt;clamav&lt;/code&gt; package.&lt;/p&gt;&lt;p&gt;If we don&apos;t install the &lt;code&gt;clamav-daemon&lt;/code&gt; package, you will always see this warning.&lt;/p&gt;&lt;p&gt;Let me explain why.&lt;/p&gt;&lt;p&gt;When you only install the &lt;code&gt;clamav&lt;/code&gt; package, you&apos;ll have access to the &lt;code&gt;clamav-freshclam&lt;/code&gt; service for updates and the &lt;code&gt;clamscan&lt;/code&gt; utility for scans.&lt;/p&gt;&lt;p&gt;As explained in the &lt;strong&gt;Resource Insights&lt;/strong&gt;  section on ClamAV, you know that ClamAV requires a certain amount of RAM to load the database before starting a scan, and it does this each time you run a scan.&lt;/p&gt;&lt;p&gt;However, if you install the &lt;code&gt;clamav-daemon&lt;/code&gt; package, you&apos;ll also have access to the &lt;code&gt;clamav-daemon&lt;/code&gt; service. When active, this service keeps the database constantly loaded in memory.&lt;/p&gt;&lt;p&gt;As a result, you&apos;ll notice approximately one GB of RAM consistently in use because the database remains in memory.&lt;/p&gt;&lt;p&gt;This setup ensures that ClamAV is always ready to run scans, as the &lt;code&gt;clamav-freshclam&lt;/code&gt; service continuously updates the database and the &lt;code&gt;clamav-daemon&lt;/code&gt; service keeps it loaded.&lt;/p&gt;&lt;p&gt;Without the &lt;code&gt;clamav-daemon&lt;/code&gt; service detected, Maldet won&apos;t monitor specified paths in real-time. Instead, it checks them every 120 seconds.&lt;/p&gt;&lt;p&gt;This is because Maldet needs ClamAV to load the database into memory before it can monitor files.&lt;/p&gt;&lt;p&gt;Installing the &lt;code&gt;clamav-daemon&lt;/code&gt; package resolves this issue, and Maldet will monitor files every second.&lt;/p&gt;&lt;p&gt;🙆‍♂️&lt;/p&gt;&lt;p&gt;I wanted to address this now because I encountered this problem myself, and after some investigation, I found the solution. I wanted you to see the warning beforehand so you could understand the situation.&lt;/p&gt;&lt;p&gt;Now, the only remaining step is to install the &lt;code&gt;clamav-daemon&lt;/code&gt; package and ensure the service remains active.&lt;/p&gt;&lt;p&gt;Use this command for installation:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt install clamav-daemon
&lt;/pre&gt;&lt;p&gt;If you check the status of the &lt;code&gt;clamav-daemon&lt;/code&gt; service, you&apos;ll notice that it&apos;s active by default.&lt;/p&gt;&lt;p&gt;If you run the &lt;code&gt;htop&lt;/code&gt; command, you&apos;ll see that approximately one GB of RAM is being used because the database is loaded into memory.&lt;/p&gt;&lt;p&gt;Now, restart the &lt;code&gt;maldet&lt;/code&gt; service:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo systemctl restart maldet
&lt;/pre&gt;&lt;p&gt;Check the logs again, and you won&apos;t see the warning anymore.&lt;/p&gt;&lt;p&gt;Now, Maldet is able to monitor in real-time.&lt;/p&gt;&lt;h2&gt;Final Testing&lt;/h2&gt;&lt;p&gt;Maldet is active and monitoring files, eliminating the need for manual scans.&lt;/p&gt;&lt;p&gt;Since Maldet uses the ClamAV scan engine, there&apos;s no need to directly use ClamAV.&lt;/p&gt;&lt;p&gt;Additionally, Maldet creates a file within the &lt;code&gt;/etc/cron.daily/&lt;/code&gt; directory, which conducts a daily scan of standard web directories.&lt;/p&gt;&lt;p&gt;This daily scan is controlled by the &lt;code&gt;cron_daily_scan&lt;/code&gt; variable in its main configuration file, which you can disable if desired.&lt;/p&gt;&lt;p&gt;To view what Maldet will scan daily, open the &lt;code&gt;/etc/cron.daily/maldet&lt;/code&gt; file and review its contents.&lt;/p&gt;&lt;p&gt;Now, let us test our setup by downloading a simulated virus file from the &lt;strong&gt;European Institute for Computer Antivirus Research&lt;/strong&gt;  (EICAR) site.&lt;/p&gt;&lt;p&gt;Download one or all of the EICAR test files inside a directory Maldet is monitoring:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;wget https://secure.eicar.org/eicar.com wget https://secure.eicar.org/eicar.com.txt wget https://secure.eicar.org/eicar_com.zip wget https://secure.eicar.org/eicarcom2.zip
&lt;/pre&gt;&lt;p&gt;After a few seconds, you&apos;ll notice that the file is gone and has been moved to the &lt;code&gt;/usr/local/maldetect/quarantine&lt;/code&gt; directory if you enabled Maldet to quarantine infected files.&lt;/p&gt;&lt;p&gt;Additionally, you should receive an alert by email.&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;Great job reaching the end!&lt;/p&gt;&lt;p&gt;I hope this guide has been incredibly helpful in showing you how to protect your Linux server from malware.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;You can find the full collection of detailed Linux server security guides &lt;a href=&quot;https://ivansalloum.com/collections/linux-server-security/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;If you found value in this guide or have any questions or feedback, please don&apos;t hesitate to share your thoughts in the &lt;strong&gt;discussion&lt;/strong&gt; section.&lt;/p&gt;&lt;p&gt;Your input is greatly appreciated, and you can also &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;contact me&lt;/a&gt; directly if you prefer.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Security</category></item><item><title>Kernel Hardening: Securing Your Linux Server</title><link>https://ivansalloum.com/kernel-hardening-securing-your-linux-server/</link><guid isPermaLink="true">https://ivansalloum.com/kernel-hardening-securing-your-linux-server/</guid><description>Learn how to improve Linux server security through kernel hardening, protecting against network attacks and information leaks.</description><pubDate>Fri, 23 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;While the Linux kernel is fairly secure by default, there are steps we can take to make it even safer.&lt;/p&gt;&lt;p&gt;Kernel hardening can reduce the risk of certain network attacks and information leaks, making it harder for attackers to plan their attacks.&lt;/p&gt;&lt;p&gt;This guide will walk you through adjusting key kernel parameters to strengthen the security of your Linux server.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;_I assume you&apos;re working on a properly set-up Ubuntu server. If not, check out my guide  on &lt;em&gt;&lt;a href=&quot;https://ivansalloum.com/preparing-your-ubuntu-server-for-first-use/&quot;&gt;&lt;em&gt;preparing  Ubuntu servers&lt;/em&gt;&lt;/a&gt; _  to get started.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;Author&apos;s Note&lt;/h2&gt;&lt;p&gt;When you search the internet for guides on kernel hardening, you&apos;ll come across various opinions.&lt;/p&gt;&lt;p&gt;I will not try to claim to have the correct information. Instead, I&apos;ll share how I harden the Linux kernel on all my servers and my experience with it.&lt;/p&gt;&lt;p&gt;Leaving the default kernel parameters unchanged usually isn&apos;t a problem and may not cause security issues. Tweaking some parameters just enhances your server&apos;s security.&lt;/p&gt;&lt;p&gt;However, this might cause problems with what you run on your server if they rely on specific values. That&apos;s why I suggest testing these tweaks in a separate environment before applying them to your main server.&lt;/p&gt;&lt;p&gt;Lastly, I just want to mention that I&apos;ve never encountered any problems with the tweaks I&apos;ll be covering on all my Linux servers.&lt;/p&gt;&lt;h2&gt;The &lt;code&gt;/proc&lt;/code&gt; Directory&lt;/h2&gt;&lt;p&gt;Before we dive into adjusting kernel parameters, let me first explain the &lt;code&gt;/proc&lt;/code&gt; directory so you can understand where these parameters come from.&lt;/p&gt;&lt;p&gt;If you look inside the &lt;code&gt;/proc&lt;/code&gt; directory, you&apos;ll find a bunch of numbered directories, and some regular files and directories.&lt;/p&gt;&lt;p&gt;The numbered directories correspond to process IDs (PIDs) of running processes.&lt;/p&gt;&lt;p&gt;Inside a numbered directory, you&apos;ll find files and subdirectories filled with details about the running process. However, not all files within these directories are easily understandable unless you&apos;re familiar with OS programming.&lt;/p&gt;&lt;p&gt;Instead, we rely on commands like &lt;code&gt;ps&lt;/code&gt; and &lt;code&gt;top&lt;/code&gt; which extract process information from the &lt;code&gt;/proc&lt;/code&gt; directory and present it in an easy-to-read format.&lt;/p&gt;&lt;p&gt;The remaining files and directories with regular names contain details about the kernel&apos;s activity. For example, the &lt;code&gt;sys&lt;/code&gt; directory contains the kernel parameters that can be adjusted to harden the kernel.&lt;/p&gt;&lt;p&gt;If you enter the &lt;code&gt;/proc/sys/net/ipv4/&lt;/code&gt; directory, you&apos;ll find various parameters related to IPv4 networking. Each file in this directory represents a parameter and holds its value.&lt;/p&gt;&lt;p&gt;Now that we know the basics of the &lt;code&gt;/proc&lt;/code&gt; directory, it&apos;s time to begin hardening the Linux kernel.&lt;/p&gt;&lt;h2&gt;The &lt;code&gt;sysctl&lt;/code&gt; Utility&lt;/h2&gt;&lt;p&gt;The old method of changing parameter values involves echoing the new value to the parameter&apos;s file.&lt;/p&gt;&lt;p&gt;This approach is outdated and doesn&apos;t work with sudo, thus requiring the use of the &lt;code&gt;bash -c&lt;/code&gt; command to enforce execution, as illustrated by the following example:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo bash -c &amp;quot;echo &apos;1&apos; &amp;gt; /proc/sys/net/ipv4/icmp_echo_ignore_all&amp;quot;
&lt;/pre&gt;&lt;p&gt;However, this isn&apos;t the recommended approach. Instead, we should use the &lt;code&gt;sysctl&lt;/code&gt; utility, which offers a more modern solution.&lt;/p&gt;&lt;p&gt;The following command will list all parameters along with their values:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo sysctl -a
&lt;/pre&gt;&lt;p&gt;Additionally, you can also check a value for a certain parameter like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo sysctl net.ipv4.icmp_echo_ignore_all
&lt;/pre&gt;&lt;p&gt;You can use these two commands to verify if the new values have taken effect after you make changes.&lt;/p&gt;&lt;p&gt;If you want to change a parameter&apos;s value temporarily, you could use the &lt;code&gt;-w&lt;/code&gt; option to set the new value like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo sysctl -w net.ipv4.icmp_echo_ignore_all=1
&lt;/pre&gt;&lt;p&gt;It will last until you reboot the server.&lt;/p&gt;&lt;p&gt;Sometimes this is useful, but for the purpose of this guide, we want changes to be permanent.&lt;/p&gt;&lt;h2&gt;Hardening the Linux Kernel&lt;/h2&gt;&lt;p&gt;In the following, we&apos;ll tweak kernel parameters by adding them to the end of the &lt;code&gt;/etc/sysctl.conf&lt;/code&gt; file along with their new values, ensuring that the changes become permanent.&lt;/p&gt;&lt;p&gt;After adding them, simply reboot the server for the new values to take effect.&lt;/p&gt;&lt;h3&gt;Reverse Path Filtering&lt;/h3&gt;&lt;p&gt;A spoofing attack occurs when an attacker pretends to be someone else or a trustworthy entity.&lt;/p&gt;&lt;p&gt;This can occur at various levels, including the network level, such as IP address spoofing, where the attacker sends network packets with spoofed IP addresses.&lt;/p&gt;&lt;p&gt;Such tactics could be used for DoS attacks or to trick access controls.&lt;/p&gt;&lt;p&gt;These two parameters that help us prevent spoofing attacks:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;net.ipv4.conf.default.rp_filter = 1 net.ipv4.conf.all.rp_filter = 1
&lt;/pre&gt;&lt;p&gt;They control reverse path filtering (RP filter), a security feature that helps protect against IP address spoofing. When enabled, the server checks if it can reach the source address of incoming packets. If it can’t, the packets are dropped.&lt;/p&gt;&lt;p&gt;By default, these parameters are set to &lt;code&gt;2&lt;/code&gt;, which enables them in loose mode. Setting them to &lt;code&gt;1&lt;/code&gt; enforces strict mode for reverse path filtering, providing better security.&lt;/p&gt;&lt;h3&gt;SYN Flood Protection&lt;/h3&gt;&lt;p&gt;One form of DoS attacks involves sending a huge number of SYN packets to a server without completing the &lt;a href=&quot;https://ivansalloum.com/understanding-tcp-and-the-three-way-handshake/&quot;&gt;three-way handshake&lt;/a&gt;, also known as SYN flood attacks.&lt;/p&gt;&lt;p&gt;This results in our server having numerous half-open connections, consuming significant resources and preventing it from accepting new legitimate connections.&lt;/p&gt;&lt;p&gt;In severe cases, it can cause the server to crash, leaving it unresponsive and inaccessible.&lt;/p&gt;&lt;p&gt;TCP SYN cookies help mitigate SYN flood attacks by managing half-open connections without consuming too many server resources.&lt;/p&gt;&lt;p&gt;The following parameter enables the use of TCP SYN cookies:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;net.ipv4.tcp_syncookies = 1
&lt;/pre&gt;&lt;p&gt;While it doesn’t provide much protection, I still recommend keeping it enabled.&lt;/p&gt;&lt;p&gt;There are two additional parameters we can add to optimize how our server handles new connections:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;net.ipv4.tcp_max_syn_backlog = 4096 net.ipv4.tcp_synack_retries = 3
&lt;/pre&gt;&lt;p&gt;The first parameter controls the size of the queue for incoming SYN requests. By default, it’s set to &lt;code&gt;256&lt;/code&gt; on most systems, which is generally too low. Increasing the backlog allows the server to handle more half-open connections, helping it absorb a higher volume of incoming requests.&lt;/p&gt;&lt;p&gt;For most systems, a range between &lt;code&gt;4096&lt;/code&gt; and &lt;code&gt;16384&lt;/code&gt; is recommended unless the system has substantial memory resources.&lt;/p&gt;&lt;p&gt;The second parameter determines how many times the server will resend the SYN-ACK packet if no ACK response is received from the client, as happens in a SYN flood attack.&lt;/p&gt;&lt;p&gt;By default, it’s set to &lt;code&gt;5&lt;/code&gt;, meaning the server waits longer and keeps resources tied up on unresponsive clients. For better SYN flood resistance, reducing this value to &lt;code&gt;2&lt;/code&gt; or &lt;code&gt;3&lt;/code&gt; helps free up resources more quickly by limiting the number of retry attempts.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;Check out my guide on &lt;a href=&quot;https://ivansalloum.com/preventing-syn-flood-attacks-on-your-linux-server/&quot;&gt;preventing SYN flood&lt;/a&gt; attacks using firewall rules and Fail2ban.&lt;/p&gt;&lt;h3&gt;Disable ICMP Redirects&lt;/h3&gt;&lt;p&gt;ICMP redirects are messages sent by a router to inform the server that it should use a different, better route for reaching a specific destination.&lt;/p&gt;&lt;p&gt;For example, if the server sends traffic to a router, and the router knows of a more efficient path to the destination, it can send an ICMP redirect message to the server to update its routing table.&lt;/p&gt;&lt;p&gt;However, ICMP packets, including ICMP redirects, are extremely easy to fake, and it would be rather simple for an attacker to forge ICMP redirect packets. This makes it a potential security risk, as attackers could use fake redirects to mislead the server into sending traffic through malicious or incorrect routes.&lt;/p&gt;&lt;p&gt;These eight parameters will disable the sending or accepting of ICMP redirects:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;net.ipv4.conf.all.accept_redirects = 0 net.ipv6.conf.all.accept_redirects = 0 net.ipv4.conf.default.accept_redirects = 0 net.ipv6.conf.default.accept_redirects = 0 net.ipv4.conf.all.secure_redirects = 0 net.ipv6.conf.all.accept_redirects = 0 net.ipv4.conf.all.send_redirects = 0 net.ipv4.conf.default.send_redirects = 0
&lt;/pre&gt;&lt;p&gt;In most secure environments, ICMP redirects should be disabled, especially on systems not acting as routers.&lt;/p&gt;&lt;h3&gt;Disable Source Routing&lt;/h3&gt;&lt;p&gt;Source routing is a feature where the sender of a packet specifies the route that the packet should take through the network, rather than allowing routers to decide the best path.&lt;/p&gt;&lt;p&gt;This can be useful for specific use cases but can also pose significant security risks, as attackers can exploit it to send packets along a malicious or unexpected path. It can also be used to bypass security measures.&lt;/p&gt;&lt;p&gt;These four parameters disable the acceptance of source routes packets:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;net.ipv4.conf.default.accept_source_route = 0 net.ipv6.conf.default.accept_source_route = 0 net.ipv4.conf.all.accept_source_route = 0 net.ipv6.conf.all.accept_source_route = 0
&lt;/pre&gt;&lt;p&gt;Disabling source routing is a common security practice, particularly on servers where network traffic should follow trusted paths.&lt;/p&gt;&lt;h3&gt;Disable Packet Forwarding&lt;/h3&gt;&lt;p&gt;Packet forwarding enables a system to transmit network packets from one network interface to another.&lt;/p&gt;&lt;p&gt;Unless your server is functioning as a router or VPN, packet forwarding should be disabled.&lt;/p&gt;&lt;p&gt;These parameters disable packet forwarding:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;net.ipv4.ip_forward = 0 net.ipv4.conf.all.forwarding = 0 net.ipv6.conf.all.forwarding = 0 net.ipv4.conf.default.forwarding = 0 net.ipv6.conf.default.forwarding = 0
&lt;/pre&gt;&lt;p&gt;Disabling packet forwarding helps prevent your server from being used as a gateway for unauthorized network traffic.&lt;/p&gt;&lt;h3&gt;Protect TCP Connections (TIME-WAIT State)&lt;/h3&gt;&lt;p&gt;When a TCP connection is closed, it goes into a &lt;strong&gt;TIME-WAIT&lt;/strong&gt;  state to prevent old or duplicate packets from interfering with new connections.&lt;/p&gt;&lt;p&gt;This parameter helps protect against &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc1337&quot;&gt;TIME-WAIT Assassination&lt;/a&gt; or TCP TIME-WAIT attacks:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;net.ipv4.tcp_rfc1337 = 1
&lt;/pre&gt;&lt;p&gt;When enabled, the server follows a safer behavior for closing TCP connections, ensuring that connections in the TIME-WAIT state cannot be hijacked.&lt;/p&gt;&lt;p&gt;This prevents potential data loss or connection issues caused by attackers exploiting this vulnerability.&lt;/p&gt;&lt;p&gt;However, please note that the kernel documentation has confused some people about this parameter, and there is an ongoing debate about whether one should enable it or not.&lt;/p&gt;&lt;p&gt;Therefore, it&apos;s advisable to test this parameter in a testing environment first.&lt;/p&gt;&lt;h3&gt;Harden the BPF JIT Compiler&lt;/h3&gt;&lt;p&gt;Berkeley Packet Filter (BPF) is a framework in the Linux kernel used for network packet filtering, tracing, and other performance monitoring tasks.&lt;/p&gt;&lt;p&gt;The Just-In-Time (JIT) compiler improves the performance of BPF programs by compiling them into machine code at runtime, instead of interpreting them line-by-line.&lt;/p&gt;&lt;p&gt;This parameter enables additional hardening features for the BPF JIT compiler:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;net.core.bpf_jit_harden = 2
&lt;/pre&gt;&lt;p&gt;When enabled, it mitigates &lt;strong&gt;JIT Spraying&lt;/strong&gt; attacks by obfuscating sensitive constants in BPF programs, such as fixed values or memory addresses.&lt;/p&gt;&lt;p&gt;JIT Spraying is an attack where hackers manipulate the JIT compiler to inject malicious code into memory, allowing them to control machine code execution.&lt;/p&gt;&lt;p&gt;By obfuscating these constants, it makes them harder for attackers to predict or exploit.&lt;/p&gt;&lt;p&gt;Next, add this parameter:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;kernel.unprivileged_bpf_disabled = 1
&lt;/pre&gt;&lt;p&gt;It prevents unauthorized users from loading and using BPF programs and restricts the BPF JIT compiler to root-only access.&lt;/p&gt;&lt;h3&gt;Restrict Core Dumps&lt;/h3&gt;&lt;p&gt;A core dump is a file the Linux kernel creates when a program crashes. It shows what was in the program&apos;s memory, registers, and call stack at the time of the crash.&lt;/p&gt;&lt;p&gt;While useful for debugging, core dumps can expose sensitive data like passwords or encryption keys.&lt;/p&gt;&lt;p&gt;By default, core dumps are saved in &lt;code&gt;/var/lib/systemd/coredump&lt;/code&gt; and compressed with &lt;code&gt;zstd&lt;/code&gt;. They’re triggered by errors like segmentation faults or illegal instructions and can be analyzed using tools like &lt;code&gt;gdb&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;However, core dumps are a security risk, especially for programs with elevated privileges. If an attacker gets access, they might extract sensitive information.&lt;/p&gt;&lt;p&gt;That’s why it’s important to disable core dumps unless absolutely needed for debugging (and honestly, I’ve never needed them).&lt;/p&gt;&lt;p&gt;Two parameters can help:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;kernel.core_pattern = |/bin/false fs.suid_dumpable = 0
&lt;/pre&gt;&lt;p&gt;&lt;code&gt;kernel.core_pattern = |/bin/false&lt;/code&gt; redirects any attempt to create a core dump to the &lt;code&gt;false&lt;/code&gt; command, effectively discarding it. While it prevents core dumps globally, some privileged processes might still bypass this restriction.&lt;/p&gt;&lt;p&gt;Adding &lt;code&gt;fs.suid_dumpable = 0&lt;/code&gt; ensures that no core dumps are created for processes with elevated privileges, adding extra protection.&lt;/p&gt;&lt;h3&gt;Disable Magic Keys&lt;/h3&gt;&lt;p&gt;Magic Keys are special key combinations that allow you to perform debugging and system control actions, even when the server becomes unresponsive due to issues like a kernel panic.&lt;/p&gt;&lt;p&gt;To disable Magic Keys, add this parameter:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;kernel.sysrq = 0
&lt;/pre&gt;&lt;p&gt;Disabling Magic Keys prevents potential misuse or exploitation of these debugging features.&lt;/p&gt;&lt;h3&gt;Restrict Access to Kernel Logs&lt;/h3&gt;&lt;p&gt;By default, any user on the server can run the &lt;code&gt;dmesg&lt;/code&gt; command to view kernel logs, which may contain sensitive information.&lt;/p&gt;&lt;p&gt;To restrict access to this command, add the following parameter:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;kernel.dmesg_restrict = 1
&lt;/pre&gt;&lt;p&gt;This ensures that only users with root privileges can view kernel logs.&lt;/p&gt;&lt;h3&gt;Restrict &lt;code&gt;ptrace&lt;/code&gt; Access&lt;/h3&gt;&lt;p&gt;The &lt;code&gt;ptrace()&lt;/code&gt; system call allows one process (tracer) to observe and control the execution of another (tracee).&lt;/p&gt;&lt;p&gt;This functionality can be exploited by attackers if an application is compromised, allowing them to attach to and manipulate other processes, such as SSH sessions.&lt;/p&gt;&lt;p&gt;To mitigate this risk, the &lt;code&gt;kernel.yama.ptrace_scope&lt;/code&gt; parameter can be set to restrict &lt;code&gt;ptrace&lt;/code&gt; usage. Here’s what each value means:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;**0**&lt;/code&gt;: Classic &lt;code&gt;ptrace&lt;/code&gt; permissions – a process can attach to any other process running under the same user.&lt;/li&gt;&lt;li&gt;&lt;code&gt;**1**&lt;/code&gt;: Restricted &lt;code&gt;ptrace&lt;/code&gt; – processes can only attach to their descendants, or specific allowed processes declared by &lt;code&gt;prctl&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;&lt;code&gt;**2**&lt;/code&gt;: Admin-only attach – only processes with &lt;code&gt;CAP_SYS_PTRACE&lt;/code&gt; privileges can use &lt;code&gt;ptrace&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;&lt;code&gt;**3**&lt;/code&gt;: No attach – no process can use &lt;code&gt;ptrace&lt;/code&gt; to attach to another process.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;For full security, setting this parameter to &lt;code&gt;3&lt;/code&gt; is recommended, as it completely prevents any process from using &lt;code&gt;ptrace&lt;/code&gt; to attach to others.&lt;/p&gt;&lt;p&gt;For most Linux servers, this is a good precaution unless &lt;code&gt;ptrace&lt;/code&gt; is explicitly needed for debugging or administration.&lt;/p&gt;&lt;p&gt;Use the following parameter to disable &lt;code&gt;ptrace&lt;/code&gt; unless required:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;kernel.yama.ptrace_scope = 3
&lt;/pre&gt;&lt;p&gt;This ensures that no process can manipulate or inspect another process.&lt;/p&gt;&lt;h3&gt;Restrict User Namespaces&lt;/h3&gt;&lt;p&gt;User namespaces are a kernel feature that enhances security by creating separate sandboxes for different users.&lt;/p&gt;&lt;p&gt;While available for unprivileged users, this feature could expose the kernel to privilege escalation.&lt;/p&gt;&lt;p&gt;This parameter restricts this feature to users with root privileges only:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;kernel.unprivileged_userns_clone = 0
&lt;/pre&gt;&lt;p&gt;This prevents unprivileged users from creating user namespaces.&lt;/p&gt;&lt;h3&gt;Control Swapping&lt;/h3&gt;&lt;p&gt;Similar to core dumps, swapping involves copying parts of the memory to the disk, potentially containing sensitive information.&lt;/p&gt;&lt;p&gt;To reduce this risk, we can instruct the kernel to swap only when absolutely necessary by setting the following parameter:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;vm.swappiness = 1
&lt;/pre&gt;&lt;p&gt;This ensures that the server will only swap data to disk when there are no other options.&lt;/p&gt;&lt;h3&gt;File Creation Restrictions&lt;/h3&gt;&lt;p&gt;To enhance security, we can restrict the creation of certain types of files in potentially insecure directories.&lt;/p&gt;&lt;p&gt;First, we prevent the creation of FIFOs (First In, First Out, also known as named pipes) and regular files in risky locations by adding the following parameters:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;fs.protected_regular = 2 fs.protected_fifos = 2
&lt;/pre&gt;&lt;p&gt;These help reduce the risk of attackers exploiting these file types in insecure directories.&lt;/p&gt;&lt;p&gt;Next, we protect hard links and symbolic links (symlinks) by adding the following parameters:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;fs.protected_hardlinks = 1 fs.protected_symlinks = 1
&lt;/pre&gt;&lt;p&gt;The first parameter ensures that users without proper access cannot create hard links to files, while the second ensures symlinks are handled safely, helping to prevent TOCTOU (time-of-check, time-of-use) race conditions.&lt;/p&gt;&lt;p&gt;Together, these add an extra layer of protection against certain file system vulnerabilities.&lt;/p&gt;&lt;h3&gt;Address Space Layout Randomization (ASLR)&lt;/h3&gt;&lt;p&gt;Address Space Layout Randomization (ASLR) is a security technique that randomizes the memory layout of key areas in a process&apos;s address space.&lt;/p&gt;&lt;p&gt;This makes it difficult for attackers to predict the location of critical data areas, such as buffers, heap, and stack, which are commonly targeted in memory-based exploits.&lt;/p&gt;&lt;p&gt;By randomly placing memory regions, ASLR makes it much harder for attackers to successfully execute memory corruption exploits. Even if an attacker can write to memory, they won’t be able to predict where to target, as the memory layout changes consistently.&lt;/p&gt;&lt;p&gt;To enable ASLR, add the following parameter:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;kernel.randomize_va_space = 2
&lt;/pre&gt;&lt;p&gt;This ensures that the kernel randomizes the memory layout of all processes.&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;Congratulations on reaching the end!&lt;/p&gt;&lt;p&gt;In this guide, you&apos;ve learned how to harden the Linux kernel to enhance your server&apos;s security.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;For more comprehensive Linux server security resources, be sure to check out the full collection of detailed guides &lt;a href=&quot;https://ivansalloum.com/collections/linux-server-security/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;If you found value in this guide or have any questions or feedback, please don&apos;t hesitate to share your thoughts in the &lt;strong&gt;discussion&lt;/strong&gt; section.&lt;/p&gt;&lt;p&gt;Your input is greatly appreciated, and you can also &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;contact me&lt;/a&gt; directly if you prefer.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Security</category></item><item><title>How to Keep Users&apos; Processes Private</title><link>https://ivansalloum.com/how-to-keep-users-processes-private/</link><guid isPermaLink="true">https://ivansalloum.com/how-to-keep-users-processes-private/</guid><description>Learn how to enhance your Linux server&apos;s security by preventing users from viewing each other&apos;s processes.</description><pubDate>Fri, 02 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;By default, any user on a Linux server can use commands like &lt;code&gt;top&lt;/code&gt; or &lt;code&gt;htop&lt;/code&gt; to view all running processes, including those owned by other users.&lt;/p&gt;&lt;p&gt;While this can be useful in certain situations, it can also pose security risks by exposing sensitive information to potential attackers.&lt;/p&gt;&lt;p&gt;This tutorial shows you how to restrict users from viewing processes owned by other users, thereby enhancing the security of your Linux server.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;_I assume you&apos;re working on a properly set-up Ubuntu server. If not, check out my guide  on &lt;em&gt;&lt;a href=&quot;https://ivansalloum.com/preparing-your-ubuntu-server-for-first-use/&quot;&gt;&lt;em&gt;preparing  Ubuntu servers&lt;/em&gt;&lt;/a&gt; _  to get started.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;Demonstration&lt;/h2&gt;&lt;p&gt;To demonstrate how any user could see processes running by other users, I added a new user named &lt;strong&gt;Elie&lt;/strong&gt;  and ran the &lt;code&gt;htop&lt;/code&gt; command.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://ivansalloum.com/content/images/2024/10/htop_output.webp&quot; alt=&quot;htop output&quot;&gt;&lt;/p&gt;&lt;p&gt;Even with their normal user privileges, Elie is able to see all processes running, including those owned by root and various system users.&lt;/p&gt;&lt;h2&gt;Keeping Users&apos; Processes Private&lt;/h2&gt;&lt;p&gt;To keep users&apos; processes private, you need to add the following line to the end of the &lt;code&gt;/etc/fstab&lt;/code&gt; file:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;proc /proc proc hidepid=2 0 0
&lt;/pre&gt;&lt;p&gt;Save and close the file.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;This solution doesn&apos;t work for RHEL 9-type distributions. Attempting it may break your server.&lt;/p&gt;&lt;p&gt;Then, remount &lt;code&gt;/proc&lt;/code&gt; like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo mount -o remount proc
&lt;/pre&gt;&lt;p&gt;Now, let me switch back to Elie and run the &lt;code&gt;htop&lt;/code&gt; command again.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://ivansalloum.com/content/images/2024/10/htop_output_after.webp&quot; alt=&quot;htop output after&quot;&gt;&lt;/p&gt;&lt;p&gt;As you can see, Elie is now only able to view their own running processes.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;hidepid&lt;/code&gt; option with a value of &lt;code&gt;2&lt;/code&gt; hides information about all running processes owned by other users, including the process directories in the &lt;code&gt;/proc&lt;/code&gt; directory.&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;Easy, isn&apos;t it?&lt;/p&gt;&lt;p&gt;I hope this tutorial was helpful for you in keeping your users&apos; running processes private.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;You can find the full collection of detailed Linux server security guides &lt;a href=&quot;https://ivansalloum.com/collections/linux-server-security/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;If you found value in this tutorial or have any questions or feedback, please don&apos;t hesitate to share your thoughts in the &lt;strong&gt;discussion&lt;/strong&gt; section.&lt;/p&gt;&lt;p&gt;Your input is greatly appreciated, and you can also &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;contact me&lt;/a&gt; directly if you prefer.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Security</category></item><item><title>How to Block Invalid Packets with UFW</title><link>https://ivansalloum.com/how-to-block-invalid-packets-with-ufw/</link><guid isPermaLink="true">https://ivansalloum.com/how-to-block-invalid-packets-with-ufw/</guid><description>Learn how to enhance your server&apos;s security by effectively blocking INVALID packets using UFW in this tutorial.</description><pubDate>Tue, 30 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;Hackers use tools to create TCP packets with unusual, weird flag combinations, known as &lt;strong&gt;Invalid  Packets&lt;/strong&gt;, capable of causing significant harm.&lt;/p&gt;&lt;p&gt;UFW blocks these invalid packets by default, but there are still instances where it may overlook and fail to block.&lt;/p&gt;&lt;p&gt;In this tutorial, I&apos;ll guide you through the most effective way to block these types of packets.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;_I assume you&apos;re working on a properly set-up Ubuntu server. If not, check out my guide  on &lt;em&gt;&lt;a href=&quot;https://ivansalloum.com/preparing-your-ubuntu-server-for-first-use/&quot;&gt;&lt;em&gt;preparing  Ubuntu servers&lt;/em&gt;&lt;/a&gt; _  to get started.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;What are Invalid Packets?&lt;/h2&gt;&lt;p&gt;Invalid packets include any incoming connections that don’t start with the SYN flag alone.&lt;/p&gt;&lt;p&gt;In a standard &lt;a href=&quot;https://ivansalloum.com/understanding-tcp-and-the-three-way-handshake/&quot;&gt;TCP Three-Way Handshake&lt;/a&gt;, every new connection begins with a SYN packet.&lt;/p&gt;&lt;p&gt;If a TCP connection starts with a different flag or an unusual combination of flags – like those generated by port-scanning tools such as &lt;strong&gt;Nmap&lt;/strong&gt;  – these packets should be blocked.&lt;/p&gt;&lt;h2&gt;How UFW Blocks Invalid Packets&lt;/h2&gt;&lt;p&gt;If you look at the &lt;code&gt;/etc/ufw/before.rules&lt;/code&gt; file, you&apos;ll notice these two rules:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;# drop INVALID packets (logs these in loglevel medium and higher) -A ufw-before-input -m conntrack --ctstate INVALID -j ufw-logging-deny -A ufw-before-input -m conntrack --ctstate INVALID -j DROP
&lt;/pre&gt;&lt;p&gt;These two rules are set to log and block any packets considered &lt;code&gt;INVALID&lt;/code&gt;, and they are added by default by UFW.&lt;/p&gt;&lt;p&gt;UFW uses the &lt;code&gt;conntrack&lt;/code&gt; module, short for connection tracking, to monitor connections associated with &lt;code&gt;INVALID&lt;/code&gt; connection states.&lt;/p&gt;&lt;p&gt;Let me demonstrate by using Nmap to run an XMAS scan from my MacBook against a server with UFW enabled, which has a rule allowing SSH traffic:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;nmap -sX 165.22.65.229
&lt;/pre&gt;&lt;p&gt;Output:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;Starting Nmap 7.80 ( https://nmap.org ) at 2024-01-30 11:14 UTC Nmap scan report for 165.22.65.229 Host is up (0.0016s latency). All 1000 scanned ports on 165.22.65.229 are open|filtered Nmap done: 1 IP address (1 host up) scanned in 27.22 seconds
&lt;/pre&gt;&lt;p&gt;By default, Nmap scans only the most commonly used 1,000 ports. The XMAS scan sends invalid packets containing the FIN, PSH, and URG flags.&lt;/p&gt;&lt;p&gt;As you can see, UFW successfully blocked this scan, resulting in Nmap being unable to determine that port 22 is open.&lt;/p&gt;&lt;p&gt;The output from Nmap that &lt;code&gt;All 1000 scanned ports on 165.22.65.229 are open|filtered&lt;/code&gt; suggests that the scan was unsuccessful.&lt;/p&gt;&lt;p&gt;But now, let&apos;s run a Window scan, which involves a significant number of ACK packets:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;nmap -sW 165.22.65.229
&lt;/pre&gt;&lt;p&gt;This should show that port 22 is open or filtered, meaning that UFW was unable to block this scan.&lt;/p&gt;&lt;h2&gt;Blocking Invalid Packets the Right Way&lt;/h2&gt;&lt;p&gt;To further enhance the security of our server, we could add two rules to log and block any new connections that don&apos;t have only the SYN flag set.&lt;/p&gt;&lt;p&gt;Add these two rules below the ones added by default by UFW:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;-A ufw-before-input -p tcp -m tcp ! --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j ufw-logging-deny -A ufw-before-input -p tcp -m tcp ! --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j DROP
&lt;/pre&gt;&lt;p&gt;Don&apos;t forget to add them to the &lt;code&gt;before6.rules&lt;/code&gt; file as well:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;-A ufw6-before-input -p tcp -m tcp ! --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j ufw6-logging-deny -A ufw6-before-input -p tcp -m tcp ! --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j DROP
&lt;/pre&gt;&lt;p&gt;Now, reload UFW:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw reload
&lt;/pre&gt;&lt;p&gt;These rules help to block invalid and potentially malicious packets:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The first rule drops any packet that’s considered &lt;code&gt;INVALID&lt;/code&gt; by the &lt;code&gt;conntrack&lt;/code&gt; module.&lt;/li&gt;&lt;li&gt;The second rule blocks TCP packets that are flagged as &lt;code&gt;NEW&lt;/code&gt; (indicating new connection attempts) but don’t have the SYN flag set alone.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Okay, let&apos;s run the Window scan again:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;nmap -sW 165.22.65.229
&lt;/pre&gt;&lt;p&gt;Output:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;Starting Nmap 7.80 ( https://nmap.org ) at 2024-01-30 13:12 UTC Nmap scan report for 165.22.65.229 Host is up (0.0017s latency). All 1000 scanned ports on 165.22.65.229 are filtered Nmap done: 1 IP address (1 host up) scanned in 21.08 seconds
&lt;/pre&gt;&lt;p&gt;Great, UFW has effectively blocked this scan, preventing Nmap from determining that port 22 is open.&lt;/p&gt;&lt;h3&gt;The &lt;code&gt;PREROUTING&lt;/code&gt; Chain&lt;/h3&gt;&lt;p&gt;There is one more thing we can do to enhance the way UFW blocks invalid packets, which is using the &lt;code&gt;PREROUTING&lt;/code&gt; chain instead of the &lt;code&gt;INPUT&lt;/code&gt; chain.&lt;/p&gt;&lt;p&gt;By leaving everything as is now, invalid packets will travel through the entire &lt;code&gt;INPUT&lt;/code&gt; chain until they reach our rules and get blocked.&lt;/p&gt;&lt;p&gt;However, with the &lt;code&gt;PREROUTING&lt;/code&gt; chain, they get blocked before reaching the &lt;code&gt;OUTPUT&lt;/code&gt;, &lt;code&gt;FORWARD&lt;/code&gt;, or &lt;code&gt;INPUT&lt;/code&gt; chains. This way, we are optimizing the performance of our firewall.&lt;/p&gt;&lt;p&gt;Since the &lt;code&gt;filter&lt;/code&gt; table doesn&apos;t have the &lt;code&gt;PREROUTING&lt;/code&gt; chain, we need to use the &lt;code&gt;PREROUTING&lt;/code&gt; chain of the &lt;code&gt;mangle&lt;/code&gt; table instead.&lt;/p&gt;&lt;p&gt;To do this, add the following lines to the end of the &lt;code&gt;before.rules&lt;/code&gt; and &lt;code&gt;before6.rules&lt;/code&gt; files, right after the last &lt;code&gt;COMMIT&lt;/code&gt; line:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;*mangle :PREROUTING ACCEPT [0:0] -A PREROUTING -m conntrack --ctstate INVALID -j DROP -A PREROUTING -p tcp -m tcp ! --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j DROP COMMIT
&lt;/pre&gt;&lt;p&gt;Reload UFW:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw reload
&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;mangle&lt;/code&gt; table is specifically used for altering packet characteristics or applying special filtering, so it’s ideal for dropping packets that don’t meet the normal connection requirements.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;PREROUTING&lt;/code&gt; chain is one of the first chains that incoming packets go through as soon as they reach the server. By applying our rules here, we can block invalid packets immediately, before they reach other parts of the firewall.&lt;/p&gt;&lt;p&gt;This reduces the load on our firewall and helps ensure that only valid traffic is processed further.&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;Great job reaching the end!&lt;/p&gt;&lt;p&gt;I hope this tutorial has helped you to block invalid packets and enhance your server&apos;s security.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;You can find the full collection of detailed Linux server security guides &lt;a href=&quot;https://ivansalloum.com/collections/linux-server-security/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;If you found value in this tutorial or have any questions or feedback, please don&apos;t hesitate to share your thoughts in the &lt;strong&gt;discussion&lt;/strong&gt; section.&lt;/p&gt;&lt;p&gt;Your input is greatly appreciated, and you can also &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;contact me&lt;/a&gt; directly if you prefer.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Security</category></item><item><title>How to Get a Free VPS Server</title><link>https://ivansalloum.com/how-to-get-a-free-vps-server/</link><guid isPermaLink="true">https://ivansalloum.com/how-to-get-a-free-vps-server/</guid><description>Discover how to get your own free VPS server for projects and testing purposes. Get started without spending a dime!</description><pubDate>Sun, 28 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;Ever wanted to kickstart a project or do some testing without breaking the bank?&lt;/p&gt;&lt;p&gt;Well, today I’ll walk you through getting a free VPS server. Yep, free!&lt;/p&gt;&lt;p&gt;Let’s dive in!&lt;/p&gt;&lt;h2&gt;Fact or Fiction?&lt;/h2&gt;&lt;p&gt;For a moment, you might think it&apos;s impossible – how could a VPS server be free? Surely, no company would offer that!&lt;/p&gt;&lt;p&gt;But the reality is, getting a free VPS server is possible. While it&apos;s not truly free and does have a price, you can obtain it at no cost from genuine server providers.&lt;/p&gt;&lt;p&gt;Let me break it down.&lt;/p&gt;&lt;p&gt;You&apos;ve probably come across ads from server providers on Google, offering credits to test their servers.&lt;/p&gt;&lt;p&gt;These credits allow you to start for free, with some providers offering them for up to two months – a significant period.&lt;/p&gt;&lt;p&gt;And that&apos;s exactly what you&apos;ll do! Many server providers are willing to give you credits to get started.&lt;/p&gt;&lt;p&gt;Need an example? When I studied for my LPIC-1 certificate, I didn&apos;t have a Linux machine.&lt;/p&gt;&lt;p&gt;So, I signed up for a server provider, used the free credits to create a VPS server, and studied for the exam. After the credits expired, I simply moved on to another provider.&lt;/p&gt;&lt;p&gt;And yes, it&apos;s entirely legitimate.&lt;/p&gt;&lt;h2&gt;Getting a Free VPS Server&lt;/h2&gt;&lt;p&gt;Now that you understand the idea behind getting a VPS server for free, let me share something with you.&lt;/p&gt;&lt;p&gt;If I provide you with my affiliate links from these providers, you&apos;ll receive free credits to begin with. That&apos;s exactly what I&apos;m going to do today.&lt;/p&gt;&lt;p&gt;It&apos;s worth noting that not all providers offer credits out-of-the-box. Sometimes you need an invitation to get them.&lt;/p&gt;&lt;p&gt;By using my links, you&apos;ll help me earn a commission, and in return, I&apos;ll help you obtain a VPS server for free.&lt;/p&gt;&lt;p&gt;But let me clarify, I didn&apos;t create this blog just to earn money through referrals.&lt;/p&gt;&lt;p&gt;This method genuinely helped me obtain my LPIC-1 certificate when I couldn&apos;t afford a Linux laptop or a VPS server.&lt;/p&gt;&lt;p&gt;I searched the internet for people sharing their invitation links, and it worked for me.&lt;/p&gt;&lt;p&gt;When you use my links, you not only support me but also enable me to continue creating guides and tutorials.&lt;/p&gt;&lt;p&gt;It&apos;s worth noting that some providers won&apos;t reward me for inviting you unless you spend real money on their services.&lt;/p&gt;&lt;p&gt;However, if you use my links, it strengthens my relationship with these providers.&lt;/p&gt;&lt;p&gt;Now, it is time to get your free VPS server:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://hetzner.cloud/?ref=MC4Yy318xX5X&quot;&gt;Hetzner&lt;/a&gt; – 20€&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.vultr.com/?ref=9585982&quot;&gt;Vultr&lt;/a&gt; – 100$&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://m.do.co/c/0281a13a6819&quot;&gt;DigitalOcean&lt;/a&gt; – 200$&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Your support by using my links is greatly appreciated.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Servers</category></item><item><title>Managing Users on Linux Server Securely</title><link>https://ivansalloum.com/managing-users-on-linux-server-securely/</link><guid isPermaLink="true">https://ivansalloum.com/managing-users-on-linux-server-securely/</guid><description>Explore the essentials of managing users on a Linux server securely with this comprehensive guide.</description><pubDate>Mon, 22 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;Managing users properly is one of the most important things you can do to keep your Linux server secure.&lt;/p&gt;&lt;p&gt;After years of running my own servers – sometimes learning the hard way – I’ve come to appreciate how critical good user management really is. It&apos;s not just about keeping things organized – it&apos;s about reducing risk and locking down potential entry points.&lt;/p&gt;&lt;p&gt;🔒&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Think of it like this:&lt;/strong&gt; every user account is a potential door into your server. Your job is to make sure each door is locked unless absolutely necessary – and only the right people have the keys.&lt;/p&gt;&lt;p&gt;One of the biggest mistakes I made early on (and I&apos;ve seen others make too) was over-relying on the &lt;code&gt;root&lt;/code&gt; account. It’s tempting because it’s convenient, but it opens the door to serious security risks. Learning how to properly use &lt;code&gt;sudo&lt;/code&gt; was a game-changer for me – it lets you give just the right amount of access, no more, no less.&lt;/p&gt;&lt;p&gt;I’ll also show you how to safely edit the &lt;code&gt;sudoers&lt;/code&gt; file – a crucial but often intimidating step for many.&lt;/p&gt;&lt;p&gt;In this guide, I’ll walk you through everything I’ve learned about managing users securely on a Linux server. These are tips and techniques I now use by default, and I hope they’ll save you from some of the headaches I ran into along the way.&lt;/p&gt;&lt;h2&gt;Potential Risks of Using Root&lt;/h2&gt;&lt;p&gt;When I first started managing my own servers, I instinctively logged in as the &lt;code&gt;root&lt;/code&gt; user. After all, it felt powerful – it had full control over everything. But I quickly learned how dangerous that could be.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Here’s the deal:&lt;/strong&gt;  The &lt;code&gt;root&lt;/code&gt; user has unlimited power. It can execute &lt;strong&gt;any command –&lt;/strong&gt; including those capable of breaking your server if you make even a tiny mistake.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;But there&apos;s an even bigger issue:&lt;/strong&gt; hackers know about this power too.&lt;/p&gt;&lt;p&gt;The &lt;code&gt;root&lt;/code&gt; user is a prime target for brute-force attacks. Leaving &lt;code&gt;root&lt;/code&gt; enabled is like leaving your front door unlocked.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;What you should do instead:&lt;/strong&gt;&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Create a regular user with  &lt;code&gt;sudo&lt;/code&gt; privileges:&lt;/strong&gt; This lets you run admin tasks safely, and you’ll be prompted for your password each time – a small but effective security layer.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Disable direct root login altogether:&lt;/strong&gt;  If &lt;code&gt;root&lt;/code&gt; can’t log in, hackers can’t brute-force it.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Use a unique admin username:&lt;/strong&gt;  Something that’s not obvious like &amp;quot;admin&amp;quot; or your name. It’s a small trick that adds another layer of protection.&lt;/li&gt;&lt;/ul&gt;&lt;h3&gt;What If You Have Multiple Admins?&lt;/h3&gt;&lt;p&gt;This is where things can really go sideways if you&apos;re not careful.&lt;/p&gt;&lt;p&gt;Let’s say a few team members all share the &lt;code&gt;root&lt;/code&gt; password. What happens if one person leaves the company? Or stores the password in plain text somewhere insecure? Now you&apos;re stuck resetting the root password and sending it around again – not ideal.&lt;/p&gt;&lt;p&gt;Instead, here&apos;s what I&apos;ve learned works best:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Give &lt;strong&gt;each admin their own user account&lt;/strong&gt;.&lt;/li&gt;&lt;li&gt;Use &lt;code&gt;sudo&lt;/code&gt; to &lt;strong&gt;assign only the privileges they actually need&lt;/strong&gt;.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Limit access intentionally&lt;/strong&gt;  – don’t hand out root-level permissions unless absolutely necessary.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Over the years, these practices have saved me from some painful mistakes and security scares. In the rest of this guide, I’ll show you how to set all of this up, step by step – so you can manage your users confidently and securely.&lt;/p&gt;&lt;h2&gt;Adding Users on Linux (Clearly Explained!)&lt;/h2&gt;&lt;p&gt;Before you can manage users, you need to know how to add them – and luckily, Linux makes this pretty straightforward.&lt;/p&gt;&lt;p&gt;Over the years, I’ve worked with both Debian-based and Red Hat-based servers, and while they use slightly different tools, the core idea is the same.&lt;/p&gt;&lt;p&gt;Let&apos;s break it down.&lt;/p&gt;&lt;h3&gt;Using &lt;code&gt;adduser&lt;/code&gt; – the Friendly Way (Ubuntu/Debian)&lt;/h3&gt;&lt;p&gt;If you&apos;re on Ubuntu or any Debian-based server, you&apos;ll probably want to use &lt;code&gt;adduser&lt;/code&gt;. It’s a more user-friendly wrapper around the lower-level &lt;code&gt;useradd&lt;/code&gt; command and handles a lot of things for you.&lt;/p&gt;&lt;p&gt;To add a new user:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;adduser username
&lt;/pre&gt;&lt;p&gt;This command will:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Create a group with the same name as the user&lt;/li&gt;&lt;li&gt;Set up a home directory under &lt;code&gt;/home/username&lt;/code&gt;&lt;/li&gt;&lt;li&gt;Ask for a password&lt;/li&gt;&lt;li&gt;Prompt for optional info like full name and phone number (you can just press &lt;strong&gt;ENTER&lt;/strong&gt; to skip those)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt;  Avoid common usernames like &lt;code&gt;admin&lt;/code&gt; or &lt;code&gt;user&lt;/code&gt;. I recommend using something unique or even a randomly generated string – it makes brute-force attacks a lot harder.&lt;/p&gt;&lt;h3&gt;Using &lt;code&gt;useradd&lt;/code&gt; – the Manual Way (CentOS/Red Hat)&lt;/h3&gt;&lt;p&gt;The &lt;code&gt;useradd&lt;/code&gt; command is more common on Red Hat-based systems like CentOS, but it&apos;s also available on Ubuntu. It&apos;s more barebones and gives you more control, which makes it great for scripting — but also easier to mess up if you&apos;re not careful.&lt;/p&gt;&lt;p&gt;While it creates a new user like the &lt;code&gt;adduser&lt;/code&gt; command, it has a few differences:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;It does &lt;strong&gt;not&lt;/strong&gt;  create a home directory.&lt;/li&gt;&lt;li&gt;It does &lt;strong&gt;not&lt;/strong&gt;  set a password.&lt;/li&gt;&lt;li&gt;It does &lt;strong&gt;not&lt;/strong&gt;  ask for any user details.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;To make it behave more like &lt;code&gt;adduser&lt;/code&gt;, you&apos;ll need to provide a few flags:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;useradd -m -d /home/username -c &amp;quot;Full Name&amp;quot; -s /bin/bash username
&lt;/pre&gt;&lt;p&gt;Here’s what each flag does:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;-m&lt;/code&gt;:&lt;/strong&gt; Creates a home directory&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;-d /home/username&lt;/code&gt;:&lt;/strong&gt; Sets the path of the home directory&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;-c &amp;quot;Full Name&amp;quot;&lt;/code&gt;:&lt;/strong&gt; Adds a comment (often used for the full name)&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;-s /bin/bash&lt;/code&gt;:&lt;/strong&gt; Sets the default shell to Bash&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Then don’t forget to set the user’s password:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;passwd username
&lt;/pre&gt;&lt;p&gt;You’ll be prompted to enter and confirm the new password.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Shell matters:&lt;/strong&gt; When you use &lt;code&gt;adduser&lt;/code&gt;, the default shell is &lt;code&gt;/bin/bash&lt;/code&gt;. But &lt;code&gt;useradd&lt;/code&gt; (unless you specify otherwise) sets the shell to &lt;code&gt;/bin/sh&lt;/code&gt;, which is more limited. For most users, especially new ones, Bash is the better choice.&lt;/p&gt;&lt;h3&gt;So, Which One Should You Use?&lt;/h3&gt;&lt;p&gt;If you&apos;re doing things manually or managing a small number of users, &lt;strong&gt;&lt;code&gt;adduser&lt;/code&gt;&lt;/strong&gt;  is almost always the better choice. It’s simple, clean, and gets everything set up with minimal effort.&lt;/p&gt;&lt;p&gt;That said, &lt;strong&gt;&lt;code&gt;useradd&lt;/code&gt;&lt;/strong&gt;  shines when you&apos;re writing scripts or doing automated setups where you don’t want interactive prompts.&lt;/p&gt;&lt;p&gt;Personally, I use &lt;code&gt;adduser&lt;/code&gt; for one-off tasks and &lt;code&gt;useradd&lt;/code&gt; in automation. Knowing both gives you flexibility depending on the situation.&lt;/p&gt;&lt;h2&gt;Locking Down Home Directories&lt;/h2&gt;&lt;p&gt;One detail that often slips under the radar – especially when working with different Linux distributions – is how &lt;strong&gt;home directories are secured by default&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Each distro handles this a little differently, and if you&apos;re not paying attention, you could end up with user directories that are readable by everyone on the server. Not ideal.&lt;/p&gt;&lt;h3&gt;Let’s see what happens on Ubuntu 20.04&lt;/h3&gt;&lt;p&gt;First, let’s add a user named &lt;code&gt;john&lt;/code&gt;:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;useradd -m -d /home/john -s /bin/bash john
&lt;/pre&gt;&lt;p&gt;Then, check the permissions on the &lt;code&gt;/home&lt;/code&gt; directory:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ls -l /home
&lt;/pre&gt;&lt;p&gt;Output:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;total 4 drwxr-xr-x 2 john john 4096 Jan 19 08:50 john
&lt;/pre&gt;&lt;p&gt;See that? John&apos;s home directory has &lt;code&gt;r-x&lt;/code&gt; (read and execute) permissions for &lt;strong&gt;everyone&lt;/strong&gt;. That means other users on the server can view his files or even browse the folder. Definitely not what you want for sensitive user environments.&lt;/p&gt;&lt;h3&gt;Setting Secure Defaults with &lt;code&gt;useradd&lt;/code&gt;&lt;/h3&gt;&lt;p&gt;Sure, users can change directory permissions manually using &lt;code&gt;chmod&lt;/code&gt;, but there’s a better way: set secure defaults.&lt;/p&gt;&lt;p&gt;Open &lt;code&gt;/etc/login.defs&lt;/code&gt;, find the &lt;code&gt;UMASK&lt;/code&gt; line and change its value to &lt;code&gt;077&lt;/code&gt; like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;UMASK 077
&lt;/pre&gt;&lt;p&gt;This setting ensures &lt;strong&gt;new&lt;/strong&gt;  users will have locked-down home directories by default.&lt;/p&gt;&lt;p&gt;Let’s try it again. This time, I’ll create a user named &lt;code&gt;ivan&lt;/code&gt;:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;useradd -m -d /home/ivan -s /bin/bash ivan ls -l /home
&lt;/pre&gt;&lt;p&gt;Output:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;total 4 drwx------ 2 ivan ivan 4096 Jan 19 09:02 ivan drwxr-xr-x 2 john john 4096 Jan 19 08:50 john
&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Success!&lt;/strong&gt; Ivan’s directory is now fully private (&lt;code&gt;drwx------&lt;/code&gt;), while John&apos;s is still open.&lt;/p&gt;&lt;h3&gt;But Wait – What About &lt;code&gt;adduser&lt;/code&gt;?&lt;/h3&gt;&lt;p&gt;If you use &lt;code&gt;adduser&lt;/code&gt; instead, it &lt;strong&gt;won’t&lt;/strong&gt;  follow the &lt;code&gt;UMASK&lt;/code&gt; setting. Instead, it uses a config file of its own: &lt;code&gt;/etc/adduser.conf&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;Open that file and change the &lt;code&gt;DIR_MODE&lt;/code&gt; value:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;DIR_MODE=0700
&lt;/pre&gt;&lt;p&gt;Now &lt;code&gt;adduser&lt;/code&gt; will also create private home directories moving forward.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Heads-up for Red Hat users:&lt;/strong&gt; Red Hat and its derivatives (like CentOS) already use secure home directory defaults out of the box – so no changes are needed there.&lt;/p&gt;&lt;h3&gt;Ubuntu 22.04+ – Good News!&lt;/h3&gt;&lt;p&gt;Starting with &lt;strong&gt;Ubuntu 22.04 and 24.04&lt;/strong&gt; , things have improved. Both &lt;code&gt;useradd&lt;/code&gt; and &lt;code&gt;adduser&lt;/code&gt; now use &lt;strong&gt;more secure default settings&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;In &lt;code&gt;/etc/login.defs&lt;/code&gt; you&apos;ll find:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;HOME_MODE 0750
&lt;/pre&gt;&lt;p&gt;In &lt;code&gt;/etc/adduser.conf&lt;/code&gt;:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;DIR_MODE=0750
&lt;/pre&gt;&lt;p&gt;This means home directories are still private to the user and their group – a reasonable default for most setups.&lt;/p&gt;&lt;h2&gt;Password Management&lt;/h2&gt;&lt;p&gt;Now that you&apos;ve seen how to add users, it’s time to talk about managing their passwords – securely and efficiently.&lt;/p&gt;&lt;p&gt;It’s not enough for each user to just “remember” their password. In any serious setup, you need a centralized, secure way to store and share credentials – and that’s where password managers come in.&lt;/p&gt;&lt;p&gt;These days, using a password manager isn’t optional – it’s essential.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Personally, I use and recommend  &lt;strong&gt;&lt;a href=&quot;https://go.getproton.me/aff_c?offer_id=38&amp;amp;aff_id=12648&quot;&gt;&lt;strong&gt;Proton Pass&lt;/strong&gt;&lt;/a&gt;&lt;/strong&gt;.&lt;/strong&gt;  It’s fast, secure, and works seamlessly across devices. Their &lt;strong&gt;Business plan&lt;/strong&gt;  supports team use, making it easy to securely share passwords and sensitive credentials within your organization – without sacrificing performance or privacy.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Bonus:&lt;/strong&gt;  If you &lt;a href=&quot;https://go.getproton.me/aff_c?offer_id=38&amp;amp;aff_id=12648&quot;&gt;sign up using my link&lt;/a&gt;, you’ll get &lt;strong&gt;50% off your first year&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Having one secure vault for storing and generating strong passwords saves you from reusing weak ones – and makes enforcing strict password policies a lot more practical.&lt;/p&gt;&lt;h2&gt;Enforcing Strong Passwords&lt;/h2&gt;&lt;p&gt;Once you’ve got a manager in place, the next step is &lt;strong&gt;making sure users create strong passwords&lt;/strong&gt;  in the first place.&lt;/p&gt;&lt;p&gt;To do this, we’ll use the &lt;strong&gt;&lt;code&gt;pwquality&lt;/code&gt;&lt;/strong&gt;  module, part of PAM (Pluggable Authentication Modules).&lt;/p&gt;&lt;p&gt;Let&apos;s begin by installing the module:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;apt install libpam-pwquality
&lt;/pre&gt;&lt;p&gt;Next, open the config file:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;vim /etc/security/pwquality.conf
&lt;/pre&gt;&lt;p&gt;This file is well-documented, so feel free to explore. But to get started, focus on these two important settings:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;minlen = 32 minclass = 4
&lt;/pre&gt;&lt;p&gt;Here’s what they mean:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;minlen&lt;/code&gt;:&lt;/strong&gt; sets the &lt;strong&gt;minimum password length&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;minclass&lt;/code&gt;:&lt;/strong&gt; sets the &lt;strong&gt;minimum number of character types&lt;/strong&gt;  (uppercase, lowercase, digits, symbols)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;strong&gt;Why so strict?&lt;/strong&gt; Because users won’t have to memorize these – they’ll use the password manager to generate and store them securely. &lt;em&gt;So go big on security here.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;⚠️&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;  Users with root privileges can bypass these rules when setting passwords.&lt;/p&gt;&lt;p&gt;I’ll leave the rest up to you to meet your own criteria – as mentioned earlier, the file is well-documented. Just make sure to &lt;strong&gt;uncomment the variables&lt;/strong&gt;  for your changes to take effect.&lt;/p&gt;&lt;p&gt;After saving the file, test it by creating a new user and trying to set a weak password. You’ll see the policy in action.&lt;/p&gt;&lt;h2&gt;Password Expiration Policy&lt;/h2&gt;&lt;p&gt;When you add a new user, you typically assign them a password and store it securely in your password manager.&lt;/p&gt;&lt;p&gt;But that’s just the start – you also want to &lt;strong&gt;enforce a password rotation policy&lt;/strong&gt;  so users change their password regularly.&lt;/p&gt;&lt;p&gt;In this example, we’ll require users to &lt;strong&gt;change their password every 30 days&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;To enforce this, open &lt;code&gt;/etc/login.defs&lt;/code&gt; and set the &lt;code&gt;PASS_MAX_DAYS&lt;/code&gt; variable like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;PASS_MAX_DAYS 30
&lt;/pre&gt;&lt;p&gt;This means each user must change their password &lt;strong&gt;within 30 days&lt;/strong&gt;  of the last password update.&lt;/p&gt;&lt;p&gt;After adding a new user, you can verify their expiration policy with:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;chage -l username
&lt;/pre&gt;&lt;p&gt;This command shows info like when the password was last changed and when it will expire.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;  Any user can run this command for their own account – no root privileges needed – but they can’t view or modify anyone else’s data.&lt;/p&gt;&lt;p&gt;Let’s test this by adding a user named &lt;code&gt;elie&lt;/code&gt; and checking the output:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;Last password change : Jan 25, 2024 Password expires : Feb 24, 2024 Password inactive : never Account expires : never Minimum number of days between password change : 0 Maximum number of days between password change : 30 Number of days of warning before password expires : 7
&lt;/pre&gt;&lt;p&gt;As you can see, Elie’s password will expire &lt;strong&gt;30 days&lt;/strong&gt;  after it was last set. He’ll also receive a warning starting &lt;strong&gt;7 days before&lt;/strong&gt;  expiration – in this case, beginning on &lt;strong&gt;Feb 17, 2024&lt;/strong&gt;.&lt;/p&gt;&lt;h3&gt;Optional: Control When Passwords Can Be Changed&lt;/h3&gt;&lt;p&gt;By default, users can change their password &lt;strong&gt;any time&lt;/strong&gt;  before the expiration date – even daily. While that might sound secure, it’s not a policy I recommend.&lt;/p&gt;&lt;p&gt;Personally, I prefer users to &lt;strong&gt;only&lt;/strong&gt;  change their password during the 7-day warning window. That way, they can’t rotate passwords unnecessarily – and are gently nudged to update only when needed.&lt;/p&gt;&lt;p&gt;To do this, we set the &lt;code&gt;PASS_MIN_DAYS&lt;/code&gt; variable in &lt;code&gt;/etc/login.defs&lt;/code&gt; to &lt;code&gt;23&lt;/code&gt;, like so:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;PASS_MIN_DAYS 23
&lt;/pre&gt;&lt;p&gt;This setting ensures users must wait &lt;strong&gt;at least 23 days&lt;/strong&gt;  before changing their password again – which lines up perfectly with the 7-day warning period (days 24–30 after their last password change).&lt;/p&gt;&lt;h2&gt;Implementing &lt;code&gt;sudo&lt;/code&gt; the Right Way&lt;/h2&gt;&lt;p&gt;When &lt;code&gt;sudo&lt;/code&gt; is set up properly, it significantly boosts the security of your server.&lt;/p&gt;&lt;p&gt;Here’s what I love about &lt;code&gt;sudo&lt;/code&gt;, and why I always recommend using it over the root account:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Granular control:&lt;/strong&gt; Give some users full root access, while limiting others to specific commands only.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;No root password sharing:&lt;/strong&gt; Users authenticate with their &lt;strong&gt;own password&lt;/strong&gt;  – no need to distribute the root password to your whole team.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Better security:&lt;/strong&gt; You can &lt;strong&gt;disable the root account&lt;/strong&gt; , making brute-force attacks far less effective. Attackers also won&apos;t know which usernames have elevated privileges.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Accountability:&lt;/strong&gt; Every use of &lt;code&gt;sudo&lt;/code&gt; is &lt;strong&gt;logged&lt;/strong&gt; , so you can track exactly who did what and when.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;And that’s just scratching the surface – let’s look at how to implement it securely and effectively.&lt;/p&gt;&lt;h3&gt;Granting Full Root Privileges to a User&lt;/h3&gt;&lt;p&gt;You’ve probably seen this recommended before: create a non-root user, add them to the &lt;code&gt;sudo&lt;/code&gt; group, and disable root login.&lt;/p&gt;&lt;p&gt;But have you ever wondered what &lt;em&gt;actually&lt;/em&gt;  gives that group its power?&lt;/p&gt;&lt;p&gt;The answer lies in the &lt;code&gt;**/etc/sudoers**&lt;/code&gt; file – the heart of &lt;code&gt;sudo&lt;/code&gt; configuration.&lt;/p&gt;&lt;h4&gt;How &lt;code&gt;sudo&lt;/code&gt; Permissions Work&lt;/h4&gt;&lt;p&gt;Whenever a user tries to run a command with &lt;code&gt;sudo&lt;/code&gt;, the server checks the &lt;code&gt;/etc/sudoers&lt;/code&gt; file to see if they’re allowed to do so.&lt;/p&gt;&lt;p&gt;To safely edit that file, use the &lt;code&gt;visudo&lt;/code&gt; command. &lt;strong&gt;Never edit it directly with a regular text editor&lt;/strong&gt; , or you risk locking yourself out due to syntax errors.&lt;/p&gt;&lt;p&gt;If &lt;code&gt;visudo&lt;/code&gt; opens with an editor you don’t like (e.g., nano), you can change it:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;update-alternatives --config editor
&lt;/pre&gt;&lt;p&gt;Select your preferred editor and then run &lt;code&gt;visudo&lt;/code&gt; again.&lt;/p&gt;&lt;p&gt;Inside the file, you’ll see something like this:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;# User privilege specification root ALL=(ALL:ALL) ALL # Members of the admin group may gain root privileges %admin ALL=(ALL) ALL # Allow members of group sudo to execute any command %sudo ALL=(ALL:ALL) ALL
&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;%&lt;/code&gt; symbol means it&apos;s referencing a &lt;strong&gt;group&lt;/strong&gt;. So when a user is added to the &lt;code&gt;sudo&lt;/code&gt; group, they inherit full root-level privileges.&lt;/p&gt;&lt;p&gt;Let’s break down that last line:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;ALL&lt;/code&gt;:&lt;/strong&gt; Applies on all hosts&lt;/li&gt;&lt;li&gt;**&lt;code&gt;(ALL:ALL)&lt;/code&gt;: **Run commands as any user and any group&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;ALL&lt;/code&gt;:&lt;/strong&gt; Run &lt;strong&gt;any&lt;/strong&gt;  command&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;That’s full root access – without needing to touch the root password.&lt;/p&gt;&lt;h4&gt;Add a User to the &lt;code&gt;sudo&lt;/code&gt; Group&lt;/h4&gt;&lt;p&gt;Here’s how to add a user named &lt;code&gt;ivan&lt;/code&gt; to the &lt;code&gt;sudo&lt;/code&gt; group:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;usermod -aG sudo ivan
&lt;/pre&gt;&lt;p&gt;Now, Ivan can run commands like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt update
&lt;/pre&gt;&lt;p&gt;...and enter &lt;strong&gt;his own password&lt;/strong&gt;  – not the root user’s.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;  On Red Hat-based servers (like CentOS or RHEL), the equivalent group is called &lt;code&gt;wheel&lt;/code&gt; instead of &lt;code&gt;sudo&lt;/code&gt;.&lt;/p&gt;&lt;h3&gt;Manual &lt;code&gt;sudo&lt;/code&gt; Privileges in the &lt;code&gt;sudoers&lt;/code&gt; File&lt;/h3&gt;&lt;p&gt;You don’t &lt;em&gt;have&lt;/em&gt;  to rely on groups. You can give individual users specific rules.&lt;/p&gt;&lt;p&gt;Let’s say you want to give full privileges to &lt;code&gt;ivan&lt;/code&gt;. In the &lt;code&gt;sudoers&lt;/code&gt; file, just add this line under the root entry:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ivan ALL=(ALL:ALL) ALL
&lt;/pre&gt;&lt;p&gt;This grants Ivan the same full access as root – across all users, groups, hosts, and commands.&lt;/p&gt;&lt;h3&gt;Add a User with &lt;code&gt;sudo&lt;/code&gt; From the Start&lt;/h3&gt;&lt;p&gt;Want to create a new user and give them &lt;code&gt;sudo&lt;/code&gt; access immediately? Use this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;useradd -G sudo -m -d /home/john -s /bin/bash ivan
&lt;/pre&gt;&lt;p&gt;Let’s break that down:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;-G sudo&lt;/code&gt;:&lt;/strong&gt; Adds Ivan to the sudo group&lt;/li&gt;&lt;li&gt;**&lt;code&gt;-m&lt;/code&gt;: **Creates a home directory&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;-d /home/ivan&lt;/code&gt;:&lt;/strong&gt; Sets the home directory path&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;-s /bin/bash&lt;/code&gt;:&lt;/strong&gt; Sets the default shell to Bash&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This saves a step and gets your user set up with privileges right away.&lt;/p&gt;&lt;h3&gt;Customized User Privileges&lt;/h3&gt;&lt;p&gt;So far, we’ve seen how to give users full root access with &lt;code&gt;sudo&lt;/code&gt;. But most of the time, &lt;strong&gt;you don’t want to do that&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Instead, it’s best to follow the principle of &lt;strong&gt;least privilege&lt;/strong&gt; : give each user just the access they need to do their job – and nothing more.&lt;/p&gt;&lt;p&gt;Let me show you how to set that up with &lt;code&gt;sudo&lt;/code&gt;.&lt;/p&gt;&lt;h4&gt;Scenario: Software Team Access Only&lt;/h4&gt;&lt;p&gt;Imagine this situation: you have two users on the Software team – &lt;code&gt;ivan&lt;/code&gt; and &lt;code&gt;john&lt;/code&gt;. You want them to be able to install and manage packages, but not touch anything else on the server.&lt;/p&gt;&lt;p&gt;Sure, you could add separate entries for each of them in the &lt;code&gt;sudoers&lt;/code&gt; file, but there’s a better, cleaner way – &lt;strong&gt;using aliases&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Open the &lt;code&gt;sudoers&lt;/code&gt; file with &lt;code&gt;visudo&lt;/code&gt;, and create a user alias for the Software team:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;# User alias specification User_Alias SOFTWAREADMINS = ivan, john
&lt;/pre&gt;&lt;p&gt;This way, instead of repeating usernames, you can refer to &lt;code&gt;SOFTWAREADMINS&lt;/code&gt; as a group.&lt;/p&gt;&lt;p&gt;Now, define a command alias for the allowed software-related commands:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;# Cmnd alias specification Cmnd_Alias SOFTWARECOMMANDS = /usr/bin/apt, /usr/bin/dpkg
&lt;/pre&gt;&lt;p&gt;You can add more commands here if needed – just separate them with commas.&lt;/p&gt;&lt;p&gt;Finally, give the &lt;code&gt;SOFTWAREADMINS&lt;/code&gt; group permission to run those commands using &lt;code&gt;sudo&lt;/code&gt;:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;SOFTWAREADMINS ALL=(ALL:ALL) SOFTWARECOMMANDS
&lt;/pre&gt;&lt;p&gt;This line says: &lt;em&gt;All users in&lt;code&gt;SOFTWAREADMINS&lt;/code&gt; can run &lt;code&gt;SOFTWARECOMMANDS&lt;/code&gt; as any user or group, on any host.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;The beauty of this setup is how easy it is to manage:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;If someone &lt;strong&gt;joins the Software team&lt;/strong&gt; , just add their username to the &lt;code&gt;SOFTWAREADMINS&lt;/code&gt; alias.&lt;/li&gt;&lt;li&gt;If you need to &lt;strong&gt;allow a new command&lt;/strong&gt; , just add it to the &lt;code&gt;SOFTWARECOMMANDS&lt;/code&gt; alias.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;This keeps your &lt;code&gt;sudoers&lt;/code&gt; file clean, readable, and scalable – especially on servers with multiple roles or teams.&lt;/p&gt;&lt;h3&gt;Be Careful: How You Specify Commands in &lt;code&gt;sudo&lt;/code&gt; Matters&lt;/h3&gt;&lt;p&gt;Here’s a common mistake I’ve seen (and made myself early on): &lt;strong&gt;defining a command in a&lt;code&gt;sudo&lt;/code&gt; alias without any arguments&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Let’s break down what that means – and why it can be risky.&lt;/p&gt;&lt;h4&gt;The Problem With Broad Command Permissions&lt;/h4&gt;&lt;p&gt;In our earlier example, we created this command alias:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;Cmnd_Alias SOFTWARECOMMANDS = /usr/bin/apt, /usr/bin/apt
&lt;/pre&gt;&lt;p&gt;At first glance, it seems fine – we’re just giving access to package management commands, right?&lt;/p&gt;&lt;p&gt;But here’s the issue: &lt;strong&gt;when you list a command alone&lt;/strong&gt; , users can run it &lt;strong&gt;with any arguments, subcommands, or options&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;This means:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;They can &lt;strong&gt;install software&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;But also &lt;strong&gt;remove&lt;/strong&gt; , &lt;strong&gt;purge&lt;/strong&gt; , or even &lt;strong&gt;break things&lt;/strong&gt;  unintentionally&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;That’s probably not what you want.&lt;/p&gt;&lt;h4&gt;Limiting What Commands Can Actually Do&lt;/h4&gt;&lt;p&gt;Let’s say we only want users to be able to update and install software – not remove or purge it.&lt;/p&gt;&lt;p&gt;You might try this:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;Cmnd_Alias SOFTWARECOMMANDS = /usr/bin/apt update, /usr/bin/apt upgrade, /usr/bin/apt install
&lt;/pre&gt;&lt;p&gt;This seems safer – but try running:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt install postfix
&lt;/pre&gt;&lt;p&gt;It &lt;strong&gt;won’t&lt;/strong&gt; work.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt; Because we didn’t specify any argument for &lt;code&gt;install&lt;/code&gt;. As far as &lt;code&gt;sudo&lt;/code&gt; is concerned, only &lt;code&gt;apt install&lt;/code&gt; with &lt;strong&gt;no arguments&lt;/strong&gt;  is allowed – and that’s useless.&lt;/p&gt;&lt;p&gt;To allow users to run &lt;code&gt;apt install&lt;/code&gt; with actual package names, modify the alias like this:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;Cmnd_Alias SOFTWARECOMMANDS = /usr/bin/apt update, /usr/bin/apt upgrade, /usr/bin/apt install *
&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;*&lt;/code&gt; wildcard tells &lt;code&gt;sudo&lt;/code&gt;: &lt;em&gt;Allow any arguments after this command.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;Now users can do:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt install nginx sudo apt update sudo apt upgrade
&lt;/pre&gt;&lt;p&gt;But they still can’t run other unrelated or destructive commands.&lt;/p&gt;&lt;h4&gt;Same Rule, Different Context: Managing Services&lt;/h4&gt;&lt;p&gt;This principle applies beyond just package management.&lt;/p&gt;&lt;p&gt;Let’s say you&apos;re managing a &lt;strong&gt;services team&lt;/strong&gt;  who needs to check the status of running services. You might be tempted to write:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;Cmnd_Alias SERVICECOMMANDS = /usr/bin/systemctl, /usr/sbin/service
&lt;/pre&gt;&lt;p&gt;Sounds reasonable, right?&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Wrong.&lt;/strong&gt; This would give them access to commands like:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo systemctl reboot sudo systemctl isolate rescue.target
&lt;/pre&gt;&lt;p&gt;That&apos;s way more access than you intended.&lt;/p&gt;&lt;h4&gt;Restricting &lt;code&gt;systemctl&lt;/code&gt; to Status Only&lt;/h4&gt;&lt;p&gt;To allow only safe, read-only actions, like checking service status, define the alias more precisely:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;Cmnd_Alias SERVICECOMMANDS = /usr/bin/systemctl status *
&lt;/pre&gt;&lt;p&gt;Now users can run:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo systemctl status ssh sudo systemctl status nginx
&lt;/pre&gt;&lt;p&gt;But they &lt;strong&gt;can’t&lt;/strong&gt;  restart, stop, or disable services – which keeps the server safe.&lt;/p&gt;&lt;h4&gt;A More Specific Example&lt;/h4&gt;&lt;p&gt;Let’s say you trust a user to manage &lt;strong&gt;only the SSH service&lt;/strong&gt; , but allow full control over it (start, stop, restart, etc.).&lt;/p&gt;&lt;p&gt;Use this:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;Cmnd_Alias SERVICECOMMANDS = /usr/bin/systemctl * ssh
&lt;/pre&gt;&lt;p&gt;This allows things like:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo systemctl restart ssh sudo systemctl status ssh
&lt;/pre&gt;&lt;p&gt;But they still can’t touch &lt;code&gt;nginx&lt;/code&gt;, &lt;code&gt;mysql&lt;/code&gt;, or anything else.&lt;/p&gt;&lt;h4&gt;Best Practices for Command Aliases&lt;/h4&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Never&lt;/strong&gt;  specify a command alone (e.g., just &lt;code&gt;/usr/bin/systemctl&lt;/code&gt;)&lt;/li&gt;&lt;li&gt;Always include &lt;strong&gt;arguments or wildcards&lt;/strong&gt;  to precisely define what&apos;s allowed&lt;/li&gt;&lt;li&gt;Think through what the user &lt;em&gt;really&lt;/em&gt;  needs – and limit everything else&lt;/li&gt;&lt;li&gt;Test your configuration with &lt;code&gt;sudo -l&lt;/code&gt; as the user to confirm permissions&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;⚠️&lt;/p&gt;&lt;p&gt;&lt;strong&gt;My rule of thumb:&lt;/strong&gt;  Never leave a command bare in a &lt;code&gt;sudo&lt;/code&gt; alias. Be specific about what comes after – it’s the only way to safely control what users can do.&lt;/p&gt;&lt;h3&gt;Mitigating Shell Escapes for Users&lt;/h3&gt;&lt;p&gt;Some programs – like text editors and pagers – allow users to &lt;strong&gt;escape into a shell&lt;/strong&gt;  and run commands without exiting the program. This feature is called a &lt;strong&gt;Shell Escape&lt;/strong&gt; , and it can be a serious security risk if left unchecked.&lt;/p&gt;&lt;p&gt;Let me show you what that looks like.&lt;/p&gt;&lt;h4&gt;Example: Escaping Shell From &lt;code&gt;vim&lt;/code&gt;&lt;/h4&gt;&lt;p&gt;Let’s say you&apos;re working inside &lt;code&gt;vim&lt;/code&gt;, and you run this:&lt;/p&gt;&lt;pre data-language=&quot;vim&quot;&gt;:!ls
&lt;/pre&gt;&lt;p&gt;This will list files in the current directory – like so:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;bib.csv lilly.sh script.sh
&lt;/pre&gt;&lt;p&gt;Seems harmless, right? But here’s where it becomes a problem...&lt;/p&gt;&lt;h4&gt;When Limited &lt;code&gt;sudo&lt;/code&gt; Access Becomes Full Root Access&lt;/h4&gt;&lt;p&gt;Let’s say we gave a user – &lt;code&gt;ivan&lt;/code&gt; – permission to edit the SSH config file using &lt;code&gt;vim&lt;/code&gt;:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;ivan ALL=(ALL:ALL) /usr/bin/vim /etc/ssh/sshd_config
&lt;/pre&gt;&lt;p&gt;That &lt;em&gt;should&lt;/em&gt;  mean Ivan can only edit that one file.&lt;/p&gt;&lt;p&gt;But now watch what happens if Ivan runs this inside &lt;code&gt;vim&lt;/code&gt;:&lt;/p&gt;&lt;pre data-language=&quot;vim&quot;&gt;:!apt update
&lt;/pre&gt;&lt;p&gt;Or worse:&lt;/p&gt;&lt;pre data-language=&quot;vim&quot;&gt;:!bash
&lt;/pre&gt;&lt;p&gt;Output:&lt;/p&gt;&lt;pre data-language=&quot;output&quot;&gt;root@testing:/home/ivan#
&lt;/pre&gt;&lt;p&gt;Just like that, Ivan is in a full root shell – with unrestricted access to the entire server. All from within &lt;code&gt;vim&lt;/code&gt;.&lt;/p&gt;&lt;h4&gt;Fix 1: Use &lt;code&gt;sudoedit&lt;/code&gt; Instead&lt;/h4&gt;&lt;p&gt;To avoid shell escapes entirely, use &lt;code&gt;sudoedit&lt;/code&gt; instead of directly calling &lt;code&gt;vim&lt;/code&gt;:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;ivan ALL=(ALL:ALL) sudoedit /etc/ssh/sshd_config
&lt;/pre&gt;&lt;p&gt;With &lt;code&gt;sudoedit&lt;/code&gt;, users still get to edit the file, but &lt;strong&gt;without the ability to escape into a root shell&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;If Ivan tries to run &lt;code&gt;:!bash&lt;/code&gt;, they&apos;ll land in their own unprivileged shell – not root.&lt;/p&gt;&lt;h4&gt;Fix 2: Use the &lt;code&gt;NOEXEC&lt;/code&gt; Flag&lt;/h4&gt;&lt;p&gt;Some other programs like &lt;code&gt;less&lt;/code&gt;, &lt;code&gt;more&lt;/code&gt;, or &lt;code&gt;emacs&lt;/code&gt; also support shell escapes. For these, you can use the &lt;code&gt;NOEXEC&lt;/code&gt; option in the &lt;code&gt;sudoers&lt;/code&gt; file to block those escape attempts.&lt;/p&gt;&lt;p&gt;For example:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;ivan ALL=(ALL:ALL) NOEXEC: /usr/bin/less /etc/ssh/sshd_config
&lt;/pre&gt;&lt;p&gt;Now, Ivan can still view the file using &lt;code&gt;less&lt;/code&gt;, but &lt;strong&gt;cannot&lt;/strong&gt;  drop into a shell from within the program.&lt;/p&gt;&lt;h4&gt;Why Not Just Use &lt;code&gt;NOEXEC&lt;/code&gt; With &lt;code&gt;vim&lt;/code&gt;?&lt;/h4&gt;&lt;p&gt;You &lt;em&gt;can&lt;/em&gt;  – and it works:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;ivan ALL=(ALL:ALL) NOEXEC: /usr/bin/vim /etc/ssh/sshd_config
&lt;/pre&gt;&lt;p&gt;But in this guide, I wanted to show both methods – because sometimes using &lt;code&gt;sudoedit&lt;/code&gt; is cleaner, especially when the user only needs to edit a file, not run a full program like &lt;code&gt;vim&lt;/code&gt;.&lt;/p&gt;&lt;h3&gt;Preventing Abuse of Shell Scripts&lt;/h3&gt;&lt;p&gt;Let’s say Ivan writes a shell script that requires root privileges to run. He asks you to allow it using &lt;code&gt;sudo&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;You might be tempted to add this line to your &lt;code&gt;sudoers&lt;/code&gt; file:&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;ivan ALL=(ALL:ALL) /home/ivan/script.sh
&lt;/pre&gt;&lt;p&gt;Now Ivan can run the script as root – but here&apos;s the problem: &lt;strong&gt;he owns the script&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;And since he owns it, he can edit it anytime he wants. That means he could add something like:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo -i
&lt;/pre&gt;&lt;p&gt;...to open a full root shell from inside the script – essentially giving himself unrestricted access. &lt;strong&gt;That’s a serious security hole.&lt;/strong&gt;&lt;/p&gt;&lt;h4&gt;The Right Way to Handle User-Created Scripts&lt;/h4&gt;&lt;p&gt;Here&apos;s the safe process I always follow:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Let the user create and test the script in their own home directory&lt;/li&gt;&lt;li&gt;Review the script thoroughly&lt;/li&gt;&lt;li&gt;Move the script to a trusted server directory, like &lt;code&gt;/usr/local/sbin&lt;/code&gt;&lt;/li&gt;&lt;li&gt;Change the owner to root and restrict write access&lt;/li&gt;&lt;li&gt;Then – and only then – grant &lt;code&gt;sudo&lt;/code&gt; access to that script&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Now, Ivan can run the script with elevated privileges, but &lt;strong&gt;he can’t modify it&lt;/strong&gt;  – which prevents abuse.&lt;/p&gt;&lt;h3&gt;Viewing Your &lt;code&gt;sudo&lt;/code&gt; Privileges&lt;/h3&gt;&lt;p&gt;Not sure what you’re allowed to do with &lt;code&gt;sudo&lt;/code&gt;? There’s a simple way to check:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo -l
&lt;/pre&gt;&lt;p&gt;This command lists your current &lt;code&gt;sudo&lt;/code&gt; privileges, along with any environment-related settings applied through &lt;code&gt;sudo&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Run &lt;code&gt;sudo -l&lt;/code&gt; often, especially when troubleshooting or verifying access. It&apos;s a simple way to confirm exactly what commands you’ve been granted – and whether your &lt;code&gt;sudoers&lt;/code&gt; entries are working as expected.&lt;/p&gt;&lt;h2&gt;Understanding the &lt;code&gt;sudo&lt;/code&gt; Timer&lt;/h2&gt;&lt;p&gt;Ever notice how, after entering your password once with &lt;code&gt;sudo&lt;/code&gt;, you can run more &lt;code&gt;sudo&lt;/code&gt; commands for a while &lt;strong&gt;without being asked again&lt;/strong&gt;?&lt;/p&gt;&lt;p&gt;That’s thanks to the &lt;strong&gt;sudo timer&lt;/strong&gt;. By default, &lt;code&gt;sudo&lt;/code&gt; remembers your authentication for &lt;strong&gt;5 minutes&lt;/strong&gt;. During that window, you can run additional &lt;code&gt;sudo&lt;/code&gt; commands without re-entering your password.&lt;/p&gt;&lt;p&gt;While convenient, this can be a &lt;strong&gt;security risk&lt;/strong&gt;  – especially on shared servers or if a user steps away from their keyboard without locking the terminal.&lt;/p&gt;&lt;p&gt;To avoid this, you might want to &lt;strong&gt;disable the  &lt;code&gt;sudo&lt;/code&gt; timer entirely&lt;/strong&gt;, requiring the password every time &lt;code&gt;sudo&lt;/code&gt; is used.&lt;/p&gt;&lt;h3&gt;How to Disable the &lt;code&gt;sudo&lt;/code&gt; Timer&lt;/h3&gt;&lt;p&gt;To make &lt;code&gt;sudo&lt;/code&gt; always prompt for a password, add this line to the &lt;code&gt;Defaults&lt;/code&gt; section of the &lt;code&gt;sudoers&lt;/code&gt; file (using &lt;code&gt;visudo&lt;/code&gt;):&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;Defaults timestamp_timeout=0
&lt;/pre&gt;&lt;p&gt;Now, users will be prompted for their password &lt;strong&gt;every time&lt;/strong&gt;  they use &lt;code&gt;sudo&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;If you&apos;re working on a server where the timer is still active (e.g., the default 5-minute window), and you’re about to walk away but don’t want to close the terminal, you can manually expire the session with:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo -k
&lt;/pre&gt;&lt;p&gt;This immediately clears the cached credentials. Any future use of &lt;code&gt;sudo&lt;/code&gt; will require re-authentication – even if it’s within the timer window.&lt;/p&gt;&lt;h2&gt;Monitoring User Activities with &lt;code&gt;sudo&lt;/code&gt;&lt;/h2&gt;&lt;p&gt;One of the best things about &lt;code&gt;sudo&lt;/code&gt; – beyond security – is &lt;strong&gt;transparency&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Every time a user runs a command with &lt;code&gt;sudo&lt;/code&gt;, it gets logged. This gives you a clear audit trail of who did what and when.&lt;/p&gt;&lt;p&gt;By default, &lt;code&gt;sudo&lt;/code&gt; logs activity in your server’s main authentication log – usually:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;/var/log/auth.log
&lt;/pre&gt;&lt;p&gt;While that works fine, I personally prefer &lt;strong&gt;keeping  &lt;code&gt;sudo&lt;/code&gt; logs separate&lt;/strong&gt;. It makes things cleaner and easier to investigate when you&apos;re troubleshooting or auditing user behavior.&lt;/p&gt;&lt;p&gt;To direct all &lt;code&gt;sudo&lt;/code&gt; activity to its own dedicated log file, just add this line to the &lt;code&gt;Defaults&lt;/code&gt; section of your &lt;code&gt;sudoers&lt;/code&gt; file (use &lt;code&gt;visudo&lt;/code&gt;):&lt;/p&gt;&lt;pre data-language=&quot;text&quot;&gt;Defaults logfile=/var/log/sudo.log
&lt;/pre&gt;&lt;p&gt;Once set, all &lt;code&gt;sudo&lt;/code&gt; commands – both successful and failed – will be logged to:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;/var/log/sudo.log
&lt;/pre&gt;&lt;p&gt;This includes:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;What command was run&lt;/li&gt;&lt;li&gt;When it was run&lt;/li&gt;&lt;li&gt;By which user&lt;/li&gt;&lt;li&gt;And whether it succeeded or failed&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;It&apos;s a small change that goes a long way in maintaining visibility on your server.&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;Awesome job making it to the end!&lt;/p&gt;&lt;p&gt;I hope this guide has given you a clear, practical understanding of how to securely manage users on a Linux server. If you’ve followed along and implemented these steps, you’ve already taken meaningful action to strengthen your server’s security.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Looking for more?&lt;/strong&gt; Check out my &lt;a href=&quot;https://ivansalloum.com/collections/linux-server-security/&quot;&gt;full collection&lt;/a&gt; of in-depth Linux server security guides.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;💬  Found this guide helpful?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;I&apos;d love to hear about your experiences, questions, or ideas in the discussion section below. Your feedback not only helps improve future guides – it helps fellow admins on their own journey.&lt;/p&gt;&lt;p&gt;Prefer a more direct conversation? Feel free to &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;contact me&lt;/a&gt; anytime.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Security</category><category>Servers</category></item><item><title>Securing SSH: Essential Steps for Linux Servers</title><link>https://ivansalloum.com/securing-ssh-essential-steps-for-linux-servers/</link><guid isPermaLink="true">https://ivansalloum.com/securing-ssh-essential-steps-for-linux-servers/</guid><description>Discover key steps to secure SSH on Linux servers, covering crucial configurations and best practices for enhanced server security.</description><pubDate>Wed, 10 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;Last year, I woke up to dozens of failed login attempts on a client’s web server. Bots were hammering &lt;strong&gt;port 22&lt;/strong&gt; every few seconds – and if even one guess had landed, they’d have had full root access.&lt;/p&gt;&lt;p&gt;🙆‍♂️&lt;/p&gt;&lt;p&gt;&lt;strong&gt;That night, I realized:&lt;/strong&gt; SSH’s defaults are convenient, but they’re also a glaring invitation to attackers.&lt;/p&gt;&lt;p&gt;These days, one of the &lt;em&gt;first&lt;/em&gt;  things I do on any fresh server is lock down SSH. The out-of-the-box config is functional, sure – but it leaves way more doors open than I’m comfortable with.&lt;/p&gt;&lt;p&gt;In a &lt;a href=&quot;https://ivansalloum.com/preparing-your-ubuntu-server-for-first-use/&quot;&gt;previous guide&lt;/a&gt;, I covered a few essential SSH tweaks for new Ubuntu setups. In this one, we’re going deeper.&lt;/p&gt;&lt;p&gt;We&apos;ll walk through the &lt;code&gt;sshd_config&lt;/code&gt; file line by line, and I’ll share the exact changes I make on every server I manage.&lt;/p&gt;&lt;p&gt;Along the way, I’ll include practical, real-world tips – not just theory – so you can confidently harden your SSH setup without locking yourself out.&lt;/p&gt;&lt;h2&gt;The Basics – What SSH Really Is (And Why the Server Side Matters)&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;SSH (Secure Shell)&lt;/strong&gt;  is a protocol that lets you securely connect to and control a remote server over a network – usually from your terminal or an SSH client.&lt;/p&gt;&lt;p&gt;SSH is split into two parts:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;The &lt;strong&gt;server-side daemon&lt;/strong&gt;  (&lt;code&gt;sshd&lt;/code&gt;) that runs on the server you want to access&lt;/li&gt;&lt;li&gt;The &lt;strong&gt;client&lt;/strong&gt;  (&lt;code&gt;ssh&lt;/code&gt;) that you use to connect to it – usually from your own machine&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;When I&apos;m managing servers, I almost always connect from my Mac’s terminal. If you’re on Windows, a tool like &lt;a href=&quot;https://www.putty.org&quot;&gt;&lt;strong&gt;PuTTY&lt;/strong&gt;&lt;/a&gt;**(or Windows Terminal with OpenSSH) will do the job.&lt;/p&gt;&lt;p&gt;But here&apos;s the important part: &lt;strong&gt;security decisions are made on the server side&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;You can set whatever preferences you want on your SSH client – but at the end of the day, it’s the server (via the &lt;code&gt;sshd_config&lt;/code&gt; file) that controls whether password logins are allowed, which keys are accepted, and so on.&lt;/p&gt;&lt;p&gt;You’ll find SSH settings in these places:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Server:&lt;/strong&gt;  &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Client-wide:&lt;/strong&gt;  &lt;code&gt;/etc/ssh/ssh_config&lt;/code&gt;&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Per-user:&lt;/strong&gt;  &lt;code&gt;~/.ssh/config&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;🙆‍♂️&lt;/p&gt;&lt;p&gt;In this guide, I’m focusing entirely on server-side SSH – the stuff that actually locks down your &lt;strong&gt;server&lt;/strong&gt;.&lt;/p&gt;&lt;h2&gt;Before You Start: A Few Things I’ve Learned the Hard Way&lt;/h2&gt;&lt;p&gt;I&apos;ve hardened dozens of servers over the years, and one thing I’ve learned is that &lt;strong&gt;not every setting works for every situation&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;You need to think through how your server is used before blindly applying changes. A tweak that improves security in one setup might break something important in another.&lt;/p&gt;&lt;p&gt;For example: &lt;strong&gt;disabling password authentication&lt;/strong&gt;  is a great move – but if no key-based access is set up yet, you&apos;ll lock yourself out.&lt;/p&gt;&lt;p&gt;Here are a few best practices I personally follow every time:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Back up your config file&lt;/strong&gt;  before touching anything.&lt;/li&gt;&lt;li&gt;Most options in the file are commented out. &lt;strong&gt;Uncomment them&lt;/strong&gt;  before making changes.&lt;/li&gt;&lt;li&gt;Some server providers (like &lt;strong&gt;Hetzner&lt;/strong&gt;) use config overrides in &lt;code&gt;/etc/ssh/sshd_config.d/&lt;/code&gt;. Always double-check that directory if something you changed isn’t working.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;New to Hetzner? &lt;a href=&quot;https://hetzner.cloud/?ref=MC4Yy318xX5X&quot;&gt;Use my link&lt;/a&gt; to get free credits!&lt;/p&gt;&lt;h2&gt;How I Reload SSH Safely (So I Don’t Lock Myself Out)&lt;/h2&gt;&lt;p&gt;Once you’ve edited your &lt;code&gt;sshd_config&lt;/code&gt; file, here’s the exact process I use to apply changes without risking a disconnect.&lt;/p&gt;&lt;blockquote data-callout=&quot;security&quot;&gt;&lt;p&gt;&lt;strong&gt;Keep a second session open&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Before you reload, &lt;strong&gt;open a second SSH session and leave it connected&lt;/strong&gt;. If a config change locks you out, that live session is your way back in to fix it — without it, a single typo in &lt;code&gt;sshd_config&lt;/code&gt; can mean a trip to the rescue console.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Test for syntax errors:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo sshd -t
&lt;/pre&gt;&lt;p&gt;If there&apos;s a mistake, the command will tell you – &lt;em&gt;before&lt;/em&gt;  you break SSH access.&lt;/p&gt;&lt;p&gt;Reload the SSH service (&lt;strong&gt;don&lt;/strong&gt; ’t restart it):&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo systemctl reload ssh
&lt;/pre&gt;&lt;p&gt;Reloading is safer than restarting. It applies the new config but keeps your existing connection alive – which has saved me more than once.&lt;/p&gt;&lt;h2&gt;Change the Default SSH Port&lt;/h2&gt;&lt;p&gt;SSH runs on port &lt;code&gt;22&lt;/code&gt; by default – and every scanner on the internet knows it. One easy way to reduce background noise (and random login attempts) is to move SSH to a non-standard port.&lt;/p&gt;&lt;p&gt;Open your SSH config file:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo vim /etc/ssh/sshd_config
&lt;/pre&gt;&lt;p&gt;Look for the &lt;code&gt;Port&lt;/code&gt; line – it’s usually commented out and set to 22 by default. Uncomment it and choose a different port number:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;# [!file /etc/ssh/sshd_config]
#Port 22
Port 592 # [!code highlight]
&lt;/pre&gt;&lt;p&gt;Avoid using ports that are already taken by common services (like 80, 443, 3306, etc.). Pick something between &lt;strong&gt;1024 and 65535&lt;/strong&gt;  that doesn’t conflict with anything else on your server.&lt;/p&gt;&lt;p&gt;From now on, when you SSH into your server, you&apos;ll need to include the &lt;code&gt;-p&lt;/code&gt; flag with your new port:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ssh -p 592 username@your.server.ip
&lt;/pre&gt;&lt;p&gt;✅&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Personal Tip:&lt;/strong&gt; I usually keep one terminal open on the old connection while testing the new port – just in case something goes wrong.&lt;/p&gt;&lt;h2&gt;Add a UFW Rule to Protect SSH&lt;/h2&gt;&lt;p&gt;Changing your SSH port helps reduce noise, but a firewall adds an extra layer of protection – especially against brute-force attacks.&lt;/p&gt;&lt;p&gt;If you&apos;re using UFW (which ships with Ubuntu), you can rate-limit SSH connections like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw limit 22/tcp
&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;limit&lt;/code&gt; rule allows connections but throttles repeated attempts from the same IP – perfect for slowing down brute-force bots without locking yourself out.&lt;/p&gt;&lt;p&gt;⚠️&lt;/p&gt;&lt;p&gt;If you changed the SSH port, make sure you &lt;code&gt;limit&lt;/code&gt; &lt;strong&gt;that&lt;/strong&gt;  port, not just the default &lt;code&gt;22&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;You can check your firewall status with:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw status
&lt;/pre&gt;&lt;p&gt;And if UFW isn’t enabled yet:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo ufw enable
&lt;/pre&gt;&lt;p&gt;🙆‍♂️&lt;/p&gt;&lt;p&gt;I always apply this step after changing the SSH port – it’s quick, simple, and adds another layer of defense.&lt;/p&gt;&lt;h2&gt;Fine-Tuning SSH Login Behavior&lt;/h2&gt;&lt;p&gt;While scrolling through your &lt;code&gt;sshd_config&lt;/code&gt; file, you’ll likely spot the following three lines – commented out by default:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;#LoginGraceTime 2m #MaxAuthTries 6 #MaxSessions 10
&lt;/pre&gt;&lt;p&gt;These settings control how users connect to your server – how long they have to log in, how many times they can try, and how many sessions they can open. The defaults are fairly relaxed, but in most cases, they’re more permissive than they need to be.&lt;/p&gt;&lt;p&gt;Here’s how I typically adjust them to reduce risk without getting in the way of normal usage:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;LoginGraceTime 20 MaxAuthTries 3 MaxSessions 5
&lt;/pre&gt;&lt;p&gt;What these do:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;LoginGraceTime 20&lt;/code&gt;:&lt;/strong&gt; This shortens the authentication window from 2 minutes to just 20 seconds. If someone can’t log in within that time, it’s usually a sign something’s wrong – or someone’s probing your server.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;MaxAuthTries 3&lt;/code&gt;:&lt;/strong&gt; Reducing the number of allowed login attempts from 6 to 3 helps block brute-force attacks more quickly. I’ve found that 3 strikes is plenty if you&apos;re using keys or proper credentials.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;MaxSessions 5&lt;/code&gt;:&lt;/strong&gt; By default, users can open up to 10 concurrent sessions. That’s rarely necessary. Cutting it down to 5 adds another layer of control without affecting usability.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;🙆‍♂️&lt;/p&gt;&lt;p&gt;These tweaks are &lt;strong&gt;subtle but effective&lt;/strong&gt; , and they’re part of my default SSH hardening routine on every server I manage.&lt;/p&gt;&lt;h2&gt;Prevent Logins with Empty Passwords&lt;/h2&gt;&lt;p&gt;Yes, it’s technically possible on a Linux server to create users without passwords – but it’s &lt;strong&gt;never&lt;/strong&gt;  a good idea. That would allow anyone to log in without authentication, which is obviously a huge security risk.&lt;/p&gt;&lt;p&gt;Thankfully, SSH is configured to &lt;strong&gt;block empty-password logins by default&lt;/strong&gt;  – but I always like to double-check this setting just to be safe.&lt;/p&gt;&lt;p&gt;Open your &lt;code&gt;sshd_config&lt;/code&gt; file and look for:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;#PermitEmptyPasswords no
&lt;/pre&gt;&lt;p&gt;As long as it&apos;s set to &lt;code&gt;no&lt;/code&gt; (even if it’s still commented out), you’re good. No one with an empty password will be allowed to connect over SSH.&lt;/p&gt;&lt;p&gt;If, for any reason, it&apos;s set to &lt;code&gt;yes&lt;/code&gt;, change it to:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;PermitEmptyPasswords no
&lt;/pre&gt;&lt;p&gt;✅&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Personal Tip:&lt;/strong&gt; I usually leave this one commented, but I always verify it&apos;s not accidentally changed – especially on servers I didn’t set up myself.&lt;/p&gt;&lt;h2&gt;Set an Idle Timeout Interval&lt;/h2&gt;&lt;p&gt;Leaving an SSH session open and idle – especially on a shared or unattended machine – can create a serious security risk.&lt;/p&gt;&lt;p&gt;To reduce that risk, I always configure SSH to &lt;strong&gt;automatically close idle connections&lt;/strong&gt;  after a short period of inactivity.&lt;/p&gt;&lt;p&gt;In your &lt;code&gt;sshd_config&lt;/code&gt; file, these two settings control that behavior:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;#ClientAliveInterval 0 #ClientAliveCountMax 3
&lt;/pre&gt;&lt;p&gt;By default, SSH does &lt;strong&gt;not&lt;/strong&gt;  close idle connections unless you explicitly configure it to do so. Here’s what the default values mean:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;**ClientAliveInterval**&lt;/code&gt; is set to &lt;code&gt;0&lt;/code&gt;, which means idle timeout is &lt;strong&gt;disabled by default&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;&lt;code&gt;**ClientAliveCountMax**&lt;/code&gt; is set to &lt;code&gt;3&lt;/code&gt;, but it only takes effect if &lt;code&gt;ClientAliveInterval&lt;/code&gt; is greater than &lt;code&gt;0&lt;/code&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Uncomment and update the values like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ClientAliveInterval 60 ClientAliveCountMax 3
&lt;/pre&gt;&lt;p&gt;Here’s what they do:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;ClientAliveInterval&lt;/code&gt;:&lt;/strong&gt; This sets the number of seconds the server waits before sending a keep-alive check to the client. I typically set it to &lt;code&gt;60&lt;/code&gt;, meaning the server checks in once per minute.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;ClientAliveCountMax&lt;/code&gt;:&lt;/strong&gt; This defines how many missed responses are allowed before the server disconnects the session. With a value of &lt;code&gt;3&lt;/code&gt;, that gives users up to &lt;strong&gt;180 seconds (3 minutes)&lt;/strong&gt;  of inactivity before the session is closed.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;✅&lt;/p&gt;&lt;p&gt;With these settings in place, &lt;strong&gt;idle SSH sessions&lt;/strong&gt; will be automatically closed after 3 minutes – a great safety net without being too aggressive.&lt;/p&gt;&lt;h2&gt;Disable Rhosts-Based Authentication&lt;/h2&gt;&lt;p&gt;Rhosts-based authentication is an old, insecure method that relies on trusting users based on their IP address or hostname – without requiring a password or key. It was more common in the early days of Unix systems, but it’s now considered unsafe and outdated.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;SSH disables it by default&lt;/strong&gt; , but I always recommend double-checking – especially on older servers or images that may carry legacy settings.&lt;/p&gt;&lt;p&gt;In your &lt;code&gt;sshd_config&lt;/code&gt; file, look for:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;#IgnoreRhosts yes
&lt;/pre&gt;&lt;p&gt;If it’s already set to &lt;code&gt;yes&lt;/code&gt; (even if commented), you’re fine. But if it’s been changed, make sure it looks like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;IgnoreRhosts yes
&lt;/pre&gt;&lt;p&gt;This ensures SSH will completely ignore &lt;code&gt;.rhosts&lt;/code&gt; files, even if they exist – closing off an old and unnecessary security risk.&lt;/p&gt;&lt;h2&gt;Disable X11 Forwarding&lt;/h2&gt;&lt;p&gt;By default, SSH allows &lt;strong&gt;X11 forwarding&lt;/strong&gt; , which lets you run graphical applications from a remote server and display them locally.&lt;/p&gt;&lt;p&gt;While that can be useful in specific cases (like running a GUI app on a remote server), &lt;strong&gt;it’s rarely needed on production servers&lt;/strong&gt;  – and the X11 protocol isn’t built with security in mind.&lt;/p&gt;&lt;p&gt;If you&apos;re not using it (and chances are, you&apos;re not), it’s best to disable it:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;X11Forwarding no
&lt;/pre&gt;&lt;p&gt;I disable this on every server I deploy. It’s a simple step that helps reduce your server’s attack surface by turning off a feature you likely don’t need.&lt;/p&gt;&lt;h2&gt;Disable Agent and TCP Forwarding&lt;/h2&gt;&lt;p&gt;SSH supports &lt;strong&gt;agent forwarding&lt;/strong&gt;  and &lt;strong&gt;TCP forwarding&lt;/strong&gt; , both useful in certain workflows – but they also come with security risks if left enabled unnecessarily.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Agent Forwarding:&lt;/strong&gt; It allows you to use your local SSH keys when hopping from one server to another – without copying keys to the first server. It’s convenient, but it &lt;strong&gt;can expose your keys&lt;/strong&gt;  if the intermediate server is compromised.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;TCP Forwarding (Port Forwarding):&lt;/strong&gt; It lets you route network traffic between your local machine and remote server – great for tunneling traffic or exposing local services temporarily. But again, if you’re not using it, it’s just &lt;strong&gt;one more thing attackers can potentially abuse&lt;/strong&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;If you don’t explicitly need these features, it’s best to disable them:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;AllowAgentForwarding no AllowTcpForwarding no
&lt;/pre&gt;&lt;p&gt;These are typically enabled by default, so I always turn them off – unless there&apos;s a specific reason to keep them on.&lt;/p&gt;&lt;h2&gt;Disable Root SSH Access&lt;/h2&gt;&lt;p&gt;When you first set up a Linux server, it&apos;s common to log in as the &lt;strong&gt;root user&lt;/strong&gt; – but it’s not something you should keep doing.&lt;/p&gt;&lt;p&gt;The root account has full control over the server, so it&apos;s easy to make a mistake that causes serious damage. It’s also a common target for brute-force attacks since the username is the same on every server.&lt;/p&gt;&lt;p&gt;Instead, I always recommend logging in with a &lt;strong&gt;non-root user&lt;/strong&gt;  that has &lt;code&gt;sudo&lt;/code&gt; privileges. This way:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;You’re prompted for your password before running administrative commands&lt;/li&gt;&lt;li&gt;Your actions are logged and traceable&lt;/li&gt;&lt;li&gt;The username isn’t obvious to attackers&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Here’s how to add one:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;adduser username usermod -aG sudo username
&lt;/pre&gt;&lt;p&gt;Now, locate the &lt;code&gt;PermitRootLogin&lt;/code&gt; variable, which is usually commented out by default, and change its value to:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;PermitRootLogin no
&lt;/pre&gt;&lt;p&gt;✅&lt;/p&gt;&lt;p&gt;Now, the root user won’t be able to log in over SSH.&lt;/p&gt;&lt;p&gt;You’ll need to connect using your non-root user – which is much harder for an attacker to guess.&lt;/p&gt;&lt;h2&gt;Disable Password Authentication&lt;/h2&gt;&lt;p&gt;By default, you can log into a server over SSH using just a username and password. But there&apos;s a much more secure way: &lt;strong&gt;SSH key authentication&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;This method uses a &lt;strong&gt;key pair&lt;/strong&gt;  – a public key (stored on the server) and a private key (kept on your machine). When you connect, the server checks if you have the matching private key. If you do, you&apos;re in.&lt;/p&gt;&lt;p&gt;This means no passwords to guess – only someone with your private key can log in.&lt;/p&gt;&lt;p&gt;To create a secure key pair, run:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ssh-keygen -b 4096
&lt;/pre&gt;&lt;p&gt;You’ll be prompted to choose a file path for saving the key – press &lt;strong&gt;ENTER&lt;/strong&gt; to accept the default or enter a custom path to avoid overwriting any existing key.&lt;/p&gt;&lt;p&gt;You’ll also be asked to enter a passphrase. While optional, using a passphrase adds another layer of security and is strongly recommended.&lt;/p&gt;&lt;p&gt;Once done, two files will be created in your &lt;code&gt;~/.ssh&lt;/code&gt; directory:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;id_ed25519&lt;/code&gt; – your &lt;strong&gt;private key&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;&lt;code&gt;id_ed25519.pub&lt;/code&gt; – your &lt;strong&gt;public key&lt;/strong&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;To enable key-based access for your &lt;strong&gt;non-root user&lt;/strong&gt; , copy the public key to the server using:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ssh-copy-id -i ~/.ssh/id_ed25519.pub your.non.root.user@your.server.ip
&lt;/pre&gt;&lt;p&gt;Once you’ve confirmed that key-based login is working, open your &lt;code&gt;sshd_config&lt;/code&gt; file and look for:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;#PasswordAuthentication yes
&lt;/pre&gt;&lt;p&gt;Uncomment it and change the value to:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;PasswordAuthentication no
&lt;/pre&gt;&lt;p&gt;This ensures your server only accepts logins from users with a valid SSH key.&lt;/p&gt;&lt;p&gt;⚠️&lt;/p&gt;&lt;p&gt;Make sure to &lt;strong&gt;keep your private key secure&lt;/strong&gt;  – it’s your access pass.&lt;/p&gt;&lt;h2&gt;Restrict Who Can SSH Into the Server&lt;/h2&gt;&lt;p&gt;Not every user on your server needs SSH access. In fact, &lt;strong&gt;the fewer people with SSH access, the better&lt;/strong&gt;  – it tightens security and reduces your attack surface.&lt;/p&gt;&lt;p&gt;There are a couple of ways to limit who can log in over SSH.&lt;/p&gt;&lt;h3&gt;Option 1: Use &lt;code&gt;AllowUsers&lt;/code&gt; (my preferred method)&lt;/h3&gt;&lt;p&gt;This method is direct and easy to manage. You specify exactly which users are allowed to connect via SSH – everyone else is denied by default.&lt;/p&gt;&lt;p&gt;At the end of your &lt;code&gt;sshd_config&lt;/code&gt; file, add:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;AllowUsers ivan john elie
&lt;/pre&gt;&lt;p&gt;Now, only &lt;code&gt;ivan&lt;/code&gt;, &lt;code&gt;john&lt;/code&gt;, and &lt;code&gt;elie&lt;/code&gt; can log in via SSH.&lt;/p&gt;&lt;h3&gt;Option 2: Use &lt;code&gt;AllowGroups&lt;/code&gt;&lt;/h3&gt;&lt;p&gt;You can also create a special group (like &lt;code&gt;sshusers&lt;/code&gt;), add your allowed users to it, and then add this to your config:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;AllowGroups sshusers
&lt;/pre&gt;&lt;p&gt;This works well if you want to manage access by group instead of listing users individually.&lt;/p&gt;&lt;h2&gt;Restrict Access by IP (Optional but Powerful)&lt;/h2&gt;&lt;p&gt;If you have a &lt;strong&gt;static IP address&lt;/strong&gt;  (one that doesn&apos;t change), you can lock down SSH access even further – only allowing connections from your trusted IP.&lt;/p&gt;&lt;p&gt;You can do this with the same &lt;code&gt;AllowUsers&lt;/code&gt; directive:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;AllowUsers ivan@203.0.113.45
&lt;/pre&gt;&lt;p&gt;Now only &lt;code&gt;ivan&lt;/code&gt; connecting from IP &lt;code&gt;203.0.113.45&lt;/code&gt; can access the server.&lt;/p&gt;&lt;p&gt;Want to allow &lt;strong&gt;all users&lt;/strong&gt; , but only from one IP? Use:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;AllowUsers *@203.0.113.45
&lt;/pre&gt;&lt;p&gt;This is a great defense-in-depth tactic – just make sure you’re not on a dynamic IP (which changes), or you might accidentally lock yourself out.&lt;/p&gt;&lt;h2&gt;Reload SSH (Don’t Forget!)&lt;/h2&gt;&lt;p&gt;Once you&apos;ve made any changes, always validate your SSH config before applying it:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo sshd -t
&lt;/pre&gt;&lt;p&gt;If there are no errors, reload SSH to apply your changes:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo systemctl reload ssh
&lt;/pre&gt;&lt;p&gt;You can also check the final, active SSH config with:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo sshd -T
&lt;/pre&gt;&lt;p&gt;This lets you confirm that your new settings – like &lt;code&gt;AllowUsers&lt;/code&gt; – are active and working.&lt;/p&gt;&lt;h2&gt;Bonus: Use &lt;code&gt;sshd_config.d/&lt;/code&gt; for Cleaner &amp;amp; More Scalable SSH Config&lt;/h2&gt;&lt;p&gt;If you&apos;re managing multiple servers – or just want a cleaner setup – consider using the &lt;code&gt;sshd_config.d/&lt;/code&gt; directory instead of editing the main config file directly.&lt;/p&gt;&lt;p&gt;Modern Linux distros support config drop-ins via:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;/etc/ssh/sshd_config.d/
&lt;/pre&gt;&lt;p&gt;Instead of changing &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt;, create a new file like this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo vim /etc/ssh/sshd_config.d/hardening.conf
&lt;/pre&gt;&lt;p&gt;Add your custom settings there – for example:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;Port 592 PermitRootLogin no PasswordAuthentication no AllowUsers ivan ClientAliveInterval 60 ClientAliveCountMax 3
&lt;/pre&gt;&lt;p&gt;This method keeps your config organized, survives server updates, and makes it easier to sync settings across multiple servers with version control or automation.&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;I hope this guide helped you go beyond the basics and gave you a clear, practical path to &lt;strong&gt;securing SSH on your Linux server&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Even small tweaks – like changing the SSH port or turning off unused features – can make a &lt;strong&gt;big difference&lt;/strong&gt;  in reducing risk and tightening your server’s defenses.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;You can check out my full collection of detailed Linux server security guides &lt;a href=&quot;https://ivansalloum.com/collections/linux-server-security/&quot;&gt;here&lt;/a&gt; – all based on real experience managing my own servers.&lt;/p&gt;&lt;hr&gt;&lt;p&gt;&lt;strong&gt;💬  Found this guide helpful?&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;I&apos;d love to hear about your experiences, questions, or ideas in the discussion section below. Your feedback not only helps improve future guides – it helps fellow admins on their own journey.&lt;/p&gt;&lt;p&gt;Prefer a more direct conversation? Feel free to &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;contact me&lt;/a&gt; anytime.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Security</category></item><item><title>How to Install and Use WordPress Locally</title><link>https://ivansalloum.com/how-to-install-and-use-wordpress-locally/</link><guid isPermaLink="true">https://ivansalloum.com/how-to-install-and-use-wordpress-locally/</guid><description>Learn to install and use WordPress locally with this simple step-by-step guide, perfect for safe website development and testing.</description><pubDate>Mon, 25 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;Are you planning to create a website in WordPress, but don&apos;t want to do it directly on the server?&lt;/p&gt;&lt;p&gt;Discover the smart solution: installing and using WordPress locally.&lt;/p&gt;&lt;p&gt;This approach streamlines your website creation process, allowing you to experiment and refine your site in a safe, offline environment.&lt;/p&gt;&lt;p&gt;In this guide, I will show you step-by-step how to install and use WordPress locally.&lt;/p&gt;&lt;p&gt;Plus, I&apos;ll also guide you through the process of taking your site live, ensuring a smooth transition from local development to online presence.&lt;/p&gt;&lt;h2&gt;Why WordPress Locally?&lt;/h2&gt;&lt;p&gt;Installing WordPress locally means your site lives on your computer.&lt;/p&gt;&lt;p&gt;It&apos;s your private playground – unseen by the outside world and search engines until you&apos;re ready.&lt;/p&gt;&lt;p&gt;This setup is ideal for creating a new site from scratch. You get to decide when it goes live.&lt;/p&gt;&lt;p&gt;A local WordPress site is a safe zone for experiments. Want to try new themes or plugins? Go ahead, without the fear of breaking your live site.&lt;/p&gt;&lt;p&gt;Updates, new features, even major redesigns – you can test them all in this secure environment.&lt;/p&gt;&lt;p&gt;If you&apos;re new to WordPress or coding, think of a local site as a practice area.&lt;/p&gt;&lt;p&gt;You can play around with WordPress files safely, without worrying about losing data or crashing your site.&lt;/p&gt;&lt;p&gt;It&apos;s a secure way to learn and try new things.&lt;/p&gt;&lt;p&gt;One of the cool things about a local WordPress site is it doesn&apos;t need the internet.&lt;/p&gt;&lt;p&gt;Work on your site anytime, anywhere, regardless of internet connectivity or speed. Perfect for those moments when you&apos;re offline.&lt;/p&gt;&lt;p&gt;And here&apos;s the best part: it&apos;s free.&lt;/p&gt;&lt;p&gt;Beginners can learn all about managing websites and WordPress without paying for hosting or domains. It&apos;s practical learning without spending money.&lt;/p&gt;&lt;p&gt;In short, a local WordPress installation is a powerful, risk-free, and cost-effective way to build and refine your WordPress skills and site.&lt;/p&gt;&lt;p&gt;It&apos;s all about creating, experimenting, and learning at your own pace, in your own space.&lt;/p&gt;&lt;h2&gt;Installing WordPress Locally&lt;/h2&gt;&lt;p&gt;To successfully install and use WordPress locally, you need to set up a few key components: a web server, PHP, and a MySQL database.&lt;/p&gt;&lt;p&gt;While it&apos;s possible to install these elements manually, it&apos;s not the most efficient route.&lt;/p&gt;&lt;p&gt;Manual installation requires individual setup and configuration for each component to create a functional local environment for WordPress.&lt;/p&gt;&lt;p&gt;Fortunately, there are numerous software options available that simplify this process.&lt;/p&gt;&lt;p&gt;These tools allow you to establish a local environment with just a few clicks, often including additional features and add-ons for an enhanced experience.&lt;/p&gt;&lt;p&gt;This approach saves time and reduces complexity, making it an ideal choice for both beginners and experienced users.&lt;/p&gt;&lt;p&gt;In this guide, we&apos;re going to use a tool called Local.&lt;/p&gt;&lt;p&gt;I&apos;ve been using Local for a long time and I absolutely love it.&lt;/p&gt;&lt;p&gt;It streamlines the process, making it accessible and straightforward, even for those new to WordPress.&lt;/p&gt;&lt;p&gt;Go to &lt;a href=&quot;https://localwp.com/&quot;&gt;localwp.com&lt;/a&gt; and click on the &lt;strong&gt;DOWNLOAD FOR FREE&lt;/strong&gt;  button.&lt;/p&gt;&lt;p&gt;You&apos;ll be prompted to select your platform.&lt;/p&gt;&lt;p&gt;If you&apos;re a Mac user like me, you have the option to install Local using &lt;a href=&quot;https://formulae.brew.sh/cask/local&quot;&gt;Homebrew&lt;/a&gt;, a convenient alternative.&lt;/p&gt;&lt;p&gt;Next, you&apos;ll need to provide some basic information, including your first and last name, work email, and the type of organization you&apos;re associated with.&lt;/p&gt;&lt;p&gt;After filling out these details, click on the &lt;strong&gt;GET IT NOW!&lt;/strong&gt;  button, and the download will start automatically.&lt;/p&gt;&lt;p&gt;Once the download is complete, open the installation file to install Local on your computer.&lt;/p&gt;&lt;h3&gt;Quick Overview&lt;/h3&gt;&lt;p&gt;After installing Local, open the application to begin exploring its features.&lt;/p&gt;&lt;p&gt;Initially, you&apos;ll be greeted with a page indicating that you haven&apos;t created any sites yet – that&apos;s perfectly normal, as we&apos;ll be creating a new site shortly.&lt;/p&gt;&lt;p&gt;Local&apos;s interface is user-friendly, featuring a menu on the left with five distinct sections.&lt;/p&gt;&lt;p&gt;The first section displays the local WordPress sites you create, which will be empty for now.&lt;/p&gt;&lt;p&gt;The second section is for linking Local to WP Engine or Flywheel, enabling you to push your local site live if you have hosting with either of these providers.&lt;/p&gt;&lt;p&gt;The third section introduces Blueprints, a convenient feature for quickly starting new sites using pre-saved settings, including themes and plugins – ideal for replicating a site.&lt;/p&gt;&lt;p&gt;The fourth section contains various add-ons to enhance your Local experience.&lt;/p&gt;&lt;p&gt;Finally, the last section is a valuable resource for support, offering helpful articles and troubleshooting guides.&lt;/p&gt;&lt;h3&gt;Preferences&lt;/h3&gt;&lt;p&gt;Start by clicking the account icon and selecting &lt;strong&gt;Preferences&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;This brings up an interface with four sections for customization.&lt;/p&gt;&lt;p&gt;In the &lt;strong&gt;Appearance &amp;amp; Behavior&lt;/strong&gt; section, you can change Local&apos;s look and toggle MagicSync on or off, and set your default terminal and browser.&lt;/p&gt;&lt;p&gt;The &lt;strong&gt;New site defaults&lt;/strong&gt;  section lets you adjust default settings for new sites, like environment type, WordPress admin email, domain suffix, and file storage location.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Exporting&lt;/strong&gt;  offers an option to exclude certain files during export.&lt;/p&gt;&lt;p&gt;Lastly, the &lt;strong&gt;Advanced&lt;/strong&gt;  section is where you can adjust the router mode (which I suggest leaving as is). Here, you can also turn on the &lt;strong&gt;Show Develop menu&lt;/strong&gt;  option for debugging tools, and enable &lt;strong&gt;Usage&lt;/strong&gt;  and &lt;strong&gt;Error&lt;/strong&gt;  Reporting to help improve Local.&lt;/p&gt;&lt;p&gt;I usually keep all the default settings as they are, except for the admin email of WordPress.&lt;/p&gt;&lt;h3&gt;Creating a Site&lt;/h3&gt;&lt;p&gt;Now, it&apos;s time to create your new site.&lt;/p&gt;&lt;p&gt;Begin by clicking on the &lt;strong&gt;+ Create a new site&lt;/strong&gt;  button.&lt;/p&gt;&lt;p&gt;You&apos;ll see a page where you can choose to start from scratch or use a blueprint. Since you don’t have any blueprints yet, select &lt;strong&gt;Create a new site&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Next, name your site. If you want, you can click on the &lt;strong&gt;Advanced options&lt;/strong&gt;  toggle to customize the domain and path.&lt;/p&gt;&lt;p&gt;For your site&apos;s environment, you can pick the &lt;strong&gt;Preferred&lt;/strong&gt;  option or set up a custom one. I usually choose the &lt;strong&gt;Preferred&lt;/strong&gt; option.&lt;/p&gt;&lt;p&gt;Then, enter your WordPress username, password, and admin email. Under &lt;strong&gt;Advanced options&lt;/strong&gt; , there’s also the choice to enable WordPress Multisite.&lt;/p&gt;&lt;p&gt;Once you&apos;ve filled in all the details, click on the &lt;strong&gt;Add Site&lt;/strong&gt;  button, and your new site will be set up and ready to go.&lt;/p&gt;&lt;h3&gt;Site&apos;s Dashboard&lt;/h3&gt;&lt;p&gt;Wow, we now have a local WordPress site to play with!&lt;/p&gt;&lt;p&gt;Wasn&apos;t that easy? In just a few minutes and clicks, we&apos;ve set up our very own local environment for WordPress.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://ivansalloum.com/content/images/2024/10/local-wp-site.webp&quot; alt=&quot;SIte&apos;s Dashboard&quot;&gt;&lt;/p&gt;&lt;p&gt;You can launch the site in your browser by clicking on the &lt;strong&gt;Open Site&lt;/strong&gt;  button.&lt;/p&gt;&lt;p&gt;To directly access the dashboard, click on the &lt;strong&gt;WP Admin&lt;/strong&gt;  button. For this, make sure to enable the &lt;strong&gt;One-click admin&lt;/strong&gt; option and select the user you wish to log in with.&lt;/p&gt;&lt;p&gt;Remember, even if you chose an environment while creating the site, you can still change the web server and PHP version later if needed.&lt;/p&gt;&lt;p&gt;You can enable &lt;strong&gt;Xdebug&lt;/strong&gt;  if you want. It helps in troubleshooting complex PHP issues.&lt;/p&gt;&lt;p&gt;See the notes sidebar? That&apos;s there because I installed an add-on called &lt;strong&gt;Notes&lt;/strong&gt;. Feel free to explore the add-ons section and install any that you find useful.&lt;/p&gt;&lt;p&gt;If you click on the three dots next to the site&apos;s name, you will see a list of options.&lt;/p&gt;&lt;p&gt;You can open the site or go directly to the admin dashboard. You have options to stop, restart, or start the site. Additionally, you can clone or export the site.&lt;/p&gt;&lt;p&gt;Creating a blueprint of the site&apos;s current state is possible too, allowing you to use it as a template for new sites.&lt;/p&gt;&lt;p&gt;There&apos;s also the option to create a new group and move the site into it.&lt;/p&gt;&lt;p&gt;Moreover, you can change the domain, rename the site, or delete it.&lt;/p&gt;&lt;h4&gt;Database&lt;/h4&gt;&lt;p&gt;The &lt;strong&gt;Database&lt;/strong&gt;  section provides information about your database and includes a tool called Adminer.&lt;/p&gt;&lt;p&gt;This tool allows you to connect to and manage your database directly in the browser.&lt;/p&gt;&lt;p&gt;By clicking on the &lt;strong&gt;Open Adminer&lt;/strong&gt;  link, a new tab will open in your browser where you can manage the database effectively.&lt;/p&gt;&lt;h4&gt;Tools&lt;/h4&gt;&lt;p&gt;In the &lt;strong&gt;Tools&lt;/strong&gt;  section, you&apos;ll find useful tools like Mailhog. It helps you see emails from your WordPress sites.&lt;/p&gt;&lt;p&gt;Just click on the &lt;strong&gt;Open Mailhog&lt;/strong&gt;  link, and you can check these emails in your browser.&lt;/p&gt;&lt;p&gt;Another handy tool is &lt;strong&gt;Live Links&lt;/strong&gt;. This allows you to create a shareable link of your local site, which is great for showing your work to clients. You can share the live link online, giving others access to view your site.&lt;/p&gt;&lt;p&gt;However, to use this feature, you need to have a Local account. Feel free to create one. It&apos;s free.&lt;/p&gt;&lt;h2&gt;Taking Your Site Live&lt;/h2&gt;&lt;p&gt;After you&apos;ve finished editing and preparing your site, it&apos;s time to make it live and publicly accessible. But how do you do that? Let me guide you through it.&lt;/p&gt;&lt;p&gt;If you&apos;re using WP Engine or Flywheel, you can utilize the &lt;strong&gt;Connect&lt;/strong&gt;  feature. However, I&apos;ll show you a method that works with any hosting provider.&lt;/p&gt;&lt;p&gt;Firstly, install and activate the &lt;strong&gt;All-in-One WP Migration&lt;/strong&gt;  plugin on both your local site and your live site.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://wordpress.org/plugins/all-in-one-wp-migration/&quot;&gt;All-in-One WP Migration on WordPress.org →&lt;/a&gt;&lt;/p&gt;&lt;p&gt;Next, on your local site, navigate to the plugin settings and select &lt;strong&gt;Export&lt;/strong&gt;  from the menu.&lt;/p&gt;&lt;p&gt;Replace the URL of your local site with the URL of your live site, then export it. Choose the &lt;strong&gt;EXPORT TO FILE&lt;/strong&gt;  option to proceed.&lt;/p&gt;&lt;p&gt;Once the export process is complete, you can download the file that has been generated.&lt;/p&gt;&lt;p&gt;Now, go to your live site, access the plugin settings, and choose &lt;strong&gt;Import&lt;/strong&gt;  from the menu.&lt;/p&gt;&lt;p&gt;Then, upload the file you just downloaded. You’ll receive a warning that this process will overwrite the entire website, including the database, media, plugins, and themes. This is expected and exactly what we need to do.&lt;/p&gt;&lt;p&gt;And that&apos;s it!&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;I hope this guide has been a valuable resource for learning how to install and use WordPress locally.&lt;/p&gt;&lt;p&gt;By working in a safe, offline environment, you can build and fine-tune your website without the pressure of being live. Once you&apos;re ready, taking your site live will be a smooth process with the steps we&apos;ve covered.&lt;/p&gt;&lt;p&gt;If you found value in this guide or have any questions or feedback, please don&apos;t hesitate to share your thoughts in the discussion section.&lt;/p&gt;&lt;p&gt;Your input is greatly appreciated, and you can also &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;contact me&lt;/a&gt; directly if you prefer.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>WordPress</category></item><item><title>How to Connect to Your VPS Server</title><link>https://ivansalloum.com/how-to-connect-to-your-vps-server/</link><guid isPermaLink="true">https://ivansalloum.com/how-to-connect-to-your-vps-server/</guid><description>Learn to securely connect to your VPS server using Secure Shell (SSH) with this simple guide.</description><pubDate>Sun, 12 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;In an earlier guide, I covered the &lt;a href=&quot;https://ivansalloum.com/getting-started-with-your-first-vps-server/&quot;&gt;basics of VPS servers&lt;/a&gt; and deploying your first one.&lt;/p&gt;&lt;p&gt;Now, it&apos;s time to focus on securely connecting to your VPS server.&lt;/p&gt;&lt;p&gt;This simple guide will show you how to connect to your VPS server using Secure Shell (SSH).&lt;/p&gt;&lt;h2&gt;What is Secure Shell?&lt;/h2&gt;&lt;p&gt;Secure Shell, or SSH, allows two computers to create a secure, direct connection within a potentially insecure network, like the internet.&lt;/p&gt;&lt;p&gt;This is crucial to prevent others from intercepting the data flow and accessing sensitive information.&lt;/p&gt;&lt;p&gt;Before SSH, there were different ways, but they needed to be safer.&lt;/p&gt;&lt;p&gt;Applications like Telnet, Remote Shell, or rlogin were often used, but they had serious security problems.&lt;/p&gt;&lt;p&gt;SSH encrypts the connection between the two computers, enabling one computer to control another.&lt;/p&gt;&lt;p&gt;For us, it&apos;s how we connect to our VPS servers and administrate them from our computer.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;The development of SSH has influenced other protocols. For instance, the insecure FTP protocol, used for uploading and downloading files, evolved into the SSH File Transfer Protocol (SFTP).&lt;/p&gt;&lt;p&gt;One of SSH&apos;s advantages is its compatibility across major operating systems.&lt;/p&gt;&lt;p&gt;Originating as a Unix application, it&apos;s inherently implemented in Linux distributions and macOS.&lt;/p&gt;&lt;p&gt;All Linux-based VPS servers let you connect to them directly using SSH, and that&apos;s exactly what we&apos;ll do in this guide.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;People often use &lt;strong&gt;ssh&lt;/strong&gt;  as a verb, saying &lt;strong&gt;how to ssh into a VPS server&lt;/strong&gt;  instead of &lt;strong&gt;how to connect to a VPS server&lt;/strong&gt;.&lt;/p&gt;&lt;h2&gt;Connecting to a VPS Server&lt;/h2&gt;&lt;p&gt;To connect to your VPS server, you&apos;ll need the root password and the server&apos;s IP address.&lt;/p&gt;&lt;p&gt;Many providers either let you set this password while creating the server on their website or send it to you via email along with the server details, like Hetzner does.&lt;/p&gt;&lt;p&gt;The root password provided by providers like Hetzner is temporary. Once you&apos;re in your VPS server, you&apos;ll need to change it.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;New to Hetzner? &lt;a href=&quot;https://hetzner.cloud/?ref=MC4Yy318xX5X&quot;&gt;Use my link&lt;/a&gt; to get free credits!&lt;/p&gt;&lt;p&gt;Once you have the server IP and root password, connecting to the VPS server is simple.&lt;/p&gt;&lt;p&gt;Just open the local terminal and type this:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;ssh root@
&lt;/pre&gt;&lt;p&gt;Enter the password, and that&apos;s it – you&apos;re in!&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;On Mac or Linux, you can simply use the local terminal. For Windows, I suggest using &lt;a href=&quot;https://gitforwindows.org/&quot;&gt;Git Bash&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;If it&apos;s your first time connecting to the server (which I assume it is, otherwise, you wouldn&apos;t be here reading this guide haha), your terminal might ask if you want to add this server to your list of known hosts - type &lt;strong&gt;yes&lt;/strong&gt;  to add it.&lt;/p&gt;&lt;p&gt;You will now be able to run commands on the server directly from your terminal.&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;Simple, right?&lt;/p&gt;&lt;p&gt;I hope this simple guide has been helpful for you.&lt;/p&gt;&lt;p&gt;If you found value in this guide or have any questions or feedback, please don&apos;t hesitate to share your thoughts in the discussion section.&lt;/p&gt;&lt;p&gt;Your input is greatly appreciated, and you can also &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;contact me&lt;/a&gt; directly if you prefer.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Servers</category></item><item><title>Getting Started with Your First VPS Server</title><link>https://ivansalloum.com/getting-started-with-your-first-vps-server/</link><guid isPermaLink="true">https://ivansalloum.com/getting-started-with-your-first-vps-server/</guid><description>Dive into VPS servers! Learn the basics, cool tips, and deploy your first server with ease.</description><pubDate>Thu, 09 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;If you&apos;re curious about what a VPS server is and how to deploy your first one, you&apos;re in the right place.&lt;/p&gt;&lt;p&gt;In this guide, I&apos;ll explain in simple terms what VPS servers are, cool things you can do with them, personal tips from someone who uses them, and I&apos;ll even guide you through deploying your very first VPS server for free.&lt;/p&gt;&lt;p&gt;And guess what? After that, I&apos;ve got some simple steps for what to do next!&lt;/p&gt;&lt;p&gt;Let&apos;s get started!&lt;/p&gt;&lt;h2&gt;What is a VPS Server?&lt;/h2&gt;&lt;p&gt;A VPS, or Virtual Private Server, is like having your own private workspace on a powerful computer that many people share.&lt;/p&gt;&lt;p&gt;Imagine a supercomputer that&apos;s used by lots of people, like a library computer that anyone can use. However, the computer is so advanced that it can be split into many smaller, private workspaces.&lt;/p&gt;&lt;p&gt;These smaller workspaces are the VPS servers. Each one runs on its own, just like having your very own computer.&lt;/p&gt;&lt;p&gt;So, a VPS server is like your private workspace within that library computer. Even though it&apos;s not a physical computer, it&apos;s your own dedicated area with its resources.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Alright, it&apos;s time to dive into the tech stuff.&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;A VPS server allows many users and administrators to work independently on the same hardware.&lt;/p&gt;&lt;p&gt;A powerful physical server machine can host multiple VPS servers, each running on its own operating system.&lt;/p&gt;&lt;p&gt;The management of the central hardware of the physical server is the responsibility of a hypervisor.&lt;/p&gt;&lt;p&gt;Specific software defines the virtual environments and allocates a portion of the server&apos;s resources to each VPS server. This includes processor power (CPU), fixed storage space, and RAM (memory).&lt;/p&gt;&lt;p&gt;In fact, it is a virtualized server that operates like a physical server, even though it isn&apos;t actually a physical server. It exists within a physical server but has its dedicated resources.&lt;/p&gt;&lt;p&gt;You may have come across VPS providers like Hetzner, Vultr, or DigitalOcean.&lt;/p&gt;&lt;p&gt;These providers have data centers located in various regions where physical servers are housed.&lt;/p&gt;&lt;p&gt;They employ virtualization software to divide these physical servers into VPS servers, each of which can have its own chosen operating system.&lt;/p&gt;&lt;p&gt;This means that users from all over the world can have VPS servers from the same data center on the same physical server, each with different operating systems or software configurations.&lt;/p&gt;&lt;h2&gt;Practical Uses of a VPS Server&lt;/h2&gt;&lt;p&gt;Now that you know what a VPS server is, let&apos;s explore all the cool stuff you can do with it.&lt;/p&gt;&lt;p&gt;Here are some ideas:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;Host a Website:&lt;/strong&gt;  You can set up a web server and host your website. Share your ideas, blog, or create an online portfolio.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Run Your Own Email Server:&lt;/strong&gt;  This one is a bit of a fun project! You can create your own email server and manage your emails.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Experiment with Open Source Projects:&lt;/strong&gt;  Dive into open-source software, contribute, or create your own projects. Learn and explore.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Create Your Own Cloud:&lt;/strong&gt;  Store your files, documents, and photos securely in your private cloud storage.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Host Bots:&lt;/strong&gt;  Run your Discord or Telegram bots. Chatbots, game bots, you name it.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Learn and Test:&lt;/strong&gt;  It&apos;s also a fantastic playground for learning and testing. Try new software, configurations, and ideas without any worries.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Literally, the possibilities are endless. You can explore, experiment, and create based on your interests and needs.&lt;/p&gt;&lt;h2&gt;How does VPS Hosting work?&lt;/h2&gt;&lt;p&gt;When it comes to hosting for your website, you have a few choices.&lt;/p&gt;&lt;p&gt;Many people start with shared hosting because it&apos;s budget-friendly.&lt;/p&gt;&lt;p&gt;But there&apos;s a catch: With shared hosting, your website shares a server with lots of other websites.&lt;/p&gt;&lt;p&gt;You all use the same resources, like the space in a crowded room. This can lead to slower performance and limited control.&lt;/p&gt;&lt;p&gt;Now, there&apos;s VPS hosting.&lt;/p&gt;&lt;p&gt;This is like having a room all to yourself. The hosting provider dedicates a VPS server just for your website.&lt;/p&gt;&lt;p&gt;You don&apos;t have to share resources with others, so your website performs better. It costs a bit more, but you get a powerful and flexible hosting environment.&lt;/p&gt;&lt;p&gt;Plus, you often get root access, which means you have more control over the server.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;Having root access means you have complete control over your server. The root user has the highest level of control on a server.&lt;/p&gt;&lt;p&gt;If you want the best of the best, there&apos;s dedicated server hosting.&lt;/p&gt;&lt;p&gt;It&apos;s like having a whole building just for you. It&apos;s super powerful and can handle massive websites with lots of users.&lt;/p&gt;&lt;p&gt;But it&apos;s also super expensive and typically only needed for very large websites.&lt;/p&gt;&lt;p&gt;For most websites, VPS hosting is the sweet spot. You can start small and scale up as needed.&lt;/p&gt;&lt;p&gt;It offers better performance than shared hosting, lower costs than dedicated hosting, and more flexibility than either option.&lt;/p&gt;&lt;p&gt;It&apos;s like having the perfect balance for your website&apos;s needs.&lt;/p&gt;&lt;p&gt;It&apos;s important to note that there&apos;s a difference between VPS hosting and having a VPS server.&lt;/p&gt;&lt;p&gt;VPS hosting is the service provided by a hosting company, where they manage the server for you.&lt;/p&gt;&lt;p&gt;In contrast, having a VPS server means you have complete control over the virtual server and are responsible for its management.&lt;/p&gt;&lt;p&gt;I just wanted to clarify all this, what VPS hosting means and its differences from shared and dedicated hosting, as well as the difference between VPS hosting and VPS server, so you don&apos;t get confused online.&lt;/p&gt;&lt;p&gt;Here, I&apos;m specifically talking about VPS servers.&lt;/p&gt;&lt;h2&gt;How I Use VPS Servers&lt;/h2&gt;&lt;p&gt;I mainly use VPS servers for two things: testing and learning. They&apos;re like my digital playground.&lt;/p&gt;&lt;p&gt;I also host all my projects on them.&lt;/p&gt;&lt;p&gt;What&apos;s cool is that most server providers let you pay by the hour. So, I can deploy a server whenever I want, and after a few hours, I can just delete it.&lt;/p&gt;&lt;p&gt;It&apos;s perfect for quick tests. VPS servers are super flexible.&lt;/p&gt;&lt;p&gt;I also use VPS servers to try out new open-source projects. I&apos;m a big fan of open source stuff, and I love experimenting with new things.&lt;/p&gt;&lt;p&gt;VPS servers played a big role in helping me earn my &lt;a href=&quot;https://cs.lpi.org/caf/Xamman/certification/verify/LPI000531754/58zs6bq77y&quot;&gt;LPIC-1&lt;/a&gt; certificate and pass the exam. I used them to learn and practice, and it was an invaluable resource on my journey to certification.&lt;/p&gt;&lt;h2&gt;Deploying Your First VPS Server&lt;/h2&gt;&lt;p&gt;When it comes to getting your first VPS server, there are many providers to choose from.&lt;/p&gt;&lt;p&gt;I&apos;ve always used three VPS providers: Hetzner, Vultr, and DigitalOcean. They all offer similar services and have easy interfaces.&lt;/p&gt;&lt;p&gt;I used them to experiment with new setups, play with open-source projects, and learn more about Linux.&lt;/p&gt;&lt;p&gt;Their servers are powerful and not too expensive, which makes them a great option for beginners who are just starting out.&lt;/p&gt;&lt;p&gt;However, I discovered that Hetzner suits my needs the best and outperforms other providers, so that&apos;s the one I use now.&lt;/p&gt;&lt;p&gt;But feel free to pick the provider that works for you.&lt;/p&gt;&lt;p&gt;We&apos;ll deploy our first VPS server from Hetzner, but the process is pretty similar with other providers.&lt;/p&gt;&lt;p&gt;👉&lt;/p&gt;&lt;p&gt;New to Hetzner? &lt;a href=&quot;https://hetzner.cloud/?ref=MC4Yy318xX5X&quot;&gt;Use my link&lt;/a&gt; to get free credits!&lt;/p&gt;&lt;p&gt;Once you&apos;re done signing up, head to the cloud dashboard, and make a new project.&lt;/p&gt;&lt;p&gt;You can name it whatever you like.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://ivansalloum.com/content/images/2024/10/cloud-dashboard.webp&quot; alt=&quot;cloud-dashboard&quot;&gt;&lt;/p&gt;&lt;p&gt;Once your project is created, step inside and click &lt;strong&gt;Create Resource&lt;/strong&gt; and choose&lt;strong&gt;Servers&lt;/strong&gt;  to deploy your very first VPS server. A new page will open up.&lt;/p&gt;&lt;p&gt;First, pick the server&apos;s location. Choose the one that&apos;s closest to you.&lt;/p&gt;&lt;p&gt;💡&lt;/p&gt;&lt;p&gt;When you&apos;re planning to host a website, consider your visitors. Pick the location nearest to them for a faster website experience.&lt;/p&gt;&lt;p&gt;Next, select the image (operating system) for your VPS server.&lt;/p&gt;&lt;p&gt;I always go with Ubuntu LTS (Long Term Support) version, which is 24.04 as of now. It&apos;s reliable and works well for all my projects. But you can choose any image you like.&lt;/p&gt;&lt;p&gt;Then, decide on the type of VPS server you want to deploy. Hetzner offers two types: &lt;strong&gt;Shared vCPU&lt;/strong&gt;  and &lt;strong&gt;Dedicated vCPU&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Most providers have similar options. To explain, think of it this way:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;With dedicated vCPU, you get your own dedicated portion of the physical server&apos;s CPU cores. It&apos;s like having your own slice of the CPU pie, and what you do won&apos;t affect others on the same server.&lt;/li&gt;&lt;li&gt;With shared vCPU, it&apos;s like everyone gets a smaller slice of the CPU pie, and the physical cores are shared among multiple VPS servers. Sometimes it&apos;s fast, other times it can feel slower if others are using lots of CPU.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;For testing and learning, go with shared vCPU.&lt;/p&gt;&lt;p&gt;But if you&apos;re running something important, like a live website, dedicated vCPU is the way to go for better performance.&lt;/p&gt;&lt;p&gt;Next, you have three networking options: &lt;strong&gt;Public IPv4&lt;/strong&gt; , &lt;strong&gt;Public IPv6&lt;/strong&gt; , and &lt;strong&gt;Private networks&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;I usually disable IPv6 and stick with IPv4. Some platforms and services don&apos;t fully support IPv6 yet, so it&apos;s safer to rely on IPv4.&lt;/p&gt;&lt;p&gt;So far, we&apos;ve set up the essentials for deploying a new VPS server.&lt;/p&gt;&lt;p&gt;There are more options on the page, like adding SSH keys, creating volumes, configuring a firewall, enabling backups, or adding a custom cloud configuration.&lt;/p&gt;&lt;p&gt;We&apos;ll skip these for now, as it might be a bit much if you&apos;re just starting out.&lt;/p&gt;&lt;p&gt;Your main focus should be on deploying the server.&lt;/p&gt;&lt;p&gt;So, scroll down to the bottom of the page, give your server a name, and then hit &lt;strong&gt;Create &amp;amp; Buy now&lt;/strong&gt; to get your server up and running.&lt;/p&gt;&lt;p&gt;You&apos;ll receive an email with your server details. This includes the IP of the server and the root password.&lt;/p&gt;&lt;p&gt;In your project, you&apos;ll see a running VPS server with its name, IP, location, and creation time, just like in the picture below.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://ivansalloum.com/content/images/2024/10/running-vps-server.webp&quot; alt=&quot;running VPS server&quot;&gt;&lt;/p&gt;&lt;p&gt;Just click on the name of your VPS server, and a new page will open up.&lt;/p&gt;&lt;p&gt;There, you can keep an eye on some metrics, change a few options, and check out some VPS settings. Feel free to explore!&lt;/p&gt;&lt;p&gt;By following these steps, you’ve successfully deployed your first VPS server.&lt;/p&gt;&lt;h2&gt;What To Do Next&lt;/h2&gt;&lt;p&gt;Before diving into configuring your server, installing software, or starting your project, it&apos;s crucial to know how to &lt;a href=&quot;https://ivansalloum.com/preparing-your-ubuntu-server-for-first-use/&quot;&gt;set up&lt;/a&gt; and &lt;a href=&quot;https://ivansalloum.com/linux-server-security/&quot;&gt;secure&lt;/a&gt; your server.&lt;/p&gt;&lt;p&gt;These are essential steps.&lt;/p&gt;&lt;p&gt;Always deploy the server, set it up, and tighten its security – then you&apos;re good to go!&lt;/p&gt;&lt;p&gt;I&apos;ve written detailed guides on these topics, so be sure to check them out!&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;Awesome job reaching the end!&lt;/p&gt;&lt;p&gt;Now you know the basics of VPS servers.&lt;/p&gt;&lt;p&gt;Whether you&apos;re planning to host a website, run experiments, or learn something new, you&apos;re on the right track.&lt;/p&gt;&lt;p&gt;I hope this guide has been super helpful for you.&lt;/p&gt;&lt;p&gt;If you found value in this guide or have any questions or feedback, please don&apos;t hesitate to share your thoughts in the discussion section.&lt;/p&gt;&lt;p&gt;Your input is greatly appreciated, and you can also &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;contact me&lt;/a&gt; directly if you prefer.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Servers</category></item><item><title>How to Secure WordPress with 2FA</title><link>https://ivansalloum.com/how-to-secure-wordpress-with-2fa/</link><guid isPermaLink="true">https://ivansalloum.com/how-to-secure-wordpress-with-2fa/</guid><description>Learn the simple steps to lock down your WordPress site with two-factor authentication.</description><pubDate>Sat, 04 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;Two-factor authentication (2FA) is one of the most secure methods today to protect your WordPress website from brute-force attacks.&lt;/p&gt;&lt;p&gt;When you use 2FA, you always have to confirm your WordPress backend login with a second method, which is a code that you receive through SMS, email, or an authenticator app like Google Authenticator.&lt;/p&gt;&lt;p&gt;Basically, you need more than just your password to log in.&lt;/p&gt;&lt;p&gt;In my opinion, it is a must-have security procedure that every WordPress website should enable.&lt;/p&gt;&lt;p&gt;In this tutorial, I&apos;ll show you how to make your WordPress site safer with 2FA using a free plugin.&lt;/p&gt;&lt;h2&gt;Installing Two-Factor Plugin&lt;/h2&gt;&lt;p&gt;I use this plugin for all my WordPress sites.&lt;/p&gt;&lt;p&gt;It&apos;s lightweight, with over 70,000 people using it, and it has a perfect 5-star rating. I&apos;ve never had any problems with it.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://wordpress.org/plugins/two-factor/&quot;&gt;Two-Factor on WordPress.org →&lt;/a&gt;&lt;/p&gt;&lt;p&gt;The first step is super easy.&lt;/p&gt;&lt;p&gt;Just install and activate the plugin, and you&apos;re good to go.&lt;/p&gt;&lt;p&gt;Open your WordPress dashboard, head to the plugins page, search for &lt;strong&gt;Two-Factor&lt;/strong&gt; , and then simply install and activate the plugin.&lt;/p&gt;&lt;h2&gt;Configuring 2FA&lt;/h2&gt;&lt;p&gt;The plugin puts its settings on the user edit page.&lt;/p&gt;&lt;p&gt;When you edit any user on your site, just scroll down until you see the &lt;strong&gt;Two-Factor Options&lt;/strong&gt;  section.&lt;/p&gt;&lt;p&gt;You&apos;ll find four 2FA methods to choose from: Email, Time-Based One-Time Password (TOTP), FIDO U2F Security Keys, and Backup Verification Codes (Single Use).&lt;/p&gt;&lt;p&gt;You can enable one or more of these 2FA methods as per your preference.&lt;/p&gt;&lt;p&gt;You can choose one of them as your primary method, which you&apos;ll use by default when signing in.&lt;/p&gt;&lt;p&gt;If you ever lose access to your primary method, you can change the method during the sign-in process. Think of the other methods as backups to your primary one.&lt;/p&gt;&lt;p&gt;I always enable Email, TOTP, and Backup Verification Codes, with TOTP as my primary method.&lt;/p&gt;&lt;p&gt;If I ever lose access to my phone and can&apos;t use the authenticator app, I can simply opt to receive a code via email or use a backup code for easy access.&lt;/p&gt;&lt;p&gt;❗&lt;/p&gt;&lt;p&gt;Your WordPress website needs to be able to &lt;a href=&quot;https://ivansalloum.com/how-to-configure-wordpress-to-send-emails-via-smtp/&quot;&gt;send emails via SMTP&lt;/a&gt; for you to receive the code by email.&lt;/p&gt;&lt;p&gt;Select your preferred primary method. I suggest TOTP, but the choice is yours.&lt;/p&gt;&lt;p&gt;Make sure to enable at least one backup method. It won&apos;t cause any issues if you enable two backup methods, as I do.&lt;/p&gt;&lt;p&gt;So, go ahead and set them up.&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;Adding Two-Factor Authentication to your WordPress site is like putting an extra lock on your door.&lt;/p&gt;&lt;p&gt;It makes sure only you can get in, even if someone has your password.&lt;/p&gt;&lt;p&gt;I hope this tutorial has been of great help to you.&lt;/p&gt;&lt;p&gt;If you found value in this tutorial or have any questions or feedback, please don&apos;t hesitate to share your thoughts in the discussion section.&lt;/p&gt;&lt;p&gt;Your input is greatly appreciated, and you can also &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;contact me&lt;/a&gt; directly if you prefer.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>WordPress</category></item><item><title>How to Configure WordPress to Send Emails via SMTP</title><link>https://ivansalloum.com/how-to-configure-wordpress-to-send-emails-via-smtp/</link><guid isPermaLink="true">https://ivansalloum.com/how-to-configure-wordpress-to-send-emails-via-smtp/</guid><description>Step-by-step tutorial to configuring WordPress for sending emails through external SMTP.</description><pubDate>Fri, 03 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;While WordPress can send emails by default, these emails often fail to reach their intended recipients or end up in the spam folder.&lt;/p&gt;&lt;p&gt;When it comes to sending emails to customers and users through WooCommerce, contact forms, member areas, forums, and more, reliable email delivery is crucial.&lt;/p&gt;&lt;p&gt;In this tutorial, I&apos;ll show you how to configure WordPress to use an external SMTP server for sending emails, and the best part is, we&apos;ll be using a free service.&lt;/p&gt;&lt;h2&gt;Why Use External SMTP Server&lt;/h2&gt;&lt;p&gt;If I have an online shop on WordPress, I need a reliable way to send important emails to my customers. These include purchase confirmations, password reset links, and welcome emails when they sign up.&lt;/p&gt;&lt;p&gt;These types of emails are called &lt;strong&gt;Transactional Emails&lt;/strong&gt; , and they&apos;re crucial because you always want them to reach your customers on time.&lt;/p&gt;&lt;p&gt;Imagine not getting a password reset link promptly – that wouldn&apos;t be ideal, right?&lt;/p&gt;&lt;p&gt;I&apos;m going to explain why using an external SMTP server for sending emails on WordPress is important.&lt;/p&gt;&lt;h3&gt;On Shared Hosting&lt;/h3&gt;&lt;p&gt;When your WordPress website is on shared hosting, it shares resources with other websites.&lt;/p&gt;&lt;p&gt;Shared hosting means your website is on a server where others have their websites too.&lt;/p&gt;&lt;p&gt;Most shared hosting providers set up their servers to send emails. This means your WordPress website can send emails.&lt;/p&gt;&lt;p&gt;However, because other people share the same server, if someone misuses email sending, it affects all the WordPress websites on that server, including yours.&lt;/p&gt;&lt;p&gt;This might lead to the server&apos;s IP address getting blacklisted or blocked, which will affect your email delivery.&lt;/p&gt;&lt;h3&gt;On VPS or Dedicated Server&lt;/h3&gt;&lt;p&gt;Most server providers block port 25 to prevent misuse for sending spam emails. This means you can&apos;t easily configure your server to send emails.&lt;/p&gt;&lt;p&gt;Some providers might open this port if you talk to them about your project, but it&apos;s not the best idea. If, for any reason, your server&apos;s IP gets blacklisted, it&apos;ll affect your email delivery.&lt;/p&gt;&lt;p&gt;Using an external SMTP server from an email service provider (ESP) helps you avoid spam blacklists.&lt;/p&gt;&lt;p&gt;When you send emails through the ESP&apos;s server, the recipient&apos;s email server checks its IP address, not yours.&lt;/p&gt;&lt;p&gt;So, if your server&apos;s IP gets blacklisted, it won&apos;t affect your email delivery.&lt;/p&gt;&lt;p&gt;ESPs have a good IP reputation, making your emails less likely to get stuck in blacklists and more likely to reach the recipient.&lt;/p&gt;&lt;h3&gt;The &amp;quot;from&amp;quot; Email Address&lt;/h3&gt;&lt;p&gt;Normally, WordPress sends notification emails from an address like wordpress@yourdomain.com.&lt;/p&gt;&lt;p&gt;The problem is, that&apos;s not a real email address. The trouble is, lots of spam filters think these emails are junk, and they don&apos;t even make it to the spam folder. Filters just delete them.&lt;/p&gt;&lt;p&gt;It&apos;s really important to use an active email address for sending emails.&lt;/p&gt;&lt;p&gt;This makes your emails look trustworthy like using hello@yourdomain.com or contact@yourdomain.com.&lt;/p&gt;&lt;p&gt;Configuring WordPress to use an external SMTP server for sending emails allows you to change the &amp;quot;from&amp;quot; email address, and I&apos;ll cover that in this tutorial.&lt;/p&gt;&lt;h3&gt;Security Benefits&lt;/h3&gt;&lt;p&gt;By default, WordPress emails lack security. PHP Mail doesn&apos;t protect your messages.&lt;/p&gt;&lt;p&gt;This can cause your emails to be treated as spam and not reach your recipient&apos;s inbox.&lt;/p&gt;&lt;p&gt;On the other hand, ESPs put in the effort to ensure your emails are secure and reach their destination.&lt;/p&gt;&lt;p&gt;Most ESPs have strong security measures in place, such as SSL or TLS encryption, to keep your emails safe. They invest significantly in maintaining their server reputation, actively watching for any issues.&lt;/p&gt;&lt;h2&gt;Free 1000 Monthly Emails with SMTP2GO&lt;/h2&gt;&lt;p&gt;There are many ESPs to choose from. Some are paid, and others have both paid and free plans.&lt;/p&gt;&lt;p&gt;This tutorial will use SMTP2GO, which offers a free plan allowing you to send 1000 emails per month.&lt;/p&gt;&lt;p&gt;I’ve been using SMTP2GO for a while now for both production and development projects, and I must say, they’re fantastic.&lt;/p&gt;&lt;p&gt;While I won’t go into a full review, I’d like to highlight some of their excellent features:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;No Credit Card Needed:&lt;/strong&gt;  Unlike other ESPs, SMTP2GO doesn’t require your credit card information for signing up for their free plan.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Generous Email Limit:&lt;/strong&gt;  With SMTP2GO’s free plan, you can send up to 1000 emails monthly, which is significantly more than what many other ESPs offer.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;User-Friendly Dashboard:&lt;/strong&gt;  Their dashboard is user-friendly and makes it easy to add a domain and set up everything you need.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Exceptional Support:&lt;/strong&gt;  SMTP2GO’s support team is top-notch. They respond quickly and are willing to help with any questions, even if they’re not directly related to their services.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;And, it’s worth noting that I’ve never encountered any issues with their service.&lt;/p&gt;&lt;h2&gt;SMTP2GO Setup&lt;/h2&gt;&lt;p&gt;To get started, you’ll need to sign up for a free account on SMTP2GO.&lt;/p&gt;&lt;p&gt;Visit the &lt;a href=&quot;https://get.smtp2go.com/r3moeahf98qf&quot;&gt;smtp2go.com&lt;/a&gt; website and click on &lt;strong&gt;Try SMTP2GO Free&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;A pop-up will appear, prompting you to enter your work email address, not a shared one like @gmail.com or @hotmail.com.&lt;/p&gt;&lt;p&gt;After entering your work email, click on &lt;strong&gt;Continue&lt;/strong&gt;  and provide your full name and password.&lt;/p&gt;&lt;p&gt;You’ll receive an activation email to confirm your email address.&lt;/p&gt;&lt;p&gt;Open the email and follow the instructions to activate your account at SMTP2GO, completing the signup process.&lt;/p&gt;&lt;p&gt;After you&apos;ve activated your account, you&apos;ll be taken to your account dashboard, which looks like the picture below.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://ivansalloum.com/content/images/2024/10/smtp2go-account-dashboard.webp&quot; alt=&quot;smtp2go-dashboard&quot;&gt;&lt;/p&gt;&lt;p&gt;Click on &lt;strong&gt;Add a verified sender&lt;/strong&gt; , which will open a new page where you can specify the domain from which you want to send emails, as shown in the picture below.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://ivansalloum.com/content/images/2024/10/add-verified-sender.webp&quot; alt=&quot;verified-sender&quot;&gt;&lt;/p&gt;&lt;p&gt;You have two choices: &lt;strong&gt;Sender domain&lt;/strong&gt;  and &lt;strong&gt;Single sender email&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Opt for &lt;strong&gt;Sender domain&lt;/strong&gt;  and click on &lt;strong&gt;Add a sender domain&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://ivansalloum.com/content/images/2024/10/sender-domain.webp&quot; alt=&quot;sender-domain&quot;&gt;&lt;/p&gt;&lt;p&gt;Enter your desired domain and click &lt;strong&gt;Continue with this domain&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;❗&lt;/p&gt;&lt;p&gt;As a best practice, your domain should match the one you&apos;re using for your WordPress website. Make sure the email address you want to use for sending emails is working. Your domain must have the right MX, DKIM and SPF records set up to send and receive emails properly.&lt;/p&gt;&lt;p&gt;Next, you’ll need to configure DNS records for the domain you’re adding.&lt;/p&gt;&lt;p&gt;SMTP2GO will automatically detect your DNS provider. In my case, it’s Cloudflare.&lt;/p&gt;&lt;p&gt;You’ll need to add just three CNAME records, as illustrated in the picture below.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://ivansalloum.com/content/images/2024/10/required-DNS-records-smtp2go.webp&quot; alt=&quot;required-dns-records&quot;&gt;&lt;/p&gt;&lt;p&gt;❗&lt;/p&gt;&lt;p&gt;If you&apos;re using Cloudflare, remember not to enable the proxy option.&lt;/p&gt;&lt;p&gt;Once the records are added, click &lt;strong&gt;Verify&lt;/strong&gt;  to proceed. Be patient, as it may take a few minutes for the records to take effect.&lt;/p&gt;&lt;p&gt;After that, SMTP2GO will automatically generate an SSL certificate for your domain.&lt;/p&gt;&lt;p&gt;Now that your domain is verified, the final step is to add an SMTP user.&lt;/p&gt;&lt;p&gt;Navigate to the &lt;strong&gt;SMTP Users&lt;/strong&gt;  page under the &lt;strong&gt;Sending&lt;/strong&gt;  dropdown menu on the left-hand side and select &lt;strong&gt;Add SMTP user&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Choose a username and password, and if desired, provide a description, then click on &lt;strong&gt;Add SMTP User&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;On the same page, you will also find the SMTP server information and the user you’ve just created. Keep this page open because we’ll need it later for the Postfix setup.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://ivansalloum.com/content/images/2024/10/smtp-server-information.webp&quot; alt=&quot;smtp-user-information&quot;&gt;&lt;/p&gt;&lt;p&gt;With this, we&apos;ve completed the necessary setup, and we&apos;re ready to start configuring WordPress to use the SMTP2GO SMTP server.&lt;/p&gt;&lt;h2&gt;Configuring WordPress&lt;/h2&gt;&lt;p&gt;Configuring WordPress to use an external SMTP server for sending emails can be done with various plugins.&lt;/p&gt;&lt;p&gt;Some popular options include WP Main SMTP by WPForms, SMTP Mailer, and Easy WP SMTP.&lt;/p&gt;&lt;p&gt;However, in this tutorial, we&apos;ll use FluentSMTP, which, in my opinion, is the best SMTP plugin for WordPress.&lt;/p&gt;&lt;p&gt;FluentSMTP is open source, meaning it&apos;s free, with no paid features.&lt;/p&gt;&lt;p&gt;It&apos;s lightweight and user-friendly, offering in-depth reporting, email logs, the ability to resend emails, and real-time email delivery.&lt;/p&gt;&lt;p&gt;Let&apos;s get started.&lt;/p&gt;&lt;p&gt;First, you need to install and activate the FluentSMTP plugin.&lt;/p&gt;&lt;p&gt;&lt;a href=&quot;https://wordpress.org/plugins/fluent-smtp/&quot;&gt;FluentSMTP on WordPress.org →&lt;/a&gt;&lt;/p&gt;&lt;p&gt;After activation, go to its settings under &lt;strong&gt;Settings » FluentSMTP&lt;/strong&gt;  to set it up.&lt;/p&gt;&lt;p&gt;You&apos;ll see a welcome message and a list of ESPs to choose from, as shown in the picture below.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://ivansalloum.com/content/images/2024/10/fluentsmtp-choose-provider.webp&quot; alt=&quot;fluentsmtp-choose-provider&quot;&gt;&lt;/p&gt;&lt;p&gt;Since SMTP2GO isn&apos;t listed, select the &lt;strong&gt;Other SMTP&lt;/strong&gt;  option.&lt;/p&gt;&lt;p&gt;This will take you to a new page.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://ivansalloum.com/content/images/2024/10/other-smtp-option.webp&quot; alt=&quot;other-smtp-configuration&quot;&gt;&lt;/p&gt;&lt;p&gt;Here, you&apos;ll need to enter the &lt;strong&gt;From Email&lt;/strong&gt;  and &lt;strong&gt;From Name&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;The &lt;strong&gt;From Email&lt;/strong&gt;  you enter will take the place of the default email WordPress uses, which is wordpress@yourdomain.com.&lt;/p&gt;&lt;p&gt;Make sure the email address you use is functional, and that your domain has the correct DNS records in place. Enter a working email address and your name or brand name.&lt;/p&gt;&lt;p&gt;Example: This website&apos;s domain is ivansalloum.com. I&apos;ve added this domain to SMTP2GO and selected the &lt;strong&gt;Sender domain&lt;/strong&gt;  option, just like we did in this tutorial. This means I can send emails from any email associated with this domain. In my case, it could be hello@ivansalloum.com, contact@ivansalloum.com, or anyname@ivansalloum.com.&lt;/p&gt;&lt;p&gt;The key is to make sure that any email address I use for sending emails is valid and has an active inbox.&lt;/p&gt;&lt;p&gt;You&apos;ll also have three options to enable: &lt;strong&gt;Force From Email&lt;/strong&gt; , &lt;strong&gt;Force Sender Name&lt;/strong&gt; , and &lt;strong&gt;Set the return-path to match the From Email&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;I, along with FluentSMTP, recommend enabling the first option to ensure all emails are sent from the sender&apos;s domain. You can also enable the second option to force the sender&apos;s name.&lt;/p&gt;&lt;p&gt;The last option is crucial. Enable it to receive notifications in case an email fails to send due to recipient issues. If this option is disabled, you won&apos;t be notified.&lt;/p&gt;&lt;p&gt;Now, it&apos;s time to fill in the &lt;strong&gt;SMTP Host&lt;/strong&gt;  and &lt;strong&gt;SMTP Port&lt;/strong&gt;  fields.&lt;/p&gt;&lt;p&gt;Remember, I mentioned earlier to keep the page open where you added your first SMTP user. You&apos;ll find all the necessary information about their SMTP server there.&lt;/p&gt;&lt;p&gt;For &lt;strong&gt;SMTP Host&lt;/strong&gt; , enter:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;mail.smtp2go.com
&lt;/pre&gt;&lt;p&gt;For &lt;strong&gt;SMTP Port&lt;/strong&gt; , enter:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;2525
&lt;/pre&gt;&lt;p&gt;SMTP2GO uses port 2525 for their SMTP server. Other ESPs use different ports, like 587.&lt;/p&gt;&lt;p&gt;If you have trouble with port 2525, SMTP2GO has some other options to use, like 8025, 587, 80, or 25.&lt;/p&gt;&lt;p&gt;Next, choose the TLS encryption option. Don&apos;t forget to enable &lt;strong&gt;Use Auto TLS&lt;/strong&gt;  and &lt;strong&gt;User Authentication&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;FluentSMTP, by default, stores the SMTP username and password in the database in an encrypted format, which is highly recommended. This keeps your information secure in the database.&lt;/p&gt;&lt;p&gt;Lastly, enter your &lt;strong&gt;SMTP Username&lt;/strong&gt;  and &lt;strong&gt;SMTP Password&lt;/strong&gt; , which you created from the SMTP2GO dashboard, and then click on &lt;strong&gt;Save Connection Settings&lt;/strong&gt;.&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;And there you have it!&lt;/p&gt;&lt;p&gt;Now your WordPress emails are set to hit the inbox reliably with the help of an external SMTP server and the FluentSMTP plugin.&lt;/p&gt;&lt;p&gt;I hope this tutorial has been of great help to you.&lt;/p&gt;&lt;p&gt;If you found value in this tutorial or have any questions or feedback, please don&apos;t hesitate to share your thoughts in the discussion section.&lt;/p&gt;&lt;p&gt;Your input is greatly appreciated, and you can also &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;contact me&lt;/a&gt; directly if you prefer.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>WordPress</category></item><item><title>How to Configure Postfix for External SMTP Relay</title><link>https://ivansalloum.com/how-to-configure-postfix-for-external-smtp-relay/</link><guid isPermaLink="true">https://ivansalloum.com/how-to-configure-postfix-for-external-smtp-relay/</guid><description>Learn step-by-step how to set up Postfix for external SMTP relay using SMTP2GO for secure and efficient email delivery.</description><pubDate>Sat, 28 Oct 2023 00:00:00 GMT</pubDate><content:encoded>&lt;article&gt;&lt;p&gt;Postfix serves as a &lt;strong&gt;Mail Transfer Agent&lt;/strong&gt;  (MTA).&lt;/p&gt;&lt;p&gt;You can configure it to specialize in sending emails, which is valuable for sending routine email notifications.&lt;/p&gt;&lt;p&gt;In this tutorial, I&apos;ll explain how to set up Postfix to send up to 1000 emails per month using a free SMTP relay service.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;_I assume you&apos;re working on a properly set-up Ubuntu server. If not, check out my guide  on &lt;em&gt;&lt;a href=&quot;https://ivansalloum.com/preparing-your-ubuntu-server-for-first-use/&quot;&gt;&lt;em&gt;preparing  Ubuntu servers&lt;/em&gt;&lt;/a&gt; _  to get started.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;h2&gt;Why Use External SMTP Relay?&lt;/h2&gt;&lt;p&gt;Most server providers block port 25 to prevent misuse for sending spam emails.&lt;/p&gt;&lt;p&gt;However, you can work around this limitation by using an external SMTP relay. An external SMTP relay uses a different port, allowing you to send emails externally.&lt;/p&gt;&lt;p&gt;External SMTP relay means your servers don&apos;t send emails directly. Instead, it relies on another server, often called a relay host, to send your emails for you.&lt;/p&gt;&lt;p&gt;This approach offers several advantages, including bypassing anti-spam blacklists.&lt;/p&gt;&lt;p&gt;When an email is sent via an external SMTP relay, the recipient&apos;s email server checks the relay host&apos;s IP address, not yours.&lt;/p&gt;&lt;p&gt;This distinction is crucial because if your server&apos;s IP gets blacklisted for any reason, it won&apos;t affect your email delivery.&lt;/p&gt;&lt;p&gt;As external SMTP relay services typically maintain a positive IP reputation, your emails are more likely to bypass IP blacklists and successfully reach their intended recipients.&lt;/p&gt;&lt;p&gt;In addition to ensuring reliable email delivery, using SMTP relay services can also save you time and money.&lt;/p&gt;&lt;p&gt;Instead of dealing with the complexities of setting up and maintaining a dedicated SMTP server, opting for a trusted SMTP relay service provides peace of mind.&lt;/p&gt;&lt;p&gt;Consider this scenario: If I have an online shop, I need a reliable way to send important emails to my customers. These include purchase confirmations, password reset links, and welcome emails when they sign up.&lt;/p&gt;&lt;p&gt;These types of emails are called &lt;strong&gt;Transactional Emails&lt;/strong&gt; , and they&apos;re crucial because you always want them to reach your customers on time.&lt;/p&gt;&lt;p&gt;Imagine not getting a password reset link promptly – that wouldn&apos;t be ideal, right?&lt;/p&gt;&lt;h2&gt;Free 1000 Monthly Emails with SMTP2GO&lt;/h2&gt;&lt;p&gt;Many &lt;strong&gt;Email Service Providers&lt;/strong&gt;  (ESPs) can serve as a relay host for Postfix.&lt;/p&gt;&lt;p&gt;Some of these are paid, while others offer both paid plans and free plans that remain free forever.&lt;/p&gt;&lt;p&gt;This tutorial will use SMTP2GO, which offers a free plan allowing you to send 1000 emails per month.&lt;/p&gt;&lt;p&gt;I&apos;ve been using SMTP2GO for a while now for both production and development projects, and I must say, they&apos;re fantastic.&lt;/p&gt;&lt;p&gt;While I won&apos;t go into a full review, I&apos;d like to highlight some of their excellent features:&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;strong&gt;No Credit Card Needed:&lt;/strong&gt;  Unlike other ESPs, SMTP2GO doesn&apos;t require your credit card information for signing up for their free plan.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Generous Email Limit:&lt;/strong&gt;  With SMTP2GO&apos;s free plan, you can send up to 1000 emails monthly, which is significantly more than what many other ESPs offer.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;User-Friendly Dashboard:&lt;/strong&gt;  Their dashboard is user-friendly and makes it easy to add a domain and set up everything you need.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;Exceptional Support:&lt;/strong&gt;  SMTP2GO&apos;s support team is top-notch. They respond quickly and are willing to help with any questions, even if they&apos;re not directly related to their services.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;And, it&apos;s worth noting that I&apos;ve never encountered any issues with their service.&lt;/p&gt;&lt;h2&gt;SMTP2GO Setup&lt;/h2&gt;&lt;p&gt;To get started, you&apos;ll need to sign up for a free account on SMTP2GO.&lt;/p&gt;&lt;p&gt;Visit the &lt;a href=&quot;https://get.smtp2go.com/r3moeahf98qf&quot;&gt;smtp2go.com&lt;/a&gt; website and click on &lt;strong&gt;Try SMTP2GO Free&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;A pop-up will appear, prompting you to enter your work email address, not a shared one like @gmail.com or @hotmail.com.&lt;/p&gt;&lt;p&gt;After entering your work email, click on &lt;strong&gt;Continue&lt;/strong&gt;  and provide your full name and password.&lt;/p&gt;&lt;p&gt;You&apos;ll receive an activation email to confirm your email address.&lt;/p&gt;&lt;p&gt;Open the email and follow the instructions to activate your account at SMTP2GO, completing the signup process.&lt;/p&gt;&lt;p&gt;After you&apos;ve activated your account, you&apos;ll be taken to your account dashboard, which looks like the picture below.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://ivansalloum.com/content/images/2024/10/smtp2go-account-dashboard.webp&quot; alt=&quot;smtp2go-dashboard&quot;&gt;&lt;/p&gt;&lt;p&gt;Now, let&apos;s prepare everything for our Postfix setup.&lt;/p&gt;&lt;p&gt;Click on &lt;strong&gt;Add a verified sender&lt;/strong&gt; , which will open a new page where you can specify the domain from which you want to send emails, as shown in the picture below.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://ivansalloum.com/content/images/2024/10/add-verified-sender.webp&quot; alt=&quot;verified-sender&quot;&gt;&lt;/p&gt;&lt;p&gt;You have two choices: &lt;strong&gt;Sender domain&lt;/strong&gt;  and &lt;strong&gt;Single sender email&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Opt for &lt;strong&gt;Sender domain&lt;/strong&gt;  and click on &lt;strong&gt;Add a sender domain&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://ivansalloum.com/content/images/2024/10/sender-domain.webp&quot; alt=&quot;sender-domain&quot;&gt;&lt;/p&gt;&lt;p&gt;❗&lt;/p&gt;&lt;p&gt;Make sure your domain is active, and the email address you want to use for sending emails is working. Your domain must have the right MX, DKIM and SPF records set up to send and receive emails properly.&lt;/p&gt;&lt;p&gt;Next, you&apos;ll need to configure DNS records for the domain you&apos;re adding.&lt;/p&gt;&lt;p&gt;SMTP2GO will automatically detect your DNS provider. In my case, it&apos;s Cloudflare.&lt;/p&gt;&lt;p&gt;You&apos;ll need to add just three CNAME records, as illustrated in the picture below.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://ivansalloum.com/content/images/2024/10/required-DNS-records-smtp2go.webp&quot; alt=&quot;required-dns-records&quot;&gt;&lt;/p&gt;&lt;p&gt;❗&lt;/p&gt;&lt;p&gt;If you&apos;re using Cloudflare, remember not to enable the proxy option.&lt;/p&gt;&lt;p&gt;Once the records are added, click &lt;strong&gt;Verify&lt;/strong&gt;  to proceed. Be patient, as it may take a few minutes for the records to take effect.&lt;/p&gt;&lt;p&gt;After that, SMTP2GO will automatically generate an SSL certificate for your domain.&lt;/p&gt;&lt;p&gt;Now that your domain is verified, the final step is to add an SMTP user.&lt;/p&gt;&lt;p&gt;Navigate to the &lt;strong&gt;SMTP Users&lt;/strong&gt;  page under the &lt;strong&gt;Sending&lt;/strong&gt;  dropdown menu on the left-hand side and select &lt;strong&gt;Add SMTP user&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;Choose a username and password, and if desired, provide a description, then click on &lt;strong&gt;Add SMTP User&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;On the same page, you will also find the SMTP server information and the user you&apos;ve just created. Keep this page open because we&apos;ll need it later for the Postfix setup.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://ivansalloum.com/content/images/2024/10/smtp-server-information.webp&quot; alt=&quot;smtp-user-information&quot;&gt;&lt;/p&gt;&lt;p&gt;With this, we&apos;ve completed the necessary setup, and we&apos;re ready to start configuring Postfix.&lt;/p&gt;&lt;h2&gt;Configuring Postfix&lt;/h2&gt;&lt;p&gt;Let&apos;s start installing and configuring Postfix to use the SMTP2GO SMTP server as a relay host.&lt;/p&gt;&lt;p&gt;First, we&apos;ll install Postfix with the following command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt install postfix libsasl2-modules
&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;libsasl2-modules&lt;/code&gt; package helps with something called Simple Authentication and Security Layer (SASL), which makes sure programs can securely send emails through a relay server.&lt;/p&gt;&lt;p&gt;It&apos;s important when setting up Postfix as a relay host.&lt;/p&gt;&lt;p&gt;During the installation process, a pop-up window will appear, prompting you to select a &lt;strong&gt;General mail configuration type&lt;/strong&gt; , as illustrated in the picture below.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://ivansalloum.com/content/images/2024/10/general-mail-configuration-type.webp&quot; alt=&quot;general-mail-configuration-type&quot;&gt;&lt;/p&gt;&lt;p&gt;Choose the &lt;strong&gt;Internet Site&lt;/strong&gt;  option.&lt;/p&gt;&lt;p&gt;Following this, you&apos;ll be asked to input your &lt;strong&gt;System mail name&lt;/strong&gt; , which should be your domain name, as shown in the picture.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://ivansalloum.com/content/images/2024/10/system-mail-name.webp&quot; alt=&quot;system-mail-name&quot;&gt;&lt;/p&gt;&lt;p&gt;❗&lt;/p&gt;&lt;p&gt;Ensure that the domain name matches the one you added to SMTP2GO.&lt;/p&gt;&lt;p&gt;Once you&apos;ve done this, the installation process will continue until it&apos;s complete.&lt;/p&gt;&lt;p&gt;After Postfix is installed, open the main configuration file by using the following command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo vim /etc/postfix/main.cf
&lt;/pre&gt;&lt;p&gt;At the end of the file, you&apos;ll come across a line that starts with &lt;code&gt;relayhost =&lt;/code&gt; and has an empty value, which is the default.&lt;/p&gt;&lt;p&gt;In our case, this value should be set to the SMTP2GO SMTP server that we want to use as a relay host.&lt;/p&gt;&lt;p&gt;Remember, I mentioned earlier to keep the page open where you added your first SMTP user. You&apos;ll find all the necessary information about their SMTP server there.&lt;/p&gt;&lt;p&gt;Now, set the &lt;code&gt;relayhost&lt;/code&gt; value to &lt;code&gt;mail.smtp2go.com:2525&lt;/code&gt;, as shown below:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;relayhost = mail.smtp2go.com:2525
&lt;/pre&gt;&lt;p&gt;SMTP2GO uses port 2525 for their SMTP server. Other ESPs use different ports, like 587.&lt;/p&gt;&lt;p&gt;If you have trouble with port 2525, SMTP2GO has some other options to use, like 8025, 587, 80, or 25.&lt;/p&gt;&lt;p&gt;Next, add the following lines to the end of the file:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;smtp_sasl_auth_enable = yes smtp_sasl_password_maps = static:yourSMTP2GOUsername:yourSMTP2GOPassword smtp_sasl_security_options = noanonymous smtp_tls_security_level = may header_size_limit = 4096000 relay_destination_concurrency_limit = 20
&lt;/pre&gt;&lt;p&gt;Replace &lt;code&gt;yourSMTP2GOUsername&lt;/code&gt; and &lt;code&gt;yourSMTP2GOPassword&lt;/code&gt; with the SMTP user and password you created.&lt;/p&gt;&lt;p&gt;Here&apos;s what each directive means:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;smtp_sasl_auth_enable&lt;/code&gt;: enables SASL authentication.&lt;/li&gt;&lt;li&gt;&lt;code&gt;smtp_sasl_password_maps&lt;/code&gt;: tells which username and password to use for SASL authentication.&lt;/li&gt;&lt;li&gt;&lt;code&gt;smtp_sasl_security_options&lt;/code&gt;: controls the security options for SASL authentication. &lt;code&gt;noanonymous&lt;/code&gt; ensures that authentication isn&apos;t anonymous. A valid username and password must be provided to authenticate, just like the SMTP2GO SMTP user you added earlier.&lt;/li&gt;&lt;li&gt;&lt;code&gt;smtp_tls_security_level&lt;/code&gt;: sets how safe email encryption should be. &amp;quot;may&amp;quot; means that TLS encryption is optional, and if the receiving server supports it, the connection will be encrypted.&lt;/li&gt;&lt;li&gt;&lt;code&gt;header_size_limit&lt;/code&gt;: specifies the maximum allowed size for email headers in bytes.&lt;/li&gt;&lt;li&gt;&lt;code&gt;relay_destination_concurrency_limit&lt;/code&gt;: decides how many connections Postfix can make to the relay destination at the same time when sending email.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Don&apos;t forget to restart Postfix for the changes to take effect:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo systemctl restart postfix
&lt;/pre&gt;&lt;p&gt;Now, the last thing we need to do is to install the &lt;code&gt;mailutils&lt;/code&gt; package, as it provides the &lt;code&gt;mail&lt;/code&gt; command used by many programs to send emails.&lt;/p&gt;&lt;p&gt;If you don&apos;t install it, several programs won&apos;t be able to send you emails, such as Unattended Upgrades.&lt;/p&gt;&lt;p&gt;To install it, use the following command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo apt install mailutils
&lt;/pre&gt;&lt;p&gt;By following these steps, you&apos;ve successfully configured Postfix to use SMTP2GO SMTP server as a relay host.&lt;/p&gt;&lt;p&gt;Now, it&apos;s time to test our setup.&lt;/p&gt;&lt;h2&gt;Testing Our Setup&lt;/h2&gt;&lt;p&gt;We can test our Postfix setup by using the &lt;code&gt;mail&lt;/code&gt; command to send a test email and check if our server is now able to send emails.&lt;/p&gt;&lt;p&gt;However, it&apos;s crucial to note something.&lt;/p&gt;&lt;p&gt;It&apos;s very important that your server&apos;s hostname matches the domain you have added to SMTP2GO and intend to use.&lt;/p&gt;&lt;p&gt;For example: If my domain is ivansalloum.com, my hostname should be set to server.ivansalloum.com or whatever.ivansalloum.com.&lt;/p&gt;&lt;p&gt;Otherwise, if the domain doesn&apos;t match your server&apos;s hostname, you will get an error when sending an email which you could examine in the &lt;code&gt;/var/log/mail.log&lt;/code&gt; file.&lt;/p&gt;&lt;p&gt;Now, something else you should note.&lt;/p&gt;&lt;p&gt;If we use the &lt;code&gt;mail&lt;/code&gt; command directly from the command line, the &lt;strong&gt;From&lt;/strong&gt;  header would be user@hostname.&lt;/p&gt;&lt;p&gt;For example: If I use the mail command directly from my terminal using root and my hostname is server.ivansalloum.com, the &lt;strong&gt;From&lt;/strong&gt;  header would be root@server.ivansallom.com.&lt;/p&gt;&lt;p&gt;Now the emails should reach their destination, but they might end up in the spam folder.&lt;/p&gt;&lt;p&gt;We only use the &lt;code&gt;mail&lt;/code&gt; command for testing.&lt;/p&gt;&lt;p&gt;If the emails end up in the spam folder, it still shows that our server can send emails.&lt;/p&gt;&lt;p&gt;However, emails generated from programs like email alerts and sent from your server will not land in spam.&lt;/p&gt;&lt;p&gt;This is because the &lt;strong&gt;From&lt;/strong&gt;  header will show as root@ivansalloum.com, which appears like a real address, even if there&apos;s no active mailbox for the root user.&lt;/p&gt;&lt;p&gt;For example, email reports sent from Unattended Upgrades won&apos;t land in the spam folder.&lt;/p&gt;&lt;p&gt;Now, let&apos;s use the &lt;code&gt;mail&lt;/code&gt; command:&lt;/p&gt;&lt;pre data-language=&quot;bash&quot;&gt;sudo mail -s hello@ivansalloum.com
&lt;/pre&gt;&lt;p&gt;This will send an email from the root user to hello@ivansalloum.com.&lt;/p&gt;&lt;p&gt;If it lands in the spam folder, no problem, as I only want to know if my server is able to send emails.&lt;/p&gt;&lt;h2&gt;Conclusion and Final Thoughts&lt;/h2&gt;&lt;p&gt;And there you have it!&lt;/p&gt;&lt;p&gt;Now you know how to configure Postfix to be a super reliable email sender using a free SMTP relay service.&lt;/p&gt;&lt;p&gt;I hope this tutorial has been of great help to you.&lt;/p&gt;&lt;p&gt;If you found value in this tutorial or have any questions or feedback, please don&apos;t hesitate to share your thoughts in the &lt;strong&gt;discussion&lt;/strong&gt; section.&lt;/p&gt;&lt;p&gt;Your input is greatly appreciated, and you can also &lt;a href=&quot;mailto:hello@ivansalloum.com&quot;&gt;contact me&lt;/a&gt; directly if you prefer.&lt;/p&gt;&lt;/article&gt;</content:encoded><category>Servers</category></item></channel></rss>