HTTP Plugins
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.
kind: Service
metadata:
name: example
spec:
mode: HTTP
config:
upstream:
url: https://api.example.com
http:
plugins:
- name: my-plugin
condition:
matchAny: true
lua:
# The rest of your configHere is another example where the plugin is only used for requests whose path prefix is /api/v1:
kind: Service
metadata:
name: example
spec:
mode: HTTP
config:
upstream:
url: https://api.example.com
http:
plugins:
- name: my-plugin
condition:
match: ctx.request.http.path.startsWith("/api/v1")
lua:
# The rest of your configDisabling a Plugin
You can also disable a specific plugin without having to change its condition or delete it as follows:
kind: Service
metadata:
name: svc1
spec:
mode: HTTP
config:
upstream:
url: https://api.example.com
http:
plugins:
- name: my-plugin
isDisabled: true
condition:
matchAny: true
lua:
# The rest of your configPhase
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:
kind: Service
metadata:
name: svc1
spec:
mode: HTTP
config:
upstream:
url: https://api.example.com
http:
plugins:
- name: check-body-size
phase: PRE_AUTH
condition:
matchAny: true
lua:
# The rest of your configTypes
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:
kind: Service
metadata:
name: svc1
spec:
mode: HTTP
config:
upstream:
url: https://api.example.com
http:
plugins:
- name: my-lua-script
condition:
matchAny: true
lua:
inline: |
function onRequest(ctx)
octelium.req.setRequestHeader("X-User-Uid", ctx.user.metadata.uid)
octelium.req.deleteRequestHeader("X-Delete")
octelium.req.setQueryParam("user", ctx.user.metadata.name)
octelium.req.deleteQueryParam("param1")
local body = json.decode(octelium.req.getRequestBody())
if strings.contains(strings.toLower(strings.trim(base64.decode(body.strField))), "privileged") then
local res = {
error = "You are not allowed"
}
octelium.req.setResponseBody(json.encode(res))
octelium.req.exit(403)
return
end
if strings.hasPrefix(ctx.request.http.path, "/users") then
octelium.req.setPath("/users/"..ctx.user.metadata.uid)
end
if strings.hasSuffix(ctx.request.http.path, ".php") then
octelium.req.setPath("/users/"..ctx.user.metadata.uid)
end
if strings.len(body.someStrField) > 1000 then
local c = http.client()
c:setBaseURL("http://my-api.default.svc")
local req = c:request()
req:setBody(json.encode(ctx.user))
req:setHeader("X-User-Uid", ctx.user.metadata.uid)
local resp, err = req:post("/v1/check-user")
if err then
octelium.req.exit(500)
return
end
if resp:code() == 200 then
local apiResp = json.decode(resp:body())
if not apiResp.userIsAllowed then
octelium.req.exit(403)
return
end
end
end
end
function onResponse(ctx)
octelium.req.setResponseHeader("X-Session-Uid", ctx.session.metadata.uid)
octelium.req.deleteResponseHeader("X-Delete")
local resp = octelium.req.getResponseBody()
resp.email = ctx.user.spec.email
octelium.req.setResponseBody(json.encode(resp))
octelium.req.setStatusCode(209)
endNote 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:
kind: Service
metadata:
name: example
spec:
mode: HTTP
config:
upstream:
url: https://api.example.com
http:
plugins:
- name: drop-php
condition:
match: ctx.request.http.path.toLower().endsWith(".php")
direct:
body:
inline: This is not a PHP server!
statusCode: 404
headers:
X-Custom-Header: some-value
X-Another-Header: another-valueJSON 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:
kind: Service
metadata:
name: example
spec:
mode: HTTP
config:
upstream:
url: https://api.example.com
http:
plugins:
- name: validate-users
condition:
match: ctx.request.http.path.startsWith("/users")
jsonSchema:
inline: |
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "User",
"description": "A user in the system",
"type": "object",
"properties": {
"id": {
"description": "The unique identifier for the user",
"type": "integer"
},
// The rest of your JSON schemaIn 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:
kind: Service
metadata:
name: example
spec:
mode: HTTP
config:
upstream:
url: https://api.example.com
http:
plugins:
- name: validate-users
condition:
match: ctx.request.http.path.startsWith("/users")
jsonSchema:
inline: <USER_JSON_SCHEMA>
- name: validate-groups
condition:
match: ctx.request.http.path.startsWith("/groups")
jsonSchema:
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:
kind: Service
metadata:
name: example
spec:
mode: HTTP
config:
upstream:
url: https://api.example.com
http:
plugins:
- name: validate-users
condition:
match: ctx.request.http.path.startsWith("/users")
jsonSchema:
inline: <USER_JSON_SCHEMA>
statusCode: 403
body:
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:
kind: Service
metadata:
name: example
spec:
mode: WEB
isPublic: true
config:
upstream:
url: https://example.com
http:
plugins:
- name: main-rate-limit
condition:
matchAny: true
ratelimit:
limit: 6
window:
seconds: 30You 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:
kind: Service
metadata:
name: example
spec:
mode: WEB
isPublic: true
config:
upstream:
url: https://example.com
http:
plugins:
- name: minute-rate-limit
condition:
matchAny: true
ratelimit:
limit: 10
window:
minutes: 1
- name: hour-rate-limit
condition:
matchAny: true
ratelimit:
limit: 100
window:
hours: 1As 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:
kind: Service
metadata:
name: example
spec:
mode: WEB
isPublic: true
config:
upstream:
url: https://example.com
http:
plugins:
- name: main-rate-limit
condition:
matchAny: true
ratelimit:
limit: 6
key:
eval: ctx.user.metadata.uid
window:
seconds: 30By 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:
kind: Service
metadata:
name: example
spec:
mode: WEB
isPublic: true
config:
upstream:
url: https://example.com
http:
plugins:
- name: main-rate-limit
condition:
matchAny: true
ratelimit:
limit: 6
window:
seconds: 30
statusCode: 400
body:
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:
kind: Service
metadata:
name: example
spec:
mode: WEB
isPublic: true
config:
upstream:
url: https://example.com
http:
plugins:
- name: cache-static-files
condition:
match: ctx.request.http.path.startsWith("/static")
cache:
ttl:
hours: 2By 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:
kind: Service
metadata:
name: example
spec:
mode: HTTP
config:
upstream:
url: https://api.example.com
http:
plugins:
- name: cache-static-files
condition:
match: ctx.request.http.path.startsWith("/static")
cache:
ttl:
hours: 2
key:
eval: ctx.service.metadata.uid + ctx.request.http.path + ctx.user.metadata.uidBy 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:
kind: Service
metadata:
name: example
spec:
mode: HTTP
config:
upstream:
url: https://api.example.com
http:
plugins:
- name: cache-static-files
condition:
match: ctx.request.http.path.startsWith("/static")
cache:
ttl:
hours: 2
allowUnsafeMethods: trueYou can also define the max size, in bytes, of responses that can be cached via the maxSize field as follows:
kind: Service
metadata:
name: example
spec:
mode: HTTP
config:
upstream:
url: https://api.example.com
http:
plugins:
- name: cache-static-files
condition:
match: ctx.request.http.path.startsWith("/static")
cache:
ttl:
hours: 2
maxSize: 1000000Path Manipulation
The path plugin can remove or/and add prefixes of the request path. Here is an example of removing a prefix:
kind: Service
metadata:
name: example
spec:
mode: HTTP
config:
upstream:
url: https://api.example.com
http:
plugins:
- name: remove-api-prefix
condition:
matchAny: true
path:
removePrefix: /api/v1You can also add a path prefix via addPrefix as follows:
kind: Service
metadata:
name: example
spec:
mode: HTTP
config:
upstream:
url: https://api.example.com
http:
plugins:
- name: remove-api-prefix
condition:
matchAny: true
path:
addPrefix: /api/v1You can also replace a prefix with another by combining both removePrefix and addPrefix as follows:
kind: Service
metadata:
name: example
spec:
mode: HTTP
config:
upstream:
url: https://api.example.com
http:
plugins:
- name: remove-api-prefix
condition:
matchAny: true
path:
removePrefix: /api/v1
addPrefix: /api/v2Envoy 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:
kind: Service
metadata:
name: example
spec:
mode: HTTP
config:
upstream:
url: https://api.example.com
http:
plugins:
- name: my-ext-proc-svc
condition:
match: ctx.request.http.path.startsWith("/api/v1")
extProc:
address: ext-proc.default.svc:8080