ManagementCore APIServices
HTTP Plugins
NOTE

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.

1
kind: Service
2
metadata:
3
name: example
4
spec:
5
mode: HTTP
6
config:
7
upstream:
8
url: https://api.example.com
9
http:
10
plugins:
11
- name: my-plugin
12
condition:
13
matchAny: true
14
lua:
15
# The rest of your config

Here is another example where the plugin is only used for requests whose path prefix is /api/v1:

1
kind: Service
2
metadata:
3
name: example
4
spec:
5
mode: HTTP
6
config:
7
upstream:
8
url: https://api.example.com
9
http:
10
plugins:
11
- name: my-plugin
12
condition:
13
match: ctx.request.http.path.startsWith("/api/v1")
14
lua:
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:

1
kind: Service
2
metadata:
3
name: svc1
4
spec:
5
mode: HTTP
6
config:
7
upstream:
8
url: https://api.example.com
9
http:
10
plugins:
11
- name: my-plugin
12
isDisabled: true
13
condition:
14
matchAny: true
15
lua:
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:

1
kind: Service
2
metadata:
3
name: svc1
4
spec:
5
mode: HTTP
6
config:
7
upstream:
8
url: https://api.example.com
9
http:
10
plugins:
11
- name: check-body-size
12
phase: PRE_AUTH
13
condition:
14
matchAny: true
15
lua:
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:

1
kind: Service
2
metadata:
3
name: svc1
4
spec:
5
mode: HTTP
6
config:
7
upstream:
8
url: https://api.example.com
9
http:
10
plugins:
11
- name: my-lua-script
12
condition:
13
matchAny: true
14
lua:
15
inline: |
16
function onRequest(ctx)
17
octelium.req.setRequestHeader("X-User-Uid", ctx.user.metadata.uid)
18
octelium.req.deleteRequestHeader("X-Delete")
19
octelium.req.setQueryParam("user", ctx.user.metadata.name)
20
octelium.req.deleteQueryParam("param1")
21
local body = json.decode(octelium.req.getRequestBody())
22
if strings.contains(strings.toLower(strings.trim(base64.decode(body.strField))), "privileged") then
23
local res = {
24
error = "You are not allowed"
25
}
26
octelium.req.setResponseBody(json.encode(res))
27
octelium.req.exit(403)
28
return
29
end
30
31
if strings.hasPrefix(ctx.request.http.path, "/users") then
32
octelium.req.setPath("/users/"..ctx.user.metadata.uid)
33
end
34
35
if strings.hasSuffix(ctx.request.http.path, ".php") then
36
octelium.req.setPath("/users/"..ctx.user.metadata.uid)
37
end
38
39
if strings.len(body.someStrField) > 1000 then
40
local c = http.client()
41
c:setBaseURL("http://my-api.default.svc")
42
local req = c:request()
43
req:setBody(json.encode(ctx.user))
44
req:setHeader("X-User-Uid", ctx.user.metadata.uid)
45
local resp, err = req:post("/v1/check-user")
46
if err then
47
octelium.req.exit(500)
48
return
49
end
50
51
if resp:code() == 200 then
52
local apiResp = json.decode(resp:body())
53
if not apiResp.userIsAllowed then
54
octelium.req.exit(403)
55
return
56
end
57
end
58
end
59
end
60
61
function onResponse(ctx)
62
octelium.req.setResponseHeader("X-Session-Uid", ctx.session.metadata.uid)
63
octelium.req.deleteResponseHeader("X-Delete")
64
local resp = octelium.req.getResponseBody()
65
resp.email = ctx.user.spec.email
66
octelium.req.setResponseBody(json.encode(resp))
67
octelium.req.setStatusCode(209)
68
end

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:

1
kind: Service
2
metadata:
3
name: example
4
spec:
5
mode: HTTP
6
config:
7
upstream:
8
url: https://api.example.com
9
http:
10
plugins:
11
- name: drop-php
12
condition:
13
match: ctx.request.http.path.toLower().endsWith(".php")
14
direct:
15
body:
16
inline: This is not a PHP server!
17
statusCode: 404
18
headers:
19
X-Custom-Header: some-value
20
X-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:

1
kind: Service
2
metadata:
3
name: example
4
spec:
5
mode: HTTP
6
config:
7
upstream:
8
url: https://api.example.com
9
http:
10
plugins:
11
- name: validate-users
12
condition:
13
match: ctx.request.http.path.startsWith("/users")
14
jsonSchema:
15
inline: |
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:

1
kind: Service
2
metadata:
3
name: example
4
spec:
5
mode: HTTP
6
config:
7
upstream:
8
url: https://api.example.com
9
http:
10
plugins:
11
- name: validate-users
12
condition:
13
match: ctx.request.http.path.startsWith("/users")
14
jsonSchema:
15
inline: <USER_JSON_SCHEMA>
16
- name: validate-groups
17
condition:
18
match: ctx.request.http.path.startsWith("/groups")
19
jsonSchema:
20
inline: <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:

1
kind: Service
2
metadata:
3
name: example
4
spec:
5
mode: HTTP
6
config:
7
upstream:
8
url: https://api.example.com
9
http:
10
plugins:
11
- name: validate-users
12
condition:
13
match: ctx.request.http.path.startsWith("/users")
14
jsonSchema:
15
inline: <USER_JSON_SCHEMA>
16
statusCode: 403
17
body:
18
inline: 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:

1
kind: Service
2
metadata:
3
name: example
4
spec:
5
mode: WEB
6
isPublic: true
7
config:
8
upstream:
9
url: https://example.com
10
http:
11
plugins:
12
- name: main-rate-limit
13
condition:
14
matchAny: true
15
ratelimit:
16
limit: 6
17
window:
18
seconds: 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:

1
kind: Service
2
metadata:
3
name: example
4
spec:
5
mode: WEB
6
isPublic: true
7
config:
8
upstream:
9
url: https://example.com
10
http:
11
plugins:
12
- name: minute-rate-limit
13
condition:
14
matchAny: true
15
ratelimit:
16
limit: 10
17
window:
18
minutes: 1
19
- name: hour-rate-limit
20
condition:
21
matchAny: true
22
ratelimit:
23
limit: 100
24
window:
25
hours: 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:

1
kind: Service
2
metadata:
3
name: example
4
spec:
5
mode: WEB
6
isPublic: true
7
config:
8
upstream:
9
url: https://example.com
10
http:
11
plugins:
12
- name: main-rate-limit
13
condition:
14
matchAny: true
15
ratelimit:
16
limit: 6
17
key:
18
eval: ctx.user.metadata.uid
19
window:
20
seconds: 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:

1
kind: Service
2
metadata:
3
name: example
4
spec:
5
mode: WEB
6
isPublic: true
7
config:
8
upstream:
9
url: https://example.com
10
http:
11
plugins:
12
- name: main-rate-limit
13
condition:
14
matchAny: true
15
ratelimit:
16
limit: 6
17
window:
18
seconds: 30
19
statusCode: 400
20
body:
21
inline: "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:

1
kind: Service
2
metadata:
3
name: example
4
spec:
5
mode: WEB
6
isPublic: true
7
config:
8
upstream:
9
url: https://example.com
10
http:
11
plugins:
12
- name: cache-static-files
13
condition:
14
match: ctx.request.http.path.startsWith("/static")
15
cache:
16
ttl:
17
hours: 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:

1
kind: Service
2
metadata:
3
name: example
4
spec:
5
mode: HTTP
6
config:
7
upstream:
8
url: https://api.example.com
9
http:
10
plugins:
11
- name: cache-static-files
12
condition:
13
match: ctx.request.http.path.startsWith("/static")
14
cache:
15
ttl:
16
hours: 2
17
key:
18
eval: 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:

1
kind: Service
2
metadata:
3
name: example
4
spec:
5
mode: HTTP
6
config:
7
upstream:
8
url: https://api.example.com
9
http:
10
plugins:
11
- name: cache-static-files
12
condition:
13
match: ctx.request.http.path.startsWith("/static")
14
cache:
15
ttl:
16
hours: 2
17
allowUnsafeMethods: true

You can also define the max size, in bytes, of responses that can be cached via the maxSize field as follows:

1
kind: Service
2
metadata:
3
name: example
4
spec:
5
mode: HTTP
6
config:
7
upstream:
8
url: https://api.example.com
9
http:
10
plugins:
11
- name: cache-static-files
12
condition:
13
match: ctx.request.http.path.startsWith("/static")
14
cache:
15
ttl:
16
hours: 2
17
maxSize: 1000000

Path Manipulation

The path plugin can remove or/and add prefixes of the request path. Here is an example of removing a prefix:

1
kind: Service
2
metadata:
3
name: example
4
spec:
5
mode: HTTP
6
config:
7
upstream:
8
url: https://api.example.com
9
http:
10
plugins:
11
- name: remove-api-prefix
12
condition:
13
matchAny: true
14
path:
15
removePrefix: /api/v1

You can also add a path prefix via addPrefix as follows:

1
kind: Service
2
metadata:
3
name: example
4
spec:
5
mode: HTTP
6
config:
7
upstream:
8
url: https://api.example.com
9
http:
10
plugins:
11
- name: remove-api-prefix
12
condition:
13
matchAny: true
14
path:
15
addPrefix: /api/v1

You can also replace a prefix with another by combining both removePrefix and addPrefix as follows:

1
kind: Service
2
metadata:
3
name: example
4
spec:
5
mode: HTTP
6
config:
7
upstream:
8
url: https://api.example.com
9
http:
10
plugins:
11
- name: remove-api-prefix
12
condition:
13
matchAny: true
14
path:
15
removePrefix: /api/v1
16
addPrefix: /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:

1
kind: Service
2
metadata:
3
name: example
4
spec:
5
mode: HTTP
6
config:
7
upstream:
8
url: https://api.example.com
9
http:
10
plugins:
11
- name: my-ext-proc-svc
12
condition:
13
match: ctx.request.http.path.startsWith("/api/v1")
14
extProc:
15
address: ext-proc.default.svc:8080
© 2025 octelium.comOctelium Labs, LLCAll rights reserved
Octelium and Octelium logo are trademarks of Octelium Labs, LLC.
WireGuard is a registered trademark of Jason A. Donenfeld