Caelan's Domain

Configuring HTTPS with a reverse proxy using pfSense

Created: April 12, 2025 | Modified: January 18, 2026

Introduction

Most HTTPS guides cover one piece of the puzzle, and the rest are 20-minute YouTube videos. I wrote this because I needed documentation that covered the whole thing in one place. Friends found it useful, so here it is.

If something is unclear, let me know and I'll fix it.

This guide uses pfSense with HAProxy as the implementation example, but the architecture applies to any reverse proxy (nginx, Caddy, Traefik). You'll need familiarity with DNS records and your reverse proxy's configuration interface.

Implementation stack: pfSense CE 2.7.2, HAProxy 0.63_2, ACME 0.9.1. Any reverse proxy capable of SSL termination, HTTP-to-HTTPS redirects, and SNI-based routing can replace HAProxy. Certificate management can be handled by Certbot, the ACME package, or your reverse proxy's built-in tooling.

Security considerations
Following this guide WILL expose your network to the public internet. You are responsible for the security and maintenance of what you configure. The safest network is one that isn't exposed publicly.

Traffic flow

Reverse proxy flow

This diagram covers what's configured in this guide, not a complete technical reference.

Regardless of which reverse proxy you use, the architecture is the same: public traffic hits the proxy first. The proxy handles SSL termination, inspects the request, and forwards it to the appropriate internal service. Your backend services never need to manage their own certificates or be directly internet-facing.

Public DNS records

Set public DNS records to point at host

This is required for Let's Encrypt certificate generation. Allow time for DNS propagation.

Let's Encrypt (and any ACME-based CA) needs to verify you control the domain before issuing a certificate. Your DNS A/AAAA records must point to your public IP, or you must use a DNS challenge (covered below). Certificate issuance will fail without this.

Set up DDoS protection via Cloudflare

Cloudflare's free plan handles DDoS protection well enough for most self-hosted setups. Upgrade if you outgrow it.

SSL certificate

Let's Encrypt certificates expire every 90 days. Configure automated renewal from the start - manual renewal is how you discover your site is down on a Saturday. Caddy, Traefik, cert-manager, Certbot with a cron job, and pfSense's ACME package all handle this.

These instructions use the ACME package on pfSense. You can also manually import a certificate via System > Certificates.

Use a DNS challenge for your certificate. It's better than the older HTTP-01 method in most self-hosted scenarios. See Let's Encrypt challenge types for details.

Two validation methods: the DNS-01 challenge validates domain ownership via a TXT record in your DNS zone (works even if port 80 is blocked, required for wildcard certificates). The HTTP-01 challenge validates by placing a file at http://yourdomain/.well-known/acme-challenge/. DNS challenge is generally preferable for self-hosted setups.

If certificate issuance fails, check Let's Debug.

Set up account key

Let's Encrypt uses this to identify who's requesting a certificate.

Account keys

Create SSL certificate request template

Using Namecheap as an example here. Fill in the fields to create your certificate template. You'll probably need an API key from your DNS registrar.

ACME request form

Issue a certificate

Hit Issue/Renew. Check the "Last renewed" column to confirm it worked. This can take a minute, so move on and check back.

Certificate example

Network configuration

Your reverse proxy needs a stable address to listen on. In pfSense this is a virtual IP; in a cloud or Linux environment it might be a dedicated interface, a loopback address, or the server's primary IP. Ports 80 and 443 must be forwarded to this address from your public WAN interface.

Create a virtual IP

(pfSense/HAProxy specific) This is the address HAProxy will listen to for requests. This virtual IP acts as a loop-back target.

Virtual IP

Create NAT rules and port forwards

THIS STEP WILL OPEN YOUR WAN ADDRESS TO INCOMING TRAFFIC BY DEFAULT:

NAT filter auto-generation

The GUI defaults to "Add associated filter rule," so forwarding a port also creates a firewall rule to pass that traffic.

Create port forwards for 80 and 443, both pointing to the virtual IP you set up above.

Why not block HTTP traffic entirely?
HTTP should not be used today, and you don't want clients connecting over HTTP. So why not block it entirely?

If a user types "caelandrayer.ca" into a browser, it may default to HTTP. End users are unlikely to understand this behavior and may believe the site is down or broken. Redirecting ensures that no matter how users access your site, they reach the right place.

While less common now, some API and SEO aspects would also be impacted.

Does HTTP redirect mean I'm safe?
No. Since users initially connect without a certificate, there's still risk from attacks like man-in-the-middle (MiTM).

To mitigate this risk, use HSTS (HTTP Strict Transport Security):

We configure this in the HTTPS backend actions below.

Port forward

This auto-generates two filter rules on the WAN interface:

Filter rule

Set up DNS resolver domain/host overrides

(pfSense/HAProxy specific) This makes sure internal DNS resolves to the virtual IP where HAProxy listens. Set the lookup server IP to the virtual IP you configured earlier.

Domain overrides

(Some setups don't need this, but if you skip it, other parts of the guide won't apply directly.)

When internal services try to reach a domain that points publicly to your WAN IP, they may fail due to NAT hairpinning. The fix - split-horizon DNS - makes internal DNS resolve that domain to the proxy's internal address instead. In pfSense this is a DNS resolver override; elsewhere it might be a /etc/hosts entry, a Pi-hole custom record, or a split-view DNS zone.

HAProxy configuration

Most reverse proxies organize configuration into two pieces: where traffic comes in (the "frontend" or "listener"), and where it goes (the "backend" or "upstream"). HAProxy calls them backends and frontends; nginx uses server blocks and upstream; Caddy uses reverse_proxy directives; Traefik uses routers and services. Same concept, different vocabulary.

Create HAProxy backend

The backend tells HAProxy where to forward traffic.

  • Name: Descriptive only, pick whatever makes sense to you
  • Address: LAN/local IP of the service
  • Port: Port the service listens on

Backend

In advanced settings, set up HSTS and security headers.

Security header warning
These settings can break things. This is an example. Open DevTools on your site (typically F12), inspect for errors, and resolve. Each browser will react differently.

Security headers configuration

http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
http-response add-header Content-Security-Policy "default-src 'self'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self'; frame-ancestors 'self'; form-action 'self'; base-uri 'none'"
http-response add-header X-Frame-Options "DENY"
http-response add-header X-XSS-Protection "0"
## Do NOT add a blanket Content-Type header here -- it would override correct
## Content-Types for CSS, JS, images, and JSON responses served through this backend.
http-response add-header X-Content-Type-Options "nosniff"
http-response add-header Referrer-Policy "strict-origin-when-cross-origin"
http-response add-header Cross-Origin-Opener-Policy "same-origin"
http-response add-header Cross-Origin-Embedder-Policy "same-site"
http-response add-header Cross-Origin-Resource-Policy "same-site"
http-response add-header Permissions-Policy "geolocation=(), camera=(), microphone=()"

These response headers are standardized HTTP - the names and values are identical regardless of which proxy you use. Only the syntax differs: nginx uses add_header, Caddy uses header, Traefik uses middleware.

Customize security headers
These settings are starting defaults and should be improved. Tailor settings to each site. More information:

Why configure headers in the backend?
The easiest location to inject headers is the backend (adding to frontend is also possible, but you can't copy and paste in those fields/actions).

Different sites may require different settings.

Unique backends
Every unique service requires its own backend.

Configure HAProxy HTTP frontend to redirect to HTTPS

This frontend listens on the virtual IP for HTTP (port 80) traffic:

HTTP redirect

Configure an action to redirect traffic to HTTPS:

HTTP redirect action

What each setting does:

  • scheme https: Redirect to HTTPS
  • code 301: Permanent redirect (browser caches it)
  • unless { ssl_fc }: Skip if already HTTPS (prevents redirect loops)

Read more about HTTP to HTTPS redirects

Every reverse proxy handles this differently. In nginx: return 301 https://$host$request_uri;. In Caddy, it's automatic. In Traefik, use an http entrypoint with a redirectscheme middleware. The 301 tells browsers to cache the redirect.

Configure HAProxy HTTPS frontend

This frontend handles HTTPS traffic. Point it at the virtual IP and enable SSL offloading.

HTTPS frontend

Set up an Access Control List (ACL). ACLs "tag" traffic so you can route it to the right backend.

HTTPS frontend ACL

Create an action for that ACL. The ACL rule name and "Condition acl names" field must match exactly. Here, matching traffic goes to the "caelandrayer.ca" backend.

HTTPS frontend action

When a single proxy handles multiple domains, it reads the hostname from the TLS handshake (SNI) or the HTTP Host header and routes to the right backend. HAProxy calls these ACLs; nginx uses server_name; Caddy and Traefik match on hostname in the site block or router rule.

Unique ACLs and actions
Every unique service requires its own ACL rule and action.

Optional, but recommended:

HTTPS frontend forward-for

Without this, your backend sees the proxy's IP on every request, not the client's. The X-Forwarded-For header preserves the real client IP for logging. Most reverse proxies add this automatically or via a configuration toggle.

Pick the primary certificate for SSL offloading:

HTTPS frontend SSL offload

SSL termination means the proxy decrypts HTTPS and forwards plain HTTP to your backend. Your internal services don't need certificates. The alternative - SSL passthrough - forwards encrypted traffic directly to the backend, which then needs its own certificate and gives up the proxy's ability to inspect or modify requests. Termination at the proxy is the standard approach.

Additional certificates
Every unique service requires a certificate. If configuring another site for HTTPS, add the certificate to the "Additional certificates" menu option.

HTTPS frontend additional certificates

Enable HAProxy

Make sure HAProxy is enabled. Set "Maximum connections" to something reasonable for your hardware.

HAProxy enable

Testing

Check two things:

  • Does HTTP redirect to HTTPS?
  • Is the certificate valid?

Once that works, test from outside your network:

  1. Scan with Mozilla Observatory
  2. Submit to HSTS preload

Troubleshooting

Certificate issuance fails. Check that your DNS A record points to your public IP and has propagated (use dig or DNS Checker). If using a DNS challenge, verify your API key and that the TXT record is being created. Let's Debug will diagnose most ACME failures.

502 Bad Gateway or connection refused. HAProxy is running but cannot reach the backend service. Verify the backend IP and port are correct, and that the service is actually listening. Check HAProxy stats page if enabled.

Site loads over HTTPS but shows certificate warnings. The wrong certificate is being served - check that the certificate in your HTTPS frontend matches the domain being accessed. If you have multiple domains, verify the additional certificates list.

Internal clients cannot reach the site. DNS hairpinning issue. Confirm your DNS resolver override (or split-horizon DNS) points the domain to the virtual IP internally, not the public WAN IP.

Security headers breaking the site. Open browser DevTools (F12), check the Console tab for CSP violations, and adjust the Content-Security-Policy header in your backend configuration. Start permissive and tighten.


Next Steps

At this point you have a working reverse proxy with automated certificate renewal and security headers. From here: add backends for additional services, tighten your CSP per-site, and monitor certificate expiry through the ACME package dashboard.