Skip to main content
Version: Next

Auth Policy Reference

auth.policy is the declarative decision layer for Nauthilus authentication. It decides from typed facts, not from ad-hoc mechanism flags.

The policy layer is the only target scheduler for auth controls, backend facts, Lua environment sources, Lua subject sources, identity lookup, and account listing. Lua and built-in mechanisms still produce facts, but auth.policy.checks decides which facts are part of a request plan and auth.policy.policies decides how those facts become a final effect.

Placement and Authority

Policy configuration lives only under:

auth:
policy:

There is no separate policy root. The supported configuration surface is auth.policy. The built-in default policy set is named standard_auth. When no custom policy rules are configured, standard_auth preserves the default Nauthilus authentication behavior through the same policy engine.

The presence of an auth.policy block does not by itself make a custom policy authoritative. A custom policy takes production authority only when the compiled request plan contains at least one matching rule from auth.policy.policies for the current operation and stage. The decision boundary is therefore the policies list, not the mere existence of auth.policy, checks, sets, registry_scripts, or default_policy.

Lua environment and subject sources define script entries. Their execution operation, auth-state guard, and start order are defined by auth.policy.checks.

checks and policies have separate authority boundaries. You may configure checks only to control fact collection, Lua script operation scope, auth-state guards, and start order while standard_auth remains the decision authority. A stage and operation become custom-authoritative only when matching rules exist in auth.policy.policies.

standard_auth is not automatically merged into a custom rule list for the same operation and stage. In mode: enforce, a matching custom stage plan owns that stage. Stages without matching custom rules still use standard_auth. For example, a custom auth_decision rule can own the final password-auth decision while pre_auth still uses standard_auth for brute-force, TLS, relay-domain, RBL, and Lua environment behavior.

If both default_policy: standard_auth and custom policies are configured, that is valid and expected. default_policy names the built-in fallback/default set; policies declares the custom rules that may override it for their own operation and stage. The key name is default_policy; misspellings such as default_poicy are rejected as unsupported configuration keys.

Root Shape

auth:
policy:
mode: enforce
default_policy: standard_auth
registry_scripts: []
attribute_exports: []
request_headers: []
request_metadata: []

attribute_sources:
lua:
environment: []
subject: []

obligation_targets:
lua:
actions: []

sets:
networks: {}
time_windows: {}

scheduler_guards: {}

report:
enabled: false
include_fsm: true
include_checks: true
include_attributes: false

checks: []
policies: []
KeyTypeDefaultPurpose
modestringenforceenforce applies selected policies; observe compares custom policy output against standard_auth without changing production output.
default_policystringstandard_authBuilt-in default policy set. This is currently the only built-in default-policy name.
registry_scriptslist of paths[]Lua scripts that register additional policy attributes during snapshot build.
attribute_exportslist[]Opt-in backend/AuthState attributes that become policy-visible subject facts.
request_headerslist[]Explicit allowlist for non-standard HTTP request headers that become normalized policy facts.
request_metadatalist[]Explicit allowlist for gRPC metadata keys that become normalized policy facts.
attribute_sources.lua.environmentlist[]Lua environment sources that run in pre_auth and can emit environment facts before the subject identity is known.
attribute_sources.lua.subjectlist[]Lua subject sources that run in subject_analysis and can evaluate or enrich subject facts after backend identity material exists.
obligation_targets.lua.actionslist[]Reusable Lua action scripts selected through policy obligations.
sets.networksmap{}Named reusable IP/CIDR sets for policy conditions.
sets.time_windowsmap{}Named local-time windows for policy conditions.
scheduler_guardsmap{}Named opt-in scheduler conditions that may skip selected checks before their adapters run.
report.enabledboolfalseEnables optional redacted policy decision reports. This does not affect enforcement, logs, metrics, traces, or client responses.
report.include_fsmbooltrueIncludes FSM decision material in report output. Selected FSM markers are still used internally even when reports are disabled.
report.include_checksbooltrueIncludes check results in report output. Check results still drive policy evaluation when reports are disabled.
report.include_attributesboolfalseIncludes emitted attributes in report output when enabled. Redaction still applies.
checkslist[]Explicit fact-producing check plan.
policieslist[]Ordered first-match decision rules.

Policy snapshots are built at startup and on reload. A candidate snapshot is activated only after complete validation succeeds. If a reload fails, the previous active snapshot remains in use.

Modes

ModeBehavior
enforceCustom configured policies may become authoritative for supported stages and operations. If no custom rules exist, standard_auth is authoritative.
observestandard_auth remains authoritative. Configured policies are evaluated for diagnostics, reports, logs, metrics, and traces only. Custom side effects do not execute.

Observe mode is for comparing behavior. It does not change config validation.

Checks-Only Scheduling

You do not need to write a full custom policy rule set just to control Lua execution. This is valid:

auth:
policy:
mode: enforce
default_policy: standard_auth

attribute_sources:
lua:
environment:
- name: geoip
script_path: /etc/nauthilus/lua/environment/geoip.lua
- name: policy_gate
script_path: /etc/nauthilus/lua/environment/policy_gate.lua

checks:
- name: lua_environment_geoip
type: lua.environment
stage: pre_auth
operations: [authenticate, lookup_identity]
config_ref: auth.policy.attribute_sources.lua.environment.geoip

- name: lua_environment_policy_gate
type: lua.environment
stage: pre_auth
operations: [authenticate, lookup_identity]
after: [lua_environment_geoip]
config_ref: auth.policy.attribute_sources.lua.environment.policy_gate

In this shape, the check plan decides which Lua environment sources run and in which order. standard_auth still decides the final result from the emitted facts. Add auth.policy.policies only when you want custom decision rules.

For a Lua script family, configured checks own that family's schedule for the active operation and stage. Scripts without a matching check do not run in that request plan. If no checks exist for that script family, Nauthilus uses the built-in default scheduling for that family.

Backend Attribute Exports

Backends can return arbitrary account attributes. Nauthilus does not expose all of them to policy automatically. This is intentional: LDAP and Lua backend attributes often contain internal fields, tokens, mailbox routing hints, or other values that should not become policy/report material by accident.

Use auth.policy.attribute_exports to make selected backend/AuthState attributes available as policy subject facts:

auth:
policy:
attribute_exports:
- name: account_status
attribute: accountStatus
type: string
sensitivity: internal

- name: entitlements
attribute: entitlements
type: string_list

- name: risk_score
attribute: riskScore
type: number
FieldRequiredPurpose
nameyesPolicy-safe export name. It becomes the final path segment in auth.subject.attribute.<name>.
attributeyesBackend/AuthState attribute name to read from the backend result.
typeyesDetail type: bool, string, string_list, or number.
sensitivitynoReport redaction class: internal default, public, or secret.

The generated policy attribute is a boolean presence fact:

if:
attribute: auth.subject.attribute.account_status
is: true

The configured value is available as a typed detail:

if:
attribute: auth.subject.attribute.account_status
detail: value
eq: locked

For string_list exports, use the values detail:

if:
attribute: auth.subject.attribute.entitlements
detail: values
contains: imap

Generated details are:

DetailTypeMeaning
attributestringOriginal backend/AuthState attribute name.
countnumberNumber of values present in the backend attribute.
valuebool, string, or numberTyped first value for scalar exports.
valuesstring_listTyped string list for string_list exports.

name is normalized like generated bucket and RBL list identifiers: letters and digits are kept, separators collapse to _, the segment is lower-cased, and leading digits are prefixed with b_. If two exports normalize to the same segment, the policy snapshot is rejected.

Operations

operations structurally selects which request operation a check or policy belongs to. If omitted on a check or policy, it defaults to:

operations: [authenticate]

An explicitly empty list is invalid.

OperationMeaning
authenticateNormal password authentication.
lookup_identityIdentity lookup without password verification, used by HTTP no-auth and gRPC LookupIdentity.
list_accountsAccount-provider listing, used by HTTP list-accounts and gRPC ListAccounts.

Caller authentication, backchannel credentials, gRPC scopes, malformed requests, and transport errors are prerequisites. They are not normal policy denials.

Policy Flow

The runtime path is operation-specific. pre_auth and auth_decision are the stages where configured policies can select terminal outcomes in enforce mode. auth_backend, subject_analysis, and account_provider collect facts that later rules can use.

Read this diagram top-down:

neutral in pre_auth means "continue", not "success". permit is valid only at auth_decision. A terminal pre_auth result stops before backend evaluation; a terminal auth_decision result selects the response class, FSM marker, obligations, and advice for the active operation.

Stages

StagePurpose
pre_authBrute force, Lua environment sources, TLS enforcement, relay domains, and RBL before backend auth.
auth_backendBackend authentication or identity lookup facts.
subject_analysisLua subject source facts after backend evaluation.
account_providerAccount-list provider facts for list_accounts.
auth_decisionFinal permit, deny, or temporary failure decision.

There is no post_decision policy stage. Lua post-actions are asynchronous enforcement work requested through obligations after a decision is selected.

Checks

auth.policy.checks defines the explicit fact-producing plan.

auth:
policy:
checks:
- name: tls_encryption
type: builtin.tls_encryption
stage: pre_auth
operations: [authenticate, lookup_identity]
config_ref: auth.controls.tls_encryption
output: checks.tls_encryption
FieldRequiredPurpose
nameyesUnique check name used by after and require_checks.
typeyesCheck type from the registry.
stageyesStage where the check emits facts. Must match the check type.
operationsnoOperation scope. Omitted means [authenticate].
run_if.auth_statenoStructural scheduler guard: any, authenticated, or unauthenticated. Omitted means any.
skip_ifnoNamed scheduler guards from auth.policy.scheduler_guards. If any referenced guard matches, the selected check is recorded as skipped and its adapter is not called.
afternoCheck-plan ordering dependencies inside the same operation/stage plan. Dependencies must cover the dependent check's operations and auth-state scheduler guard.
config_refnoCanonical mechanism config path used by the check.
outputnoUnique output name for reports and internal plan identity.
observe_safenoAllows observe execution only for check types that permit operator assertion.

Check-Type Registry

TypeStageDefault operationsConfig reference
builtin.brute_forcepre_authauthenticateauth.controls.brute_force
builtin.tls_encryptionpre_authauthenticate, lookup_identityauth.controls.tls_encryption
builtin.relay_domainspre_authauthenticateauth.controls.relay_domains
builtin.rblpre_authauthenticate, lookup_identityauth.controls.rbl
lua.environmentpre_authauthenticateauth.policy.attribute_sources.lua.environment.<name>
backend.ldapauth_backendauthenticate, lookup_identityauth.backends.ldap
backend.luaauth_backendauthenticate, lookup_identityauth.backends.lua.backend
lua.subjectsubject_analysisauthenticateauth.policy.attribute_sources.lua.subject.<name>
backend.account_provideraccount_providerlist_accountsauth.backends

Lua environment and subject sources are singular check types. Use one check per named script. Aggregate check types such as lua.environments or lua.subjects are invalid.

Design Lineage

Nauthilus policy design is inspired by OASIS XACML 3.0 concepts, but it is not an XACML implementation and does not expose XACML XML/JSON request or policy syntax.

The borrowed concepts are:

  • a policy-decision layer separated from enforcement code
  • policy enforcement points that collect facts and apply the selected decision
  • typed subject, resource, action, environment, and system attributes
  • explicit effects such as permit, deny, neutral, and temporary failure
  • obligations and advice attached to selected decisions
  • ordered rule evaluation with deterministic first-match behavior

The Nauthilus-specific parts are the YAML configuration surface, fixed auth operations and stages, built-in check registry, Lua attribute registry, FSM markers, response markers, and the built-in standard_auth policy set.

For administrators, the practical takeaway is that Nauthilus uses an XACML-like PDP/PEP split: mechanisms and Lua code produce facts, the policy decision point selects an effect, and enforcement bridges apply the response, FSM marker, obligations, and advice.

Attribute Categories

Every registered policy attribute has a category. The category is metadata for the policy registry, report readers, and future tooling. It does not change how an if condition is written: policy conditions still reference the full attribute ID.

Nauthilus currently uses these categories:

CategoryMeaning in Nauthilus policiesExamples
environmentRequest context or external environment around the login attempt. This is where pre-auth controls usually emit facts.request.client.ip, auth.tls.secure, auth.brute_force.triggered, auth.rbl.score, auth.relay_domain.rejected
subjectFacts about the authenticating identity or account after backend lookup/authentication.auth.authenticated, auth.identity.found, configured auth.subject.attribute.<name> exports, Lua billing/account facts
resourceFacts about a requested resource or a resource-producing operation.auth.account_provider.completed for account-list responses
actionFacts about the requested action. Nauthilus currently models the main action through request.operation instead of many separate action attributes.
systemImplementation or system-level facts. This is reserved for future registry/tooling use; built-in policy decisions currently do not require user-authored system attributes.

Two names often look abstract at first:

  • subject means "the user/account side of the request". Backend attributes are not exported automatically because they may contain secrets or directory-internal values. Use auth.policy.attribute_exports when a backend field should become policy material.
  • environment means "facts around the request". This includes network, time, TLS, RBL, relay-domain, brute-force, and Lua risk signals. It does not mean operating-system environment variables.

Request Attributes

Policies do not read the Lua request table or Go request structs directly. They can only use registered policy attributes. Some attributes happen to describe the current request and use the request.* prefix, but they are a stable policy surface, not a 1:1 copy of fields available to Lua environment sources, subject sources, actions, or backends.

Built-in request attributes are:

AttributeTypeOperationsMeaning
request.operationstringallActive operation: authenticate, lookup_identity, or list_accounts.
request.time.nowdatetimeallRequest evaluation timestamp.
request.client.ipipallEffective client IP after Nauthilus request-header/proxy handling.
request.client.ip.presentboolallTrue when the effective client IP parsed successfully.
request.client.ip.trustedboolallTrue when the selected source is trusted for scheduler decisions.
request.client.ip.sourcestringallSource label such as direct_peer, proxy_protocol, trusted_proxy_header, grpc_peer, metadata, or unknown.
request.protocolstringallEffective authentication protocol, such as imap, smtp, submission, http, or IdP-related protocol names.
request.transport.kindstringallTransport family such as HTTP, gRPC, mail protocol, IdP, hook, internal, or unknown.
request.listener.namestringallConfigured listener identity when available.
request.connection.tlsboolallAlready-known transport TLS state.
request.initiator.kindstringallServer-derived initiator class such as external user traffic, backend health check, internal service, or unknown.
request.http.routestringall when availableNormalized server route, not the raw path or query string.
request.grpc.methodstringall when availablegRPC service method.
request.idp.client_idstringall when availableParsed OIDC client identifier; do not trust it alone for scheduler skips.
request.saml.sp_entity_idstringall when availableParsed SAML service-provider entity ID; do not trust it alone for scheduler skips.

Use the policy attribute ID, not Lua field names. For example, Lua commonly uses request.client_ip, but YAML policies use request.client.ip:

if:
attribute: request.client.ip
cidr_contains: "@network.trusted_clients"

Hostname-style request fields such as the Lua request.client_host value are not built-in policy attributes today. If an environment source, subject source, or backend needs such a value in policy decisions, expose it deliberately: register a Lua-owned policy attribute with auth.policy.registry_scripts and emit it with the Lua policy module, or export a selected backend result field with auth.policy.attribute_exports.

Non-standard HTTP request headers and gRPC metadata are not exposed automatically. Use request_headers and request_metadata when a trusted proxy or client supplies a bounded value that policy may read:

auth:
policy:
request_headers:
- header: X-Company-Language
attribute: request.header.company_language
visibility: public
normalize:
trim: true
case: lower
max_length: 16

request_metadata:
- key: x-company-language
attribute: request.metadata.company_language
visibility: public
normalize:
trim: true
case: lower
max_length: 16

Rules:

  • HTTP header names are matched case-insensitively and are stored under request.header.* attribute IDs.
  • gRPC metadata keys must be lowercase and are stored under request.metadata.* attribute IDs.
  • Attribute IDs must be unique across both allowlists.
  • Credential and session carriers such as Authorization, Proxy-Authorization, Cookie, and Set-Cookie are rejected.
  • Allowed values are single string facts. trim, case: lower, case: upper, and max_length define normalization before the value reaches the policy context.
  • visibility currently accepts public; keep values short and low-cardinality because public request facts can appear in diagnostics when reports include attributes.

Example policy using an allowlisted header as a response-language selector:

auth:
policy:
request_headers:
- header: X-Company-Language
attribute: request.header.company_language
visibility: public
normalize:
trim: true
case: lower
max_length: 16

policies:
- name: deny_account_locked_with_company_language
stage: auth_decision
operations: [authenticate]
if:
attribute: auth.subject.attribute.account_status
detail: value
eq: locked
then:
decision: deny
reason: account_locked
response_marker: auth.response.fail
response_language:
from: attribute
attribute: request.header.company_language
fallback: en
response_message:
from: i18n
i18n_key: auth.policy.company.account_locked
fallback: "Login failed because the account is locked."

Lua Check Scheduling

GoalPolicy expression
Run a check for normal password authenticationOmit operations or set operations: [authenticate].
Run a check for identity lookupAdd lookup_identity to operations.
Run a check only after backend authentication succeededUse run_if.auth_state: authenticated.
Run a check only before or after failed authenticationUse run_if.auth_state: unauthenticated.
Run a check regardless of authentication stateOmit run_if or set run_if.auth_state: any.
Start one check after another checkAdd after: [check_name] on the dependent check.
Make a policy rule depend on a check resultAdd the check name to require_checks.
Skip a selected check for a guarded operational caseAdd skip_if: [guard_name] on the check and define guard_name under auth.policy.scheduler_guards.

Example:

auth:
policy:
attribute_sources:
lua:
environment:
- name: geoip
script_path: /etc/nauthilus/lua/environment/geoip.lua
- name: policy_gate
script_path: /etc/nauthilus/lua/environment/policy_gate.lua

checks:
- name: lua_environment_geoip
type: lua.environment
stage: pre_auth
operations: [authenticate, lookup_identity]
config_ref: auth.policy.attribute_sources.lua.environment.geoip
output: checks.lua_environment_geoip

- name: lua_environment_policy_gate
type: lua.environment
stage: pre_auth
operations: [authenticate, lookup_identity]
after: [lua_environment_geoip]
config_ref: auth.policy.attribute_sources.lua.environment.policy_gate
output: checks.lua_environment_policy_gate

Scheduler Guards

Scheduler guards are opt-in, need-based check-scheduler conditions. They decide whether a selected check adapter runs. They are not final authorization decisions, they do not grant authentication success, and they do not replace auth.policy.policies.

Use scheduler guards only when reducing check coverage is operationally intentional, for example avoiding RBL DNS work for a trusted health probe source, skipping a specific pre-auth check during a documented maintenance window, or narrowing service-to-service traffic that is already constrained by listener or caller authentication. If a request must be permitted or denied, write an ordered policy rule. If a check should merely not run in a narrow case, use skip_if.

Scheduler guards live under auth.policy.scheduler_guards and are referenced by check-level skip_if:

auth:
policy:
sets:
networks:
pre_auth_exempt_sources:
- 127.0.0.0/8
- ::1

scheduler_guards:
pre_auth_exempt_source:
on_missing_attribute: run
if:
all:
- attribute: request.client.ip.present
is: true
- attribute: request.client.ip.trusted
is: true
- attribute: request.client.ip
cidr_contains: "@network.pre_auth_exempt_sources"

checks:
- name: rbl
type: builtin.rbl
stage: pre_auth
operations: [authenticate, lookup_identity]
skip_if: [pre_auth_exempt_source]
config_ref: auth.controls.rbl
output: checks.rbl

Scheduler Guard Fields

scheduler_guards is a map. The map key is the guard name referenced by checks[*].skip_if.

FieldRequiredTypeDefaultPurpose
map keyyessimple identifiernoneStable guard name. Use names such as monitoring_pre_auth_source; avoid vague names such as bypass.
ifyescondition treenoneRequest-only condition evaluated before the selected check adapter starts. It must contain exactly one expression node: attribute, all, any, not, or always: true.
on_missing_attributenostringrunMissing-attribute behavior. The only supported value is run, which means the guarded check still runs when any attribute referenced by the guard is missing.

on_missing_attribute: run is fail closed. A missing client IP, an unparsable client IP, a missing detail, or an untrusted header-derived IP must not suppress a security check. Because run is also the default, omitting on_missing_attribute has the same runtime effect. The explicit form is recommended for operational exemptions because it documents the intended safety behavior.

Scheduler guard conditions use the same YAML condition tree shape as policy rules, but with a narrower authority:

Guard condition itemSupported in scheduler guardsNotes
attributeyesMust reference a request.* attribute. Check-produced, Lua-produced, backend, and subject attributes are rejected.
all, any, notyesRecursive grouping. If any referenced attribute anywhere in the guard tree is missing, on_missing_attribute: run makes the check run.
always: trueyesValid but broad; use only when a check should always be skipped wherever the guard is attached.
existsyesPresence test for any request attribute type.
is, eq, ne, in, not_inyesBoolean or string request attributes only.
cidr_containsyesIP or CIDR request attributes, usually request.client.ip with an @network.* set.
within_time_windowyesDatetime request attributes, usually request.time.now with an @time_window.* set.
matches, contains, contains_any, contains_all, contains_none, gt, gte, lt, ltenoThese rule-condition operators are not accepted for scheduler guards.

Client-controlled request values are not trusted scheduler facts by themselves. A guard that references request.header.*, request.metadata.*, request.idp.client_id, or request.saml.sp_entity_id must combine that value with a server-derived criterion, for example trusted client IP, listener identity, transport kind, or TLS state.

Guard Evaluation

Scheduler guards are evaluated after structural selection and before the check adapter is called:

  1. operations, run_if.auth_state, and after build the active check plan.
  2. For each selected check, Nauthilus evaluates that check's skip_if guards.
  3. Multiple guard names in skip_if are OR-combined.
  4. If any guard matches, the check adapter is not called.
  5. The policy report records the check as status: "skipped" with a reason such as scheduler_guard:pre_auth_exempt_source.

Skipped checks are not technical adapter errors. Reports and metrics distinguish scheduler-guard skips from operation or run_if skips.

after dependencies stay deterministic. If check B declares after: [A] and A can be skipped by guard_x, B must also include guard_x in skip_if; otherwise the configuration is rejected.

require_checks with Skipped Checks

require_checks is a policy applicability contract, not a scheduler. A skipped check does not satisfy require_checks.

The runtime semantics are:

  • only check results with status ok or error satisfy require_checks;
  • a missing or skipped required check makes that policy rule non-applicable;
  • non-applicable is not the same as a false condition;
  • later rules in the same stage may still match.

This allows a source-exempt request to skip rbl, make a rule such as require_checks: [rbl] non-applicable, and still reach a later rule that does not require rbl.

Safe Request Surface

Scheduler guards should use conservative, request-local attributes that are available before the check adapter starts. Prefer server-derived facts over values supplied by a client.

AttributeTypeTrust modelGood scheduler-guard use
request.operationstringserver-derivedLimit a guard to authenticate, lookup_identity, or list_accounts.
request.protocolstringserver-derived or normalized by the protocol adapterScope a guard to a protocol family.
request.time.nowdatetimecaptured once per request by NauthilusTime-window guards.
request.client.ipipselected client source after parsingCIDR/network-set guards when combined with presence and trust facts.
request.client.ip.presentboolserver-derivedRequire a stable parsed client IP before matching.
request.client.ip.trustedboolserver-derivedPrevent untrusted headers or metadata from skipping checks.
request.client.ip.sourcestringserver-derivedExplain where the IP came from, such as direct_peer, proxy_protocol, trusted_proxy_header, grpc_peer, metadata, or unknown.
request.transport.kindstringserver-derivedDistinguish HTTP, gRPC, mail protocol, IdP, hook, or internal execution.
request.listener.namestringconfigured listener identityPrefer listener identity over IP when deployments have separate internal and external listeners.
request.connection.tlsbooltransport-derivedDepend on already-known transport security, not on a check result.
request.initiator.kindstringserver-derivedDistinguish external user traffic, backend health checks, internal service calls, and unknown callers.
request.http.routestringnormalized server routeScope HTTP traffic without using raw path or query input.
request.grpc.methodstringgRPC transport-derivedScope gRPC service methods.
request.idp.client_idstringparsed request value, not trusted aloneOptional IdP/OIDC scoping when combined with trusted transport or source facts.
request.saml.sp_entity_idstringparsed request value, not trusted aloneOptional SAML scoping when combined with trusted transport or source facts.

Do not use these values as scheduler-guard inputs:

  • password, token, OTP, recovery code, or other credential material;
  • arbitrary raw HTTP headers, raw gRPC metadata, raw paths, raw queries, cookies, User-Agent, or language headers;
  • username or account as a standalone bypass criterion;
  • check-produced attributes;
  • Lua-produced attributes.

Allowlisted request headers and gRPC metadata may be normal policy attributes, but they are not trusted scheduler facts by default. A guard that uses a client-controlled value must combine it with trusted server-derived facts, and deployments should prefer listener, transport, and source trust attributes where available.

Client IP Trust Model

request.client.ip is useful only after Nauthilus has parsed it and assigned trust metadata:

  • direct peer addresses may be trusted only when they come from the actual transport peer;
  • proxy-header addresses may be trusted only through configured trusted proxy handling;
  • gRPC or request metadata is untrusted unless the transport and caller identity make it trustworthy;
  • empty IP is not loopback;
  • loopback is not universally safe and should not be used as a blanket authorization signal.

A network-set guard should therefore normally require all three client-IP facts:

if:
all:
- attribute: request.client.ip.present
is: true
- attribute: request.client.ip.trusted
is: true
- attribute: request.client.ip
cidr_contains: "@network.pre_auth_exempt_sources"

When any of these facts is missing or false, the guard does not match and the protected check runs.

Time-Window Guards

Use request.time.now with a named @time_window.* set. request.time.now is captured once for the request, so all guards and policies see the same timestamp.

auth:
policy:
sets:
time_windows:
pre_auth_exempt_windows:
timezone: Europe/Berlin
days: [sunday]
intervals:
- start: "02:00"
end: "04:00"

scheduler_guards:
pre_auth_exempt_window:
on_missing_attribute: run
if:
attribute: request.time.now
within_time_window: "@time_window.pre_auth_exempt_windows"

checks:
- name: rbl
type: builtin.rbl
stage: pre_auth
operations: [authenticate]
skip_if: [pre_auth_exempt_window]
config_ref: auth.controls.rbl
output: checks.rbl

This skips only the attached check during the named window. It does not permit the request and does not skip final policy evaluation.

Network-Set Guards

Use request.client.ip with @network.* only after proving the IP is present and trusted:

auth:
policy:
sets:
networks:
monitoring_sources:
- 192.0.2.10/32

scheduler_guards:
monitoring_pre_auth_source:
on_missing_attribute: run
if:
all:
- attribute: request.client.ip.present
is: true
- attribute: request.client.ip.trusted
is: true
- attribute: request.client.ip
cidr_contains: "@network.monitoring_sources"

checks:
- name: rbl
type: builtin.rbl
stage: pre_auth
operations: [authenticate, lookup_identity]
skip_if: [monitoring_pre_auth_source]
config_ref: auth.controls.rbl
output: checks.rbl

Use documentation and deployment comments to explain why the guard exists. A purpose name such as monitoring_pre_auth_source is better than a mechanism name such as loopback_skip.

Policies

auth.policy.policies is an ordered first-match rule list.

auth:
policy:
policies:
- name: deny_no_tls
stage: pre_auth
operations: [authenticate, lookup_identity]
require_checks: [tls_encryption]
if:
attribute: auth.tls.secure
is: false
then:
decision: tempfail
reason: no_tls
response_marker: auth.response.tempfail.no_tls
FieldRequiredPurpose
nameyesStable rule name for reports, logs, and metrics.
stageyesStage where the rule is evaluated.
operationsnoOperation scope. Omitted means [authenticate].
require_checksnoChecks that must have produced ok or error results for this rule to be applicable.
ifyesStructured condition tree.
thenyesDecision and optional enforcement outputs.

Rules are evaluated in YAML order within the active operation/stage plan. A matching terminal decision stops evaluation for that stage.

neutral is not permit. A neutral pre-auth result allows the request to continue. Final auth decisions are deny-biased: if no auth_decision rule permits, the operation is denied.

When a rule lists require_checks, each named check must have produced an ok or error result for the rule to be applicable. A check skipped by a scheduler guard does not satisfy require_checks; the rule is skipped as non-applicable and later rules in the same stage may still match.

Conditions

Conditions are YAML objects. Free-form expression strings are not supported.

Condition Nodes

NodeShape
Attribute comparisonattribute, optional detail, exactly one operator
alllist of child condition objects; all must match
anylist of child condition objects; at least one must match
notone child condition object
alwaysalways: true

Condition trees are recursive. A child below all, any, or not may be an attribute comparison, always, or another all, any, or not node. Each condition object must still contain exactly one expression node: one of attribute, all, any, not, or always.

Attribute comparisons are leaf nodes. They must contain exactly one operator such as is, eq, ne, gte, contains, or within_time_window. Operators are not nested, and two operators cannot be placed on the same attribute comparison. To combine multiple comparisons, put them into all, any, or not.

The then block is not recursive. A policy rule has one if tree and one then output block. For else-style behavior or different outcomes, write separate ordered policies.

Examples:

if:
attribute: auth.brute_force.triggered
is: true
if:
all:
- attribute: auth.relay_domain.present
is: true
- attribute: auth.relay_domain.known
is: false
if:
not:
attribute: request.time.now
within_time_window: "@time_window.business_hours"
if:
all:
- any:
- attribute: auth.rbl.threshold_reached
is: true
- attribute: auth.brute_force.triggered
is: true
- not:
any:
- attribute: auth.rbl.soft_allowlisted
is: true
- attribute: auth.relay_domain.soft_allowlisted
is: true

Operators

OperatorValid forPurpose
isbool and exact scalar checksShort exact comparison.
eqscalar or exact listExact equality.
nescalar or exact listExact inequality.
inscalar attributeAttribute value is in the configured list.
not_inscalar attributeAttribute value is not in the configured list.
matchesstringGo RE2 regular expression match.
existsany attribute/detailPresence test with boolean operand.
containsstring-list attributeList contains one string.
contains_anystring-list attributeList contains at least one configured string.
contains_allstring-list attributeList contains all configured strings.
contains_nonestring-list attributeList contains none of the configured strings.
gt, gte, lt, ltenumber or datetimeComparable comparisons.
cidr_containsIP or CIDRCIDR/IP containment or network-set match.
within_time_windowdatetimeTime-window-set membership.

Missing attributes are not equal to false, an empty string, zero, or an empty list. Use exists when presence is part of the decision.

Sets

Network and time-window sets are reusable operands for conditions.

auth:
policy:
sets:
networks:
trusted_clients:
- 10.0.0.0/8
- 192.168.0.0/16
- 2001:db8::/32

time_windows:
business_hours:
timezone: Europe/Berlin
days: [mon, tue, wed, thu, fri]
intervals:
- start: "08:00"
end: "18:00"

Use them from conditions:

if:
attribute: request.client.ip
cidr_contains: "@network.trusted_clients"
if:
attribute: request.time.now
within_time_window: "@time_window.business_hours"

Set names must use lowercase letters, digits, and underscores. Time-window intervals use HH:MM and must not cross midnight; split cross-midnight windows into two intervals.

Decision Outputs

The then block is the consequence block of a policy rule. The if tree decides whether the rule matches; then says what Nauthilus should do with that match.

then is not another condition tree and it does not contain else branches. For different outcomes, write multiple ordered policies. The first matching terminal rule for a stage wins according to the policy combining rules.

The then block always needs decision.

then:
decision: deny
reason: billing_locked
response_marker: auth.response.fail

At snapshot-build time, Nauthilus compiles then into a typed decision plan:

  • decision becomes the transport-independent effect.
  • fsm_event_marker and response_marker are derived from stage and decision when the normal mapping is unambiguous.
  • response_message, obligations, and advice are validated against the registries.
  • invalid stage/effect combinations are rejected, for example decision: permit in pre_auth.
FieldPurpose
decisionRequired effect: neutral, deny, permit, or tempfail. permit is not allowed in pre_auth.
reasonOptional internal reason for logs, reports, metrics, obligations, advice, and operator diagnosis. It is not a client-visible message by itself.
outcome_markerOptional stable outcome label for reports and tooling. Built-in standard_auth uses stable outcome markers; custom policies may set their own.
fsm_event_markerOptional target FSM event marker. If omitted, Nauthilus derives the normal marker from stage and decision.
response_markerOptional response class. If omitted, Nauthilus derives the normal class from the decision when possible.
response_messageOptional final client-visible message selection inside the selected response class.
obligationsMandatory registered enforcement work to execute with the selected decision.
adviceNon-binding registered context that may be used for reporting, logging, or follow-up context.
control.skip_remaining_stage_checksStage-local control for neutral pre_auth decisions that should stop later checks without denying the request.

Decision Effects

decision is the central then output.

Decisionpre_auth behaviorauth_decision behavior
neutralContinue the request unless control.skip_remaining_stage_checks stops later checks in the stage.Non-terminal. Evaluation continues; final enforcement denies if no later rule permits.
denyStop before backend evaluation and fail the operation.Fail the active operation.
permitInvalid. Pre-auth cannot grant final success.Permit the active operation.
tempfailStop before backend evaluation with a temporary failure.Temporary failure for the active operation.

neutral is deliberately not permit. It means "this rule did not choose a terminal security result".

Reason and Outcome Marker

Use reason for stable internal diagnosis:

then:
decision: deny
reason: relay_domain_rejected

reason can appear in logs, reports, traces, counters, and POST-Action context. It must not contain secrets or user-specific free text. It does not override the client-visible response.

Use outcome_marker when you need a stable, namespaced outcome label for tooling or reports:

then:
decision: deny
reason: brute_force_reject
outcome_marker: auth.outcome.brute_force_reject

If you do not have a reporting/tooling need for a custom marker, leave outcome_marker unset.

Response Marker and Message

response_marker selects the transport-independent response class. It is validated against the selected decision; for example auth.response.ok is compatible with permit, not with deny.

If omitted, Nauthilus derives the normal response marker:

DecisionDerived response marker
permitauth.response.ok
denyauth.response.fail
tempfailauth.response.tempfail
neutralnone

Use an explicit marker for specialized classes:

then:
decision: tempfail
reason: no_tls
response_marker: auth.response.tempfail.no_tls

response_message can override only the message inside the selected response class. It does not change HTTP status codes, gRPC status codes, redirect behavior, OIDC/SAML semantics, or the FSM state.

Supported message sources:

SourceRequired fieldsMeaning
omitted or from: defaultnoneUse the default message of response_marker.
from: literaltextUse a static policy-configured message.
from: attribute_detailattribute, detail, optional fallbackUse a public string detail from a registered policy attribute.
from: i18ni18n_key, fallbackUse a stable localization key and keep the fallback text for missing translations or non-localized response boundaries.

Literal message:

then:
decision: deny
response_marker: auth.response.fail
response_message:
from: literal
text: "Account temporarily locked"

Lua-provided public detail:

then:
decision: deny
response_marker: auth.response.fail
response_message:
from: attribute_detail
attribute: auth.lua.subject.billing_lock.rejected
detail: status_message
fallback: "Invalid login or password"

For attribute_detail, the referenced detail must be a registered string detail with sensitivity: public and purpose: response_message. If the detail is absent or empty at runtime, Nauthilus uses fallback; if no fallback is configured, it uses the response-marker default.

Localized message:

then:
decision: deny
response_marker: auth.response.fail
response_message:
from: i18n
i18n_key: auth.policy.company.account_locked
fallback: "Login failed because the account is locked."

For from: i18n, i18n_key is a stable catalog key. fallback is the safe text returned when no matching catalog entry exists or when the response surface cannot localize. text, attribute, and detail are not valid with from: i18n. Existing from: default, from: literal, and from: attribute_detail behavior is unchanged.

response_language is optional response-rendering metadata. It never changes whether a policy permits, denies, or tempfails the request.

SourceRequired fieldsMeaning
from: literallanguageUse a configured BCP 47 language tag such as de or en-US.
from: attributeattribute, optional fallbackRead a BCP 47 language tag from a policy attribute, falling back to the configured fallback tag when the attribute is absent or invalid.

Literal language:

then:
decision: deny
response_marker: auth.response.fail
response_language:
from: literal
language: de
response_message:
from: i18n
i18n_key: auth.policy.company.account_locked
fallback: "Login failed because the account is locked."

Attribute-driven language:

then:
decision: deny
response_marker: auth.response.fail
response_language:
from: attribute
attribute: lua.company.preferred_language
fallback: en
response_message:
from: i18n
i18n_key: auth.policy.company.account_locked
fallback: "Login failed because the account is locked."

Language selection happens at the response boundary:

  • IdP browser responses use explicit UI language from URL or cookie first, then policy-selected response_language, then Accept-Language, then the configured default language.
  • HTTP auth responses use policy-selected response_language, then Accept-Language, then the configured default language.
  • gRPC auth responses use policy-selected response_language, then incoming metadata accept-language, then the resolver default.
  • HTTP responses set Content-Language and gRPC responses set content-language metadata when localization selected a language.
  • Missing translations return the configured fallback text and keep the selected policy decision unchanged.

Deployment Translation Catalogs

Nauthilus ships system-owned resource bundles for product messages. Deployment-owned policy keys such as auth.policy.company.* should live in deployment catalogs, not in the server repository's server/resources/*.json files.

Example deployment catalog:

policy-en.json
{
"auth.policy.company.account_locked": "Login failed because the account is locked.",
"auth.policy.company.account_unpaid": "Login failed because open payments exist and the account is locked."
}

Deployment overlays are merged after the system catalog in deterministic order. They may add new keys and override system keys. Overrides are logged with the language, key, namespace, and override status. The effective catalog is frozen before request processing; request-time Lua cannot mutate it. On reload, Nauthilus builds the complete next effective catalog first and activates it atomically only after the reload succeeds. If reload fails, the previous effective catalog stays active.

Startup Lua can register deployment overlays with nauthilus_i18n.register_catalog(...). Request-time Lua can resolve messages for Lua-owned logs or notices with nauthilus_i18n.get_localized(...), but final auth responses should still use policy-selected response_message plus optional response_language so reports and transports share one decision path.

Testing and Mocking Boundaries

Policy localization should be tested at boundaries with fakes instead of production services:

BoundaryMock or fake
Policy compiler and evaluatorTable-driven config tests with fake policy attributes and fake catalogs.
Localization resolverFake MessageResolver or fake effective catalog covering selected language, missing key, fallback, and truncation.
HTTP auth renderinghttptest requests with explicit Accept-Language, allowlisted headers, and a fake resolver.
gRPC auth renderingIn-memory gRPC or handler tests with incoming metadata such as accept-language and a fake auth service outcome.
IdP renderingMocked auth outcomes that carry fallback text, optional i18n_key, and optional policy-selected response language.
Request attributesHeader and metadata fixtures that prove allowlisted values are normalized and non-allowlisted values are absent.
Lua policy emissionsHermetic Lua states or --test-lua fixtures that assert nauthilus_policy.emit_attribute(...) calls.
nauthilus_i18nFake resolver and fake startup catalog collector; no production resource files or backend authentication required.

Keep example translation keys such as auth.policy.company.* in tests or deployment-owned documentation examples. Do not add documentation-only keys to server/resources/*.json.

Obligations, Advice, and Control

obligations request registered enforcement work. They are mandatory for the selected decision and are not arbitrary Lua extension points.

Registered obligations:

IDWhat it does
auth.obligation.brute_force.updateUpdates brute-force counters, toleration, and learning state.
auth.obligation.lua_action.dispatchDispatches an existing configured synchronous Lua action selected by the policy decision.
auth.obligation.lua_post_action.enqueueEnqueues an existing Lua POST-Action after the request-time decision is known.

Example:

then:
decision: deny
obligations:
- id: auth.obligation.brute_force.update
- id: auth.obligation.lua_action.dispatch
args:
action: brute_force
- id: auth.obligation.lua_post_action.enqueue
args:
action: brute_force

advice is non-binding context. It may be used for reports, logging, or follow-up context, but failing or ignoring advice must not change the selected decision or response.

Registered advice:

IDWhat it does
auth.advice.audit_reasonCarries sanitized audit context.

Example:

then:
decision: deny
advice:
- id: auth.advice.audit_reason
args:
reason: blocked_country

control.skip_remaining_stage_checks is a narrow stage-local control. It is useful for neutral pre-auth rules that should stop later pre-auth checks without granting success:

then:
decision: neutral
reason: pre_auth_check_aborted
control:
skip_remaining_stage_checks: true

This control does not skip final auth_decision. It only stops remaining checks in the current stage.

Lua Actions and POST-Actions

Nauthilus has two Lua side-effect surfaces with similar names but different runtime timing.

SurfaceConfig action typeRuntime timingPolicy relationship
Synchronous Lua actionsbrute_force, lua, tls_encryption, relay_domains, rbl in auth.policy.obligation_targets.lua.actionsDispatched and waited for before the request continues.Policy-owned through auth.obligation.lua_action.dispatch.
Lua POST-Actionspost in auth.policy.obligation_targets.lua.actionsEnqueued after the request-time decision context is known.Policy-owned through auth.obligation.lua_post_action.enqueue.

Action scripts remain configured under auth.policy.obligation_targets.lua.actions; policy does not define script code. The selected policy decision decides whether a configured synchronous action runs. A triggered brute-force, Lua, TLS, relay-domain, or RBL fact does not dispatch a synchronous action by itself.

auth.obligation.lua_action.dispatch accepts these arguments:

ArgumentRequiredMeaning
actionyesOne of brute_force, lua, tls_encryption, relay_domains, or rbl.
environmentnoStable environment control or source name for environment-specific reports and learning context. It is most useful with action: lua.
waitnoBoolean, defaults to true. The current runtime preserves synchronous wait behavior; use true or omit it.

The built-in standard_auth policy attaches synchronous action obligations where earlier releases ran configured actions directly:

Triggering conditionSynchronous action obligation
Brute-force denialauth.obligation.lua_action.dispatch with action: brute_force.
TLS-required temporary failureauth.obligation.lua_action.dispatch with action: tls_encryption.
Unknown relay-domain denialauth.obligation.lua_action.dispatch with action: relay_domains.
RBL threshold denialauth.obligation.lua_action.dispatch with action: rbl.
Lua environment source trigger denialauth.obligation.lua_action.dispatch with action: lua and environment: <check>.

The built-in standard_auth policy attaches all mutable brute-force side effects to the standard_brute_force_deny decision:

then:
decision: deny
reason: brute_force_reject
response_marker: auth.response.fail
obligations:
- id: auth.obligation.brute_force.update
- id: auth.obligation.lua_action.dispatch
args:
action: brute_force
- id: auth.obligation.lua_post_action.enqueue
args:
action: brute_force

For custom policies, add these obligations explicitly when you want the same policy-owned side effects. Without them, a custom terminal policy decision can deny or tempfail without dispatching the synchronous Lua action, scheduling the POST-Action, or updating brute-force counters through the policy obligation path.

There is no post_decision policy stage. POST-Actions are enforcement follow-up work requested by obligations after a decision has been selected. A POST-Action must not change the already selected decision, FSM terminal state, response_marker, or response_message.

In mode: observe, custom obligations are diagnostic only: custom synchronous Lua action dispatch, POST-Action enqueueing, brute-force updates, learning updates, and other mutable side effects are not executed.

FSM Event Markers

FSM means finite-state machine. In Nauthilus it is the deterministic request-state tracker that records how an auth request moved through parsing, pre-auth checks, backend or account-provider evaluation, and the final decision.

In policy terms, three outputs have different jobs:

OutputAnswersExample
decisionWhat did this rule decide?deny, permit, tempfail, neutral
fsm_event_markerWhich auth-FSM event should record and enforce that path?auth.fsm.event.pre_auth_deny
response_markerWhich transport response profile should be rendered?auth.response.fail

The FSM is not a second policy language and is not admin-editable. Policies reference registered event markers; Nauthilus applies those events to the internal FSM and reaches terminal states such as auth_ok, auth_fail, auth_tempfail, or aborted. Those terminal state names are not valid fsm_event_marker values.

Most rules should omit fsm_event_marker. The compiler derives the normal marker from the policy stage and decision, then validates any explicit marker against the same registry.

StageDecisionDerived FSM event markerResulting meaning
pre_authneutralauth.fsm.event.pre_auth_okContinue after pre-auth. This is not a successful login.
pre_authdenyauth.fsm.event.pre_auth_denyStop before backend auth and terminate as auth_fail.
pre_authtempfailauth.fsm.event.pre_auth_tempfailStop before backend auth and terminate as auth_tempfail.
pre_authpermitnot allowedPre-auth cannot grant final authentication success.
auth_decisionpermitauth.fsm.event.auth_permitTerminal success for the active operation.
auth_decisiondenyauth.fsm.event.auth_denyTerminal denial for the active operation.
auth_decisiontempfailauth.fsm.event.auth_tempfailTerminal temporary failure for the active operation.
auth_decisionneutralnoneEvaluation continues; if no later rule permits the request, final enforcement denies.

Explicit FSM markers are useful when a custom policy needs a more specific built-in terminal path, for example empty-user, empty-password, or a deliberate pre-auth abort. They are also useful when reports, logs, metrics, and enforcement traces must distinguish two rules that share the same high-level decision.

Operator policies may reference policy-visible target markers only:

MarkerValid stageUse for
auth.fsm.event.pre_auth_okpre_authContinue after pre-auth.
auth.fsm.event.pre_auth_denypre_authDenial before backend auth.
auth.fsm.event.pre_auth_tempfailpre_authTemporary failure before backend auth.
auth.fsm.event.pre_auth_abortpre_authAbort pre-auth processing.
auth.fsm.event.auth_permitauth_decisionFinal permit for the active operation.
auth.fsm.event.auth_denyauth_decisionFinal deny for the active operation.
auth.fsm.event.auth_tempfailauth_decisionFinal temporary failure for the active operation.
auth.fsm.event.auth_empty_userauth_decisionEmpty username behavior; normally paired with decision: tempfail.
auth.fsm.event.auth_empty_passauth_decisionEmpty password behavior; normally paired with decision: deny.

The final auth_permit, auth_deny, and auth_tempfail markers are operation-terminal events. For authenticate they describe password authentication. For lookup_identity they describe identity lookup. For list_accounts they describe account-listing completion or denial.

Internal parser, stage-orchestration, caller-auth, and runtime abort markers are produced by Nauthilus itself and are not policy-visible. Examples include auth.fsm.event.parse_ok, auth.fsm.event.auth_evaluated, auth.fsm.event.account_provider_evaluated, auth.fsm.event.basic_auth_ok, and auth.fsm.event.abort.

Normal rule with derived FSM marker:

then:
decision: deny
reason: billing_locked
response_marker: auth.response.fail

In pre_auth, this derives auth.fsm.event.pre_auth_deny. In auth_decision, it derives auth.fsm.event.auth_deny.

Advanced rule with an explicit empty-password path:

then:
decision: deny
reason: empty_password
fsm_event_marker: auth.fsm.event.auth_empty_pass
response_marker: auth.response.fail

Response Marker Registry

MarkerDecisionPurpose
auth.response.okpermitSuccessful auth or lookup response.
auth.response.faildenyAuthentication, lookup, or account-list denial.
auth.response.tempfailtempfailTemporary failure.
auth.response.tempfail.no_tlstempfailTLS-required temporary failure.
auth.response.list_accounts.okpermitSuccessful list_accounts response.

Policies select response markers, not raw HTTP status codes, headers, gRPC status codes, OIDC protocol fields, or SAML protocol fields.

Response Message Reminder

If response_message is omitted or from: default, Nauthilus uses the default message from the response marker. literal uses configured text. attribute_detail is valid only for a registered string detail with sensitivity: public and purpose: response_message; generated Lua environment source and Lua subject source decision attributes expose status_message this way. i18n stores a stable i18n_key plus fallback text and is rendered only at the HTTP, gRPC, or IdP response boundary.

Obligation and Advice Registry

IDKindPurpose
auth.obligation.brute_force.updateobligationUpdate brute-force counters, toleration, and learning state.
auth.obligation.lua_action.dispatchobligationDispatch an existing configured synchronous Lua action after decision selection.
auth.obligation.lua_post_action.enqueueobligationEnqueue an existing Lua post-action after decision selection.
auth.advice.audit_reasonadviceAdd sanitized audit context.

Policy YAML references registered IDs. It cannot define executable obligation logic.

Built-In Attributes

The built-in registry includes at least these attributes.

AttributeStageOperationsTypeDetails
request.operationpre_authallstringnone
request.time.nowpre_authalldatetimenone
request.client.ippre_authallipnone
request.client.ip.presentpre_authallboolnone
request.client.ip.trustedpre_authallboolnone
request.client.ip.sourcepre_authallstringnone
request.transport.kindpre_authallstringnone
request.listener.namepre_authallstringnone
request.connection.tlspre_authallboolnone
request.initiator.kindpre_authallstringnone
request.http.routepre_authall when availablestringnone
request.grpc.methodpre_authall when availablestringnone
request.idp.client_idpre_authall when availablestringnone
request.saml.sp_entity_idpre_authall when availablestringnone
request.protocolpre_authallstringnone
auth.brute_force.triggeredpre_authauthenticateboolrule, bucket_id, client_net, repeating, rwp_active, bucket_count, bucket_ratio, effective_limit
auth.brute_force.repeatingpre_authauthenticateboolselected bucket summary
auth.brute_force.rwp.activepre_authauthenticateboolselected bucket summary
auth.brute_force.rwp.enforce_bucket_updatepre_authauthenticateboolselected bucket summary
auth.brute_force.toleration.activepre_authauthenticatebooltoleration summary
auth.brute_force.toleration.modepre_authauthenticatestringstatic, adaptive, or disabled
auth.brute_force.toleration.custompre_authauthenticatebooltoleration summary
auth.brute_force.toleration.positivepre_authauthenticatenumbertoleration summary
auth.brute_force.toleration.negativepre_authauthenticatenumbertoleration summary
auth.brute_force.toleration.max_negativepre_authauthenticatenumbertoleration summary
auth.brute_force.toleration.percentpre_authauthenticatenumbertoleration summary
auth.brute_force.toleration.ttl_secondspre_authauthenticatenumbertoleration summary
auth.brute_force.toleration.suppressed_blockpre_authauthenticatebooltoleration summary
auth.brute_force.bucket.matched_countpre_authauthenticatenumberselected bucket summary
auth.brute_force.bucket.triggered_countpre_authauthenticatenumberselected bucket summary
auth.brute_force.bucket.max_countpre_authauthenticatenumberselected bucket summary
auth.brute_force.bucket.max_ratiopre_authauthenticatenumberselected bucket summary
auth.brute_force.errorpre_authauthenticateboolreason_code, retryable
auth.tls.securepre_authauthenticate, lookup_identityboolnone
auth.relay_domain.presentpre_authauthenticateboolrelay-domain details
auth.relay_domain.knownpre_authauthenticateboolrelay-domain details
auth.relay_domain.valuepre_authauthenticatestringrelay-domain details
auth.relay_domain.rejectedpre_authauthenticateboolrelay-domain details
auth.relay_domain.static_matchpre_authauthenticateboolrelay-domain details
auth.relay_domain.soft_allowlistedpre_authauthenticateboolrelay-domain details
auth.relay_domain.configured_countpre_authauthenticatenumberrelay-domain details
auth.relay_domain.errorpre_authauthenticateboolreason_code, retryable
auth.rbl.threshold_reachedpre_authauthenticate, lookup_identityboolRBL summary
auth.rbl.scorepre_authauthenticate, lookup_identitynumberRBL summary
auth.rbl.thresholdpre_authauthenticate, lookup_identitynumberRBL summary
auth.rbl.matched_countpre_authauthenticate, lookup_identitynumberRBL summary
auth.rbl.matched_listspre_authauthenticate, lookup_identitystring_listRBL summary
auth.rbl.list_countpre_authauthenticate, lookup_identitynumberRBL summary
auth.rbl.allow_failure_error_countpre_authauthenticate, lookup_identitynumberRBL summary
auth.rbl.effective_errorpre_authauthenticate, lookup_identityboolRBL summary
auth.rbl.soft_allowlistedpre_authauthenticate, lookup_identityboolRBL summary
auth.rbl.ip_allowlistedpre_authauthenticate, lookup_identityboolRBL summary
auth.rbl.errorpre_authauthenticate, lookup_identityboolreason_code, retryable
auth.authenticatedauth_backendauthenticateboolbackend
auth.identity.foundauth_backendlookup_identityboolbackend
auth.backend.tempfailauth_backendauthenticate, lookup_identityboolbackend, reason_code, retryable
auth.backend.empty_usernameauth_backendauthenticate, lookup_identityboolnone
auth.backend.empty_passwordauth_backendauthenticateboolnone
auth.account_provider.completedaccount_providerlist_accountsboolcount
auth.account_provider.tempfailaccount_providerlist_accountsboolreason_code, retryable

For each configured brute-force bucket, Nauthilus also registers per-bucket attributes:

PatternTypeMeaning
auth.brute_force.bucket.<bucket>.matchedboolThe bucket matched the current protocol, OIDC client, IP family, and network context.
auth.brute_force.bucket.<bucket>.countnumberCurrent read-only sliding-window counter value.
auth.brute_force.bucket.<bucket>.limitnumberConfigured failed_requests value.
auth.brute_force.bucket.<bucket>.effective_limitnumberEffective Redis-side limit after adaptive toleration.
auth.brute_force.bucket.<bucket>.remainingnumberRemaining attempts until the effective limit is exceeded.
auth.brute_force.bucket.<bucket>.rationumbercount / effective_limit; useful with gt, gte, lt, and lte.
auth.brute_force.bucket.<bucket>.over_limitboolThe bucket is currently over the effective limit.
auth.brute_force.bucket.<bucket>.already_bannedboolA ban/repeating state already exists for this bucket.
auth.brute_force.bucket.<bucket>.repeatingboolThe bucket is either over limit or already banned.

The <bucket> segment is derived from auth.controls.brute_force.buckets[].name. It is lower-cased and normalized to an ASCII identifier segment: letters, digits, and _ are kept, other separators collapse to _, and leading digits are prefixed with b_. For example, IMAP Short becomes imap_short, and 24h becomes b_24h. If two configured bucket names normalize to the same identifier, policy snapshot compilation fails.

Per-bucket attributes carry internal details: rule, bucket_id, client_net, matched, over_limit, already_banned, repeating, limit, effective_limit, remaining, ratio, period_seconds, ban_time_seconds, and cidr.

Brute-force toleration attributes describe the reputation decision that may suppress a block after a bucket is over limit:

AttributeMeaning
auth.brute_force.toleration.activeThe current client IP is tolerated by the reputation calculation.
auth.brute_force.toleration.modestatic, adaptive, or disabled.
auth.brute_force.toleration.customA custom toleration entry matched the client IP.
auth.brute_force.toleration.positivePositive reputation counter.
auth.brute_force.toleration.negativeNegative reputation counter.
auth.brute_force.toleration.max_negativeMaximum tolerated negative counter.
auth.brute_force.toleration.percentEffective tolerated percentage.
auth.brute_force.toleration.ttl_secondsEffective reputation TTL in seconds.
auth.brute_force.toleration.suppressed_blockToleration suppressed a block that would otherwise have been applied.

Relay-domain attributes carry internal details: domain, matched_domain, configured_count, present, known, rejected, static_match, and soft_allowlisted.

RBL summary attributes carry internal details: lists, score, threshold, matched_count, list_count, allow_failure_error_count, effective_error, soft_allowlisted, and ip_allowlisted.

For each configured RBL list, Nauthilus also registers per-list attributes:

PatternTypeMeaning
auth.rbl.list.<list>.listedboolThe client IP matched this RBL list.
auth.rbl.list.<list>.weightnumberConfigured weight for this RBL list.
auth.rbl.list.<list>.errorboolLookup for this list ended with a technical error.
auth.rbl.list.<list>.allow_failureboolThe list is configured with allow_failure.

The <list> segment is derived from auth.controls.rbl.lists[].name with the same identifier normalization used for brute-force buckets. If two RBL list names normalize to the same identifier, policy snapshot compilation fails.

Per-list RBL attributes carry internal details: list, list_id, host, query, return_code, reason_code, ip_family, listed, error, allow_failure, and weight.

For each configured Lua environment source check, Nauthilus also registers:

  • auth.lua.environment.<name>.triggered
  • auth.lua.environment.<name>.abort
  • auth.lua.environment.<name>.error

For each configured Lua subject source check, Nauthilus also registers:

  • auth.lua.subject.<name>.rejected
  • auth.lua.subject.<name>.error

Lua trigger/reject attributes include an optional public status_message detail that policies can select as a response message.

Lua Attribute Registry Scripts

Use registry_scripts when request-time Lua needs to emit custom policy attributes.

auth:
policy:
registry_scripts:
- /etc/nauthilus/policy/attributes.lua

Example registry script:

nauthilus_policy.register_attribute({
id = "lua.billing.account_locked",
stage = "subject_analysis",
operations = { "authenticate" },
category = "subject",
type = "bool",
description = "The account is locked by the billing system",
details = {
reason = {
type = "string",
sensitivity = "internal",
},
status_message = {
type = "string",
sensitivity = "public",
purpose = "response_message",
max_length = 256,
},
},
})

If operations is omitted in a Lua registry script, it defaults to authenticate. An explicitly empty operation table is invalid.

Request-time Lua can emit only attributes that exist in the active snapshot registry. It cannot register attributes during a request.

Appendix: Complete standard_auth Policy

standard_auth is the built-in default policy set. The table below is verified against the current server/policy/evaluation/standard.go implementation and the policy constants in server/policy/types.go.

Selection is ordered and first-match. Pre-auth rules run first for authenticate and lookup_identity. If a pre-auth rule selects deny or tempfail, evaluation stops before final auth-decision rules. list_accounts skips pre-auth and starts at the account-provider decision rules.

Rules with requires need the named check result to be present with status ok or error. Dynamic Lua rules are generated per emitted Lua check result.

Pre-Auth Rules

OrderRule nameOperationsRequiresConditionEffectFSM markerResponse markerExtra
10standard_brute_force_error_tempfailauthenticatebrute_forceauth.brute_force.error == truetempfailauth.fsm.event.pre_auth_tempfailauth.response.tempfail
20standard_brute_force_denyauthenticatebrute_forceauth.brute_force.triggered == truedenyauth.fsm.event.pre_auth_denyauth.response.failObligations: auth.obligation.brute_force.update; auth.obligation.lua_action.dispatch with action: brute_force; auth.obligation.lua_post_action.enqueue with action: brute_force.
30standard_tls_enforcementauthenticate, lookup_identitytls_encryptionauth.tls.secure == falsetempfailauth.fsm.event.pre_auth_tempfailauth.response.tempfail.no_tlsObligation: auth.obligation.lua_action.dispatch with action: tls_encryption.
40standard_relay_domain_error_tempfailauthenticaterelay_domainsauth.relay_domain.error == truetempfailauth.fsm.event.pre_auth_tempfailauth.response.tempfail
50standard_relay_domain_rejectauthenticaterelay_domainsauth.relay_domain.present == true and auth.relay_domain.known == falsedenyauth.fsm.event.pre_auth_denyauth.response.failObligation: auth.obligation.lua_action.dispatch with action: relay_domains.
60standard_rbl_error_tempfailauthenticate, lookup_identityrblauth.rbl.error == truetempfailauth.fsm.event.pre_auth_tempfailauth.response.tempfail
70standard_rbl_rejectauthenticate, lookup_identityrblauth.rbl.threshold_reached == truedenyauth.fsm.event.pre_auth_denyauth.response.failObligation: auth.obligation.lua_action.dispatch with action: rbl.
80standard_lua_environment_<script>_erroractive operation: authenticate or lookup_identityemitted Lua environment source checkauth.lua.environment.<script>.error == truetempfailauth.fsm.event.pre_auth_tempfailauth.response.tempfailGenerated once per Lua environment source check result.
90standard_lua_environment_<script>_triggeractive operation: authenticate or lookup_identityemitted Lua environment source checkauth.lua.environment.<script>.triggered == truedenyauth.fsm.event.pre_auth_denyauth.response.failUses public status_message detail from auth.lua.environment.<script>.triggered when selected. Obligation: auth.obligation.lua_action.dispatch with action: lua and environment: <script>.
100standard_lua_environment_<script>_abortactive operation: authenticate or lookup_identityemitted Lua environment source checkauth.lua.environment.<script>.abort == trueneutralauth.fsm.event.pre_auth_oknoneSets control.skip_remaining_stage_checks: true.
110implicit_pre_auth_passauthenticate, lookup_identitynoneno pre-auth terminal or abort rule matchedneutralauth.fsm.event.pre_auth_oknoneInternal pass decision added by standard_auth.

The <script> placeholder is derived from emitted Lua attributes such as auth.lua.environment.geoip.triggered. This keeps hand-written check names valid as long as the check points to the named Lua script through config_ref.

Final Auth-Decision Rules

OrderRule nameOperationsRequiresConditionEffectFSM markerResponse markerExtra
200standard_backend_tempfailauthenticate, lookup_identitynoneauth.backend.tempfail == truetempfailauth.fsm.event.auth_tempfailauth.response.tempfail
210standard_empty_usernameauthenticate, lookup_identitynoneauth.backend.empty_username == truetempfailauth.fsm.event.auth_empty_userauth.response.tempfail
220standard_empty_passwordauthenticatenoneauth.backend.empty_password == truedenyauth.fsm.event.auth_empty_passauth.response.fail
230standard_lua_subject_<script>_erroractive operation: authenticate or lookup_identityemitted Lua subject source checkauth.lua.subject.<script>.error == truetempfailauth.fsm.event.auth_tempfailauth.response.tempfailGenerated once per Lua subject source check result.
240standard_lua_subject_<script>_rejectactive operation: authenticate or lookup_identityemitted Lua subject source checkauth.lua.subject.<script>.rejected == truedenyauth.fsm.event.auth_denyauth.response.failUses public status_message detail from auth.lua.subject.<script>.rejected when selected.
250standard_auth_successauthenticatenoneauth.authenticated == truepermitauth.fsm.event.auth_permitauth.response.ok
260standard_auth_failureauthenticatenoneauth.authenticated == falsedenyauth.fsm.event.auth_denyauth.response.fail
300standard_lookup_identity_successlookup_identitynoneauth.identity.found == truepermitauth.fsm.event.auth_permitauth.response.ok
310standard_lookup_identity_failurelookup_identitynoneauth.identity.found == falsedenyauth.fsm.event.auth_denyauth.response.fail
400standard_list_accounts_tempfaillist_accountsaccount_providerauth.account_provider.tempfail == truetempfailauth.fsm.event.auth_tempfailauth.response.tempfail
410standard_list_accounts_successlist_accountsaccount_providerauth.account_provider.completed == truepermitauth.fsm.event.auth_permitauth.response.list_accounts.ok
420standard_list_accounts_failurelist_accountsaccount_providerauth.account_provider.completed == falsedenyauth.fsm.event.auth_denyauth.response.fail
900standard_default_denyauthenticate, lookup_identity, list_accountsnoneno earlier final rule matcheddenyauth.fsm.event.auth_denyauth.response.failFinal fallback.

Standard FSM Event Sequence

For a terminal pre-auth decision, the target FSM marker sequence is:

auth.fsm.event.parse_ok
<selected pre_auth fsm_event_marker>

For an authenticate or lookup_identity final decision, the sequence is:

auth.fsm.event.parse_ok
<latest selected pre_auth marker or auth.fsm.event.pre_auth_ok>
auth.fsm.event.auth_evaluated
<selected final auth_decision fsm_event_marker>

For a list_accounts final decision, the sequence is:

auth.fsm.event.parse_ok
auth.fsm.event.pre_auth_ok
auth.fsm.event.account_provider_evaluated
<selected final auth_decision fsm_event_marker>

Standard Response Messages

When a rule does not select a specific public Lua status_message, the response marker chooses the default response class:

Response markerDefault message source
auth.response.failInvalid-login response text.
auth.response.tempfailGeneric temporary-failure response text.
auth.response.tempfail.no_tlsTLS-required temporary-failure response text.
auth.response.okNo default failure message.
auth.response.list_accounts.okNo default failure message.

Brute force is first-class policy material. The built-in default runs brute-force first and evaluates the brute-force policy checkpoint before later pre-auth checks, preserving the default evaluation order without making brute force a separate policy bypass.

Observability and Reports

Policy observability is redaction-aware. It has several layers:

LayerPurposeControlled by auth.policy.report
Request-local DecisionReportThe in-memory policy diagnostic object used while the request is evaluated.no
Normal structured logsBounded final facts for operations and alerting.no
Debug logs with module policyDetailed compiler, check, evaluation, FSM, observe, and report diagnosis.no
Prometheus and OpenTelemetryMetrics and traces for policy orchestration.no
Optional decision report outputRedacted report payload for deeper inspection.yes

The request-local report is created for the active auth operation and collects the facts that the policy engine needs:

Report fieldMeaning
session_idRequest/session correlation ID when available.
operationauthenticate, lookup_identity, or list_accounts.
stageLast evaluated policy stage.
attributesPolicy attributes emitted by built-ins, Lua, request facts, or backend exports.
checksCheck results with status, matched flag, decision hint, and emitted attributes.
missing_checksRequired checks that were not available for a policy rule.
unavailableFacts/checks intentionally unavailable, for example custom-only non-observe-safe checks in observe mode.
policiesSelected policy decisions in evaluation order.
finalFinal selected decision that enforcement applies.
observeDefault-vs-custom comparison result in mode: observe.

Reports are diagnostic material, not authentication responses. They do not add fields to HTTP, CBOR, Nginx auth-request, gRPC, OIDC, or SAML responses, and enabling reports does not change policy decisions.

Report Configuration

auth:
policy:
report:
enabled: true
include_fsm: true
include_checks: true
include_attributes: false
KeyDefaultEffect
enabledfalseEnables optional redacted decision report output. The in-memory report still exists when this is false.
include_fsmtrueKeeps FSM decision material in report output. The selected FSM marker is still enforced when reports are disabled.
include_checkstrueKeeps check results in report output. Checks still run and affect policy decisions when reports are disabled.
include_attributesfalseIncludes emitted attributes in report output. Leave this off unless you are actively diagnosing a policy, because reports become larger and redaction matters more.

The implementation defaults include_fsm and include_checks to true when omitted. include_attributes defaults to false because attributes may contain internal diagnostic details.

Redaction Rules

Decision reports must not expose passwords, tokens, cookies, LDAP bind secrets, raw runtime errors, stack traces, or non-public attribute details.

Attribute details carry sensitivity metadata:

SensitivityReport behavior
publicMay appear only when the detail is selected for a public purpose, such as the final response message.
internalRedacted from normal reports.
secretAlways redacted.

The redacted value placeholder is [redacted]. A public Lua status_message detail appears only after a policy explicitly selects it through then.response_message.

Observe Mode Reports

In mode: observe, standard_auth remains the production decision. Custom policies run as shadow evaluation and populate observe with comparison data:

Observe fieldMeaning
productionThe authoritative standard_auth final decision.
shadowThe custom-policy final decision.
surfaceResponse surface used for comparison, such as http_json, grpc_auth_service, or http_list_accounts.
mismatch and mismatch_typeWhether custom and production behavior differ, and why.
production_terminal_state / shadow_terminal_stateFSM terminal-state comparison.
response_message_matchWhether sanitized rendered response messages match.
obligations_matchWhether planned obligations match.

Observe mode deliberately does not execute custom obligations, synchronous Lua action dispatch, Lua POST-Action enqueueing, brute-force counter updates, learning updates, or other custom mutable side effects.

Report Example

{
"operation": "authenticate",
"stage": "pre_auth",
"attributes": {
"auth.rbl.threshold_reached": {
"id": "auth.rbl.threshold_reached",
"stage": "pre_auth",
"operation": "authenticate",
"value": true,
"details": {
"lists": {
"value": "[redacted]"
}
}
}
},
"checks": {
"rbl": {
"name": "rbl",
"type": "builtin.rbl",
"stage": "pre_auth",
"status": "ok",
"decision_hint": "deny",
"matched": true,
"attributes": ["auth.rbl.threshold_reached"]
}
},
"policies": [
{
"policy_name": "standard_rbl_reject",
"stage": "pre_auth",
"effect": "deny",
"fsm_event_marker": "auth.fsm.event.pre_auth_deny",
"response_marker": "auth.response.fail",
"response_message": {
"source": "response_marker",
"message": "Invalid login or password"
}
}
],
"final": {
"policy_name": "standard_rbl_reject",
"stage": "pre_auth",
"effect": "deny",
"fsm_event_marker": "auth.fsm.event.pre_auth_deny",
"response_marker": "auth.response.fail"
}
}

The exact output surface is diagnostic and not a public authentication API contract. Use stable policy IDs, reason, outcome_marker, response_marker, and fsm_event_marker for automation rather than parsing raw transport-specific auth responses.

Logs, Metrics, and Traces

Normal structured logs include bounded final facts such as:

  • policy_mode
  • policy_set
  • policy_name
  • operation
  • stage
  • decision
  • reason
  • response_marker
  • fsm_event_marker
  • snapshot_generation
  • observe mismatch flags

Debug logs use one debug module named policy and a policy_component field such as compiler, snapshot, checks, eval, fsm, observe, or report.

Prometheus and OpenTelemetry instrumentation covers snapshot build/reload, check execution, policy evaluation, require_checks, observe comparison, FSM application, response rendering, obligations, and advice. Labels are bounded. Usernames, client IPs, tokens, raw errors, response text, and attribute-detail values are not used as Prometheus labels.

Validation and Dumps

Policy errors use canonical config paths such as:

auth.policy.checks[2].type is invalid
auth.policy.policies[1].require_checks[0] references unknown check "foo"
auth.policy.policies[3].if.attribute references unknown attribute

Validate a file:

nauthilus --config /etc/nauthilus/nauthilus.yml --config-check

Inspect defaults and non-defaults:

nauthilus -d
nauthilus -n --config /etc/nauthilus/nauthilus.yml

auth.policy values appear in the canonical dump output. Sensitive values stay redacted unless -P is used.