(2024 update, deploy haproxy in a container instead.)

The article is about using haproxy for http redirect of whole domains. There are many ways to do this, but the foregoing case provides some good operational knowledge about all the pieces and how they can be used. A later article will cover the Let's Encrypt portion, but the full build and configure will be here.



These are the components:

  • haproxy 1.8 (custom build)
    • lua 5.3.5 (custom build)
    • openssl (from os)
    • pcre (from os)
    • zlib (from os)
  • CentOS 7

Building

Since CentOS provides substantially outdated versions of haproxy and lua, we're going to download and build them.

YUM Steps

Assuming you're starting with a default install, this is how to start the process:

yum install @'development tools'
yum install openssl-devel
yum install pcre-devel
yum install zlib-devel
yum install readline-devel

Build LUA

curl -R -O http://www.lua.org/ftp/lua-5.3.5.tar.gz
tar zxf lua-5.3.5.tar.gz
cd lua-5.3.5
make linux test

Build haproxy

wget https://www.haproxy.org/download/1.8/src/haproxy-1.8.19.tar.gz
tar -tzf haproxy-1.8.19.tar.gz
cd haproxy-1.8.19
make TARGET=linux2628 USE_PCRE=1 USE_OPENSSL=1 USE_ZLIB=1 LUA_LIB=~/lua-5.3.5/src LUA_INC=~/lua-5.3.5/src USE_LUA=1 USE_SYSTEMD=1

The above assumes you're building in the home directory and that's where lua can be found. If that's not the case, adjust the LUA_LIB and LUA_INC variables.

If you're on a linux you're probably using systemd, so let's get that:

cd contrib/systemd
make PREFIX=""

Install

From the root of the haproxy build:

sudo cp haproxy /sbin/haproxy
sudo mkdir /etc/haproxy
sudo cp contrib/systemd/haproxy.service /etc/systemd/system/

Configuration

Below is the entire haproxy configuration. It should end up in the file /etc/haproxy/haproxy.cfg . We'll take it apart following. Note that where possible, settings are links to the haproxy documentation for details.

global
  # From default package cfg
  log         127.0.0.1 local2
  chroot      /var/lib/haproxy
  pidfile     /var/run/haproxy.pid
  maxconn     4000
  user        haproxy
  group       haproxy
  # turn on stats unix socket
  stats socket /var/lib/haproxy/stats

  tune.ssl.default-dh-param 2048
  server-state-file /tmp/haproxy_server_state
# 
#  lua-load /etc/haproxy/luascript_acme-webroot.lua

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 queue           1m
  timeout connect         10s
  timeout client          1m
  timeout server          1m
  timeout http-keep-alive 10s
  timeout check           10s
  maxconn                 3000

listen stats
   bind 127.0.0.1:2200 name localstats
   stats enable
   stats admin if LOCALHOST
   stats show-legends
   timeout client 5000
   timeout connect 5000
   timeout server 5000

frontend fe-http
  bind proxy-host.my.domain:80 name proxy-host.my.domain:80
  acl acme    var(txn.txnpath) -m beg -i /.well-known/acme-challenge
  acl trivial-domain var(txn.txnhost) -m end -i trivial-domain.com
  http-request set-var(txn.txnhost) hdr(host)
  http-request set-var(txn.txnpath) path
#  http-request use-service lua.acme-http01 if acme
  http-request redirect prefix https://www.real-domain.com if trivial-domain

frontend fe-https
  bind proxy-host.my.domain:443 name proxy-host.my.domain:443 ssl crt-list /etc/haproxy/ssl/fe-https.crt_list
  acl trivial-domain var(txn.txnhost) -m end -i trivial-domain.com
  http-request set-var(txn.txnhost) hdr(host)
  http-request redirect prefix https://www.real-domain.com if trivial-domain

Global

The log 127.0.0.1 local2 will require a change to /etc/rsyslog.conf:

# Provides UDP syslog reception
$ModLoad imudp
$UDPServerRun 514

These lines are in the RPM distributed file, but are commented out. I was not able to find a setting to limit rsyslogd to binding only to localhost, so this opens UDP port 514 to off-host traffic.

Create the file /etc/rsyslog.d/haproxy.conf:

local2.*	/var/log/haproxy

I think you can combine the imudp lines into the this file and just not comment them out of the /etc/rsyslog.conf, but this seems bad form since it really is a global rsyslog change.

The lua-load pertains to handling acme-challenge requests for SSL, and will be covered in a later article, so it is commented out here.

Defaults

These are settings apply to all following sections unless they are overridden. For our purposes they don't have much effect except the mode is defaulted to http and the option being httplog.

Stats

This is not strictly necessary since in this use case haproxy is not handling actual traffic. But it is a great overview of what haproxy is doing and if you use haproxy for anything else you will want it. This section results in a navigable url of http://127.0.0.1:220/haproxy?stats

frontend fe-http

Here we deal with two situations, redirection and acme certificates, the latter of which we will deal with below. The bind is the IP and port haproxy should listen on for connections. The name parameter is what will be used on the stats page.

acl is one of the largest topics in haproxy, it defines a piece of logic. Let's examine these specific cases

acl acme    var(txn.txnpath) -m beg -i /.well-known/acme-challenge

Read this as: declare a name acme whose value is the comparison of the variable txn.txnpath as a path where the beginning is a case insensitive match on /.well-known/acme-challenge. The variable txn.txnpath doesn't exist in haproxy, it is created when the request is processed by the line that follows:

http-request set-var(txn.txnpath) path

This sets the variable to the path from the request. To be honest, this threw me off the first time I saw it due to the ordering.

This acl is then used as a condition in the ongoing http request handler as such:

http-request use-service lua.acme-http01 if acme

Which in this case, if the acme variable is hold a true (that the request path starts with /.well-known/acme-challenge) then the lua service lua.acme-http01 is called for handling the request. These lines are commented out, but see the later article about Let's Encrypt, then these lines will be turned on and used.

Redirection

The lines

acl trivial-domain var(txn.txnhost) -m end -i trivial-domain.com
...
http-request set-var(txn.txnhost) hdr(host)
...
http-request redirect prefix https://www.real-domain.com if trivial-domain

are a very similar pattern. txn.txnhost is being set during the http-request, specifically the hostname provided in the Host: header. The acl will trivial-domain be true if txn.txnhost ends with trivial-domain.com. Then, if trivial-domain is true, a prefix redirection will be sent to the client. A prefix will rebuild the URL with the new prefix and the requested path. The action is if a request arrives for http://anything-at.trivial-domain.com/buckets/of/fish a redirect will be returned to https://www.real-domain.com/buckets/of/fish

frontend fe-https

This is mostly the same except for bind which includes ssl crt-list /etc/haproxy/ssl/fe-https.crt_list and this is to provide haproxy with SSL certificates since this is an https frontend. The crt_list is a text file that lists, each on a line, files that contain certificate and keys concatenated together. It can also specify SNI filters which are used to select the right certificate for a requesting client. Without filters haproxy will chose a matching certificate. A file might look like this:

cert1.pem
cert2.pem *.some-domain !special.some-domain
cert3.pem special.some-domain

The important thing to know is that a file like cert1.pem is created by combining at cert and a key, simply by doing cat cert.crt key.pem > cert1.pem in this example.