IT post header

Reverse Proxy with HAProxy + ACME in pfSense

In this post we are going to see how to configure HAProxy and ACME in our pfSense firewall to be able to access services hosted on our servers, for example our Home Assistant interface or our web server.

In order for the connection to be secure we are going to use a Let’s Encrypt certificate. This part is optional but highly recommended; For this we do not need to have a domain or dynamic DNS, although if we have one of these two things the configuration will be much easier. In case of not having either of the two options, we can still use the server to host the validation file through the Webroot Local Folder option or in the worst case the Standalone option.

The first thing of all will be to install the necessary packages in pfSense. For this we will go to System → Package Manager → Available packages and install the ACME and HAProxy packages.

Configuration and obtaining the Let’s Encrypt certificate (OPTIONAL)

In our pfSense we will go to Services → Acme Certificates → Account keys and click Add.

We will choose a name and as ACME server we will choose Let’s Encrypt Production ACME v2, we will fill in our email address and click on Create to generate our account key.

Next we will click on Register ACME account key and then on Save.

Once we have the password for our account we can create our certificate. To do this we go to Certificates and click Add.

We will give it a name and description, and we will make sure that the account we just created is selected under ACME account.

We will go down to Domain SAN List; This is where we will validate that we own the destination of the certificate. For the tutorial I will use my domain but if you do not have one and your DDNS service accepts TXT records (such as DuckDNS) you can also use it. If not, you can use the Webroot or Standalone local directory methods..

We are going to generate a wildcard certificate that will be valid for the domain and all subdomains. For this we are going to create an entry with *.domain_name in the FQDN field. In method we will choose our DNS provider and we will fill in the data that it asks for. If our provider is not on the list we will choose manual.

Then we will click on Save and this will take us back to the screen with the list of certificates.

Once on this screen we will see our certificate with issue date January 1, 1970, we will click on the Issue/Renew button and if everything goes well a green message will appear at the top of the screen.

It may be that in this message we have lines similar to these:

[Sat Jun 19 17:10:38 ACDT 2021] Add the following TXT record:
[Sat Jun 19 17:10:38 ACDT 2021] Domain: ‘_acme-challenge.danatec.org’
[Sat Jun 19 17:10:38 ACDT 2021] TXT value: ‘EfUgo1h8THgJH78YUiJGYHgfRTf33YJHiH’

If so, we must add a new TXT DNS entry with the value indicated in TXT value in our DNS provider.

After adding the TXT entry (if necessary) we will click on Issue/Renew again to see that the certificate is renewed without problems; We will reload the page and if everything has gone well we will see that the renewal date matches the current date.

Finally, in the General Settings tab, we will activate Cron Entry to make sure that the certificate is automatically renewed.

With this we conclude the configuration of the SSL certificate. Next we will see how to configure HAProxy.

Configure HAProxy

To configure HAProxy we will go to Services → HAProxy → Settings.

On this screen we are going to check the Enable HAProxy checkbox and set the Maximum connections value to 1000 and the Max SSL Diffie-Hellman size to 2048. Then we will press the Save button.

Configure Backend

Next we will go to the Backend tab. In this tab is where we are going to define our server or servers.

To add a server we will press the Add button, we will give it a name (I use the name of the server or subdomain to which it is going to refer) and we will press the arrow-shaped button indicated in the following image.

A drop-down will appear in which we will fill in at least the following parameters:

  • Name: Here we will fill in the subdomain or name of the server.
  • Address: The IP address of our server.
  • Port: The port on which the server is listening.

It will not be necessary to fill in any of the fields referring to the certificates since this is handled by HAProxy and not the servers.

Note: My web server is listening on port 80, but if your server is listening on another port you will have to fill it in here.

On this screen there are many options, take a look at them and try the ones that seem interesting to you.

Modifications for Home Assistant

When I was configuring the Home Assistant Backend I ran into a problem. The method to check the health of the server that is assigned by default (Http check method → OPTIONS) did not work correctly and when I tried to access Home Assistant in the browser a 503 error appeared.

This I have fixed by changing the server health check method to Http check method → GET.

Configure Frontend

First we are going to create a common frontend for all HTTPS traffic.

We are going to go to the Frontend tab and press the Add button.

As the name of the service we are going to use https_shared.

In port we will select port 443 and mark the SSL Offloading checkbox. Then we will go down to the SSL Offloading section and select the certificate that we have created previously.

If we do not use an SSL certificate, we will leave the SSL Offloading checkbox unchecked and we will not select anything in the SSL Offloading section.

Next we are going to create another Frontend to redirect HTTP traffic to HTTPS.

We will create a new rule called http_redirect that listens on port 80 of the WAN interface, with the SSL Offloading box unchecked.

We will move to the actions section and create a new action by pressing the green arrow. As an action we will choose http-request redirect and in rule we will write scheme https.

We will save and apply the configuration.

Then we can create the frontends of our servers or services.

To do this we create a new frontend, we will give it a name, we will mark the Shared Frontend checkbox and we will select https_shared.

Next we will add an entry in the Access Control lists by pressing the green arrow.

Here we define criteria that will serve as a filter for the actions that we will define later. For example I want that if someone writes www.danatec.org or danatec.org to access the web hosted on my server, for this I have created an entry called web-server with the expression Host matches: and as a value danatec.org and another entry called www-web-server with the expression Host matches: and as value www.danatec.org

There are many more options so you can choose the one that best suits your case.

After this we are going to add the following actions, one for each of the rules that we have defined above:

  • Action → Use Backend
  • Condition acl names → Name of the entry created in Access Control lists
  • Backend → The service or server that we want to expose when the rule is met

Finally in Default Backend we could choose if we want to show another backend in case the previous one does not respond.

We will press Save and apply the changes.

After we have created the services we need, we are going to create some rules in the Firewall.

Frontend WordPress

One of my servers is a WordPress server, which I accessed through Traefik, another reverse proxy that I had configured in a Docker container and which I have decided to move to HAProxy to simplify things.

WordPress was already configured to use an SSL connection but as now the SSL connection is managed by HAProxy, WordPress does not know that the connection is SSL and when trying to access it it received the error “Too Many Redirects”. After giving many turns I have managed to make it work by adding the following actions in the Frontend (it is the same action repeated for each of the rules defined in Access Control lists):

  • Action → http-request header add
  • Condition acl names → Name of entry created in Access Control lists
  • name → X-Forwarded-Proto
  • fmt → https

Creating the Firewall rules

We will go to Firewall → Rules.

We will create a new rule within the WAN tab with the following parameters:

  • Action → Pass
  • Interface → WAN
  • Address Family → IPV4
  • Protocol → TCP
  • Destination → This Firewall (self)
  • Destination Port Range From → HTTP (80)
  • Destination Port Range To → HTTP (80)
  • Description → HAProxy HTTP

We will create another rule also in the WAN interface with these parameters:

  • Action → Pass
  • Interface → WAN
  • Address Family → IPV4
  • Protocol → TCP
  • Destination → This Firewall (self)
  • Destination Port Range From → HTTPS (443)
  • Destination Port Range To → HTTPS (443)
  • Description → HAProxy HTTPS

Once the rules have been created and the changes applied, our servers and/or services will be accessible from outside our network.

Bonus: Protect Backend with username and password

It is possible that we want to access a service on our network but that it does not have any type of authentication, so if we make it accessible, anyone could access it. To avoid this, we are going to see how to protect this service with a username and password.

First of all will be to create a list of users following the instructions in the HAProxy documentation. We can use passwords in plain text although this is not advisable since they will be stored that way. It is best to use encrypted passwords in DES, MD5, SHA-256, or SHA-512 format.

Here we can see two examples of a user list called Danatec with encrypted passwords and in plain text:

# List of users with encrypted passwords
 
userlist Danatec
user User1 password Encrypted_Password
user User2 password Encrypted_Password

# List of users with passwords in plain text

userlist Danatec
user User1 insecure-password Plain_text_Password
user User2 insecure-password Plain_text_Password

Generate encrypted passwords

To generate the encrypted passwords we can use the following command in our Linux distribution:

printf TheSuperSecretPasswordHere | mkpasswd --stdin --method=sha-512

We will have a list of users similar to this:

userlist Danatec
group is-user
user Danatec password CONTRASEÑA_AQUI groups is-user

Once we have our list of users we will paste it in the field Settings → Global Advanced pass thru → Custom options and we will save and apply the changes.

Note: The list of users must always be at the end of the Custom Options.

Modify Backend

Now we are going to modify the Backend that we want to protect with username and password.

We will edit the backend and create a new entry in Access Control lists with the parameters:

  • Name → BackendPassword (any other name is possible)
  • Expression → Custom acl:
  • Value → http_auth(User_list_name), in my case http_auth(Danatec)

We will also create an action with the parameters:

  • Action: http-request auth
  • realm: realm User_list_name unless Custom_ACL_name, in my case realm Danatec unless BackendPassword

We will save and apply the changes and it would be ready. Now when trying to access our Backend it will ask us for username and password.

All users who are in the user list will have access to this Backend; if we want we can also create different groups in the list of users as follows:

userlist Danatec
group is-user
group is-admin
user Danatec password PASSWORD_HERE groups is-user
user AdminDanatec password PASSWORD_HERE groups is-admin

To give access to the Backend only to the administrators group we would do the following:

We will modify the entry in Access Control lists with the parameters:

  • Name → AdminAccess (any other name is possible)
  • Expression → Custom acl:
  • Value → http_auth_group(User_list_name) group_name, in my case http_auth_group(Danatec) is-admin

And we will modify the action with the parameters:

  • Action: http-request auth
  • realm: realm User_list_name unless Custom_ACL_name, en mi caso realm Danatec unless AdminAccess

With this configuration, only users who are members of the is-admin group could authenticate.

If you have made it this far, thank you very much! If you have any questions, do not hesitate to leave them in the comments and I will do my best to help.

And don’t forget to subscribe to receive an email when new articles are published.

Don’t miss any new post!

We don’t spam! Read more in our privacy policy

5 thoughts on “Reverse Proxy with HAProxy + ACME in pfSense”

  1. “Modifications for Home Assistant
    When I was configuring the Home Assistant Backend I ran into a problem. The method to check the health of the server that is assigned by default (Http check method → OPTIONS) did not work correctly and when I tried to access Home Assistant in the browser a 503 error appeared.

    This I have fixed by changing the server health check method to Http check method → GET.”

    Can you explain how you got to here? I have followed along but I get 503 error when pulling up HA in the web browser.

    1. Hi Jeff,

      What I did was the following:

      In the HAProxy configuration, within the backend configuration You should have a Backend for Home Assistant.

      When you edit it, you will see a section called “Health Check”; Inside that section there is a line called “Http check method” that was configured by default as “OPTIONS”; I changed it to “GET” and in my case this fixed the problem.

      You can also check that your Home Assistant configuration.yaml file contains the following lines:


      # Reverse Proxy configuration
      http:
      use_x_forwarded_for: true
      trusted_proxies:
      - PfSense IP address (probably your gateway IP)
      - 127.0.0.1
      - ::1

      Let me know if this fixes your problem

  2. How do you avoid blocking yourself out of the web interface for pfsense? Immediatly after I complete the step “Configure Frontend” – poof I get to: “The Connection fo this site is not secure”. And now it shows “FQDN.hostdomain.com sent an invalid response” ERR_SSL_PROTOCOL_ERROR.

    I can roll back to the last change – but I don’t know how to protect the pfsense.hostdomain.com from getting locked out.

Leave a Comment

Your email address will not be published.