{"id":151,"date":"2026-09-14T11:59:17","date_gmt":"2026-09-14T02:59:17","guid":{"rendered":"https:\/\/haibara.us\/?p=151"},"modified":"2026-09-14T11:59:17","modified_gmt":"2026-09-14T02:59:17","slug":"protecting-ssh-https-and-icmp-with-nftables-dynamic-dns-and-cron","status":"publish","type":"post","link":"https:\/\/haibara.us\/?p=151","title":{"rendered":"Protecting SSH, HTTPS, and ICMP with nftables, Dynamic DNS, and Cron"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">When running a Linux server directly on the Internet, SSH and other management ports are constantly scanned. One simple way to reduce exposure is to allow access only from trusted IP addresses.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This guide shows how to:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Protect a custom SSH port with an IP allowlist<\/li>\n\n\n\n<li>Restrict TCP\/UDP port 443 to a trusted source<\/li>\n\n\n\n<li>Restrict ICMP ping requests without completely breaking ICMP<\/li>\n\n\n\n<li>Resolve a trusted hostname dynamically with <code>dig<\/code><\/li>\n\n\n\n<li>Automatically refresh the firewall when the hostname IP changes<\/li>\n\n\n\n<li>Run the update script using <code>crontab<\/code><\/li>\n\n\n\n<li>Avoid exposing personal or production IP addresses in a public script<\/li>\n<\/ul>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>Important:<\/strong> All IP addresses in this article are documentation examples. They are not real server addresses.<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">For example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>203.0.113.10\n198.51.100.20\n192.0.2.30\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">These address ranges are intended for documentation and examples.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">1. The Basic Idea<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Imagine that I have a trusted hostname:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>trusted.example.com\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The IP behind this hostname may change.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Instead of manually editing the firewall every time the IP changes, the server can periodically resolve the hostname:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>dig +short A trusted.example.com\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The returned IP is then inserted into an nftables ruleset.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The workflow looks like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>trusted.example.com\n        |\n        v\n      DNS\n        |\n        v\n203.0.113.x\n        |\n        v\nShell Script\n        |\n        v\n    nftables\n        |\n        v\nSSH \/ HTTPS \/ ICMP filtering\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A cron job can run the script every few minutes so that the firewall follows DNS changes automatically.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">2. Understanding the Original Script<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">A simplified version of the original approach looks like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#!\/bin\/bash\n\nexport PATH=$PATH:\/usr\/sbin:\/usr\/bin:\/sbin:\/bin\n\ndeclare -A HOSTS=(\n    &#91;trusted]=\"trusted.example.com\"\n)\n\nfor key in \"${!HOSTS&#91;@]}\"; do\n    ip=$(dig +short \"${HOSTS&#91;$key]}\" \\\n        | grep -Eo '^&#91;0-9.]+$' \\\n        | head -n1)\n\n    if &#91;&#91; -z \"$ip\" ]]; then\n        echo \"Unable to resolve ${HOSTS&#91;$key]}\" &gt;&amp;2\n        exit 1\n    fi\n\n    echo \"${HOSTS&#91;$key]} -&gt; $ip\"\ndone\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>PATH<\/code> line is especially useful when the script runs from cron because cron usually has a smaller environment than an interactive shell.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The associative array allows multiple hostnames to be added later:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>declare -A HOSTS=(\n    &#91;office]=\"office.example.com\"\n    &#91;home]=\"home.example.com\"\n)\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>dig<\/code> command resolves the hostname:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>dig +short A trusted.example.com\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The script then extracts an IPv4 address.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">3. One Important Problem: Do Not Flush the Firewall Before DNS Works<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">A dangerous pattern is:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>nft flush ruleset\n\nip=$(dig ...)\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If DNS fails after the firewall has already been flushed, the server temporarily has no nftables protection.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A much safer order is:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>1. Resolve DNS\n2. Validate the result\n3. Build the new firewall\n4. Replace the old firewall\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">So DNS should always be checked first.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">4. Protecting the SSH Port<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Suppose SSH is running on TCP port:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>122\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This is only an example. Changing the SSH port reduces automated noise, but it should <strong>not<\/strong> be considered a replacement for authentication security.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The firewall can allow SSH only from trusted addresses:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ip saddr @trusted_v4 tcp dport 122 accept\ntcp dport 122 drop\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The order matters.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">First:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ip saddr @trusted_v4 tcp dport 122 accept\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">allows trusted addresses.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Then:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>tcp dport 122 drop\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">blocks everybody else.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The result is effectively:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Trusted IP -&gt; TCP 122 -&gt; ACCEPT\nOther IP   -&gt; TCP 122 -&gt; DROP\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">SSH normally does not need UDP<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Standard OpenSSH uses TCP.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Therefore this is normally unnecessary:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>udp dport 122 accept\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Unless you are actually running another UDP-based service on port 122, there is no reason to expose it.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">5. Protecting Port 443<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">The same approach can be used for HTTPS:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ip saddr @trusted_v4 tcp dport 443 accept\ntcp dport 443 drop\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If HTTP\/3 or QUIC is being used, UDP 443 may also be necessary:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ip saddr @trusted_v4 udp dport 443 accept\nudp dport 443 drop\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If HTTP\/3 is not used, UDP 443 can simply remain blocked.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Be careful with this rule on a public website.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you do:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>tcp dport 443 drop\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">after allowing only a private trusted IP, ordinary visitors will no longer be able to access HTTPS.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This configuration therefore makes sense for private services, management interfaces, VPN entry points, reverse-proxy links, or other restricted services.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">6. ICMP Should Not Be Completely Blocked<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">A common firewall rule is:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ip protocol icmp drop\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">I generally avoid this.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">ICMP is not only used by <code>ping<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It also provides important network error messages and troubleshooting information.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Instead, I prefer blocking or restricting only ICMP echo requests while allowing important ICMP error messages.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ip protocol icmp icmp type {\n    destination-unreachable,\n    time-exceeded,\n    parameter-problem\n} accept\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then allow ping only from trusted addresses:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ip saddr @trusted_v4 icmp type echo-request \\\n    limit rate 5\/second burst 10 packets accept\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Finally block other IPv4 ping requests:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>icmp type echo-request drop\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This produces behavior similar to:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Trusted IP -&gt; Ping server -&gt; Allowed\nUnknown IP -&gt; Ping server -&gt; Dropped\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">while not blindly dropping every type of ICMP packet.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">7. Do Not Blindly Block ICMPv6<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">IPv6 is different.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">ICMPv6 is an important part of IPv6 operation and is used for functions such as neighbor discovery.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Therefore this kind of rule is a bad idea:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Drop every ICMPv6 packet\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If the server uses IPv6, create proper IPv6 and ICMPv6 policies separately.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The example in this article focuses primarily on IPv4 source allowlisting.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">8. A Safer Reference Script<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Here is the version I would use as a reusable template.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Create:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/usr\/local\/sbin\/dynamic-nftables.sh\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">with the following contents:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#!\/usr\/bin\/env bash\n\nset -euo pipefail\n\nexport PATH=\/usr\/local\/sbin:\/usr\/local\/bin:\/usr\/sbin:\/usr\/bin:\/sbin:\/bin\n\n# ------------------------------------------------------------\n# Configuration\n# ------------------------------------------------------------\n\nTRUSTED_HOST=\"trusted.example.com\"\n\n# Example custom SSH port\nSSH_PORT=\"122\"\n\n# HTTPS port\nHTTPS_PORT=\"443\"\n\n# Optional emergency\/static administrator IP.\n#\n# 203.0.113.10 is a documentation-only example address.\n# Replace it with your own trusted administrator IP.\nSTATIC_ADMIN_IP=\"203.0.113.10\"\n\n# ------------------------------------------------------------\n# Safety checks\n# ------------------------------------------------------------\n\nif &#91;&#91; $EUID -ne 0 ]]; then\n    echo \"This script must be run as root.\" &gt;&amp;2\n    exit 1\nfi\n\ncommand -v nft &gt;\/dev\/null 2&gt;&amp;1 || {\n    echo \"nft command not found.\" &gt;&amp;2\n    exit 1\n}\n\ncommand -v dig &gt;\/dev\/null 2&gt;&amp;1 || {\n    echo \"dig command not found.\" &gt;&amp;2\n    exit 1\n}\n\n# ------------------------------------------------------------\n# Resolve the trusted hostname BEFORE touching the firewall\n# ------------------------------------------------------------\n\nTRUSTED_IP=$(\n    dig +short A \"$TRUSTED_HOST\" |\n    grep -E '^&#91;0-9]+\\.&#91;0-9]+\\.&#91;0-9]+\\.&#91;0-9]+$' |\n    head -n1\n)\n\nif &#91;&#91; -z \"$TRUSTED_IP\" ]]; then\n    echo \"ERROR: Unable to resolve $TRUSTED_HOST\" &gt;&amp;2\n    exit 1\nfi\n\necho \"$TRUSTED_HOST -&gt; $TRUSTED_IP\"\n\n# ------------------------------------------------------------\n# Apply nftables rules\n# ------------------------------------------------------------\n\nnft -f - &lt;&lt;EOF\nflush ruleset\n\ntable inet filter {\n\n    set trusted_v4 {\n        type ipv4_addr\n        elements = {\n            $TRUSTED_IP,\n            $STATIC_ADMIN_IP\n        }\n    }\n\n    chain input {\n        type filter hook input priority 0;\n        policy accept;\n\n        # Keep existing connections working\n        ct state established,related accept\n\n        # Always allow localhost\n        iifname \"lo\" accept\n\n        # ----------------------------------------------------\n        # HTTPS\n        # ----------------------------------------------------\n\n        ip saddr @trusted_v4 tcp dport $HTTPS_PORT accept\n        ip saddr @trusted_v4 udp dport $HTTPS_PORT accept\n\n        tcp dport $HTTPS_PORT drop\n        udp dport $HTTPS_PORT drop\n\n        # ----------------------------------------------------\n        # SSH\n        # ----------------------------------------------------\n\n        ip saddr @trusted_v4 tcp dport $SSH_PORT accept\n\n        tcp dport $SSH_PORT drop\n\n        # ----------------------------------------------------\n        # ICMP\n        # ----------------------------------------------------\n\n        # Keep useful IPv4 ICMP error messages\n        ip protocol icmp icmp type {\n            destination-unreachable,\n            time-exceeded,\n            parameter-problem\n        } accept\n\n        # Allow ping only from trusted addresses\n        ip saddr @trusted_v4 icmp type echo-request \\\n            limit rate 5\/second burst 10 packets accept\n\n        # Block other ping requests\n        icmp type echo-request drop\n    }\n\n    chain forward {\n        type filter hook forward priority 0;\n        policy accept;\n    }\n\n    chain output {\n        type filter hook output priority 0;\n        policy accept;\n    }\n}\nEOF\n\necho\necho \"Current nftables rules:\"\nnft list ruleset\n\necho\necho \"Firewall successfully updated.\"\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">9. Why I Use an nftables Set<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Instead of repeating rules such as:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ip saddr 203.0.113.10 ...\nip saddr 198.51.100.20 ...\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">I create an nftables set:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>set trusted_v4 {\n    type ipv4_addr\n    elements = {\n        203.0.113.10,\n        198.51.100.20\n    }\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The rules can then simply reference:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ip saddr @trusted_v4\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ip saddr @trusted_v4 tcp dport 122 accept\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This is easier to read and much easier to maintain.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">10. Why <code>ct state established,related<\/code> Is Useful<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">This rule is important:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ct state established,related accept\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">It allows packets belonging to an existing connection.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For example, imagine that the trusted hostname originally resolves to:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>203.0.113.20\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You establish an SSH connection.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Later the hostname changes to:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>203.0.113.21\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">After the firewall refreshes, new connections from the old address can be rejected, but an already established connection can continue normally.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is especially helpful when updating a firewall remotely.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">11. Why the Input Policy Is Still <code>accept<\/code><\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">The example uses:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>policy accept;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">instead of:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>policy drop;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This is intentional.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This tutorial focuses on protecting specific sensitive services:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SSH\nHTTPS\nICMP echo requests\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">It is not intended to be a complete host firewall.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A server with:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>policy drop;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">needs explicit rules for every service that must remain reachable.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That is a stronger security model, but blindly changing an existing server to a default-drop policy can easily break services or lock the administrator out.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Once all required services have been identified, moving to:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>policy drop;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">can be a good next step.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">12. Why Some <code>output<\/code> Rules Do Nothing<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Consider this configuration:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>chain output {\n    type filter hook output priority 0;\n    policy accept;\n}\n\nip daddr 203.0.113.10 accept\nip daddr 198.51.100.10 accept\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Because the default output policy is already:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>policy accept;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">those additional <code>accept<\/code> rules do not restrict anything.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Traffic to every other destination is also accepted.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To implement true outbound filtering, the policy would have to become something such as:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>policy drop;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">followed by explicit rules for DNS, package repositories, NTP, HTTPS, monitoring systems, and other required services.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That is a separate topic and should be configured carefully because overly aggressive outbound filtering can easily break a server.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">13. Install the Required Packages<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">On Debian or Ubuntu:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt update\nsudo apt install nftables dnsutils\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>dnsutils<\/code> provides the <code>dig<\/code> command.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">On AlmaLinux, Rocky Linux, RHEL, or similar systems:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo dnf install nftables bind-utils\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">14. Save the Script<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Create the file:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo nano \/usr\/local\/sbin\/dynamic-nftables.sh\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Paste the script into it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Then make it executable:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo chmod 700 \/usr\/local\/sbin\/dynamic-nftables.sh\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The permission:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>700\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">means that only root can read, modify, or execute the script.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You can verify it with:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ls -l \/usr\/local\/sbin\/dynamic-nftables.sh\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">15. Check the Shell Script Before Running It<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Before modifying a firewall, check the Bash syntax:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo bash -n \/usr\/local\/sbin\/dynamic-nftables.sh\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If there is no output, the shell syntax is valid.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Then execute it manually:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo \/usr\/local\/sbin\/dynamic-nftables.sh\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Finally inspect the rules:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo nft list ruleset\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">16. Very Important SSH Safety Procedure<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">When modifying firewall rules over SSH, I recommend keeping the existing SSH connection open.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Do not immediately close it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">After applying the firewall:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Keep the current SSH session connected.<\/li>\n\n\n\n<li>Open a second terminal.<\/li>\n\n\n\n<li>Try connecting to SSH again.<\/li>\n\n\n\n<li>Verify that your trusted address works.<\/li>\n\n\n\n<li>Verify the firewall rules.<\/li>\n\n\n\n<li>Only then close the original SSH session.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">It is also a good idea to have access to the VPS provider&#8217;s web console, serial console, KVM, or recovery console before experimenting with remote firewall rules.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A small firewall mistake can otherwise turn into a very inconvenient afternoon.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">17. Test SSH<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">From an authorized network:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ssh -p 122 user@your-server\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The connection should succeed.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">From a source IP that is not in the trusted set, the connection should fail.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You can verify the SSH rule with:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo nft list chain inet filter input\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">18. Test ICMP<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">From the trusted address:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ping your-server\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">It should work.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">From another Internet connection:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ping your-server\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The server should not respond to the ICMP echo requests.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This does not mean the server is invisible.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A firewall should never be treated as a method for making a server completely undetectable.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">19. Automatically Update the Firewall with Cron<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">The main reason for resolving a hostname is that its IP may change.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Instead of manually running:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo \/usr\/local\/sbin\/dynamic-nftables.sh\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">every time the DNS record changes, we can use cron.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Because firewall modification requires root privileges, edit the root crontab:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo crontab -e\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Add:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>*\/5 * * * * \/usr\/bin\/flock -n \/run\/dynamic-nftables.lock \/usr\/local\/sbin\/dynamic-nftables.sh &gt;&gt; \/var\/log\/dynamic-nftables.log 2&gt;&amp;1\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This runs the firewall update every five minutes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The schedule:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>*\/5 * * * *\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">means:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Every 5 minutes\nEvery hour\nEvery day\nEvery month\nEvery day of the week\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">20. Why Use <code>flock<\/code>?<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">I use:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>flock -n \/run\/dynamic-nftables.lock\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">to prevent two copies of the script from running at the same time.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Normally the script should finish very quickly.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">However, using a lock is still a simple way to avoid overlapping executions caused by DNS delays, system load, or accidental manual execution.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">21. Save Cron Output to a Log<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">This part:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&gt;&gt; \/var\/log\/dynamic-nftables.log 2&gt;&amp;1\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">stores both normal output and errors in:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/var\/log\/dynamic-nftables.log\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You can inspect it with:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo tail -f \/var\/log\/dynamic-nftables.log\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A successful update might look like:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>trusted.example.com -&gt; 203.0.113.25\n\nCurrent nftables rules:\n...\n\nFirewall successfully updated.\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If DNS fails, you may instead see:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ERROR: Unable to resolve trusted.example.com\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The important part is that DNS was checked <strong>before<\/strong> the existing firewall was replaced.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">22. Apply the Firewall After Reboot<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Cron can also run the script when the machine boots.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Edit:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo crontab -e\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">and add:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>@reboot sleep 15 &amp;&amp; \/usr\/bin\/flock -n \/run\/dynamic-nftables.lock \/usr\/local\/sbin\/dynamic-nftables.sh &gt;&gt; \/var\/log\/dynamic-nftables.log 2&gt;&amp;1\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">I use a short delay because the network and DNS resolver may not be completely ready immediately after the operating system starts.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The complete root crontab might therefore contain:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>@reboot sleep 15 &amp;&amp; \/usr\/bin\/flock -n \/run\/dynamic-nftables.lock \/usr\/local\/sbin\/dynamic-nftables.sh &gt;&gt; \/var\/log\/dynamic-nftables.log 2&gt;&amp;1\n\n*\/5 * * * * \/usr\/bin\/flock -n \/run\/dynamic-nftables.lock \/usr\/local\/sbin\/dynamic-nftables.sh &gt;&gt; \/var\/log\/dynamic-nftables.log 2&gt;&amp;1\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If DNS is unavailable during the <code>@reboot<\/code> execution, the scheduled five-minute job can try again later.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">23. Verify That Cron Is Installed<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Depending on the distribution, the service may be called <code>cron<\/code> or <code>crond<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">On Debian\/Ubuntu:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>systemctl status cron\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">On RHEL-compatible distributions:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>systemctl status crond\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">To enable it if necessary:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Debian\/Ubuntu:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl enable --now cron\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">RHEL-compatible systems:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl enable --now crond\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">24. Why Not Put a Domain Name Directly in the Firewall?<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">It may be tempting to write something similar to:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Allow trusted.example.com\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">and expect the firewall to automatically follow DNS changes forever.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That is not how this design works.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The hostname is resolved when the rules are created. If the DNS record later changes, the existing firewall rule does not magically update itself.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That is why the process is:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>DNS hostname\n    \u2193\ndig\n    \u2193\nCurrent IP\n    \u2193\nnftables\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">and cron repeats the process periodically.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">25. What Happens When the Trusted IP Changes?<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Assume the hostname initially resolves to:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>203.0.113.20\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The firewall contains:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>203.0.113.20\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Later DNS changes to:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>203.0.113.21\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">During the next cron run:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>dig +short A trusted.example.com\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">returns:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>203.0.113.21\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The script rebuilds the trusted set and the new firewall contains:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>203.0.113.21\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">New SSH connections from the previous address can then be rejected.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">26. A Limitation of <code>head -n1<\/code><\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">This command:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>dig +short A \"$TRUSTED_HOST\" |\ngrep ... |\nhead -n1\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">uses only the first IPv4 address returned by DNS.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That is perfectly fine if the hostname normally has one address.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">However, services using:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>multiple A records<\/li>\n\n\n\n<li>round-robin DNS<\/li>\n\n\n\n<li>load balancing<\/li>\n\n\n\n<li>CDN addresses<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">may return several IPs.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For those environments, the script should collect every returned address and add all of them to an nftables set rather than selecting only the first result.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">27. Example Firewall Logic<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">The final policy can be visualized like this:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"> <code>                        Internet\n                            |\n                            v\n                     +---------------+\n                     |   nftables    |\n                     +---------------+\n                            |\n          +-----------------+-----------------+\n          |                 |                 |\n          v                 v                 v\n       SSH 122           HTTPS 443           ICMP\n          |                 |                 |\n    Trusted IP?       Trusted IP?       Echo Request?\n       \/    \\             \/    \\             |\n     Yes     No         Yes     No       Trusted IP?\n      |       |          |       |          \/    \\\n   ACCEPT    DROP      ACCEPT   DROP      Yes     No\n                                             |      |\n                                          ACCEPT   DROP\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This dramatically reduces exposure of the protected services.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">28. Security Improvements Compared with the Original Version<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">There are several changes I recommend compared with a simple firewall script.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Resolve DNS before flushing rules<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Bad:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>nft flush ruleset\ndig ...\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Better:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>dig ...\nvalidate IP\nnft ...\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A temporary DNS failure should not remove your firewall.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Use root consistently<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Instead of:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo nft flush ruleset\nnft add ...\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">run the entire script as root:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo \/usr\/local\/sbin\/dynamic-nftables.sh\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The script itself checks:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>if &#91;&#91; $EUID -ne 0 ]]; then\n    exit 1\nfi\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Do not open UDP for SSH<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">OpenSSH normally needs only:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>TCP\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">not UDP.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Do not drop all ICMP blindly<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Restrict ping while keeping useful network error messages.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Use nftables sets<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Instead of repeating the same source address in many rules:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ip saddr @trusted_v4\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">is easier to maintain.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Use <code>flock<\/code><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This prevents simultaneous cron executions.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Keep logs<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Cron output is stored in:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/var\/log\/dynamic-nftables.log\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">which makes troubleshooting much easier.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">29. Additional SSH Hardening<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Firewall restrictions should be combined with proper SSH configuration.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For an Internet-facing server I also recommend:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SSH public-key authentication\nDisable direct root login\nDisable password login when possible\nKeep OpenSSH updated\nUse strong private-key protection\nLimit which users can log in\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Typical <code>\/etc\/ssh\/sshd_config<\/code> options may include:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>PermitRootLogin no\nPasswordAuthentication no\nPubkeyAuthentication yes\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">After changing SSH configuration, validate it before restarting:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo sshd -t\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If the test succeeds, reload the SSH service according to your distribution.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Do not disable password authentication until you have confirmed that public-key authentication works correctly.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">30. Do Not Rely on a Non-Standard SSH Port Alone<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Moving SSH from:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>22\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">to something like:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>122\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">can reduce automated login attempts and log noise.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">However, a port scanner can still discover it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Think of a custom SSH port as reducing background noise, not providing authentication or serious access control.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The real protection comes from combining:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Firewall allowlisting\n+\nSSH keys\n+\nNo root login\n+\nNo password authentication\n+\nSystem updates\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">31. Useful Commands<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Display the complete ruleset:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo nft list ruleset\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Display only the input chain:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo nft list chain inet filter input\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Run the script manually:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo \/usr\/local\/sbin\/dynamic-nftables.sh\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Check script syntax:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo bash -n \/usr\/local\/sbin\/dynamic-nftables.sh\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Check the resolved address:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>dig +short A trusted.example.com\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Watch the cron log:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo tail -f \/var\/log\/dynamic-nftables.log\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Display root cron jobs:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo crontab -l\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">32. Final Notes<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">This approach is useful when access to an Internet-facing service should follow a trusted dynamic IP address.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The overall design is simple:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Dynamic DNS\n     \u2193\nResolve with dig\n     \u2193\nValidate the IP\n     \u2193\nGenerate nftables rules\n     \u2193\nAllow trusted source\n     \u2193\nBlock untrusted access\n     \u2193\nRepeat with cron\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The most important lesson is not the exact port numbers.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It is the firewall strategy:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Allow what is trusted first.\nBlock the protected service second.\nResolve dynamic addresses before modifying the firewall.\nDo not expose unnecessary protocols.\nDo not blindly block essential ICMP traffic.\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">With a small Bash script, nftables, and cron, a Linux server can automatically maintain a source-IP allowlist without manually updating firewall rules every time a trusted dynamic IP changes.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Disclaimer<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This article is provided as a reference configuration.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Firewall requirements vary between servers, distributions, hosting providers, Docker environments, VPN configurations, IPv6 deployments, and network architectures.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Always test firewall changes while retaining console or recovery access to the server.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The IP addresses used in this article are documentation examples and should be replaced with addresses appropriate for your own environment.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>When running a Linux server directly on the Internet, SSH and other management ports are constantly scanned. One simple way to reduce exposure is to allow access only from trusted IP addresses. This guide shows how to: Important: All IP addresses in this article are documentation examples. They are not real server addresses. For example:&#46;&#46;&#46;<\/p>\n","protected":false},"author":1,"featured_media":152,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-151","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-literature"],"_links":{"self":[{"href":"https:\/\/haibara.us\/index.php?rest_route=\/wp\/v2\/posts\/151","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/haibara.us\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/haibara.us\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/haibara.us\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/haibara.us\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=151"}],"version-history":[{"count":1,"href":"https:\/\/haibara.us\/index.php?rest_route=\/wp\/v2\/posts\/151\/revisions"}],"predecessor-version":[{"id":153,"href":"https:\/\/haibara.us\/index.php?rest_route=\/wp\/v2\/posts\/151\/revisions\/153"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/haibara.us\/index.php?rest_route=\/wp\/v2\/media\/152"}],"wp:attachment":[{"href":"https:\/\/haibara.us\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=151"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/haibara.us\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=151"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/haibara.us\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=151"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}