Authentication configuration
Yarilo authenticates IMAP/POP3/Submission credentials against an ordered chain of passdb entries. The first passdb that returns a definitive result (success or failure for a known user) wins. Unknown users fall through to the next entry. A temporary failure — a database that does not answer — stops the chain there: the login is answered as a temporary failure, not passed to the next entry.
auth.passdb
A list of passdb entries. Each entry has a driver and a dsn. Order matters — entries are tried left-to-right.
driver
- Default
- none
- Type
- string
- Values
sqlitemysqlpostgrespasswd-filestatic
The backend this passdb entry reads.
dsn
- Default
- none
- Type
- string
SQL drivers: the driver's connection string. ${ENV_ is expanded at startup.
passwd_file_path
- Default
- none
- Type
- string
- Also accepted as
passwd_file
passwd-file driver: path to the user file. ${ENV_ is expanded at startup.
passdb_sql_query
- Default
- none
- Type
- string
- Also accepted as
password_query- See also
userdb_sql_query
SQL: a custom SELECT for authentication. Unset, the built-in yarilo_ schema is used. See Custom queries.
userdb_sql_query
- Default
- none
- Type
- string
- Also accepted as
user_query- See also
passdb_sql_query
SQL: the userdb lookup (home, mail, extra fields) used by delivery, quota-status and the backend API. Unset, the built-in SELECT username, home, mail FROM yarilo_ runs, so set it with a schema of your own. See Contract.
userdb_sql_iterate_query
- Default
- none
- Type
- string
- Also accepted as
iterate_query
SQL: the query that lists users, for admin tooling.
passdb_default_password_scheme
- Default
PLAINfor SQL,CRYPTfor passwd-file- Type
- string
- Also accepted as
default_pass_scheme
The scheme assumed when a stored password has neither a {SCHEME} prefix nor a crypt(3) marker.
skip_schema
- Default
false- Type
- boolean
SQL: skip CREATE TABLE IF NOT EXISTS yarilo_ at startup, for an existing schema.
max_open_conns
- Default
25for mysql,8for postgres,1for sqlite- Type
- integer
- See also
max_idle_conns
SQL: connections the entry's pool may hold. 0 is unlimited. A negative value is refused at startup.
max_idle_conns
- Default
- the same as
max_open_conns - Type
- integer
- See also
max_open_conns
SQL: idle connections kept for reuse. 0 keeps none. A negative value is refused at startup.
conn_max_lifetime
- Default
300- Type
- integer, seconds
SQL: age at which a connection is recycled. 0 is never. A negative value is refused at startup.
conn_max_idle_time
- Default
60- Type
- integer, seconds
SQL: how long an idle connection is kept. 0 is never. A negative value is refused at startup.
username_filter
- Default
- none
- Type
- string
- Added
- 2.4.2
Any driver: the entry is tried only for names that match. Masks are separated by spaces or commas, with * and ? wildcards, case-sensitive; a leading ! excludes. With at least one inclusion, a name must match one. A skipped entry is as if absent for that name: on login, on a userdb lookup, and for SCRAM. Empty tries every name.
auth:
passdb:
- driver: sqlite
dsn: /var/lib/yarilo/users.dbSQL passdb
All three SQL backends share one schema:
CREATE TABLE yarilo_users (
username TEXT/VARCHAR(255) PRIMARY KEY,
password TEXT/VARCHAR(255) NOT NULL,
home TEXT/VARCHAR(255) NOT NULL DEFAULT '',
mail TEXT/VARCHAR(255) NOT NULL DEFAULT '',
enabled INTEGER/TINYINT(1) NOT NULL DEFAULT 1
);Yarilo runs CREATE TABLE IF NOT EXISTS on startup — fresh installs work without manual migration. Existing tables are left untouched.
SQLite
Pure-Go driver (modernc.), no cgo. Best for single-node deployments and dev environments.
auth:
passdb:
- driver: sqlite
dsn: /var/lib/yarilo/users.dbMySQL / MariaDB
DSN format (go-sql-driver/): user:password@tcp(host:3306)/.
auth:
passdb:
- driver: mysql
dsn: "yarilo:${DB_PASSWORD}@tcp(db.internal:3306)/yarilo?charset=utf8mb4"PostgreSQL
DSN format (pgx): standard postgres:/ URL.
auth:
passdb:
- driver: postgres
dsn: "postgres://yarilo:${DB_PASSWORD}@db.internal:5432/yarilo?sslmode=require"passwd-file passdb
A flat, colon-separated user file (classic / layout) that serves both passdb and userdb roles — a self-hosted deployment can authenticate and resolve mail storage without any database.
auth:
passdb:
- driver: passwd-file
passwd_file_path: /etc/yarilo-passwd/passwd
passdb_default_password_scheme: CRYPTKubernetes note. Mount the file from a Secret/ConfigMap into its own directory — do not place it under
/, which is already the rendered-config mount. Aetc/ yarilo subPathfile mounted inside that directory fails withnot a directoryand crash-loops every pod. With the Helm chart, supply the content viaextraVolumes/extraVolumeMountsat a distinct path (e.g./) and pointetc/ yarilo-passwd passwd_at it:file_ path yamlextraVolumeMounts: - name: passwd-file mountPath: /etc/yarilo-passwd readOnly: true extraVolumes: - name: passwd-file secret: secretName: yarilo-passwd-file
File format
One user per line:
user:password:uid:gid:gecos:home:shell:extra_fields- Only
userandpasswordare required. uid/gid/gecos/shellare parsed for layout compatibility but ignored — yarilo derives privilege drop from config and storage paths from the home template, not per-user uid/gid.home(column 6) populates the userdbhomefield.extra_(column 8 onward) are space-separatedfields key=valuepairs. Keys with auserdb_prefix populate the userdb (userdb_,mail userdb_, …); bare keys are passdb-side (quota_ rule allow_,nets nologin, …).- Lines that are empty or begin with
#or:are skipped. - The file is reloaded automatically when its mtime or size changes.
Passwords carry a {SCHEME} prefix or a crypt(3) marker; unmarked values assume passdb_ (default CRYPT = crypt(3) autodetection). The same scheme set as the SQL passdb applies, including {SCRAM-SHA-256} verifiers.
# comment
[email protected]:{BCRYPT}$2b$12$...:1000:1000::/mail/alice::userdb_mail=maildir:~/Maildir userdb_quota_rule=*:storage=2G
[email protected]:{SHA512-CRYPT}$6$salt$hashstatic passdb
One shared credential and a set of templated fields applied to every user. Serves both passdb and userdb roles. For tests, single-mailbox installs, and proxy front-ends. Because it matches every username it must be placed last in the chain, and given a username_ unless every name really is a user: without one, a userdb lookup — delivery, quota-status — finds any address, so mail to a name nobody has is accepted.
auth:
passdb:
- driver: sqlite
dsn: /var/lib/yarilo/users.db
- driver: static # catch-all — last in the chain
static_password: "${YARILO_STATIC_PASSWORD}"
passdb_default_password_scheme: BCRYPT
fields:
userdb_home: "/var/vmail/%d/%n"
userdb_mail: "maildir:/var/vmail/%d/%n/Maildir"static_password
- Default
- none
- Type
- string
- See also
nopassword
static driver: the shared password, with a {SCHEME} prefix or in passdb_. ${ENV_ is expanded at startup.
nopassword
- Default
false- Type
- boolean
- See also
static_password
static driver: accept any password, for proxy front-ends where the upstream authenticates. Excludes static_.
fields
- Default
{}- Type
- map
static driver: templated user fields, expanding %u, %n and %d. userdb_-prefixed keys fill the userdb; bare keys travel on the passdb path (allow_, proxy, …).
userdb_static_allow_all_users
- Default
false- Type
- boolean
- Added
- 2.4.2
static driver: false answers a userdb lookup that no login preceded only for a user some passdb in the chain knows; true answers any name. A static entry is its own passdb too, so the two agree for every name its username_ accepts.
Only one account, the rest from SQL:
auth:
passdb:
- driver: mysql
dsn: "${YARILO_DB_DSN}"
- driver: static
username_filter: "[email protected]"
static_password: "${YARILO_STATIC_PASSWORD}"Proxy front-end (accept any credential, let the backend verify):
auth:
passdb:
- driver: static
nopassword: true
fields:
proxy: "y"
host: "backend.internal"Password schemes
The password column accepts a {SCHEME}hash prefix. Without a prefix, the format is autodetected from common crypt(3) markers.
| Scheme | Prefix | Hash format | Notes |
|---|---|---|---|
| Bcrypt | {BCRYPT} / {BLF-CRYPT} | $2a$.$2b$.../$2y$. | Recommended for new deployments. |
| SHA-512 crypt | {SHA512-CRYPT} | $6$salt$hash | Linux user import path. |
| crypt(3) | {CRYPT} | $2…$ or $6$… | Picks bcrypt or SHA-512 crypt by the hash marker. The passwd-file default. |
| SCRAM-SHA-256 | {SCRAM-SHA-256} | iter,salt,stored_ | Serves SCRAM and PLAIN/LOGIN from one value; see SCRAM. |
| SCRAM-SHA-1 | {SCRAM-SHA-1} | iter,salt,stored_ | As above, for clients without SHA-256. |
| Plain | {PLAIN} / {CLEARTEXT} | literal | Dev only. Never store production passwords in plain text. |
Autodetection (no {SCHEME} prefix):
| Stored value starts with | Treated as |
|---|---|
$2a$ / $2b$ / $2y$ | BCRYPT |
$6$ | SHA512-CRYPT |
| anything else | passdb_ — PLAIN for SQL unless set |
Generating a bcrypt hash
htpasswd -nbB [email protected] "topsecret"
# [email protected]:$2y$05$LhJ...Generating a SHA-512 crypt hash
mkpasswd -m sha-512 -S NaClNaCl topsecret
# $6$NaClNaCl$...Adding a user (SQLite example)
sqlite3 /var/lib/yarilo/users.db <<EOF
INSERT INTO yarilo_users (username, password, enabled)
VALUES (
'[email protected]',
'{BCRYPT}$2y$05$LhJOlnSj4N8u7CC8mvjLeOZjzPGq8GwS9ux/dRrK7uW5UlMnG7r4q',
1
);
EOFMultiple passdbs
Yarilo tries each entry in order until one returns a result (OK or fail for a known user). Unknown users are passed to the next entry. Useful for: hot-migrating between databases, or shadowing one source with another for testing.
auth:
passdb:
- driver: sqlite
dsn: /var/lib/yarilo/legacy.db # checked first
- driver: postgres
dsn: "postgres://...:5432/main" # falls through to hereCustom queries
passdb_, userdb_, and userdb_ accept any SELECT and can connect yarilo to an existing schema. The query may reference these variables, which are substituted as parameterised values (no string interpolation, no injection risk):
| Variable | Long form | Meaning | Example |
|---|---|---|---|
%u | %{user} | Full username | alice@example. |
%n | %{username} | Local part (before @) | alice |
%d | %{domain} | Domain (after @) | example. |
%s | %{protocol}, %{service} | The protocol of the login or delivery | imap |
%r | %{remote_, %{rip} | The client's address | 192. |
%l | %{local_, %{lip} | The address the client connected to | 198. |
Not every query sees every variable:
| Query | Variables |
|---|---|
passdb_ (and a master-user masterdb entry) | all |
userdb_ | %u %n %d %s |
userdb_ | none |
A query naming a variable it cannot have, or an unknown %{., stops the configuration from loading: bound as empty, it would silently change what the query matches.
Do not quote the variables in your YAML. They are rewritten to
?(sqlite/mysql) or$1,$2, … (postgres) at runtime. Writing'%u'produces literal'?'which the DB will treat as a string, not a placeholder.
Contract
passdb_must return asql_ query passwordcolumn. Columns are matched by name, not position, so the order is free; useASaliases to map an existing schema (pw_).hash AS password home,mailandenabledare optional — an absentenabledcounts as active.passwordis the only value used downstream whenuserdb_is also set.sql_ query userdb_must return:sql_ query home,mail. It answers every userdb lookup — after a login, and on its own for LMTP delivery, quota-status and the backend API, where no password is involved. Unset, it is not taken frompassdb_: the built-in query againstsql_ query yarilo_runs, and on your own schema every such lookup fails. Set it wheneverusers passdb_is set.sql_ query userdb_must return one column:sql_ iterate_ query username.
PostgreSQL with a
BOOLEANenabled column. The built-inyarilo_schema declaresusers enabledasINTEGER, so the default queries filter withWHERE enabled = 1. Point yarilo at an existing Postgres schema that types the columnBOOLEANand that clause fails withoperator does not exist: boolean = integer— writeWHERE enabled = truein your ownpassdb_/sql_ query userdb_. MySQL is unaffected: itssql_ query BOOLEANisTINYINT(1), so= 1is valid. The Go-side check is dialect-agnostic and accepts1/true/t/yes/oneither way.
Disabling versus blocking login
Two passdb columns stop a login, and they mean different things.
enabled disables the account. The SQL passdb reads it before it checks the password: a false value is refused there, and for login the user does not exist. Delivery is decided by userdb_ alone, so unless that query also filters on the column, mail keeps arriving for a disabled account:
enabled filtered in | Login | Delivery |
|---|---|---|
passdb_ and userdb_ | refused | refused: the user is unknown to LMTP |
passdb_ only | refused | accepted: mail keeps arriving |
-- passdb_sql_query, both shapes
SELECT password, active AS enabled FROM mailbox WHERE username = %u
-- userdb_sql_query, first shape only
SELECT home, maildir AS mail FROM mailbox WHERE username = %u AND active = 1nologin blocks the login and keeps the account. The password is verified first; then the login is refused with exactly the answer a wrong password gets, and the log carries reason=nologin. Delivery and the userdb record are untouched. Typical uses: freezing an account during an incident, and archive or technical mailboxes that receive mail but are never opened.
nologin comes from any passdb column or expression of that name. True is 1 / yes / y / true / t / on (case-insensitive); anything else, an empty value or NULL included, lets the login through. A schema with an active column usually has no nologin, so the operator adds one of these:
-- a column
ALTER TABLE mailbox ADD nologin TINYINT(1) NOT NULL DEFAULT 0;
SELECT password, active AS enabled, nologin FROM mailbox WHERE username = %u
-- or an expression, with no schema change
SELECT password, active AS enabled,
CASE WHEN username IN ('[email protected]') THEN 'yes' END AS nologin
FROM mailbox WHERE username = %uPer-protocol access
%s lets one account be refused one protocol and keep the others: one column per protocol, read through CASE. A variable is a value, never part of the SQL text, so allow_ cannot name a column; CASE %s is the form that works.
SELECT password, home, mail FROM mailbox
WHERE username = %u AND active = 1
AND CASE %s WHEN 'imap' THEN imap_enabled
WHEN 'pop3' THEN pop3_enabled
WHEN 'submission' THEN submission_enabled
ELSE 1 END = 1%s takes these values:
| Protocol | %s |
|---|---|
| IMAP | imap |
| POP3 | pop3 |
| Submission | submission |
| Postfix over the SASL login | smtp |
The Submission service names itself submission everywhere it reports a service: auth logs (proto=submission), the policy server's service, the auth cache. Releases before 2.4.3 used smtp there.
Upgrading from a release before 2.4.3: while the rollout runs, Submission logins fail wherever a new and an old component meet, a new proxy with an old backend or the reverse; this is the StatefulSet rollout window, minutes. Other protocols and delivery are not affected. | ManageSieve | sieve | | LMTP delivery | lmtp | | JMAP | jmap |
Where the rule goes decides what it covers.
| Rule in | Password and SCRAM logins | Token logins (OAUTHBEARER, XOAUTH2, JMAP Bearer) | Delivery |
|---|---|---|---|
passdb_ | covered | not covered: no password query runs | not covered |
userdb_ | covered | covered | covered (lmtp) |
When userdb_ uses %s, every login, whatever proved it, runs that query with the login's protocol before it is accepted. That is one more database query per login: on our test stand it added about 1.6 ms to the mean authentication time and nothing measurable to throughput. Without %s in userdb_, no extra query runs. To cover every way in, put the rule there, or in both queries. Lookups that are neither a login nor a delivery (shared folders, the backend API) carry no protocol: %s is empty for them, so keep an ELSE 1 branch, which lets them through.
A login refused by a rule gets exactly the answer and delay of a wrong password; the server log carries reason=protocol. A delivery refused by a rule in userdb_ is refused as an unknown recipient.
%r and %l restrict by address the same way, for example AND (allowed_; for networks rather than single addresses, the allow_ field below takes CIDR lists. A passdb that reads %r or %l caches its answer per address.
Userdb / passdb extra fields
Beyond home / mail, a lookup may return extra fields — as a SQL column alias, userdb_ output, or an auth-socket key=value / userdb_ pair — that tune per-user mailbox, access, and proxying behaviour. Unset fields fall back to the global config. Comma-separated fields (groups, acl_, quota_, allow_) accept multiple values and merge across repeated assignments. Boolean fields accept 1 / yes / y / true / t / on (case-insensitive). Unknown keys are preserved in Extra; forward_ keys populate the proxy forward map.
Identity
| Field | Meaning |
|---|---|
username / user | Canonical username (overrides the login for master-user flows). |
original_ | The login as typed before normalisation. |
master_ | On a userdb answer: the master when the login impersonated a user. On a passdb answer, a true value (master_) grants this login master rights — see Master users. |
login_ | Login user for delegated lookups (equals username otherwise). |
System identity & groups
| Field | Meaning |
|---|---|
uid / gid | System user/group id for privilege drop. |
home | User home directory. |
chroot | Chroot directory. |
system_ | Username override for system group lookup. |
groups | Supplementary group names (comma-separated). Matched against group= / group-override= ACL entries. |
client_ | Bool: TLS client-cert auth was used. |
ACL evaluation
| Field | Meaning |
|---|---|
acl_ | Override the identity used when evaluating ACLs (the reference's acl_). Typically set on a master-user session so ACL checks resolve as the impersonated user rather than the login. Empty = evaluate as the login user. |
acl_ | Group names (comma-separated) used alongside acl_ for ACL evaluation. |
acl_/user acl_only affect non-owner namespaces (shared / public / other-users). A user always has full rights in their own personal namespace regardless of the override.groups
Mail storage
| Field | Meaning |
|---|---|
mail / mail_ | Per-user mail location (maildir:~/, mdbox:…), with :INDEX=, :CONTROL=, :ALT=, :VOLATILEDIR= modifiers. |
mail_ | Base mailbox path (derived from mail when unset). |
mail_ | Explicit INBOX path override. |
mail_ / mail_ / mail_ / mail_ | Direct overrides for the corresponding mail-location modifier (win over modifiers embedded in mail). The names are the reference implementation's too, so a userdb query written for it works unchanged. |
mail_ / mail_ | Ownership for mail files, distinct from the system uid/gid. |
mailbox_ / mail_ | maildir | sdbox | mdbox — the storage backend that opens this user's mail. Wins over the driver prefix in mail. A name yarilo does not implement is refused and the prefix stands. |
mail_ | Dict URL backing RFC 5464 METADATA. |
Each of these paths can carry the same variables as the corresponding configuration key — %u / %{user}, %h / ~/, hash buckets, and the rest. See Path templates.
One value, three places. The four path fields can arrive as a modifier inside
:INDEX=…) or as the explicit field (mail_— one spelling now, ours and the reference's alike). An explicit field always beats the modifier, whichever spelling the field uses — so moving a userdb query from one spelling to the other never changes which source wins.index_ path
How a userdb answer is applied. One order, for every protocol — IMAP, POP3 and delivery (LMTP, JMAP, FTS, quota-status) share the code that does it:
- the separate path fields are expanded against the user and written onto the session;
mail/mail_is parsed: its driver prefix is stamped, and its modifiers fill in only the paths step 1 left blank;location - the explicit driver field (
mail_/driver mailbox_) is stamped last, so it wins over the prefix.format
A mail_ that cannot be parsed leaves the user on the globally configured backend rather than failing the login, and a driver name yarilo does not implement is refused with the stamped one left in place. Both are logged.
mailbox_used to be inert. Before 2.3.133 the field was parsed, carried and reported by the backend API — and never consulted. A userdb answeringformat mdboxopened the user with the globally configured driver. If you set it and saw no effect, that is why; it works now, so check that its value is the one you want before upgrading.
Quota
| Field | Meaning |
|---|---|
quota_ | Per-user quota rule, e.g. *:storage=5G or *:messages=100000 (repeatable). |
quota_ | Value marking the user as over quota. |
Login control
| Field | Meaning |
|---|---|
allow_ | Allowed source IP/CIDR list (comma-separated); empty = no restriction. |
identity_ | Addresses besides the login the user sends as, separated by spaces or commas; JMAP offers each as an Identity (see JMAP). An entry that is not an address is dropped with a warning naming the user. |
nologin | Bool: refuse the login after the password is verified, with a wrong password's answer; the account and its delivery stay. See Disabling versus blocking login. |
noauthenticate | Bool: auth disabled for this user. |
nodelay | Bool: bypass auth-penalty backoff. |
pass_ | Bool: password expired, client must reset. |
nopassword | Bool: accept any password (passdb). |
enabled | SQL filter column (WHERE enabled = 1, or = true on a Postgres BOOLEAN); not stored as a field. Optional: an absent column counts as active. Disables the account for login only; see Disabling versus blocking login for delivery. |
Proxy / director (see Director configuration)
| Field | Meaning |
|---|---|
proxy / proxy_ | Bool: proxy this session (unconditionally / only if remote). |
host / port | Upstream backend address. |
destuser | Username to present to the upstream. |
proxy_ | SASL mechanism for the upstream login. |
proxy_ | In place of the global value for this user: login_ in the IMAP/POP3/Submission/ManageSieve login proxies, lmtp_ in yarilo-lmtp-login. A bare number is seconds (30); otherwise a number and a unit, ms, s, m, h, d or w, spelled out or not, any case (500ms, 2m, 30 secs). At most 2³² ms; missing or 0 keeps the global value. The auth service passes the value on unread; a value the proxy cannot read refuses the login (NO [UNAVAILABLE]) or fails the recipient (550 5.). |
proxy_ | Bool: re-auth on redirect. |
proxy_ | Bool: disable command pipelining to the upstream. |
ssl / starttls | Upstream TLS mode / bool STARTTLS. |
Connection limits
| Field | Meaning |
|---|---|
mail_ | Max concurrent connections per user+IP. |
mail_ | Max concurrent connections per user. |
Misc
| Field | Meaning |
|---|---|
service | Restrict the entry to a named service. |
local_ | TLS SNI name this entry applies to. |
forward_ | Arbitrary key forwarded to the proxy backend (populates the forward map). |
| (any other key) | Preserved verbatim in Extra. |
Example: map an existing schema
auth:
passdb:
- driver: postgres
dsn: "postgres://yarilo:${DB_PASSWORD}@db.internal:5432/mailapp"
skip_schema: true
passdb_sql_query: |
SELECT pw_hash AS password, maildir AS home, mail_path AS mail, active AS enabled
FROM mailbox_users WHERE email = %u
userdb_sql_query: |
SELECT maildir AS home, mail_path AS mail
FROM mailbox_users WHERE email = %u
userdb_sql_iterate_query: |
SELECT email FROM mailbox_users WHERE active = true
passdb_default_password_scheme: BCRYPTEvery column an existing schema names differently needs an AS alias: password for the passdb, home and mail for the userdb. A query that returns pw_ without the alias authenticates nobody, because the lookup is by column name. WHERE active = true rather than = 1 because this schema types the column BOOLEAN; see the note above.
Example: PostfixAdmin
PostfixAdmin's mailbox table already names its password column password, so only active needs an alias. skip_ keeps yarilo-auth from creating its own yarilo_ table alongside it. A single passdb entry serves both roles: passdb_ answers authentication, userdb_ answers the userdb lookup, and every column the latter returns is mapped by name onto a userdb field, so quota_ and the rest arrive as aliases.
auth:
passdb:
- driver: mysql
dsn: "${YARILO_DB_DSN}"
skip_schema: true
passdb_default_password_scheme: BCRYPT
passdb_sql_query: |
SELECT password, active AS enabled, allow_nets
FROM mailbox
WHERE username = %u
userdb_sql_query: |
SELECT CONCAT(home, mpath) AS home,
CONCAT(mbtype, ':~/', maildir,
':INDEX=~/index',
':VOLATILEDIR=/tmp/yarilo-volatile/%2.256Nu/%u') AS mail,
CONCAT('*:bytes=', quota_bytes, ':messages=', quota_messages) AS quota_rule
FROM mailbox
WHERE username = %u
userdb_sql_iterate_query: |
SELECT username
FROM mailbox
WHERE active = 1
ORDER BY usernameusername, password, active, maildir and quota are stock PostfixAdmin; mpath, mbtype, quota_, quota_ and allow_ are columns this schema adds. Note that %2. and the second %u sit inside a quoted SQL string and are left alone — substitution skips quoted sections, so those reach the mail layer as templates, while the bare %u in the WHERE becomes a bound parameter.
On PostgreSQL the same queries work with one token changed:
-- passdb_sql_query, userdb_sql_query: unchanged. CONCAT() exists in PostgreSQL and
-- takes non-text arguments, and a BOOLEAN arriving as "enabled" is read as
-- active either way, because that check happens in Go rather than in SQL.
-- userdb_sql_iterate_query: PostfixAdmin types active as BOOLEAN on PostgreSQL and
-- TINYINT(1) on MySQL, and PostgreSQL rejects boolean = integer outright.
SELECT username
FROM mailbox
WHERE active -- was: WHERE active = 1
ORDER BY usernameLeaving = 1 in place breaks only enumeration: mail keeps flowing while yarctl backend user iterate fails, which makes the cause easy to miss.
Example: split passdb across hot/cold sources
auth:
passdb:
# Fast cache table — recent logins, refreshed by app.
- driver: postgres
dsn: "postgres://yarilo:${DB_PASSWORD}@cache.internal:5432/auth"
skip_schema: true
passdb_sql_query: |
SELECT pw_hash AS password, '/srv/' || %n AS home, '' AS mail, 1 AS enabled
FROM auth_cache WHERE email = %u
userdb_sql_query: |
SELECT '/srv/' || %n AS home, '' AS mail
FROM auth_cache WHERE email = %u
# Authoritative store — falls through when not in cache.
- driver: mysql
dsn: "yarilo:${DB_PASSWORD}@tcp(db.internal:3306)/billing"
skip_schema: true
passdb_sql_query: |
SELECT password, mail_home AS home, '' AS mail, enabled
FROM users WHERE email = %u
userdb_sql_query: |
SELECT mail_home AS home, '' AS mail
FROM users WHERE email = %u AND enabled = 1Each entry carries its own userdb_: without one, delivery to a user of that source would look them up in yarilo_.
Attempts, delays and the auth cache
Settings of yarilo-auth itself, under auth:.
auth_max_attempts
- Default
3- Type
- integer
- Helm
components.auth.auth_max_attempts
Failed authentications one IMAP or POP3 connection may make before it is closed. Submission closes after the first.
auth_failure_delay
- Default
2- Type
- integer, seconds
- Helm
components.auth.failure_delay- See also
internal_failure_delay_ms
How long every failed reply (wrong password, unknown user, malformed exchange) is held back, so the timing does not tell an unknown user from a wrong password. 0 disables it, for tests.
internal_failure_delay_ms
- Default
2000- Type
- integer, milliseconds
- Helm
components.auth.internal_failure_delay_ms- See also
auth_failure_delay
The same delay for internal failures, such as a passdb that does not answer.
auth_cache_size
- Default
- none
- Type
- string
- Also accepted as
cache_size- Helm
components.auth.cache.cache_size
Size of the in-process cache of passdb answers ("100M", "512k"). Empty or 0 disables it; the chart sets 100M.
auth_cache_ttl
- Default
1800- Type
- integer, seconds
- Also accepted as
ttl_seconds- Helm
components.auth.cache.ttl_seconds
How long a successful answer is kept.
auth_cache_negative_ttl
- Default
1800- Type
- integer, seconds
- Also accepted as
negative_ttl_seconds- Helm
components.auth.cache.negative_ttl_seconds
How long a failed answer is kept. Each entry carries a digest of the password that produced it, so a cached failure answers only that password again and cannot lock out the right one.
A cached answer outlives a password change or a deleted user until its TTL runs out; yarctl auth cache flush [<user-mask> …] evicts entries at once (yarctl). Further layers — the cross-pod penalty and an external policy server — are on Auth penalty and Auth policy.
Helm
The chart builds auth: from components.. Two differences from the keys on this page matter:
- Names. In
components.the chart reads the older names:auth. passdb[] passwd_,file password_,query user_,query iterate_andquery default_.pass_ scheme driver,dsn,skip_,schema static_,password nopasswordandfieldsare the same, and so are the pool settingsmax_,open_ conns max_,idle_ conns conn_andmax_ lifetime conn_. From 2.4.2 a key under the name this page uses fails the render and names the chart's; in 2.4.1 it is dropped without a warning, and the pool settings are not passed on at all. The same holds formax_ idle_ time components.(the names withoutauth. oauth2[] oauth2_, withscopesandextra_forfields oauth2_andscope oauth2_) and forfields components.(auth. policy url,api_,header hash_,truncate_ bits check_, … for thebefore auth_keys).policy_ * - Delays and cache.
auth_andmax_ attempts internal_keep their names,failure_ delay_ ms auth_isfailure_ delay failure_, and the cache isdelay cache.(defaultcache_ size "100M", so the cache is on),cache.andttl_ seconds cache.. From 2.4.2 anegative_ ttl_ seconds failure_ordelay internal_offailure_ delay_ ms 0reaches the config; in 2.4.1 it is rendered as the default.
components:
auth:
passdb:
- driver: mysql
dsn: "${YARILO_DB_DSN}"
skip_schema: true
password_query: |
SELECT password, active AS enabled FROM mailbox WHERE username = %u
user_query: |
SELECT home, maildir AS mail FROM mailbox WHERE username = %u
failure_delay: 2
cache:
cache_size: "100M"Postfix SASL integration (yarilo-sasl-login)
Postfix does not talk to yarilo-auth. A separate service, yarilo-sasl-login (port 12345), speaks Postfix's SASL protocol and relays each exchange to yarilo-auth, which runs the mechanism. Enable it with components..
Postfix main.:
smtpd_sasl_type = dovecot
smtpd_sasl_path = inet:yarilo-sasl-login.<namespace>.svc:12345
smtpd_sasl_auth_enable = yes
smtpd_sasl_security_options = noanonymousWhere to publish it, and how to restrict who reaches it, depends on where Postfix runs: see MTA-facing services.
Testing
Unit tests for the SQL passdb cover SQLite end-to-end (via t.). MySQL and PostgreSQL smoke tests are opt-in via env vars and skipped otherwise:
YARILO_TEST_MYSQL_DSN="yarilo:secret@tcp(localhost:3306)/yarilo_test?charset=utf8mb4" \
YARILO_TEST_POSTGRES_DSN="postgres://yarilo:secret@localhost:5432/yarilo_test?sslmode=disable" \
go test ./internal/auth/sql/These tests require pre-created empty databases (yarilo_). The schema is auto-created by New().