Skip to content

Auth Policy Server Integration ​

yarilo-auth can POST every login attempt to an external HTTP policy service that decides whether to allow, reject, or tarpit the request. The wire shape follows the wforce (weakforced) HTTP API so any policy server speaking that dialect plugs in without a custom adapter.

When to enable ​

A policy server is useful when:

  • credential-stuffing detection needs cross-IP pattern analysis (one IP brute-forcing 1000 distinct users — invisible to per-IP penalty)
  • access policies depend on signals that don't fit a SQL passdb: geolocation, time of day, fraud-score, SIEM integration
  • multi-tenant deployments need per-tenant abuse policies
  • compliance requires policy decisions in an audit pipeline

When the deployment is small and the threat model is satisfied by in-process auth_failure_delay + auth_penalty + per-user@IP connection limits, the policy hook is overkill.

Configuration ​

auth.policy in yarilo.yaml (the Helm names are below):

yaml
auth:
  policy:
    auth_policy_server_url: "https://wforce.internal:8084/?ctx=imap"
    auth_policy_server_api_header: "X-API-Key: change-me"
    auth_policy_hash_mech: sha256
    auth_policy_hash_nonce: "deployment-specific-salt-please-rotate"
    auth_policy_hash_truncate: 12
    timeout_ms: 5000
    auth_policy_reject_on_fail: false
    auth_policy_log_only: false
    auth_policy_check_before_auth: true
    auth_policy_check_after_auth: true
    auth_policy_report_after_auth: true

auth_policy_server_url ​

Default
none
Type
string
Also accepted as
url
Helm
components.auth.policy.url

The policy server. Empty disables the hook. A URL ending in & extends its query string instead of starting a new one (https://w/?tenant=42&).

auth_policy_server_api_header ​

Default
none
Type
string
Also accepted as
api_header
Helm
components.auth.policy.api_header
See also
api_header_secret_ref

"Key: value" sends a header of your own; "value" sends X-API-Key: value. ${ENV} is expanded.

auth_policy_hash_mech ​

Default
sha256
Type
string
Values
sha256 sha512
Also accepted as
hash_mech
Helm
components.auth.policy.hash_mech

Digest of pwhash. It must match the policy server's setting.

auth_policy_hash_nonce ​

Default
none
Type
string
Also accepted as
hash_nonce
Helm
components.auth.policy.hash_nonce
See also
hash_nonce_secret_ref

Salt mixed into pwhash, required when a policy server is set. Two deployments with different nonces have different hash spaces. ${ENV} is expanded.

auth_policy_hash_truncate ​

Default
12
Type
integer, bits
Also accepted as
hash_truncate_bits
Helm
components.auth.policy.hash_truncate_bits

Top bits of the digest that are sent. 12 gives 4096 buckets, enough for rate-limit patterns and useless for password recovery. 0 sends the full digest.

timeout_ms ​

Default
5000
Type
integer, milliseconds
Helm
components.auth.policy.timeout_ms

Cap on one round trip to the policy server.

auth_policy_reject_on_fail ​

Default
false
Type
boolean
Also accepted as
reject_on_fail
Helm
components.auth.policy.reject_on_fail

true rejects the login when the policy server is unreachable or answers something malformed. false carries on without its guidance.

auth_policy_log_only ​

Default
false
Type
boolean
Also accepted as
log_only
Helm
components.auth.policy.log_only

Ask the policy server and log its verdicts without enforcing them, to roll out a new policy in shadow mode.

auth_policy_check_before_auth ​

Default
true
Type
boolean
Also accepted as
check_before
Helm
components.auth.policy.check_before

Ask ?command=allow before the passdb chain runs. A rejection skips the passdb call entirely.

auth_policy_check_after_auth ​

Default
true
Type
boolean
Also accepted as
check_after
Helm
components.auth.policy.check_after

Ask ?command=allow once the chain's result is known. A rejection turns a successful login into a failure (account-takeover detection).

auth_policy_report_after_auth ​

Default
true
Type
boolean
Also accepted as
report_after
Helm
components.auth.policy.report_after

Send ?command=report after every decision, without waiting for the answer. It never delays the reply to the client.

Helm ​

The chart builds auth.policy from components.auth.policy, with shorter names; each card above names its chart value.

From 2.4.2 a key under its auth.policy name fails the render and names the chart's, and check_before, check_after and report_after set to false and hash_truncate_bits: 0 reach the config. In 2.4.1 such a key is dropped, the three switches cannot be turned off through the chart (a false is rendered as true), and hash_truncate_bits: 0 is rendered as 12.

auth_policy_server_api_header and auth_policy_hash_nonce expand ${ENV}, so both can come from a Secret; 2.4.1 and earlier take them literally. Through the chart, name the Secret in *_secret_ref to keep the value out of the config ConfigMap:

yaml
components:
  auth:
    policy:
      url: https://wforce.internal:8084/
      api_header_secret_ref:
        name: yarilo-policy
        key: api_header   # the default
      hash_nonce_secret_ref:
        name: yarilo-policy
        key: hash_nonce   # the default

The chart renders "${YARILO_AUTH_POLICY_API_HEADER}" and "${YARILO_AUTH_POLICY_HASH_NONCE}" and sets both variables in the yarilo-auth pod from the Secret. Setting a value and its *_secret_ref together fails the render.

api_header_secret_ref ​

Set in
Helm values
Default
name: , key: api_header
Type
map
See also
auth_policy_server_api_header

A Secret (name, key) holding the API header. The chart renders "${YARILO_AUTH_POLICY_API_HEADER}" and sets the variable in the yarilo-auth pod, so the value stays out of the ConfigMap. Setting it together with api_header fails the render.

hash_nonce_secret_ref ​

Set in
Helm values
Default
name: , key: hash_nonce
Type
map
See also
auth_policy_hash_nonce

A Secret (name, key) holding the nonce. The chart renders "${YARILO_AUTH_POLICY_HASH_NONCE}" and sets the variable in the yarilo-auth pod. Setting it together with hash_nonce fails the render.

Wire shape ​

Every request is POST {url}?command={allow|report} with header Content-Type: application/json and JSON body:

json
{
  "device_id": "<client_id>",
  "fail_type": "policy" | "internal" | "credentials" | "expired" | "disabled" | "account" | "",
  "login": "[email protected]",
  "protocol": "imap",
  "pwhash": "ae30",
  "remote": "203.0.113.42",
  "session_id": "<unique-per-attempt>",
  "tls": true,
  "success": true,
  "policy_reject": false
}
  • success and policy_reject appear only in auth_policy_check_after_auth and report calls.
  • Keys are alphabetic so the payload is byte-stable across releases — wforce Lua rules that hash the body for caching work without surprises.

pwhash ​

digest = hash_mech(hash_nonce || requested_username || "\0" || password)
pwhash = hex(truncate_to_top_N_bits(digest))

With the default 12-bit truncation:

  • pwhash is 4 hex chars (a leading byte + the top nibble of the second byte, masked off)
  • 4096 possible distinct values for the entire user base
  • a single user's pwhash is stable across attempts with the same password, changes on password change — rate-limiters can count reused-vs-novel passwords per IP without ever seeing plain text

If your policy server needs higher precision, set auth_policy_hash_truncate to 24, 48, or 0 (full digest). Anything above ~32 bits starts to leak which-user-has-which-password information; pick the smallest value your detection logic tolerates.

Response ​

json
{
  "status": 0,
  "msg": "optional human-readable reason"
}
  • status == 0 → allow / continue
  • status < 0 → reject (auth fails with opaque wire response; msg goes to server-side logs)
  • status > 0 → tarpit: yarilo-auth sleeps status seconds before proceeding

A 2xx HTTP status with malformed JSON or missing status field is treated as a failover per auth_policy_reject_on_fail.

Master-user exemption ​

Master-user impersonation (SASL PLAIN with non-empty authzid) bypasses the policy server entirely. Admin sessions are not subject to user-facing abuse policy. This matches the policy exemption applied to the cross-pod penalty store.

wforce example ​

Minimal wforce.conf that approves every attempt and counts failures per IP:

lua
setACL({"0.0.0.0/0"})
addListener("0.0.0.0:8084")

function allow(args)
    if args.attrs.protocol == "imap" then
        local key = args.attrs.remote
        local fails = newCA(key)
        if fails:get() > 50 then
            return -1, "too many failures", {}
        end
        if fails:get() > 10 then
            return 5, "slow down", {}
        end
    end
    return 0, "", {}
end

function report(args)
    if not args.success then
        local key = args.attrs.remote
        local fails = newCA(key)
        fails:add(1, 3600)  -- expire after 1h
    end
end

setAllow(allow)
setReport(report)

Rollout strategy ​

  1. Shadow mode (week 1) — set auth_policy_log_only: true, watch the logs for would-have-rejected events. Tune policy server thresholds until false-positive rate is acceptable.
  2. Soft enforce (week 2) — flip auth_policy_log_only: false, keep auth_policy_reject_on_fail: false (fail-open). A policy server outage does not affect logins.
  3. Hard enforce (week 3+) — flip auth_policy_reject_on_fail: true once the policy server has proven HA. A policy server outage now blocks logins; pair this with a monitored healthcheck.

Tuning notes ​

  • auth_policy_check_before_auth + auth_policy_check_after_auth doubles the per-attempt latency. Most operators disable auth_policy_check_after_auth once auth_policy_check_before_auth is trusted (not possible through the chart yet, see Helm); auth_policy_report_after_auth is fire-and-forget so it doesn't add perceived latency.
  • The HTTP client uses keep-alive — a single yarilo-auth pod reuses connections to the policy server across thousands of requests.
  • Set timeout_ms shorter than auth.auth_failure_delay so a slow policy server doesn't push the entire wire response past the client timeout.