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

ElementContextRequiredDescription
scripts { ... }top-levelNo (only needed if you use scripts)Container for script definitions
lua "NAME" { ... }inside scriptsYes (≥1)Declares a script with the given name
filename "path";inside luaOne of filename or contentLoads Lua source from file
content { ... }inside luaOne of filename or contentInline 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)

  1. Configuration parser registers all script definitions (name + source origin).
  2. 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.
  3. Subsequent invocations reuse the compiled function unless the script changed.
  4. Script runs inside a sandboxed Lua VM with restricted environment (no direct uncontrolled filesystem or OS access unless exposed explicitly).
  5. 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 = ...
ParameterTypeDescription
contextcontextAAA context object providing access to request data
previousboolean?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 accepted
  • false — previous action rejected
  • nil — 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;
            }
        }
    }
}
Navigation
Children