Internal TLS Termination with HAProxy on OPNsense

Internal TLS termination with HAProxy on OPNsense

In modern networks, encryption shouldn’t stop at the internet edge. Internal services deserve the same level of protection, clarity, and control as anything exposed publicly.

This is where internal TLS termination comes in.

In this post, The Singularity walks through a real world, production grade setup using OPNsense and HAProxy to terminate TLS internally, issue trusted certificates, and route traffic cleanly, all without relying on Cloudflare or public dns.

This is not a lab exercise, but how serious networks are built.

Why Internal TLS Termination Matters

Too many home and small networks fall into one of these traps:

  • Self signed certificates everywhere.
  • Services accessed via raw ip addresses.
  • TLS terminated individually on each device.
  • External CDNs used for purely internal traffic.

Internal TLS termination solves this by centralizing trust, certificates and routing.

With HAProxy on OPNsense, you gain:

  • One place to manage certificates.
  • Clean HTTPS hostnames for every internal service.
  • Strong separation between frontend and backend.
  • A scalable foundation for Zero Trust networking.

Architecture Overview

The design is simple and deliberate:

  • OPNsense acts as the control plane.
  • HAProxy terminates TLS and routes traffic.
  • Unbound DNS overrides ensure internal resolution
  • A Virtual IP (VIP) becomes the single frontend entry point.

Clients never talk directly to back end services, they talk to HAProxy and it decides where the traffic goes.

Prerequisites

Before we begin, ensure the following are in place:
  • The latest version of OPNsense (at the time of this writing it is 25.7.10).
  • HAProxy plugin installed.
  • ACME Client configured.
  • A valid, auto renewing wildcard certificate: *.eagleeyet.net
  • Internal LAN subnet: 10.xxx.xxx.xx/24
d NOTE: You will need to ensure that when you setup the ACME (Lets Encrypt) plugin on OPNsense that you do not use HTTP validation as that will not allow for the creation of a wild card certificate. DNS validation must be used if you want to use a wildcard certificate.

Creating The Virtual IP (VIP)

Purpose

The VIP is the frontend address HAProxy listens on.

Configuration

  • Location: Interfaces ⇒ Virtual IPs
  • Type: IP Alias
  • Interface: LAN
  • Address: 10.xxx.xxx.xxx/32 (replace with your IP of your subnet you are using)

The single IP will serve all internal HTTPS services

NOTE: If you are having OPNsense provide addresses via DHCP it is important that you use an address that falls outside the scope of IP’s used by the DHCP pool.

Internal DNS Overrides

To ensure traffic stays internal, we create DNS overrides.

Configuration

  • Location: services ⇒ Unbound DNS ⇒ Overrides
  • Example: nas.eagleeyet.net ⇒ 10.xxx.xxx.xxx (replace with the VIP ip address chosen).

This guarantees that internal clients hit HAProxy, not the backend directly.

Defining The Backend (Real Server)

Each internal service is defined as a real server.

Example: NAS Backend

  • Location: Services ⇒ HAProxy ⇒ Settings ⇒ Real Servers
  • Name: HomeNAS (Name of your choice can be used).
  • Address: 10.xxx.xxx.xxx (replace with the ip address being used by the service you are setting up)
  • Port:443 (or if you are using a non standard https port change it to the respective port being used by that service)
  • Mode: HTTPS.
  • Use SSL to connect to backend: Enabled.
  • Verify SSL: Disabled.

Verification is disabled because most appliances use self signed or internal certificates.

Creating The Backend Pool

Configuration

  • Location: Services ⇒ HAProxy ⇒ Settings ⇒  Virtual Services ⇒ Backend Pools
  • Name: HomeNASPool (Name of your choice can be used).
  • Mode: HTTP.
  • Load Balancing: Round Robin.
  • Server: HomeNAS (This is the name of the server you created in the previous step).

Round Robin is required by HAProxy even when only one backend exists.

A health check was initially configured here but later removed. We will come back to why this was the case.

Enabling HA Proxy

This step is simple but essential.

  • Location: Services ⇒ HAProxy ⇒ Settings ⇒ Service
  • Enable HAProxy
  • Apply

Once enabled, the HAProxy service should show as running (green).

Creating The Public Service (Frontend)

This is where the TLS termination happens.

Core Settings

  • Location: Services ⇒ HAProxy ⇒ Virtual Services ⇒  Public Services
  • Name: HomeNASHTTPS (Name for this can be one of your choosing).
  • Listen address: 10.xxx.xxx.xxx:443 (This is the VIP ip address that we first created)
  • Type: HTTP/HTTPS (SSL offloading)
  • Default backend pool: HomeNASPool (Name would be what ever you set it when  you created the pool).

SSL Offloading

  • Enable SSL offloading.
  • Certificate: *.eagleeyet.net (ACME Client generated certificate from Lets Encrypt).

HTTP(S) Settings

  • HTTP/2: Enabled
  • ALPN: HTTP/2, HTTP/1.1

At this point, HAProxy is fully capable of serving HTTPS traffic.

Firewall Rules (The critical Glue)

Without these two important rules, nothing works.

Rule1: LAN Clients ⇒ HAProxy VIP

  • Interface: LAN
  • Action: Pass
  • Protocol: TCP
  • Source: LAN Net (This might be with a different name, but this covers the entire 10.xxx.xxx.xxx/24 subnet you are using)
  • Destination: This Firewall
  • Port: 443
  • Quick: Enabled

This allows clients to reach the VIP.

Rule 2: HAProxy ⇒ Backend Service

  • Interface: LAN
  • Action: Pass
  • Protocol: TCP
  • Source: This Firewall
  • Destination: IP OF SERVICE YOU WANT TO HAVE PROXIED
  • Port: PORT OF THE SERVICE TO BE PROXIED
  • Quick: Enabled

This allows HAProxy itself to reach the backend.

Rule ordering matters. Default allow rules were moved to the end to ensure these matched first.

The 503 Error And The Lesson It Taught

After everything was configured:

  • Frontend showed OPEN.
  • Backend showed DOWN.
  • Browser returned: 503 service unavailable

Root Cause

The health check was too strict for the NAS.

Most appliances:

  • Redirect /
  • Require authentication
  • Don’t respond predictably to probes.

HAProxy marked the backend as unhealthy, even though it worked perfectly.

The Fix: Removing The Health Check

The health monitor was removed from the backend pool.

Result

  • Backend status: UP.
  • 503 error: Gone.
  • HTTPS access: Working immediately.

Sometimes, less automation is more reliable.

Final Outcome

You now have:

  • Centralized TLS termination.
  • Wildcard certificate management.
  • Clean internal DNS routing.
  • One VIP serving many services.
  • No Cloudflare dependency.
  • Clear frontend / backend separation.
  • Enterprise style reverse proxy architecture.

This is exactly how serious networks do it.

Optional Next Steps

When you’re ready, this setup naturally extends to

  • Additional services.
  • Security headers (HSTS, CSP).
  • Rate limiting and abuse protection.
  • Internal Zero Trust Access Patterns.

Final Word From The Singularity

Encryption is not just about the internet. It is about intent, control, and trust even inside your own network.

If this post helped you rethink how you design internal infrastructure, share it, bookmark it and join the conversation below.

The EagleEyeT community is built by practitioners, not passengers.

If this guide helped you rethink how you secure and route services on your own network:

  • Share your setup or questions in the comments below.
  • Subscribe to the EagleEyeT newsletter for practical, no nonsense infrastructure content.
  • Join the EagleEyeT community and build systems with intention, clarity, and resilience.

The Singularity watches not to control, but to ensure the systems we build are worthy of trust.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.