Custom Lua scripts configuration for extending AAA processing logic
scripts
The scripts clause registers named Lua scripts that can be invoked from AAA
pipelines (authentication / authorization / accounting hooks, maps, or other
extensible points). Scripts enable flexible custom logic, enrichment, transformation, and external integration.
Each lua block defines exactly one script by name. Source can be loaded from a
file (filename) or embedded inline using content. A script is compiled to
Lua bytecode the first time it is executed, then cached until its source changes
(e.g. configuration reload with modified content).
Structure
scripts {
lua "auth_enrichment" {
filename "lua/auth_enrichment.lua";
}
lua "inline_example" {
content {
local context, previous = ...
-- context is an AAA context object
-- previous is the result from prior action (true/false/nil)
-- Return:
-- true -> accept (if used in a decision point)
-- false -> reject
-- nil -> no explicit decision
return previous
--END
}
}
}
Blocks and Statements
| Element | Context | Required | Description |
|---|---|---|---|
scripts { ... } | top-level | No (only needed if you use scripts) | Container for script definitions |
lua "NAME" { ... } | inside scripts | Yes (≥1) | Declares a script with the given name |
filename "path"; | inside lua | One of filename or content | Loads Lua source from file |
content { ... } | inside lua | One of filename or content | Inline Lua source (end with --END marker on its own line) |
Exactly one of filename or content must be present in a lua block. If both
are provided the configuration is invalid.
Naming
Script names are case sensitive. Reusing a name overwrites the earlier definition in the same configuration pass (last one wins). Using script overloading like this is a bad practise.
Runtime Model (Summary)
- Configuration parser registers all script definitions (name + source origin).
- When a script is executed the runtime:
- Compiles it (if not yet compiled).
- Caches the compiled function keyed by script name and a version token.
- Subsequent invocations reuse the compiled function unless the script changed.
- Script runs inside a sandboxed Lua VM with restricted environment (no direct uncontrolled filesystem or OS access unless exposed explicitly).
- Script receives:
- An AAA context userdata exposing sub-contexts (protocol, acct, user data, etc.).
- The previous optional boolean result (if part of a sequential pipeline).
Return semantics:
true→ Treated as an explicit accept (where meaningful).false→ Treated as an explicit reject.nil→ Neutral; processing continues.
Exact decision integration depends on where you inject the script; a neutral
return simply delegates to subsequent pipeline steps. Common pattern is to return the previous value received.
Script Parameters
Scripts receive two parameters via Lua varargs:
local context, previous = ...
| Parameter | Type | Description |
|---|---|---|
context | context | AAA context object providing access to request data |
previous | boolean? | Result from the previous action in the pipeline |
The previous parameter contains the result of the action that ran before this script:
true— previous action acceptedfalse— previous action rejectednil— no previous result (first action, or previous action returned nil)
This allows scripts to make decisions based on earlier pipeline results, for example to override or modify the outcome of a prior authentication check.
Scripts that only read data or perform side effects (logging, caching) should
return previous to preserve the pipeline result:
local context, previous = ...
-- Log and pass through
context:log("DEBUG", "Processing " .. tostring(context.aaa.identity))
return previous
Scripts that make authentication decisions return true, false, or nil:
local context, previous = ...
-- Check condition and reject if needed
local failures = context.vars:get("failure_count")
if failures and tonumber(failures) >= 3 then
return false -- Reject
end
return previous -- Otherwise preserve previous result
Script Context
The context object passed to each script provides access to AAA request data,
protocol-specific information, caches, user data, and server utilities. Sub-contexts
are accessed as fields (e.g., context.radius, context.aaa) while operations
use method syntax (e.g., context:log(), request:attr()).
For complete API documentation of all available sub-contexts and their fields/methods, see Script Context API.
Example Script (File-Based)
lua/auth_enrichment.lua:
local context, previous = ...
-- Example: add a reply message if identity matches a realm
local identity = context.aaa.identity
if identity and identity:match("@example%.com$") then
context.radius.reply:append_attr("Reply-Message", nil, "Welcome example.com user")
end
return previous
Configuration:
scripts {
lua "auth_enrichment" {
filename "lua/auth_enrichment.lua";
}
}
Then used inside an authorization block (pseudo):
authorization {
# call script "auth_enrichment"
script "auth_enrichment";
accept;
}
Example: Inline Script in RADIUS PAP Configuration
A complete .radconf example using inline Lua scripts for RADIUS PAP authentication
with login throttling. Two levels of throttling are implemented:
- Per-user throttling: Block user after 3 failures within 5 minutes
- Per-device throttling: Block device (NAS) after 30 failures within 1 minute
caches {
cache "USER_THROTTLE" {
timeout 300s; # 5 minute default TTL
}
cache "DEVICE_THROTTLE" {
timeout 60s; # 1 minute default TTL
}
}
scripts {
lua "check_throttle" {
content {
-- Pre-auth: check if user or device is throttled
local context, previous = ...
-- Check per-device throttle first (protects against brute force attacks)
local nas_ip = context.radius.request:attr("nas-ip-address")
if nas_ip then
local device_failures = context.cache:get("DEVICE_THROTTLE", nas_ip)
-- Block after 30 failed auth attempts from same NAS
if device_failures and tonumber(device_failures) >= 30 then
context.aaa.message = "Device temporarily blocked due to excessive failures."
return false
end
end
-- Check per-user throttle
local identity = context.aaa.identity
if identity then
local user_failures = context.cache:get("USER_THROTTLE", identity)
if user_failures and tonumber(user_failures) >= 3 then
context.aaa.message = "Too many failed attempts. Try again later."
return false
end
end
return nil
--END
}
}
lua "record_result" {
content {
-- Post-auth: record failures, clear on success
local context, previous = ...
local identity = context.aaa.identity
local nas_ip = context.radius.request:attr("nas-ip-address")
local result = context.aaa.result
if result == "reject" then
-- Auth failed: increment failure counts
if identity then
local user_failures = context.cache:get("USER_THROTTLE", identity)
user_failures = (tonumber(user_failures) or 0) + 1
context.cache:set("USER_THROTTLE", identity, tostring(user_failures))
end
if nas_ip then
local device_failures = context.cache:get("DEVICE_THROTTLE", nas_ip)
device_failures = (tonumber(device_failures) or 0) + 1
context.cache:set("DEVICE_THROTTLE", nas_ip, tostring(device_failures))
end
elseif result == "accept" then
-- Auth succeeded: clear user failures (device failures expire naturally)
if identity then
context.cache:remove("USER_THROTTLE", identity)
end
end
return previous
--END
}
}
}
backends {
file "USERS" {
filename "users.file";
}
}
clients {
client "LOCAL" {
address 127.0.0.1;
secret "testing123";
}
}
servers {
radius "AUTH_UDP" {
listen {
protocol udp;
port 1812;
ip 0.0.0.0;
}
clients "LOCAL";
}
}
aaa {
policy "DEFAULT" {
handler "AUTHENTICATION" {
authentication {
# Check throttle before authentication
script "check_throttle";
# Authenticate against users file
backend {
name "USERS";
}
# Verify password
pap;
}
post-authentication {
# Record success/failure for throttling
script "record_result";
}
authorization {
accept;
}
}
}
}