Using iocaine as a HAProxy SPOA
Last updated: 2025-11-14

> 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:

- `req_method` - the method used to make the request.
- `req_hdrs` - the headers of the request.
- `req_path` - the path which has been requested.
- `req_query` - the query parameters passed with the request.

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