openHAB with reverse proxy using OPNsense and HAProxy

I’ve been cleaning up my homelab and router configurations and when I get something relevant to OH I like to post a quick tutorial in case anyone else is interested. If there is interest I can post a full layout of how I run everything in the offtopic section. My previous post was on dealing with running your own DNS server in a dual IPv4/IPv6 network which, Matter requires. AdGuardHome/PiHole in a IPv6 world (plus parental controls)

Why a Reverse Proxy if I’m not Exposing services to the Internet?

Due to technical limitations, OH works best when using HTTPS. However, using HTTPS without the browser throwing up “this page is unsafe!” warnings first requires a trusted certificate.

One could create a CA, add that CA as trusted on all your machines, and then sign a certificate with that CA and change OH’s built in certificate with that. That certainly would work but it comes with the long term maintenance headache. The CA certificate would have to be added to every new device that joins your network. Do you really want to have to tell your house guests to hand over their phone so you can mess with their settings so they cna turn off the lights without the security warning?

Note, I am not using this reverse proxy to expose openHAB to the Internet. If you need remote access to OH, the free openHAB Cloud Server (i.e. myopenhab.org) is your first best option. Your second best option will be to set up Tailscale. I do not recommend exposing OH to the Internet directly, even through a reverse proxy.

Having said that, I do have a few services that I do have to expose to the Internet because setting up Tailscale on devices for family members and teaching them how to use it properly is more long term work than the security and monitoring required to expose a couple of hardened services to the internet. This will be apparent in my HAProxy configs you see below.

Prerequisites

  • a domain name and, if required DynDNS set up to keep that domain synced with your public IP address. I use Namecheap and put enough money on balance to let me use the API the DynDNS client can update the IP and ACME can auto-renew my LetsEncrypt certificate
  • locally hosted DNS server so you can add a DNS rewrite (see link above)
  • I’m running OPNsense 26.1.6 but these settings have looked the same for quite some time
  • ACME Client, and HAProxy installed
  • The OPNsense web admin UI needs to be configured to listen on ports other than 80 and 443 (HAProxy is going to need to listen on those).

You probably can use these instructions to make it work with different configurations and base software but this is what I am using.

Note: I set some of this up years ago. Some of the steps may be a little vague and require you to go off and do some research (or ask an AI chatbot).

LetsEncrypt

This is one of those places where I’m going to be a little vague. Most of the details for setting up LetsEncrypt will be related to where you got your domain name from. But in general you need to make sure you’ve installed the ACME Client, configure it to work with your domain name provider and set up your login for LetsEncrypt and all that stuff. There are tons an tons of tutorials out there for this. You might need to configure HAProxy for this too if you end up using the ACME HTTP-1 or TLS_ALPN-1 challenge type instead of the DNS-1 challenge type.

Note: With the HTTP and TLS_ALPN challenge types, LetsEncrypt will attempt to connect to your servers to prove you own the domain. With the DNS challenge type, LetsEncrypt will only need to interact with your domain name registrar, but your registrar requires a supported API so the ACME client can edit a TXT record and LetsEncrypt can read that TXT record.

Note: NameCheap requires you to create an IP address allow list specifying which IP addresses are allowed to access your account. Normally this would be a good thing but this means you need to find all the LetsEncrypt IP addresses and add them to the allow list in addition to your public IP address.

You could also just buy a certificate from one of the many other certificate authorities.

The biggest thing is you need to get a wild card certificate. For example, if your domain is foo.com, you need a certificate for *.foo.com. This will let us use this same certificate for all our home services. It also means we don’t need to mess with any of the sub-folder nonsense (e.g. hosting at https://foo.com/openhab) which many services, openHAB included do not support.

Once it works, you’ll have a new certificate in OPNsense controlled by the ACME client. You will definitely want this certificate to automatically renew.

DNS

You probably do not want to leak your DNS queries to the Internet nor have your traffic go out and come back in to access your local services. So we need a DNS rewrite on our self hosted DNS server. You want a rewrite for *.foo.com to <ip of HAProxy> so any domain ending with foo.com will be routed striaght to the HAProxy and never leave your LAN.

HAProxy

Unlike ngnx and Apache which are web servers first, HAProxy is a load balancer first. This means it’s configuration is more modular which means it’s also a little more scattered. I’m going to show the UI way but you can edit the onfig file yourself. But you’ll have to look up the formatting and fields.

The configuration is split into:

  • Real Servers: this is where you define the IP address and port inside your LAN where the server is listening.
  • Backend Pools: this is where you link HAProxy to one or more “real servers”.
  • Public Services: here is where you create the port 80 and 443 mappings which are exposed to the internet. Access control and redirects are implemented here.
  • Health Monitors: these are periodic checks HAProxy will do to see if a real server is online or not. It will return a 503 error if it determines a service is offline.
  • Conditions: these will match requests based on various criteria (e.g. match internal IP addresses, match requests to openhab.foo.com, etc).
  • Rules: these “do stuff” to the requests such as rerouting, blocking, adding/removing headers, etc. Rules can work with conditions.
  • User Management: here is where you can define users and groups to put basic auth in front of your services that don’t have this.
  • Map Files: this is the only “Advanced” configuration we will use with a rule to map domain names with a backend pool.

This seems like a lot but in many ways this modularity and spread out approach can save a lot of work. For example, you can reuse health checks so even if you have two dozen services, you can only define one or two health checks to work with them all. Once it’s all set up, going forward you’ll only need to edit the real servers, backend, and map file for the most part.

Listen for HTTPS

First we will create a service that listens on port 80 and port 443. In the tables below, if a field is not mentioned leave it at the default value.

Real Servers

Under Services → HAProxy → Real Servers → Real Servers click + to create a new real server.

Field Value Notes
Enabled checked
Name or Prefix SSL_server
Description HAProxy SSL Server
Type static
FQDN or IP 127.0.0.1

After each setting is made, save and then “Test Syntax” for errors. Do not wait for the end to test the syntax.

This will take any incomming traffic and route it to 127.0.0.1 on the same port.

Backend Pools

Under Services → HAProxy → Virtual Services → Backend Pools + to create a new backend pool.

Field Value Notes
Enabled checked
Name or Prefix SSL_backend
Description SSL offloading backend
Mode TCP (Layer 4)
Servers SSL_server this is what we created above

Public Services

Under Services → HAProxy → Virtual Services → Public Services click + to create a new endpoint.

Field Value Notes
Enabled checked
Name 0_SNI_frontend
Description Listening on 0.0.0.0:80, 0.0.0.0:443
Listen Addresses 0.0.0.0:80 0.0.0.0:443 type one, hit enter, then type the second one
Type TCP
Default Backend Pool SSL_backend this is what we created above
Field Value Notes
Enabled checked
Name 1_HTTPS_frontend
Description Listening on 127.0.0.1:443
Listen Addresses 127.0.0.1:443 type then hit enter hit enter
Bind option pass-through accept-proxy
Type HTTP/HTTPS (SSL offloading)
Default Backend Pool none
Enable SSL offloading checked
Certificates LetsEncrypt certificate
SSL option passthrough curves secp384r1
Enable Advanced Settings checked
Cipher List ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256
Cipher Suites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
Enable HSTS checked
HSTS IncludeSubDomains checked
HSTS Preload checked
HSTS max age 15552000
Bind options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
Enable HTTP/2 checked
Advertise Protocols HTTP/2 HTTP/1.1
X-Forwarded-For checked this is now marked deprecated, IO need to research what replaces it
Select Rules we will come back and add rules later

At this point all traffic comming from outside your LAN will hit the 0_SNI_frontend which will pass it through the SSL_backend to the SSL_server which will pass the HTTPS traffic on 443 to 127.0.0.1:443 which is where 01_HTTPS_frontend picks it up. We will add some rules later to tell the HTTPS frontent what to do with the traffic.

Note: I don’t remember why I set it up like this. I think it had something to do with monitoring.

Reroute HTTP to HTTPS

This will automatically upgrade any HTTP traffic to HTTPS.

Conditions

Under Services → HAProxy → Settings → Rules and Conditions → Conditions click + to add a new condition.

Field Value Notes
Name NoSSL_condition
Description Traffic is not SSL encrypted
Condition type ssl_fc - Traffic is SSL (locally decyphered)
Negate Condition checked this makes the rule match all non-SSL traffic

Rules

Under Services → HAProxy → Settings → Rules and Conditions → Rules click + to add a new rule.

Field Value Notes
Enabled checked
Name HTTPtoHTTPS_rule
Description Redirect HTTP to HTTPS
Select Conditions NoSSL_condition created above
Logic Operator None
Type http-request
Action ridirect
Options/Values scheme https code 301

Public Services

Field Value Notes
Enabled checked
Name 1_HTTP_frontend
Description Listening on 127.0.0.1:80
Listen Address 127.0.0.1:80 type in and hit enter
Bind option pass-through accept-proxy
Type HTTP/HTTPS (SSL offloading)
Enable HTTP/2 checked
Advertise Protocols HTTP/2 HTTP/1.1
X-forwarded-for checked
Select Rules HTTPtoHTTPS_rule

From here any traffic on port 80 passes from 0_SNI_frontend which will pass it through the SSL_backend to the SSL_server which will pass the HTTP traffic on 80 to 127.0.0.1:80 which is where 01_HTTP_frontend picks it up. The HTTPtoHTTPS_rule detects the not-encrypted HTTP connection and issues a redirect to the same URL but as HTTPS where we go through the flow above.

So all HTTP traffic gets routed to HTTPS and all HTTPS traffic will be routed based on some rules we will set up.

Service to Backend Mapping Rules

Here we will create a map file that maps domain names to backend pools. In the future, when you add a new service to your system, you will only need to add a line to this file. If you take a service down or want to change the name you can just edit this file. This greatly simplifies the configuration if you have a lot of services to manage like I do.

Map Files

Go to Services → HAProxy → Settings → Advanced → Map Files and click + to create a new file.

Each line will take the form of <FQDN> <backend>. For example, if your domain name is “foo” and your backend is named “openhab_backend” (we’ll create this soon) the line will be openhab.foo.com openhab_backend.

You’ll have one line per service.

Go ahead and add your openHAB entry to this file now or wait. I’ll mention it again later when we add openhab as a service.

Field Value Notes
Name all_services
Description All hosted services mapping to backends
Type dom - Domains

Note, you could put this file somewhere that HAProxy can download it over HTTP(s) (e.g. an internal gitea server) in which case you’d put the FQDN to backend mappings in that file and fill out the Download URL.

Rules

Field Value Notes
Enabled checked
Name Unified_Routing_Map
Description Map domains to backends using a map file
Logic Operator None
Type Map domains to backend pools using a map file
Map file all_services

Apply the Rule

Return to the 1_HTTPS_frontend and down at the bottom, select Unified_Routing_Map as a rule to apply.

This will cause all HTTP requests to be matched against the map file and route the traffic to the backend defined in the map.

Protect Internal Services

Most if not all of your services should not be exposed to the internet. Let’s add a rule that allows internal traffic to reach all services defined in that map file and rejects all connections from outside. This way only services you explicitely define can be reached from outside your LAN (if any).

Conditions

Field Value Notes
Name is_not_public_host
Description Matches services that should be exposed to the internet
Condition type hdr - HTTP Host Header matches
Negate Condition checked this makes it match all domains not in the list
Host String <domain> <domain> ... space separated list of domains to expose to the internet. For example if you have Nextcloud and Vaultwarden to expose you might enter nextcloud.foo.com vaultwarden.foo.com.

Note, I think these too could be defined in a map file but I ran into trouble and gave up. I might come back someday to try again.

Field Value Notes
Name is_not_source_internal
Description Matches LAN and Tailscale clients
Condition type src - Source IP matches specified IP
Negate Condition checked this makes it match all IPs not in the list
Source IP <LAN ip range> 100.64.0.0/10 If your IP range is 192.168.1.1-255 enter 192.168.1.0/24 for <LAN ip range>. If you don’t use Tailscale omit the 100.64.0.0/10.

Rules

Field Value Notes
Enabled checked
Name Gatekeeper_Deny_if_not_Public_and_not_Internal
Description Allows access to public services from internet, all from internal
Select conditions is_not_public_host, is_not_source_internal
Logic Operator AND
Type http-request
Action deny

Return to the 1_ HTTPS_frontend and add the Gatekeeper_Deny_if_not_Public_and_not_Internal rule. Rules are processed in order so drag this one to be before the Unified_Routing_Map rule.

At this point, and URL that matches the map file will be routed to the correct backend. Any URL with a domain that is from the outside and not one of the ones listed in the is_not_public_host will get denied.

Health Checks (Optional)

So far I’ve found these three health checks to work with just about everything I’m hosting. YMMV. If a healthcheck fails you can always add another one. Or you can not enable health checks at all. Sometimes they are more of a pain than the benefit they provide.

Navigate to Services → HAProxy → Settings → Checks & Rules → Health Checks and click + to add a new one.

Field Value Notes
Enabled checked
Name HTTP GET Check
Description Uses the server settings to do a simple HTTP check
HTTP Method GET

This one will work almost all the time.

Field Value Notes
Enabled checked
Name HTTP GET with host - router
Description HTTP/1.1 GET with host
HTTP Method GET
HTTP Host <ip or fqdn of your OPNsense host>

OPNsense’s webui wouldn’t work with the simpler helath check but adding the hostname and HTTP version to the check works. This is a case of a one off health check.

Field Value Notes
Enabled checked
Name TCP Check
Description Works around basic auth protected servers
Check type TCP

This is a fall back check which really just pings the host and port.

Add a Service (Finally!)

Everything above only needs to be done once. You shouldn’t have to mess with any of that stuff ever again unless you need another special one off health check.

The general steps to add a service to your reverse proxy are:

  1. Add the Real Server
  2. Add the Backend Pool
  3. Add an entry to the map

Real Server

We will use the OPNsense admin webpage as our first service because openHAB requires some extra settings and I want to show the more typical flow. Also, if you are following this tutorial I know you have this on your network.

Field Value Notes
Enabled checked
Name or Prefix router_server
Description Router and firewall
Type static
FQDN or IP <IP address> I recommend against FQDN becuase if your service or DNS is down when HAProxy starts up, HAProxy will refuse to start up. There is a way to get around that by adding static mappings to your DNS server but I’ve elected not to do that.
Port What ever you moved the web admin port to above.
SSL checked Assuming you put the HTTPS port in above, unchecked if using HTTP.
Verify SSL certificate Unchecked OPNsense is using a self signed cert so verification will fail

Backend Pool

Field Value Notes
Enabled checked
Name or Prefix router_backend
Description OPNsense web ui
Servers router_server
Enable Health Cheking checked
Health Monitor HTTP GET with host - router
Option passthrough timeout tunnel 1h any web service that uses websockets needs a long timeout tunnel

Mapping

Open the all_services map file and add a line with the domain you want and “router_backend” as the backend. For example

 router.foo.com router_backend

It’s case sensitive so I usually copy and paste from the backend to make sure I get it right.

Expose to the Internet

If you have a service you just have to expose to the internet, all you need to do is edit the is_not_public_host condition and add the domain exactly as you entered it in the map to the list of domains to match.

By default all services are only accessible locally. You will need to add a firewall rule to allow HTTP and HTTPS connections to port 80 and 443 on WAN and LAN.

Test it Out

Finally we can try this out! Any machine on your LAN should now be able to go to https://router.foo.com to bring up the OPNsense web admin page. Furthermore, the browser will not complain about the self signed certificate.

Any device outside your LAN would receive a 403 Forbidden code.

Note: If you try to go to a host not in your map file you’ll get a 503 error code. I don’t like that it’s a different code if you happen to guess the URL correctly so I’m going to research how to make it return the same code (I think it’s just a change to the http-response from deny to something else) and possibly just forward all these to a “fail whale” type page.

openHAB

Finally! The whole reason we are here!

Real Server

Field Value Notes
Enabled checked
Name or Prefix openHAB_server
Description Home automation
Type static
FQDN or IP <IP address> IP addres of your openHAB host
Port 8443
SSL checked Unchecked if you use port 8080 instead
Verify SSL certificate Unchecked openHAB is using a self signed cert so verification will fail

Rules

openHAB requires a little extra to work behind a reverse proxy. We need to create four rules to pass through and set/unset some headers so everything is happy. These will be applied to the openHAB_backend so they only impact openHAB and not the other services.

Field Value Notes
Enabled checked
Name openhab_auth_cookie
Logic Operator None
Type http-response
Action set-header
Options/Values Set-Cookie X-OPENHAB-AUTH-HEADER=1
Field Value Notes
Enabled checked
Name openhab_clear_auth
Logic Operator None
Type http-request
Action del-header
Options/Values Authorization
Field Value Notes
Enabled checked
Name openhab_cors_origin
Logic Operator None
Type http-response
Action set-header
Options/Values Access-Control-Allow-Origin *
Field Value Notes
Enabled checked
Name openhab_hsts_header
Logic Operator None
Type http-response
Action set-header
Options/Values Strict-Transport-Security “max-age=31536000”

See the reverse proxy docs for an explanation on why these are needed and what they do.

Backend Pool

Field Value Notes
Enabled checked
Name or Prefix openHAB_backend
Description openHAB home automation server
Servers openHAB_server
Enable Health Cheking checked
Health Monitor HTTP GET
Option passthrough timeout tunnel 1h any web service that uses websockets needs a long timeout tunnel
Select Rules openhab_auth_cookie openhab_clear_auth openhab_cors_origin openhab_hsts_header

Mapping

Open the all_services map file and add a line with the domain you want and “openHAB_backend” as the backend. For example

 openhab.foo.com openHAB_backend

At this point the full map file will be

openhab.foo.com openHAB_backend
router.foo.com router_backend

Test it Out!

From your LAN you should be able to go to https://openhab.foo.com and MainUI will come up and the browser will not complain about the self signed certificate.

From outside your LAN you’ll get a 403 Forbidden error code.

If You Must

If you must expose OH to the internet, add some users under Users & Groups. In the openHAB_backend check to enable “Basic Authentication”. You can select the users who are allowed or select the groups that are allowed access.

This will put up a basic authentication dialog you must get past before you ever get to OH. You’ll need to then log into openHAB itself to gain access to the admin parts of MainUI.

To expose openHAB to the internet, go to the is_not_public_host condition and add openhab.foo.com to the list.

Again, if you haven’t already, you’ll also need a firewall rule to allow access to oprt 80 and 443 from the WAN.

Repeat

I’m sure you have a bunch of other services running on your LAN: Zigbee2MQTT, NodeRed, Grafana, etc. Repeat the three steps to add each of those to your list of services. Now they all will be reachable by a friendly domain name (no more remembering port numbers) with HTTPS and a trusted certificate.

If you have any services that are available on the internet, you can use the same URL both internally and externally.

Because HAProxy is a generic load balancer, you could even put services like PostgeSQL and Mosquitto in the proxy. But that’s an exercise left to the student.

Conclusion

It’s a lot of work up front, but the quality of life improvements are pretty remarkable. Now I don’t have to create a special dashboard for all my home services for the family to use. They now have easily remembered URLs they can use to bring up any of my home services (no more port numbers) and all services use HTTPS and none complain about unsigned certificates.

It is a pretty polished experience over all.

3 Likes