Linux Firewall (Part 5) : Reverse Proxy

Intro

In this post, we will setup HAProxy on our firewall. We can use it for some simple services running on our DMZ servers.

Install

We will install it from RHEL/Fedora repository

dnf install haproxy

Configure

Backup the default configuration if needed

cp /etc/haproxy/haproxy.cfg /etc/haproxy/haproxy.cfg.old

Edit or create a new configuration file

nano /etc/haproxy/haproxy.cfg

Global

Create a global section in config file. We will specify maximum connections, user and group HAProxy will run as, and some crypto settings.

global
    chroot      /var/lib/haproxy
    pidfile     /var/run/haproxy.pid
    maxconn     4000
    user        haproxy
    group       haproxy
    daemon

    # one process and 4 threads
    nbproc                      1
    nbthread                    4

    # prefer client certs and min TLSv1.2
    ssl-default-bind-options prefer-client-ciphers ssl-min-ver TLSv1.2

    # utilize system-wide crypto-policies
    ssl-default-bind-ciphers PROFILE=SYSTEM
    ssl-default-server-ciphers PROFILE=SYSTEM
    
resolvers localdns
   nameserver unbound 127.0.0.1:53

Defaults

Add some defaults that all the listen and backend blocks will use if not designated in their block

defaults
    mode                    http
    log                     global
    option                  httplog
    option                  dontlognull
    option                  http-server-close
    option                  forwardfor       except 127.0.0.0/8
    option                  redispatch
    retries                 3
    timeout http-request    10s
    timeout http-keep-alive 10s
    timeout queue           30s
    timeout connect         30s
    timeout client          30s
    timeout server          30s
    timeout check           10s
    maxconn                 3000

HTTP

Create a block for handing HTTP traffic (port 80). We want to redirect all HTTP requests to HTTPS.

# Frontend: frontend_80 (http traffic on port 80)
frontend frontend_80
    bind 0.0.0.0:80
    mode http
    option http-keep-alive
    option forwardfor
    timeout client 30s

    # Redirect HTTP to HTTPS
    http-request redirect scheme https code 302

HTTPS

Create a block for handling HTTPS traffic (port 443). This is going to be a bigger block.

We will enable TLS with the certificates we installed in the previous post

# Frontend: frontend_443 (https traffic on port 443)
frontend frontend_443
    bind 0.0.0.0:443 ssl crt /etc/haproxy/example.com.pem
    mode http
    option http-keep-alive
    option forwardfor
    timeout client 30s

Enable stats endpoint at /haproxy/stats

    stats enable
    stats uri /haproxy/stats
    stats refresh 10s

[!TIP]

For better security use a different URI for stats page.

Set some headers that will be used by our applications behind the reverse proxy

    http-request set-header X-Forwarded-Proto https
    http-request set-header X-Forwarded-Protocol https
    http-request set-header X-Client-Port %[src_port]
    http-request set-header X-Real-IP %[src]
    http-request set-header HTTP_X_FORWARDED_PROTO https
    http-request set-header HTTP_X_FORWARDED_HOST %[hdr(host)]
    http-request set-header HTTP_X_FORWARDED_PORT %[dst_port]
    http-request set-header HTTP_X_FORWARDED_SSL on
    http-request set-header Host %[hdr(host)]

We will create two different conditions to route traffic to backends. The first one will match the host and if found in our map file, will redirect to the specified backend. The second condition will execute if the first one is not found and will redirect based on the URL path. This is useful for hiding services behind random/obfuscated paths like example.com/<random_secret_string1>/mysecretapplication/login

    use_backend %[req.hdr(host),lower,map(/etc/haproxy/hosts.map)] if { req.hdr(host),lower,map(/etc/haproxy/hosts.map) -m found }
    use_backend %[base,lower,map_beg(/etc/haproxy/api.map)] if { base,lower,map_beg(/etc/haproxy/api.map) -m found }

That’s the end of our HTTPS block. Let’s look at the hosts and api map files we need.

The hosts.map file maps subdomains to backend names.

/etc/haproxy/hosts.map

app1.example.com               app1
app2.example.com               app2
ha.example.com                 homeassistant

The api.map file maps URL paths to backend names.

/etc/haproxy/api.map

api.example.com/somerandomstring/secretapp/     secretapp

Now the request app1.example.com/login will use app1 backend and api.example.com/somerandomstring/secretapp/login will use secretapp backend.

Backends

Let’s add the backends now. Each backend will have one or more upstream severs and we can pick which method we want to use for load balancing. We will also add health checks and the method for health checks

Add backend for app1

backend app1
    option httpchk
    mode http
    balance source
    timeout connect 30s
    timeout server 30s
    http-reuse safe
    server app1_aa aa.example.com:1000 resolvers localdns check inter 10s fall 5 rise 2

Add backend for ha. httpchk sends HTTP OPTIONS request. We can send a GET request for health check by adding http-check with GET method.

backend homeassistant
    option httpchk
    http-check send meth GET  uri /manifest.json
    mode http
    balance source
    timeout connect 30s
    timeout server 30s
    http-reuse safe
    server ha_aa aa.example.com:1001 resolvers localdns check inter 10s fall 5 rise 2

Add backend for secret API. Nothing special here, it’s just like a regular backend

backend secretapp
    option httpchk
    mode http
    balance source
    timeout connect 30s
    timeout server 30s
    http-reuse safe
    server sec_aa aa.example.com:1005 resolvers localdns check inter 10s fall 5 rise 2

Run

By default, SELinux only allow HAProxy to connect to a small subset of upstream ports. If we try to connect to other ports, HAProxy health checks will get errors

Layer4 connection problem, info: "General socket error (Permission denied)"

The SELinux audit log will show avc: denied { name_connect } with scontext=system_u:system_r:haproxy_t:s0 tclass=tcp_socket message.

We can fix this by allowing HAProxy to connect to any port. Set SELinux boolean

setsebool -P haproxy_connect_any 1

We can now enable and run it

systemctl enable --now haproxy

Everything should be working now. Verify that all hosts are up and running using the stats page at /haproxy/stats.

Thank you for reading. Check out the other parts in the series below.