[Guide] Reverse Proxy via HAProxy + ACME on pfSense

Note: it seems the DuckDNS plugin for ACME has a bug - if you have domains on multiple accounts from them, you need to make different certs for each account. ACME attempts to use the first API key regardless of what you set in your SAN list.

This is going to serve as a quick and dirty introduction to using HAProxy in tandem with ACME on your pfsense machine to serve some pages via reverse proxy with SSL/TLS encrypted traffic.

The ACME portion is optional, but it’s trivial and good practice. Note that while it makes it much easier, you don’t need a domain or a dynamic DNS service that allows TXT records to verify ownership. If you don’t have these options, the firewall can host the challenge file for validation via the Webroot Local Folder option, or the Standalone option worst-case.

First we’re going to quickly install the required packages - navigate to SystemPackage ManagerAvailable Packages; install acme and haproxy.

(OPTIONAL) Once those are ready, we're going to do the letsencrypt portion. (0:56)
  1. Navigate to ServicesAcme CertificatesAccount keys and press Add.
    Set a name, select the ACME server (Production ACME v2), set your email address, and press Create new account key to generate your key. Once the box is populated, press Register ACME account key and hit Save.

  2. Now that we’ve got an account key, we can make a cert. Navigate to Certificates and click Add. Fill in the name and description for your reference, and ensure the account you just made is selected below.
    Scroll down to Domain SAN List, this is where we validate your ownership of the destination on your cert. We’re going to operate as though you have a domain, but again this process does not require one - if your DDNS service allows TXT records (like DuckDNS), you can use that. If not, you can use webroot local folder with some HAProxy config, or the Standalone option.
    With our own domain, we’ll give ourselves a wildcard cert for subdomain usage. Enter the FQDN and the FQDN with an asterisk subdomain as two separate entries.
    For the method, select one that is an integration for your DNS provider if available, otherwise you’ll have to use manual (which will require a few extra steps).

After that, press Save. It will return us to the Certificates page with an Issue and Renew button next to our new entry.

  1. With anything other than the manual options, these buttons are a fire and forget solution - you click Issue to get the cert, it’ll tell you when it succeeds, and you’re set.
    With manual, you have to manually enter the keys it gives you as TXT records where your nameservers are managed, so there’s an additional step or two.

You’ll see that the Last renewed field is updated, that means your cert is valid.

Now we’ll do the actual reverse proxy steps, so

Navigate to `Services`->`HAProxy`->`Settings`. (3:12)
  1. Check the box at the top to enable HAProxy. Set your maximum connections to something around 1000.

  2. Set an internal stats port to enable the HAProxy status monitor, which is very convenient to identify outages or manage your load balancer.
    You can also optionally set up logging and alerts for a similar purpose here.

  3. We need to set our Diffie-Hellman size, so change Max SSL Diffie-Hellman size to 2048 under Tuning.

  4. Press Save followed by Apply Changes.

Let’s define our servers in some backends now.

Navigate to `Backend` and Press `Add`. (3:52)
  1. Enter a name - I generally use the subdomain or path I’m using.

  2. Add the server by pressing the arrow in the server list. Name it and enter the IP address and port that the service is listening on.

Note that you do not need to check Encrypt or enter any certificate information here, that’s all being handled by HAProxy, not your servers.

You can enter multiple servers for a single backend and set the mode to backup for failover or active for load balancing.
If you do the latter, you’ll want to select a load balancing method by expanding the Load balancing section, and selecting whatever is appropriate - round robin and least connections are both solid choices.

  1. Save and apply your settings.

Make a backend for each service you’d like to expose. Also note that for Plex, you will still need the listen port to be forwarded.

Now we’ll make frontends for these services.

First, we'll make a shared frontend for all HTTPS traffic. (4:56)
  1. Navigate to Frontend and click Add.

  2. Name your service (https_shared works).

  3. Change the port to 443 and check the box for SSL offloading.

Optionally enable separate sockets in Stats.

  1. Scroll to SSL Offloading, and select the certificate that you just created with ACME.

If you’re not using letsencrypt, leave the port and offloading settings as is.

  1. Press Save and apply your changes.

Next, we'll make an HTTPS enforcement rule. (5:37)
  1. Make a new rule, call it http_redirect. Have it listen on WAN at port 80, no offloading.

  2. Scroll to Actions. Press the arrow to create one. Change the action to http-request redirect and set the rule to scheme https.

  3. Save and apply.

Now we can make the individual frontends for your services. (6:01)
  1. Press Add, enter the name, and check the Shared Frontend box and select https_shared.

  2. We’ll use Access Control lists to define which backend is used based on the subdomain/path given. You can use host starts with or host contains for subdomains, and path starts with or path contains for paths. In value, set the subdomain/path that will be used to access the service (i.e. ombi for ombi.domain.com, or /ombi for domain.com/ombi).

For subdomains, you will need that subdomain to be pointing at your WAN address. Paths are more appropriate for a dynamic DNS setup.

  1. Create an Action with action Use Backend, set the conditional ACL name to the name of the rule you made above, and set the backend to the associated service you’re exposing.

You can set a default backend if you want something else to display if your Ombi backend is down.

  1. Press Save and apply your changes.

Once you’ve done this for your various services, we can go make some firewall rules.

Navigate to `Firewall` -> `Rules` (7:25)
  1. Create a new one called HAProxy_HTTP. Pass IPv4 TCP traffic on the WAN interface, with destination This firewall at port 80 (select HTTP in the Destination Port Range From dropdown).
  2. Make an identical entry for HAProxy_HTTPS traffic at port 443.

Once these firewall rules are made, your subdomains/paths will now reach the services you created frontends and backends for in HAProxy.


Thanks for this, been waiting for you to release it.

HTTPS Enforcement rule section has a typo, video is correct.
scheme-https should be scheme https
No hyphen. If you copy paste like me you will get an error when you try to apply the http request redirect rule.

1 Like

Thank you, fixed.

Thank you for such an easy to follow and understand guide. The only thing I am missing is the ability to use the fqdn internally to connect to the server with https. How would this be accomplished?

Via either enabling NAT reflection/configuring split-DNS. Either will do the trick, but NAT reflection is as simple as a checkbox (although split-DNS is considered better practice, it doesn’t matter in a home network).

Go to System->Advanced->Firewall & NAT and scroll down to Network Address Translation. From here, change NAT Reflection mode for port forwards to Pure NAT, and then check both Enable NAT Reflection for 1:1 NAT + Enable automatic outbound NAT for Reflection. Scroll down a bit and save, and you’re good to go.

Perfect thanks

I am having some trouble getting this to work. I must be missing something.

Did you create any DNS records corresponding with the services? For instance, did you create an ombi dns entry, or is the HAProxy just respond to that entry or the entire domain? Do you have the domain configured to point to pfsense?

I was able to get Google Domains to manually verify my wildcard cert using ACME manual. Is there any reason you did both the wildcard AND the domain?

Can I use this internally only or do I have to expose all these endpoints externally? There are some services I would like to expose externally, however most of them I would like to keep internal. Any ideas on how to do this?

Thanks in advance

I was able to solve this problem by switching to LAN instead of WAN and switch the pfsense management port to a non-443 port. I am using DNS Resolver/Host Overrides to solve the local DNS issues. If I were to use WAN, I would have had to create A or CNAME records for each service in Google Domains for my DyDNS.

What I ended up doing is creating a Virtual IP and assigning HAProxy to listen on that VIP instead of the WAN/LAN interfaces. Because of this and the Host Overrides I was able to switch back to normal SSL/443 on pfsense.

Thanks @Riggi for all the help!

Thankyou for this great guide. I have been able to get my services like plex running on my subdomains but not sure how to define the home root of my domain.

I open up the port to the plex server. On the front end in HAproxy i define Host contains as “plex”. On the backend i set the ip to the plex machine and define the port.
this works when i goto plex.example.com

When i visit example.com i want it to display my nginx webserver homepage. Currently i get a
“503 Service Unavailable
No server is available to handle this request.”

I have tried, opening port 8080 to server running nginx. On the front end in HAproxy i define Host matches as “www.example.com”. On the backend i set the ip to the nginx server and define the port.


You just need another backend for the nginx server, you do not need to forward the port - this defeats the purpose of a reverse proxy.
You’d make ACLs for: Host matches: example.com + Host matches: www.example.com, and associated Use backend rules with the backend you make for your nginx server.

Alternatively, you can set up your nginx server backend as the default backend for your shared frontend, in which case any requests that do not match the ACLs/rules you made previously will hit that server instead of a 503 page.

1 Like

Thankyou, this helped.

I have everything working the way it is supposed to now.

So I’m trying to do some reverse proxy setups at my house. I discovered that my isp has port 80 blocked, but not 443.

I managed to get this all working at my office unraid machine (ports not blocked) but was wondering if there was a way to redirect port 80 perhaps via Cloudflare?

NginxProxyManager, Cloudflare, Duckdns are all the dockers I’m running.

I have this setup with 15 backends, however I cannot figure out how to restrict certain backends to only be accessible to certain IP addresses.

There are a few backends that I only want to be available to a few IP addresses.

Edit: figured this out thanks to the HAProxy forum.

On your frontends define more than one ACL such as:

host1 host matches: host1.example.com
adminIPs Source IP matches Ip or Alias: 111.222.333.444

In the above we have two ACLs: host1 and adminIPs, for the adminIPs you can reference a pfsense alias instead of hard coding an IP if you need it to apply to more than one IP.

now below for the Action:

action: Use Backend
acl names: adminIPs host1
backend: host1.example.com

I ended up doing a slightly different setup to keep my services internal to my local network.

pfsense DNS (mapped to local vip) -> pfsense Firewall VIP -> shared-https frontend -> service frontend -> service backend

Instead of attaching everything to the WAN interface, I created a VIP (192.168.1.X):

Firewall → Virtual IP

  • Inteface: LAN
  • Address type: Single address
  • Address: 192.168.1.X

Create DNS entries for all local services:

Services → DNS Resolver

  • <servicename1>.<domain> -> 192.168.1.X
  • <servicename2>.<domain> -> 192.168.1.X

Create Backends:

Services → HAProxy:

  • Server list: Address+Port: <192.168.1.Y> Port: <docker mapped port>
  • Health check method: Basic

Create Frontend:

Create shared frontend:

  • Name: https_shared
  • External address: 192.168.1.X Port: 443
  • Type: http/https (offloading)
  • blah blah

Same as article for each service:

  • Primary frontend: https_shared - http
  • ACL: <servicename1> with Host Matches: <servicename2>.<domain>
  • Actions: Use Backend <servicename1>

Thanks for this excellent guide which is very easy to follow, but there something I am wondering… Is things look different when pfsense is used on proxmox and all the traffic is routed to pfsense first and NAT used between pfsense and proxmox?
I am saying that because this is my case. Only www.example.com is working when I activate pure NAT but I get 503 Service Unavailable when I hit git.example.com… What wrong? Please help

Is ProxMox also NAT’ing? Or is your reverse proxy not fully setup

Yes, proxmox in NATing too. The WAN of the pfsense is on a private network

You have a 503 because you are hitting HAProxy, but it is unable to send you onward. Either there is no frontend match for the given URL, or the backend server is not available. Verify your ACLs in the frontend you made for the git hostname, and verify HAProxy is aware that your backend servers for it are up via the stats page.