Using the Proxy-Wasm Filter
This guide explains how your Quarkus application can utilize Proxy-Wasm plugins to filter requests to Jakarta REST (formerly known as JAX-RS) endpoints.
Proxy-Wasm is a plugin system for network proxies. It lets you write plugins that can act as request filters in a portable, sandboxed, and language-agnostic way, thanks to WebAssembly.
Docs and SDKs for plugin authors:
Popular Proxy-Wasm plugins:
Installation
If you want to use this extension, you need to add the io.quarkiverse.proxy-wasm:quarkus-proxy-wasm
extension first to your build file.
For instance, with Maven, add the following dependency to your POM file:
<dependency>
<groupId>io.quarkiverse.proxy-wasm</groupId>
<artifactId>quarkus-proxy-wasm</artifactId>
<version>0.0.1</version>
</dependency>
Annotating the Jakarta REST resource
We will walk you through the steps to create add a Proxy-Wasm filter to a Jakarta REST resource. Let’s assume you have an existing Jakarta REST resource that returns a simple string:
package org.example;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/example")
public class Example {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "hello";
}
}
To filter requests to that resource you would add a @ProxyWasm
at either the class or method level depending on whether you want to filter all requests to the resource or just a specific method.
The @ProxyWasm
annotation should list the names of all the pluigns you want to apply to the resource or method.
package org.example;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import io.roastedroot.proxywasm.jaxrs.ProxyWasm;
@ProxyWasm("waf")
@Path("/example")
public class Example {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "hello";
}
}
The example above will apply the waf
plugin instance to all requests to the /example
resource.
Configuring the Proxy-Wasm plugin
package org.example;
import com.dylibso.chicory.wasm.Parser;
import com.dylibso.chicory.wasm.WasmModule;
import io.roastedroot.proxywasm.StartException;
import io.roastedroot.proxywasm.Plugin;
import io.roastedroot.proxywasm.PluginFactory;
import io.roastedroot.proxywasm.SimpleMetricsHandler;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
@ApplicationScoped
public class App {
private static WasmModule module =
Parser.parse(App.class.getResourceAsStream("coraza-proxy-wasm.wasm")); (1)
// loads the plugin configuration as a classpath resource
static final String CONFIG; (4)
static {
try (InputStream is = App.class.getResourceAsStream("waf-config.json")) {
CONFIG = new String(is.readAllBytes(), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
// creates a plugin factory that loads the wasm module and configuration
@Produces
public PluginFactory waf() throws StartException {
return () ->
Plugin.builder(module)
.withName("waf") (2)
.withPluginConfig(CONFIG)
.withMetricsHandler(new SimpleMetricsHandler()) (3)
.build();
}
}
1 | This will load the src/main/resources/org/example/coraza-proxy-wasm.wasm file. You can get that wasm module here. |
2 | The name of the plugin must match the name used in the @ProxyWasm annotation. |
3 | The corazawf wasm module requires access to publish custom metrics. We give it SimpleMetricsHandler implementation which gives it access to do so, but those metrics are only kept in memory and don’t get exposed anywhere. You can implement your own MetricsHandler to publish the metrics to a monitoring system of your choice. |
4 | The configuration that will be passed to the plugin. Since we are loading from the classpath store the config inf src/main/resources/org/example/waf-config.json : |
{
"directives_map": {
"rs1": [
"Include @demo-conf",
"Include @crs-setup-conf",
"SecDefaultAction \"phase:3,log,auditlog,pass\"",
"SecDefaultAction \"phase:4,log,auditlog,pass\"",
"SecDefaultAction \"phase:5,log,auditlog,pass\"",
"SecDebugLogLevel 9",
"Include @owasp_crs/*.conf",
"SecRule REQUEST_URI \"@streq /admin\" \"id:101,phase:1,t:lowercase,deny\" \nSecRule REQUEST_BODY \"@rx maliciouspayload\" \"id:102,phase:2,t:lowercase,deny\" \nSecRule RESPONSE_HEADERS::status \"@rx 406\" \"id:103,phase:3,t:lowercase,deny\" \nSecRule RESPONSE_BODY \"@contains responsebodycode\" \"id:104,phase:4,t:lowercase,deny\""
]
},
"default_directives": "rs1",
"metric_labels": {
"owner": "coraza",
"identifier": "global"
},
"per_authority_directives":{
"bar.example.com":"rs1"
}
}