Traefik 3.0: Deep Dive Into Wasm Support With Coraza WAF Plugin
We’re continuing our deep dive series on Traefik 3.0 which was released a week ago. Make sure to check out last week’s article on the migration path from Traefik v2. Today, we will deep dive into WebAssembly support.
Custom plugins for Traefik are one of the most requested features going back to the early days of the project, starting with this issue, from back in 2017:
The author suggested using the brand new plugin feature introduced in go 1.8. The idea was to compile go code into dynamic libraries which could be loaded and executed at runtime. It looked very promising and we immediately started working on this. Sadly, some severe limitations were found and this pursuit was put on hold. A second attempt was made a few months later, but was eventually abandoned due to the incomplete plugin implementation provided by the go team (only on Linux with CGO_ENABLED).
After many discussions, we ended up with a different solution. If the go language ecosystem wouldn’t provide any solid solutions for building plugins, let’s implement our own (crazy, I know) fully compliant Go interpreter. In 2019, Yaegi—Yet Another Elegant Go Interpreter—was released. The project quickly got a lot of traction being the best Go interpreter, and middleware plugins became a reality within Traefik soon after. Yaegi enabled many to develop middlewares or providers for their context. To this day, we can count more than a hundred middleware plugins made available through the catalog, plus many more that are kept private.
However, Yaegi only provides the option to build plugins using go, and this can be a bit restrictive for some people. This is the reason why some community members started to work on the brand new plugin engine for Traefik in 2023: Web Assembly, abbreviated WASM.
WASM Overview
WebAssembly is a low-level assembly-like language with a compact binary format that runs with near-native performance. It provides languages such as C/C++, Go or Rust (and many more) with a compilation target so that they can run on the web, and in other runtimes thanks to WASI, the WebAssembly System Interface.
Announced in 2015, WASM evolved into an open standard and got support in all major browsers in 2017.
In a nutshell, WASM allows you to compile almost any language in a portable binary format that can run on any platform, with native performance.
It looks like we found the perfect plugin engine for Traefik 🙂
Traefik + WASM
The idea to bring WASM support to Traefik was appealing. However, many challenges had to be tackled. The first one was to write an interface between Traefik, the host, and WASM plugins, the guests. This is called an Application Binary Interface (ABI) and in the case of Traefik middleware plugins, we had to provide an interface to the net/http HTTP server library. Writing and maintaining a fully compliant ABI for the net/http library is not an easy task but luckily, talented developers had already released different options, two of which stood out: proxy-wasm and http-wasm. After many discussions with community members, the clear winner was http-wasm, thanks to its much simpler and straightforward integration within a go codebase.
Then everything accelerated. A community contributor—Jesse Haka—opened a pull request, and a month later, after careful reviews from maintainers and external contributors like José Carlos Chávez, Traefik had support for WASM middleware plugins!
Let’s see how to write a WASM plugin for Traefik that customizes the routing pipeline that handles requests and responses. The Traefik routing pipeline basically splits into 3 main components: entrypoints, routers and services. Entrypoints define the network entry points into Traefik. Routers are in charge of connecting incoming requests to the services that can handle them. In the process, routers may use a chain of middleware to do some pre- and/or post-processing of the request. Our goal here is to write a custom middleware that will be added to the chain in a Traefik router. To simplify, we will use go as the plugin langage, but we could also use C or Rust, as soon as the code implements the http-wasm interface.
Ultimately, the only thing you have to do is implement 1 function: handleRequest which handles the pre-processing or handleResponse, the post-processing. You can get a simple example of a WASM plugin in the repository. Here is boilerplate:
package main
import (
"encoding/json"
"fmt"
"os"
"github.com/http-wasm/http-wasm-guest-tinygo/handler"
"github.com/http-wasm/http-wasm-guest-tinygo/handler/api"
)
// Config the plugin configuration.
type Config struct {
Headers map[string]string `json:"headers,omitempty"`
}
func main() {
var config Config
err := json.Unmarshal(handler.Host.GetConfig(), &config)
if err != nil {
handler.Host.Log(api.LogLevelError, fmt.Sprintf("Could not load config %v", err))
os.Exit(1)
}
mw, err := New(config)
if err != nil {
handler.Host.Log(api.LogLevelError, fmt.Sprintf("Could not load config %v", err))
os.Exit(1)
}
handler.HandleRequestFn = mw.handleRequest
}
// Demo a Demo plugin.
type Demo struct{}
// New created a new Demo plugin.
func New(config Config) (*Demo, error) {
return &Demo{}, nil
}
func (d Demo) handleRequest(req api.Request, resp api.Response) (next bool, reqCtx uint32) {
return true, 0
}
Then load your compiled plugin into the static configuration:
# Static configuration
experimental:
localPlugins:
example:
moduleName: github.com/traefik/plugindemowasm
```
And finally, customize a router with your new plugin:
# Dynamic configuration
http:
routers:
my-router:
rule: host(`demo.localhost`)
service: service-foo
entryPoints:
- web
middlewares:
- my-plugin
services:
service-foo:
loadBalancer:
servers:
- url: http://127.0.0.1:5000
middlewares:
my-plugin:
plugin:
example:
headers:
Foo: Bar
```
As you can see, this is pretty straightforward to customize the routing pipeline in Traefik thanks to WASM plugins. Let’s tackle a production use case. How about embedding a Web Application Firewall into the middleware chain?
Traefik + Coraza
Traefik is a critical piece of many companies’ infrastructure and is playing a great role in securing connectivity with applications and APIs. It's impossible for any single security measure to cover every potential attack angle. Security operates in layers, each with its own focus, sometimes with areas of overlap. A global approach, acknowledging the diverse threat landscape, is the way to go. One of the main links in the security chain are Web Application Firewalls. They look for malicious and unwanted content within incoming requests and mitigate those potential threats. Having a WAF integrated to Traefik as a plugin is clearly a game changer and adds a first line protection layer to your infrastructure.
Coraza is an open source, go based, high performance, Web Application Firewall. It’s an OWASP project, understands Modsecurity’s seclang language, and it’s able to enforce OWASP core rule sets.
Of course, when Coraza’s leaders proposed to provide an integration of Coraza in Traefik within a WASM plugin, we were all pretty excited. José Carlos Chávez quickly came up with a working solution now available on the Traefik Plugins Catalog!
First of all, let’s load the Coraza plugin into the static configuration:
# This config was generated by "mage updateVersion". DO NOT EDIT.
entryPoints:
web:
address: :80
providers:
file:
filename: /etc/traefik/config-dynamic.yaml
experimental:
plugins:
coraza:
moduleName: github.com/jcchavezs/coraza-http-wasm-traefik
version: v0.2.1
Let’s update the middleware section for a router in the dynamic configuration:
http:
# ...
middlewares:
waf:
plugin:
coraza:
directives:
- SecRuleEngine On
- SecDebugLog /dev/stdout
- SecDebugLogLevel 9
- SecRule REQUEST_URI "@streq /admin" "id:101,phase:1,log,deny,status:403"
```
Then curl -I 'http://localhost:8080/admin'
will return a 403 error as specified by the configuration rule SecRule REQUEST_URI "@streq /admin.
curl -I 'http://localhost:8080/anything'
will return a 200 as there is no matching rule.
A more advanced example can be found here, where we attempt to use the log4shell attack on a vulnerable application. Log4shell is a zero-day vulnerability in Log4j involving arbitrary code execution. This attack can be mitigated by simply enabling CRS rule 932130 to look into REQUEST_HEADERS:
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
name: config
namespace: traefik
data:
config.yaml: |
http:
middlewares:
waf:
plugin:
coraza:
directives:
- Include @recommended-conf
- Include @crs-setup-conf
- Include @owasp_crs/*.conf
- SecRuleUpdateTargetById 932130 "REQUEST_HEADERS"
- SecRuleEngine On
EOF
As you can see, the WASM plugin feature in Traefik provides a state-of-the-art extension technology and covers the most advanced use cases.
Takeaways
Traefik already had an extension engine based on the Yaegi Go interpreter. It is extremely easy to set up and provides a powerful foundation for customizing Traefik. However, it requires writing plugins in Go, and some people could find this too restrictive.
WebAssembly has gained huge popularity lately and provides exactly what is needed to build a perfect plugin engine: portable binaries, open standard, fast, multiple languages support, and multiple host environments. Traefik 3.0 adds full support of WASM plugins for middlewares and allows to run binary code compiled from many different languages like C, Rust, or JavaScript.
Finally, to show the full potential of this new plugin engine, we have integrated the powerful Web Application Firewall Coraza, as a WASM plugin. This will bring new critical capabilities to Traefik, adding an additional layer of security to your environment.
The Traefik community has played a central role in brainstorming and implementing those major features. We can’t say enough good things about the exceptional work made by Traefik contributors ❤️
To learn more about v3, watch the recording of our recent Traefik v3 Online Meetup.
Stay tuned for more deep dives on Traefik 3.0 key features!