HTTP plugins are still under heavy development as of version v0.20.
Overview
Octelium already supports native ways to manipulate HTTP request/response headers, validate request JSON body and send direct responses back to the downstreams (read more here). However, in many real-world use cases, such native methods might be not flexible enough.
Plugins provide an additional, more advanced and dynamic way to manipulate HTTP requests and responses in arbitrarily complex ways. Such plugins can be especially useful for many use cases that can be applied to ZTNA/BeyondCorp, API gateways, AI gateways, MCP gateways, etc...
A plugin has a required unique name, a required condition that decides whether the plugin should be triggered on a per-request basis via policy-as-code, and the type-specific configuration (e.g. Lua, rate limiting, etc... as will be shown below). Uoi can have one or more plugins, including multiple plugins of the same type. For example, here is a list of plugins that includes one lua plugin that is triggered on all requests.
1kind: Service2metadata:3name: example4spec:5mode: HTTP6config:7upstream:8url: https://api.example.com9http:10plugins:11- name: my-plugin12condition:13matchAny: true14lua:15# The rest of your config
Here is another example where the plugin is only used for requests whose path prefix is /api/v1:
1kind: Service2metadata:3name: example4spec:5mode: HTTP6config:7upstream:8url: https://api.example.com9http:10plugins:11- name: my-plugin12condition:13match: ctx.request.http.path.startsWith("/api/v1")14lua:15# The rest of your config
Disabling a Plugin
You can also disable a specific plugin without having to change its condition or delete it as follows:
1kind: Service2metadata:3name: svc14spec:5mode: HTTP6config:7upstream:8url: https://api.example.com9http:10plugins:11- name: my-plugin12isDisabled: true13condition:14matchAny: true15lua:16# The rest of your config
Phase
By default, plugins are invoked after the authentication and authorization processes are done. However, you might choose to invoke a plugin before authentication/authorization by explicity setting a phase value for that plugin. Currently the values available are: POST_AUTH which is the default behavior when not explicitly set, and PRE_AUTH to force a pre-auth invocation. PRE_AUTH plugins might be useful to do pre-authentication manipulation or even completely dropping the requests before proceeding further to the authentication/authorization process.
Note that PRE_AUTH plugins must be in the default Service configuration. In other words, you can not have a PRE_AUTH in a named dynamic config (read more here). Here is an example:
1kind: Service2metadata:3name: svc14spec:5mode: HTTP6config:7upstream:8url: https://api.example.com9http:10plugins:11- name: check-body-size12phase: PRE_AUTH13condition:14matchAny: true15lua:16# The rest of your config
Types
Lua
Lua plugin invokes a Lua script that includes two functions: onRequest that is invoked upon receiving the request before proxying it to the upstream, and onResponse that is invoked upon receiving the response from the upstream. Here is a detailed example:
1kind: Service2metadata:3name: svc14spec:5mode: HTTP6config:7upstream:8url: https://api.example.com9http:10plugins:11- name: my-lua-script12condition:13matchAny: true14lua:15inline: |16function onRequest(ctx)17octelium.req.setRequestHeader("X-User-Uid", ctx.user.metadata.uid)18octelium.req.deleteRequestHeader("X-Delete")19octelium.req.setQueryParam("user", ctx.user.metadata.name)20octelium.req.deleteQueryParam("param1")21local body = json.decode(octelium.req.getRequestBody())22if strings.contains(strings.toLower(strings.trim(base64.decode(body.strField))), "privileged") then23local res = {24error = "You are not allowed"25}26octelium.req.setResponseBody(json.encode(res))27octelium.req.exit(403)28return29end3031if strings.hasPrefix(ctx.request.http.path, "/users") then32octelium.req.setPath("/users/"..ctx.user.metadata.uid)33end3435if strings.hasSuffix(ctx.request.http.path, ".php") then36octelium.req.setPath("/users/"..ctx.user.metadata.uid)37end3839if strings.len(body.someStrField) > 1000 then40local c = http.client()41c:setBaseURL("http://my-api.default.svc")42local req = c:request()43req:setBody(json.encode(ctx.user))44req:setHeader("X-User-Uid", ctx.user.metadata.uid)45local resp, err = req:post("/v1/check-user")46if err then47octelium.req.exit(500)48return49end5051if resp:code() == 200 then52local apiResp = json.decode(resp:body())53if not apiResp.userIsAllowed then54octelium.req.exit(403)55return56end57end58end59end6061function onResponse(ctx)62octelium.req.setResponseHeader("X-Session-Uid", ctx.session.metadata.uid)63octelium.req.deleteResponseHeader("X-Delete")64local resp = octelium.req.getResponseBody()65resp.email = ctx.user.spec.email66octelium.req.setResponseBody(json.encode(resp))67octelium.req.setStatusCode(209)68end
Note that you do not have to define both onRequest and onResponse in every Lua script. If any of these functions does not exist, then it is silently skipped.
Direct Response
The direct plugin enables you to directly return a response without proceeding to the upstream. This acts as a much more performant, yet less flexible, way compared to Lua plugins to, for example, drop unwanted requests that contain undesirable request paths, body content, etc.... Here is an example:
1kind: Service2metadata:3name: example4spec:5mode: HTTP6config:7upstream:8url: https://api.example.com9http:10plugins:11- name: drop-php12condition:13match: ctx.request.http.path.toLower().endsWith(".php")14direct:15body:16inline: This is not a PHP server!17statusCode: 40418headers:19X-Custom-Header: some-value20X-Another-Header: another-value
JSON Schema Validation
The JSON schema validation plugin ise used to validate the request body content according to a predefined JSON schema. Here is an example:
1kind: Service2metadata:3name: example4spec:5mode: HTTP6config:7upstream:8url: https://api.example.com9http:10plugins:11- name: validate-users12condition:13match: ctx.request.http.path.startsWith("/users")14jsonSchema:15inline: |16{17"$schema": "http://json-schema.org/draft-07/schema#",18"title": "User",19"description": "A user in the system",20"type": "object",21"properties": {22"id": {23"description": "The unique identifier for the user",24"type": "integer"25},26// The rest of your JSON schema
In practice, however, you might want to use different JSON schemas that are used depending on the context (e.g. request path). Here is an example:
1kind: Service2metadata:3name: example4spec:5mode: HTTP6config:7upstream:8url: https://api.example.com9http:10plugins:11- name: validate-users12condition:13match: ctx.request.http.path.startsWith("/users")14jsonSchema:15inline: <USER_JSON_SCHEMA>16- name: validate-groups17condition:18match: ctx.request.http.path.startsWith("/groups")19jsonSchema:20inline: <GROUP_JSON_SCHEMA>
By default, if the JSON schema validation fails, the Service returns a status code of 400. You can, however, explicitly configure the returned status code and the body as follows:
1kind: Service2metadata:3name: example4spec:5mode: HTTP6config:7upstream:8url: https://api.example.com9http:10plugins:11- name: validate-users12condition:13match: ctx.request.http.path.startsWith("/users")14jsonSchema:15inline: <USER_JSON_SCHEMA>16statusCode: 40317body:18inline: You are not allowed!
Rate Limiting
The rate limiting plugin provides a global sliding-window rate limiting mechanism that is backed by the Cluster's Redis store. Here is an example where a Session is allowed to have up to 6 requests within a 30-second window:
1kind: Service2metadata:3name: example4spec:5mode: WEB6isPublic: true7config:8upstream:9url: https://example.com10http:11plugins:12- name: main-rate-limit13condition:14matchAny: true15ratelimit:16limit: 617window:18seconds: 30
You can also define multiple rate limits. For example you might allow up to 10 requests per minute as well as up to 100 requests per hour per Session as follows:
1kind: Service2metadata:3name: example4spec:5mode: WEB6isPublic: true7config:8upstream:9url: https://example.com10http:11plugins:12- name: minute-rate-limit13condition:14matchAny: true15ratelimit:16limit: 1017window:18minutes: 119- name: hour-rate-limit20condition:21matchAny: true22ratelimit:23limit: 10024window:25hours: 1
As mentioned above, by default the Service applies the rate limit on a per-Session basis. You can, however, override that behavior and dynamically evaluate your own key. Here is an example where the User's uid is used as the key:
1kind: Service2metadata:3name: example4spec:5mode: WEB6isPublic: true7config:8upstream:9url: https://example.com10http:11plugins:12- name: main-rate-limit13condition:14matchAny: true15ratelimit:16limit: 617key:18eval: ctx.user.metadata.uid19window:20seconds: 30
By default, when the rate limit is exceeded, the Service returns a status code of 429. You can, however, override that behavior and set your own status code and body content as follows:
1kind: Service2metadata:3name: example4spec:5mode: WEB6isPublic: true7config:8upstream:9url: https://example.com10http:11plugins:12- name: main-rate-limit13condition:14matchAny: true15ratelimit:16limit: 617window:18seconds: 3019statusCode: 40020body:21inline: "Invalid request!"
Caching
The cache plugin returns globally cached responses that are stored in the Cluster's Redis store. Here is an example where content in /static prefix path are cached for 2 hours:
1kind: Service2metadata:3name: example4spec:5mode: WEB6isPublic: true7config:8upstream:9url: https://example.com10http:11plugins:12- name: cache-static-files13condition:14match: ctx.request.http.path.startsWith("/static")15cache:16ttl:17hours: 2
By default, the Service uses the request URI (i.e. path + query parameters) on a per Service basis as the key for the cache entry. You can, however, override that default behavior and set your own key. Here is an example where the key is set to a combination of the Service uid, the request path and the User uid to cache the responses on a per-User basis:
1kind: Service2metadata:3name: example4spec:5mode: HTTP6config:7upstream:8url: https://api.example.com9http:10plugins:11- name: cache-static-files12condition:13match: ctx.request.http.path.startsWith("/static")14cache:15ttl:16hours: 217key:18eval: ctx.service.metadata.uid + ctx.request.http.path + ctx.user.metadata.uid
By default, the Service only does cache GET and HEAD requests. You can, however, override that behavior and allow for caching other HTTP methods via the allowUnsafeMethods field as follows:
1kind: Service2metadata:3name: example4spec:5mode: HTTP6config:7upstream:8url: https://api.example.com9http:10plugins:11- name: cache-static-files12condition:13match: ctx.request.http.path.startsWith("/static")14cache:15ttl:16hours: 217allowUnsafeMethods: true
You can also define the max size, in bytes, of responses that can be cached via the maxSize field as follows:
1kind: Service2metadata:3name: example4spec:5mode: HTTP6config:7upstream:8url: https://api.example.com9http:10plugins:11- name: cache-static-files12condition:13match: ctx.request.http.path.startsWith("/static")14cache:15ttl:16hours: 217maxSize: 1000000
Path Manipulation
The path plugin can remove or/and add prefixes of the request path. Here is an example of removing a prefix:
1kind: Service2metadata:3name: example4spec:5mode: HTTP6config:7upstream:8url: https://api.example.com9http:10plugins:11- name: remove-api-prefix12condition:13matchAny: true14path:15removePrefix: /api/v1
You can also add a path prefix via addPrefix as follows:
1kind: Service2metadata:3name: example4spec:5mode: HTTP6config:7upstream:8url: https://api.example.com9http:10plugins:11- name: remove-api-prefix12condition:13matchAny: true14path:15addPrefix: /api/v1
You can also replace a prefix with another by combining both removePrefix and addPrefix as follows:
1kind: Service2metadata:3name: example4spec:5mode: HTTP6config:7upstream:8url: https://api.example.com9http:10plugins:11- name: remove-api-prefix12condition:13matchAny: true14path:15removePrefix: /api/v116addPrefix: /api/v2
Envoy Ext Proc
Octelium also supports Envoy's ext_proc. In this plugin type, Octelium's identity-aware proxy, Vigil, behaves like an Envoy instance and acts as an ext_proc gRPC client that sends ProcessingRequest to your ext_proc gRPC compliant server and waits for ProcessingResponse on every request. Here is an example:
1kind: Service2metadata:3name: example4spec:5mode: HTTP6config:7upstream:8url: https://api.example.com9http:10plugins:11- name: my-ext-proc-svc12condition:13match: ctx.request.http.path.startsWith("/api/v1")14extProc:15address: ext-proc.default.svc:8080