{"id":3919,"date":"2026-10-08T21:19:36","date_gmt":"2026-10-09T02:19:36","guid":{"rendered":"https:\/\/osric.com\/chris\/accidental-developer\/?p=3919"},"modified":"2026-10-08T21:19:36","modified_gmt":"2026-10-09T02:19:36","slug":"using-haproxy-to-allow-ingress-traffic-from-internet-to-intranet-resources","status":"publish","type":"post","link":"https:\/\/osric.com\/chris\/accidental-developer\/2026\/10\/using-haproxy-to-allow-ingress-traffic-from-internet-to-intranet-resources\/","title":{"rendered":"Using HAProxy to allow ingress traffic from internet to intranet resources"},"content":{"rendered":"<p>As mentioned in a previous post, 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. The proposed solution was to run an HAProxy server on the corporate network that could proxy traffic from the AWS compute instance to the intranet server. This post will give a demonstration of that.<\/p>\n<p><strong>Please note:<\/strong> Don&#8217;t bypass your organization&#8217;s security controls! Implementing a solution like this without prior authorization or approval would likely constitute a violation of an organization&#8217;s acceptable use policy or information security policies.<\/p>\n<p>The 3 servers for this example:<\/p>\n<ol>\n<li>The corporate intranet resource, intranet.osric.net or 192.0.2.43<\/li>\n<li>The HAProxy host, haproxy.osric.net or 198.51.100.200<\/li>\n<li>The AWS EC2 instance, ec2-203-0-113-155.compute-1.amazonaws.com or 203.0.113.155<\/li>\n<\/ol>\n<p>Although I&#8217;ve primarily used HAProxy for load balancing in the past, it can easily be used as a basic reverse proxy server. A load-balancer, after all, is basically a reverse proxy with multiple backend servers. I set up a new RedHat Enterprise Linux (RHEL) 10 host, haproxy.osric.net and installed and enabled HAProxy:<\/p>\n<pre><code>$ sudo dnf install haproxy\r\n$ sudo systemctl enable haproxy<\/code><\/pre>\n<p>I then added an HAProxy config at <code>\/etc\/haproxy\/conf.d\/proxy.cfg<\/code> HAProxy should automatically find a config file ending with <code>.cfg<\/code> at that location. The default config included at <code>\/etc\/haproxy\/haproxy.cfg<\/code> will also be loaded. It includes some config options you won&#8217;t need!<\/p>\n<p>Here is the content I added to <code>proxy.cfg<\/code>:<\/p>\n<pre><code>listen http-proxy\r\n    bind :80\r\n    server intranet 192.0.2.43:80<\/code><\/pre>\n<p>I restarted the <code>haproxy<\/code> service:<\/p>\n<pre><code>$ sudo systemctl restart haproxy<\/code><\/pre>\n<p>After that I was able to use <code>curl<\/code> to access the intranet resource, both from the <code>haproxy<\/code> host and from the EC2 instance:<\/p>\n<p>From the <code>haproxy<\/code> host:<\/p>\n<pre><code>$ curl localhost\r\n&lt;!DOCTYPE html&gt;\r\n&lt;html&gt;\r\n&lt;head&gt;\r\n&lt;title&gt;Intranet - osric.net&lt;\/title&gt;\r\n&lt;\/head&gt;\r\n&lt;body&gt;\r\n&lt;h1&gt;Intranet - osric.net&lt;\/h1&gt;\r\n&lt;p&gt;There is nothing to see here.&lt;\/p&gt;\r\n&lt;\/body&gt;\r\n&lt;\/html&gt;<\/code><\/pre>\n<p>From 203.0.113.155 (the AWS EC2 instance):<\/p>\n<pre><code>$ curl 198.51.100.200\r\n&lt;!DOCTYPE html&gt;\r\n&lt;html&gt;\r\n&lt;head&gt;\r\n&lt;title&gt;Intranet - osric.net&lt;\/title&gt;\r\n&lt;\/head&gt;\r\n&lt;body&gt;\r\n&lt;h1&gt;Intranet - osric.net&lt;\/h1&gt;\r\n&lt;p&gt;There is nothing to see here.&lt;\/p&gt;\r\n&lt;\/body&gt;\r\n&lt;\/html&gt;<\/code><\/pre>\n<p><strong>Restricting proxy access<\/strong><\/p>\n<p>As it is, the proxy is open to the world. Anyone can now access resources from intranet.osric.net via the proxy. How can we lock it down so that only the intended IP address, <code>203.0.113.155<\/code>, can access it?<\/p>\n<p>I found the article <a href=\"https:\/\/adhdecode.com\/articles\/haproxy\/haproxy-ip-allowlist-blocklist\/\">Allow and Block IP Addresses in HAProxy (2026)<\/a> with some tips. I modified the <code>proxy.cfg<\/code> file:<\/p>\n<pre><code>$ cat \/etc\/haproxy\/conf.d\/proxy.cfg\r\nlisten http-proxy\r\n    bind :80\r\n    acl authorized_ip src 203.0.113.155\r\n    http-request deny if !authorized_ip\r\n    server intranet 192.0.2.43:80<\/code><\/pre>\n<p>From the HAProxy server the HTTP request is denied by HAProxy with an HTTP 403 error:<\/p>\n<pre><code>$ curl localhost\r\n&lt;html&gt;&lt;body&gt;&lt;h1&gt;403 Forbidden&lt;\/h1&gt;\r\nRequest forbidden by administrative rules.\r\n&lt;\/body&gt;&lt;\/html&gt;<\/code><\/pre>\n<p>However, the request still works from the IP address of the authorized AWS EC2 instance:<\/p>\n<pre><code>$ curl 198.51.100.200\r\n&lt;!DOCTYPE html&gt;\r\n&lt;html&gt;\r\n&lt;head&gt;\r\n&lt;title&gt;Intranet - osric.net&lt;\/title&gt;\r\n&lt;\/head&gt;\r\n&lt;body&gt;\r\n&lt;h1&gt;Intranet - osric.net&lt;\/h1&gt;\r\n&lt;p&gt;There is nothing to see here.&lt;\/p&gt;\r\n&lt;\/body&gt;\r\n&lt;\/html&gt;<\/code><\/pre>\n<p><strong>Host-based firewall restrictions<\/strong><\/p>\n<p>It&#8217;s best if HAProxy never even sees the request from unauthorized sources. We can achieve this by allowing HTTP requests from only the authorized IP address via the host-based firewall.<\/p>\n<p>In this case, I&#8217;m using <code>nftables<\/code> to manage the host-based firewall. There&#8217;s a sample configuration for <code>nftables<\/code> on RHEL 10 at <code>\/etc\/nftables\/main.nft<\/code>. I added the following line to the <code>allow<\/code> chain:<\/p>\n<pre><code>ip saddr 203.0.113.155 tcp dport 443 accept<\/code><\/pre>\n<p>The full allow chain now looks like this:<\/p>\n<pre><code>chain allow {\r\n    ct state established,related accept\r\n    meta l4proto @allowed_protocols accept\r\n    iifname @allowed_interfaces accept\r\n    tcp dport @allowed_tcp_dports accept\r\n    ip saddr 203.0.113.155 tcp dport 443 accept\r\n}<\/code><\/pre>\n<p>To load the ruleset into <code>nftables<\/code>, I used the following:<\/p>\n<pre><code>$ sudo nft -f \/etc\/nftables\/main.nft<\/code><\/pre>\n<p>It&#8217;s important when working with <code>nftables<\/code> that you don&#8217;t cut off your own access! The sample file at <code>\/etc\/nftables\/main.nft<\/code> includes SSH as an allowed port in <code>@allowed_tcp_dports<\/code>, but the configuration is set to reject any connection that is not explicitly allowed. It is also important to note that the sample file also contains other rules you likely won&#8217;t need (it allows port 9090\/tcp connections from all sources, for example).<\/p>\n<p>Also important to note: changes to <code>nftables<\/code> are not persistent by default. For example, if you reboot the system and check for existing <code>nftables<\/code> rules, nothing is returned:<\/p>\n<pre><code>$ sudo nft list ruleset<\/code><\/pre>\n<p>To persist the rules on RHEL 10, take the following steps:<\/p>\n<p>Uncomment the following line in <code>\/etc\/sysconfig\/nftables.conf<\/code>:<\/p>\n<pre><code>#include \"\/etc\/nftables\/main.nft\"<\/code><\/pre>\n<p>Then, enable and start the <code>nftables<\/code> service:<\/p>\n<pre><code>$ sudo systemctl enable nftables\r\n$ sudo systemctl start nftables<\/code><\/pre>\n<p>The <code>nftables<\/code> service is not a daemon, it just runs at startup (or when the service is started). It is normal for the service to report that it is exited, as in the excerpt below:<\/p>\n<pre><code>$ systemctl status nftables\r\n\u25cf nftables.service - Netfilter Tables\r\n     Loaded: loaded (\/usr\/lib\/systemd\/system\/nftables.service; enabled; preset: disabled)\r\n     Active: active (exited) since Thu 2026-10-08 22:03:58 UTC; 3h 52min ago<\/code><\/pre>\n<p>You can check and confirm that the rules are present (and that they persist after a reboot):<\/p>\n<pre><code>$ sudo nft list ruleset<\/code><\/pre>\n<p><strong>What about TLS connections (HTTPS)?<\/strong><\/p>\n<p>That&#8217;s a great question. I did find a site that has what looks to be helpful info, but I have not tested it yet:<\/p>\n<p><a href=\"https:\/\/oneuptime.com\/blog\/post\/2026-03-20-haproxy-ssl-passthrough-ipv4\/view\">How to Configure HAProxy SSL Passthrough for IPv4 Backend Servers <\/a><\/p>\n<p>That will be a project for another day.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>This post demonstrates how to use HAProxy between a public AWS EC2 host and a private intranet host to allow the EC2 instance to pass HTTP requests to the intranet host. It also includes information on how to restrict HTTP request to proxy server, both via the HAProxy configuration as well as nftables, the host-based firewall on the proxy host.<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[533,579],"class_list":["post-3919","post","type-post","status-publish","format-standard","hentry","category-uncategorized","tag-haproxy","tag-nftables"],"_links":{"self":[{"href":"https:\/\/osric.com\/chris\/accidental-developer\/wp-json\/wp\/v2\/posts\/3919","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=3919"}],"version-history":[{"count":19,"href":"https:\/\/osric.com\/chris\/accidental-developer\/wp-json\/wp\/v2\/posts\/3919\/revisions"}],"predecessor-version":[{"id":3940,"href":"https:\/\/osric.com\/chris\/accidental-developer\/wp-json\/wp\/v2\/posts\/3919\/revisions\/3940"}],"wp:attachment":[{"href":"https:\/\/osric.com\/chris\/accidental-developer\/wp-json\/wp\/v2\/media?parent=3919"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/osric.com\/chris\/accidental-developer\/wp-json\/wp\/v2\/categories?post=3919"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/osric.com\/chris\/accidental-developer\/wp-json\/wp\/v2\/tags?post=3919"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}