Using iocaine as a HAProxy SPOA

  Important

The configuration in this guide requires HAProxy 3.1 or later. If your distribution of choice has an older version, you may need to look for backports or third party packages. For example, for Debian and Ubuntu, backports are provided here, by HAProxy’s Debian maintainer.

Getting started

In this guide, we’ll be working off of the assumption that we’ve set up iocaine with the default, built-in request handler, as shown in the Getting Started guide. On the HAProxy side, our example will have a web service at localhost:8000, which we want to proxy via HAProxy as example.com.

The relevant parts of our haproxy.cfg would look something along these lines:

frontend main
    bind [::]:80

    acl example_acl req.hdr(host) -i example.com

    use_backend localhost_8000 if example_acl

backend localhost_8000
    mode http
    server localhost:8000
(View the full configuration)
global
    log stdout format iso local0 debug

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

frontend main
    bind [::]:80

    acl example_acl req.hdr(host) -i example.com

    use_backend localhost_8000 if example_acl

backend localhost_8000
    mode http
    server localhost:8000

HAProxy SPOE

HAProxy has a Stream Processing Offload Engine, which requires a Stream Processing Offload Agent - something iocaine can provide through haproxy-spoa-server. HAProxy works differently than the other reverse proxies: the decision and response generation steps are more clearly separated, and isolated: it’s possible to use iocaine for decision making only, and use something else to generate a response: some static content, another garbage generator - the possibilities are endless.

Because the decision and the output generation are isolated from one another, we need a way to carry the decision across to the output generation part. The simplest way to do that is to set a special, trusted HTTP header when talking to the output generating backend. Ideally, we’d remove the header before passing it through the filter, but that does not seem to be possible, so we’ll do the next best thing: if our trusted header - which we’ll name Iocaine-Decision - is present in the incoming request, we’ll treat that as a malicious request, and will skip decision making, marking it as such right there and then. This ensures that if the output generating backend sees an Iocaine-Decision header, it can be sure it was set inside HAProxy, and did not come from an untrusted source.

The built-in request handler supports short-circuiting the decision making if there’s a trusted decision header, which Iocaine-Decision will be. Setting trusted-decision-header will tell it which header to use for this purpose.

Integrating iocaine

Integrating iocaine into our HAProxy configuration will involve updating the configuration of both. We’ll start with iocaine’s. If you just want to get going fast, read the next two sections, and skip to the full haproxy.cfg

Updating the iocaine configuration

As discussed above, we’ll need an agent for HAProxy. Lets configure iocaine to spawn us one, by placing the following configuration into /opt/iocaine/etc/iocaine/config.d/04-haproxy-spoa.kdl:

haproxy-spoa-server spoa {
    bind "/run/iocaine/iocaine.sock" unix-socket-access="group"
    use handler-from=default metrics=metrics
}

declare-handler default {
    trusted-decision-header "iocaine-decision"
}

This assumes that iocaine and HAProxy share at least one group. For testing purposes, you can use unix-socket-access="everbody" - just don’t keep that in production! It uses the same metrics and handler (the built-in one) as we ended up with in the Getting Started guide. This also sets the trusted-decision-header, as discussed previously.

Configuring the SPOA agent

Next, we need a separate configuration file for the HAProxy side of our Stream Processing Offload Agent. Lets place it into, say, iocaine-spoa.cfg, in the same directory where our haproxy.cfg is. For the sake of simplicity, lets assume we’ll place it into /etc/haproxy/iocaine-spoa.cfg. It should look something like this:

[iocaine]
spoe-agent iocaine
    log global
    option var-prefix iocaine
    timeout processing 5s
    messages check-request
    use-backend iocaine

spoe-message check-request
    args req_method=method req_hdrs=req.hdrs req_path=path req_query=query
    event on-frontend-http-request

This snippet specifies an iocaine SPOE agent. The agent has a variable prefix of iocaine for variables set inside the handler. On incoming requests, a check-request messages gets sent to the agent. The message contains the following variables, which can be used to make decisions:

For more information on the SPOE configuration, see section 2.1 of doc/SPOA.txt.

Updating the HAProxy configuration

In the above configuration, we configured the agent to use a backend named “iocaine”. Lets set that up! Add the following to your haproxy.cfg:

backend iocaine
    mode spop
    option spop-check
    server iocaine "/run/iocaine/iocaine.sock"

The path used here must match the path set in the iocaine configuration above.

Now, we must decide how to approach wrapping our frontend with iocaine. In this guide, we’ll filter all requests through iocaine, and depending on the decision made, we’ll choose a backend: either iocaine-output if the decision was to serve garbage, or localhost_8000 if the decision was to serve the Real Thing. If iocaine is unavailable, we shall return a HTTP 503.

We will be replacing our entire frontend main block, with a configuration that looks like the following:

frontend main
    bind [::]:80

    # If we see an `iocaine-decision` header, serve it garbage immediately.
    # We do not accept this header from the outside.
    acl forbidden_hdr hdr_cnt(iocaine-decision) gt 0
    http-request set-header iocaine-decision "garbage" if forbidden_hdr
    use_backend iocaine-output if forbidden_hdr

    # filter all requests through iocaine
    filter spoe engine iocaine config /etc/haproxy/iocaine-spoa.cfg

    # fallback if iocaine is unavailable
    http-request set-var(txn.iocaine.response,ifnotset) str("borked")

    # iocaine decision check
    acl iocaine_passed          var(txn.iocaine.response) -m str eq "default"
    acl iocaine_unavailable     var(txn.iocaine.response) -m str eq "borked"

    acl example_acl req.hdr(host) -i example.com

    # Set the Iocaine-Decision trusted header, in case we'll use
    # the `iocaine-output` backend.
    http-request add-header Iocaine-Decision "%[var(txn.iocaine.response)]" unless forbidden_hdr

    # show a challenge/garbage if the request has not passed checks and
    # iocaine is available, and we're serving the appropriate site.
    use_backend iocaine-output if !iocaine_passed !iocaine_unavailable example_acl

    # only show the website if iocaine decided so
    use_backend localhost_8000 if example_acl iocaine_passed

    # for debugging purposes, the response of iocaine can be added as a
    # response header. The previous Iocaine-Decision header only gets added for
    # the request to be used by iocaine-output, not in the response.
    # http-after-response add-header Iocaine-Decision "%[var(txn.iocaine.response)]"

We’ll also need to add another backend: iocaine-output. This will use the HTTP server we have set up in the Getting Started guide, the one that’s listening on 127.0.0.1:42069. Because its handler is configured to trust the Iocaine-Decision header, once we send the request to this backend, iocaine will short-circuit the decision making process, and use the value in the header, avoiding performing the same process again, skipping quickly to output generation.

Add the following to your haproxy.cfg:

backend iocaine-output
    mode http
    server iocaine-http 127.0.0.1:42069

And with this, our task is complete!

The full haproxy.cfg

To provide an overview, and a full configuration you can try, the haproxy.cfg assembled together in this guide are provided below, in full:

global
    log stdout format iso local0 debug

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

frontend main
    bind [::]:80

    # If we see an `iocaine-decision` header, serve it garbage immediately.
    # We do not accept this header from the outside.
    acl forbidden_hdr hdr_cnt(iocaine-decision) gt 0
    http-request set-header iocaine-decision "garbage" if forbidden_hdr
    use_backend iocaine-output if forbidden_hdr

    # filter all requests through iocaine
    filter spoe engine iocaine config /etc/haproxy/iocaine-spoa.cfg

    # fallback if iocaine is unavailable
    http-request set-var(txn.iocaine.response,ifnotset) str("borked")

    # iocaine decision check
    acl iocaine_passed          var(txn.iocaine.response) -m str eq "default"
    acl iocaine_unavailable     var(txn.iocaine.response) -m str eq "borked"

    acl example_acl req.hdr(host) -i example.com

    # Set the Iocaine-Decision trusted header, in case we'll use
    # the `iocaine-output` backend.
    http-request add-header Iocaine-Decision "%[var(txn.iocaine.response)]" unless forbidden_hdr

    # show a challenge/garbage if the request has not passed checks and
    # iocaine is available, and we're serving the appropriate site.
    use_backend iocaine-output if !iocaine_passed !iocaine_unavailable example_acl

    # only show the website if iocaine decided so
    use_backend localhost_8000 if example_acl iocaine_passed

    # for debugging purposes, the response of iocaine can be added as a
    # response header. The previous Iocaine-Decision header only gets added for
    # the request to be used by iocaine-output, not in the response.
    # http-after-response add-header Iocaine-Decision "%[var(txn.iocaine.response)]"

backend localhost_8000
    mode http
    server localhost:8000

backend iocaine
    mode spop
    option spop-check

    server iocaine "/run/iocaine/iocaine.sock"

backend iocaine-output
    mode http
    server iocaine-http 127.0.0.1:42069