Skip to main content
Version: Next

Lua Policy Plugins

Nauthilus Lua plugins can emit typed policy attributes directly into the active request decision context. This lets a plugin collect a signal once and then let a top-level Policy rule decide the final effect.

Enable the Bundled Registry

The bundled policy-aware plugins emit attributes below lua.plugin.*. Register these attributes before using them in policies:

policy:
namespaces:
authn:
schema_contributions:
lua:
registry_scripts:
- "/etc/nauthilus/lua-plugins.d/policy/registry.lua"

The registry is loaded when the policy snapshot is built. If a plugin emits an unknown attribute, the Lua execution fails instead of silently creating a loose fact.

Plugin Helper

Bundled plugins use nauthilus_policy_facts:

local policy_facts = require("nauthilus_policy_facts")

policy_facts.emit("soft_delay", "risky", true)
policy_facts.emit_public("geoip", "rejected", true, {
status_message = "Policy violation",
})
policy_facts.status_message("geoip", "Policy violation")
  • emit and emit_many write policy attributes and request-local policy_facts.
  • emit_public and emit_many_public also write policy_fact_<namespace>_<key> custom logs.
  • status_message sets the normal Nauthilus status message and emits lua.plugin.<namespace>.status_message.
  • set stores only request-local Lua context data and does not emit a policy attribute.

Attribute Reference

All attributes below are registered by lua-plugins.d/policy/registry.lua.

Environment Source Plugins

PluginAttributeTypeMeaning
account_longwindow_metrics.lualua.plugin.account_longwindow.usernamestringAccount name used by the long-window collector.
account_longwindow_metrics.lualua.plugin.account_longwindow.authenticatedboolWhether the request was authenticated when metrics were collected.
account_longwindow_metrics.lualua.plugin.account_longwindow.uniq_ips_24hnumberUnique account IP estimate over 24 hours.
account_longwindow_metrics.lualua.plugin.account_longwindow.uniq_ips_7dnumberUnique account IP estimate over 7 days.
account_longwindow_metrics.lualua.plugin.account_longwindow.fails_24hnumberFailed account attempts over 24 hours.
account_longwindow_metrics.lualua.plugin.account_longwindow.fails_7dnumberFailed account attempts over 7 days.
account_longwindow_metrics.lualua.plugin.account_longwindow.has_pw_tokenboolWhether the request produced a sprayed-password token.
global_pattern_monitoring.lualua.plugin.global_pattern.attemptsnumberGlobal attempts in the current window.
global_pattern_monitoring.lualua.plugin.global_pattern.unique_ipsnumberGlobal unique IP estimate.
global_pattern_monitoring.lualua.plugin.global_pattern.unique_usersnumberGlobal unique user estimate.
global_pattern_monitoring.lualua.plugin.global_pattern.attempts_per_ipnumberAttempts-per-IP ratio.
global_pattern_monitoring.lualua.plugin.global_pattern.attempts_per_usernumberAttempts-per-user ratio.
global_pattern_monitoring.lualua.plugin.global_pattern.ips_per_usernumberIPs-per-user ratio.
security_metrics.lualua.plugin.security_metrics.global_ips_per_user_24hnumberGlobal IPs-per-user ratio over 24 hours.
security_metrics.lualua.plugin.security_metrics.global_ips_per_user_7dnumberGlobal IPs-per-user ratio over 7 days.
security_metrics.lualua.plugin.security_metrics.protected_accountsnumberNumber of accounts currently in protection mode.
failed_login_hotspot.lualua.plugin.failed_login_hotspot.usernamestringUsername evaluated against the failed-login hotspot set.
failed_login_hotspot.lualua.plugin.failed_login_hotspot.countnumberFailed-login hotspot score.
failed_login_hotspot.lualua.plugin.failed_login_hotspot.ranknumberFailed-login hotspot rank.
failed_login_hotspot.lualua.plugin.failed_login_hotspot.triggeredboolWhether the hotspot threshold matched.
blocklist.lualua.plugin.blocklist.matchedboolWhether the remote client matched the blocklist.
blocklist.lualua.plugin.blocklist.client_ipipClient IP sent to the blocklist service.
blocklist.lualua.plugin.blocklist.status_messagestringClient-visible blocklist message.

lua.plugin.blocklist.matched carries a public status_message detail when it is emitted.

Subject Source Plugins

PluginAttributeTypeMeaning
account_centric_monitoring.lualua.plugin.account_monitoring.attack_detectedboolWhether account-centric monitoring detected an attack pattern.
account_centric_monitoring.lualua.plugin.account_monitoring.usernamestringUsername evaluated by the monitor.
account_centric_monitoring.lualua.plugin.account_monitoring.uniq_ips_1hnumberUnique account IP estimate over 1 hour.
account_centric_monitoring.lualua.plugin.account_monitoring.uniq_ips_24hnumberUnique account IP estimate over 24 hours.
account_centric_monitoring.lualua.plugin.account_monitoring.uniq_ips_7dnumberUnique account IP estimate over 7 days.
account_centric_monitoring.lualua.plugin.account_monitoring.failed_24hnumberFailed account attempts over 24 hours.
account_centric_monitoring.lualua.plugin.account_monitoring.ratio_24hnumberAccount IP-to-failure ratio over 24 hours.
account_protection_mode.lualua.plugin.account_protection.activeboolWhether account protection mode is active.
account_protection_mode.lualua.plugin.account_protection.reasonstringComma-separated protection reason codes.
account_protection_mode.lualua.plugin.account_protection.backoff_levelnumberCurrent protection backoff level.
account_protection_mode.lualua.plugin.account_protection.delay_msnumberDelay applied in milliseconds.
account_protection_mode.lualua.plugin.account_protection.enforce_rejectboolWhether protection mode rejects unauthenticated traffic.
account_protection_mode.lualua.plugin.account_protection.status_messagestringClient-visible protection message.
geoip.lualua.plugin.geoip.guidstringGeoIP service request identifier.
geoip.lualua.plugin.geoip.current_country_codestringCurrent ISO-3166 alpha-2 country code.
geoip.lualua.plugin.geoip.country_codesstring_listCountry codes observed by the GeoIP service.
geoip.lualua.plugin.geoip.rejectedboolWhether the GeoIP service requested rejection.
geoip.lualua.plugin.geoip.errorboolWhether the GeoIP service returned an error.
geoip.lualua.plugin.geoip.status_messagestringClient-visible GeoIP message.
idp_policy.lualua.plugin.idp_policy.rejectedboolWhether the IdP Lua policy rejected the request.
idp_policy.lualua.plugin.idp_policy.reasonstringReason returned by the IdP Lua policy.
idp_policy.lualua.plugin.idp_policy.oidc_cidstringOIDC client identifier evaluated by the plugin.
idp_policy.lualua.plugin.idp_policy.grant_typestringOIDC grant type evaluated by the plugin.
idp_policy.lualua.plugin.idp_policy.status_messagestringClient-visible IdP policy message.
monitoring.lualua.plugin.director.backend_serverstringBackend server selected by the director subject source.
soft_delay.lualua.plugin.soft_delay.riskyboolWhether the soft-delay subject source considered the request risky.
soft_delay.lualua.plugin.soft_delay.applied_msnumberDelay applied in milliseconds.

lua.plugin.account_protection.active, lua.plugin.geoip.rejected, and lua.plugin.idp_policy.rejected carry a public status_message detail when emitted with a client-visible message.

Policy Example

policy:
namespaces:
authn:
schema_contributions:
lua:
registry_scripts:
- "/etc/nauthilus/lua-plugins.d/policy/registry.lua"
providers:
lua_subject_geoip:
kind: lua_subject
script_path: /etc/nauthilus/lua-plugins.d/subject/geoip.lua
targets: [{action: authenticate}]
executions: [host_sync]
domain_plans:
password:
checkpoints:
subject_analysis:
providers:
- name: geoip
use: authn/lua_subject_geoip
actions: [authenticate]
auth_decision:
providers: []
policy_sets:
configured:
visibility: private
rules:
- name: deny_geoip_rejection
checkpoint: auth_decision
actions: [authenticate]
if:
attribute: lua.plugin.geoip.rejected
is: true
then:
decision: deny
reason: geoip_policy_rejected
response_marker: auth.response.fail
response_message:
from: attribute_detail
attribute: lua.plugin.geoip.rejected
detail: status_message
fallback: "Invalid login or password"
targets:
- namespace: authn
action: authenticate
schema: authn/authenticate/v1
mode: enforce
default_policy: authn/standard_auth
domain_plan: authn/password
plans:
auth_decision:
policy_sets: [authn/configured]

Writing Your Own Emitter

Register the attribute:

nauthilus_policy.register_attribute({
id = "lua.plugin.example.risky",
stage = "subject_analysis",
operations = { "authenticate" },
category = "environment",
type = "bool",
description = "Example risk flag",
})

Emit it from a request-time Lua plugin:

local policy = require("nauthilus_policy")

policy.emit_attribute({
id = "lua.plugin.example.risky",
value = true,
})

The emitted type, operation, and stage must match the registry definition.

Lua backends can emit the built-in master-user fact through the dedicated helper:

local policy = require("nauthilus_policy")

policy.emit_master_user({
active = true,
master_user = "admin@example.test",
target_user = "alice@example.test",
})

This records auth.master_user.active for the active authenticate request and attaches master_user and target_user details when provided.

Generic Target-Aware Lua Providers

The emitter interface above belongs to the authentication pipeline. A generic Policy target instead uses a provider with kind: lua and one or both exact callbacks:

_G["policy.facts.collect"] = function(request)
return {
facts = {
{name = "risk_score", value = {kind = "integer", value = "42"}},
},
}
end

_G["policy.effects.execute"] = function(request)
return {state = "succeeded"}
end

Each invocation receives a fresh restricted Lua state. Generic scripts have deterministic base, table, string, and math operations, but no io, os, package, require, file loading, dynamic loading, or legacy Nauthilus modules. Host deadlines and cancellation stop execution, and globals cannot leak into the next request.

policy.facts.collect receives detached, redacted target, caller, and admitted facts views. It returns either a bounded fact list or one closed error_class (invalid_input, unavailable, timeout, or internal). Local names are host-qualified as lua.<module>.<name> and must match produced_facts, the target schema, source lua, type, and bounds. Unknown fields, duplicate facts, foreign authorities, or invalid values are contract violations and fail closed.

policy.effects.execute is called only for a policy-selected obligation whose typed parameters already passed the effect schema. Its closed results are succeeded, failed, and outcome_unknown; failed and unknown outcomes require an error class. Lua cannot select a decision, change plan ordering, authorize a target, mutate a response, request a retry, or detach its own work. The host owns synchronous finalization and accepted host_post_action scheduling.

policy:
namespaces:
dkim2:
providers:
risk:
kind: lua
module: reputation
script_path: /etc/nauthilus/lua/reputation.lua
targets: [{action: sign-message}]
produced_facts: [lua.reputation.risk_score]
executions: [host_sync]
failure: indeterminate
timeout: 100ms

Every value uses the same strict Policy representation: strings and booleans are native, signed integers are base-10 strings, doubles must be finite, byte values use standard Base64, timestamps use UTC RFC 3339, and string lists are dense and ordered. Empty lists and missing values remain distinct. Records are schema-defined ordered field lists; they cannot nest records recursively. See Generic Policy API for the wire form and Policy configuration for plans, schemas, failure handling, and effects.