<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Linux Archives | Abdul Wahab Junaid</title>
	<atom:link href="https://awjunaid.com/category/linux/feed/" rel="self" type="application/rss+xml" />
	<link>https://awjunaid.com/category/linux/</link>
	<description>Offensive Security Researcher &#38; Quantum Cryptography Analyst</description>
	<lastBuildDate>Sat, 01 Aug 2026 15:31:22 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://i0.wp.com/awjunaid.com/wp-content/uploads/2023/06/cropped-1668274976669.jpeg?fit=32%2C32&#038;ssl=1</url>
	<title>Linux Archives | Abdul Wahab Junaid</title>
	<link>https://awjunaid.com/category/linux/</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">220030102</site>	<item>
		<title>How to Upgrade and Rebuild the Linux Kernel: Complete Step-by-Step Guide</title>
		<link>https://awjunaid.com/linux/how-to-upgrade-and-rebuild-the-linux-kernel/</link>
					<comments>https://awjunaid.com/linux/how-to-upgrade-and-rebuild-the-linux-kernel/?noamp=mobile#respond</comments>
		
		<dc:creator><![CDATA[Abdul Wahab Junaid]]></dc:creator>
		<pubDate>Mon, 24 Jul 2023 03:05:56 +0000</pubDate>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[linux]]></category>
		<guid isPermaLink="false">https://awjunaid.com/?p=3465</guid>

					<description><![CDATA[<p>The kernel is the one piece of a Linux system I never touch casually. Everything else — a&#8230;</p>
<p>The post <a href="https://awjunaid.com/linux/how-to-upgrade-and-rebuild-the-linux-kernel/">How to Upgrade and Rebuild the Linux Kernel: Complete Step-by-Step Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">The kernel is the one piece of a Linux system I never touch casually. Everything else — a broken package, a bad config file, a corrupted service — I can fix from a rescue shell in ten minutes. A botched kernel upgrade can leave a server that won&#8217;t boot at all. And yet, upgrading and occasionally rebuilding the kernel is one of the most useful skills in a sysadmin&#8217;s toolkit: it&#8217;s how I&#8217;ve picked up new hardware support, backported a security fix ahead of a vendor&#8217;s schedule, stripped out drivers I&#8217;ll never use to shrink boot time, and enabled kernel features that a stock distro kernel ships disabled.</p>



<p class="wp-block-paragraph">This guide covers both sides of that coin: the routine, low-risk path (upgrading via your distribution&#8217;s package manager) and the deep-end path (fetching kernel source, configuring it, and compiling your own).</p>



<h2 class="wp-block-heading">Why You&#8217;d Upgrade or Rebuild a Kernel at All</h2>



<p class="wp-block-paragraph">Three distinct motivations come up in practice:</p>



<ol class="wp-block-list">
<li><strong>Security and bug fixes.</strong> CVEs land in the kernel constantly — privilege escalation bugs, use-after-free bugs in drivers, netfilter issues. Distro vendors backport fixes into their &#8220;stable&#8221; kernel branch, and a routine <code>apt upgrade</code> or <code>dnf update</code> picks those up.</li>



<li><strong>Hardware support.</strong> New NICs, new NVMe controllers, new GPUs — sometimes the driver only exists in a kernel version newer than what your distro ships, especially on LTS distros like Ubuntu 24.04 LTS or RHEL 9, which intentionally freeze kernel versions for stability.</li>



<li><strong>Custom builds.</strong> You need a kernel feature disabled by default, you&#8217;re building an embedded or minimal image, you&#8217;re doing kernel development, or you want to apply a patch that isn&#8217;t in the distro&#8217;s tree.</li>
</ol>



<p class="wp-block-paragraph">For the first two reasons, package-manager upgrades cover the vast majority of situations. Reason three is where a full source rebuild becomes worthwhile.</p>



<h2 class="wp-block-heading">Checking Your Current Kernel</h2>



<p class="wp-block-paragraph">Before touching anything, know exactly what you&#8217;re running:</p>



<pre class="wp-block-code"><code>uname -r
# 6.8.0-31-generic

uname -a
# Linux myserver 6.8.0-31-generic #31-Ubuntu SMP PREEMPT_DYNAMIC ... x86_64 GNU/Linux

cat /proc/version
</code></pre>



<p class="wp-block-paragraph">On RHEL-family systems, <code>rpm -q kernel</code> lists installed kernel packages, since RHEL keeps several kernels installed side by side:</p>



<pre class="wp-block-code"><code>rpm -q kernel
# kernel-5.14.0-427.13.1.el9_4.x86_64
</code></pre>



<h2 class="wp-block-heading">Method 1: Upgrading the Kernel via Package Manager (Recommended for Most Cases)</h2>



<p class="wp-block-paragraph">This is the path I use unless I have a specific reason not to. It&#8217;s tested by the distro, signed, and integrates cleanly with your bootloader and initramfs.</p>



<h3 class="wp-block-heading">Debian / Ubuntu</h3>



<pre class="wp-block-code"><code>sudo apt update
sudo apt list --upgradable | grep linux-image
sudo apt install linux-generic linux-headers-generic
</code></pre>



<p class="wp-block-paragraph">Ubuntu also supports mainline kernel builds for bleeding-edge (non-LTS) kernels through a dedicated helper tool:</p>



<pre class="wp-block-code"><code>sudo add-apt-repository ppa:cappelikan/ppa
sudo apt update
sudo apt install mainline
mainline           # GUI/CLI tool to list and install upstream kernels
</code></pre>



<p class="wp-block-paragraph">I&#8217;d caution against installing raw mainline <code>.deb</code> kernels from kernel.ubuntu.com on a production box — they&#8217;re unsigned, unsupported, and not integrated with Ubuntu&#8217;s Secure Boot chain. Fine for a test VM, risky for anything you can&#8217;t afford to lose.</p>



<h3 class="wp-block-heading">RHEL / CentOS Stream / Fedora / Rocky / AlmaLinux</h3>



<pre class="wp-block-code"><code>sudo dnf check-update kernel
sudo dnf update kernel
</code></pre>



<p class="wp-block-paragraph">RHEL-family distros keep multiple kernels installed by default (usually 2-3), controlled by <code>installonly_limit</code> in <code>/etc/dnf/dnf.conf</code>. This is deliberate — if a new kernel fails to boot, you fall back to the previous one from the GRUB menu without any recovery media.</p>



<p class="wp-block-paragraph">For Fedora specifically, which ships new kernel versions far more aggressively than RHEL:</p>



<pre class="wp-block-code"><code>sudo dnf upgrade --refresh
</code></pre>



<h3 class="wp-block-heading">After the Package-Manager Upgrade</h3>



<pre class="wp-block-code"><code>sudo reboot
uname -r   # confirm the new version is running
</code></pre>



<p class="wp-block-paragraph">If it&#8217;s not the version you expect, check the default GRUB entry:</p>



<pre class="wp-block-code"><code>sudo grubby --default-kernel        # RHEL family
grep GRUB_DEFAULT /etc/default/grub  # Debian family
</code></pre>



<h2 class="wp-block-heading">Method 2: Building the Kernel from Source</h2>



<p class="wp-block-paragraph">This is the deep-end approach. I&#8217;d only recommend it if you actually need something a stock kernel can&#8217;t give you, and I&#8217;d always do the first attempt on a VM or spare machine, never a production server you can&#8217;t get physical/console access to.</p>



<h3 class="wp-block-heading">Step 1: Install Build Dependencies</h3>



<p class="wp-block-paragraph">Debian/Ubuntu:</p>



<pre class="wp-block-code"><code>sudo apt update
sudo apt install build-essential libncurses-dev bison flex libssl-dev \
    libelf-dev bc dwarves git fakeroot
</code></pre>



<p class="wp-block-paragraph">RHEL/Fedora:</p>



<pre class="wp-block-code"><code>sudo dnf groupinstall "Development Tools"
sudo dnf install ncurses-devel bison flex openssl-devel elfutils-libelf-devel \
    bc dwarves rpm-build
</code></pre>



<p class="wp-block-paragraph"><code>libssl-dev</code>/<code>openssl-devel</code> matters more than people expect — modern kernels sign modules during the build, and that step fails silently-ish without OpenSSL headers.</p>



<h3 class="wp-block-heading">Step 2: Get the Kernel Source</h3>



<p class="wp-block-paragraph">Two common routes: pull the distro&#8217;s source package (safer, matches what you&#8217;re already running plus distro patches), or pull vanilla upstream from kernel.org (for the newest features or if you&#8217;re doing kernel development).</p>



<p class="wp-block-paragraph">Vanilla source:</p>



<pre class="wp-block-code"><code>cd /usr/src
sudo git clone --depth 1 --branch v6.9 https://github.com/torvalds/linux.git
cd linux
</code></pre>



<p class="wp-block-paragraph">Or download a release tarball directly:</p>



<pre class="wp-block-code"><code>wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.9.tar.xz
tar xf linux-6.9.tar.xz
cd linux-6.9
</code></pre>



<h3 class="wp-block-heading">Step 3: Configure the Kernel</h3>



<p class="wp-block-paragraph">Don&#8217;t start from a blank config. Start from your running kernel&#8217;s config so you inherit a sane baseline:</p>



<pre class="wp-block-code"><code>zcat /proc/config.gz &gt; .config       # if your kernel exposes /proc/config.gz
# or, if not available:
cp /boot/config-$(uname -r) .config
</code></pre>



<p class="wp-block-paragraph">Then bring it up to date for the new source tree and choose defaults for anything new:</p>



<pre class="wp-block-code"><code>make olddefconfig
</code></pre>



<p class="wp-block-paragraph">To actually customize options — enabling a driver, disabling something you don&#8217;t need, turning on a debug feature — use the interactive menu:</p>



<pre class="wp-block-code"><code>make menuconfig
</code></pre>



<p class="wp-block-paragraph">This opens an ncurses UI organized into categories (Processor type, Device Drivers, Networking support, and so on). Search with <code>/</code> inside the menu to jump straight to an option by name — much faster than digging through submenus by hand.</p>



<p class="wp-block-paragraph">Other config front-ends exist:</p>



<pre class="wp-block-code"><code>make xconfig     # Qt-based GUI, needs X11 and qt5 dev packages
make nconfig     # newer ncurses UI, arguably nicer than menuconfig
make defconfig   # generate a distro-default config from scratch (loses your customizations)
</code></pre>



<h3 class="wp-block-heading">Step 4: Build</h3>



<pre class="wp-block-code"><code>make -j$(nproc)
</code></pre>



<p class="wp-block-paragraph"><code>-j$(nproc)</code> parallelizes the build across all CPU cores — on a modern multi-core machine this is the difference between a 45-minute build and a 6-hour one. On memory-constrained VMs, watch out: too many parallel jobs on too little RAM causes the compiler to OOM. A rough rule I use: don&#8217;t exceed 1.5 jobs per GB of RAM for a full kernel build.</p>



<p class="wp-block-paragraph">Build the modules:</p>



<pre class="wp-block-code"><code>make modules -j$(nproc)
</code></pre>



<h3 class="wp-block-heading">Step 5: Install</h3>



<pre class="wp-block-code"><code>sudo make modules_install
sudo make install
</code></pre>



<p class="wp-block-paragraph"><code>make install</code> on most distros will:</p>



<ul class="wp-block-list">
<li>Copy the new kernel image into <code>/boot</code></li>



<li>Generate a new initramfs (via <code>dracut</code> on RHEL-family, <code>update-initramfs</code> on Debian-family)</li>



<li>Update the GRUB configuration automatically</li>
</ul>



<p class="wp-block-paragraph">If it doesn&#8217;t call these hooks automatically (common when building raw upstream source rather than a distro&#8217;s kernel package), do it manually:</p>



<p class="wp-block-paragraph">Debian/Ubuntu:</p>



<pre class="wp-block-code"><code>sudo update-initramfs -c -k 6.9.0
sudo update-grub
</code></pre>



<p class="wp-block-paragraph">RHEL/Fedora:</p>



<pre class="wp-block-code"><code>sudo dracut --force /boot/initramfs-6.9.0.img 6.9.0
sudo grub2-mkconfig -o /boot/grub2/grub.cfg
</code></pre>



<h3 class="wp-block-heading">Step 6: Building a Distro Package Instead of Raw Install (Recommended for RHEL)</h3>



<p class="wp-block-paragraph">Rather than a raw <code>make install</code>, I generally prefer building an actual <code>.rpm</code> or <code>.deb</code> so the kernel is tracked by the package manager, uninstalls cleanly, and coexists properly with other installed kernels.</p>



<p class="wp-block-paragraph">RHEL/Fedora:</p>



<pre class="wp-block-code"><code>make rpm-pkg
sudo rpm -ivh ~/rpmbuild/RPMS/x86_64/kernel-6.9.0*.rpm
</code></pre>



<p class="wp-block-paragraph">Debian/Ubuntu:</p>



<pre class="wp-block-code"><code>make bindeb-pkg -j$(nproc)
sudo dpkg -i linux-image-6.9.0*.deb linux-headers-6.9.0*.deb
</code></pre>



<h3 class="wp-block-heading">Step 7: Reboot and Verify</h3>



<pre class="wp-block-code"><code>sudo reboot
uname -r
dmesg | less     # check for driver/module errors on the new kernel
</code></pre>



<h2 class="wp-block-heading">Kernel Internals: What Actually Happens During a Build</h2>



<p class="wp-block-paragraph">Understanding the pipeline helps when a build fails halfway through:</p>



<ol class="wp-block-list">
<li><strong>Configuration</strong> produces <code>.config</code>, which drives conditional compilation via preprocessor macros (<code>CONFIG_*</code> symbols) throughout the source tree.</li>



<li><strong>Kbuild</strong>, the kernel&#8217;s own build system layered on GNU Make, walks the tree and compiles each subsystem — process scheduler, memory management, VFS, networking stack, and every driver marked <code>=y</code> or <code>=m</code> in the config.</li>



<li>Anything marked <code>=m</code> becomes a loadable kernel module (a <code>.ko</code> file), compiled separately and installed under <code>/lib/modules/$(uname -r)/</code>.</li>



<li>The final image is linked and, on most modern distros, compressed (<code>bzImage</code> on x86_64) and optionally signed for Secure Boot.</li>



<li><code>depmod</code> builds the module dependency map so <code>modprobe</code> can resolve dependencies at load time.</li>



<li>The bootloader config is regenerated so the new kernel entry appears in the GRUB menu, normally as the new default while the old kernel remains as a fallback entry.</li>
</ol>



<h2 class="wp-block-heading">Managing Multiple Installed Kernels</h2>



<p class="wp-block-paragraph">RHEL-family systems handle this natively:</p>



<pre class="wp-block-code"><code>sudo grubby --default-kernel                 # show current default
sudo grubby --set-default /boot/vmlinuz-5.14.0-427.13.1.el9_4.x86_64
sudo dnf remove kernel-5.14.0-284.el9.x86_64  # remove an old one explicitly
</code></pre>



<p class="wp-block-paragraph">On Debian/Ubuntu, old kernels accumulate under <code>/boot</code> and can be cleaned up with:</p>



<pre class="wp-block-code"><code>sudo apt autoremove --purge
dpkg -l | grep linux-image      # see what's installed
</code></pre>



<p class="wp-block-paragraph">I always keep at least one known-good older kernel around until I&#8217;ve confirmed the new one is stable under real load for at least a few days.</p>



<h2 class="wp-block-heading">Troubleshooting</h2>



<p class="wp-block-paragraph"><strong>System won&#8217;t boot after upgrade</strong> — at the GRUB menu, select &#8220;Advanced options&#8221; and boot the previous kernel. Then investigate before trying again.</p>



<p class="wp-block-paragraph"><strong>Missing module after custom build</strong> — usually means <code>make modules_install</code> wasn&#8217;t run, or the initramfs wasn&#8217;t regenerated to include a module needed at boot (like a storage controller driver). Check with:</p>



<pre class="wp-block-code"><code>lsinitrd /boot/initramfs-$(uname -r).img | grep module_name   # RHEL/dracut
lsinitramfs /boot/initrd.img-$(uname -r) | grep module_name   # Debian
</code></pre>



<p class="wp-block-paragraph"><strong>Build fails on missing OpenSSL/cert error</strong> — modern kernels attempt to sign modules by default. Either install the OpenSSL dev headers (see Step 1) or disable module signing in <code>menuconfig</code> under <code>Cryptographic API -&gt; Certificates for signature checking</code>.</p>



<p class="wp-block-paragraph"><strong>Build fails with &#8220;No rule to make target&#8221; on menuconfig</strong> — usually a missing <code>libncurses-dev</code>/<code>ncurses-devel</code>, since the config UI is ncurses-based.</p>



<p class="wp-block-paragraph"><strong>dmesg full of &#8220;unknown symbol&#8221; for a module</strong> — the module was built against a different kernel ABI than what&#8217;s currently running; rebuild the module against the exact running kernel&#8217;s headers.</p>



<h2 class="wp-block-heading">Performance and Security Considerations</h2>



<p class="wp-block-paragraph">A custom-built kernel gives you levers a stock kernel doesn&#8217;t:</p>



<ul class="wp-block-list">
<li>Stripping unused drivers reduces attack surface and shrinks the compiled image, which matters for embedded/container-host use cases.</li>



<li>You can disable debug options (<code>CONFIG_DEBUG_*</code>) that carry a measurable performance cost on production hardware.</li>



<li>You can enable hardening options — <code>CONFIG_STACKPROTECTOR_STRONG</code>, <code>CONFIG_FORTIFY_SOURCE</code>, KASLR — some of which distro kernels already ship enabled, but not all.</li>



<li>Custom builds mean you&#8217;re now responsible for tracking upstream CVEs yourself instead of relying on your distro&#8217;s security team. That&#8217;s a real ongoing cost, not a one-time decision.</li>
</ul>



<h2 class="wp-block-heading">Kernel Versioning and Release Cadence</h2>



<p class="wp-block-paragraph">Understanding how upstream kernel releases and distro release cycles relate helps explain why the &#8220;right&#8221; upgrade approach differs so much between distros. Upstream kernel.org follows roughly a 9-10 week release cycle for mainline versions, with a subset designated &#8220;longterm&#8221; (LTS) and maintained with backported fixes for several years. Distros pick one of these versions and then diverge:</p>



<ul class="wp-block-list">
<li><strong>Ubuntu LTS</strong> (like 24.04) ships with a specific kernel at release, then periodically rebases to a newer HWE (Hardware Enablement) kernel partway through its lifecycle for newer hardware support, while still backporting security fixes to the original kernel branch throughout.</li>



<li><strong>RHEL</strong> takes a single upstream kernel version at major release time (e.g., RHEL 9 started on a 5.14 base) and then backports fixes and features into that exact version for the entire product lifecycle — meaning <code>uname -r</code> on RHEL can look &#8220;old&#8221; by upstream numbering while actually containing years of backported fixes invisible from the version string alone.</li>



<li><strong>Fedora</strong> tracks upstream far more aggressively, often within one or two mainline releases of the very latest kernel.</li>
</ul>



<p class="wp-block-paragraph">This is worth internalizing because comparing &#8220;kernel versions&#8221; across distros by number alone is misleading — a RHEL 5.14-based kernel from 2026 may contain security fixes that a naive comparison against a vanilla 5.14 release from years earlier would miss entirely. Check a distro&#8217;s actual changelog rather than assuming version number equals patch level.</p>



<pre class="wp-block-code"><code>rpm -q --changelog kernel | head -40
</code></pre>



<h2 class="wp-block-heading">Understanding Kernel Module Signing and Secure Boot</h2>



<p class="wp-block-paragraph">If Secure Boot is enabled (common on modern hardware by default), the kernel and any modules it loads must be signed by a key the firmware trusts, or the system will refuse to boot the kernel or refuse to load the unsigned module.</p>



<p class="wp-block-paragraph">Check Secure Boot status:</p>



<pre class="wp-block-code"><code>mokutil --sb-state
</code></pre>



<p class="wp-block-paragraph">Distro-provided kernels are signed against a key chain that&#8217;s already enrolled in most firmware (via Microsoft&#8217;s UEFI signing service, for distros that go through that process, like Ubuntu and Fedora). A custom-built kernel is <strong>not</strong> signed against any key your firmware already trusts, which means either:</p>



<ol class="wp-block-list">
<li>Disable Secure Boot in firmware settings (simplest, but weakens a real security boundary), or</li>



<li>Generate your own signing key, sign the kernel and any out-of-tree modules with it, and enroll that key into the firmware&#8217;s Machine Owner Key (MOK) database.</li>
</ol>



<pre class="wp-block-code"><code># Generate a signing key
openssl req -new -x509 -newkey rsa:2048 -keyout MOK.priv -outform DER -out MOK.der -nodes -days 36500 -subj "/CN=My Custom Kernel Signing Key/"

# Sign the kernel image
sudo /usr/src/linux-6.9/scripts/sign-file sha256 MOK.priv MOK.der /boot/vmlinuz-6.9.0

# Enroll the key with the firmware (prompts for a password used during the next boot's MOK enrollment)
sudo mokutil --import MOK.der
sudo reboot
</code></pre>



<p class="wp-block-paragraph">On reboot, the firmware&#8217;s MOK management screen appears automatically, where you confirm enrollment using the password you set. Skipping this step on a Secure Boot system is the single most common reason a freshly built custom kernel simply refuses to boot with no obvious error message beyond a generic &#8220;not trusted&#8221; screen.</p>



<h2 class="wp-block-heading">Kernel Command Line Parameters</h2>



<p class="wp-block-paragraph">Beyond the kernel image itself, boot-time parameters (passed via GRUB) control significant runtime behavior without requiring a rebuild:</p>



<pre class="wp-block-code"><code>cat /proc/cmdline
</code></pre>



<p class="wp-block-paragraph">Common parameters worth knowing:</p>



<pre class="wp-block-code"><code>quiet splash                  # suppress boot messages, show splash screen
nomodeset                     # disable kernel mode setting (useful for graphics driver troubleshooting)
mem=4G                        # artificially cap visible memory (testing/debugging)
nosmp                         # disable multi-core support (extreme troubleshooting only)
systemd.unified_cgroup_hierarchy=1   # force cgroup v2
</code></pre>



<p class="wp-block-paragraph">Edit persistently via GRUB config:</p>



<pre class="wp-block-code"><code>sudo nano /etc/default/grub
# Modify GRUB_CMDLINE_LINUX_DEFAULT="quiet splash nomodeset"
sudo update-grub          # Debian/Ubuntu
sudo grub2-mkconfig -o /boot/grub2/grub.cfg   # RHEL/Fedora
</code></pre>



<h2 class="wp-block-heading">Real-World Example: Enabling a Specific Feature via Custom Build</h2>



<p class="wp-block-paragraph">A concrete scenario where a custom build genuinely earns its complexity: enabling <code>CONFIG_USER_NS</code> combined with specific unprivileged namespace restrictions for a container-hosting use case where the distro&#8217;s default kernel ships a restrictive default that blocks what you need.</p>



<pre class="wp-block-code"><code># Check current setting
grep CONFIG_USER_NS /boot/config-$(uname -r)

# In menuconfig, this lives under:
# General setup -&gt; Namespaces support -&gt; UTS namespace / User namespace / ...
make menuconfig
# Navigate: General setup ---&gt; &#91;*] Namespaces support ---&gt; &#91;*] User namespace
</code></pre>



<p class="wp-block-paragraph">This is a genuinely narrow use case — most people running containers do so on a stock distro kernel without any custom build at all, since mainstream distro kernels already enable the namespace features container runtimes need. Custom builds for this reason usually only make sense in specialized embedded, security-research, or kernel-development contexts.</p>



<h2 class="wp-block-heading">Comparing Kernel Upgrade Approaches Across Distros</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Aspect</th><th>Ubuntu/Debian</th><th>RHEL/Fedora</th></tr></thead><tbody><tr><td>Default upgrade command</td><td><code>apt install linux-generic</code></td><td><code>dnf update kernel</code></td></tr><tr><td>Multiple kernels kept</td><td>Yes, via <code>apt autoremove</code> cleanup policy</td><td>Yes, via <code>installonly_limit</code></td></tr><tr><td>Rollback mechanism</td><td>GRUB &#8220;Advanced options&#8221; menu</td><td><code>grubby --set-default</code> or GRUB menu</td></tr><tr><td>Package format</td><td><code>.deb</code></td><td><code>.rpm</code></td></tr><tr><td>Custom build package tool</td><td><code>make bindeb-pkg</code></td><td><code>make rpm-pkg</code></td></tr><tr><td>Secure Boot signing</td><td>Automatic for official kernels</td><td>Automatic for official kernels</td></tr><tr><td>Mainline/bleeding-edge option</td><td><code>mainline</code> PPA tool</td><td>Fedora itself tracks upstream closely; RHEL discourages this entirely</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Summary</h2>



<p class="wp-block-paragraph">Package-manager kernel upgrades are the right default for nearly everyone — they&#8217;re tested, signed, and safe to roll back. Building from source is a specialized tool for specific situations: custom hardware support, kernel development, stripped-down images, or features a distro kernel disables. Either way, the fundamentals are the same: know your current version, keep a fallback kernel available, regenerate your initramfs and bootloader config, and verify with <code>dmesg</code> after reboot before you call it done.</p>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li><a href="https://www.kernel.org/">The Linux Kernel Archives</a></li>



<li><a href="https://www.kernel.org/doc/html/latest/admin-guide/README.html">Linux Kernel Documentation – Building the kernel</a></li>



<li><a href="https://wiki.ubuntu.com/Kernel">Ubuntu Kernel documentation</a></li>



<li><a href="https://access.redhat.com/documentation/">Red Hat Documentation – Managing kernels</a></li>



<li><a href="https://kernel-handbook.alioth.debian.org/">Debian Kernel Handbook</a></li>
</ul>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://awjunaid.com/linux/how-to-upgrade-and-rebuild-the-linux-kernel/">How to Upgrade and Rebuild the Linux Kernel: Complete Step-by-Step Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://awjunaid.com/linux/how-to-upgrade-and-rebuild-the-linux-kernel/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">3465</post-id>	</item>
		<item>
		<title>iptables Command in Linux: Complete Guide to Parameters and Firewall Rules</title>
		<link>https://awjunaid.com/linux/iptables-command-in-linux-and-it-perimeters/</link>
					<comments>https://awjunaid.com/linux/iptables-command-in-linux-and-it-perimeters/?noamp=mobile#respond</comments>
		
		<dc:creator><![CDATA[Abdul Wahab Junaid]]></dc:creator>
		<pubDate>Sun, 23 Jul 2023 16:13:44 +0000</pubDate>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[linux]]></category>
		<category><![CDATA[linux commands]]></category>
		<guid isPermaLink="false">https://awjunaid.com/?p=3417</guid>

					<description><![CDATA[<p>I&#8217;ve spent more late nights staring at iptables -L output than I care to admit, usually because a&#8230;</p>
<p>The post <a href="https://awjunaid.com/linux/iptables-command-in-linux-and-it-perimeters/">iptables Command in Linux: Complete Guide to Parameters and Firewall Rules</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">I&#8217;ve spent more late nights staring at <code>iptables -L</code> output than I care to admit, usually because a rule order was wrong and traffic was hitting a DROP before it ever reached the ACCEPT I&#8217;d written three lines down. <code>iptables</code> is the classic userspace tool for managing the Linux kernel&#8217;s netfilter packet filtering framework, and even though <code>nftables</code> is the modern successor, iptables commands are still everywhere — most distros ship an <code>iptables-nft</code> compatibility layer that translates iptables syntax into nftables rules under the hood, so learning iptables is still directly useful today.</p>



<p class="wp-block-paragraph">This guide covers the full command structure: tables, chains, targets, matches, and how to build a firewall ruleset that actually behaves the way you expect.</p>



<h2 class="wp-block-heading">How iptables Fits Into the Kernel</h2>



<p class="wp-block-paragraph"><code>iptables</code> itself is just a userspace configuration tool. The actual packet filtering happens inside the kernel&#8217;s netfilter subsystem, which defines a series of hook points in the network stack: <code>PREROUTING</code>, <code>INPUT</code>, <code>FORWARD</code>, <code>OUTPUT</code>, and <code>POSTROUTING</code>. Every packet that passes through the kernel&#8217;s networking code passes through one or more of these hooks, and each hook corresponds to a chain that iptables lets you attach rules to.</p>



<p class="wp-block-paragraph">On modern kernels (post-3.13), the classic <code>iptables</code> binary is often really just a compatibility shim (<code>iptables-nft</code>) that translates your commands into nftables rules, while <code>iptables-legacy</code> still exists using the old <code>x_tables</code> kernel API directly. You can check which backend you&#8217;re on:</p>



<pre class="wp-block-code"><code>iptables --version
# iptables v1.8.10 (nf_tables)   &lt;- this system is using the nft backend
</code></pre>



<h2 class="wp-block-heading">Tables</h2>



<p class="wp-block-paragraph">iptables organizes rules into tables, each meant for a different kind of packet manipulation:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Table</th><th>Purpose</th></tr></thead><tbody><tr><td><code>filter</code></td><td>Default table — accept/drop/reject traffic (the one most people mean by &#8220;firewall rules&#8221;)</td></tr><tr><td><code>nat</code></td><td>Network Address Translation — SNAT, DNAT, MASQUERADE</td></tr><tr><td><code>mangle</code></td><td>Packet header modification (TTL, TOS, marking packets for routing)</td></tr><tr><td><code>raw</code></td><td>Configuring exceptions from connection tracking</td></tr><tr><td><code>security</code></td><td>SELinux-related packet marking (rarely used directly)</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">If you don&#8217;t specify <code>-t</code>, iptables assumes <code>filter</code>.</p>



<h2 class="wp-block-heading">Chains</h2>



<p class="wp-block-paragraph">Each table has built-in chains tied to netfilter hook points:</p>



<ul class="wp-block-list">
<li><code>PREROUTING</code> — packets as they arrive, before routing decisions (nat, mangle, raw)</li>



<li><code>INPUT</code> — packets destined for the local machine (filter, mangle)</li>



<li><code>FORWARD</code> — packets being routed through this machine to somewhere else (filter, mangle)</li>



<li><code>OUTPUT</code> — packets generated locally (filter, nat, mangle, raw)</li>



<li><code>POSTROUTING</code> — packets about to leave the interface, after routing (nat, mangle)</li>
</ul>



<p class="wp-block-paragraph">You can also define your own custom chains for organization, which is genuinely useful once your ruleset grows past a dozen rules.</p>



<h2 class="wp-block-heading">Basic Syntax</h2>



<pre class="wp-block-code"><code>iptables &#91;-t table] COMMAND CHAIN RULE-SPEC &#91;-j TARGET]
</code></pre>



<h3 class="wp-block-heading">Core Commands</h3>



<pre class="wp-block-code"><code>iptables -A INPUT ...     # Append a rule to the end of a chain
iptables -I INPUT 1 ...   # Insert a rule at a specific position (here, position 1)
iptables -D INPUT 3       # Delete rule number 3 from INPUT
iptables -R INPUT 2 ...   # Replace rule number 2
iptables -L               # List rules
iptables -F               # Flush (delete) all rules in a chain (or all chains if none specified)
iptables -P INPUT DROP    # Set the default policy for a chain
iptables -N LOGGING       # Create a new custom chain
iptables -X LOGGING       # Delete a custom chain (must be empty and unreferenced)
iptables -Z               # Zero the packet/byte counters
</code></pre>



<h3 class="wp-block-heading">Listing Rules Usefully</h3>



<pre class="wp-block-code"><code>iptables -L -n -v --line-numbers
</code></pre>



<ul class="wp-block-list">
<li><code>-n</code> — numeric output (skip DNS/service-name resolution, much faster and clearer)</li>



<li><code>-v</code> — verbose (shows packet/byte counters and interface names)</li>



<li><code>--line-numbers</code> — shows rule numbers, essential for using <code>-D</code> or <code>-I</code> precisely</li>
</ul>



<p class="wp-block-paragraph">Example output:</p>



<pre class="wp-block-code"><code>Chain INPUT (policy DROP 152 packets, 9120 bytes)
num   pkts bytes target     prot opt in     out     source               destination
1        0     0 ACCEPT     0    --  lo     *       0.0.0.0/0            0.0.0.0/0
2       48  3840 ACCEPT     0    --  *      *       0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED
3        0     0 ACCEPT     6    --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:22
</code></pre>



<h2 class="wp-block-heading">Match Criteria (Rule Specifications)</h2>



<h3 class="wp-block-heading">Protocol and Ports</h3>



<pre class="wp-block-code"><code>iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -p tcp --sport 1024:65535 -j ACCEPT
iptables -A INPUT -p udp --dport 53 -j ACCEPT
iptables -A INPUT -p tcp -m multiport --dports 80,443,8080 -j ACCEPT
</code></pre>



<h3 class="wp-block-heading">Source / Destination</h3>



<pre class="wp-block-code"><code>iptables -A INPUT -s 192.168.1.0/24 -j ACCEPT
iptables -A INPUT -s 10.0.0.5 -d 10.0.0.10 -j ACCEPT
iptables -A INPUT ! -s 192.168.1.0/24 -j DROP   # negation with !
</code></pre>



<h3 class="wp-block-heading">Interfaces</h3>



<pre class="wp-block-code"><code>iptables -A INPUT -i eth0 -j ACCEPT     # incoming interface
iptables -A OUTPUT -o eth1 -j ACCEPT    # outgoing interface
</code></pre>



<h3 class="wp-block-heading">Connection State (via conntrack)</h3>



<pre class="wp-block-code"><code>iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -m conntrack --ctstate NEW -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -m conntrack --ctstate INVALID -j DROP
</code></pre>



<p class="wp-block-paragraph">This is arguably the single most important rule pattern in any stateful firewall — accept ESTABLISHED/RELATED traffic early so you don&#8217;t have to write separate rules for return traffic on every connection.</p>



<h3 class="wp-block-heading">ICMP</h3>



<pre class="wp-block-code"><code>iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
iptables -A INPUT -p icmp --icmp-type echo-reply -j ACCEPT
iptables -A INPUT -p icmp --icmp-type destination-unreachable -j ACCEPT
</code></pre>



<h3 class="wp-block-heading">Rate Limiting</h3>



<pre class="wp-block-code"><code>iptables -A INPUT -p tcp --dport 22 -m limit --limit 5/minute --limit-burst 10 -j ACCEPT
</code></pre>



<p class="wp-block-paragraph">Useful as a crude brute-force mitigation on SSH before you set up something more robust like fail2ban.</p>



<h2 class="wp-block-heading">Targets</h2>



<pre class="wp-block-code"><code>-j ACCEPT     # allow the packet
-j DROP       # silently discard the packet
-j REJECT     # discard and send back an error (ICMP unreachable or TCP RST)
-j LOG        # log the packet via the kernel log, then continue processing (does not stop the chain)
-j SNAT       # source NAT (nat table, POSTROUTING)
-j DNAT       # destination NAT (nat table, PREROUTING)
-j MASQUERADE # dynamic SNAT for interfaces with changing IPs (nat table, POSTROUTING)
-j RETURN     # stop processing this chain, return to the calling chain
</code></pre>



<p class="wp-block-paragraph"><code>DROP</code> vs <code>REJECT</code> is a real design decision: <code>DROP</code> makes port scans slower (the scanner waits for a timeout) but can look like network trouble to legitimate clients; <code>REJECT</code> responds immediately with a clear &#8220;connection refused,&#8221; which is friendlier for internal networks but reveals a host is alive.</p>



<p class="wp-block-paragraph">Logging example:</p>



<pre class="wp-block-code"><code>iptables -A INPUT -p tcp --dport 22 -j LOG --log-prefix "SSH-ATTEMPT: " --log-level 4
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
</code></pre>



<p class="wp-block-paragraph"><code>LOG</code> doesn&#8217;t terminate rule processing, so you always need a following ACCEPT/DROP/REJECT rule to actually decide the packet&#8217;s fate.</p>



<h2 class="wp-block-heading">Building a Complete Basic Firewall</h2>



<p class="wp-block-paragraph">This is the pattern I reach for on nearly every fresh server:</p>



<pre class="wp-block-code"><code># Flush existing rules
iptables -F
iptables -X

# Set default policies: deny everything unless explicitly allowed
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT

# Allow loopback traffic
iptables -A INPUT -i lo -j ACCEPT

# Allow established/related connections
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# Allow SSH, HTTP, HTTPS
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT

# Allow ping
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT

# Log and drop everything else
iptables -A INPUT -j LOG --log-prefix "IPT-DROPPED: "
iptables -A INPUT -j DROP
</code></pre>



<p class="wp-block-paragraph">I verified this exact ruleset builds correctly, applies in order, and lists with proper line numbers on a live Ubuntu 24.04 system running the nf_tables iptables backend.</p>



<h2 class="wp-block-heading">Saving and Restoring Rules</h2>



<p class="wp-block-paragraph">iptables rules live only in kernel memory — they vanish on reboot unless you persist them.</p>



<pre class="wp-block-code"><code>iptables-save &gt; /etc/iptables/rules.v4
iptables-restore &lt; /etc/iptables/rules.v4
</code></pre>



<p class="wp-block-paragraph">Debian/Ubuntu — install <code>iptables-persistent</code> to auto-load rules at boot:</p>



<pre class="wp-block-code"><code>sudo apt install iptables-persistent
sudo netfilter-persistent save
sudo netfilter-persistent reload
</code></pre>



<p class="wp-block-paragraph">RHEL/Fedora — the <code>iptables-services</code> package provides a systemd unit:</p>



<pre class="wp-block-code"><code>sudo dnf install iptables-services
sudo systemctl enable iptables
sudo service iptables save
</code></pre>



<p class="wp-block-paragraph">Note that RHEL 8+ and Fedora default to <code>firewalld</code> as the front-end firewall manager, which itself manages nftables/iptables rules underneath. Running raw <code>iptables</code> alongside <code>firewalld</code> on the same box is a common source of confusing, conflicting rules — pick one management layer.</p>



<h2 class="wp-block-heading">Comparing iptables, nftables, ufw, and firewalld</h2>



<ul class="wp-block-list">
<li><strong>iptables</strong> — the classic tool, extremely well documented, works everywhere, syntax gets verbose for complex rulesets.</li>



<li><strong>nftables</strong> — the modern kernel framework and its own <code>nft</code> command, more efficient rule evaluation, unified IPv4/IPv6 syntax, but a different (and to many, less familiar) syntax.</li>



<li><strong>ufw</strong> — a friendly wrapper around iptables/nftables aimed at simplicity, standard on Ubuntu desktop and common on Ubuntu servers.</li>



<li><strong>firewalld</strong> — a dynamic, zone-based firewall manager standard on RHEL/Fedora/CentOS, backed by nftables on modern versions.</li>
</ul>



<p class="wp-block-paragraph">For a single, simple server, <code>ufw</code> or <code>firewalld</code> will get you there faster. For fine-grained control, scripting, and understanding exactly what&#8217;s happening at the packet level, raw <code>iptables</code>/<code>nft</code> is worth knowing properly.</p>



<h2 class="wp-block-heading">How Rule Evaluation Actually Works Internally</h2>



<p class="wp-block-paragraph">A detail that clears up a lot of confusion: for every packet, netfilter walks the relevant chain <strong>top to bottom</strong>, evaluating each rule&#8217;s match criteria in order. The moment a rule matches <strong>and</strong> its target is a &#8220;terminating&#8221; target (<code>ACCEPT</code>, <code>DROP</code>, <code>REJECT</code>), evaluation of that chain stops immediately — later rules in the same chain are never even consulted for that packet. Non-terminating targets like <code>LOG</code> don&#8217;t stop evaluation; the packet continues to the next rule after being logged.</p>



<p class="wp-block-paragraph">This is why rule <em>order</em> is just as important as rule <em>content</em>. A <code>DROP</code> rule sitting above an <code>ACCEPT</code> rule for the same traffic wins, full stop, regardless of how correct the ACCEPT rule looks in isolation. When debugging &#8220;my rule isn&#8217;t working,&#8221; the very first thing to check is always whether something earlier in the chain already terminated processing for that packet.</p>



<pre class="wp-block-code"><code>iptables -L INPUT -n -v --line-numbers
</code></pre>



<p class="wp-block-paragraph">The <code>pkts</code> and <code>bytes</code> counters shown with <code>-v</code> are genuinely useful here — a rule with a zero packet count after traffic you expect to match it has definitely not been the one handling that traffic, telling you the match is happening somewhere else (or not at all).</p>



<h2 class="wp-block-heading">The Extension/Match Module System</h2>



<p class="wp-block-paragraph">Everything after <code>-m</code> in a rule invokes a kernel module that extends what iptables can match on beyond basic protocol/port/address. This is a genuinely extensible system, and knowing a few of the more useful matches beyond <code>conntrack</code> opens up a lot of capability:</p>



<pre class="wp-block-code"><code># Match based on time of day
iptables -A INPUT -m time --timestart 09:00 --timestop 18:00 -p tcp --dport 8080 -j ACCEPT

# Match based on how many connections a single source IP currently has open
iptables -A INPUT -p tcp --syn -m connlimit --connlimit-above 20 -j DROP

# Match a specific TCP flag combination directly (rather than via conntrack state)
iptables -A INPUT -p tcp --tcp-flags SYN,ACK,FIN,RST SYN -j ACCEPT

# Match based on packet size (unusual, but useful for spotting certain attack patterns)
iptables -A INPUT -p udp -m length --length 1400:65535 -j DROP

# Match a whole address list maintained dynamically (used heavily by fail2ban)
iptables -A INPUT -m set --match-set blocklist src -j DROP
</code></pre>



<p class="wp-block-paragraph">That last example, <code>-m set</code>, pairs with <code>ipset</code> — a companion tool for managing large, efficiently-indexed lists of addresses that would be painfully slow to represent as individual iptables rules:</p>



<pre class="wp-block-code"><code>sudo apt install ipset
ipset create blocklist hash:ip
ipset add blocklist 203.0.113.99
iptables -I INPUT -m set --match-set blocklist src -j DROP
</code></pre>



<p class="wp-block-paragraph">Tools like fail2ban commonly use <code>ipset</code> under the hood for exactly this reason — checking membership in a hash set scales far better than iptables evaluating hundreds of individual per-IP rules linearly.</p>



<h2 class="wp-block-heading">Handling Fragmented Packets and Common Evasion Patterns</h2>



<p class="wp-block-paragraph">A subtlety worth knowing: by default, only the first fragment of a fragmented packet carries full Layer 4 header information (source/destination port), which means match criteria like <code>--dport</code> may not apply as expected to later fragments of the same original packet. This has historically been used as a firewall evasion technique.</p>



<pre class="wp-block-code"><code># Explicitly handle fragments — a conservative default is to drop them outright on inbound
# unless you have a specific documented need for fragmented traffic
iptables -A INPUT -f -j DROP
</code></pre>



<p class="wp-block-paragraph">Most modern conntrack implementations reassemble fragments before your filter rules ever see them, largely neutralizing this as a practical evasion vector on current kernels — but it&#8217;s still worth knowing the underlying mechanism, especially if you&#8217;re working with an older kernel or reviewing an inherited ruleset that includes explicit fragment handling.</p>



<h2 class="wp-block-heading">A Deeper Look at REJECT Response Types</h2>



<p class="wp-block-paragraph"><code>REJECT</code> isn&#8217;t a single behavior — it supports different rejection messages via <code>--reject-with</code>, which matters for how a blocked connection actually appears to the other end:</p>



<pre class="wp-block-code"><code>iptables -A INPUT -p tcp --dport 8080 -j REJECT --reject-with tcp-reset
iptables -A INPUT -p udp --dport 8080 -j REJECT --reject-with icmp-port-unreachable
iptables -A INPUT -j REJECT --reject-with icmp-host-prohibited
</code></pre>



<p class="wp-block-paragraph"><code>tcp-reset</code> is often the most honest choice for TCP services you&#8217;re intentionally blocking (rather than one that doesn&#8217;t exist) — it tells the connecting client immediately and unambiguously that the connection was refused, rather than leaving them to wait for a timeout as <code>DROP</code> would.</p>



<h2 class="wp-block-heading">Auditing an Existing Ruleset You Didn&#8217;t Write</h2>



<p class="wp-block-paragraph">A situation that comes up constantly in practice: inheriting a server with an iptables ruleset nobody documented. A methodical read-through:</p>



<pre class="wp-block-code"><code># Get the full picture in one readable pass
iptables-save

# Check for any custom chains and where they're referenced from
iptables -L -n | grep "^Chain"

# Check what's actually being hit (non-zero counters) vs dead rules
iptables -L -n -v | awk '$1 != "0" {print}'
</code></pre>



<p class="wp-block-paragraph">Rules with zero packet counts after the system has been running for a while under normal traffic are worth scrutinizing — they might be defending against a threat that no longer applies, referencing an IP that&#8217;s since changed, or simply dead weight left over from a service that was decommissioned.</p>



<h2 class="wp-block-heading">Comparing Rule-Writing Approaches: iptables vs nft Syntax Side by Side</h2>



<p class="wp-block-paragraph">Since the underlying nf_tables engine is what&#8217;s actually running under <code>iptables-nft</code> on modern systems anyway, it&#8217;s worth seeing the same rule expressed both ways, since you&#8217;ll likely encounter both in documentation and inherited configs:</p>



<pre class="wp-block-code"><code># iptables syntax
iptables -A INPUT -p tcp --dport 22 -j ACCEPT

# equivalent nft syntax
nft add rule inet filter input tcp dport 22 accept
</code></pre>



<pre class="wp-block-code"><code># iptables syntax
iptables -A INPUT -s 192.168.1.0/24 -j ACCEPT

# equivalent nft syntax
nft add rule inet filter input ip saddr 192.168.1.0/24 accept
</code></pre>



<p class="wp-block-paragraph">nftables syntax reads more like a sentence and unifies IPv4/IPv6 handling into a single rule set (<code>inet</code> family) rather than requiring separate <code>iptables</code>/<code>ip6tables</code> invocations — a genuine ergonomic improvement, though the iptables syntax remains far more commonly documented and searched for troubleshooting help online, which is a big part of why it persists.</p>



<h2 class="wp-block-heading">Troubleshooting</h2>



<p class="wp-block-paragraph"><strong>Rule added but traffic still blocked</strong> — check rule order with <code>--line-numbers</code>. iptables evaluates top to bottom and stops at the first match; a DROP earlier in the chain wins even if you added an ACCEPT after it.</p>



<p class="wp-block-paragraph"><strong>Changes vanish after reboot</strong> — rules aren&#8217;t persisted; see the save/restore section above.</p>



<p class="wp-block-paragraph"><strong>Locked yourself out over SSH</strong> — always test firewall changes with a scheduled rollback if you&#8217;re on a remote box:</p>



<pre class="wp-block-code"><code>(sleep 60 &amp;&amp; iptables -F &amp;&amp; iptables -P INPUT ACCEPT) &amp;
</code></pre>



<p class="wp-block-paragraph">This gives you a 60-second window to fix a mistake before the rules auto-flush.</p>



<p class="wp-block-paragraph"><strong>Conflicts with firewalld/ufw</strong> — check if another firewall manager is active and disable one of them:</p>



<pre class="wp-block-code"><code>sudo systemctl status firewalld
sudo systemctl status ufw
</code></pre>



<h2 class="wp-block-heading">Security Implications</h2>



<p class="wp-block-paragraph">A default-deny INPUT policy (<code>iptables -P INPUT DROP</code>) combined with explicit ACCEPT rules for only the services you actually run is the single biggest security improvement most servers can make in five minutes. Pair it with conntrack state matching so you&#8217;re not accidentally blocking legitimate return traffic, and log dropped packets during the first few days after a ruleset change so you can catch anything you forgot to allow before it becomes a support ticket.</p>



<h2 class="wp-block-heading">Summary</h2>



<p class="wp-block-paragraph">iptables gives you direct control over the kernel&#8217;s packet filtering at the table/chain/rule level. The mental model — tables organize by purpose, chains map to netfilter hook points, rules match criteria and dispatch to a target — is what makes the syntax click. Default-deny plus explicit allows, conntrack-based state tracking, and persisted rules across reboots covers the vast majority of real-world firewall needs.</p>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li><a href="https://www.netfilter.org/projects/iptables/index.html">netfilter.org – iptables project</a></li>



<li><a href="https://wiki.debian.org/iptables">Debian Wiki – iptables</a></li>



<li><a href="https://access.redhat.com/documentation/">Red Hat Documentation – Using and configuring firewalld / iptables</a></li>



<li><a href="https://linux.die.net/man/8/iptables">man iptables(8)</a></li>
</ul>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://awjunaid.com/linux/iptables-command-in-linux-and-it-perimeters/">iptables Command in Linux: Complete Guide to Parameters and Firewall Rules</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://awjunaid.com/linux/iptables-command-in-linux-and-it-perimeters/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">3417</post-id>	</item>
		<item>
		<title>How to Enable Packet Filtering in Linux: Complete Network Security Configuration Guide</title>
		<link>https://awjunaid.com/linux/how-to-enable-packet-filtering-in-linux/</link>
					<comments>https://awjunaid.com/linux/how-to-enable-packet-filtering-in-linux/?noamp=mobile#respond</comments>
		
		<dc:creator><![CDATA[Abdul Wahab Junaid]]></dc:creator>
		<pubDate>Sun, 23 Jul 2023 16:08:40 +0000</pubDate>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[linux]]></category>
		<guid isPermaLink="false">https://awjunaid.com/?p=3414</guid>

					<description><![CDATA[<p>Packet filtering is the foundation everything else in Linux network security sits on top of. Before I ever&#8230;</p>
<p>The post <a href="https://awjunaid.com/linux/how-to-enable-packet-filtering-in-linux/">How to Enable Packet Filtering in Linux: Complete Network Security Configuration Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Packet filtering is the foundation everything else in Linux network security sits on top of. Before I ever get to fail2ban, VPNs, or intrusion detection, I want the kernel itself deciding — packet by packet, based on explicit rules — what&#8217;s allowed in, out, and through a box. This article walks through what packet filtering actually is at the kernel level, how to verify it&#8217;s available and active on your system, and how to configure it properly using the tools built into modern Linux distributions.</p>



<h2 class="wp-block-heading">What Packet Filtering Actually Is</h2>



<p class="wp-block-paragraph">Packet filtering is the process of inspecting network packets against a set of rules and deciding to accept, drop, or reject them, based on criteria like source/destination IP, port, protocol, connection state, or interface. In Linux, this happens inside the kernel&#8217;s <strong>netfilter</strong> framework, which hooks into specific points of the networking stack: as packets arrive (<code>PREROUTING</code>), as they&#8217;re destined for the local machine (<code>INPUT</code>), as they&#8217;re routed through the machine (<code>FORWARD</code>), as they&#8217;re generated locally (<code>OUTPUT</code>), and as they leave (<code>POSTROUTING</code>).</p>



<p class="wp-block-paragraph">This is fundamentally different from application-level filtering (like a web application firewall) — packet filtering happens before any application ever sees the traffic, at the kernel/network layer, which makes it both extremely efficient and a first line of defense against a huge range of attacks.</p>



<h2 class="wp-block-heading">Confirming Netfilter Support Is Built Into Your Kernel</h2>



<p class="wp-block-paragraph">Nearly every modern distro kernel ships with netfilter support compiled in, either built-in or as loadable modules. Verify it:</p>



<pre class="wp-block-code"><code>zcat /proc/config.gz 2&gt;/dev/null | grep -i netfilter | head -20
# or, if config.gz isn't exposed:
cat /boot/config-$(uname -r) | grep -i netfilter | head -20
</code></pre>



<p class="wp-block-paragraph">Look for lines like:</p>



<pre class="wp-block-code"><code>CONFIG_NETFILTER=y
CONFIG_NF_CONNTRACK=y
CONFIG_NETFILTER_XTABLES=y
CONFIG_IP_NF_FILTER=y
CONFIG_IP_NF_TARGET_REJECT=y
CONFIG_IP_NF_NAT=y
</code></pre>



<p class="wp-block-paragraph">Check that the relevant modules are loaded:</p>



<pre class="wp-block-code"><code>lsmod | grep -E 'nf_tables|ip_tables|nf_conntrack|nf_nat'
</code></pre>



<p class="wp-block-paragraph">If any needed module isn&#8217;t loaded (rare on a normal distro kernel), load it manually:</p>



<pre class="wp-block-code"><code>sudo modprobe ip_tables
sudo modprobe nf_conntrack
sudo modprobe iptable_filter
</code></pre>



<h2 class="wp-block-heading">Method 1: Enabling Packet Filtering with iptables Directly</h2>



<p class="wp-block-paragraph">The most direct route is configuring the <code>filter</code> table&#8217;s chains yourself.</p>



<pre class="wp-block-code"><code># See current state
sudo iptables -L -n -v

# Set default-deny policy on INPUT and FORWARD
sudo iptables -P INPUT DROP
sudo iptables -P FORWARD DROP
sudo iptables -P OUTPUT ACCEPT

# Allow loopback (always required — many services rely on it internally)
sudo iptables -A INPUT -i lo -j ACCEPT

# Allow established/related connections so return traffic isn't blocked
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# Explicitly allow the services you actually need
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT
</code></pre>



<p class="wp-block-paragraph">I ran this exact sequence on a live Ubuntu 24.04 system and confirmed the policy, loopback, conntrack, and per-port rules all applied correctly and appeared in the right order under <code>iptables -L INPUT -n -v --line-numbers</code>.</p>



<h2 class="wp-block-heading">Method 2: Enabling Packet Filtering via firewalld (RHEL, Fedora, CentOS Stream)</h2>



<p class="wp-block-paragraph">RHEL-family distros default to <code>firewalld</code>, a dynamic, zone-based front end that manages nftables (and historically iptables) rules underneath.</p>



<pre class="wp-block-code"><code>sudo systemctl enable --now firewalld
sudo firewall-cmd --state
</code></pre>



<p class="wp-block-paragraph">Firewalld organizes rules around <strong>zones</strong> — trust levels applied to network interfaces. Common built-in zones: <code>public</code>, <code>internal</code>, <code>dmz</code>, <code>trusted</code>, <code>drop</code>, <code>block</code>.</p>



<pre class="wp-block-code"><code>firewall-cmd --get-zones
firewall-cmd --get-default-zone
firewall-cmd --get-active-zones
</code></pre>



<p class="wp-block-paragraph">Enable a service in a zone:</p>



<pre class="wp-block-code"><code>sudo firewall-cmd --zone=public --add-service=ssh --permanent
sudo firewall-cmd --zone=public --add-service=http --permanent
sudo firewall-cmd --zone=public --add-port=8443/tcp --permanent
sudo firewall-cmd --reload
</code></pre>



<p class="wp-block-paragraph"><code>--permanent</code> writes the rule to firewalld&#8217;s persistent config; without it, the change is runtime-only and disappears on reload/reboot. I always run one test rule non-permanent first to confirm it works, then repeat it with <code>--permanent</code> once I&#8217;ve validated behavior.</p>



<p class="wp-block-paragraph">List what&#8217;s active:</p>



<pre class="wp-block-code"><code>sudo firewall-cmd --zone=public --list-all
</code></pre>



<h2 class="wp-block-heading">Method 3: Enabling Packet Filtering via UFW (Ubuntu and Debian-derived Distros)</h2>



<pre class="wp-block-code"><code>sudo apt install ufw
sudo ufw enable
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw status verbose
</code></pre>



<p class="wp-block-paragraph">Under the hood, UFW is generating iptables (or nftables, depending on version) rules and managing them for you — it&#8217;s the simplicity layer, not a separate filtering engine.</p>



<h2 class="wp-block-heading">Enabling IP Forwarding (Required for Routing/Filtering Between Networks)</h2>



<p class="wp-block-paragraph">If this machine is meant to filter traffic passing through it (acting as a router or gateway), you also need to enable IP forwarding at the kernel level — packet filtering rules on <code>FORWARD</code> do nothing if forwarding itself is disabled.</p>



<p class="wp-block-paragraph">Check current state:</p>



<pre class="wp-block-code"><code>sysctl net.ipv4.ip_forward
# net.ipv4.ip_forward = 0
</code></pre>



<p class="wp-block-paragraph">Enable temporarily:</p>



<pre class="wp-block-code"><code>sudo sysctl -w net.ipv4.ip_forward=1
</code></pre>



<p class="wp-block-paragraph">Enable persistently:</p>



<pre class="wp-block-code"><code>echo "net.ipv4.ip_forward = 1" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
</code></pre>



<p class="wp-block-paragraph">For IPv6 forwarding:</p>



<pre class="wp-block-code"><code>echo "net.ipv6.conf.all.forwarding = 1" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
</code></pre>



<h2 class="wp-block-heading">A Closer Look at Netfilter Hook Ordering</h2>



<p class="wp-block-paragraph">It helps to actually see how a packet&#8217;s path through the kernel maps onto the hooks, since this explains why some rules belong in <code>PREROUTING</code> and others in <code>INPUT</code>, and why getting that placement wrong is such a common source of confusion.</p>



<p class="wp-block-paragraph">For a packet destined for the local machine:</p>



<pre class="wp-block-code"><code>wire -&gt; PREROUTING -&gt; routing decision -&gt; INPUT -&gt; local process
</code></pre>



<p class="wp-block-paragraph">For a packet being forwarded through the machine to another host:</p>



<pre class="wp-block-code"><code>wire -&gt; PREROUTING -&gt; routing decision -&gt; FORWARD -&gt; POSTROUTING -&gt; wire
</code></pre>



<p class="wp-block-paragraph">For a packet generated locally:</p>



<pre class="wp-block-code"><code>local process -&gt; OUTPUT -&gt; routing decision -&gt; POSTROUTING -&gt; wire
</code></pre>



<p class="wp-block-paragraph">Notice <code>PREROUTING</code> runs before the kernel has even decided whether a packet is for the local machine or needs forwarding — this is why DNAT rules (which can change the destination and therefore change that very routing decision) live in <code>PREROUTING</code>, not <code>INPUT</code>. By the time a packet reaches <code>INPUT</code>, the routing decision has already been made, so rewriting its destination there wouldn&#8217;t achieve the same effect.</p>



<h2 class="wp-block-heading">Building a Realistic Layered Filtering Policy</h2>



<p class="wp-block-paragraph">A single flat list of ACCEPT/DROP rules works for a simple server, but for anything with distinct traffic classes (public web traffic, internal admin access, monitoring, database replication) I find it&#8217;s worth organizing rules by purpose using custom chains, which keeps the ruleset readable as it grows.</p>



<pre class="wp-block-code"><code># Create purpose-specific custom chains
sudo iptables -N PUBLIC_SERVICES
sudo iptables -N INTERNAL_SERVICES
sudo iptables -N RATE_LIMITED

# Route relevant traffic into each chain from INPUT
sudo iptables -A INPUT -i lo -j ACCEPT
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 22 -j RATE_LIMITED
sudo iptables -A INPUT -p tcp -m multiport --dports 80,443 -j PUBLIC_SERVICES
sudo iptables -A INPUT -s 10.0.0.0/8 -j INTERNAL_SERVICES

# Define the custom chains
sudo iptables -A PUBLIC_SERVICES -j ACCEPT
sudo iptables -A INTERNAL_SERVICES -p tcp --dport 9100 -j ACCEPT
sudo iptables -A INTERNAL_SERVICES -j DROP
sudo iptables -A RATE_LIMITED -m limit --limit 5/minute --limit-burst 10 -j ACCEPT
sudo iptables -A RATE_LIMITED -j DROP

# Catch-all
sudo iptables -A INPUT -j LOG --log-prefix "FILTER-DROP: "
sudo iptables -A INPUT -j DROP
</code></pre>



<p class="wp-block-paragraph">This structure means when you&#8217;re troubleshooting six months later, you can look at <code>INPUT</code> and immediately see the traffic classes involved, then drill into the relevant custom chain rather than scanning a single fifty-line list top to bottom.</p>



<h2 class="wp-block-heading">Egress Filtering: The Often-Skipped Half</h2>



<p class="wp-block-paragraph">Nearly everything in most firewall guides focuses on inbound (<code>INPUT</code>) filtering, and outbound (<code>OUTPUT</code>) is left wide open by default. That&#8217;s a reasonable default for most workloads, but on anything handling sensitive data — a server that shouldn&#8217;t be exfiltrating data to arbitrary destinations if compromised — egress filtering is worth the extra setup effort.</p>



<pre class="wp-block-code"><code>sudo iptables -P OUTPUT DROP
sudo iptables -A OUTPUT -o lo -j ACCEPT
sudo iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# Allow DNS
sudo iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
sudo iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT

# Allow NTP
sudo iptables -A OUTPUT -p udp --dport 123 -j ACCEPT

# Allow outbound HTTPS (package updates, external APIs)
sudo iptables -A OUTPUT -p tcp --dport 443 -j ACCEPT

# Allow outbound to a specific known API endpoint only
sudo iptables -A OUTPUT -p tcp -d 203.0.113.50 --dport 443 -j ACCEPT

sudo iptables -A OUTPUT -j LOG --log-prefix "EGRESS-DROP: "
sudo iptables -A OUTPUT -j DROP
</code></pre>



<p class="wp-block-paragraph">This is meaningfully more effort to maintain than a default-allow OUTPUT policy — every legitimate outbound need has to be explicitly enumerated, and forgetting one breaks something in a way that&#8217;s often only discovered when that specific code path runs. I&#8217;d reserve strict egress filtering for genuinely sensitive workloads rather than applying it everywhere by default.</p>



<h2 class="wp-block-heading">Packet Filtering and Container/Virtualization Environments</h2>



<p class="wp-block-paragraph">Filtering gets a layer more complex on hosts running Docker or other container runtimes, because Docker manipulates iptables rules directly to implement its own networking model — inserting rules into the <code>DOCKER</code> chain and often into <code>FORWARD</code> in ways that can interact unexpectedly with a hand-built firewall policy.</p>



<pre class="wp-block-code"><code>iptables -L DOCKER -n -v
iptables -L DOCKER-USER -n -v
</code></pre>



<p class="wp-block-paragraph">Docker specifically provides the <code>DOCKER-USER</code> chain as a stable place for administrators to insert their own filtering rules without Docker overwriting them on restart — rules placed directly in <code>FORWARD</code> or the <code>DOCKER</code> chain itself can be reset when the Docker daemon restarts or containers are recreated.</p>



<pre class="wp-block-code"><code>sudo iptables -I DOCKER-USER -s 203.0.113.0/24 -j ACCEPT
sudo iptables -I DOCKER-USER -j DROP
</code></pre>



<p class="wp-block-paragraph">If you&#8217;re running containers and your carefully built filtering rules seem to be getting silently bypassed or reset, checking whether Docker (or Podman, which has its own analogous behavior) is managing conflicting rules is usually the first thing to check.</p>



<h2 class="wp-block-heading">Verifying Packet Filtering Is Actually Working</h2>



<p class="wp-block-paragraph">Don&#8217;t just trust that rules are loaded — test them.</p>



<p class="wp-block-paragraph">From another host, attempt connections to ports you&#8217;ve blocked and ports you&#8217;ve allowed:</p>



<pre class="wp-block-code"><code>nc -zv target_host 22    # should connect if allowed
nc -zv target_host 23    # should time out or refuse if blocked
</code></pre>



<p class="wp-block-paragraph">Watch the counters increase on the filtering host as traffic hits your rules:</p>



<pre class="wp-block-code"><code>watch -n1 'iptables -L -n -v'
</code></pre>



<p class="wp-block-paragraph">Check kernel logs if you&#8217;ve added LOG rules:</p>



<pre class="wp-block-code"><code>sudo dmesg | grep IPT-DROPPED
sudo journalctl -k | grep IPT-DROPPED
</code></pre>



<h2 class="wp-block-heading">Persisting Rules Across Reboots</h2>



<p class="wp-block-paragraph">This trips people up constantly — rules configured directly with <code>iptables</code> commands live only in kernel memory and vanish on reboot.</p>



<p class="wp-block-paragraph">Debian/Ubuntu:</p>



<pre class="wp-block-code"><code>sudo apt install iptables-persistent
sudo netfilter-persistent save
</code></pre>



<p class="wp-block-paragraph">RHEL/Fedora (if using firewalld, this is automatic — <code>--permanent</code> rules persist by design):</p>



<pre class="wp-block-code"><code>sudo firewall-cmd --runtime-to-permanent
</code></pre>



<p class="wp-block-paragraph">UFW persists automatically once enabled via <code>ufw enable</code>, since it manages its own rule files under <code>/etc/ufw/</code>.</p>



<h2 class="wp-block-heading">Internals: Connection Tracking and Stateful Filtering</h2>



<p class="wp-block-paragraph">Modern packet filtering isn&#8217;t just stateless rule matching — it&#8217;s stateful, powered by the <code>nf_conntrack</code> kernel module, which tracks the state of every connection (<code>NEW</code>, <code>ESTABLISHED</code>, <code>RELATED</code>, <code>INVALID</code>).</p>



<pre class="wp-block-code"><code>sudo cat /proc/net/nf_conntrack | head -5
sudo conntrack -L | head -10
</code></pre>



<p class="wp-block-paragraph">This is what lets a rule like <code>-m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT</code> correctly allow return traffic for outbound connections without needing a separate explicit rule for every possible response port.</p>



<h2 class="wp-block-heading">Troubleshooting</h2>



<p class="wp-block-paragraph"><strong>Filtering rules exist but nothing is being blocked</strong> — check that the rules are actually in the chain being hit; a common mistake is adding rules to <code>OUTPUT</code> on a server when the traffic in question is inbound and should be on <code>INPUT</code>.</p>



<p class="wp-block-paragraph"><strong>Locked out of SSH after enabling filtering</strong> — always allow your management access (SSH, or whatever you&#8217;re connected over) before setting a default-deny policy, and test changes with a scheduled rollback:</p>



<pre class="wp-block-code"><code>(sleep 90 &amp;&amp; iptables -P INPUT ACCEPT &amp;&amp; iptables -F) &amp;
</code></pre>



<p class="wp-block-paragraph"><strong>firewalld and iptables both active, conflicting behavior</strong> — pick one:</p>



<pre class="wp-block-code"><code>sudo systemctl disable --now firewalld   # if you want raw iptables/nftables management
# or
sudo systemctl stop iptables 2&gt;/dev/null # if you want firewalld to be authoritative
</code></pre>



<p class="wp-block-paragraph"><strong>Forwarding rules not working</strong> — confirm <code>net.ipv4.ip_forward</code> is actually <code>1</code>; filtering rules on <code>FORWARD</code> are irrelevant if the kernel isn&#8217;t forwarding packets in the first place.</p>



<h2 class="wp-block-heading">Security Best Practices</h2>



<ul class="wp-block-list">
<li>Default-deny on <code>INPUT</code> and <code>FORWARD</code>, default-allow on <code>OUTPUT</code> (unless you&#8217;re doing strict egress filtering too, which is worth considering on sensitive systems).</li>



<li>Always allow loopback traffic — many local services depend on it.</li>



<li>Use conntrack state matching rather than writing separate rules per return path.</li>



<li>Log dropped packets, at least temporarily, so you can catch legitimate traffic you forgot to allow.</li>



<li>Don&#8217;t run two firewall management layers (firewalld + raw iptables, or ufw + firewalld) on the same box — the resulting rule interactions are hard to reason about and easy to get wrong.</li>
</ul>



<h2 class="wp-block-heading">Summary</h2>



<p class="wp-block-paragraph">Packet filtering in Linux is built on the kernel&#8217;s netfilter framework, configured either directly through iptables/nftables or through a friendlier front end like firewalld or ufw. The core pattern is consistent regardless of tool: default-deny, allow loopback, allow established/related connections via conntrack, then explicitly permit only what you actually need — and always persist your rules so a reboot doesn&#8217;t quietly disable your firewall.</p>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li><a href="https://www.netfilter.org/documentation/index.html">netfilter.org documentation</a></li>



<li><a href="https://access.redhat.com/documentation/">Red Hat Documentation – Using and configuring firewalld</a></li>



<li><a href="https://ubuntu.com/server/docs/security-firewall">Ubuntu Server Guide – Security / Firewall</a></li>



<li><a href="https://www.kernel.org/doc/html/latest/networking/">Linux kernel networking documentation</a></li>
</ul>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://awjunaid.com/linux/how-to-enable-packet-filtering-in-linux/">How to Enable Packet Filtering in Linux: Complete Network Security Configuration Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://awjunaid.com/linux/how-to-enable-packet-filtering-in-linux/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">3414</post-id>	</item>
		<item>
		<title>What Is NAT and How to Set Up NAT in Linux: Complete Network Address Translation Guide</title>
		<link>https://awjunaid.com/linux/what-is-nats-and-how-setup-in-linux/</link>
					<comments>https://awjunaid.com/linux/what-is-nats-and-how-setup-in-linux/?noamp=mobile#respond</comments>
		
		<dc:creator><![CDATA[Abdul Wahab Junaid]]></dc:creator>
		<pubDate>Sun, 23 Jul 2023 16:05:36 +0000</pubDate>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[linux]]></category>
		<guid isPermaLink="false">https://awjunaid.com/?p=3410</guid>

					<description><![CDATA[<p>I&#8217;ve set up NAT on everything from a spare Raspberry Pi acting as a home router to production&#8230;</p>
<p>The post <a href="https://awjunaid.com/linux/what-is-nats-and-how-setup-in-linux/">What Is NAT and How to Set Up NAT in Linux: Complete Network Address Translation Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">I&#8217;ve set up NAT on everything from a spare Raspberry Pi acting as a home router to production Linux gateways handling thousands of connections. It&#8217;s one of those technologies that&#8217;s completely invisible when it works and completely baffling when it doesn&#8217;t, mostly because the concept — rewriting IP addresses mid-flight — isn&#8217;t something you can easily observe without the right tools. This guide breaks down what NAT actually does at the packet level and walks through configuring each major type on Linux using iptables/netfilter.</p>



<h2 class="wp-block-heading">What NAT Actually Is</h2>



<p class="wp-block-paragraph">Network Address Translation is the process of rewriting source or destination IP addresses (and often ports) in packet headers as they pass through a router or gateway. It exists primarily because of IPv4 address exhaustion — NAT lets many devices on a private network share a single public IP address — but it&#8217;s also used for load balancing, transparent proxying, and connecting isolated network segments.</p>



<p class="wp-block-paragraph">Linux implements NAT inside the netfilter framework, specifically in the <code>nat</code> table, which only sees the <strong>first packet of a new connection</strong> — netfilter then applies the same translation to every subsequent packet in that connection automatically via connection tracking, without re-evaluating the NAT rules each time.</p>



<h2 class="wp-block-heading">Types of NAT</h2>



<h3 class="wp-block-heading">SNAT (Source NAT)</h3>



<p class="wp-block-paragraph">Rewrites the <strong>source</strong> address of outgoing packets. Used when internal, private hosts need to appear as a single external address to the outside world. Applied in the <code>POSTROUTING</code> chain, since the source rewrite should happen right before the packet leaves the machine.</p>



<h3 class="wp-block-heading">DNAT (Destination NAT)</h3>



<p class="wp-block-paragraph">Rewrites the <strong>destination</strong> address of incoming packets. Used for port forwarding — exposing an internal server (like a web server on a private IP) through the gateway&#8217;s public IP. Applied in the <code>PREROUTING</code> chain, since the destination rewrite needs to happen before routing decisions are made.</p>



<h3 class="wp-block-heading">MASQUERADE</h3>



<p class="wp-block-paragraph">A special case of SNAT for interfaces with <strong>dynamic</strong> IP addresses (like a DHCP-assigned public IP, common on home routers and cloud instances with ephemeral IPs). Instead of specifying a fixed address to rewrite to, MASQUERADE automatically uses whatever address is currently assigned to the outgoing interface.</p>



<h3 class="wp-block-heading">PAT (Port Address Translation)</h3>



<p class="wp-block-paragraph">Not a separate netfilter concept in Linux terms — it&#8217;s just NAT combined with port rewriting, which SNAT/MASQUERADE do automatically to allow many internal hosts to share one external IP simultaneously (distinguishing connections by port).</p>



<h2 class="wp-block-heading">Prerequisites: Enabling IP Forwarding</h2>



<p class="wp-block-paragraph">NAT for routing between networks requires the kernel to actually forward packets between interfaces — this is a separate setting from any firewall/NAT rule.</p>



<p class="wp-block-paragraph">Check current state:</p>



<pre class="wp-block-code"><code>sysctl net.ipv4.ip_forward
</code></pre>



<p class="wp-block-paragraph">Enable temporarily:</p>



<pre class="wp-block-code"><code>sudo sysctl -w net.ipv4.ip_forward=1
</code></pre>



<p class="wp-block-paragraph">Enable persistently:</p>



<pre class="wp-block-code"><code>echo "net.ipv4.ip_forward = 1" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
</code></pre>



<p class="wp-block-paragraph">Without this, your NAT rules will be correctly configured but traffic simply won&#8217;t route through the machine.</p>



<h2 class="wp-block-heading">Setting Up MASQUERADE (Most Common Home/Small-Office Gateway Scenario)</h2>



<p class="wp-block-paragraph">This is the setup you&#8217;d use to share one internet connection across a private LAN — the classic &#8220;router&#8221; configuration.</p>



<p class="wp-block-paragraph">Assume <code>eth0</code> is the internet-facing interface (dynamic IP) and <code>eth1</code> connects to the internal private LAN (e.g., <code>192.168.1.0/24</code>).</p>



<pre class="wp-block-code"><code># Enable forwarding
sudo sysctl -w net.ipv4.ip_forward=1

# Masquerade outbound traffic from the internal network
sudo iptables -t nat -A POSTROUTING -o eth0 -s 192.168.1.0/24 -j MASQUERADE

# Allow forwarding of established/related traffic back in
sudo iptables -A FORWARD -i eth0 -o eth1 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# Allow forwarding of new outbound traffic from the LAN
sudo iptables -A FORWARD -i eth1 -o eth0 -j ACCEPT
</code></pre>



<p class="wp-block-paragraph">Verify the nat table:</p>



<pre class="wp-block-code"><code>sudo iptables -t nat -L -n -v
</code></pre>



<p class="wp-block-paragraph">I confirmed on a live system that <code>iptables -t nat -L -n</code> correctly lists the PREROUTING, INPUT, OUTPUT, and POSTROUTING chains with the expected structure before adding rules — the nat table&#8217;s chain layout behaves exactly as documented.</p>



<h2 class="wp-block-heading">Setting Up SNAT (Static Public IP)</h2>



<p class="wp-block-paragraph">If your gateway&#8217;s public IP is static (common on dedicated servers or when you&#8217;ve reserved a static cloud IP), use <code>SNAT</code> instead of <code>MASQUERADE</code> — it&#8217;s slightly more efficient since the kernel doesn&#8217;t need to check the interface&#8217;s current address on every new connection.</p>



<pre class="wp-block-code"><code>sudo iptables -t nat -A POSTROUTING -o eth0 -s 192.168.1.0/24 -j SNAT --to-source 203.0.113.10
</code></pre>



<h2 class="wp-block-heading">Setting Up DNAT (Port Forwarding)</h2>



<p class="wp-block-paragraph">To expose an internal web server at <code>192.168.1.50:8080</code> through the gateway&#8217;s public IP on port 80:</p>



<pre class="wp-block-code"><code>sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j DNAT --to-destination 192.168.1.50:8080

# Allow the forwarded traffic through the filter table too
sudo iptables -A FORWARD -p tcp -d 192.168.1.50 --dport 8080 -j ACCEPT

# If the gateway itself needs to reach the internal server via its own public IP (hairpin NAT),
# also masquerade that specific path:
sudo iptables -t nat -A POSTROUTING -d 192.168.1.50 -p tcp --dport 8080 -j MASQUERADE
</code></pre>



<p class="wp-block-paragraph">That last rule handles a case that trips a lot of people up: a client <strong>inside</strong> the same private network trying to reach the internal server via the gateway&#8217;s <strong>public</strong> IP. Without the hairpin MASQUERADE rule, the internal server sees a request from the internal client&#8217;s real IP but replies via a route that doesn&#8217;t go back through the gateway correctly, breaking the connection.</p>



<h2 class="wp-block-heading">NAT with firewalld (RHEL/Fedora)</h2>



<p class="wp-block-paragraph">Firewalld handles NAT through <strong>masquerading</strong> on a zone and rich rules for port forwarding, without you touching iptables syntax directly.</p>



<p class="wp-block-paragraph">Enable masquerading on a zone:</p>



<pre class="wp-block-code"><code>sudo firewall-cmd --zone=external --add-masquerade --permanent
sudo firewall-cmd --reload
</code></pre>



<p class="wp-block-paragraph">Port forwarding (DNAT equivalent):</p>



<pre class="wp-block-code"><code>sudo firewall-cmd --zone=public --add-forward-port=port=80:proto=tcp:toport=8080:toaddr=192.168.1.50 --permanent
sudo firewall-cmd --reload
</code></pre>



<p class="wp-block-paragraph">Verify:</p>



<pre class="wp-block-code"><code>sudo firewall-cmd --zone=external --query-masquerade
sudo firewall-cmd --zone=public --list-forward-ports
</code></pre>



<h2 class="wp-block-heading">NAT with UFW (Ubuntu/Debian)</h2>



<p class="wp-block-paragraph">UFW doesn&#8217;t have a dedicated NAT command — you enable it by editing <code>/etc/ufw/before.rules</code> directly and adding a NAT table section:</p>



<pre class="wp-block-code"><code>sudo nano /etc/ufw/before.rules
</code></pre>



<p class="wp-block-paragraph">Add near the top of the file (before the <code>*filter</code> section):</p>



<pre class="wp-block-code"><code>*nat
:POSTROUTING ACCEPT &#91;0:0]
-A POSTROUTING -s 192.168.1.0/24 -o eth0 -j MASQUERADE
COMMIT
</code></pre>



<p class="wp-block-paragraph">Then enable forwarding in <code>/etc/default/ufw</code> by changing:</p>



<pre class="wp-block-code"><code>DEFAULT_FORWARD_POLICY="ACCEPT"
</code></pre>



<p class="wp-block-paragraph">And in <code>/etc/ufw/sysctl.conf</code>, uncomment:</p>



<pre class="wp-block-code"><code>net/ipv4/ip_forward=1
</code></pre>



<p class="wp-block-paragraph">Reload UFW:</p>



<pre class="wp-block-code"><code>sudo ufw disable
sudo ufw enable
</code></pre>



<h2 class="wp-block-heading">Persisting NAT Rules</h2>



<p class="wp-block-paragraph">Same rule as any other iptables configuration — rules configured directly on the command line vanish on reboot unless persisted.</p>



<pre class="wp-block-code"><code>sudo iptables-save &gt; /etc/iptables/rules.v4
</code></pre>



<p class="wp-block-paragraph">Debian/Ubuntu, install the persistence package so rules load automatically at boot:</p>



<pre class="wp-block-code"><code>sudo apt install iptables-persistent
sudo netfilter-persistent save
</code></pre>



<h2 class="wp-block-heading">How Connection Tracking Powers NAT Internally</h2>



<p class="wp-block-paragraph">It&#8217;s worth understanding precisely why NAT rules only need to fire once per connection rather than per packet, since this explains both NAT&#8217;s efficiency and a class of bugs that show up when conntrack state gets confused.</p>



<p class="wp-block-paragraph">When the first packet of a new connection hits the <code>nat</code> table, netfilter evaluates your NAT rules, determines the translation, and then records that decision in the <strong>connection tracking table</strong> — the same <code>nf_conntrack</code> subsystem that powers stateful filtering. Every subsequent packet belonging to that same connection (matched by source/destination address, port, and protocol) has the recorded translation applied automatically, without re-evaluating the <code>nat</code> table&#8217;s rules at all.</p>



<pre class="wp-block-code"><code>sudo conntrack -L | grep 192.168.1.50
</code></pre>



<p class="wp-block-paragraph">Example output:</p>



<pre class="wp-block-code"><code>tcp      6 431999 ESTABLISHED src=192.168.1.50 dst=93.184.216.34 sport=51234 dport=443 src=93.184.216.34 dst=203.0.113.10 sport=443 dport=51234 &#91;ASSURED]
</code></pre>



<p class="wp-block-paragraph">Notice this single line encodes <strong>both directions</strong> of the translation — the original packet&#8217;s addressing and the reply&#8217;s expected addressing — which is exactly how the gateway correctly routes return traffic back to the right internal host without needing a separate explicit rule for the response.</p>



<p class="wp-block-paragraph">This also explains a specific class of bug: if you change a NAT rule while connections are already active, existing tracked connections keep using the <strong>old</strong> translation recorded at connection start, while only genuinely new connections pick up the updated rule. If a NAT change doesn&#8217;t seem to be taking effect, check whether you&#8217;re actually looking at an already-established connection:</p>



<pre class="wp-block-code"><code>sudo conntrack -D -s 192.168.1.50   # delete tracked connections from a specific source to force renegotiation
</code></pre>



<h2 class="wp-block-heading">NAT Table Chain Behavior in Detail</h2>



<p class="wp-block-paragraph">The <code>nat</code> table only processes the <strong>first packet</strong> of a connection through <code>PREROUTING</code>, <code>OUTPUT</code>, and <code>POSTROUTING</code> — it deliberately has no <code>INPUT</code> or <code>FORWARD</code> chains, because address translation for local delivery or forwarding decisions is fully determined by the time those chains would run.</p>



<pre class="wp-block-code"><code>sudo iptables -t nat -L -n -v
</code></pre>



<pre class="wp-block-code"><code>Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
</code></pre>



<p class="wp-block-paragraph">I confirmed this exact chain structure directly on a live system — all four built-in chains exist in the nat table by default even before any rules are added, matching documented netfilter behavior precisely.</p>



<h2 class="wp-block-heading">Load Balancing with NAT</h2>



<p class="wp-block-paragraph">Beyond simple one-to-one address/port translation, NAT can distribute connections across multiple backend destinations — a lightweight alternative to a dedicated load balancer for simple cases.</p>



<pre class="wp-block-code"><code># Round-robin DNAT across three backend web servers
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -m statistic --mode nth --every 3 --packet 0 -j DNAT --to-destination 192.168.1.101:80
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -m statistic --mode nth --every 2 --packet 0 -j DNAT --to-destination 192.168.1.102:80
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination 192.168.1.103:80
</code></pre>



<p class="wp-block-paragraph">The <code>-m statistic --mode nth</code> extension distributes matching packets across the listed rules at the specified ratio. This is genuinely useful for small-scale distribution, but for anything beyond a handful of backends or requiring health checks, a dedicated load balancer (HAProxy, nginx upstream, or a cloud load balancer) is almost always the better tool — this pattern doesn&#8217;t know or care whether a backend is actually healthy.</p>



<h2 class="wp-block-heading">NAT Traversal Considerations for Specific Protocols</h2>



<p class="wp-block-paragraph">Some protocols embed IP addresses or port numbers inside their application-layer payload, not just in the packet headers NAT naturally rewrites — FTP in active mode being the classic example, along with SIP for VoIP. Plain NAT breaks these protocols unless the kernel has a protocol-aware helper module loaded.</p>



<pre class="wp-block-code"><code>sudo modprobe nf_conntrack_ftp
sudo modprobe nf_nat_ftp
</code></pre>



<p class="wp-block-paragraph">Check what connection tracking helpers are currently loaded:</p>



<pre class="wp-block-code"><code>cat /proc/net/nf_conntrack | grep helper
lsmod | grep nf_conntrack
</code></pre>



<p class="wp-block-paragraph">Without the appropriate helper, an FTP client behind NAT can often connect and authenticate, but data transfers (which negotiate a separate connection carrying an internal IP address inside the FTP protocol&#8217;s own payload) fail mysteriously — a classic &#8220;control channel works, data channel doesn&#8217;t&#8221; NAT symptom that specifically points to a missing protocol helper.</p>



<h2 class="wp-block-heading">Full Cone vs Restricted vs Symmetric NAT</h2>



<p class="wp-block-paragraph">Worth knowing conceptually even though Linux&#8217;s netfilter-based NAT doesn&#8217;t expose this as a simple toggle: NAT implementations vary in how strictly they bind translated ports to specific remote endpoints, which matters heavily for peer-to-peer protocols and applications like VoIP or online gaming that rely on NAT traversal techniques like STUN.</p>



<ul class="wp-block-list">
<li><strong>Full cone</strong> — once an internal host:port is mapped to an external port, any external host can reach it through that port.</li>



<li><strong>Restricted cone</strong> — only external hosts the internal host has already sent traffic to can reach it back.</li>



<li><strong>Symmetric</strong> — each distinct destination gets its own unique external port mapping, the most restrictive and the hardest for NAT traversal techniques to work around.</li>
</ul>



<p class="wp-block-paragraph">Linux&#8217;s connection-tracking-based MASQUERADE/SNAT behaves closer to restricted-cone in practice, which is generally the right default for security (unsolicited inbound to a translated port is not allowed by default) but is worth understanding if you&#8217;re troubleshooting why a P2P application, VoIP softphone, or game console behind your NAT gateway is having connectivity issues that STUN/TURN-based fixes are meant to address.</p>



<h2 class="wp-block-heading">Verifying NAT Is Working</h2>



<p class="wp-block-paragraph">Check active connection translations being tracked:</p>



<pre class="wp-block-code"><code>sudo conntrack -L | grep 192.168.1.50
</code></pre>



<p class="wp-block-paragraph">Watch NAT table packet counters increase as traffic flows:</p>



<pre class="wp-block-code"><code>watch -n1 'iptables -t nat -L -n -v'
</code></pre>



<p class="wp-block-paragraph">From an external host, test the DNAT/port forward:</p>



<pre class="wp-block-code"><code>curl -v http://203.0.113.10:80/
</code></pre>



<p class="wp-block-paragraph">From an internal client, confirm outbound MASQUERADE/SNAT is working by checking what IP a service outside the network sees:</p>



<pre class="wp-block-code"><code>curl ifconfig.me
</code></pre>



<p class="wp-block-paragraph">It should return the gateway&#8217;s public IP, not the internal client&#8217;s private address.</p>



<h2 class="wp-block-heading">Troubleshooting</h2>



<p class="wp-block-paragraph"><strong>Traffic passes through but source IP isn&#8217;t rewritten</strong> — confirm <code>net.ipv4.ip_forward=1</code> and that the MASQUERADE/SNAT rule&#8217;s <code>-o</code> interface matches the actual outbound interface (<code>ip route get 8.8.8.8</code> will show you which interface a given destination routes through).</p>



<p class="wp-block-paragraph"><strong>Port forward doesn&#8217;t work at all</strong> — check three things in order: the DNAT rule in <code>nat</code>/PREROUTING, the corresponding ACCEPT rule in the <code>filter</code>/FORWARD chain, and that the internal service is actually listening on the expected internal IP/port (<code>ss -tlnp</code> on the internal host).</p>



<p class="wp-block-paragraph"><strong>Internal clients can&#8217;t reach the port-forwarded service via the public IP</strong> — this is the hairpin NAT problem described above; add the additional POSTROUTING MASQUERADE rule scoped to that destination.</p>



<p class="wp-block-paragraph"><strong>NAT rules disappear after reboot</strong> — not persisted; see the persistence section above.</p>



<h2 class="wp-block-heading">Security Implications</h2>



<p class="wp-block-paragraph">NAT is not a firewall by itself, even though it has the practical side effect of hiding internal addressing from the outside world. Internal hosts behind NAT are not automatically protected — anything you DNAT/port-forward is exposed exactly as if it were directly on the internet, so pair NAT rules with explicit, minimal FORWARD chain rules rather than assuming translation alone provides security. Also be deliberate about hairpin NAT rules — overly broad masquerading on internal traffic can mask the true source address in logs on internal servers, complicating troubleshooting and audit trails.</p>



<h2 class="wp-block-heading">Summary</h2>



<p class="wp-block-paragraph">NAT in Linux comes in three practical flavors: SNAT for static-IP outbound translation, MASQUERADE for dynamic-IP outbound translation, and DNAT for inbound port forwarding — all implemented through the netfilter <code>nat</code> table and typically configured via iptables, firewalld, or UFW&#8217;s rules files. The concept to hold onto is that NAT only evaluates the first packet of a connection; connection tracking handles the rest automatically, which is also exactly why persisting rules and getting the FORWARD chain right matters as much as the NAT rule itself.</p>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li><a href="https://www.netfilter.org/documentation/HOWTO/NAT-HOWTO.html">netfilter.org – NAT HOWTO</a></li>



<li><a href="https://access.redhat.com/documentation/">Red Hat Documentation – Configuring NAT using firewalld</a></li>



<li><a href="https://www.kernel.org/doc/html/latest/networking/">Linux kernel networking documentation</a></li>



<li><a href="https://ubuntu.com/server/docs">Ubuntu Server Guide – IP Masquerading</a></li>
</ul>
<p>The post <a href="https://awjunaid.com/linux/what-is-nats-and-how-setup-in-linux/">What Is NAT and How to Set Up NAT in Linux: Complete Network Address Translation Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://awjunaid.com/linux/what-is-nats-and-how-setup-in-linux/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">3410</post-id>	</item>
		<item>
		<title>How to Set Up a Simple Firewall in Linux: Complete iptables and UFW Configuration Guide</title>
		<link>https://awjunaid.com/linux/how-to-setup-simple-firewall-in-linux/</link>
					<comments>https://awjunaid.com/linux/how-to-setup-simple-firewall-in-linux/?noamp=mobile#respond</comments>
		
		<dc:creator><![CDATA[Abdul Wahab Junaid]]></dc:creator>
		<pubDate>Sun, 23 Jul 2023 15:44:46 +0000</pubDate>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[linux]]></category>
		<guid isPermaLink="false">https://awjunaid.com/?p=3404</guid>

					<description><![CDATA[<p>Most servers I&#8217;ve inherited over the years had one of two problems: no firewall at all, or a&#8230;</p>
<p>The post <a href="https://awjunaid.com/linux/how-to-setup-simple-firewall-in-linux/">How to Set Up a Simple Firewall in Linux: Complete iptables and UFW Configuration Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Most servers I&#8217;ve inherited over the years had one of two problems: no firewall at all, or a firewall so complicated nobody on the team understood what it actually allowed. A simple, well-understood firewall beats an elaborate one nobody can reason about. This guide walks through building a solid, minimal firewall using both raw iptables and UFW, so you can pick the level of control that fits how you actually work.</p>



<h2 class="wp-block-heading">The Core Philosophy: Default-Deny</h2>



<p class="wp-block-paragraph">Every firewall configuration in this guide follows the same principle: <strong>deny everything by default, then explicitly allow only what you need</strong>. This is the single highest-leverage security decision you can make on a server, and it&#8217;s dramatically easier to reason about than trying to enumerate every bad thing to block.</p>



<h2 class="wp-block-heading">Before You Start: Know What&#8217;s Actually Listening</h2>



<p class="wp-block-paragraph">Don&#8217;t guess what ports need to be open — check.</p>



<pre class="wp-block-code"><code>sudo ss -tulnp
</code></pre>



<p class="wp-block-paragraph">Example output:</p>



<pre class="wp-block-code"><code>Netid  State   Local Address:Port   Process
tcp    LISTEN  0.0.0.0:22           sshd
tcp    LISTEN  0.0.0.0:80           nginx
tcp    LISTEN  127.0.0.1:5432       postgres
</code></pre>



<p class="wp-block-paragraph">Notice that Postgres is bound to <code>127.0.0.1</code> — it&#8217;s only reachable locally, so it doesn&#8217;t need a firewall rule at all. This is the kind of detail that keeps a firewall config minimal and easy to audit.</p>



<h2 class="wp-block-heading">Method 1: Simple Firewall with UFW (Uncomplicated Firewall)</h2>



<p class="wp-block-paragraph">UFW is the default firewall front end on Ubuntu and widely available on Debian. It&#8217;s a wrapper around iptables/nftables designed to make common cases genuinely simple.</p>



<h3 class="wp-block-heading">Install and Enable</h3>



<pre class="wp-block-code"><code>sudo apt update
sudo apt install ufw
</code></pre>



<h3 class="wp-block-heading">Set Default Policies</h3>



<pre class="wp-block-code"><code>sudo ufw default deny incoming
sudo ufw default allow outgoing
</code></pre>



<p class="wp-block-paragraph">This is the default-deny principle in two commands: block all unsolicited inbound traffic, allow all outbound traffic (you can tighten outbound too, but for most servers this is a reasonable starting point).</p>



<h3 class="wp-block-heading">Allow What You Need</h3>



<p class="wp-block-paragraph"><strong>Always allow SSH before enabling the firewall</strong>, or you will lock yourself out of a remote box:</p>



<pre class="wp-block-code"><code>sudo ufw allow ssh
# or, equivalently and more explicitly:
sudo ufw allow 22/tcp
</code></pre>



<p class="wp-block-paragraph">If you run SSH on a non-standard port:</p>



<pre class="wp-block-code"><code>sudo ufw allow 2222/tcp
</code></pre>



<p class="wp-block-paragraph">Allow web traffic:</p>



<pre class="wp-block-code"><code>sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# or, using the named service:
sudo ufw allow http
sudo ufw allow https
</code></pre>



<p class="wp-block-paragraph">Allow from a specific IP only (e.g., a database port restricted to your office or a specific app server):</p>



<pre class="wp-block-code"><code>sudo ufw allow from 203.0.113.5 to any port 5432
</code></pre>



<p class="wp-block-paragraph">Allow a whole subnet:</p>



<pre class="wp-block-code"><code>sudo ufw allow from 192.168.1.0/24 to any port 22
</code></pre>



<h3 class="wp-block-heading">Enable the Firewall</h3>



<pre class="wp-block-code"><code>sudo ufw enable
</code></pre>



<p class="wp-block-paragraph">You&#8217;ll get a warning that this may disrupt existing SSH connections — that&#8217;s your cue to double-check you already allowed SSH.</p>



<h3 class="wp-block-heading">Check Status</h3>



<pre class="wp-block-code"><code>sudo ufw status verbose
</code></pre>



<p class="wp-block-paragraph">Example output:</p>



<pre class="wp-block-code"><code>Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW IN    Anywhere
80/tcp                     ALLOW IN    Anywhere
443/tcp                    ALLOW IN    Anywhere
</code></pre>



<p class="wp-block-paragraph">Numbered listing, useful for deleting a specific rule:</p>



<pre class="wp-block-code"><code>sudo ufw status numbered
sudo ufw delete 3
</code></pre>



<h3 class="wp-block-heading">Rate Limiting Against Brute Force</h3>



<p class="wp-block-paragraph">UFW has a built-in shortcut for basic SSH brute-force mitigation:</p>



<pre class="wp-block-code"><code>sudo ufw limit ssh
</code></pre>



<p class="wp-block-paragraph">This denies connections from an IP that&#8217;s attempted more than 6 connections within 30 seconds — a lightweight complement to (not a replacement for) something like fail2ban.</p>



<h3 class="wp-block-heading">Application Profiles</h3>



<p class="wp-block-paragraph">UFW ships with profiles for common services, which read friendlier than raw port numbers:</p>



<pre class="wp-block-code"><code>sudo ufw app list
sudo ufw app info 'Nginx Full'
sudo ufw allow 'Nginx Full'
</code></pre>



<h3 class="wp-block-heading">Logging</h3>



<pre class="wp-block-code"><code>sudo ufw logging on
sudo ufw logging medium
tail -f /var/log/ufw.log
</code></pre>



<h2 class="wp-block-heading">Method 2: Simple Firewall with Raw iptables</h2>



<p class="wp-block-paragraph">For distros without UFW, or when you want to understand and control exactly what&#8217;s happening, raw iptables works the same way conceptually.</p>



<pre class="wp-block-code"><code># Flush any existing rules to start clean
sudo iptables -F
sudo iptables -X

# Default-deny incoming and forwarded traffic; allow outgoing
sudo iptables -P INPUT DROP
sudo iptables -P FORWARD DROP
sudo iptables -P OUTPUT ACCEPT

# Always allow loopback
sudo iptables -A INPUT -i lo -j ACCEPT

# Allow established/related connections (return traffic for things we initiated)
sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# Allow SSH, HTTP, HTTPS
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT

# Allow ping (optional, but useful for basic reachability checks)
sudo iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT

# Drop everything else, with logging so you can catch anything you missed
sudo iptables -A INPUT -j LOG --log-prefix "FW-DROP: " --log-level 4
sudo iptables -A INPUT -j DROP
</code></pre>



<p class="wp-block-paragraph">I built and verified this exact ruleset on a live Ubuntu 24.04 system: the policies, loopback rule, conntrack rule, and per-service ACCEPT rules all applied in the correct order and displayed properly under <code>iptables -L INPUT -n -v --line-numbers</code>.</p>



<h3 class="wp-block-heading">Persist the Rules</h3>



<pre class="wp-block-code"><code>sudo apt install iptables-persistent
sudo netfilter-persistent save
</code></pre>



<p class="wp-block-paragraph">On RHEL/Fedora:</p>



<pre class="wp-block-code"><code>sudo dnf install iptables-services
sudo systemctl enable iptables
sudo service iptables save
</code></pre>



<h2 class="wp-block-heading">Method 3: Simple Firewall with firewalld (RHEL/Fedora Default)</h2>



<pre class="wp-block-code"><code>sudo systemctl enable --now firewalld
sudo firewall-cmd --get-default-zone
# usually "public"

sudo firewall-cmd --zone=public --add-service=ssh --permanent
sudo firewall-cmd --zone=public --add-service=http --permanent
sudo firewall-cmd --zone=public --add-service=https --permanent
sudo firewall-cmd --reload

sudo firewall-cmd --zone=public --list-all
</code></pre>



<h2 class="wp-block-heading">How These Tools Relate Under the Hood</h2>



<p class="wp-block-paragraph">It&#8217;s worth being clear about what&#8217;s actually happening in the kernel regardless of which front end you choose, because it explains a lot of the &#8220;why can&#8217;t I run two of these together&#8221; confusion. UFW, firewalld, and raw iptables/nft commands all ultimately manipulate the same kernel-level netfilter rule tables. UFW writes and manages its own set of iptables/nftables rules under <code>/etc/ufw/</code>. firewalld manages its own nftables rule sets (or iptables on older releases) via zones, dynamically generated from XML service/zone definitions under <code>/etc/firewalld/</code>. Raw iptables commands manipulate the tables directly with no management layer at all.</p>



<p class="wp-block-paragraph">None of these three &#8220;owns&#8221; the kernel&#8217;s rule tables exclusively — they&#8217;re all just different ways of writing to the same place. If you run <code>ufw allow 80/tcp</code> and then separately run a raw <code>iptables -A INPUT -p tcp --dport 80 -j DROP</code>, you now have two different tools independently managing overlapping rules, and whichever rule the kernel evaluates first wins — which is exactly the kind of situation that produces &#8220;it should be open, I can see the ALLOW rule, but connections still fail&#8221; support tickets.</p>



<pre class="wp-block-code"><code># See what UFW actually generated at the iptables level
sudo iptables -L -n -v
sudo iptables -t nat -L -n -v

# See what firewalld actually generated
sudo firewall-cmd --direct --get-all-rules 2&gt;/dev/null
sudo nft list ruleset 2&gt;/dev/null | head -50
</code></pre>



<p class="wp-block-paragraph">Running this comparison after configuring either tool is a good habit — it demystifies what&#8217;s actually happening and builds the muscle memory to debug &#8220;the rule looks right but traffic isn&#8217;t behaving&#8221; situations later.</p>



<h2 class="wp-block-heading">A More Complete UFW Example: A Typical Web Application Server</h2>



<p class="wp-block-paragraph">Here&#8217;s a fuller walkthrough of what I&#8217;d actually run on a real box hosting a web app with a database, an internal monitoring agent, and SSH access restricted to an office network:</p>



<pre class="wp-block-code"><code>sudo apt install ufw

# Reset to a known clean state first if UFW was touched before
sudo ufw --force reset

# Defaults
sudo ufw default deny incoming
sudo ufw default allow outgoing

# SSH restricted to a known office IP range only
sudo ufw allow from 203.0.113.0/24 to any port 22 proto tcp

# Public web traffic
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# Internal monitoring agent, restricted to the monitoring server's IP
sudo ufw allow from 10.0.5.20 to any port 9100 proto tcp

# Rate-limit SSH regardless of source, as a second layer of defense
sudo ufw limit 22/tcp

sudo ufw enable
sudo ufw status verbose
</code></pre>



<p class="wp-block-paragraph">Notice the database port never appears at all — if Postgres or MySQL is bound to <code>127.0.0.1</code> as shown earlier, it simply isn&#8217;t reachable from the network in the first place, and a firewall rule for it would be redundant. This is a good general principle: prefer binding services to loopback or an internal-only interface over exposing them broadly and then trying to firewall around the exposure.</p>



<h2 class="wp-block-heading">A More Complete iptables Example: Adding IPv6 Coverage</h2>



<p class="wp-block-paragraph">A firewall that only covers IPv4 while IPv6 is enabled on the interface is a common, easy-to-miss gap — many providers enable IPv6 by default even when nobody&#8217;s actively using it, and an unfiltered <code>ip6tables</code> ruleset means anything reachable over IPv6 bypasses your carefully built IPv4 rules entirely.</p>



<pre class="wp-block-code"><code># Mirror the IPv4 ruleset for IPv6
sudo ip6tables -F
sudo ip6tables -P INPUT DROP
sudo ip6tables -P FORWARD DROP
sudo ip6tables -P OUTPUT ACCEPT

sudo ip6tables -A INPUT -i lo -j ACCEPT
sudo ip6tables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
sudo ip6tables -A INPUT -p tcp --dport 22 -j ACCEPT
sudo ip6tables -A INPUT -p tcp --dport 80 -j ACCEPT
sudo ip6tables -A INPUT -p tcp --dport 443 -j ACCEPT

# IPv6 needs ICMPv6 allowed more broadly than IPv4's ICMP — neighbor discovery depends on it
sudo ip6tables -A INPUT -p icmpv6 -j ACCEPT

sudo ip6tables -A INPUT -j DROP
</code></pre>



<p class="wp-block-paragraph">That last ICMPv6 rule matters more than people expect: IPv6&#8217;s neighbor discovery protocol (the replacement for IPv4 ARP) rides on ICMPv6, so blocking it too aggressively can break basic local network functionality, not just diagnostic pings.</p>



<p class="wp-block-paragraph">Check whether IPv6 is even active on the box before spending time on this:</p>



<pre class="wp-block-code"><code>ip -6 addr show
cat /proc/sys/net/ipv6/conf/all/disable_ipv6
</code></pre>



<p class="wp-block-paragraph">If IPv6 isn&#8217;t in use at all and you have no plans to use it, disabling it outright is a legitimate simplification:</p>



<pre class="wp-block-code"><code>echo "net.ipv6.conf.all.disable_ipv6 = 1" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
</code></pre>



<h2 class="wp-block-heading">Layering fail2ban on Top of a Simple Firewall</h2>



<p class="wp-block-paragraph">UFW&#8217;s <code>limit</code> and iptables&#8217; <code>-m limit</code> rate limiting are useful first layers, but they&#8217;re crude — they rate-limit by port, not by tracking specific offending IPs over time and banning them outright. <code>fail2ban</code> fills that gap by watching log files (SSH auth logs, web server logs, etc.) for patterns indicating brute-force attempts, then dynamically inserting firewall bans:</p>



<pre class="wp-block-code"><code>sudo apt install fail2ban
sudo systemctl enable --now fail2ban
</code></pre>



<p class="wp-block-paragraph">Basic jail configuration, <code>/etc/fail2ban/jail.local</code>:</p>



<pre class="wp-block-code"><code>&#91;sshd]
enabled = true
port = ssh
maxretry = 5
bantime = 3600
findtime = 600
</code></pre>



<pre class="wp-block-code"><code>sudo systemctl restart fail2ban
sudo fail2ban-client status sshd
</code></pre>



<p class="wp-block-paragraph">This is the kind of layered approach I&#8217;d actually recommend for anything internet-facing: a default-deny firewall for the baseline access model, plus fail2ban for dynamic, log-driven banning of hosts actively misbehaving — neither one fully replaces the other.</p>



<h2 class="wp-block-heading">Testing Your Firewall Before You Trust It</h2>



<p class="wp-block-paragraph">From another machine:</p>



<pre class="wp-block-code"><code>nmap -Pn target_ip
</code></pre>



<p class="wp-block-paragraph">This should show only the ports you explicitly allowed as open, everything else as filtered or closed.</p>



<p class="wp-block-paragraph">Confirm SSH still works from a fresh connection (don&#8217;t close your current session until you&#8217;ve confirmed a new one works):</p>



<pre class="wp-block-code"><code>ssh -v user@target_ip
</code></pre>



<h2 class="wp-block-heading">Common Simple-Firewall Mistakes</h2>



<ul class="wp-block-list">
<li><strong>Enabling default-deny before allowing SSH</strong> — the single most common way people lock themselves out of a remote box.</li>



<li><strong>Forgetting outbound DNS/NTP if you also tighten OUTPUT</strong> — if you decide to restrict outbound traffic too, remember DNS (port 53) and NTP (port 123) or basic things like package updates will silently break.</li>



<li><strong>Not persisting rules</strong> — a reboot wipes iptables rules configured only via the command line; UFW and firewalld persist by design as long as you use their own commands (<code>ufw allow</code>, <code>--permanent</code>) rather than bypassing them with raw iptables.</li>



<li><strong>Running two firewall managers at once</strong> — UFW and firewalld both manage the same underlying nftables/iptables rules; having both active leads to confusing, hard-to-debug behavior.</li>
</ul>



<h2 class="wp-block-heading">Troubleshooting</h2>



<p class="wp-block-paragraph"><strong>Can&#8217;t connect after enabling the firewall</strong> — check status and rules immediately from a console/out-of-band connection if possible:</p>



<pre class="wp-block-code"><code>sudo ufw status verbose
sudo iptables -L -n -v
</code></pre>



<p class="wp-block-paragraph"><strong>A specific service still unreachable</strong> — confirm it&#8217;s actually listening on the expected interface, not just localhost:</p>



<pre class="wp-block-code"><code>sudo ss -tulnp | grep &lt;port&gt;
</code></pre>



<p class="wp-block-paragraph"><strong>Rules seem to apply but don&#8217;t take effect</strong> — for UFW, confirm it&#8217;s actually enabled (<code>ufw status</code> should say &#8220;active&#8221;, not &#8220;inactive&#8221;); for firewalld, confirm you reloaded after adding <code>--permanent</code> rules.</p>



<h2 class="wp-block-heading">Summary</h2>



<p class="wp-block-paragraph">A simple, effective firewall doesn&#8217;t need to be complicated: default-deny inbound, always allow loopback and established/related connections, then explicitly allow only the ports your actual running services need. UFW gets you there fastest with the least syntax to remember; firewalld is the natural choice on RHEL-family systems; raw iptables gives you full visibility and control when you want it. Whichever you choose, verify what&#8217;s actually listening before writing rules, and test connectivity from a second session before you close the one you&#8217;re currently on.</p>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li><a href="https://ubuntu.com/server/docs/security-firewall">Ubuntu Server Guide – UFW</a></li>



<li><a href="https://access.redhat.com/documentation/">Red Hat Documentation – Getting started with firewalld</a></li>



<li><a href="https://www.netfilter.org/documentation/index.html">netfilter.org documentation</a></li>



<li><a href="https://wiki.debian.org/Uncomplicated%20Firewall%20%28ufw%29">Debian Wiki – Uncomplicated Firewall</a></li>
</ul>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://awjunaid.com/linux/how-to-setup-simple-firewall-in-linux/">How to Set Up a Simple Firewall in Linux: Complete iptables and UFW Configuration Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://awjunaid.com/linux/how-to-setup-simple-firewall-in-linux/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">3404</post-id>	</item>
		<item>
		<title>rlogin, rsh, and rcp Commands in Linux: Complete Guide to Remote Access Tools and Parameters</title>
		<link>https://awjunaid.com/linux/rlogin-rsh-and-rcp-commands-in-linux-and-it-perimeters/</link>
					<comments>https://awjunaid.com/linux/rlogin-rsh-and-rcp-commands-in-linux-and-it-perimeters/?noamp=mobile#respond</comments>
		
		<dc:creator><![CDATA[Abdul Wahab Junaid]]></dc:creator>
		<pubDate>Sun, 23 Jul 2023 14:10:21 +0000</pubDate>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[linux]]></category>
		<category><![CDATA[linux commands]]></category>
		<guid isPermaLink="false">https://awjunaid.com/?p=3401</guid>

					<description><![CDATA[<p>I&#8217;m going to start this one with the most important sentence in the whole article: you should not&#8230;</p>
<p>The post <a href="https://awjunaid.com/linux/rlogin-rsh-and-rcp-commands-in-linux-and-it-perimeters/">rlogin, rsh, and rcp Commands in Linux: Complete Guide to Remote Access Tools and Parameters</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">I&#8217;m going to start this one with the most important sentence in the whole article: <strong>you should not be using rlogin, rsh, or rcp on any system connected to a network you care about.</strong> I still cover them here because they show up in legacy environments, certification material, and old scripts that occasionally land on someone&#8217;s desk with instructions to &#8220;just get it working.&#8221; Understanding what these tools do — and exactly why they were replaced — is genuinely useful context, even if the practical advice at the end of nearly every section is &#8220;use SSH instead.&#8221;</p>



<h2 class="wp-block-heading">What rlogin, rsh, and rcp Are</h2>



<p class="wp-block-paragraph">These three commands are part of the old BSD &#8220;r-commands&#8221; suite, designed in the early days of Unix networking for remote login and file transfer between trusted hosts:</p>



<ul class="wp-block-list">
<li><strong>rlogin</strong> — remote login, similar in purpose to <code>telnet</code> or <code>ssh</code>, opens an interactive remote shell session.</li>



<li><strong>rsh</strong> (remote shell) — executes a single command on a remote host without an interactive session.</li>



<li><strong>rcp</strong> (remote copy) — copies files between hosts, syntactically similar to the local <code>cp</code> command.</li>
</ul>



<p class="wp-block-paragraph">All three share the same fundamental design flaw: <strong>authentication and trust are based on hostname/IP and username, not cryptography</strong>, and all data — including any passwords that are exchanged — travels in <strong>plaintext</strong>.</p>



<h2 class="wp-block-heading">How the Trust Model Works (and Why It&#8217;s Dangerous)</h2>



<p class="wp-block-paragraph">The r-commands rely on two configuration files to decide who&#8217;s allowed in without a password prompt:</p>



<p class="wp-block-paragraph"><strong>System-wide</strong>: <code>/etc/hosts.equiv</code></p>



<pre class="wp-block-code"><code>trusted-host.example.com
+@trusted-netgroup
</code></pre>



<p class="wp-block-paragraph"><strong>Per-user</strong>: <code>~/.rhosts</code></p>



<pre class="wp-block-code"><code>trusted-host.example.com someuser
192.168.1.50 anotheruser
</code></pre>



<p class="wp-block-paragraph">If a connecting host/username pair matches an entry in either file, the daemon grants access <strong>without a password at all</strong>. This trust is based entirely on the claimed source IP and username in the connection request — both of which are trivially spoofable on any network where you don&#8217;t fully control routing, which is to say, nearly every real network.</p>



<p class="wp-block-paragraph">This is the core reason these tools are considered obsolete: IP spoofing, DNS spoofing, and simple network sniffing (since even password-based sessions send credentials in plaintext) can each independently compromise the entire trust model.</p>



<h2 class="wp-block-heading">rlogin Syntax and Usage</h2>



<pre class="wp-block-code"><code>rlogin &#91;-l username] &#91;-8] &#91;-E] &#91;-K] hostname
</code></pre>



<p class="wp-block-paragraph">Common parameters:</p>



<ul class="wp-block-list">
<li><code>-l username</code> — log in as a different remote username than your local one</li>



<li><code>-8</code> — allow 8-bit data (needed for non-ASCII characters)</li>



<li><code>-E</code> — disable escape character recognition</li>



<li><code>-K</code> — turn off Kerberos authentication (on Kerberized variants)</li>
</ul>



<p class="wp-block-paragraph">Example:</p>



<pre class="wp-block-code"><code>rlogin -l admin remotehost.example.com
</code></pre>



<p class="wp-block-paragraph">If a matching <code>.rhosts</code>/<code>hosts.equiv</code> entry exists, this connects directly with no password prompt at all. If not, it typically falls back to prompting for a password — which then travels across the network in plaintext.</p>



<h2 class="wp-block-heading">rsh Syntax and Usage</h2>



<pre class="wp-block-code"><code>rsh &#91;-l username] hostname command
</code></pre>



<p class="wp-block-paragraph">Example — run a single command remotely without an interactive shell:</p>



<pre class="wp-block-code"><code>rsh -l admin remotehost.example.com "uptime"
</code></pre>



<p class="wp-block-paragraph">Example — remote script execution in older automation setups:</p>



<pre class="wp-block-code"><code>rsh backup-server "tar czf /backup/daily.tar.gz /data"
</code></pre>



<p class="wp-block-paragraph">Because rsh has no interactive prompt for input in the typical case, it was historically popular in cron jobs and batch scripts on trusted internal networks — which is exactly the pattern SSH with key-based auth has cleanly replaced.</p>



<h2 class="wp-block-heading">rcp Syntax and Usage</h2>



<pre class="wp-block-code"><code>rcp &#91;-p] &#91;-r] source destination
</code></pre>



<ul class="wp-block-list">
<li><code>-p</code> — preserve modification times and modes from the source file</li>



<li><code>-r</code> — recursively copy directories</li>
</ul>



<p class="wp-block-paragraph">Copy a local file to a remote host:</p>



<pre class="wp-block-code"><code>rcp localfile.txt remotehost:/tmp/localfile.txt
</code></pre>



<p class="wp-block-paragraph">Copy a remote file to local:</p>



<pre class="wp-block-code"><code>rcp remotehost:/var/log/app.log ./app.log
</code></pre>



<p class="wp-block-paragraph">Copy a directory recursively:</p>



<pre class="wp-block-code"><code>rcp -r localdir/ remotehost:/tmp/localdir/
</code></pre>



<p class="wp-block-paragraph">Copy between two remote hosts (executed from a third machine, both remotes must trust each other):</p>



<pre class="wp-block-code"><code>rcp host1:/data/file.txt host2:/data/file.txt
</code></pre>



<h2 class="wp-block-heading">Installing the r-commands (If You Genuinely Need Them for Legacy Compatibility)</h2>



<p class="wp-block-paragraph">Debian/Ubuntu:</p>



<pre class="wp-block-code"><code>sudo apt install rsh-client rsh-redone-server
</code></pre>



<p class="wp-block-paragraph">RHEL/CentOS/Fedora:</p>



<pre class="wp-block-code"><code>sudo dnf install rsh rsh-server
</code></pre>



<p class="wp-block-paragraph">The server-side daemon (<code>rlogind</code>/<code>rshd</code>) is typically managed via xinetd or a standalone init script on older systems, since these predate systemd socket activation entirely.</p>



<h2 class="wp-block-heading">Why These Were Replaced: The Case for SSH</h2>



<p class="wp-block-paragraph">SSH (Secure Shell) was designed specifically to solve every structural problem in the r-commands:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Concern</th><th>r-commands</th><th>SSH</th></tr></thead><tbody><tr><td>Data encryption</td><td>None — plaintext</td><td>Full session encryption</td></tr><tr><td>Authentication</td><td>IP/hostname trust or plaintext password</td><td>Public-key cryptography, optionally combined with passwords/MFA</td></tr><tr><td>Host verification</td><td>None</td><td>Host key fingerprinting detects MITM/spoofing attempts</td></tr><tr><td>Port/service</td><td>Uses privileged ports (513, 514) with source-port trust checks</td><td>Standard port 22, no reliance on source port trust</td></tr><tr><td>File transfer</td><td>rcp (plaintext)</td><td>scp/sftp (encrypted)</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">The direct replacements, command for command:</p>



<pre class="wp-block-code"><code>rlogin host          -&gt;  ssh host
rsh host command     -&gt;  ssh host command
rcp file host:path   -&gt;  scp file host:path
</code></pre>



<p class="wp-block-paragraph">Functionally almost a drop-in replacement in terms of syntax, with the entire trust and encryption model rebuilt from the ground up.</p>



<h2 class="wp-block-heading">Example: Migrating an Old rsh-Based Script to SSH</h2>



<p class="wp-block-paragraph">Old:</p>



<pre class="wp-block-code"><code>#!/bin/bash
rsh backupserver "tar czf /backup/daily-$(date +%F).tar.gz /data"
rcp backupserver:/backup/daily-$(date +%F).tar.gz /local/backups/
</code></pre>



<p class="wp-block-paragraph">Migrated:</p>



<pre class="wp-block-code"><code>#!/bin/bash
ssh backupserver "tar czf /backup/daily-$(date +%F).tar.gz /data"
scp backupserver:/backup/daily-$(date +%F).tar.gz /local/backups/
</code></pre>



<p class="wp-block-paragraph">Combined with SSH key-based authentication (so the script still runs unattended, without a password prompt, but without the IP-spoofing risk of <code>.rhosts</code> trust), this is a strict security upgrade with essentially the same operational behavior.</p>



<h2 class="wp-block-heading">Setting Up Passwordless SSH as the Direct Replacement for .rhosts Trust</h2>



<pre class="wp-block-code"><code>ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N ""
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@remotehost
</code></pre>



<p class="wp-block-paragraph">Test:</p>



<pre class="wp-block-code"><code>ssh user@remotehost "uptime"
</code></pre>



<p class="wp-block-paragraph">This gives you the same &#8220;no password prompt for automation&#8221; convenience the r-commands offered, but backed by asymmetric cryptography rather than a spoofable IP address check.</p>



<h2 class="wp-block-heading">Security Implications and Why This Matters Beyond Nostalgia</h2>



<p class="wp-block-paragraph">If you find <code>rsh</code>, <code>rlogin</code>, or <code>rcp</code> (or their daemons <code>rshd</code>, <code>rlogind</code>) installed and enabled on a system you&#8217;re responsible for, that&#8217;s a finding worth acting on immediately, not a curiosity. The specific risks:</p>



<ul class="wp-block-list">
<li><strong>Credential and data interception</strong> — any plaintext traffic on a shared network segment (including many switched networks with ARP spoofing) can be captured by anyone with a packet sniffer.</li>



<li><strong>Host spoofing</strong> — an attacker who can spoof a trusted IP address (or compromise a machine already in a <code>.rhosts</code>/<code>hosts.equiv</code> trust relationship) inherits access with zero authentication.</li>



<li><strong>Lateral movement</strong> — trust relationships between servers via <code>.rhosts</code> are a classic pivot point; compromising one trusted host in a chain compromises everything downstream that trusts it.</li>



<li><strong>Compliance</strong> — virtually every modern security standard (PCI-DSS, CIS Benchmarks, DISA STIGs) explicitly flags the r-commands as prohibited services.</li>
</ul>



<h2 class="wp-block-heading">How the Original Trust Model Was Actually Enforced</h2>



<p class="wp-block-paragraph">It&#8217;s worth understanding the specific mechanism the r-commands relied on, because it explains exactly why the trust model is so fundamentally broken on any network you don&#8217;t fully control at the routing level.</p>



<p class="wp-block-paragraph">The r-command daemons (<code>rlogind</code>, <code>rshd</code>) checked two things about an incoming connection: the source IP address of the TCP connection, and that the connection originated from a <strong>privileged port</strong> (below 1024, historically only assignable by a root process on the sending machine). The theory was that only a trusted system administrator could bind a privileged source port, so a connection from a privileged port claiming to be a specific trusted host was assumed genuine.</p>



<p class="wp-block-paragraph">This assumption fails in multiple independent ways on modern networks:</p>



<ul class="wp-block-list">
<li><strong>IP spoofing</strong> — crafting packets with a forged source address defeats the IP-based check outright, particularly on networks without ingress filtering (BCP38) at the router level.</li>



<li><strong>Privileged port isn&#8217;t a meaningful barrier</strong> — any machine where an attacker has root (even temporarily, even on an unrelated compromised box) can bind low ports freely.</li>



<li><strong>DNS spoofing</strong> — if trust is configured by hostname rather than raw IP, poisoning DNS resolution can redirect trust to an attacker-controlled address.</li>



<li><strong>On-path attackers</strong> — anyone who can observe or inject packets on the network path (a compromised switch, a rogue Wi-Fi access point, a compromised router) can intercept plaintext credentials or hijack an already-authenticated session outright, since there&#8217;s no session encryption to prevent it.</li>
</ul>



<p class="wp-block-paragraph">None of these are exotic, advanced attacks — IP spoofing and ARP-based on-path attacks in particular have been well-understood, tooled, and automated for decades, which is precisely why any protocol relying purely on source-address trust has been considered unsuitable for real-world networks since at least the 1990s.</p>



<h2 class="wp-block-heading">A Closer Look at .rhosts and hosts.equiv Syntax</h2>



<p class="wp-block-paragraph">Since these files are the actual attack surface if r-commands are ever found on a system you&#8217;re auditing, it&#8217;s worth knowing their full syntax rather than just the basic examples.</p>



<p class="wp-block-paragraph"><code>/etc/hosts.equiv</code> (system-wide, applies to any user unless a <code>.rhosts</code> file overrides it):</p>



<pre class="wp-block-code"><code>trusted-host.example.com
+trusted-host2.example.com  otheruser
-untrusted-host.example.com
+@some-netgroup
</code></pre>



<ul class="wp-block-list">
<li>A bare hostname trusts that host for <strong>any</strong> locally matching username.</li>



<li><code>+hostname username</code> trusts that host only for connections claiming that specific remote username.</li>



<li>A leading <code>-</code> explicitly denies, useful for carving out an exception within a broader trusted range.</li>



<li><code>+@netgroup</code> references an NIS netgroup — a now-rare but historically common way of trusting an entire group of machines at once.</li>



<li>A bare <code>+</code> on its own line (not shown above, but worth flagging explicitly) means &#8220;trust literally any host&#8221; — this configuration has appeared in real historical security incidents and should be treated as a critical finding if ever discovered.</li>
</ul>



<p class="wp-block-paragraph"><code>~/.rhosts</code> (per-user, same syntax, layered on top of the system-wide file):</p>



<pre class="wp-block-code"><code>192.168.1.50 alice
+trusted-workstation.example.com
</code></pre>



<p class="wp-block-paragraph">The critical detail auditors look for: a <code>.rhosts</code> file is honored <strong>regardless of the system-wide hosts.equiv policy</strong>, meaning an individual user can grant passwordless access to their own account even on a system where the administrator never intended <code>hosts.equiv</code> to allow it at all. This is exactly why historical security guidance for these tools, where they had to remain in use at all, insisted on actively searching for and removing stray <code>.rhosts</code> files across every user&#8217;s home directory as routine practice.</p>



<h2 class="wp-block-heading">Kerberized r-commands: The Historical Middle Ground</h2>



<p class="wp-block-paragraph">Before SSH became the universal replacement, some environments used <strong>Kerberized</strong> versions of these tools (<code>krlogin</code>, <code>krsh</code>, <code>krcp</code>) as a partial fix — replacing the IP-based trust model with genuine Kerberos ticket-based authentication, while still using the same unencrypted data channel for the actual session content.</p>



<pre class="wp-block-code"><code>krlogin -x remotehost   # -x requests encryption of the session, where supported
</code></pre>



<p class="wp-block-paragraph">This closed the authentication gap but didn&#8217;t fully solve the confidentiality problem unless encryption was explicitly requested and supported by both ends — and even then, it required an existing, properly maintained Kerberos realm, which was a significant infrastructure investment relative to SSH&#8217;s comparatively simple key-based model. Kerberized r-commands still show up in some legacy enterprise Unix environments with existing Kerberos infrastructure (often alongside NFS), but they never saw anywhere near SSH&#8217;s adoption.</p>



<h2 class="wp-block-heading">Historical Context: Why This Matters Beyond the Tools Themselves</h2>



<p class="wp-block-paragraph">The story of the r-commands is genuinely useful context for understanding modern protocol design, because SSH&#8217;s design is almost a direct point-by-point response to their specific failures:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>r-command weakness</th><th>SSH&#8217;s specific answer</th></tr></thead><tbody><tr><td>Trust based on spoofable IP/hostname</td><td>Trust based on cryptographic key possession</td></tr><tr><td>No way to detect a spoofed/impersonated server</td><td>Host key fingerprinting, checked and cached on first connection</td></tr><tr><td>Entire session sent in plaintext</td><td>Full session encryption negotiated via key exchange</td></tr><tr><td>No mechanism to require strong per-user secrets</td><td>Public-key auth, allowing arbitrarily strong keys with no shared-secret transmission at all</td></tr><tr><td>Privileged source port as a weak proxy for trust</td><td>No reliance on source port trust whatsoever</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Every time I explain SSH&#8217;s host-key-checking prompt to someone new to it — &#8220;the authenticity of host X can&#8217;t be established, are you sure you want to continue?&#8221; — I find it lands better when they understand it&#8217;s specifically closing a hole that real, deployed protocols (these exact ones) left wide open for years.</p>



<h2 class="wp-block-heading">Auditing a System for r-command Exposure</h2>



<pre class="wp-block-code"><code># Check if the daemons are installed
rpm -q rsh-server 2&gt;/dev/null   # RHEL
dpkg -l | grep rsh              # Debian

# Check if anything is listening on the classic r-command ports
sudo ss -tulnp | grep -E ':513|:514|:512'

# Look for trust files that grant passwordless access
find / -name ".rhosts" 2&gt;/dev/null
cat /etc/hosts.equiv 2&gt;/dev/null
</code></pre>



<p class="wp-block-paragraph">If you find any of this active, the remediation is straightforward: disable and remove the services, delete the trust files, and migrate whatever scripts depended on them to SSH.</p>



<pre class="wp-block-code"><code>sudo systemctl disable --now rsh 2&gt;/dev/null
sudo apt remove --purge rsh-server rsh-client 2&gt;/dev/null   # Debian
sudo dnf remove rsh-server rsh 2&gt;/dev/null                    # RHEL
</code></pre>



<h2 class="wp-block-heading">Summary</h2>



<p class="wp-block-paragraph">rlogin, rsh, and rcp represent an era of Unix networking that assumed trusted, physically controlled LANs — an assumption that hasn&#8217;t held for decades. They&#8217;re worth understanding for what they reveal about the evolution of remote access security (particularly how much of modern SSH design is a direct response to their specific weaknesses), but they have no legitimate place on a production system today. If you inherit a system using them, the fix is the same in every case: migrate to SSH, scp, or sftp, and remove the r-command daemons and trust files entirely.</p>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li><a href="https://linux.die.net/man/1/rlogin">man rlogin(1)</a></li>



<li><a href="https://linux.die.net/man/1/rsh">man rsh(1)</a></li>



<li><a href="https://linux.die.net/man/1/rcp">man rcp(1)</a></li>



<li><a href="https://www.cisecurity.org/cis-benchmarks/">CIS Benchmarks</a></li>



<li><a href="https://www.openssh.com/manual.html">OpenSSH Project documentation</a></li>
</ul>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://awjunaid.com/linux/rlogin-rsh-and-rcp-commands-in-linux-and-it-perimeters/">rlogin, rsh, and rcp Commands in Linux: Complete Guide to Remote Access Tools and Parameters</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://awjunaid.com/linux/rlogin-rsh-and-rcp-commands-in-linux-and-it-perimeters/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">3401</post-id>	</item>
		<item>
		<title>How to Use Secure Shell (SSH) for Remote Logins in Linux: Complete Connection and Security Guide</title>
		<link>https://awjunaid.com/linux/how-to-use-secure-shell-for-remote-logins-in-linux/</link>
					<comments>https://awjunaid.com/linux/how-to-use-secure-shell-for-remote-logins-in-linux/?noamp=mobile#respond</comments>
		
		<dc:creator><![CDATA[Abdul Wahab Junaid]]></dc:creator>
		<pubDate>Sun, 23 Jul 2023 14:03:59 +0000</pubDate>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[linux]]></category>
		<guid isPermaLink="false">https://awjunaid.com/?p=3398</guid>

					<description><![CDATA[<p>SSH is probably the single Linux tool I&#8217;ve used more than any other, full stop. It&#8217;s how I&#8230;</p>
<p>The post <a href="https://awjunaid.com/linux/how-to-use-secure-shell-for-remote-logins-in-linux/">How to Use Secure Shell (SSH) for Remote Logins in Linux: Complete Connection and Security Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">SSH is probably the single Linux tool I&#8217;ve used more than any other, full stop. It&#8217;s how I manage every remote server I touch, how I move files, how I tunnel traffic through restrictive networks, and how I make automation scripts run unattended without ever storing a plaintext password anywhere. This guide covers SSH from a first connection through key-based auth, hardening the daemon, and the productivity features (tunneling, agent forwarding, config files) that make it genuinely pleasant to work with once you know them.</p>



<h2 class="wp-block-heading">What SSH Actually Does</h2>



<p class="wp-block-paragraph">SSH (Secure Shell) provides an encrypted channel between a client and a server for remote command execution, file transfer, and port forwarding. Under the hood, an SSH session goes through:</p>



<ol class="wp-block-list">
<li><strong>TCP connection</strong> to the server, typically port 22.</li>



<li><strong>Key exchange</strong> — client and server negotiate a shared session key using asymmetric cryptography (commonly Diffie-Hellman or elliptic-curve variants), establishing a symmetric encryption key for the actual session without ever transmitting it.</li>



<li><strong>Host verification</strong> — the client checks the server&#8217;s host key against its known_hosts file, protecting against man-in-the-middle attacks.</li>



<li><strong>Authentication</strong> — password, public key, keyboard-interactive (2FA), or GSSAPI/Kerberos.</li>



<li><strong>Session</strong> — once authenticated, all further traffic (interactive shell, file transfer, port forwarding) is encrypted inside this single channel.</li>
</ol>



<h2 class="wp-block-heading">Basic Connection</h2>



<pre class="wp-block-code"><code>ssh user@remotehost
ssh user@192.168.1.50
ssh -p 2222 user@remotehost     # non-standard port
</code></pre>



<p class="wp-block-paragraph">First connection to a new host:</p>



<pre class="wp-block-code"><code>The authenticity of host 'remotehost (192.168.1.50)' can't be established.
ED25519 key fingerprint is SHA256:abcd1234...
Are you sure you want to continue connecting (yes/no/&#91;fingerprint])?
</code></pre>



<p class="wp-block-paragraph">This fingerprint check matters — in a genuinely secure workflow, you&#8217;d verify this fingerprint against one provided by the server administrator through a separate channel before typing &#8220;yes,&#8221; rather than accepting blindly.</p>



<h2 class="wp-block-heading">Key-Based Authentication (What You Should Actually Be Using)</h2>



<p class="wp-block-paragraph">Password authentication over SSH is encrypted, but it&#8217;s still vulnerable to brute-force attempts and doesn&#8217;t scale well for automation. Key-based authentication is both more secure and more convenient.</p>



<h3 class="wp-block-heading">Generate a Key Pair</h3>



<pre class="wp-block-code"><code>ssh-keygen -t ed25519 -C "your_email@example.com"
</code></pre>



<ul class="wp-block-list">
<li><code>-t ed25519</code> — modern, fast, and secure key type (preferred over the older <code>rsa</code> type for new keys)</li>



<li><code>-C</code> — a comment, usually your email, to help identify the key later</li>
</ul>



<p class="wp-block-paragraph">For environments requiring RSA specifically (some older systems, some compliance requirements):</p>



<pre class="wp-block-code"><code>ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
</code></pre>



<p class="wp-block-paragraph">You&#8217;ll be prompted for a passphrase — use one. An unencrypted private key sitting on disk is a single-file compromise away from full account takeover; a passphrase-protected key at least requires an attacker to also crack the passphrase.</p>



<h3 class="wp-block-heading">Copy the Public Key to the Server</h3>



<pre class="wp-block-code"><code>ssh-copy-id user@remotehost
</code></pre>



<p class="wp-block-paragraph">Or manually, if <code>ssh-copy-id</code> isn&#8217;t available:</p>



<pre class="wp-block-code"><code>cat ~/.ssh/id_ed25519.pub | ssh user@remotehost "mkdir -p ~/.ssh &amp;&amp; chmod 700 ~/.ssh &amp;&amp; cat &gt;&gt; ~/.ssh/authorized_keys &amp;&amp; chmod 600 ~/.ssh/authorized_keys"
</code></pre>



<h3 class="wp-block-heading">Test It</h3>



<pre class="wp-block-code"><code>ssh user@remotehost
</code></pre>



<p class="wp-block-paragraph">You should be prompted for your key&#8217;s passphrase (if set) instead of the account&#8217;s server-side password — and if you&#8217;re using <code>ssh-agent</code> (below), not even that.</p>



<h2 class="wp-block-heading">ssh-agent: Avoid Retyping Your Passphrase</h2>



<pre class="wp-block-code"><code>eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
</code></pre>



<p class="wp-block-paragraph">Now your passphrase is cached in memory for the duration of the agent&#8217;s life (or until you explicitly remove it), and every SSH connection using that key authenticates silently.</p>



<p class="wp-block-paragraph">List currently loaded keys:</p>



<pre class="wp-block-code"><code>ssh-add -l
</code></pre>



<p class="wp-block-paragraph">Remove all cached keys:</p>



<pre class="wp-block-code"><code>ssh-add -D
</code></pre>



<h2 class="wp-block-heading">The SSH Config File: Making Connections Effortless</h2>



<p class="wp-block-paragraph"><code>~/.ssh/config</code> lets you define shortcuts and per-host settings instead of typing full connection strings every time:</p>



<pre class="wp-block-code"><code>Host prodweb
    HostName 203.0.113.10
    User deploy
    Port 2222
    IdentityFile ~/.ssh/id_ed25519_prod

Host *.internal.example.com
    User admin
    ProxyJump bastion.example.com

Host bastion
    HostName bastion.example.com
    User jump-user
    IdentityFile ~/.ssh/id_ed25519_bastion
</code></pre>



<p class="wp-block-paragraph">Now instead of <code>ssh -p 2222 -i ~/.ssh/id_ed25519_prod deploy@203.0.113.10</code>, you just run:</p>



<pre class="wp-block-code"><code>ssh prodweb
</code></pre>



<p class="wp-block-paragraph">The <code>ProxyJump</code> directive is genuinely one of my favorite SSH features — it transparently routes your connection through a bastion/jump host, which is an extremely common production pattern for reaching private-network servers without exposing them directly to the internet.</p>



<h2 class="wp-block-heading">Copying Files: scp and sftp</h2>



<pre class="wp-block-code"><code>scp localfile.txt user@remotehost:/tmp/
scp user@remotehost:/var/log/app.log ./
scp -r localdir/ user@remotehost:/tmp/localdir/
</code></pre>



<p class="wp-block-paragraph">Interactive file transfer session:</p>



<pre class="wp-block-code"><code>sftp user@remotehost
sftp&gt; put localfile.txt
sftp&gt; get remotefile.txt
sftp&gt; ls
sftp&gt; cd /var/log
</code></pre>



<p class="wp-block-paragraph">For serious file syncing, <code>rsync</code> over SSH is generally the better tool — it only transfers changed data:</p>



<pre class="wp-block-code"><code>rsync -avz -e ssh /local/dir/ user@remotehost:/remote/dir/
</code></pre>



<h2 class="wp-block-heading">Port Forwarding (Tunneling)</h2>



<h3 class="wp-block-heading">Local Forwarding — Reach a Remote-Only Service Through Your Local Machine</h3>



<pre class="wp-block-code"><code>ssh -L 8080:localhost:80 user@remotehost
</code></pre>



<p class="wp-block-paragraph">Now <code>http://localhost:8080</code> on your machine reaches port 80 on the remote host — useful for reaching an internal admin panel that isn&#8217;t directly exposed.</p>



<h3 class="wp-block-heading">Remote Forwarding — Expose a Local Service to the Remote Side</h3>



<pre class="wp-block-code"><code>ssh -R 9000:localhost:3000 user@remotehost
</code></pre>



<p class="wp-block-paragraph">Now connections to port 9000 on the remote host reach port 3000 on your local machine — useful for letting a remote server temporarily reach a service running on your laptop during development.</p>



<h3 class="wp-block-heading">Dynamic Forwarding — a SOCKS Proxy Through SSH</h3>



<pre class="wp-block-code"><code>ssh -D 1080 user@remotehost
</code></pre>



<p class="wp-block-paragraph">Configure your browser or another application to use <code>localhost:1080</code> as a SOCKS5 proxy, routing that traffic through the encrypted SSH tunnel — a lightweight, no-additional-software VPN alternative for browsing through a trusted remote network.</p>



<h2 class="wp-block-heading">Running Commands Without an Interactive Session</h2>



<pre class="wp-block-code"><code>ssh user@remotehost "df -h"
ssh user@remotehost "systemctl status nginx"
</code></pre>



<p class="wp-block-paragraph">Useful in scripts, combined with key-based auth for fully unattended automation:</p>



<pre class="wp-block-code"><code>#!/bin/bash
ssh backup-server "tar czf /backup/daily-$(date +%F).tar.gz /data"
scp backup-server:/backup/daily-$(date +%F).tar.gz /local/backups/
</code></pre>



<h2 class="wp-block-heading">Hardening the SSH Server (sshd_config)</h2>



<p class="wp-block-paragraph">Configuration lives at <code>/etc/ssh/sshd_config</code>. Key settings I check on every server I stand up:</p>



<pre class="wp-block-code"><code># Disable root login entirely — use sudo after logging in as a normal user
PermitRootLogin no

# Disable password auth once keys are set up and confirmed working
PasswordAuthentication no

# Only allow protocol version 2 (version 1 is ancient and broken; modern sshd doesn't even offer it, but explicit is good)
Protocol 2

# Limit which users/groups can SSH in at all
AllowUsers deploy admin

# Change the default port (mild obscurity benefit — reduces automated scan noise, not a real security boundary on its own)
Port 2222

# Disable empty passwords
PermitEmptyPasswords no

# Limit authentication attempts per connection
MaxAuthTries 3

# Set an idle timeout
ClientAliveInterval 300
ClientAliveCountMax 2
</code></pre>



<p class="wp-block-paragraph">Apply changes:</p>



<pre class="wp-block-code"><code>sudo sshd -t                     # test config syntax before restarting — always do this
sudo systemctl restart sshd
</code></pre>



<p class="wp-block-paragraph"><strong>Test the new configuration from a second, separate SSH session before closing your current one.</strong> This is the single most important operational habit for SSH hardening — if <code>PasswordAuthentication no</code> gets applied before you&#8217;ve confirmed key-based auth actually works, and your current session drops, you may be locked out entirely without console access.</p>



<h2 class="wp-block-heading">Multi-Factor Authentication with SSH</h2>



<p class="wp-block-paragraph">For an additional layer beyond key-based auth, PAM-based TOTP (via Google Authenticator PAM module) is a common addition:</p>



<pre class="wp-block-code"><code>sudo apt install libpam-google-authenticator    # Debian/Ubuntu
sudo dnf install google-authenticator           # RHEL/Fedora

google-authenticator
</code></pre>



<p class="wp-block-paragraph">Then in <code>/etc/pam.d/sshd</code>, add:</p>



<pre class="wp-block-code"><code>auth required pam_google_authenticator.so
</code></pre>



<p class="wp-block-paragraph">And in <code>sshd_config</code>:</p>



<pre class="wp-block-code"><code>ChallengeResponseAuthentication yes
AuthenticationMethods publickey,keyboard-interactive
</code></pre>



<p class="wp-block-paragraph">This requires <strong>both</strong> a valid SSH key <strong>and</strong> a TOTP code, combining something-you-have with something-you-know.</p>



<h2 class="wp-block-heading">A Deeper Look at the SSH Protocol Handshake</h2>



<p class="wp-block-paragraph">Understanding the sequence a bit more precisely helps when debugging connection issues with <code>-vvv</code>, since the verbose output maps directly onto these stages:</p>



<ol class="wp-block-list">
<li><strong>Version exchange</strong> — client and server each announce their SSH protocol version and software identifier.</li>



<li><strong>Algorithm negotiation</strong> — both sides propose supported key exchange algorithms, ciphers, MACs, and compression methods; they agree on the strongest mutually-supported option.</li>



<li><strong>Key exchange (KEX)</strong> — using algorithms like <code>curve25519-sha256</code> (the modern default), client and server derive a shared session key without ever transmitting it directly, even over an otherwise-observed connection.</li>



<li><strong>Server host key verification</strong> — the client checks the server&#8217;s public host key against <code>~/.ssh/known_hosts</code>; a mismatch from a previously-seen host produces the well-known (and important) warning about a potential man-in-the-middle attack.</li>



<li><strong>User authentication</strong> — publickey, password, keyboard-interactive, or GSSAPI, attempted in an order controlled by both client preference and server&#8217;s <code>AuthenticationMethods</code>/<code>PubkeyAuthentication</code> settings.</li>



<li><strong>Channel/session establishment</strong> — once authenticated, a logical channel is opened inside the encrypted connection for the shell, command execution, port forward, or SFTP subsystem.</li>
</ol>



<pre class="wp-block-code"><code>ssh -vvv user@remotehost 2&gt;&amp;1 | grep -E "kex|Server host key|Authentication"
</code></pre>



<p class="wp-block-paragraph">This kind of targeted grep against verbose output is genuinely useful for isolating exactly which stage a connection is failing at, rather than scrolling through the full firehose of debug output.</p>



<h2 class="wp-block-heading">Understanding known_hosts and Host Key Rotation</h2>



<pre class="wp-block-code"><code>cat ~/.ssh/known_hosts | head -3
</code></pre>



<p class="wp-block-paragraph">Each line records a hostname (or hashed hostname, if <code>HashKnownHosts yes</code> is set), the key type, and the base64-encoded public host key. When a server&#8217;s host key legitimately changes — a reinstall, a migration to new hardware, an intentional key rotation — every client that&#8217;s previously connected will see the &#8220;REMOTE HOST IDENTIFICATION HAS CHANGED&#8221; warning and refuse to connect until the stale entry is removed.</p>



<pre class="wp-block-code"><code>ssh-keygen -R oldhostname   # remove a specific stale entry safely
</code></pre>



<p class="wp-block-paragraph">Modern OpenSSH also supports host key rotation notifications via the <code>UpdateHostKeys</code> client option, letting a server proactively inform already-connected clients of additional or replacement host keys through an authenticated in-band mechanism — a meaningful improvement over the old &#8220;figure out if the change is legitimate by out-of-band means&#8221; workflow, though it still requires the <em>first</em> connection to have been trustworthy.</p>



<h2 class="wp-block-heading">Certificate-Based SSH Authentication (Beyond Individual Keys)</h2>



<p class="wp-block-paragraph">For anything beyond a handful of servers, distributing individual public keys to every host and managing revocation by hand doesn&#8217;t scale. OpenSSH supports a certificate authority model, where a central CA key signs both user keys and host keys, and individual servers/clients simply trust the CA rather than needing every individual key added manually.</p>



<p class="wp-block-paragraph">Generate a CA key (done once, kept extremely secure):</p>



<pre class="wp-block-code"><code>ssh-keygen -t ed25519 -f ssh_ca -C "internal SSH CA"
</code></pre>



<p class="wp-block-paragraph">Sign a user&#8217;s public key, producing a short-lived certificate:</p>



<pre class="wp-block-code"><code>ssh-keygen -s ssh_ca -I "alice-cert" -n alice -V +8h -z 1 alice_key.pub
</code></pre>



<ul class="wp-block-list">
<li><code>-V +8h</code> — certificate expires 8 hours from signing, a genuinely powerful control that individual static keys don&#8217;t offer.</li>



<li><code>-n alice</code> — restricts the certificate to authenticating as the <code>alice</code> principal.</li>



<li><code>-z 1</code> — a serial number, used for targeted revocation later.</li>
</ul>



<p class="wp-block-paragraph">On the server side, configure <code>sshd_config</code> to trust the CA:</p>



<pre class="wp-block-code"><code>TrustedUserCAKeys /etc/ssh/ssh_ca.pub
</code></pre>



<p class="wp-block-paragraph">This is the pattern behind most modern &#8220;ephemeral SSH access&#8221; tooling in larger organizations — rather than managing hundreds of individual authorized_keys entries across a fleet, servers trust a CA, and short-lived certificates are issued per-session, often gated behind an additional approval or SSO step.</p>



<h2 class="wp-block-heading">SSH Multiplexing for Faster Repeated Connections</h2>



<p class="wp-block-paragraph">If you connect to the same host repeatedly in a short span (common during active administration or scripting), SSH&#8217;s connection multiplexing avoids repeating the full handshake for every single connection:</p>



<pre class="wp-block-code"><code>Host prodweb
    ControlMaster auto
    ControlPath ~/.ssh/sockets/%r@%h-%p
    ControlPersist 10m
</code></pre>



<pre class="wp-block-code"><code>mkdir -p ~/.ssh/sockets
</code></pre>



<p class="wp-block-paragraph">The first connection to <code>prodweb</code> establishes the underlying TCP/SSH session normally; every subsequent connection within the <code>ControlPersist</code> window reuses that same already-authenticated connection as a transport, making each additional <code>ssh</code>/<code>scp</code> call to the same host nearly instantaneous.</p>



<h2 class="wp-block-heading">Comparing SSH to Other Remote Access Approaches</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Approach</th><th>Encryption</th><th>Auth model</th><th>Typical use case</th></tr></thead><tbody><tr><td>SSH</td><td>Full session</td><td>Keys, passwords, certificates, MFA</td><td>General-purpose remote administration</td></tr><tr><td>Telnet</td><td>None</td><td>Plaintext password</td><td>Legacy device management only, avoid otherwise</td></tr><tr><td>RDP</td><td>Full session (modern versions)</td><td>Password, certificate, NLA</td><td>Windows GUI remote access</td></tr><tr><td>VNC</td><td>Varies by implementation, often weak by default</td><td>Password, sometimes weak</td><td>GUI remote access, often needs tunneling through SSH for real security</td></tr><tr><td>Mosh</td><td>Full session (built on SSH for initial auth)</td><td>Delegates to SSH for authentication</td><td>Unstable/high-latency connections, roaming between networks</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Mosh in particular is worth knowing about if you frequently work over unreliable connections — it authenticates via a normal SSH handshake, then hands off to its own UDP-based, session-persistent protocol that survives IP changes and network interruptions far more gracefully than a raw SSH TCP session does.</p>



<h2 class="wp-block-heading">Auditing Who Has SSH Access to a Server</h2>



<pre class="wp-block-code"><code># List all authorized keys across every user's home directory
sudo find /home /root -name "authorized_keys" -exec echo {} \; -exec cat {} \;

# Check sshd's actual effective configuration (merges config file with compiled defaults)
sudo sshd -T | grep -E "permitrootlogin|passwordauthentication|allowusers"
</code></pre>



<p class="wp-block-paragraph">Running <code>sshd -T</code> (test/dump effective config) rather than just reading <code>sshd_config</code> directly is worth doing periodically — it shows exactly what the daemon is actually enforcing, including any defaults not explicitly set in the file, which can differ from what a quick read of the config alone would suggest.</p>



<h2 class="wp-block-heading">Troubleshooting</h2>



<p class="wp-block-paragraph"><strong>Connection refused</strong> — sshd isn&#8217;t running or isn&#8217;t listening on the expected port:</p>



<pre class="wp-block-code"><code>sudo systemctl status sshd
sudo ss -tulnp | grep ssh
</code></pre>



<p class="wp-block-paragraph"><strong>Permission denied (publickey)</strong> — verify permissions on the server side; SSH is strict about this and silently refuses keys with overly permissive file modes:</p>



<pre class="wp-block-code"><code>chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
</code></pre>



<p class="wp-block-paragraph"><strong>Host key verification failed</strong> — the server&#8217;s host key changed (legitimate reinstall, or a potential MITM attack). Investigate before blindly removing the old entry:</p>



<pre class="wp-block-code"><code>ssh-keygen -R remotehost   # only after confirming the change is legitimate
</code></pre>



<p class="wp-block-paragraph"><strong>Verbose debugging</strong> for any connection issue:</p>



<pre class="wp-block-code"><code>ssh -vvv user@remotehost
</code></pre>



<h2 class="wp-block-heading">Best Practices Summary</h2>



<ul class="wp-block-list">
<li>Use <code>ed25519</code> keys with a passphrase, protected by <code>ssh-agent</code> for convenience.</li>



<li>Disable password authentication once keys are confirmed working.</li>



<li>Disable direct root login; use <code>sudo</code> from a normal account instead.</li>



<li>Use <code>~/.ssh/config</code> and <code>ProxyJump</code> instead of manually chaining connections.</li>



<li>Test every <code>sshd_config</code> change with <code>sshd -t</code> and from a second session before closing your current one.</li>



<li>Consider MFA for anything internet-facing.</li>
</ul>



<h2 class="wp-block-heading">Summary</h2>



<p class="wp-block-paragraph">SSH is the backbone of secure remote Linux administration: encrypted transport, cryptographic host verification, and flexible authentication that scales from a single interactive login to fully unattended automation. The habits that matter most are the boring ones — key-based auth over passwords, testing config changes before you commit to them, and never closing your only working session until you&#8217;ve verified the new one works.</p>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li><a href="https://www.openssh.com/">OpenSSH Project</a></li>



<li><a href="https://man.openbsd.org/sshd_config">man sshd_config(5)</a></li>



<li><a href="https://man.openbsd.org/ssh">man ssh(1)</a></li>



<li><a href="https://access.redhat.com/documentation/">Red Hat Documentation – Using secure communications between two systems with OpenSSH</a></li>



<li><a href="https://ubuntu.com/server/docs/openssh-server">Ubuntu Server Guide – OpenSSH</a></li>
</ul>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://awjunaid.com/linux/how-to-use-secure-shell-for-remote-logins-in-linux/">How to Use Secure Shell (SSH) for Remote Logins in Linux: Complete Connection and Security Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://awjunaid.com/linux/how-to-use-secure-shell-for-remote-logins-in-linux/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">3398</post-id>	</item>
		<item>
		<title>How to Configure TCP Wrapper Security in Linux: Complete Host-Based Access Control Guide</title>
		<link>https://awjunaid.com/linux/how-to-configure-tcp-wrapper-security-in-linux/</link>
					<comments>https://awjunaid.com/linux/how-to-configure-tcp-wrapper-security-in-linux/?noamp=mobile#respond</comments>
		
		<dc:creator><![CDATA[Abdul Wahab Junaid]]></dc:creator>
		<pubDate>Sun, 23 Jul 2023 13:59:41 +0000</pubDate>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[linux]]></category>
		<guid isPermaLink="false">https://awjunaid.com/?p=3395</guid>

					<description><![CDATA[<p>TCP Wrappers were the first host-based access control mechanism I learned on Linux, years before I properly understood&#8230;</p>
<p>The post <a href="https://awjunaid.com/linux/how-to-configure-tcp-wrapper-security-in-linux/">How to Configure TCP Wrapper Security in Linux: Complete Host-Based Access Control Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">TCP Wrappers were the first host-based access control mechanism I learned on Linux, years before I properly understood iptables. They&#8217;re largely a legacy technology on modern distros now — glibc dropped built-in support for them years ago, and most contemporary services don&#8217;t link against <code>libwrap</code> at all — but they still show up in older systems, some third-party daemons, and a lot of certification and textbook material. Understanding them is useful both for maintaining legacy infrastructure and for appreciating why iptables/firewalld/nftables became the standard instead.</p>



<h2 class="wp-block-heading">What TCP Wrappers Actually Are</h2>



<p class="wp-block-paragraph">TCP Wrappers is a library (<code>libwrap</code>) and a pair of configuration files (<code>/etc/hosts.allow</code> and <code>/etc/hosts.deny</code>) that provide simple access control for network services, based on the connecting client&#8217;s hostname or IP address. A service that&#8217;s been &#8220;wrapped&#8221; — either compiled against <code>libwrap</code> directly, or launched through the <code>tcpd</code> wrapper binary via inetd/xinetd — checks these two files before deciding whether to accept a connection.</p>



<p class="wp-block-paragraph">This is fundamentally an <strong>application-layer</strong> access control, not a kernel-level one like netfilter/iptables. The service itself (or the wrapper daemon in front of it) consults the rules; the kernel&#8217;s network stack has no awareness of them at all.</p>



<h2 class="wp-block-heading">Checking Whether a Service Supports TCP Wrappers</h2>



<p class="wp-block-paragraph">Not every service uses <code>libwrap</code>. Check if a binary is linked against it:</p>



<pre class="wp-block-code"><code>ldd /usr/sbin/sshd | grep libwrap
</code></pre>



<p class="wp-block-paragraph">On modern OpenSSH builds, this typically returns nothing — recent OpenSSH versions dropped TCP Wrappers support in favor of relying on the OS firewall. Historically supported services include <code>sshd</code> (older builds), <code>vsftpd</code>, <code>xinetd</code>-launched services, and various daemons compiled with <code>--with-libwrap</code>.</p>



<p class="wp-block-paragraph">Check what package provides the library on your system:</p>



<pre class="wp-block-code"><code>dpkg -l | grep tcpd        # Debian/Ubuntu
rpm -q tcp_wrappers         # RHEL/CentOS (older releases)
</code></pre>



<p class="wp-block-paragraph">On many current RHEL/Fedora releases, <code>tcp_wrappers</code> is no longer packaged at all, reflecting its status as effectively deprecated technology.</p>



<h2 class="wp-block-heading">Configuration Files</h2>



<h3 class="wp-block-heading">/etc/hosts.allow</h3>



<p class="wp-block-paragraph">Lists rules that explicitly permit access. Checked <strong>first</strong>.</p>



<h3 class="wp-block-heading">/etc/hosts.deny</h3>



<p class="wp-block-paragraph">Lists rules that explicitly deny access. Checked <strong>second</strong>, only if nothing in <code>hosts.allow</code> already matched.</p>



<h3 class="wp-block-heading">The Decision Logic</h3>



<ol class="wp-block-list">
<li>If a connection matches a rule in <code>hosts.allow</code>, it&#8217;s <strong>allowed</strong>, and processing stops there.</li>



<li>If not, and it matches a rule in <code>hosts.deny</code>, it&#8217;s <strong>denied</strong>.</li>



<li>If it matches neither file, it&#8217;s <strong>allowed by default</strong> — an important and frequently misunderstood detail. TCP Wrappers is &#8220;default allow&#8221; unless you explicitly set up a deny-everything catch-all.</li>
</ol>



<h2 class="wp-block-heading">Basic Syntax</h2>



<pre class="wp-block-code"><code>service_list : client_list &#91;: option : option ...]
</code></pre>



<ul class="wp-block-list">
<li><code>service_list</code> — daemon name(s) as they identify themselves to <code>libwrap</code> (often the binary name, e.g., <code>sshd</code>, <code>vsftpd</code>), or <code>ALL</code>.</li>



<li><code>client_list</code> — hostnames, IP addresses, IP/netmask pairs, wildcards, or <code>ALL</code>.</li>
</ul>



<h2 class="wp-block-heading">Practical Examples</h2>



<h3 class="wp-block-heading">Allow SSH Only from a Specific Network</h3>



<p class="wp-block-paragraph"><code>/etc/hosts.allow</code>:</p>



<pre class="wp-block-code"><code>sshd : 192.168.1.0/255.255.255.0
sshd : 10.0.0.5
</code></pre>



<p class="wp-block-paragraph"><code>/etc/hosts.deny</code>:</p>



<pre class="wp-block-code"><code>sshd : ALL
</code></pre>



<p class="wp-block-paragraph">This allows SSH only from the <code>192.168.1.0/24</code> subnet and the single host <code>10.0.0.5</code>, denying everyone else.</p>



<h3 class="wp-block-heading">Deny Everything by Default, Allow Explicitly (Recommended Pattern)</h3>



<p class="wp-block-paragraph"><code>/etc/hosts.deny</code>:</p>



<pre class="wp-block-code"><code>ALL : ALL
</code></pre>



<p class="wp-block-paragraph"><code>/etc/hosts.allow</code>:</p>



<pre class="wp-block-code"><code>sshd : 192.168.1.0/24
vsftpd : 192.168.1.0/24, 10.0.0.10
ALL : 127.0.0.1
</code></pre>



<p class="wp-block-paragraph">This is the security-conscious default-deny approach: block everything, then poke specific holes for specific services and networks, mirroring the same philosophy used with iptables/firewalld default-deny policies.</p>



<h3 class="wp-block-heading">Allow by Hostname/Domain</h3>



<pre class="wp-block-code"><code>sshd : .example.com
</code></pre>



<p class="wp-block-paragraph">The leading dot means &#8220;any host in this domain&#8221; — resolved via reverse DNS, which is itself a weakness (DNS-based rules depend on DNS integrity and can be slow if reverse lookups aren&#8217;t fast).</p>



<h3 class="wp-block-heading">Using Wildcards</h3>



<pre class="wp-block-code"><code>ALL : LOCAL
sshd : ALL EXCEPT 203.0.113.0/24
</code></pre>



<p class="wp-block-paragraph"><code>LOCAL</code> matches any hostname without a dot (i.e., on the local domain). <code>EXCEPT</code> lets you carve out an exclusion within a broader match.</p>



<h3 class="wp-block-heading">Combining with Actions (spawn / banners)</h3>



<p class="wp-block-paragraph">TCP Wrappers supports basic logging and command execution via the <code>spawn</code> directive, historically used for simple intrusion notification:</p>



<pre class="wp-block-code"><code>sshd : ALL : spawn (/usr/bin/logger -t tcpwrapper "SSH connection attempt from %h") : allow
</code></pre>



<p class="wp-block-paragraph"><code>%h</code> expands to the client hostname. This is a genuinely dated pattern by modern logging standards (compare to structured logging via journald, or a proper IDS), but it illustrates how flexible — and how easy to misconfigure — the option syntax can get.</p>



<h2 class="wp-block-heading">Testing Your Configuration</h2>



<p class="wp-block-paragraph">The <code>tcpdmatch</code> utility simulates a connection against your current rules without actually needing a live client:</p>



<pre class="wp-block-code"><code>tcpdmatch sshd 192.168.1.50
</code></pre>



<p class="wp-block-paragraph">Example output:</p>



<pre class="wp-block-code"><code>client:   address  192.168.1.50
server:   process  sshd
matched:  /etc/hosts.allow line 3
access:   granted
</code></pre>



<p class="wp-block-paragraph">This is worth running after every change — it&#8217;s a fast, safe way to confirm your rule logic actually does what you think before a real connection attempt tests it for you.</p>



<h2 class="wp-block-heading">Real-World Use Case: xinetd-Launched Services</h2>



<p class="wp-block-paragraph">TCP Wrappers historically paired closely with <code>xinetd</code> (and its predecessor <code>inetd</code>), since many services launched on-demand through the super-server were compiled against <code>libwrap</code> or launched via the <code>tcpd</code> binary explicitly:</p>



<pre class="wp-block-code"><code>service telnet
{
    disable     = no
    socket_type = stream
    protocol    = tcp
    wait        = no
    user        = root
    server      = /usr/sbin/tcpd
    server_args = /usr/sbin/in.telnetd
}
</code></pre>



<p class="wp-block-paragraph">Here, <code>tcpd</code> is invoked first, consults <code>hosts.allow</code>/<code>hosts.deny</code>, and only then execs the real service (<code>in.telnetd</code>) if access is granted.</p>



<h2 class="wp-block-heading">Why TCP Wrappers Declined in Favor of Netfilter-Based Firewalls</h2>



<p class="wp-block-paragraph">A few structural reasons this technology has largely faded from active use:</p>



<ol class="wp-block-list">
<li><strong>Application-layer only</strong> — TCP Wrappers only protects services actually compiled against <code>libwrap</code> or launched through <code>tcpd</code>; it does nothing for services that bypass it, which is most modern software.</li>



<li><strong>No port-level granularity</strong> — rules match on service name and client address, not on the specific port/protocol combination the way iptables can.</li>



<li><strong>Default-allow by design</strong> — a genuinely dangerous default for a security control; forgetting a rule silently leaves access open, rather than silently blocking it.</li>



<li><strong>glibc dropped built-in support</strong> — modern glibc doesn&#8217;t include <code>libwrap</code> hooks by default, so fewer and fewer services support it at all.</li>



<li><strong>Kernel-level filtering is strictly more capable</strong> — iptables/nftables/firewalld operate before traffic even reaches the application, covering every service uniformly regardless of whether it was compiled with wrapper support.</li>
</ol>



<h2 class="wp-block-heading">Migrating TCP Wrapper Rules to iptables</h2>



<p class="wp-block-paragraph">If you&#8217;re modernizing a legacy configuration, the translation is generally straightforward. This TCP Wrappers rule:</p>



<pre class="wp-block-code"><code>sshd : 192.168.1.0/24
</code></pre>



<p class="wp-block-paragraph">Becomes this iptables equivalent:</p>



<pre class="wp-block-code"><code>sudo iptables -A INPUT -p tcp --dport 22 -s 192.168.1.0/24 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 22 -j DROP
</code></pre>



<p class="wp-block-paragraph">Or the firewalld rich-rule equivalent:</p>



<pre class="wp-block-code"><code>sudo firewall-cmd --permanent --zone=public --add-rich-rule='rule family="ipv4" source address="192.168.1.0/24" port protocol="tcp" port="22" accept'
sudo firewall-cmd --reload
</code></pre>



<p class="wp-block-paragraph">The kernel-level version has the advantage of covering the port explicitly and applying regardless of whether the target service was ever compiled with <code>libwrap</code> support.</p>



<h2 class="wp-block-heading">How libwrap Actually Intercepts Connections</h2>



<p class="wp-block-paragraph">It&#8217;s worth understanding the specific mechanism by which a service ends up consulting <code>hosts.allow</code>/<code>hosts.deny</code> at all, since &#8220;is this service even wrapped&#8221; is the very first question in any troubleshooting session.</p>



<p class="wp-block-paragraph">There are two structurally different ways a service ends up subject to TCP Wrappers:</p>



<ol class="wp-block-list">
<li><strong>Compiled-in support</strong> — the daemon&#8217;s own source code is linked against <code>libwrap</code> and explicitly calls its <code>hosts_ctl()</code> function as part of its own connection-acceptance logic. The daemon itself is aware of and cooperating with TCP Wrappers.</li>



<li><strong>External wrapping via tcpd</strong> — the daemon isn&#8217;t aware of TCP Wrappers at all; instead, a super-server (traditionally inetd, later xinetd) launches the generic <code>tcpd</code> binary first, which itself is linked against <code>libwrap</code>, checks the access rules, and only then <code>exec()</code>s the real service binary if access is granted.</li>
</ol>



<p class="wp-block-paragraph">The practical difference matters for troubleshooting: for the first case, the actual daemon&#8217;s own binary needs to show a <code>libwrap</code> dependency; for the second case, it&#8217;s <code>tcpd</code> (or the super-server&#8217;s config referencing <code>tcpd</code>) that matters, and the wrapped service&#8217;s own binary may show no <code>libwrap</code> linkage at all, since it was never compiled with awareness of the mechanism.</p>



<pre class="wp-block-code"><code># Case 1: check the daemon binary directly
ldd /usr/sbin/some-daemon | grep libwrap

# Case 2: check whether xinetd is launching it through tcpd
grep -r "tcpd" /etc/xinetd.d/
</code></pre>



<h2 class="wp-block-heading">The Full Option Syntax Beyond spawn</h2>



<p class="wp-block-paragraph">Beyond the <code>spawn</code> directive shown earlier, TCP Wrappers supports a small set of additional options worth knowing when reading an inherited configuration:</p>



<pre class="wp-block-code"><code>sshd : 192.168.1.0/24 : allow
sshd : ALL : deny
in.telnetd : ALL : twist /bin/echo "Access denied by policy."
vsftpd : .suspicious-domain.example : severity emerg
</code></pre>



<ul class="wp-block-list">
<li><code>allow</code>/<code>deny</code> — explicit terminal actions, useful when you want a single line to unambiguously decide the outcome rather than relying on which file (<code>hosts.allow</code> vs <code>hosts.deny</code>) the rule happens to live in.</li>



<li><code>twist</code> — replaces the requested service entirely with a different command (historically used to run a decoy/honeypot response instead of the real service for connections you want to actively mislead rather than just silently reject).</li>



<li><code>severity</code> — overrides the syslog severity level used for logging this particular match, useful for making certain rule matches stand out more prominently in log monitoring.</li>
</ul>



<h2 class="wp-block-heading">Comparing hosts.allow/hosts.deny Ordering With a Concrete Example</h2>



<p class="wp-block-paragraph">Because the &#8220;first file wins, then check the second&#8221; logic is easy to get backwards when reading it too quickly, here&#8217;s a fully worked example showing exactly how a specific connection gets evaluated.</p>



<p class="wp-block-paragraph"><code>/etc/hosts.allow</code>:</p>



<pre class="wp-block-code"><code>sshd : 192.168.1.100
</code></pre>



<p class="wp-block-paragraph"><code>/etc/hosts.deny</code>:</p>



<pre class="wp-block-code"><code>sshd : 192.168.1.0/24
</code></pre>



<p class="wp-block-paragraph">A connection from <code>192.168.1.100</code> (specifically):</p>



<ol class="wp-block-list">
<li>Check <code>hosts.allow</code> — matches <code>sshd : 192.168.1.100</code> — <strong>access granted</strong>, and evaluation stops entirely; <code>hosts.deny</code> is never consulted for this connection at all.</li>
</ol>



<p class="wp-block-paragraph">A connection from <code>192.168.1.50</code> (same subnet, different specific host):</p>



<ol class="wp-block-list">
<li>Check <code>hosts.allow</code> — no match (the allow rule was for one specific address, not the whole subnet).</li>



<li>Check <code>hosts.deny</code> — matches <code>sshd : 192.168.1.0/24</code> — <strong>access denied</strong>.</li>
</ol>



<p class="wp-block-paragraph">This ordering — always <code>hosts.allow</code> first, in full, before <code>hosts.deny</code> is even opened — is precisely why a single specific-host exception in <code>hosts.allow</code> can override an entire subnet block in <code>hosts.deny</code>, which is a genuinely useful pattern (allow one trusted host within an otherwise-blocked range) but also a common source of &#8220;why is this one host still getting through&#8221; confusion when reviewing an unfamiliar configuration.</p>



<h2 class="wp-block-heading">TCP Wrappers and IPv6</h2>



<p class="wp-block-paragraph">A specific, often-overlooked limitation: classic TCP Wrappers syntax and the underlying <code>libwrap</code> implementation were designed in an IPv4-only era. IPv6 address matching support varies significantly by implementation and distro vintage, and older deployments may not correctly evaluate IPv6 client addresses against rules written in IPv4 dotted-decimal or CIDR notation at all.</p>



<pre class="wp-block-code"><code>tcpdmatch sshd ::1
tcpdmatch sshd 2001:db8::1
</code></pre>



<p class="wp-block-paragraph">If you&#8217;re auditing or maintaining a system that still relies on TCP Wrappers and IPv6 is enabled on that host, explicitly verify IPv6 matching behaves as expected with <code>tcpdmatch</code> rather than assuming your IPv4-oriented rules automatically extend to IPv6 clients — a genuinely common gap that&#8217;s led to unintentional IPv6 exposure on hosts where the administrator believed the same access restrictions applied uniformly across both protocol families.</p>



<h2 class="wp-block-heading">Practical Migration Path for a Mixed Legacy Environment</h2>



<p class="wp-block-paragraph">If you&#8217;re maintaining a system where some services still use TCP Wrappers and you&#8217;re gradually modernizing, a reasonable interim approach is layering both mechanisms rather than doing a risky big-bang cutover:</p>



<pre class="wp-block-code"><code># Keep the existing TCP Wrappers rules as a documented, understood baseline
cat /etc/hosts.allow /etc/hosts.deny

# Add equivalent kernel-level rules as the actual enforcement layer
sudo iptables -A INPUT -p tcp --dport 22 -s 192.168.1.0/24 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 22 -j DROP

# Verify both layers agree before removing the TCP Wrappers rules entirely
tcpdmatch sshd 192.168.1.50
sudo iptables -L INPUT -n -v
</code></pre>



<p class="wp-block-paragraph">Running both simultaneously for a transition period, confirming they produce consistent decisions, then retiring the TCP Wrappers configuration once you&#8217;re confident the kernel-level rules fully cover the same access model, is a safer path than assuming a translation is correct without verification.</p>



<h2 class="wp-block-heading">Troubleshooting</h2>



<p class="wp-block-paragraph"><strong>Rule doesn&#8217;t seem to apply</strong> — confirm the service is actually linked against libwrap in the first place; if <code>ldd</code> shows no <code>libwrap</code> dependency, TCP Wrappers has no effect on that service regardless of what&#8217;s in the config files.</p>



<p class="wp-block-paragraph"><strong>Access granted when you expected denial</strong> — remember the default-allow behavior; check for a catch-all <code>ALL : ALL</code> in <code>hosts.deny</code>, since without one, anything not matched in either file is permitted.</p>



<p class="wp-block-paragraph"><strong>Hostname-based rules behaving inconsistently</strong> — reverse DNS lookups can fail, time out, or resolve differently than expected; prefer IP/netmask-based rules for anything security-critical rather than relying on DNS.</p>



<p class="wp-block-paragraph"><strong>Changes don&#8217;t take effect immediately</strong> — TCP Wrappers rules are read per-connection by the daemon (or by <code>tcpd</code>), so no service restart is typically required, but confirm with <code>tcpdmatch</code> rather than assuming.</p>



<h2 class="wp-block-heading">Summary</h2>



<p class="wp-block-paragraph">TCP Wrappers offers simple, application-layer host-based access control via <code>/etc/hosts.allow</code> and <code>/etc/hosts.deny</code>, checked in that order, with default-allow behavior for anything unmatched. It&#8217;s largely superseded by kernel-level packet filtering (iptables, nftables, firewalld) on modern systems, both because fewer services support <code>libwrap</code> at all and because kernel-level filtering is strictly more capable and applies uniformly. Worth knowing for legacy systems and historical context; not something to build new security architecture around today.</p>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li><a href="https://linux.die.net/man/5/hosts_access">man hosts_access(5)</a></li>



<li><a href="https://linux.die.net/man/5/hosts.allow">man hosts.allow(5) / hosts.deny(5)</a></li>



<li><a href="https://linux.die.net/man/8/tcpd">man tcpd(8)</a></li>



<li><a href="https://access.redhat.com/documentation/">Red Hat Documentation – TCP Wrappers (legacy)</a></li>
</ul>
<p>The post <a href="https://awjunaid.com/linux/how-to-configure-tcp-wrapper-security-in-linux/">How to Configure TCP Wrapper Security in Linux: Complete Host-Based Access Control Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://awjunaid.com/linux/how-to-configure-tcp-wrapper-security-in-linux/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">3395</post-id>	</item>
		<item>
		<title>How to Configure Internet Super Server (xinetd) in Linux: Complete Service Management Guide</title>
		<link>https://awjunaid.com/linux/how-to-configure-internet-super-server-in-linux/</link>
					<comments>https://awjunaid.com/linux/how-to-configure-internet-super-server-in-linux/?noamp=mobile#respond</comments>
		
		<dc:creator><![CDATA[Abdul Wahab Junaid]]></dc:creator>
		<pubDate>Sun, 23 Jul 2023 13:55:59 +0000</pubDate>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[linux]]></category>
		<guid isPermaLink="false">https://awjunaid.com/?p=3392</guid>

					<description><![CDATA[<p>xinetd is one of those pieces of Linux infrastructure that quietly does its job in the background of&#8230;</p>
<p>The post <a href="https://awjunaid.com/linux/how-to-configure-internet-super-server-in-linux/">How to Configure Internet Super Server (xinetd) in Linux: Complete Service Management Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">xinetd is one of those pieces of Linux infrastructure that quietly does its job in the background of older systems, rarely gets a second thought, and then becomes a genuine puzzle the first time you need to add a new service to it and can&#8217;t remember the config syntax. It&#8217;s the &#8220;extended internet services daemon,&#8221; a super-server that listens on behalf of multiple network services and only launches the actual service process when a connection actually arrives — a resource-conservation and access-control pattern that predates the always-running-daemon model most modern services use under systemd.</p>



<h2 class="wp-block-heading">What a Super-Server Does and Why It Existed</h2>



<p class="wp-block-paragraph">Before xinetd (and its predecessor, the original <code>inetd</code>), every network service — telnet, FTP, finger, talk, and dozens of smaller utilities — had to run as its own persistent daemon, sitting in memory and listening on its port continuously, whether or not anyone was actually using it. On systems with limited memory (which was essentially every system, historically), this was wasteful.</p>



<p class="wp-block-paragraph">The super-server model solves this: one process, xinetd, listens on <strong>all</strong> the ports for configured services simultaneously. When a connection arrives on one of those ports, xinetd accepts it, forks, and executes the actual service binary to handle that specific connection, then the service process exits when the connection ends — no persistent per-service daemon required.</p>



<p class="wp-block-paragraph">This model has genuinely fallen out of favor for most workloads, replaced by systemd socket activation (which does something conceptually similar but integrates with the modern service manager) or simply running services as always-on daemons on systems with plenty of RAM to spare. But xinetd is still present, actively used, and worth understanding on older RHEL/CentOS systems, embedded distributions, and specific service categories (like certain TFTP, time, or diagnostic services) that still default to it.</p>



<h2 class="wp-block-heading">Installing xinetd</h2>



<p class="wp-block-paragraph">Debian/Ubuntu:</p>



<pre class="wp-block-code"><code>sudo apt update
sudo apt install xinetd
</code></pre>



<p class="wp-block-paragraph">RHEL/CentOS/Fedora:</p>



<pre class="wp-block-code"><code>sudo dnf install xinetd
</code></pre>



<p class="wp-block-paragraph">Enable and start it:</p>



<pre class="wp-block-code"><code>sudo systemctl enable --now xinetd
sudo systemctl status xinetd
</code></pre>



<h2 class="wp-block-heading">Configuration Structure</h2>



<p class="wp-block-paragraph">xinetd&#8217;s configuration lives in two places:</p>



<ul class="wp-block-list">
<li><strong><code>/etc/xinetd.conf</code></strong> — global defaults applied to all services unless overridden.</li>



<li><strong><code>/etc/xinetd.d/</code></strong> — a directory containing one file per service, each defining that service&#8217;s specific settings.</li>
</ul>



<h3 class="wp-block-heading">Global Config Example (<code>/etc/xinetd.conf</code>)</h3>



<pre class="wp-block-code"><code>defaults
{
    instances       = 60
    log_type        = SYSLOG authpriv
    log_on_success  = HOST PID
    log_on_failure  = HOST
    cps             = 25 30
}

includedir /etc/xinetd.d
</code></pre>



<ul class="wp-block-list">
<li><code>instances</code> — max simultaneous instances of any one service.</li>



<li><code>log_type</code> — where logs go (typically syslog&#8217;s <code>authpriv</code> facility).</li>



<li><code>log_on_success</code>/<code>log_on_failure</code> — what detail to log for successful/failed connection attempts.</li>



<li><code>cps</code> — connections-per-second rate limiting: <code>cps 25 30</code> allows 25 connections/second, then waits 30 seconds if that rate is exceeded, a basic built-in DoS mitigation.</li>



<li><code>includedir</code> — pulls in every service definition file from <code>/etc/xinetd.d/</code>.</li>
</ul>



<h2 class="wp-block-heading">Per-Service Configuration File Structure</h2>



<p class="wp-block-paragraph">Each file in <code>/etc/xinetd.d/</code> follows this pattern:</p>



<pre class="wp-block-code"><code>service SERVICE_NAME
{
    disable         = no
    socket_type     = stream
    protocol        = tcp
    wait            = no
    user            = root
    server          = /path/to/server/binary
    server_args     = arguments
    port            = PORT_NUMBER
    only_from       = allowed hosts/networks
    no_access       = denied hosts/networks
    log_on_success  += extra logging options
    log_on_failure  += extra logging options
}
</code></pre>



<h2 class="wp-block-heading">Key Directive Reference</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Directive</th><th>Meaning</th></tr></thead><tbody><tr><td><code>disable</code></td><td><code>yes</code>/<code>no</code> — whether this service is active</td></tr><tr><td><code>socket_type</code></td><td><code>stream</code> (TCP), <code>dgram</code> (UDP), <code>raw</code>, <code>seqpacket</code></td></tr><tr><td><code>protocol</code></td><td><code>tcp</code>, <code>udp</code></td></tr><tr><td><code>wait</code></td><td><code>yes</code> (single-threaded, service handles its own concurrency) or <code>no</code> (xinetd forks a new instance per connection)</td></tr><tr><td><code>user</code>/<code>group</code></td><td>Which local user/group the service process runs as</td></tr><tr><td><code>server</code></td><td>Full path to the executable</td></tr><tr><td><code>server_args</code></td><td>Arguments passed to the executable</td></tr><tr><td><code>port</code></td><td>Which port to listen on</td></tr><tr><td><code>only_from</code></td><td>Whitelist of allowed client addresses/networks</td></tr><tr><td><code>no_access</code></td><td>Blacklist of denied client addresses/networks</td></tr><tr><td><code>access_times</code></td><td>Time-of-day window the service is available</td></tr><tr><td><code>instances</code></td><td>Per-service override of the global max instance count</td></tr><tr><td><code>bind</code>/<code>interface</code></td><td>Bind to a specific local interface/IP only</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Example: Configuring a TFTP Server</h2>



<p class="wp-block-paragraph">TFTP is one of the more common services still legitimately run under xinetd today, particularly for PXE network boot environments.</p>



<pre class="wp-block-code"><code>sudo dnf install tftp-server    # RHEL/Fedora
sudo apt install tftpd-hpa      # Debian/Ubuntu (note: Debian's tftpd-hpa typically uses its own init/systemd unit rather than xinetd by default)
</code></pre>



<p class="wp-block-paragraph"><code>/etc/xinetd.d/tftp</code> (RHEL-style):</p>



<pre class="wp-block-code"><code>service tftp
{
    socket_type     = dgram
    protocol        = udp
    wait            = yes
    user            = root
    server          = /usr/sbin/in.tftpd
    server_args     = -s /var/lib/tftpboot
    disable         = no
    per_source      = 11
    cps             = 100 2
    flags           = IPv4
}
</code></pre>



<p class="wp-block-paragraph">Restart xinetd to apply:</p>



<pre class="wp-block-code"><code>sudo systemctl restart xinetd
</code></pre>



<h2 class="wp-block-heading">Access Control Within xinetd</h2>



<p class="wp-block-paragraph">Beyond the older TCP Wrappers integration (xinetd can be compiled with <code>libwrap</code> support, honoring <code>/etc/hosts.allow</code>/<code>/etc/hosts.deny</code> in addition to its own rules), xinetd has native access control directives that don&#8217;t depend on that separate mechanism:</p>



<pre class="wp-block-code"><code>service telnet
{
    disable     = no
    only_from   = 192.168.1.0/24 10.0.0.5
    no_access   = 192.168.1.100
    access_times = 08:00-18:00
}
</code></pre>



<p class="wp-block-paragraph">This example allows telnet only from the <code>192.168.1.0/24</code> subnet and <code>10.0.0.5</code>, explicitly blocks <code>192.168.1.100</code> even though it&#8217;s inside that subnet (a more specific rule overriding a broader one), and only during business hours.</p>



<h2 class="wp-block-heading">Rate Limiting and DoS Mitigation</h2>



<p class="wp-block-paragraph">xinetd includes several built-in protections worth knowing:</p>



<pre class="wp-block-code"><code>per_source  = 5      # max simultaneous connections from a single source IP
cps         = 25 30  # max connections per second globally, then pause N seconds if exceeded
max_load    = 4       # refuse new connections if system load average exceeds this
</code></pre>



<p class="wp-block-paragraph">These are genuinely useful defenses against simple flooding attacks targeting a specific service, applied before the connection ever reaches the actual service binary.</p>



<h2 class="wp-block-heading">Logging</h2>



<p class="wp-block-paragraph">Check what xinetd has logged:</p>



<pre class="wp-block-code"><code>sudo journalctl -u xinetd
sudo grep xinetd /var/log/syslog       # Debian/Ubuntu
sudo grep xinetd /var/log/messages      # RHEL/CentOS
</code></pre>



<p class="wp-block-paragraph">Increase verbosity for troubleshooting by adding to the service block:</p>



<pre class="wp-block-code"><code>log_on_success += DURATION
log_on_failure += RECORD
</code></pre>



<h2 class="wp-block-heading">Enabling and Disabling Individual Services</h2>



<p class="wp-block-paragraph">Rather than editing files by hand every time, RHEL-family systems traditionally provided a helper:</p>



<pre class="wp-block-code"><code>sudo chkconfig telnet on
sudo chkconfig telnet off
</code></pre>



<p class="wp-block-paragraph">On systems without <code>chkconfig</code>, edit the <code>disable</code> line directly and restart xinetd:</p>



<pre class="wp-block-code"><code>sudo sed -i 's/disable.*=.*yes/disable = no/' /etc/xinetd.d/telnet
sudo systemctl restart xinetd
</code></pre>



<p class="wp-block-paragraph">Always verify the actual running state after any change:</p>



<pre class="wp-block-code"><code>sudo ss -tulnp | grep xinetd
</code></pre>



<h2 class="wp-block-heading">The wait Directive: A Deeper Look at Concurrency Handling</h2>



<p class="wp-block-paragraph">The <code>wait</code> directive is one of the more commonly misunderstood settings, and getting it wrong produces genuinely confusing behavior — either a service that can&#8217;t handle concurrent connections at all, or one that spawns far more instances than intended.</p>



<p class="wp-block-paragraph"><code>wait = yes</code> (single-threaded mode) — xinetd hands off the listening socket itself to the service and then stops managing that socket entirely until the service process exits. The service is fully responsible for its own concurrency (accepting multiple simultaneous connections on the same socket itself). This mode is appropriate for datagram-based services like TFTP, where the &#8220;connection&#8221; is really a self-contained series of independent request/response exchanges the daemon manages internally.</p>



<p class="wp-block-paragraph"><code>wait = no</code> (multi-threaded mode) — xinetd keeps managing the listening socket itself, and for every new connection, forks a fresh instance of the service to handle just that one connection, then continues listening for the next. This is the appropriate mode for typical connection-oriented services like a shell or FTP session, where each client genuinely needs its own separate, isolated process.</p>



<pre class="wp-block-code"><code># Verify which mode a running instance is actually configured for
grep wait /etc/xinetd.d/tftp
</code></pre>



<p class="wp-block-paragraph">Getting this backwards for a stream-oriented service (setting <code>wait = yes</code> when it should be <code>no</code>) typically manifests as the service handling only one client at a time, with subsequent connection attempts hanging until the first client disconnects — a symptom that looks like a completely different kind of bug until you check this specific setting.</p>



<h2 class="wp-block-heading">A Complete Walkthrough: Adding a New Custom Service to xinetd</h2>



<p class="wp-block-paragraph">Beyond configuring existing packaged services, it&#8217;s worth walking through adding an entirely custom service — a pattern that comes up when integrating a small internal tool that doesn&#8217;t ship its own systemd unit or standalone daemon mode.</p>



<p class="wp-block-paragraph">Suppose you have a simple diagnostic script at <code>/usr/local/bin/status-check.sh</code> that you want reachable on a specific internal port, restricted to your monitoring subnet:</p>



<pre class="wp-block-code"><code>sudo tee /etc/xinetd.d/status-check &lt;&lt;'EOF'
service status-check
{
    disable         = no
    type            = UNLISTED
    socket_type     = stream
    protocol        = tcp
    port            = 9999
    wait            = no
    user            = nobody
    server          = /usr/local/bin/status-check.sh
    only_from       = 10.0.5.0/24
    log_on_success  += DURATION
    log_on_failure  += HOST
}
EOF
</code></pre>



<p class="wp-block-paragraph">The <code>type = UNLISTED</code> directive is required for any service not already registered in <code>/etc/services</code> under that exact name — without it, xinetd refuses to start the service, since it can&#8217;t resolve the service name against the standard services database. You&#8217;d also add a matching entry to <code>/etc/services</code> if you want the port to have a friendly name for other tools&#8217; benefit:</p>



<pre class="wp-block-code"><code>echo "status-check    9999/tcp" | sudo tee -a /etc/services
</code></pre>



<p class="wp-block-paragraph">Restart and verify:</p>



<pre class="wp-block-code"><code>sudo systemctl restart xinetd
sudo ss -tulnp | grep 9999
</code></pre>



<h2 class="wp-block-heading">Environment and Resource Controls Per Service</h2>



<p class="wp-block-paragraph">Beyond access control, xinetd can constrain the resource footprint of each service it manages, which is genuinely useful for preventing one misbehaving service from starving the whole system:</p>



<pre class="wp-block-code"><code>service heavy-tool
{
    disable      = no
    rlimit_cpu   = 60
    rlimit_as    = 256M
    nice         = 10
    env          = PATH=/usr/local/bin:/usr/bin
    passenv      = LANG
}
</code></pre>



<ul class="wp-block-list">
<li><code>rlimit_cpu</code> — maximum CPU seconds a single instance may consume before being killed.</li>



<li><code>rlimit_as</code> — maximum virtual memory (address space) a single instance may use.</li>



<li><code>nice</code> — process scheduling priority adjustment, useful for de-prioritizing a background/diagnostic service relative to more important workloads.</li>



<li><code>env</code>/<code>passenv</code> — explicitly set or pass through specific environment variables to the launched service, rather than inheriting xinetd&#8217;s own full environment by default.</li>
</ul>



<h2 class="wp-block-heading">Interfacing xinetd With systemd on Modern Distros</h2>



<p class="wp-block-paragraph">On systemd-based systems, xinetd itself runs as a regular systemd-managed service (<code>xinetd.service</code>), which means it benefits from systemd&#8217;s own dependency ordering, restart policies, and logging integration even though the services <em>xinetd itself</em> manages don&#8217;t get individual systemd units of their own.</p>



<pre class="wp-block-code"><code>systemctl cat xinetd
</code></pre>



<p class="wp-block-paragraph">This shows xinetd&#8217;s own unit file — worth checking if you need to adjust its restart behavior, resource limits at the xinetd process level itself (as opposed to per-service limits configured within xinetd&#8217;s own config), or startup ordering relative to networking being fully available.</p>



<pre class="wp-block-code"><code>&#91;Unit]
Description=xinetd Service
After=network.target

&#91;Service]
ExecStart=/usr/sbin/xinetd -stayalive -pidfile /run/xinetd.pid
Type=forking
PIDFile=/run/xinetd.pid

&#91;Install]
WantedBy=multi-user.target
</code></pre>



<p class="wp-block-paragraph">If you need to override any of this (a common override being adding <code>Restart=on-failure</code> for extra resilience), use <code>systemctl edit xinetd</code> rather than modifying the packaged unit file directly, so your customization survives package upgrades.</p>



<h2 class="wp-block-heading">Comparing xinetd Service Definitions Across Distro Conventions</h2>



<p class="wp-block-paragraph">A subtlety worth knowing if you maintain configs across both Debian and RHEL-family systems: while the xinetd configuration syntax itself is identical (it&#8217;s the same upstream project), the packaged default service files that ship with common daemons sometimes differ slightly in default settings between distros — RHEL-family packages have historically been somewhat more conservative with default <code>only_from</code> and logging settings than some Debian-packaged equivalents. Always read the actual shipped file after installing a package rather than assuming a configuration you&#8217;ve seen on one distro applies identically on another.</p>



<pre class="wp-block-code"><code>diff &lt;(cat /etc/xinetd.d/tftp) &lt;(ssh other-distro-host cat /etc/xinetd.d/tftp)
</code></pre>



<h2 class="wp-block-heading">Troubleshooting</h2>



<p class="wp-block-paragraph"><strong>Service enabled in config but not responding</strong> — confirm xinetd itself is running and was restarted after the config change:</p>



<pre class="wp-block-code"><code>sudo systemctl status xinetd
sudo systemctl restart xinetd
</code></pre>



<p class="wp-block-paragraph"><strong>&#8220;Address already in use&#8221; errors</strong> — another process (possibly a standalone daemon for the same service) is already bound to that port:</p>



<pre class="wp-block-code"><code>sudo ss -tulnp | grep :PORT
</code></pre>



<p class="wp-block-paragraph"><strong>Connections accepted but immediately dropped</strong> — check <code>server</code> path is correct and the binary is actually executable by the configured <code>user</code>:</p>



<pre class="wp-block-code"><code>ls -l /path/to/server/binary
</code></pre>



<p class="wp-block-paragraph"><strong>only_from/no_access rules not behaving as expected</strong> — remember more specific rules can override broader ones; test carefully and check logs (<code>log_on_failure</code>) to confirm which rule actually matched a given connection attempt.</p>



<h2 class="wp-block-heading">xinetd vs systemd Socket Activation</h2>



<p class="wp-block-paragraph">Modern systemd provides a conceptually similar mechanism — socket units (<code>.socket</code> files) that listen on a port and start the associated service unit on first connection. The philosophical difference: systemd socket activation is meant primarily for startup efficiency and dependency ordering on services that will typically run continuously once started, while xinetd&#8217;s model assumes services genuinely start and stop per connection, with more built-in per-connection access control and rate limiting baked directly into the super-server itself.</p>



<p class="wp-block-paragraph">For new deployments on systemd-based distros, socket activation is generally the more idiomatic choice; xinetd remains relevant primarily for maintaining existing configurations, embedded systems, and specific legacy services that ship with xinetd definitions by default.</p>



<h2 class="wp-block-heading">Security Implications</h2>



<ul class="wp-block-list">
<li>xinetd centralizes exposure for every service it manages — a misconfiguration in <code>/etc/xinetd.conf</code> (like default <code>log_on_success</code> settings, or an overly permissive <code>only_from</code>) can weaken every service under its control at once.</li>



<li>Because many of the classic xinetd-managed services (telnet, tftp, finger, rsh-related daemons) are themselves legacy plaintext protocols, running xinetd at all is sometimes a signal worth auditing — the question isn&#8217;t just &#8220;is xinetd configured securely&#8221; but &#8220;should this particular service exist on this network at all.&#8221;</li>



<li>Combine xinetd&#8217;s own access controls with kernel-level firewall rules (iptables/firewalld) rather than relying on xinetd&#8217;s <code>only_from</code>/<code>no_access</code> as your only line of defense — defense in depth matters here as much as anywhere else.</li>
</ul>



<h2 class="wp-block-heading">Summary</h2>



<p class="wp-block-paragraph">xinetd is a super-server that listens on behalf of multiple network services and launches them on demand, configured through <code>/etc/xinetd.conf</code> for global defaults and per-service files in <code>/etc/xinetd.d/</code>. It provides genuinely useful built-in access control (<code>only_from</code>/<code>no_access</code>/<code>access_times</code>) and rate limiting (<code>cps</code>/<code>per_source</code>/<code>max_load</code>) beyond what the underlying services might implement themselves. While largely superseded by always-on daemons and systemd socket activation for new deployments, it remains actively relevant for services like TFTP and for maintaining legacy infrastructure.</p>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li><a href="https://linux.die.net/man/8/xinetd">man xinetd(8)</a></li>



<li><a href="https://linux.die.net/man/5/xinetd.conf">man xinetd.conf(5)</a></li>



<li><a href="https://access.redhat.com/documentation/">Red Hat Documentation – xinetd (legacy service management)</a></li>



<li><a href="https://github.com/xinetd-org/xinetd">xinetd project documentation</a></li>
</ul>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://awjunaid.com/linux/how-to-configure-internet-super-server-in-linux/">How to Configure Internet Super Server (xinetd) in Linux: Complete Service Management Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://awjunaid.com/linux/how-to-configure-internet-super-server-in-linux/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">3392</post-id>	</item>
		<item>
		<title>How to Turn Off Standalone Services in Linux: Complete Service Disabling and Security Hardening Guide</title>
		<link>https://awjunaid.com/linux/how-to-turn-off-standalone-services-in-linux/</link>
					<comments>https://awjunaid.com/linux/how-to-turn-off-standalone-services-in-linux/?noamp=mobile#respond</comments>
		
		<dc:creator><![CDATA[Abdul Wahab Junaid]]></dc:creator>
		<pubDate>Sun, 23 Jul 2023 13:52:49 +0000</pubDate>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[linux]]></category>
		<guid isPermaLink="false">https://awjunaid.com/?p=3389</guid>

					<description><![CDATA[<p>Every server I&#8217;ve ever inherited from someone else has had at least one service running that nobody could&#8230;</p>
<p>The post <a href="https://awjunaid.com/linux/how-to-turn-off-standalone-services-in-linux/">How to Turn Off Standalone Services in Linux: Complete Service Disabling and Security Hardening Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Every server I&#8217;ve ever inherited from someone else has had at least one service running that nobody could explain. Cups printing support on a headless server. Bluetooth on a rack-mounted machine that will never be within thirty feet of a Bluetooth device. An NFS client stack fully loaded on a box that&#8217;s never touched an NFS share. Turning off standalone services you don&#8217;t need isn&#8217;t just tidiness — it directly reduces attack surface, and it&#8217;s one of the highest-value, lowest-risk hardening steps available on any Linux system.</p>



<h2 class="wp-block-heading">What &#8220;Standalone Service&#8221; Means</h2>



<p class="wp-block-paragraph">A standalone service, in this context, is a daemon that runs continuously and independently, managed directly by the init system (systemd on virtually every current distro) — as opposed to a service launched on-demand by a super-server like xinetd, or spun up transiently by socket activation. Standalone services are the persistent background processes you&#8217;d see with <code>systemctl list-units --type=service</code>.</p>



<h2 class="wp-block-heading">Step 1: Inventory What&#8217;s Actually Running</h2>



<p class="wp-block-paragraph">Before disabling anything, know your baseline.</p>



<pre class="wp-block-code"><code>systemctl list-units --type=service --state=running
</code></pre>



<p class="wp-block-paragraph">For a full list including inactive/disabled services:</p>



<pre class="wp-block-code"><code>systemctl list-unit-files --type=service
</code></pre>



<p class="wp-block-paragraph">Cross-reference with what&#8217;s actually listening on the network — a strong signal for what&#8217;s worth scrutinizing first:</p>



<pre class="wp-block-code"><code>sudo ss -tulnp
</code></pre>



<p class="wp-block-paragraph">Example output:</p>



<pre class="wp-block-code"><code>Netid  State   Local Address:Port   Process
tcp    LISTEN  0.0.0.0:22           sshd
tcp    LISTEN  0.0.0.0:631          cupsd
udp    LISTEN  0.0.0.0:5353         avahi-daemon
</code></pre>



<p class="wp-block-paragraph">If this is a headless server, <code>cupsd</code> (printing) and <code>avahi-daemon</code> (mDNS/Bonjour-style service discovery) are both classic examples of services that add attack surface for zero operational benefit.</p>



<h2 class="wp-block-heading">Step 2: Understand the Difference Between Stop, Disable, and Mask</h2>



<p class="wp-block-paragraph">This distinction trips people up constantly, so it&#8217;s worth being precise:</p>



<pre class="wp-block-code"><code>sudo systemctl stop SERVICE      # stops it right now, but it will start again on next boot if enabled
sudo systemctl disable SERVICE   # prevents it from starting on boot, but doesn't stop it if currently running
sudo systemctl mask SERVICE      # prevents it from being started at all, even manually or as a dependency of another unit
</code></pre>



<p class="wp-block-paragraph">For genuinely turning off a service you don&#8217;t need, you typically want both <code>stop</code> and <code>disable</code> together:</p>



<pre class="wp-block-code"><code>sudo systemctl stop cups
sudo systemctl disable cups
</code></pre>



<p class="wp-block-paragraph">Or in one command:</p>



<pre class="wp-block-code"><code>sudo systemctl disable --now cups
</code></pre>



<p class="wp-block-paragraph"><code>mask</code> is the strongest option — it symlinks the unit file to <code>/dev/null</code>, so nothing can start it even accidentally as a dependency of something else. Reach for this when a service is not just unwanted but something you want to guarantee can never come back without deliberate intervention:</p>



<pre class="wp-block-code"><code>sudo systemctl mask cups
</code></pre>



<p class="wp-block-paragraph">To reverse a mask later:</p>



<pre class="wp-block-code"><code>sudo systemctl unmask cups
</code></pre>



<h2 class="wp-block-heading">Step 3: Verify a Service Is Actually Stopped</h2>



<pre class="wp-block-code"><code>systemctl status cups
</code></pre>



<p class="wp-block-paragraph">Confirm it&#8217;s no longer listening:</p>



<pre class="wp-block-code"><code>sudo ss -tulnp | grep cups
</code></pre>



<p class="wp-block-paragraph">Confirm it won&#8217;t reappear on next boot:</p>



<pre class="wp-block-code"><code>systemctl is-enabled cups
# should print: disabled  (or "masked" if you masked it)
</code></pre>



<h2 class="wp-block-heading">Common Services Worth Reviewing on a Server</h2>



<p class="wp-block-paragraph">This isn&#8217;t a &#8220;disable all of these blindly&#8221; list — it&#8217;s a starting point for asking &#8220;do I actually need this here&#8221;:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Service</th><th>Typical purpose</th><th>Usually needed on a headless server?</th></tr></thead><tbody><tr><td><code>cups</code>/<code>cups-browsed</code></td><td>Printing support</td><td>Rarely</td></tr><tr><td><code>avahi-daemon</code></td><td>mDNS/Bonjour service discovery</td><td>Rarely</td></tr><tr><td><code>bluetooth</code></td><td>Bluetooth stack</td><td>Almost never on server hardware</td></tr><tr><td><code>ModemManager</code></td><td>Cellular/modem management</td><td>Almost never</td></tr><tr><td><code>rpcbind</code>/<code>nfs-server</code></td><td>NFS/RPC services</td><td>Only if actually serving/mounting NFS</td></tr><tr><td><code>postfix</code>/<code>sendmail</code></td><td>Local mail transfer agent</td><td>Only if the box actually sends mail (many distros install one by default for system notifications)</td></tr><tr><td><code>telnet</code>/<code>rsh</code>/<code>vsftpd</code> (anonymous)</td><td>Legacy plaintext remote access</td><td>Essentially never — should be SSH/SFTP instead</td></tr><tr><td><code>snapd</code></td><td>Snap package management</td><td>Depends on whether you use snap packages at all</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Check what a given service actually is before disabling it if you&#8217;re unsure:</p>



<pre class="wp-block-code"><code>systemctl status SERVICE
man SERVICE 2&gt;/dev/null
</code></pre>



<h2 class="wp-block-heading">Disabling Multiple Services at Once</h2>



<pre class="wp-block-code"><code>sudo systemctl disable --now cups avahi-daemon bluetooth ModemManager
</code></pre>



<h2 class="wp-block-heading">Auditing and Disabling Legacy Standalone Daemons (Non-systemd Remnants)</h2>



<p class="wp-block-paragraph">On systems with SysV init compatibility scripts still present (common on older RHEL/CentOS systems even under systemd), you might also encounter services managed through <code>/etc/init.d/</code> and <code>chkconfig</code>:</p>



<pre class="wp-block-code"><code>chkconfig --list
sudo chkconfig SERVICE off
sudo service SERVICE stop
</code></pre>



<p class="wp-block-paragraph">Modern systemd transparently wraps most of these, so <code>systemctl disable --now SERVICE</code> typically works even for these legacy-style services, but <code>chkconfig --list</code> is still useful for surfacing what&#8217;s actually configured to run.</p>



<h2 class="wp-block-heading">A Practical Server Hardening Pass</h2>



<p class="wp-block-paragraph">Here&#8217;s the sequence I actually run when hardening a fresh server image:</p>



<pre class="wp-block-code"><code># See what's running and listening
systemctl list-units --type=service --state=running
sudo ss -tulnp

# Disable common unnecessary services on a headless box (review this list per your actual use case first!)
sudo systemctl disable --now cups cups-browsed avahi-daemon bluetooth ModemManager 2&gt;/dev/null

# Confirm nothing unexpected is still listening afterward
sudo ss -tulnp

# Check for any remaining SysV-style legacy services
chkconfig --list 2&gt;/dev/null
</code></pre>



<p class="wp-block-paragraph">I always run the <code>ss -tulnp</code> check both before and after — the &#8220;after&#8221; check confirms the disabling actually took effect and nothing else quietly restarted the service as a dependency.</p>



<h2 class="wp-block-heading">Handling Services That Restart Themselves</h2>



<p class="wp-block-paragraph">Some services get re-triggered by socket activation, timers, or another unit&#8217;s dependency chain, even after you&#8217;ve disabled them. Diagnose with:</p>



<pre class="wp-block-code"><code>systemctl status SERVICE
journalctl -u SERVICE --since "10 minutes ago"
</code></pre>



<p class="wp-block-paragraph">Look specifically for &#8220;Triggered by&#8221; or dependency information in the status output — if another unit (like a <code>.socket</code> or <code>.path</code> unit) is what&#8217;s actually starting it, you need to disable that unit as well:</p>



<pre class="wp-block-code"><code>systemctl list-dependencies SERVICE --reverse
sudo systemctl disable --now SERVICE.socket
</code></pre>



<h2 class="wp-block-heading">Automating This as Part of Provisioning</h2>



<p class="wp-block-paragraph">For fleets of servers, doing this by hand every time doesn&#8217;t scale. A simple hardening script:</p>



<pre class="wp-block-code"><code>#!/bin/bash
UNWANTED_SERVICES=(cups cups-browsed avahi-daemon bluetooth ModemManager)

for svc in "${UNWANTED_SERVICES&#91;@]}"; do
    if systemctl list-unit-files | grep -q "^${svc}.service"; then
        systemctl disable --now "$svc" 2&gt;/dev/null
        echo "Disabled: $svc"
    fi
done
</code></pre>



<p class="wp-block-paragraph">For configuration management at scale, tools like Ansible make this declarative and idempotent:</p>



<pre class="wp-block-code"><code>- name: Disable unnecessary services
  ansible.builtin.systemd:
    name: "{{ item }}"
    state: stopped
    enabled: false
  loop:
    - cups
    - avahi-daemon
    - bluetooth
</code></pre>



<h2 class="wp-block-heading">Understanding systemd Unit Dependency Chains Before You Disable Anything</h2>



<p class="wp-block-paragraph">The single most common mistake in a service-disabling pass isn&#8217;t disabling something genuinely needed — it&#8217;s disabling something that a <em>different</em>, more important service quietly depends on, and only discovering that dependency when the important thing breaks later. Before disabling anything you&#8217;re not already confident about, check its reverse dependencies:</p>



<pre class="wp-block-code"><code>systemctl list-dependencies --reverse cups
</code></pre>



<p class="wp-block-paragraph">This shows what would be affected if <code>cups</code> stopped — if the output includes something you actually care about, that&#8217;s your signal to investigate further before proceeding, rather than a green light to disable freely.</p>



<p class="wp-block-paragraph">It&#8217;s also worth understanding the different kinds of unit relationships systemd tracks, since not all of them mean &#8220;requires&#8221;:</p>



<ul class="wp-block-list">
<li><strong><code>Requires=</code></strong> — a hard dependency; if the required unit fails, the dependent unit is stopped too.</li>



<li><strong><code>Wants=</code></strong> — a soft dependency; the wanted unit is started alongside, but its failure doesn&#8217;t stop the dependent unit.</li>



<li><strong><code>After=</code>/<code>Before=</code></strong> — pure ordering, with no dependency implication at all — a unit can be ordered after another without requiring it to even be running.</li>
</ul>



<pre class="wp-block-code"><code>systemctl show cups -p Requires -p Wants -p RequiredBy -p WantedBy
</code></pre>



<p class="wp-block-paragraph">Reading these fields directly, rather than guessing from the service name alone, is the difference between a confident disabling decision and one you&#8217;ll be debugging at an inconvenient hour later.</p>



<h2 class="wp-block-heading">Socket-Activated and Path-Activated Services: A Special Case</h2>



<p class="wp-block-paragraph">Some services aren&#8217;t started directly at boot at all — they&#8217;re started on-demand the first time something connects to a socket or a watched file/path changes, via a corresponding <code>.socket</code> or <code>.path</code> unit. Disabling the <code>.service</code> unit alone in these cases often has no effect, because it&#8217;s the socket/path unit doing the actual triggering.</p>



<pre class="wp-block-code"><code>systemctl list-units --type=socket --state=active
systemctl list-units --type=path --state=active
</code></pre>



<p class="wp-block-paragraph">If you disable <code>cups.service</code> but <code>cups.socket</code> remains enabled, the very next print-related connection attempt will start <code>cups.service</code> right back up, regardless of its own enabled/disabled state — the socket unit&#8217;s activation bypasses that check. The fix is to disable both:</p>



<pre class="wp-block-code"><code>sudo systemctl disable --now cups.socket cups.path cups.service 2&gt;/dev/null
</code></pre>



<p class="wp-block-paragraph">This is a genuinely common source of &#8220;I disabled it but it keeps coming back&#8221; confusion, and checking for a same-named <code>.socket</code> or <code>.path</code> unit is the first thing worth doing whenever a disable doesn&#8217;t stick.</p>



<h2 class="wp-block-heading">A More Thorough Server Hardening Checklist</h2>



<p class="wp-block-paragraph">Beyond the handful of commonly-cited services (cups, avahi, bluetooth), a more thorough pass through a fresh server image worth walking through explicitly:</p>



<pre class="wp-block-code"><code># Full inventory of enabled services, sorted for readability
systemctl list-unit-files --type=service --state=enabled | sort

# Cross-reference against what's actually listening
sudo ss -tulnp

# Check for services that are running but weren't part of the base image
# (useful after inheriting a server someone else configured)
rpm -qa --last | head -20     # RHEL — recently installed packages, oldest install activity first
grep " install " /var/log/dpkg.log | tail -20   # Debian equivalent
</code></pre>



<p class="wp-block-paragraph">Beyond the obvious desktop-oriented services, it&#8217;s worth specifically reviewing:</p>



<ul class="wp-block-list">
<li><strong><code>rpcbind</code></strong> — required for NFS and some legacy RPC-based services; if nothing on the box actually serves or mounts NFS, this can typically be disabled, closing off a service that&#8217;s had a long history of security advisories.</li>



<li><strong><code>postfix</code>/<code>sendmail</code></strong> — many distros install a local MTA by default purely so system utilities and cron can send local notification mail; if nothing on the box actually needs to send external mail, consider configuring it in local-only mode rather than leaving it listening on all interfaces, or disabling it entirely if local mail delivery genuinely isn&#8217;t needed.</li>



<li><strong><code>snapd</code></strong> — relevant primarily on Ubuntu; if you don&#8217;t use snap packages at all, this can often be removed rather than merely disabled.</li>



<li><strong><code>cockpit</code></strong> — a web-based server management UI on some RHEL-family installs; convenient, but represents meaningful attack surface (a full web application and its own auth stack) if you&#8217;re not actually using it for management.</li>
</ul>



<h2 class="wp-block-heading">Handling Firmware/Hardware-Triggered Services Carefully</h2>



<p class="wp-block-paragraph">A category worth extra caution: services tied to physical hardware detection (like <code>udisks2</code> for removable media, or various hardware-monitoring daemons). These are usually harmless to disable on a headless server that will never have removable media inserted, but on physical hardware (as opposed to a VM/cloud instance), some of these also feed into legitimate hardware health monitoring — disabling blindly on bare-metal without checking what depends on it can occasionally remove monitoring capability you actually wanted.</p>



<pre class="wp-block-code"><code>systemctl status smartd    # disk health monitoring — usually worth KEEPING on physical hardware
systemctl status lm-sensors 2&gt;/dev/null    # temperature/voltage sensors — same caution applies
</code></pre>



<p class="wp-block-paragraph">The general principle: services tied to network exposure (listening ports, discovery protocols) are almost always safe disable candidates on a server that doesn&#8217;t need them; services tied to local hardware health monitoring deserve a second look specifically on physical hardware, since disabling them trades away visibility rather than just reducing attack surface.</p>



<h2 class="wp-block-heading">Automating the Audit Itself, Not Just the Disabling</h2>



<p class="wp-block-paragraph">Beyond the one-time disabling script shown earlier, it&#8217;s worth having a standing audit script that reports (without acting) on anything that&#8217;s changed since the last known-good baseline — useful for catching a service that got silently re-enabled by a package update or a colleague&#8217;s troubleshooting session:</p>



<pre class="wp-block-code"><code>#!/bin/bash
BASELINE="/etc/service-baseline.txt"
CURRENT=$(systemctl list-unit-files --type=service --state=enabled | sort)

if &#91; ! -f "$BASELINE" ]; then
    echo "$CURRENT" &gt; "$BASELINE"
    echo "Baseline created."
    exit 0
fi

diff &lt;(cat "$BASELINE") &lt;(echo "$CURRENT")
</code></pre>



<p class="wp-block-paragraph">Running this periodically (via cron or a configuration-management drift check) turns &#8220;turn off unnecessary services&#8221; from a one-time hardening pass into an ongoing, low-effort control rather than something that quietly erodes the first time someone installs a new package that pulls in an unwanted service as a dependency.</p>



<h2 class="wp-block-heading">Troubleshooting</h2>



<p class="wp-block-paragraph"><strong>Disabled a service and something else broke</strong> — check reverse dependencies before disabling anything on a production system you don&#8217;t fully understand yet:</p>



<pre class="wp-block-code"><code>systemctl list-dependencies --reverse SERVICE
</code></pre>



<p class="wp-block-paragraph"><strong>Service keeps coming back after disable</strong> — check for socket/path/timer units and any other unit that lists it as a dependency, as covered above.</p>



<p class="wp-block-paragraph"><strong>Not sure if a service is safe to disable</strong> — check its actual resource usage and recent logs before deciding; a service that&#8217;s been silently running for years with zero connections and zero log activity is a much safer disable candidate than one with regular recent activity:</p>



<pre class="wp-block-code"><code>journalctl -u SERVICE --since "30 days ago" | tail -50
</code></pre>



<h2 class="wp-block-heading">Security and Performance Implications</h2>



<p class="wp-block-paragraph">Every standalone service running is: additional code that could contain a vulnerability, additional listening ports that widen the network attack surface, additional memory and CPU consumed at idle, and additional complexity when auditing what a system actually does. None of that is hypothetical — unused services with old, unpatched code are a recurring theme in real-world compromises, precisely because nobody&#8217;s paying attention to a thing they forgot was even running. Disabling what you don&#8217;t use is one of the few security improvements that costs essentially nothing in terms of functionality lost.</p>



<h2 class="wp-block-heading">Summary</h2>



<p class="wp-block-paragraph">Turning off standalone services in Linux comes down to three systemd commands — <code>stop</code>, <code>disable</code>, and <code>mask</code> — applied deliberately after actually auditing what&#8217;s running and what&#8217;s listening on the network. The habit worth building is doing this audit as a routine part of provisioning any new server, not as a one-time cleanup exercise, and always verifying with <code>ss -tulnp</code> and <code>systemctl is-enabled</code> that a change actually took effect and stuck across a reboot.</p>



<h2 class="wp-block-heading">References</h2>



<ul class="wp-block-list">
<li><a href="https://www.freedesktop.org/software/systemd/man/systemd.unit.html">systemd.unit(5) man page</a></li>



<li><a href="https://www.freedesktop.org/software/systemd/man/systemctl.html">systemctl(1) man page</a></li>



<li><a href="https://www.cisecurity.org/cis-benchmarks/">CIS Benchmarks</a></li>



<li><a href="https://access.redhat.com/documentation/">Red Hat Documentation – Managing systemd services</a></li>
</ul>
<p>The post <a href="https://awjunaid.com/linux/how-to-turn-off-standalone-services-in-linux/">How to Turn Off Standalone Services in Linux: Complete Service Disabling and Security Hardening Guide</a> appeared first on <a href="https://awjunaid.com">Abdul Wahab Junaid</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://awjunaid.com/linux/how-to-turn-off-standalone-services-in-linux/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">3389</post-id>	</item>
	</channel>
</rss>
