{"id":1579,"date":"2025-11-09T18:26:33","date_gmt":"2025-11-09T15:26:33","guid":{"rendered":"https:\/\/www.dchost.com\/blog\/the-nftables-firewall-cookbook-for-vps-rate-limiting-port-knocking-and-ipv6-rules-without-the-drama\/"},"modified":"2025-11-09T18:26:33","modified_gmt":"2025-11-09T15:26:33","slug":"the-nftables-firewall-cookbook-for-vps-rate-limiting-port-knocking-and-ipv6-rules-without-the-drama","status":"publish","type":"post","link":"https:\/\/www.dchost.com\/blog\/en\/the-nftables-firewall-cookbook-for-vps-rate-limiting-port-knocking-and-ipv6-rules-without-the-drama\/","title":{"rendered":"The nftables Firewall Cookbook for VPS: Rate Limiting, Port Knocking, and IPv6 Rules (Without the Drama)"},"content":{"rendered":"<div class=\"dchost-blog-content-wrapper\"><p>So there I was, a sleepy Sunday morning, coffee in hand, poking through a small <a href=\"https:\/\/www.dchost.com\/vps\">VPS<\/a> I use for side projects. A handful of weird SSH connection attempts popped up in the logs, then a few aggressive HTTP requests with random query strings\u2014clearly not from my friends. You\u2019ve probably had that moment too: you open your VPS, and there\u2019s a steady drumbeat of the internet keeping your ports warm. It\u2019s not personal; it\u2019s just the background noise of the web. Still, it\u2019s your server, and you want it calm, predictable, and safe.<\/p>\n<p>That\u2019s when nftables clicked for me. It felt like moving from tangled headphone wires to a tidy wireless setup. Powerful, consistent, and way less drama. In this guide, I\u2019ll share a practical, step-by-step nftables cookbook I use on real VPSes\u2014covering per\u2011IP rate limiting that doesn\u2019t lock out legit users, a simple port knocking flow for when you want stealth without ceremony, and IPv6 rules that \u201cjust work.\u201d We\u2019ll keep it friendly and focused, and I\u2019ll sprinkle in real-world notes where it matters. Ready?<\/p>\n<div id=\"toc_container\" class=\"toc_transparent no_bullets\"><p class=\"toc_title\">\u0130&ccedil;indekiler<\/p><ul class=\"toc_list\"><li><a href=\"#Why_nftables_And_What_Were_Going_to_Build\"><span class=\"toc_number toc_depth_1\">1<\/span> Why nftables? And What We\u2019re Going to Build<\/a><\/li><li><a href=\"#A_Clean_Baseline_You_Can_Trust\"><span class=\"toc_number toc_depth_1\">2<\/span> A Clean Baseline You Can Trust<\/a><ul><li><a href=\"#Start_with_a_safe_apply-and-rollback_habit\"><span class=\"toc_number toc_depth_2\">2.1<\/span> Start with a safe apply-and-rollback habit<\/a><\/li><li><a href=\"#Enable_nftables_on_boot\"><span class=\"toc_number toc_depth_2\">2.2<\/span> Enable nftables on boot<\/a><\/li><li><a href=\"#Baseline_ruleset_default_deny_inbound_sensible_outbound\"><span class=\"toc_number toc_depth_2\">2.3<\/span> Baseline ruleset: default deny inbound, sensible outbound<\/a><\/li><\/ul><\/li><li><a href=\"#Rate_Limiting_That_Helps_and_Doesnt_Bite_You\"><span class=\"toc_number toc_depth_1\">3<\/span> Rate Limiting That Helps (and Doesn\u2019t Bite You)<\/a><ul><li><a href=\"#PerIP_SSH_guard_with_meters\"><span class=\"toc_number toc_depth_2\">3.1<\/span> Per\u2011IP SSH guard with meters<\/a><\/li><li><a href=\"#Optional_log_overflow_without_spamming_yourself\"><span class=\"toc_number toc_depth_2\">3.2<\/span> Optional: log overflow without spamming yourself<\/a><\/li><\/ul><\/li><li><a href=\"#Port_Knocking_Without_the_Ceremony\"><span class=\"toc_number toc_depth_1\">4<\/span> Port Knocking Without the Ceremony<\/a><ul><li><a href=\"#Define_the_sets\"><span class=\"toc_number toc_depth_2\">4.1<\/span> Define the sets<\/a><\/li><li><a href=\"#Create_the_knocking_sequence_rules\"><span class=\"toc_number toc_depth_2\">4.2<\/span> Create the knocking sequence rules<\/a><\/li><\/ul><\/li><li><a href=\"#IPv6_Rules_That_Just_Work\"><span class=\"toc_number toc_depth_1\">5<\/span> IPv6 Rules That Just Work<\/a><ul><li><a href=\"#Let_ICMPv6_breathe\"><span class=\"toc_number toc_depth_2\">5.1<\/span> Let ICMPv6 breathe<\/a><\/li><li><a href=\"#Dont_forget_DNS_over_IPv6\"><span class=\"toc_number toc_depth_2\">5.2<\/span> Don\u2019t forget DNS over IPv6<\/a><\/li><li><a href=\"#Services_dualstacked_by_default\"><span class=\"toc_number toc_depth_2\">5.3<\/span> Services dual\u2011stacked by default<\/a><\/li><\/ul><\/li><li><a href=\"#Save_It_Test_It_and_Sleep_Better\"><span class=\"toc_number toc_depth_1\">6<\/span> Save It, Test It, and Sleep Better<\/a><ul><li><a href=\"#Use_a_working_file_and_atomic_swaps\"><span class=\"toc_number toc_depth_2\">6.1<\/span> Use a working file and atomic swaps<\/a><\/li><li><a href=\"#Log_a_little_trace_when_needed\"><span class=\"toc_number toc_depth_2\">6.2<\/span> Log a little, trace when needed<\/a><\/li><li><a href=\"#Backups_are_boring_Perfect\"><span class=\"toc_number toc_depth_2\">6.3<\/span> Backups are boring. Perfect.<\/a><\/li><li><a href=\"#Persistence_on_boot\"><span class=\"toc_number toc_depth_2\">6.4<\/span> Persistence on boot<\/a><\/li><\/ul><\/li><li><a href=\"#Putting_It_All_Together_A_Practical_Example\"><span class=\"toc_number toc_depth_1\">7<\/span> Putting It All Together: A Practical Example<\/a><\/li><li><a href=\"#WrapUp_Calm_Defenses_Happy_Services\"><span class=\"toc_number toc_depth_1\">8<\/span> Wrap\u2011Up: Calm Defenses, Happy Services<\/a><ul><li><a href=\"#Useful_references_I_keep_bookmarked\"><span class=\"toc_number toc_depth_2\">8.1<\/span> Useful references I keep bookmarked<\/a><\/li><\/ul><\/li><\/ul><\/div>\n<h2 id=\"section-1\"><span id=\"Why_nftables_And_What_Were_Going_to_Build\">Why nftables? And What We\u2019re Going to Build<\/span><\/h2>\n<p>I remember struggling with iptables rules that felt like a pile of sticky notes. They worked\u2026 until the day they didn\u2019t. Nftables brings a cleaner model. One ruleset, one syntax, families that handle both IPv4 and IPv6, and thoughtful features like sets, maps, meters, and timeouts. Think of it like going from a drawer full of mismatched forks to a simple, matching cutlery set. You still cook the meal, but you\u2019re not wrestling your tools every time.<\/p>\n<p>Here\u2019s the vibe of what we\u2019ll build together:<\/p>\n<p>First, a calm baseline: default deny on inbound traffic with only what we need allowed, per-family ICMP configured properly (especially crucial for IPv6), and a clean separation of input, forward, and output behavior. Then, pragmatic rate limiting: per\u2011IP guards on SSH, sensible protection on web ports, and a few patterns you can adapt to other services. After that, we\u2019ll add a simple port knocking flow. Not as a magic bullet\u2014more like a small velvet rope at a private door. Finally, IPv6 rules that don\u2019t break neighbor discovery, ping, or your sanity.<\/p>\n<p>We\u2019ll also talk about saving, testing, and rolling back safely\u2014because nothing ruins your day like locking yourself out of your own server. Ask me how I know.<\/p>\n<h2 id=\"section-2\"><span id=\"A_Clean_Baseline_You_Can_Trust\">A Clean Baseline You Can Trust<\/span><\/h2>\n<h3><span id=\"Start_with_a_safe_apply-and-rollback_habit\">Start with a safe apply-and-rollback habit<\/span><\/h3>\n<p>Before we write a single rule, adopt a ritual: apply changes with a built\u2011in rollback window. I usually keep a second SSH session open and run something like this when testing:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">sudo bash -c '\n  nft -f \/etc\/nftables.conf.new &amp;&amp; \n  cp \/etc\/nftables.conf \/etc\/nftables.conf.backup &amp;&amp; \n  mv \/etc\/nftables.conf.new \/etc\/nftables.conf &amp;&amp; \n  ( sleep 30; nft list ruleset &gt; \/dev\/null || nft -f \/etc\/nftables.conf.backup ) &amp;\n'<\/code><\/pre>\n<p>The idea is simple: if your connection drops and you can\u2019t get back in, that sleeping command will try to restore the previous ruleset. It\u2019s saved me more than once when I was too clever for my own good.<\/p>\n<h3><span id=\"Enable_nftables_on_boot\">Enable nftables on boot<\/span><\/h3>\n<p>On most modern distros, it\u2019s straightforward:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">sudo apt-get install nftables   # or your distro\u2019s package manager\nsudo systemctl enable --now nftables<\/code><\/pre>\n<p>From here on, you\u2019ll manage your persistent configuration in <strong>\/etc\/nftables.conf<\/strong>. I like to keep a working file, test it, then swap it in.<\/p>\n<h3><span id=\"Baseline_ruleset_default_deny_inbound_sensible_outbound\">Baseline ruleset: default deny inbound, sensible outbound<\/span><\/h3>\n<p>Let\u2019s sketch a sturdy baseline. We\u2019ll use a single <strong>inet<\/strong> table, which can hold rules for both IPv4 and IPv6. It\u2019s convenient, but remember that some data types (like address sets) still need v4\/v6-specific types.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># \/etc\/nftables.conf\n\nflush ruleset\n\ntable inet filter {\n  # Optional: named counters to see traffic\n  counter ssh_hits {}\n  counter web_hits {}\n\n  # IPv4 and IPv6 allowlists, if you need them later (empty for now)\n  set allowed_ssh_v4 {\n    type ipv4_addr;\n    flags interval;\n  }\n  set allowed_ssh_v6 {\n    type ipv6_addr;\n    flags interval;\n  }\n\n  chain input {\n    type filter hook input priority 0; policy drop;\n\n    # Always allow loopback\n    iif lo accept\n\n    # Drop obviously broken stuff first\n    ct state invalid drop\n\n    # Allow established\/related, keeps ongoing connections happy\n    ct state established,related accept\n\n    # IPv4 ICMP: allow basic ping and fragmentation needed bits\n    ip protocol icmp icmp type { echo-request, echo-reply, time-exceeded, destination-unreachable } accept\n\n    # IPv6 ICMPv6: absolutely essential for neighbor discovery, MTU, etc.\n    ip6 nexthdr icmpv6 icmpv6 type {\n      echo-request, echo-reply,\n      nd-neighbor-solicit, nd-neighbor-advert,\n      nd-router-solicit, nd-router-advert,\n      packet-too-big, time-exceeded, parameter-problem\n    } accept\n\n    # SSH, HTTP\/HTTPS will be layered below with rate limits\n    # (We\u2019ll add those rules in the next sections.)\n\n    # Example: allow DNS replies if you run a local resolver on UDP\/53 (optional)\n    # udp dport 53 accept\n\n    # Everything else inbound: drop by default\n  }\n\n  chain forward {\n    type filter hook forward priority 0; policy drop;\n  }\n\n  chain output {\n    type filter hook output priority 0; policy accept;\n  }\n}<\/code><\/pre>\n<p>This already calms the noise. You\u2019ll still need to explicitly open ports you care about, but the shape is clear: keep what\u2019s working, and say no to the random drive\u2011bys.<\/p>\n<h2 id=\"section-3\"><span id=\"Rate_Limiting_That_Helps_and_Doesnt_Bite_You\">Rate Limiting That Helps (and Doesn\u2019t Bite You)<\/span><\/h2>\n<p>Here\u2019s the thing about rate limiting: it\u2019s a balancing act. Too strict and you lock out real users during bursts (or yourself when your connection hiccups). Too loose and it\u2019s just decorative. The trick is to be just protective enough without being precious.<\/p>\n<h3><span id=\"PerIP_SSH_guard_with_meters\">Per\u2011IP SSH guard with meters<\/span><\/h3>\n<p>For SSH, I like a per\u2011IP meter on new connection attempts. It lets one person type their password wrong a couple of times without drama, but squashes the fog of bots hammering you from one address.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">table inet filter {\n  # ... previous content ...\n\n  chain input {\n    type filter hook input priority 0; policy drop;\n    iif lo accept\n    ct state invalid drop\n    ct state established,related accept\n\n    # ICMP\/ICMPv6 as before\n\n    # SSH allowlist if you use it (optional)\n    ip saddr @allowed_ssh_v4 tcp dport 22 accept\n    ip6 saddr @allowed_ssh_v6 tcp dport 22 accept\n\n    # Per\u2011IP SSH rate limit (IPv4)\n    tcp dport 22 ct state new meter ssh_v4 { ip saddr limit rate 20\/minute burst 20 } counter name ssh_hits accept\n    # Per\u2011IP SSH rate limit (IPv6)\n    tcp dport 22 ct state new meter ssh_v6 { ip6 saddr limit rate 20\/minute burst 20 } counter name ssh_hits accept\n\n    # If a sender exceeds the limit, this rule catches the overflow\n    tcp dport 22 counter drop\n\n    # Web ports (rate limit lightly on new connections, not on in\u2011flow traffic)\n    tcp dport {80, 443} ct state new meter web_v4 { ip saddr limit rate 200\/second burst 400 } counter name web_hits accept\n    tcp dport {80, 443} ct state new meter web_v6 { ip6 saddr limit rate 200\/second burst 400 } counter name web_hits accept\n\n    # If you run UDP services (e.g., DNS), a light limit helps\n    # udp dport 53 meter dns_v4 { ip saddr limit rate 50\/second burst 100 } accept\n    # udp dport 53 meter dns_v6 { ip6 saddr limit rate 50\/second burst 100 } accept\n\n    # ... rest as before ...\n  }\n}<\/code><\/pre>\n<p>In my experience, these values are friendly. Twenty SSH connection attempts per minute per IP is generous enough for humans and aggressive enough for lazy bots. And for web traffic, remember this limits <strong>new<\/strong> connections only. Persistent connections (or already established flows) aren\u2019t touched.<\/p>\n<p>One more practical suggestion: pair network\u2011level rate limiting with app\u2011level protections. For example, if your SSH is public, consider using modern auth patterns. I shared my battle\u2011tested setup in <a href=\"https:\/\/www.dchost.com\/blog\/en\/vpste-ssh-guvenligi-nasil-saglamlasir-fido2-anahtarlari-ssh-ca-ve-rotasyonun-sicacik-yolculugu\/\">a no\u2011drama SSH hardening workflow with FIDO2 keys<\/a>. It\u2019s honestly a joy once it\u2019s in place.<\/p>\n<h3><span id=\"Optional_log_overflow_without_spamming_yourself\">Optional: log overflow without spamming yourself<\/span><\/h3>\n<p>Sometimes you want to see who\u2019s getting rate\u2011limited. A small log line is enough\u2014just don\u2019t spam your disks.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># Example: right before the &quot;drop&quot; rule for SSH\nlimit rate 5\/second over tcp dport 22 log prefix &quot;nft-ssh-overflow &quot; flags all level info<\/code><\/pre>\n<p>If your distro uses rate\u2011limited kernel logging by default, great. Otherwise, keep the log limit low. You can also pipe kernel logs into something civilized later. If you want a neat path to organized logs, I\u2019ve written about lightweight setups that play nice with Grafana and Loki.<\/p>\n<h2 id=\"section-4\"><span id=\"Port_Knocking_Without_the_Ceremony\">Port Knocking Without the Ceremony<\/span><\/h2>\n<p>Port knocking sometimes sparks heated debates. I treat it as a light seatbelt\u2014handy on hobby servers or small team boxes with static admin IPs changing occasionally. It\u2019s not a replacement for strong auth; it\u2019s a little curtain you can draw, so the door isn\u2019t obviously there.<\/p>\n<p>Here\u2019s a simple three\u2011step knock that opens SSH for a limited window per source IP. We\u2019ll do it for both IPv4 and IPv6 with dynamic sets that expire on their own. The flow is: knock A, then knock B within 10 seconds, then knock C within 10 seconds. If you complete the sequence, your IP gets into the \u201copen\u201d set for, say, 30 minutes.<\/p>\n<h3><span id=\"Define_the_sets\">Define the sets<\/span><\/h3>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">table inet filter {\n  set knock1_v4 { type ipv4_addr; flags timeout; timeout 10s; }\n  set knock2_v4 { type ipv4_addr; flags timeout; timeout 10s; }\n  set knock_open_v4 { type ipv4_addr; flags timeout; timeout 30m; }\n\n  set knock1_v6 { type ipv6_addr; flags timeout; timeout 10s; }\n  set knock2_v6 { type ipv6_addr; flags timeout; timeout 10s; }\n  set knock_open_v6 { type ipv6_addr; flags timeout; timeout 30m; }\n\n  # ... chains ...\n}<\/code><\/pre>\n<h3><span id=\"Create_the_knocking_sequence_rules\">Create the knocking sequence rules<\/span><\/h3>\n<p>We\u2019ll use dynsets to add the source address dynamically when a port gets hit. Choose three high, non\u2011conflicting UDP or TCP ports that you don\u2019t use for anything else. I\u2019ll use 42111, 42112, 42113 below. If you\u2019re behind a tricky NAT, TCP works more reliably for some clients, but UDP is slightly lighter.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">chain input {\n  type filter hook input priority 0; policy drop;\n  # ... baseline rules above ...\n\n  # Stage 1: hit port 42111 to start\n  tcp dport 42111 add @knock1_v4 { ip saddr timeout 10s } drop\n  tcp dport 42111 add @knock1_v6 { ip6 saddr timeout 10s } drop\n\n  # Stage 2: hit port 42112 within 10s of stage 1\n  tcp dport 42112 ip saddr @knock1_v4 add @knock2_v4 { ip saddr timeout 10s } drop\n  tcp dport 42112 ip6 saddr @knock1_v6 add @knock2_v6 { ip6 saddr timeout 10s } drop\n\n  # Stage 3: hit port 42113 within 10s of stage 2; open SSH for 30m\n  tcp dport 42113 ip saddr @knock2_v4 add @knock_open_v4 { ip saddr timeout 30m } drop\n  tcp dport 42113 ip6 saddr @knock2_v6 add @knock_open_v6 { ip6 saddr timeout 30m } drop\n\n  # SSH open only for IPs that completed knocking or are allowlisted\n  tcp dport 22 ip saddr @knock_open_v4 accept\n  tcp dport 22 ip6 saddr @knock_open_v6 accept\n\n  # You can still keep per\u2011IP meters on top, if you like\n  tcp dport 22 ct state new meter ssh_v4 { ip saddr limit rate 20\/minute burst 20 } accept\n  tcp dport 22 ct state new meter ssh_v6 { ip6 saddr limit rate 20\/minute burst 20 } accept\n\n  # ... rest of input chain ...\n}<\/code><\/pre>\n<p>A couple of real-world notes. First, the knock rules deliberately <strong>drop<\/strong> the knock packets; that\u2019s the point. They don\u2019t reveal anything. Second, you don\u2019t have to remove addresses from earlier stages; timeouts will expire them quickly. Third, write down your chosen ports somewhere safe. I\u2019ve forgotten mine once and spent midnight guessing\u2014a fun game I don\u2019t recommend.<\/p>\n<p>Is port knocking perfect? Of course not. But used lightly and paired with strong SSH auth, it\u2019s a surprisingly effective layer that makes scans much less noisy. It\u2019s also a nice way to keep SSH effectively \u201cclosed\u201d most of the time on lab servers.<\/p>\n<h2 id=\"section-5\"><span id=\"IPv6_Rules_That_Just_Work\">IPv6 Rules That Just Work<\/span><\/h2>\n<p>When I first enabled IPv6 on a VPS years ago, I tried to treat it exactly like IPv4. Every five minutes I\u2019d break something subtle and wonder why pings failed or neighbors didn\u2019t resolve. The truth is, IPv6 wants to be understood a little. Once you get the rhythm, it\u2019s actually friendlier.<\/p>\n<h3><span id=\"Let_ICMPv6_breathe\">Let ICMPv6 breathe<\/span><\/h3>\n<p>IPv6 leans heavily on ICMPv6 for neighbor discovery, router advertisements, and path MTU discovery. Blocking these is like trying to drive with your eyes closed. The baseline we wrote earlier already allows the common types: echo request\/reply, neighbor solicit\/advertise, router solicit\/advertise, packet-too-big, time-exceeded, and parameter-problem. That\u2019s a healthy core.<\/p>\n<p>If you\u2019re curious and like official documentation, the <a href=\"https:\/\/wiki.nftables.org\/wiki-nftables\/index.php\/Main_Page\" rel=\"nofollow noopener\" target=\"_blank\">official nftables wiki<\/a> and the <a href=\"https:\/\/man7.org\/linux\/man-pages\/man8\/nft.8.html\" rel=\"nofollow noopener\" target=\"_blank\">nft(8) man page<\/a> are excellent to keep handy. I often keep a tab open while I tweak rules.<\/p>\n<h3><span id=\"Dont_forget_DNS_over_IPv6\">Don\u2019t forget DNS over IPv6<\/span><\/h3>\n<p>If your VPS runs a resolver or you need outbound DNS over IPv6, you don\u2019t need a special firewall dance\u2014our outbound policy is accept. But if you host a public DNS service on v6, remember to open UDP\/53 (and TCP\/53 if needed) with the same per\u2011IP limits you\u2019d use for v4.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># Example for a public resolver on IPv6\nudp dport 53 meter dns_v6 { ip6 saddr limit rate 50\/second burst 100 } accept\n# For large zone transfers or DNS over TCP\n# tcp dport 53 meter dns_tcp_v6 { ip6 saddr limit rate 10\/second burst 20 } accept<\/code><\/pre>\n<h3><span id=\"Services_dualstacked_by_default\">Services dual\u2011stacked by default<\/span><\/h3>\n<p>When you expose a service, remember that many modern clients will try IPv6 first if you publish AAAA records. That\u2019s a win. Just be sure you\u2019ve permitted the corresponding port on IPv6. I\u2019ve helped teams debug \u201cmy site is down on mobile data\u201d only to find out IPv6 was half\u2011open. Consistency is everything.<\/p>\n<h2 id=\"section-6\"><span id=\"Save_It_Test_It_and_Sleep_Better\">Save It, Test It, and Sleep Better<\/span><\/h2>\n<p>Firewall changes are like repairing a plane in flight. You want a plan. In my experience, a few habits make this predictable and calm.<\/p>\n<h3><span id=\"Use_a_working_file_and_atomic_swaps\">Use a working file and atomic swaps<\/span><\/h3>\n<p>I keep two files: <strong>\/etc\/nftables.conf<\/strong> (live) and <strong>\/etc\/nftables.work<\/strong> (staging). I edit the work file, validate it with <code>nft -f \/etc\/nftables.work<\/code> (which loads the rules into the kernel immediately), then if all\u2019s well I replace the persistent config and restart the service:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">sudo nft -f \/etc\/nftables.work\nsudo cp \/etc\/nftables.work \/etc\/nftables.conf\nsudo systemctl restart nftables<\/code><\/pre>\n<p>That way, a reboot won\u2019t surprise you. And if you prefer a dry run without applying, you can validate syntax with:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">sudo nft -c -f \/etc\/nftables.work   # -c = check, don't apply<\/code><\/pre>\n<h3><span id=\"Log_a_little_trace_when_needed\">Log a little, trace when needed<\/span><\/h3>\n<p>If something doesn\u2019t behave, start small. Add a temporary log rule right before a drop. I keep logs rate\u2011limited to avoid noise. For deeper mysteries, nftables has a built\u2011in tracer that\u2019s shockingly helpful:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># Enable tracing on a specific packet flow by setting nftrace=1 via iptables\/nft\n# Or run a live monitor\nsudo nft monitor trace<\/code><\/pre>\n<p>This prints the chain walk for matching packets, line by line. The first time I used it, I found a rule in the wrong chain\u2014one of those forehead\u2011slap moments that would\u2019ve taken an hour otherwise.<\/p>\n<h3><span id=\"Backups_are_boring_Perfect\">Backups are boring. Perfect.<\/span><\/h3>\n<p>Every time you\u2019re happy, save a snapshot:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">sudo nft list ruleset &gt; ~\/nftables-$(date +%F-%H%M).conf<\/code><\/pre>\n<p>It takes a second and gives you a time machine when you\u2019re experimenting. I keep a few of these in my home directory and rotate them occasionally.<\/p>\n<h3><span id=\"Persistence_on_boot\">Persistence on boot<\/span><\/h3>\n<p>Double\u2011check the boot path once and you\u2019re done. With the service enabled, <strong>\/etc\/nftables.conf<\/strong> will be loaded at startup. If you ever suspect the service didn\u2019t load properly, a quick restart is usually enough:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">sudo systemctl status nftables\nsudo systemctl restart nftables<\/code><\/pre>\n<p>If you want to dig deeper into nftables feature details, the official resources are consistently clear and up to date. I\u2019ve linked them above, and they\u2019re worth bookmarking.<\/p>\n<h2 id=\"section-7\"><span id=\"Putting_It_All_Together_A_Practical_Example\">Putting It All Together: A Practical Example<\/span><\/h2>\n<p>To make this concrete, here\u2019s a stitched\u2011together ruleset that brings the pieces into one file. You will, of course, adapt ports and limits to your world. But this will give you a good starting point that\u2019s both IPv4 and IPv6 aware, with rate limiting and an optional knock flow for SSH.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">#!\/usr\/sbin\/nft -f\nflush ruleset\n\ntable inet filter {\n  counter ssh_hits {}\n  counter web_hits {}\n\n  # Optional allowlists\n  set allowed_ssh_v4 { type ipv4_addr; flags interval; }\n  set allowed_ssh_v6 { type ipv6_addr; flags interval; }\n\n  # Port knocking sets (optional)\n  set knock1_v4 { type ipv4_addr; flags timeout; timeout 10s; }\n  set knock2_v4 { type ipv4_addr; flags timeout; timeout 10s; }\n  set knock_open_v4 { type ipv4_addr; flags timeout; timeout 30m; }\n  set knock1_v6 { type ipv6_addr; flags timeout; timeout 10s; }\n  set knock2_v6 { type ipv6_addr; flags timeout; timeout 10s; }\n  set knock_open_v6 { type ipv6_addr; flags timeout; timeout 30m; }\n\n  chain input {\n    type filter hook input priority 0; policy drop;\n\n    iif lo accept\n    ct state invalid drop\n    ct state established,related accept\n\n    # ICMP (v4)\n    ip protocol icmp icmp type { echo-request, echo-reply, time-exceeded, destination-unreachable } accept\n\n    # ICMPv6 (must-have for IPv6 to work properly)\n    ip6 nexthdr icmpv6 icmpv6 type {\n      echo-request, echo-reply,\n      nd-neighbor-solicit, nd-neighbor-advert,\n      nd-router-solicit, nd-router-advert,\n      packet-too-big, time-exceeded, parameter-problem\n    } accept\n\n    # --- OPTIONAL PORT KNOCKING (three-step) ---\n    # Choose your knock ports (example: 42111-42113)\n    tcp dport 42111 add @knock1_v4 { ip saddr timeout 10s } drop\n    tcp dport 42111 add @knock1_v6 { ip6 saddr timeout 10s } drop\n\n    tcp dport 42112 ip saddr @knock1_v4 add @knock2_v4 { ip saddr timeout 10s } drop\n    tcp dport 42112 ip6 saddr @knock1_v6 add @knock2_v6 { ip6 saddr timeout 10s } drop\n\n    tcp dport 42113 ip saddr @knock2_v4 add @knock_open_v4 { ip saddr timeout 30m } drop\n    tcp dport 42113 ip6 saddr @knock2_v6 add @knock_open_v6 { ip6 saddr timeout 30m } drop\n\n    # SSH allowlist or opened-by-knock\n    ip saddr @allowed_ssh_v4 tcp dport 22 accept\n    ip6 saddr @allowed_ssh_v6 tcp dport 22 accept\n\n    tcp dport 22 ip saddr @knock_open_v4 accept\n    tcp dport 22 ip6 saddr @knock_open_v6 accept\n\n    # Per-IP SSH rate limits (fallback if you keep SSH public)\n    tcp dport 22 ct state new meter ssh_v4 { ip saddr limit rate 20\/minute burst 20 } counter name ssh_hits accept\n    tcp dport 22 ct state new meter ssh_v6 { ip6 saddr limit rate 20\/minute burst 20 } counter name ssh_hits accept\n\n    # Web services (HTTP\/HTTPS)\n    tcp dport {80, 443} ct state new meter web_v4 { ip saddr limit rate 200\/second burst 400 } counter name web_hits accept\n    tcp dport {80, 443} ct state new meter web_v6 { ip6 saddr limit rate 200\/second burst 400 } counter name web_hits accept\n\n    # Optional: DNS server\n    # udp dport 53 meter dns_v4 { ip saddr limit rate 50\/second burst 100 } accept\n    # udp dport 53 meter dns_v6 { ip6 saddr limit rate 50\/second burst 100 } accept\n\n    # Logging of overflows (gentle)\n    # limit rate 5\/second over tcp dport 22 log prefix &quot;nft-ssh-overflow &quot; flags all level info\n\n    # Default catch-all: drop\n  }\n\n  chain forward {\n    type filter hook forward priority 0; policy drop;\n  }\n\n  chain output {\n    type filter hook output priority 0; policy accept;\n  }\n}<\/code><\/pre>\n<p>That\u2019s a tidy, readable base. The chains tell a story: we accept what\u2019s clearly safe, keep new connections under control, allow the services we mean to expose, and otherwise remain politely closed. A simple life.<\/p>\n<h2 id=\"section-8\"><span id=\"WrapUp_Calm_Defenses_Happy_Services\">Wrap\u2011Up: Calm Defenses, Happy Services<\/span><\/h2>\n<p>If there\u2019s a theme to this whole cookbook, it\u2019s this: your firewall should feel boring in the best way. It should quietly keep strangers at bay, treat your real users with respect, and make debugging easier, not harder. Nftables has grown into a tool that lets us do all that without feeling like we\u2019re juggling chainsaws.<\/p>\n<p>We covered a sane baseline with default deny on inbound, friendly ICMP rules (especially for IPv6), and simple counters so you can peek at what\u2019s happening. We added per\u2011IP rate limits for SSH and web traffic that protect you without getting in your way. We sprinkled in a no\u2011drama port knocking flow for teams and hobby setups where it fits. And we wrapped with a few comfort habits: working files, atomic swaps, light logging, and trace tools that tell you exactly what nftables is doing.<\/p>\n<p>My parting advice: start small, test with a second session open, and keep a couple of backups around. Once you have a baseline you trust, the rest becomes a gentle rhythm of tiny changes. If you\u2019re hardening SSH, don\u2019t forget to pair the firewall with strong keys and modern auth\u2014your future self will thank you. Hope this was helpful! If you try these patterns on your VPS and run into something odd, keep those logs handy, and you\u2019ll get to the bottom of it quickly. See you in the next post.<\/p>\n<h3><span id=\"Useful_references_I_keep_bookmarked\">Useful references I keep bookmarked<\/span><\/h3>\n<p>When I\u2019m tweaking nftables, I often keep the <a href=\"https:\/\/wiki.nftables.org\/wiki-nftables\/index.php\/Main_Page\" rel=\"nofollow noopener\" target=\"_blank\">official nftables wiki<\/a> and the <a href=\"https:\/\/man7.org\/linux\/man-pages\/man8\/nft.8.html\" rel=\"nofollow noopener\" target=\"_blank\">nft(8) man page<\/a> open in a tab. Clear, current, and to the point.<\/p>\n<\/div>","protected":false},"excerpt":{"rendered":"<p>So there I was, a sleepy Sunday morning, coffee in hand, poking through a small VPS I use for side projects. A handful of weird SSH connection attempts popped up in the logs, then a few aggressive HTTP requests with random query strings\u2014clearly not from my friends. You\u2019ve probably had that moment too: you open [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":1580,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[26],"tags":[],"class_list":["post-1579","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-teknoloji"],"_links":{"self":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/1579","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/comments?post=1579"}],"version-history":[{"count":0,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/1579\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media\/1580"}],"wp:attachment":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media?parent=1579"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/categories?post=1579"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/tags?post=1579"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}