Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Network scripting

Middleware entries under [network.middleware] trigger transparent TLS interception on matching connections. This gives your Lua scripts access to the full HTTP request and response, letting you do things like inject credentials, enforce path-level access control, or rewrite payloads.

If you haven’t read the Network chapter yet, start there — it covers the rule system and policy that middleware builds on.

How it works

Each middleware entry is a named Lua script with target patterns that determine which connections it applies to. The script receives a req object representing the intercepted HTTP request.

[network.middleware.add-header]
target = ["api.example.com:443"]
script = '''
req:setHeader("X-Custom", "added-by-airlock")
'''

If the script doesn’t explicitly block or forward the request, airlock forwards it automatically after the script finishes — with any modifications you’ve applied.

Environment variables

Middleware can reference host environment variables through the env table. Define the mapping in the middleware config:

[network.middleware.api-auth]
target = ["api.example.com:443"]
env.TOKEN = "${MY_API_KEY}"
script = '''
req:setHeader("Authorization", "Bearer " .. env.TOKEN)
'''

The ${VAR} syntax reads from the host environment first and the secret vault as fallback. If a referenced name resolves in neither, airlock start aborts with an error — middleware never runs with silently-missing inputs, so scripts can treat every declared entry as present.

TLS interception

A per-project CA certificate is automatically generated and installed in the VM’s system trust store the first time you start a sandbox. Processes inside the container see valid certificates for intercepted connections — no manual trust configuration is needed.

All allowed TLS connections are intercepted so requests are visible in the Monitor tab, regardless of whether a middleware script matches. Middleware runs only for connections that match its target; connections with no matching middleware are still MITM-decrypted but pass through unmodified.

Request API

The req object is available in every middleware script:

Field / MethodDescription
req.methodHTTP method ("GET", "POST", etc.)
req.pathURL path (readable and writable)
req.hostHost header
req.headersFull headers table (readable and writable)
req:header(name)Read a single header value
req:setHeader(name, value)Set or overwrite a header
req:hostMatches(pattern)Match host against a wildcard pattern
req:body()Read the request body (returns a Body object)
req:setBody(value)Replace the body (string, table, Body, or nil)
req:deny()Block the request with a 403 response
req:send()Forward the request and return the response
log(msg)Write to the supervisor debug log

Blocking a request

Call req:deny() to immediately block the request with a 403 response:

if req.path:find("^/admin") then
    req:deny()
end

Forwarding and inspecting the response

Call req:send() to forward the request to the upstream server. This returns a response object that you can inspect and modify before it reaches the client:

local res = req:send()
if res.status == 200 then
    log("request succeeded: " .. req.path)
end

If you don’t call req:send() or req:deny(), the request is forwarded automatically when the script finishes — but you won’t get access to the response.

Response API

The response object returned by req:send() has a similar interface:

Field / MethodDescription
res.statusHTTP status code (readable and writable)
res.headersFull headers table (readable and writable)
res:header(name)Read a single header value
res:setHeader(name, value)Set or overwrite a header
res:body()Read the response body (returns a Body object)
res:setBody(value)Replace the body (string, table, Body, or nil)

Body objects

Both req:body() and res:body() return a Body object with these methods:

MethodDescription
body:text()Raw bytes as a Lua string
body:json()Parse as JSON, return a Lua table
body:len() or #bodyByte length

When you call req:setBody() or res:setBody() with a Lua table, it’s serialized as JSON automatically. The Content-Length header is updated to match the new body size.

Performance note: request and response bodies are streamed lazily by default. Calling req:body() or res:body() reads the entire body into memory. For most API traffic this is fine, but be careful with endpoints that transfer large payloads — a multi-gigabyte upload or download will be fully materialized in the proxy’s memory and could cause an out-of-memory crash. If you only need to inspect headers or the request path, avoid calling body() altogether.

Chaining middleware

Multiple middleware entries with overlapping target patterns all apply to matching connections. They run in order, and each one sees the modifications made by the previous:

[network.middleware.add-id]
target = ["api.example.com:443"]
script = '''
req:setHeader("X-Request-ID", "abc123")
'''

[network.middleware.log-id]
target = ["api.example.com:443"]
script = '''
-- this script sees the X-Request-ID header set above
log("request id: " .. req:header("X-Request-ID"))
'''

If any script calls req:deny(), the chain stops and the request is blocked.

Examples

Path-based access control

Allow GitHub API requests only to specific endpoints:

[network.rules.github]
allow = ["api.github.com:443"]

[network.middleware.github-paths]
target = ["api.github.com:443"]
script = '''
local p = req.path
if not (p == "/user" or p:find("^/repos/myorg/")) then
    req:deny()
end
'''

Logging request bodies

[network.middleware.log-posts]
target = ["api.example.com:443"]
script = '''
if req.method == "POST" then
    local b = req:body()
    log("POST " .. req.path .. " (" .. b:len() .. " bytes)")
end
'''

Modifying a response

[network.middleware.sandbox-header]
target = ["api.example.com:443"]
script = '''
local res = req:send()
res:setHeader("X-Sandbox", "true")
'''

Sandbox restrictions

Middleware scripts run in a restricted Lua environment. The following standard libraries are disabled: os, io, debug, require, load, loadfile, and dofile. There is no way to access the host filesystem or execute external processes from a script.

Each request has an instruction limit of 1,000,000 operations. If a script exceeds this limit, the request fails with an error. This prevents runaway scripts from blocking the network proxy.