One public IPv4 address and a dozen internal web services is the normal state of a small hosting environment or office. HAProxy on pfSense solves the problem by terminating TLS at the firewall and routing by hostname to the right backend, while the ACME package keeps the certificates renewed without anyone touching them. This tutorial uses pfSense CE 2.9 with the haproxy and acme packages and publishes two applications, app.example.com and git.example.com, on the same address.
Table of Contents
Short answer: Install the ACME and HAProxy packages, create an ACME account key and a certificate for your hostnames using a DNS or HTTP challenge, then in HAProxy define one backend per internal server, a shared frontend on WAN port 443 that offloads TLS with the ACME certificate, and ACL rules that match the host header to send each name to its backend. Add a WAN firewall rule for TCP 443, enable HAProxy, and check that the certificate renews by watching the ACME package log.
Issue the certificate with the ACME package
Install both packages from System » Package Manager. Under Services » ACME Certificates » Account keys, create a key for the Let’s Encrypt production server and register it. Then under Certificates add one entry with both hostnames as domain SANs. Choose the validation method carefully: the DNS method with your DNS provider’s API is the most reliable because it does not depend on port 80 reaching pfSense, and it is the only option for wildcard names.
If you use the webroot or standalone HTTP method, add a WAN rule for TCP 80 first and make sure nothing else is listening there. Issue the certificate and confirm it appears under System » Certificate Manager. Certificates issued by Let’s Encrypt are short-lived and the package handles renewal on a cron schedule, so tick the option to run a renewal check and leave the default interval.
Define HAProxy backends
Under Services » HAProxy » Backend add one entry per application. Give it a name such as app_backend, add the server with its internal address and port, and set the health check to HTTP with a path that returns 200. If the internal service speaks HTTPS with a self-signed certificate, tick SSL on the server line and disable certificate verification for that server. Set the balance mode even for a single server; it does no harm and simplifies adding a second later.
Build the shared frontend
Under Frontend add one entry listening on the WAN address, port 443, type http / https (offloading). Under SSL Offloading select the ACME certificate and enable the option to add ACME-issued certificates automatically. Then add access control lists: one named is_app of type Host matches with value app.example.com, one named is_git with git.example.com. Under Actions add a Use Backend action per ACL pointing at the matching backend. Set a default backend if you want unmatched names to land somewhere sensible, or leave it blank so they fail closed.
Add a second small frontend on port 80 of type http that redirects everything to HTTPS with an action of http-request redirect and rule scheme https code 301, unless you use the HTTP ACME challenge, in which case the ACME package installs its own redirect exception.
Under Settings enable HAProxy, set a maximum connection count and, on the same page, enable the stats page on an internal port so you can watch backend health. Save and apply. On Firewall » Rules » WAN add allow rules for TCP 443 and, if used, TCP 80 to the WAN address.
Verify
Test from outside the network so the WAN rules are exercised:
curl -sI https://app.example.com | head -5
curl -sI https://git.example.com | head -5
openssl s_client -connect app.example.com:443 -servername app.example.com </dev/null 2>/dev/null | openssl x509 -noout -dates -subject
Each name should return the right application’s headers and the certificate dates should be recent. On pfSense, Status » HAProxy Stats shows every backend in green; a red backend means the health check path is wrong or the internal firewall rule from the pfSense interface to the server is missing. The ACME package’s log under Services » ACME Certificates » General settings records each renewal attempt.
Pitfalls and ongoing care
The most common failure is a certificate that issues correctly but never reaches HAProxy because the frontend was created before the certificate existed and still references the pfSense default certificate. Re-open the frontend and select the ACME certificate explicitly. Another trap is HTTP challenge validation failing because Cloudflare or another proxy sits in front of the address and rewrites or caches the challenge path; the DNS method avoids that entirely.
Since public CAs now issue certificates with lifetimes measured in weeks rather than a year, do not disable the ACME cron job, and pair this setup with an external check such as the SSL expiry check script so a silent renewal failure is noticed before browsers complain.
PfSense HAProxy at a glance

Official documentation: pfSense documentation, Let’s Encrypt documentation, Linux man pages.
Related guides: Configure VLANs on pfSense with a managed switch · Install pfSense CE 2.9 step by step with the network installer and ZFS · Troubleshoot MTU, fragmentation and slow VPN throughput.
Frequently asked questions
Does HAProxy on pfSense also work for non-HTTP TCP services?
Yes. Create a frontend of type TCP and a TCP backend for services such as SMTP or a database, but hostname routing is not available there because it relies on the HTTP Host header or TLS SNI.
How long does Let’s Encrypt validation take on pfSense?
An HTTP challenge completes in under a minute. DNS challenges take a few minutes because the package waits for the TXT record to propagate before asking the CA to validate.
Can I undo the HAProxy setup and return to plain port forwards?
Yes. Disable HAProxy under Settings, remove the WAN rules for 80 and 443, and add NAT port forwards to a single internal server; the ACME certificates remain in the certificate manager for later use.