{"id":3892,"date":"2026-09-13T11:54:56","date_gmt":"2026-09-13T16:54:56","guid":{"rendered":"https:\/\/osric.com\/chris\/accidental-developer\/?p=3892"},"modified":"2026-09-13T14:03:45","modified_gmt":"2026-09-13T19:03:45","slug":"using-nftables-to-allow-ingress-traffic-from-internet-to-intranet-resources","status":"publish","type":"post","link":"https:\/\/osric.com\/chris\/accidental-developer\/2026\/09\/using-nftables-to-allow-ingress-traffic-from-internet-to-intranet-resources\/","title":{"rendered":"Using nftables to allow ingress traffic from internet to intranet resources"},"content":{"rendered":"<p>I recently heard about a case where someone wanted to allow an AWS EC2 instance on a public IP address to access a corporate intranet resource. Their solution was to run an HAProxy server on the corporate network that could proxy traffic from the AWS compute instance to the intranet server.<\/p>\n<p>Whether or not accessing intranet resources from an external host is good idea or violates corporate policy I will leave aside for now, but I thought of 6 additional ways to accomplish the same objective.<\/p>\n<p>What follow is one of those 6 ideas: using Network Address Translation (NAT) on the intermediary host via nftables. I have spent far more time with <code>iptables<\/code> than I have with nftables. But this seemed like a good opportunity to experiment.<\/p>\n<p>The 3 servers for this example:<\/p>\n<ol>\n<li>The corporate intranet resource, <code>intranet.osric.net<\/code> or <code>192.0.2.43<\/code><\/li>\n<li>The NAT host, <code>nat.osric.net<\/code> or <code>198.51.100.200<\/code><\/li>\n<li>The AWS EC2 instance, <code>ec2-203-0-113-155.compute-1.amazonaws.com<\/code> or <code>203.0.113.155<\/code><\/li>\n<\/ol>\n<p>The <code>nat.osric.net<\/code> server is running RHEL 10, although the commands should work on other systems using <code>nftables<\/code>.<\/p>\n<p>I followed the steps provided by <a href=\"https:\/\/docs.redhat.com\/en\/documentation\/red_hat_enterprise_linux\/10\/html\/configuring_firewalls_and_packet_filters\/getting-started-with-nftables#configuring-destination-nat-using-nftables\">Configuring destination NAT using nftables<\/a> on <code>nat.osric.net<\/code>:<\/p>\n<pre><code>sudo nft add table nat\r\nsudo nft -- add chain nat prerouting { type nat hook prerouting priority -100 \\; }\r\nsudo nft add chain nat postrouting { type nat hook postrouting priority 100 \\; }<\/code><\/pre>\n<p>The next commands needed the interface name, which I found using <code>nmcli device status<\/code> (<code>ens5<\/code> in this case).<\/p>\n<pre><code>sudo nft add rule nat prerouting iifname ens5 tcp dport { 80, 443 } dnat to 192.0.2.43\r\nsudo nft add rule nat postrouting oifname \"ens5\" snat to 198.51.100.200\r\necho \"net.ipv4.ip_forward=1\" | sudo tee \/etc\/sysctl.d\/95-IPv4-forwarding.conf\r\nsudo sysctl -p \/etc\/sysctl.d\/95-IPv4-forwarding.conf<\/code><\/pre>\n<p>It worked! Well, with a couple caveats: an issue with the TLS certificate, and a wide-open NAT configuration.<\/p>\n<p><strong>The TLS certificate<\/strong><\/p>\n<p>From the EC2 instance:<\/p>\n<pre><code>$ curl https:\/\/198.51.100.200\r\ncurl: (60) SSL: no alternative certificate subject name matches target host name '198.51.100.200'\r\nMore details here: https:\/\/curl.se\/docs\/sslcerts.html\r\n\r\ncurl failed to verify the legitimacy of the server and therefore could not\r\nestablish a secure connection to it. To learn more about this situation and\r\nhow to fix it, please visit the web page mentioned above.<\/code><\/pre>\n<p>I could think of a few ways around this:<\/p>\n<ul>\n<li>Add an entry to <code>\/etc\/hosts<\/code> on the client, i.e. on <code>ec2-203-0-113-155.compute-1.amazonaws.com<\/code><\/li>\n<li>Ignore the warning, e.g. <code>curl --insecure https:\/\/198.51.100.200\/<\/code><\/li>\n<li>Add a subject alternative name (e.g. <code>nat.osric.net<\/code>) to the TLS cert for <code>intranet.osric.net<\/code><\/li>\n<\/ul>\n<p>The last method, adding a Subject Alternative Name (SAN) sounded like a pain, and also may not be feasible if you don&#8217;t manage the TLS certificate for that host. And I never like ignoring security warnings. But editing the <code>\/etc\/hosts<\/code> file was a simple solution that worked. Here&#8217;s the line I added to the EC2 instance (<code>ec2-203-0-113-155.compute-1.amazonaws.com<\/code>):<\/p>\n<pre><code>198.51.100.200   intranet.osric.net<\/code><\/pre>\n<p>After that, I was able to use <code>curl https:\/\/intranet.osric.net\/<\/code> to return content without errors or warnings.<\/p>\n<p><strong>The wide-open NAT<\/strong><\/p>\n<p>The configuration, as implemented, could allow <em>anyone<\/em> on the Internet to bypass the network security controls around <code>intranet.osric.net<\/code>. All they have to do is visit <code>nat.osric.net<\/code> (and maybe ignore some TLS warnings). This is terrible! Fortunately, we can use nftables to limit NAT to only specific inbound traffic.<\/p>\n<p>How can I review what has already been added to nftables? Fortunately, <a href=\"https:\/\/wiki.nftables.org\/wiki-nftables\/index.php\/Quick_reference-nftables_in_10_minutes#List_ruleset\">Quick reference-nftables in 10 minutes<\/a> was helpful here.<\/p>\n<pre><code>$ sudo nft list tables\r\ntable ip nat\r\n$ sudo nft list chains\r\ntable ip nat {\r\n        chain prerouting {\r\n                type nat hook prerouting priority dstnat; policy accept;\r\n        }\r\n        chain postrouting {\r\n                type nat hook postrouting priority srcnat; policy accept;\r\n        }\r\n}\r\n$ sudo nft list ruleset\r\ntable ip nat {\r\n        chain prerouting {\r\n                type nat hook prerouting priority dstnat; policy accept;\r\n                iifname \"ens5\" tcp dport { 80, 443 } dnat to 192.0.2.43\r\n        }\r\n\r\n        chain postrouting {\r\n                type nat hook postrouting priority srcnat; policy accept;\r\n                oifname \"ens5\" snat to 198.51.100.200\r\n        }\r\n}<\/code><\/pre>\n<p>In order to delete the dnat rule, I needed its handle. To get this, I used the <code>-a<\/code> flag:<\/p>\n<pre><code>$ sudo nft -a list ruleset\r\ntable ip nat { # handle 1\r\n        chain prerouting { # handle 1\r\n                type nat hook prerouting priority dstnat; policy accept;\r\n                iifname \"ens5\" tcp dport { 80, 443 } dnat to 192.0.2.43 # handle 8\r\n        }\r\n\r\n        chain postrouting { # handle 2\r\n                type nat hook postrouting priority srcnat; policy accept;\r\n                oifname \"ens5\" snat to 198.51.100.200 # handle 9\r\n        }\r\n}<\/code><\/pre>\n<p>I deleted handle 8:<\/p>\n<pre><code>sudo nft delete rule nat prerouting \"handle 8\"<\/code><\/pre>\n<p>And added a new prerouting rule:<\/p>\n<pre><code>sudo nft add rule nat prerouting iifname ens5 ip saddr 203.0.113.155 tcp dport { 80, 443 } dnat to 192.0.2.43<\/code><\/pre>\n<p>That provides NAT for <em>only<\/em> the EC2 instance at <code>203.0.113.155<\/code>.<\/p>\n<p>The thing that I like about this solution is that it requires no new software. It uses the networking functionality that is already present on the host. However, unless you take additional steps, there is virtually no logging. <code>nftables<\/code> can log messages to syslog for packets that match the DNAT rule, but even that is very different than the level of logging that a proxy server would provide.<\/p>\n<p><strong>References:<\/strong><\/p>\n<ul>\n<li><a href=\"https:\/\/serverfault.com\/questions\/140622\/how-can-i-port-forward-with-iptables\">How can I port forward with iptables?<\/a> (serverfault.com)<\/li>\n<li><a href=\"https:\/\/docs.redhat.com\/en\/documentation\/red_hat_enterprise_linux\/10\/html\/configuring_firewalls_and_packet_filters\/getting-started-with-nftables#configuring-destination-nat-using-nftables\">Configuring destination NAT using nftables<\/a> (docs.redhat.com)<\/li>\n<li><a href=\"https:\/\/wiki.nftables.org\/wiki-nftables\/index.php\/Quick_reference-nftables_in_10_minutes\">Quick reference-nftables in 10 minutes<\/a> (wiki.nftables.org)<\/li>\n<li>The man page for nft<\/li>\n<\/ul>\n<p>During some initial testing, I was using another EC2 instance as the NAT server and it did not work immediately. That&#8217;s because the EC2 instance was not aware of its own public IPv4 address, it was only aware of its private IPv4 address: <code>172.31.15.224<\/code>. Removing the SNAT rule for the public IP address and adding a new SNAT rule for <code>172.31.15.224<\/code> solved that.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>In this example, I configure Network Address Translation (NAT) using nftables on an intermediary Linux host to route HTTPS requests from an external host to an intranet host.<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[232],"tags":[408,580,579],"class_list":["post-3892","post","type-post","status-publish","format-standard","hentry","category-tips-tricks","tag-iptables","tag-nat","tag-nftables"],"_links":{"self":[{"href":"https:\/\/osric.com\/chris\/accidental-developer\/wp-json\/wp\/v2\/posts\/3892","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/osric.com\/chris\/accidental-developer\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/osric.com\/chris\/accidental-developer\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/osric.com\/chris\/accidental-developer\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/osric.com\/chris\/accidental-developer\/wp-json\/wp\/v2\/comments?post=3892"}],"version-history":[{"count":13,"href":"https:\/\/osric.com\/chris\/accidental-developer\/wp-json\/wp\/v2\/posts\/3892\/revisions"}],"predecessor-version":[{"id":3910,"href":"https:\/\/osric.com\/chris\/accidental-developer\/wp-json\/wp\/v2\/posts\/3892\/revisions\/3910"}],"wp:attachment":[{"href":"https:\/\/osric.com\/chris\/accidental-developer\/wp-json\/wp\/v2\/media?parent=3892"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/osric.com\/chris\/accidental-developer\/wp-json\/wp\/v2\/categories?post=3892"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/osric.com\/chris\/accidental-developer\/wp-json\/wp\/v2\/tags?post=3892"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}