Skip to content

raskell-io/sentinel-agent-lua

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

sentinel-agent-lua

Lua scripting agent for Sentinel reverse proxy. Write custom request/response processing logic in Lua.

Features

  • Execute Lua scripts on request/response lifecycle events
  • Sandboxed execution with resource limits (memory, CPU, time)
  • Hot-reload of scripts without restart
  • Rich standard library (JSON, crypto, HTTP utilities, regex)
  • Script metadata for routing and prioritization
  • VM pooling for performance

Installation

From crates.io

cargo install sentinel-agent-lua

From source

git clone https://github.com/raskell-io/sentinel-agent-lua
cd sentinel-agent-lua
cargo build --release

Usage

sentinel-lua-agent --socket /var/run/sentinel/lua.sock \
  --scripts /etc/sentinel/scripts

Command Line Options

Option Environment Variable Description Default
--socket AGENT_SOCKET Unix socket path /tmp/sentinel-lua.sock
--scripts LUA_SCRIPTS_DIR Scripts directory /etc/sentinel/scripts
--config LUA_CONFIG Configuration file -
--log-level RUST_LOG Log level info

Writing Scripts

Script Structure

-- name: my-script
-- version: 1.0.0
-- hook: request_headers
-- paths: /api/*
-- methods: GET, POST
-- priority: 100

function on_request_headers(request)
    -- Add custom header
    request:add_header("X-Processed-By", "my-script")

    -- Check authorization
    local auth = request:get_header("Authorization")
    if not auth then
        sentinel.set_decision("block")
        return
    end

    sentinel.set_decision("allow")
end

Available Hooks

Hook Description
on_request_headers(request) Called when request headers are received
on_request_body(request, body) Called when full request body is buffered
on_request_body_chunk(request, chunk, is_last) Called for each body chunk (streaming)
on_response_headers(response) Called when response headers are received
on_response_body(response, body) Called when full response body is buffered

Request/Response API

-- Request object
request:get_header("name")           -- Get header value
request:add_header("name", "value")  -- Add header
request:remove_header("name")        -- Remove header
request:get_path()                   -- Get request path
request:get_method()                 -- Get HTTP method
request:get_query()                  -- Get query string
request:get_source_ip()              -- Get client IP

-- Response object
response:get_status()                -- Get status code
response:get_header("name")          -- Get header value
response:add_header("name", "value") -- Add header
response:remove_header("name")       -- Remove header

Standard Library

JSON

local obj = json.decode('{"key": "value"}')
local str = json.encode({key = "value"})
local pretty = json.encode_pretty(obj)

Crypto

local hash = crypto.sha256("data")
local hash384 = crypto.sha384("data")
local hash512 = crypto.sha512("data")
local mac = crypto.hmac_sha256("key", "message")
local bytes = crypto.random_bytes(32)
local hex = crypto.random_hex(16)

HTTP Utilities

local encoded = http.url_encode("hello world")
local decoded = http.url_decode("hello%20world")
local params = http.parse_query("foo=bar&baz=qux")
local query = http.build_query({foo = "bar"})
local cookies = http.parse_cookies(cookie_header)
local text = http.status_text(200)  -- "OK"

Encoding

local b64 = encoding.base64_encode("data")
local data = encoding.base64_decode(b64)
local hex = encoding.hex_encode("data")
local data = encoding.hex_decode(hex)
local compressed = encoding.gzip_compress("data")
local data = encoding.gzip_decompress(compressed)

Regex

local matched = regex.match("^hello", "hello world")
local found = regex.find("\\d+", "value: 42")
local all = regex.find_all("\\d+", "1 2 3")
local replaced = regex.replace("\\d+", "value: 42", "X")

Time

local now = time.now()           -- Unix timestamp
local now_ms = time.now_ms()     -- Milliseconds
local formatted = time.format(now, "%Y-%m-%d")
local ts = time.parse("2024-01-01", "%Y-%m-%d")

Sentinel

sentinel.log("info", "message")
sentinel.set_decision("allow")   -- or "block", "redirect"
sentinel.add_metadata("key", "value")
sentinel.version                 -- Agent version

Configuration

Sentinel Proxy Configuration

agents {
    agent "lua" {
        type "custom"
        transport "unix_socket" {
            path "/var/run/sentinel/lua.sock"
        }
        events ["request_headers", "response_headers"]
        timeout-ms 100
        failure-mode "open"
    }
}

Agent Configuration (KDL)

socket-path "/var/run/sentinel/lua.sock"

scripts {
    directory "/etc/sentinel/scripts"
    hot-reload true
    watch-interval 5
    timeout 100
    cache-size 200
}

vm-pool {
    size 20
    max-age 600
    max-executions 5000
}

resource-limits {
    max-memory 52428800      // 50MB
    max-instructions 10000000
    max-execution-time 200
    allow-filesystem false
    allow-network false
}

safety {
    fail-open true
    debug-scripts false
    max-concurrent 200
}

Resource Limits

The agent enforces strict resource limits on Lua execution:

Limit Default Description
Memory 50MB Maximum memory per VM
Instructions 10M Maximum CPU instructions
Execution time 100ms Maximum script runtime
String length 10MB Maximum string size
Table size 10,000 Maximum table entries

Security

Scripts run in a sandboxed environment with:

  • No filesystem access (by default)
  • No network access (by default)
  • No process spawning
  • Dangerous functions removed (dofile, loadfile, etc.)
  • Limited libraries (string, table, math, utf8, coroutine)

Development

# Run with debug logging
RUST_LOG=debug cargo run -- --socket /tmp/test.sock --scripts ./scripts

# Run tests
cargo test

License

MIT OR Apache-2.0

About

Lua scripting agent for Sentinel reverse proxy - custom logic via Lua scripts

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 3

  •  
  •  
  •  

Languages