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:
- Add the Real Server
- Add the Backend Pool
- 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.