Public operation of Gitea with OPNsense

Goals

The operation of a Gitea server with a Let's Encrypt certificate and HAProxy is nicely done 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:

  • 03.03.2024: Minor adjustments
  • 05.01.2024: Various minor adjustments
  • 20.08.2023: Automation for HAProxy extended
  • 29.07.2023: Initial document

Requirements

  • Gitea from the first part is installed and ready to go
  • A dedicated subdomain with fixed IP or DyDNS address is available (e.g. git.domain.de)
  • opnSense firewall
    • With the plugins os-haproxy and os-acme-client installed
    • Access to TCP port 443 is allowed/enabled on the WAN interface
    • Internally the EXTERNALHOSTNAME should be set to the IP of the OPNsense (so that internal calls are also routed through HAProxy)

Screenshot%202022-10-30%20124502

Diagram

This makes the setup look like this:

                                                               ┌─────────────────────────────┐
                                                               │ TrueNAS                     │
                  ┌──────────────────────┐                     │ ┌─────────────────────────┐ │
                  │ OPNsense             │                     │ │ jails/git               │ │
WAN: 0.0.0.0:80  ─┼─► acme.sh:80  ───────┼─ LAN: 0.0.0.0:3000 ─┼─┼─► gitea                 │ │
WAN: 0.0.0.0:443 ─┼─► HAProxy:443        │                     │ │                         │ │
                  │                      │                     │ └─────────────────────────┘ │
                  └──────────────────────┘                     └─────────────────────────────┘

Lets Encrypt 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: JA
Auto Renewal: JA

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

Screenshot%202022-10-30%20123726

Accounts:

Then we switch to the "Services / ACME Client / Accounts" section and create a new account with the "+" sign.

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

The account must be registered once. On the far right there is a button "Register Account". If the registration is successful, the account will be marked with "OK (registered)".

Screenshot%202022-10-30%20131719

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: JA
Name: GITEAHOSTNAME
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: JA
Common Name: EXTERNALHOSTNAME
ACME Account: ACCOUNTNAME (aus Accounts)
Challenge Type: GITEAHOSTNAME (aus 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".

Screenshot%202022-10-30%20132151

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://git.domain.de is forwarded to Gitea 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://EXTERNALHOSTNAME
  • Harden HTTPS and make it less vulnerable to weak encryptions

The process is: Public Service (EXTERNALHOSTNAME:443) ─► Condition ─► Rule ─► Pool ─► GITEAHOSTNAME:3000

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

Screenshot%202022-10-30%20134041

Real Servers:

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

Name: server_GITEAHOSTNAME
Description: IP Adresse des lokalen Gitea Servers
Type: static
FQDN or IP: GITEAIP
Port: 3000

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_GITEAHOSTNAME
Description: Pool of local Vaultwarden servers (there is only one)
Servers: server_GITEAHOSTNAME

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: Gitea host check
Description: Check if the EXTERNALHOSTNAME has been called
Condition type: Host matches
Path Regex: EXTERNALHOSTNAME

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: Gitea host rule
Description: Externer Zugriff auf **EXTERNALHOSTNAME** an den richten Server (im Pool) weiterleiten
Select conditions: Gitea host check
Execute function: Use specified Backend Pool
Use backend pool: pool_GITEAHOSTNAME

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 auf Port 443 der alle externen Anfragen entgegenimmt
Listen Addresses: 0.0.0.0:443
Default Backend Pool: none
Certificates: EXTERNALHOSTNAME (ACME Client)
Default certificate: EXTERNALHOSTNAME (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: Gitea host rule

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

Please remember to allow port 443 in the firewall.

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

Please test the following:

  • The call https://EXTERNALHOSTNAME opens the Vaultwarden login screen - OK!
  • The call https://EXTERNALIPADCRESS gets a "Forbidden" error message - OK!

Voilá