Nauthilus 4.0.0
Nauthilus 4.0.0 is published as tag v4.0.0 on October 12, 2026. It is the stable upgrade target for deployments on
the 3.1 line.
This page is the release document for the whole v4 line. It lists the highlights, every breaking change and upgrade action from 3.x, and the new features. Operators who ran a v4 prerelease find the additional steps in Upgrading from a v4 prerelease.
Nauthilus 4 is a central policy engine, a Policy Decision Point, for authentication and authorization. One declarative decision layer sits in front of mail (IMAP, SMTP, POP3, JMAP), web and IdP flows (OIDC, SAML2), and other services. Brute-force protection, reputation, GeoIP, MFA assurance, native Go plugins, Lua, and the gRPC identity proxy feed the same decisions and are governed by the same policy.
Highlights
- One decision layer. Top-level
policyis the only configuration and runtime authority for authentication, IdP, backchannel, and Generic Policy decisions. Targets, checkpoints, providers, effects, and rules compile into atomic generations that fail closed. See Policy configuration. - Generic Policy API.
POST /api/v1/policy/decisionsand gRPCPolicyDecisionService/Evaluateevaluate typed facts for any service, not only logins, with separate caller profiles, schemas, and limits. See Operating the Generic Policy API. - Host-evidence auth FSM. Policy can tighten an authentication result but never loosen it: a
permittakes effect only when the host itself verified the credential, found the identity, or completed the account listing. - Reputation and network intelligence. The bundled
reputation,geoip, anddkim2-intelligenceplugins learn from authentication outcomes and publish assessments for policy, with an optional Kafka journal and audited management API. See Reputation operations. - Identity Provider. Restricted OIDC Dynamic Client Registration for native mail clients, RFC 8707 resource indicators with allowlisted introspection for resource servers, a direct MFA self-service portal, and canonical browser session state. See OIDC and MFA self-service.
- Native Go plugins. Fact and effect providers for policy, artifact identity checks before loading, replay-safety declarations, opt-in password caching for backend modules, and configuration reload on SIGHUP. See Native Go plugins.
- Distributed identity proxy. Edge and authority nodes talk over gRPC with connection ageing, shared admission limits, and self-healing caller tokens. See the distributed identity proxy guide.
- Operations. A dependency-free
/livezprobe, anldap_queuereadiness check, an opt-in LDAP identity lookup cache, NOTICE log field filtering, and pipelined brute-force Redis access.
Upgrading from 3.x
Nauthilus 4 deliberately breaks compatibility at several boundaries. There is no configuration converter, no dual configuration root, and no mixed-version browser pool. Follow the v4 migration guide step by step and validate the complete deployment in a non-production environment before rollout. Items marked Action below and in What's new in 4.0 need an operator decision or a configuration change.
Build, module, and plugins
- The Go module is
github.com/croessner/nauthilus/v4. Builds use Go 1.27.1 withGOEXPERIMENT=runtimesecret. See Compiling. - Rebuild every native
.soagainst the exact v4 host build.pluginapi/v1contract compatibility does not make a mismatched Go plugin load-compatible. Embed the host artifact identity with-ldflags "$(go run -mod=vendor ./scripts/native_artifact_fingerprint)"; unmarked, stale, or mixed artifacts are rejected beforeplugin.Open. See the plugin build guide. - Native
DecisionFactProviderandDecisionEffectProviderinterfaces replace the former Policy plugin surface. EveryDecisionEffectDescriptordeclaresReplaySafety(unsafe, oridempotentwith anIdempotencyKey); Lua effects are always replay-unsafe. HostgainsOpaqueIdentifierTagger(), configured throughplugins.opaque_identifier_tagger.PostActionRequest.PasswordHashneeds thepassword_hashcapability, so native ClickHouse modules needallow_capabilities: [password_hash]. Admin hooks (HookScopeAdmin,HookAuthAdmin) always requirenauthilus:admin;required_scopescan only narrow that requirement.- The GeoIP plugin requires
decision_bindings; each binding registers one provider for exact Policy targets. The Luageoip_reputation.luaplugin, itslua.plugin.geoip_reputation.*facts, and theGEOIP_REPUTATION_*settings are removed. ClickHouse reputation columns are filled only from an explicitplugin.exchange.geoip_reputationmap. Use the bundledgeoip,reputation, anddkim2-intelligenceplugins; convert static reputation data withscripts/convert-static-reputation.py. See the GeoIP plugin. - Go protobuf imports move to the v4 module. Protobuf packages, services, field numbers, and HTTP paths keep their wire meaning. See Public protobuf APIs.
Policy configuration
- Top-level
policyis the sole configuration and runtime authority for authentication, IdP, backchannel, Policy HTTP, and Policy gRPC decisions. The v3auth.policyroot, mixed roots, unqualifiedstandard_auth, rule/checkstage, and checkconfig_refare rejected. Migration is manual; see the policy configuration migration. - Rule
then.decisionacceptspermitanddeny, plustempfailandneutralinauthn. The internal effect namesindeterminateandnot_applicableare rejected as configured decisions. - Record predicates put
fieldinto each leaf ofrecords.where; the outerrecords.fieldkey is removed.whereaccepts nestedall,any, andnotagainst one record. Host scheduler guards reject record predicates. - Action: authn rule decisions are checked against the checkpoint position, and explicit
fsm_event_markervalues are validated. Runnauthilus --config-checkbefore the upgrade. See Policy and the auth FSM. - Authored integer and double kinds are preserved (
50.0stays a double), and integer/double comparisons are exact. - Policy, management, and backchannel callers have separate exact audiences and credentials. Issue, cache, rotate, and present them independently; Policy Basic and Policy Bearer never fall back to management or backchannel credentials.
Identity Provider and browser state
- Browser state uses one opaque envelope and typed, revisioned Redis records. Replace the whole browser-serving pool at once; do not mix 3.1 and 4 replicas. Old records stay inert and expire. See the browser session migration.
- Action: the IdP Redis token layout is a hard cut that spreads across Redis Cluster slots and never stores raw
secrets in key names. All existing opaque tokens, refresh tokens, and JWTs become invalid and clients must
reauthenticate; running device flows and unredeemed authorization codes are lost. Run the IdP Redis 6.2 or newer with
maxmemory-policy noeviction.storage.redis.encryption_secretis mandatory (at least 16 characters) and also keys the HMAC-SHA256 digests in token and code key names. See OIDC configuration. - The session-cookie
/api/v1/mfa/*routes are retired. The portal uses/mfa/*, and the cookie-free/api/v1/mfa-backchannel/*family is unchanged. Users with an enrolled factor must complete MFA to enter the portal. TOTP and recovery codes share an attempt budget of 10 per 5 minutes per identity. - Introspection fails closed on missing or malformed audiences and returns 503 when the token store is unavailable. Dynamic clients can never introspect.
- Stale login or MFA state in browser flows answers 409 instead of 503.
Behavior changes to review
- Brute force: every distinct wrong password counts immediately; only repeats of an already recorded password hash
are exempt. Review bucket limits that were tuned around the old grace period. Backend faults and an undecided
repeating-wrong-password check return
tempfailand are never counted. For LDAP, onlyinvalidCredentialscounts as a wrong password. See Brute-force configuration. - Client IP: all
X-Forwarded-Forlines are evaluated. An invalid chain falls back to the direct peer, not toX-Real-IP;X-Real-IPstill applies when noX-Forwarded-Forheader is sent. - Backchannel and Policy Basic callers: lockouts (429 /
RESOURCE_EXHAUSTED) are removed. Rejections get a fixed 300 ms delay, and undecidable token validation answers 503 withRetry-AfterorUNAVAILABLE. With the HTTP rate middleware enabled, failed caller authentications consume a per-IP failure budget. - gRPC: authority connections age out after about five minutes by default so clients rebalance; set
keep_alive.max_connection_age: 0sto opt out.AuthServiceadmission and dependency failures map to specific status codes instead ofINTERNAL. - LDAP: pool tuning options now take effect for every pool connection. Review them before upgrading; see LDAP and backends.
- Probes: point Kubernetes liveness probes at
/livez, and keep/healthzfor readiness and startup.
Upgrading from a v4 prerelease
Deployments that already run a v4.0.0 alpha, beta, or release candidate need these additional steps:
- Remove the DCR keys
oidc:dcr:{dynamic}:*andoidc:dcr:{registry}:activeof early prereleases manually; they have no TTL. Dynamic clients must register again after the IdP Redis hard cut. - The
dkim2-reputationplugin of early prereleases is removed; switch todkim2-intelligence. See the DKIM2/Rspamd guide. - The reputation filesystem outbox (
outbox_*keys) of early prereleases is gone; durable learning uses the optional Kafka journal. - Redis password state uses only the full 64-hex password hash. The short hash of earlier prereleases is no longer read or written; old values expire with their TTL, no cleanup is required.
- The ClickHouse login deduplication key changed, so a fresh deduplication window starts after the upgrade.
- A manual flush of cached edge caller tokens is no longer needed; edges replace rejected tokens automatically.
What's new in 4.0
Policy engine and Generic Policy API
POST /api/v1/policy/decisionsand gRPC/nauthilus.policy.v1.PolicyDecisionService/Evaluateexpose the same protected unary evaluation contract for any service, not only logins.- Caller profiles bind credentials to targets, schemas, fact allowlists, diagnostics, mTLS, request bounds, rates, and concurrency.
- Values are strictly typed. Ordered record lists are schema-defined, bounded, non-recursive, and distinguish missing
from empty;
any/allevaluation cannot turn a malformed or absent collection into a vacuous permit. Composite record-local expressions are supported. - Decisions are
permit,deny,not_applicable, orindeterminate; obligations, advice, diagnostics, and failure behavior are bounded and closed by configuration. - Candidate generations compile atomically. Required-provider failure, schema and provenance violations, effect outcomes, timeouts, rejected post-action scheduling, metrics, logs, and traces have explicit fail-closed semantics.
- Host-owned
nauthilus.request.*IP facts, master-user and toleration facts, the nativecomponentselector, the aggregate backend provider identityauthn/auth_backend(instance namebackend_order), and record schemas for native providers. Restricted generic Lua providers can contribute declared facts and execute only selected effects.
See Operating the Generic Policy API and the native plugin Policy API.
Policy and the auth FSM
- Action: configuration loading and
--config-checknow reject authn rules whose decision does not fit the checkpoint position. The final checkpoint of a target plan must decidepermit,deny, ortempfail. Earlier checkpoints can only decidedeny,tempfail, orneutral. Before, such rules loaded and every request that selected them ended as a temporary failure, so a rejected configuration was already broken at runtime. - An omitted
fsm_event_markeron an authn rule is still derived from checkpoint and decision, so existing rules do not need a marker. An explicit marker must match the decision and checkpoint; a contradicting one is rejected at load time. Rules in generic namespaces do not drive the auth FSM and need no marker. - The auth FSM is a host-evidence guard. A selected
permitapplies only when the host verified the credential, found the identity, or got an answer from every account database. Otherwise the request fails closed as a temporary failure, runs none of the permit's obligations, post-actions, or advice, logsPolicy permit rejected by the auth FSM guard, and incrementsauthn_fsm_guard_violations_total{operation,checkpoint}. Subject providers, plugin patches, and policy facts cannot raise the host verdict. - Action: a
list_accountspolicy that permitted a partial listing while an account database failed now answers a temporary failure. Alert on any increase ofauthn_fsm_guard_violations_total; it means a permit rule does not test the backend evidence it needs. - Subject providers are skipped when the backend settled without a result, instead of failing with HTTP 500.
- Unexpected application errors on HTTP auth endpoints and the gRPC authority are answered as regular temporary
failures, with
Auth-Statuson HTTP and nginx, instead of a bare 500 orINTERNAL. They are counted inauth_application_errors_total{transport}. - Internal authentication callers (backchannel, nginx, gRPC, IdP) no longer inherit the Generic Policy API per-client
limits.
policy.runtime.authn.max_concurrencyandrequests_per_secondbound them separately; the default0means unbounded. A capacity rejection is a regular temporary failure, counted bypolicy_authn_admission_rejections_total{reason}. policy.runtime.post_actions.workersandqueue_capacitysize the post-action supervisor (defaults 8 and 256). Rejected hand-offs are counted bypost_action_acceptance_failures_total{error_class}.- The policy hot path shares compiled state, fact sets, and record schemas instead of copying them per request, which reduces allocations and latency.
See Policy configuration and the policy configuration migration.
LDAP and backends
-
New, opt-in LDAP identity lookup cache (UCI).
storage.redis.identity_cachecaches successful LDAP results of passwordless identity lookups (mode=no-authHTTP requests and gRPCLookupIdentity, for example userdb lookups) in Redis:storage:redis:identity_cache:enabled: truettl: 60sThe default is disabled. An omitted or zero TTL means one minute; cache hits do not extend it. Entries are encrypted, never satisfy a password request, and are separated by protocol, client, backend, and LDAP lookup context. Policy and authorization still run on every hit. Password requests, master-user requests, and IdP flows never use it. A user-cache flush makes all identity entries under the Redis prefix cold on every instance. After an external LDAP account or group change, flush the user or wait for the TTL. See Identity lookup cache and Positive caches: UCP and UCI.
-
Action: LDAP pool connections now use the settings of their pool section. Before,
search_timeout,bind_timeout,modify_timeout,search_size_limit,search_time_limit,retry_*,cb_*,health_check_*, the cache settings,include_raw_result, andauth_rate_limit_*were ignored for the operations. Review these values before upgrading; for example, asearch_size_limiton the default section now limits every search of that section. A request rejected byauth_rate_limit_per_secondreturns a temporary failure. -
Every pool connection has a default operation timeout (
search_timeout, or 30 s). Connects dial with a 5 s timeout and TCP keepalive, one 30 s deadline bounds the whole connect loop, and the StartTLS handshake is bounded too. -
A pool deadlock after an LDAP server restart under load is fixed. Pool maintenance runs in the background, timed-out binds retire their connection, and borrowed slots stay with their owner after a transport error.
-
Concurrent searches for one account no longer share one attribute map, which fixed a
concurrent map writescrash. The in-flight deduplication key now includes scope and attribute list. -
/healthzreports a newldap_queuecheck that makes an instance unready when an LDAP pool holds queued requests without worker progress for 40 s by default.
See LDAP backend and Tuning LDAP and Lua.
Password caches and brute force
- Action (plugin backends): native backend modules can use the Redis positive password cache. Both the plugin's
PositivePasswordCacheable()declaration andplugins.modules[].positive_password_cache: trueare required, withcachebeforeplugin(module.backend)in the backend order. The default stays off; acacheentry before a plugin without opt-in now logs a configuration warning instead of silently doing nothing. Changing the setting requires a restart. - Plugin cache entries live in an isolated namespace and are bound to username, protocol, OIDC client, and an optional
additional identity scope (
PositivePasswordCacheScopeBackend) for plugins whose login resolution depends on a domain or tenant. Typed LDAP and Lua providers keep their cache-first order and accept only entries of their own backend family. - Action (dashboards): brute-force Redis access is pipelined.
bruteforce_redis_roundtrips_totalhas newkindlabels (pipeline_pw_hist_load,pipeline_preauth_check,pipeline_eval_bucket_counter_save,pipeline_affected_account,pipeline_pw_hist_save),redis_write_totalcounts each pipelined script once (its failed-login share halves), andbf_update_loop_totalobserves once per failed login. See Metrics. storage.redis.batchingserializes every command of a Redis client behind one flush worker. Leave it disabled (the default) unless your own measurements show a gain.- The shared password helpers verify bcrypt hashes (
$2a$,$2b$,$2y$), so plugin and Lua backends with PHP-written hashes work.
Identity Provider
- Dynamic Client Registration: restricted RFC 7591 registration for public native clients with exact redirect
validation, RFC 8252 loopback handling, PKCE
S256, bounded metadata, rate limits, and no secret issuance. Native application profiles supportdefault_scopes,implied_scopes,access_token_type,skip_consent, claim mappings, and path-less loopback redirects. Native applications that pass through the consent page must use127.0.0.1loopback redirects:http://[::1]:<port>cannot be expressed in the consent page's CSPform-action. - MFA self-service portal at
/mfa/register/home[/lang]: it authenticates with the normal first factor, rotates the browser session, and manages TOTP, WebAuthn, and recovery codes under explicit enrollment and freshness gates. See MFA self-service. - OIDC, SAML, login, consent, logout, MFA, and WebAuthn operations use typed, revisioned, expiring Redis records owned by one opaque browser-session anchor.
- Dedicated backchannel introspection clients via
allow_backchannel_introspection. - RFC 8707 resource indicators and allowlisted introspection. A confidential static client can declare a
token_introspectionblock withresources,clients, anddynamic_client_profiles. A resource server such as a mail server can then introspect user tokens of allowlisted clients and own resource indicators./oidc/authorizeand/oidc/deviceaccept the multi-valuedresourceparameter; the token endpoint can narrow access tokens to a subset of the grant. - Action (resource servers): user access tokens now carry an
azpclaim. When resources are granted,audbecomes an array of the client id followed by the resources. Token consumers that readaudmust accept an array. - DCR delayed response. The DCR profile setting
delayed_responseenables the delayed login-failure presentation for all dynamic clients, including already registered ones. - DCR
require_mfaenforces enrollment of every listed method;supported_mfaandrequired_mfa_levelapply to existing registrations without re-registration. prompt=loginandprompt=select_accountstart a fresh browser session for explicit account selection, and a duplicate navigation after MFA enrollment is fixed.- The DCR registration budget and native plugin hooks resolve client addresses with
runtime.servers.http.trusted_proxieslike the rest of the request pipeline.
See OIDC configuration and the Dynamic Client Registration guide.
Reputation, GeoIP, and DKIM2
- Reputation subsystem: the bundled native
reputationplugin learns from authentication outcomes and admitted observations, stores decaying evidence in Redis, and publishes assessment records for Policy. An optional Kafka journal with a separatereputation-workerconsumer provides durable learning; delivery requires a direct broker acknowledgement. An audited Management API and the Python admin client support lookups, overrides, and allocation maintenance. See Reputation operations and the reputation plugin reference. - DKIM2 intelligence: correlated DKIM2 signer and SMTP peer assessment with operator identity contracts. The
aligned Rspamd adapter of DKIM2
v0.1.22uses strict echoedrequest_idcorrelation and the complete DMARC signal mapping. A live-daemon verifier pass through real DNS and message-wire handling remains a separate operator acceptance gate before production enforcement. See the DKIM2 intelligence plugin and the DKIM2/Rspamd integration contract. - Release images ship
reputation.soanddkim2-intelligence.so; the stable image also contains/usr/app/reputation-worker.
Backchannel, gRPC, and the identity proxy
- Action: with
runtime.servers.http.middlewares.rateenabled, the per-IP rate limit on caller-authenticated/api/v1routes is a failure budget. Successful caller authentications never consume it; each failed one and eachauth/basicrequest consumes one token. An address without tokens gets429before its credentials are evaluated. Callers that share an address with a failing client lose access while that client keeps failing. auth.pipeline.max_concurrent_requestsnow also bounds the gRPC authority. HTTP and gRPC share one counter; calls beyond it getRESOURCE_EXHAUSTED. Review the value if the authority serves most of the load over gRPC.- Edge nodes replace an authority caller token that the authority rejects and retry once, with a guard window that stops a permanently rejected edge from fetching a token per RPC. The manual flush of edge caller tokens after an upgrade is no longer needed.
- Refused authority requests and rejected edge credentials are classified and logged with backend and authority
names.
PERMISSION_DENIEDstays a plain decline. - gRPC identity lookups without
client_ipfall back to the observed transport peer, so GeoIP bindings see the right address.
See Identity proxy configuration.
Native plugins
- SIGHUP reload. Changes inside
plugins.modules[].configtake effect on a reload when the plugin implementsReloadablePlugin. Every other field of thepluginssection is restart-bound, and a restart-bound change rejects the whole reload withplugin restart required. Each reload is counted inplugin_reconfigure_total{module,result}. The bundled GeoIP, HIBP, DKIM2 intelligence, and ClickHouse plugins support it. - Obligation targets can read the submitted password through a capability-gated credential provider. Credentials are
redacted in every log and trace rendering and dropped from providers without the
credentialsgrant. - Configuration errors of the bundled plugins no longer echo configured values such as URLs with credentials.
- ClickHouse: inserts are serialized with an exponential retry pause, the buffer is capped by
max_buffer_rows(default 10000),flush_intervalflushes quiet pods, and pending rows are flushed on stop.dedup_success(defaulttrue) anddedup_failure(defaultfalse) control login deduplication; the dedup key changed, so a fresh window starts after the upgrade. The Basic auth header is now correctly padded. See the ClickHouse plugin. - GeoIP: refresh workers stop cleanly, refreshes for replaced state are discarded, and GeoIP uses the host-normalized client address.
- Reputation: the storage start retries transient Redis errors (up to 10 attempts within about 50 s). The admin
client can import static reputation override artifacts with
reputation override import. See the admin client guide.
See Native Go plugins.
Observability and operations
- Action:
GET /livezis a dependency-free liveness endpoint. Point Kubernetes liveness probes at/livezand keep/healthzfor readiness and startup. Allow enough startup budget for the Redis readiness loop and the reputation start retry (the source example uses 150 s). - NOTICE log field exclusion.
observability.log.notice_ignore_fieldsremoves the listed field keys from NOTICE records. Required keys (time,level,instance,session,msg) cannot be removed, and other log levels keep all fields. The selection applies on reload. - The authentication latency metric
authentication_response_time_secondslabelssmtps,submission,imaps,pop3s,lmtps, andjmapseparately instead of folding them intoother. Update dashboards that rely onother. - Startup failures are logged once at
ERRORwith the failing step. The exit code stays1. - An empty
storage.redis.sentinelsblock, as printed bynauthilus -d, counts as "not configured".nauthilus -dprints built-in defaults, not a loadable configuration. - Plugin and remote backends are no longer logged as unknown during worker setup.
- The debug image deliberately ships without the
reputation-worker; use the stable image for it. - Repeating-wrong-password and toleration metrics,
backchannel_caller_auth_total, and reputation, GeoIP freshness, and Redis script metrics. - Configuration and secret files mounted as Kubernetes projected volumes (symlinked artifacts) are supported.
See Health endpoints, NOTICE field filtering, and Metrics.
Public APIs
Public protobuf sources live below api/auth/v1, api/common/v1, api/identity/v1, and api/policy/v1. Go imports
use the v4 module. Protobuf package names, service and method names, field numbers, wire types, and unchanged HTTP
paths keep their wire meaning; generated Go source identity does not. See
Public protobuf APIs.
Security
- Revoked access tokens are additionally denylisted under a SHA-256 digest key
(
oidc:denied_access_token:sha256:<hex>). For rollback safety, 4.0 still writes and reads the legacy raw-token key; it will be removed in a later release. - Opaque and JWT access tokens are bound to authoritative user revocation epochs.
- Redis trace spans no longer contain
db.statement, so credentials do not reach the tracing backend. - Request credentials are redacted in every log and trace rendering.
Fixes and performance
- Per-request Lua heap growth in policy profiles, which could lead to out-of-memory restarts, is fixed.
- The compiled schema is shared instead of rebuilt per request, and policy regex and CIDR operands are precompiled at load time.
- Batched Redis commands stay owned by one goroutine, fixing a production crash.
- Completed Policy decisions survive late cancellation.
- Provider facts are carried across authentication checkpoints, fixing lost GeoIP country facts in subject sources.
Build
- The exact toolchain is Go 1.27.1 with
GOEXPERIMENT=runtimesecret, golangci-lint 2.13.2, and protoc 36.1. - Builds support profile-guided optimization (PGO). See Compiling.
- Lua:
nauthilus_ldap.ldap_modify_assertand theassertion_filterfield allow RFC 4528 conditional LDAP updates. Registering a Prometheus metric twice reuses a compatible collector.
Go API
util.RequestClientIPis removed. Useutil.RequestClientIPWithConfig, which trusts no proxy when no configuration is passed.pluginapi/v1addsErrRestartRequired, the optionalReconfigureValidator,PositivePasswordCacheBackend,PositivePasswordCacheScopeBackend, andObligationRequest.Credentials. See the Go plugin developer guide.