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_ + auth_ + per-user@IP connection limits, the policy hook is overkill.
Configuration
auth. in yarilo. (the Helm names are below):
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: trueauth_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:/).
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
sha256sha512- 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. from components., with shorter names; each card above names its chart value.
From 2.4.2 a key under its auth. name fails the render and names the chart's, and check_, check_ and report_ set to false and hash_ 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_ is rendered as 12.
auth_ and auth_ expand ${ENV}, so both can come from a Secret; 2.4.1 and earlier take them literally. Through the chart, name the Secret in *_ to keep the value out of the config ConfigMap:
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 defaultThe chart renders "${YARILO_ and "${YARILO_ and sets both variables in the yarilo-auth pod from the Secret. Setting a value and its *_ 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_ and sets the variable in the yarilo-auth pod, so the value stays out of the ConfigMap. Setting it together with api_ 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_ and sets the variable in the yarilo-auth pod. Setting it together with hash_ fails the render.
Wire shape
Every request is POST {url}?command={allow|report} with header Content-Type: application/ and JSON body:
{
"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
}successandpolicy_appear only inreject auth_andpolicy_ check_ after_ auth reportcalls.- 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:
pwhashis 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
pwhashis 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_ 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
{
"status": 0,
"msg": "optional human-readable reason"
}status == 0→ allow / continuestatus < 0→ reject (auth fails with opaque wire response;msggoes to server-side logs)status > 0→ tarpit: yarilo-auth sleepsstatusseconds before proceeding
A 2xx HTTP status with malformed JSON or missing status field is treated as a failover per auth_.
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. that approves every attempt and counts failures per IP:
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
- Shadow mode (week 1) — set
auth_, watch the logs for would-have-rejected events. Tune policy server thresholds until false-positive rate is acceptable.policy_ log_ only: true - Soft enforce (week 2) — flip
auth_, keeppolicy_ log_ only: false auth_(fail-open). A policy server outage does not affect logins.policy_ reject_ on_ fail: false - Hard enforce (week 3+) — flip
auth_once the policy server has proven HA. A policy server outage now blocks logins; pair this with a monitored healthcheck.policy_ reject_ on_ fail: true
Tuning notes
auth_+policy_ check_ before_ auth auth_doubles the per-attempt latency. Most operators disablepolicy_ check_ after_ auth auth_oncepolicy_ check_ after_ auth auth_is trusted (not possible through the chart yet, see Helm);policy_ check_ before_ auth auth_is fire-and-forget so it doesn't add perceived latency.policy_ report_ after_ auth - The HTTP client uses keep-alive — a single yarilo-auth pod reuses connections to the policy server across thousands of requests.
- Set
timeout_shorter thanms auth.so a slow policy server doesn't push the entire wire response past the client timeout.auth_ failure_ delay