haproxy for redirect
(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.