Skip to main content
Version: Next

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 policy is 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/decisions and gRPC PolicyDecisionService/Evaluate evaluate 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 permit takes 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, and dkim2-intelligence plugins 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 /livez probe, an ldap_queue readiness 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 with GOEXPERIMENT=runtimesecret. See Compiling.
  • Rebuild every native .so against the exact v4 host build. pluginapi/v1 contract 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 before plugin.Open. See the plugin build guide.
  • Native DecisionFactProvider and DecisionEffectProvider interfaces replace the former Policy plugin surface. Every DecisionEffectDescriptor declares ReplaySafety (unsafe, or idempotent with an IdempotencyKey); Lua effects are always replay-unsafe.
  • Host gains OpaqueIdentifierTagger(), configured through plugins.opaque_identifier_tagger.
  • PostActionRequest.PasswordHash needs the password_hash capability, so native ClickHouse modules need allow_capabilities: [password_hash]. Admin hooks (HookScopeAdmin, HookAuthAdmin) always require nauthilus:admin; required_scopes can only narrow that requirement.
  • The GeoIP plugin requires decision_bindings; each binding registers one provider for exact Policy targets. The Lua geoip_reputation.lua plugin, its lua.plugin.geoip_reputation.* facts, and the GEOIP_REPUTATION_* settings are removed. ClickHouse reputation columns are filled only from an explicit plugin.exchange.geoip_reputation map. Use the bundled geoip, reputation, and dkim2-intelligence plugins; convert static reputation data with scripts/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 policy is the sole configuration and runtime authority for authentication, IdP, backchannel, Policy HTTP, and Policy gRPC decisions. The v3 auth.policy root, mixed roots, unqualified standard_auth, rule/check stage, and check config_ref are rejected. Migration is manual; see the policy configuration migration.
  • Rule then.decision accepts permit and deny, plus tempfail and neutral in authn. The internal effect names indeterminate and not_applicable are rejected as configured decisions.
  • Record predicates put field into each leaf of records.where; the outer records.field key is removed. where accepts nested all, any, and not against one record. Host scheduler guards reject record predicates.
  • Action: authn rule decisions are checked against the checkpoint position, and explicit fsm_event_marker values are validated. Run nauthilus --config-check before the upgrade. See Policy and the auth FSM.
  • Authored integer and double kinds are preserved (50.0 stays 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_secret is 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 tempfail and are never counted. For LDAP, only invalidCredentials counts as a wrong password. See Brute-force configuration.
  • Client IP: all X-Forwarded-For lines are evaluated. An invalid chain falls back to the direct peer, not to X-Real-IP; X-Real-IP still applies when no X-Forwarded-For header 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 with Retry-After or UNAVAILABLE. 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: 0s to opt out. AuthService admission and dependency failures map to specific status codes instead of INTERNAL.
  • 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 /healthz for 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}:* and oidc:dcr:{registry}:active of early prereleases manually; they have no TTL. Dynamic clients must register again after the IdP Redis hard cut.
  • The dkim2-reputation plugin of early prereleases is removed; switch to dkim2-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/decisions and gRPC /nauthilus.policy.v1.PolicyDecisionService/Evaluate expose 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/all evaluation cannot turn a malformed or absent collection into a vacuous permit. Composite record-local expressions are supported.
  • Decisions are permit, deny, not_applicable, or indeterminate; 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 native component selector, the aggregate backend provider identity authn/auth_backend (instance name backend_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-check now reject authn rules whose decision does not fit the checkpoint position. The final checkpoint of a target plan must decide permit, deny, or tempfail. Earlier checkpoints can only decide deny, tempfail, or neutral. 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_marker on 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 permit applies 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, logs Policy permit rejected by the auth FSM guard, and increments authn_fsm_guard_violations_total{operation,checkpoint}. Subject providers, plugin patches, and policy facts cannot raise the host verdict.
  • Action: a list_accounts policy that permitted a partial listing while an account database failed now answers a temporary failure. Alert on any increase of authn_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-Status on HTTP and nginx, instead of a bare 500 or INTERNAL. They are counted in auth_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_concurrency and requests_per_second bound them separately; the default 0 means unbounded. A capacity rejection is a regular temporary failure, counted by policy_authn_admission_rejections_total{reason}.
  • policy.runtime.post_actions.workers and queue_capacity size the post-action supervisor (defaults 8 and 256). Rejected hand-offs are counted by post_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_cache caches successful LDAP results of passwordless identity lookups (mode=no-auth HTTP requests and gRPC LookupIdentity, for example userdb lookups) in Redis:

    storage:
    redis:
    identity_cache:
    enabled: true
    ttl: 60s

    The 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, and auth_rate_limit_* were ignored for the operations. Review these values before upgrading; for example, a search_size_limit on the default section now limits every search of that section. A request rejected by auth_rate_limit_per_second returns 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 writes crash. The in-flight deduplication key now includes scope and attribute list.

  • /healthz reports a new ldap_queue check 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 and plugins.modules[].positive_password_cache: true are required, with cache before plugin(module.backend) in the backend order. The default stays off; a cache entry 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_total has new kind labels (pipeline_pw_hist_load, pipeline_preauth_check, pipeline_eval_bucket_counter_save, pipeline_affected_account, pipeline_pw_hist_save), redis_write_total counts each pipelined script once (its failed-login share halves), and bf_update_loop_total observes once per failed login. See Metrics.
  • storage.redis.batching serializes 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 support default_scopes, implied_scopes, access_token_type, skip_consent, claim mappings, and path-less loopback redirects. Native applications that pass through the consent page must use 127.0.0.1 loopback redirects: http://[::1]:<port> cannot be expressed in the consent page's CSP form-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_introspection block with resources, clients, and dynamic_client_profiles. A resource server such as a mail server can then introspect user tokens of allowlisted clients and own resource indicators. /oidc/authorize and /oidc/device accept the multi-valued resource parameter; the token endpoint can narrow access tokens to a subset of the grant.
  • Action (resource servers): user access tokens now carry an azp claim. When resources are granted, aud becomes an array of the client id followed by the resources. Token consumers that read aud must accept an array.
  • DCR delayed response. The DCR profile setting delayed_response enables the delayed login-failure presentation for all dynamic clients, including already registered ones.
  • DCR require_mfa enforces enrollment of every listed method; supported_mfa and required_mfa_level apply to existing registrations without re-registration.
  • prompt=login and prompt=select_account start 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_proxies like the rest of the request pipeline.

See OIDC configuration and the Dynamic Client Registration guide.

Reputation, GeoIP, and DKIM2​

  • Reputation subsystem: the bundled native reputation plugin learns from authentication outcomes and admitted observations, stores decaying evidence in Redis, and publishes assessment records for Policy. An optional Kafka journal with a separate reputation-worker consumer 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.22 uses strict echoed request_id correlation 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.so and dkim2-intelligence.so; the stable image also contains /usr/app/reputation-worker.

Backchannel, gRPC, and the identity proxy​

  • Action: with runtime.servers.http.middlewares.rate enabled, the per-IP rate limit on caller-authenticated /api/v1 routes is a failure budget. Successful caller authentications never consume it; each failed one and each auth/basic request consumes one token. An address without tokens gets 429 before its credentials are evaluated. Callers that share an address with a failing client lose access while that client keeps failing.
  • auth.pipeline.max_concurrent_requests now also bounds the gRPC authority. HTTP and gRPC share one counter; calls beyond it get RESOURCE_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_DENIED stays a plain decline.
  • gRPC identity lookups without client_ip fall 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[].config take effect on a reload when the plugin implements ReloadablePlugin. Every other field of the plugins section is restart-bound, and a restart-bound change rejects the whole reload with plugin restart required. Each reload is counted in plugin_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 credentials grant.
  • 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_interval flushes quiet pods, and pending rows are flushed on stop. dedup_success (default true) and dedup_failure (default false) 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 /livez is a dependency-free liveness endpoint. Point Kubernetes liveness probes at /livez and keep /healthz for 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_fields removes 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_seconds labels smtps, submission, imaps, pop3s, lmtps, and jmap separately instead of folding them into other. Update dashboards that rely on other.
  • Startup failures are logged once at ERROR with the failing step. The exit code stays 1.
  • An empty storage.redis.sentinels block, as printed by nauthilus -d, counts as "not configured". nauthilus -d prints 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_assert and the assertion_filter field allow RFC 4528 conditional LDAP updates. Registering a Prometheus metric twice reuses a compatible collector.

Go API​

  • util.RequestClientIP is removed. Use util.RequestClientIPWithConfig, which trusts no proxy when no configuration is passed.
  • pluginapi/v1 adds ErrRestartRequired, the optional ReconfigureValidator, PositivePasswordCacheBackend, PositivePasswordCacheScopeBackend, and ObligationRequest.Credentials. See the Go plugin developer guide.