Offer your own services publicly with OPNsense

Goals

It's also nice to be able to access your own services while travelling. The operation of a publicly accessible service installed in the local network can be realised ingeniously simply with a Let's Encrypt certificate and HAProxy on the OPNsense firewall.

Nevertheless: This expansion stage is now suitable for public operation, but it has to be updated and maintained regularly now. Nothing is worse than an outdated and thus vulnerable installation.

Last updated:

  • 02.09.2024: Converted into a stand-alone article and rebuilt as a module for others
  • 03.03.2024: Minor adjustments
  • 05.01.2024: Various minor adjustments
  • 20.08.2023: Automation for HAProxy extended
  • 29.07.2023: Initial document

Requirements

  • A service is already installed and internally accessible, (e.g. FreshRSS, Gitea oder Vaultwarden
  • A separate subdomain with a fixed IP or DyDNS address is available (e.g. dienstname.domain.de, or meinhostname.dyndns.org)
  • A opnSense Firewall
    • The TCP port was set to 8443 under System - Settings - Administration (previously 443)
    • With the installed plugins os-haproxy and os-acme-client
    • Access to TCP port 443 is permitted on the WAN interface under Firewall - Rules - WAN. (Protocol: TCP, Source: Any, Destination: This Firewall, Port: 443)
    • Internally, the EXTERNHOSTNAME should be set to the IP of the OPNsense (so that internal calls are also routed through HAProxy). Usually with the Overrides in Unbound under Services.

Diagram

This makes the setup look like this:

                                                          ┌─────────────────────────────┐
                                                          │ TrueNAS / FreeBSD           │
                  ┌──────────────────────┐                │ ┌─────────────────────────┐ │
                  │ OPNsense             │                │ │ YESils/YESilname          │ │
WAN: 0.0.0.0:80  ─┼─► acme.sh:80  ───────┼─ LAN: IP:PORT ─┼─┼─► service               │ │
WAN: 0.0.0.0:443 ─┼─► HAProxy:443        │                │ │                         │ │
                  └──────────────────────┘                │ └─────────────────────────┘ │
                                                          └─────────────────────────────┘

Terminology

  • ACCOUNTNAME = Name of the Let's Encrypt account (e.g. bsdbox)
  • EMAIL = Email Adresse of the Let's Encrypt Accounts (e.g. marcel@bsdbox.de)
  • EXTERNHOSTNAME = The externally accessible host name (e.g. rss.bsdbox.de oder bsdbox.dyndns.org)
  • IP = IP Address of the local server
  • HOSTNAME = Hostname of the local server (e.g. rss.bsdbox.local)

Certificates

With the ACME Client Plugin (os-acme-client) OPNsense is able to create and renew Let's Encrypt certificates automatically. The huge advantage is that we have a central certificate management, don't need a separate management on each internal target system and don't need to configure NAT or other firewall settings.

Services: ACME Client

Settings

To get a certificate for your own domain, only a few steps are necessary under Services - ACME Client. For this we change to the area Services - ACME Client - Settings of the OPNsense and set the following checkmarks:

Enable Plugin: YES
Auto Renewal: YES

This activates the plugin and sets the automatic renewal of certificates.

Accounts:

We then switch to the Services - ACME Client - Accounts' area and create a new account with the + sign.

Enabled: YES
Name: ACCOUNTNAME
E-Mail Address: EMAIL
ACME CA: Let's Encrypt [default]

The account must be registered once. On the far right you will find the Register account button. If successful, the account will then be labelled OK (registered) under Status.

Tip: One registration is enough, no matter how many certificates we need in the end.

Challenge Types

Next, we specify how the challenge should take place. This is done in the section Services - ACME Client - Challenge Types. Again, we create a new challenge with the following settings by pressing the + sign:

Enabled: YES
Name: HOSTNAME
Challenge Type: HTTP-01
HTTP Service: OPNsense Web Service (automatic port forward)
Interface: WAN

Automations

A new entry is created under Services - ACME Client - Automations.

Name: Restart HAProxy
Run Command: Restart HAProxy

Certificates

The last step is to create the certificate. For this we change to Services - ACME Client - Certificates and create again a new entry by the + sign. In the field ACME Account we select the account we just created, in the field Challange Type we select the created challange. We select the remaining settings as follows:

Enabled: YES
Common Name: EXTERNHOSTNAME
ACME Account: ACCOUNTNAME (from Accounts)
Challenge Type: HOSTNAME (from Challenge Types)
Key Length: ec-384
Automations: Restart HAProxy  

Now all settings are done and the certificate can be created on the right side with the button Issue or renew certificate. If successful (this takes a few minutes) the certificate will be marked with OK at Last ACME Status.

Done! The certificate is now renewed fully automatically every 90 days according to the settings.

HAProxy

HAProxy receives the calls for port 443 from the outside, encrypts the connection and forwards it to the internal Gitea server on port 3000. The nice thing is that here again the administration takes place centrally, the certificates can be used without any effort from Let's Encrypt and several services can be provided at the same time on port 443. The difference is made by the called external hostname. This makes it possible that e.g. https://service.domain.de is forwarded to the service and https://domain.de to the actual website, although both actually require port 443 on the same external IP address.

The task of HAProxy here is:

  • Allow external access for the URL https://EXTERNHOSTNAME
  • Harden HTTPS and make it less vulnerable to weak encryptions

The process is: Public Service (EXTERNHOSTNAME:443) ─► Condition ─► Rule ─► Pool ─► HOSTNAME:PORT

Now to get a forwarding of the EXTERNALHOSTNAME to the internal Gitea, only a few steps are necessary under Services - HAProxy: In the first step we define a server under Services - HAProxy - Settings in the tab Real Servers. Again, this is done by pressing the + button.

Services: HAProxy

Settings

Enable HAProxy: YES

Real Servers:

Let's start with the service, this gets the following entries:

Name: server_HOSTNAME
Description: IP address of the local server
Type: static
FQDN or IP: IP
Port: PORT

Virtual Services: Backend Pools

In the second step we create the Backend Pools, this we do under Services - HAProxy - Settings in the tab Virtual Services the entry Backend Pools.

Name: pool_HOSTNAME
Description: Pool of local Vaultwarden servers (there is only one)
Servers: server_HOSTNAME

Rules & Checks: Conditions

Now we define the set of rules that determine which incoming connections are forwarded to which service. To do this, we select the Conditions entry under Services - HAProxy - Settings in the Rules & Checks tab and create two conditions:

Name: HOSTNAME host check
Description: Check if the EXTERNHOSTNAME has been called
Condition type: Host matches
Path Regex: EXTERNHOSTNAME

Rules & Checks: Rules

These conditions are checked by a rule. We create rules as follows under Services - HAProxy - Settings in the Rules & Checks tab in the Rules entry by pressing the + button:

Name: HOSTNAME host rule
Description: Forward external access to the direct server (in the pool)
Select conditions: HOSTNAME host check
Execute function: Use specified Backend Pool
Use backend pool: pool_HOSTNAME

Virtual Services: Public Services

Finally we create a Public Services under Services - HAProxy - Settings in the tab Virtual Services the entry ´Public Services:

Name: service_https
Description: Server on port 443 that accepts all external requests
Listen Addresses: 0.0.0.0:443
Default Backend Pool: none
Certificates: EXTERNHOSTNAME (ACME Client)
Default certificate: EXTERNHOSTNAME (ACME Client)
Cipher List: ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
Cipher Suites: TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
Enable HSTS: YES
HSTS includeSubDomains: YES
Select Rules: HOSTNAME host rule

Tip: If there are several internal servers, Select Rules can receive multiple entries.

With Apply the configuration is written and the HAProxy service is started.

Now external access to the Gitea is set up and allowed.

Please remember to allow port 443 in the firewall.

Voilá