Hey,

If you’ve ever wondered whether you can tie Access Control Lists (ACLs) with maps in HAProxy, the answer is: yes.

Let’s tailor a scenario here:

  • based on a map, decide whether a request to a given domain should be answered by the current frontend or not - in the negative case, forward the request to a different frontend.

One application that can be thought from this is the case where some domains are meant to be served by both a plain HTTP frontend and a HTTPS frontend too, but some other domains must be redirected to HTTPS when a request is made to HTTP.

For instance:

  • http://test.example.com and https://test.example.com OK;
  • https://prod.example.com is OK but no http://prod.example.com such that when a request is made to http://prod.example.com, this should be redirected to the HTTPS frontend.

That said, this is how our configuration would look like:

Illustration of the frontend configuration of the haproxy configuration

Let’s write it then.

# Load the `./script.lua` script that defines some services
# that are meant to respond with `SERVICE_N OK` when a request
# is made to them.
#
# Having these services defined allows us to not need a real
# server behind the scenes.
global
	lua-load	./script.lua


defaults
	mode		http
	timeout		client 10000
	timeout		server 10000
	timeout		connect 1000


# The frontend `fe1` is the one that clients are meant to
# make requests to in order to access the backends as mapped
# by the `./domain-map` file.
#
# The contents of the `domain-map` file are:
#
#       foo.com backend_1
#       bar.com backend_2
#
# This way, requests that are not for `foo.com` or `bar.com`
# are sent to the other frontend (fe2) based on an ACL, otherwise,
# the respective backend is used.
frontend		fe1
	bind		127.0.0.1:8000
	acl		has_domain hdr(Host),map_str(./domain-map) -m found
	redirect	prefix http://127.0.0.1:8001 if ! has_domain
	use_backend	%[req.hdr(host),lower,map_str(./domain-map)]


# A sample frontend that always responds with a specific
# service (demonstration purposes).
frontend		fe2
	bind		127.0.0.1:8001
	http-request	use-service lua.service3


backend			backend_1
	http-request	use-service lua.service1


backend			backend_2
	http-request	use-service lua.service2

Being service1, service2 and service3:

core.register_service("service1", "http", function (applet)
	local response = "SERVICE 1 OK"

	applet:set_status(200)
	applet:start_response()
	applet:send(response)
end)

core.register_service("service2", "http", function (applet)
	local response = "SERVICE 2 OK"

	applet:set_status(200)
	applet:start_response()
	applet:send(response)
end)

core.register_service("service3", "http", function (applet)
	local response = "SERVICE 3 OK"

	applet:set_status(200)
	applet:start_response()
	applet:send(response)
end)

We can see it in practice:

# Make a request with `foo.com` as the destination host.
# Given that `foo.com` is a mapped backend, the request
# will get directed to a backend controlled by the frontend
# itself (backend_1)
curl \
        --location \
        --header "Host: foo.com" \
        --verbose \
        localhost:8000/
* Connected to localhost (127.0.0.1) port 8000 (#0)
> GET / HTTP/1.1
> Host: foo.com
> User-Agent: curl/7.59.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Transfer-encoding: chunked
< 
SERVICE 1 OK

# Just like in the case above, `bar.com` is also a
# domain controlled by this frontend.
curl \
        --location \
        --header "Host: bar.com" \
        --verbose \
        localhost:8000/
* Connected to localhost (127.0.0.1) port 8000 (#0)
> GET / HTTP/1.1
> Host: bar.com
> User-Agent: curl/7.59.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Transfer-encoding: chunked
< 
* Connection #0 to host localhost left intact
SERVICE 2 OK


# Differently from the two cases above, `test.com`
# is not included in the frontend1 map.
#
# This means that the ACL we placed there will evaluate
# `has_domain` to false and then will redirect us to
# the second frontend  (which always responds with
# SERVICE 3). 
#
# Check out how we get redirected to the other frontend.
curl \
        --location \
        --header "Host: test.com" \
        --verbose \
        localhost:8000/
* Connected to localhost (127.0.0.1) port 8000 (#0)
> GET / HTTP/1.1
> Host: test.com
> User-Agent: curl/7.59.0
> Accept: */*
> 
< HTTP/1.1 302 Found
< Cache-Control: no-cache
< Content-length: 0
< Location: http://127.0.0.1:8001/      # << REDIRECTION
<                                       # sends us to another frontend

* Connection #0 to host localhost left intact
* Issue another request to this URL: 'http://127.0.0.1:8001/'
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8001 (#1)
> GET / HTTP/1.1
> Host: 127.0.0.1:8001
> User-Agent: curl/7.59.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Transfer-encoding: chunked
< 
* Connection #1 to host 127.0.0.1 left intact
SERVICE 3 OK

That’s it!

If you have any questions or spotted something off, please let me know! I’m cirowrc on Twitter.

Have a good one!

finis