Documentation
¶
Overview ¶
Package protocol implements the yarilo-auth TCP+mTLS protocol. Server → Client handshake, then AUTH/CONT/CANCEL commands.
Index ¶
- func AssignField(info *UserInfo, key, value string) error
- func IsTruthy(s string) bool
- func MakeCacheKey(service, username string) string
- func ParseFieldPair(token string) (key, value string)
- func SplitCSV(s string) []string
- func SplitMasterFromAuthid(authid, sep string) (master, target string)
- type AuthResponse
- type AuthResult
- type Authenticator
- type AuthenticatorOption
- type Cache
- func (c *Cache) Clear() uint32
- func (c *Cache) ClearByUserMask(masks []string) uint32
- func (c *Cache) Insert(key, username, password string, result Result, fields *Fields)
- func (c *Cache) Lookup(key, password string) (*CacheEntry, bool)
- func (c *Cache) Remove(key string) bool
- func (c *Cache) Stats() (hits, misses uint64, sizeBytes, entryCount int64)
- type CacheEntry
- type Chain
- type Fields
- func (f *Fields) Delete(key string)
- func (f *Fields) Each(fn func(key, value string) bool)
- func (f *Fields) Get(key string) (string, bool)
- func (f *Fields) Has(key string) bool
- func (f *Fields) Len() int
- func (f *Fields) Rollback(snap *Snapshot)
- func (f *Fields) Set(key, value string)
- func (f *Fields) SetValidated(key, value string) error
- func (f *Fields) Snapshot() *Snapshot
- func (f *Fields) WireForm() []string
- type MasterAuthenticator
- type MasterServer
- type MasterServerOption
- type Passdb
- type PenaltyStore
- type PenaltyToSecsFunc
- type PolicyChecker
- type PolicyDecision
- type PolicyMode
- type PolicyRequest
- type Request
- type Result
- type SCRAMSha1Lookup
- type SCRAMSha256Lookup
- type Scope
- type Server
- type ServerOption
- func WithCache(c *Cache) ServerOption
- func WithFailureDelay(d time.Duration) ServerOption
- func WithInternalFailureDelay(d time.Duration) ServerOption
- func WithMasterUserSeparator(sep string) ServerOption
- func WithMasterUsers(enabled bool) ServerOption
- func WithMasterdb(passdbs []Passdb) ServerOption
- func WithPenalty(store PenaltyStore, toSecs PenaltyToSecsFunc) ServerOption
- func WithPolicy(checker PolicyChecker, mode PolicyMode) ServerOption
- func WithTokenStore(ts TokenStore) ServerOption
- func WithUserdb(u Userdb) ServerOption
- type Snapshot
- type TokenStore
- type UserInfo
- type Userdb
- type UserdbChain
- type UserdbIterator
- type Validator
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func AssignField ¶ added in v1.39.0
AssignField populates a single field on UserInfo from its canonical name + string value. Used by every layer that reconstructs a UserInfo from a string-keyed source: the SQL userdb driver (SELECT row column names) and pkg/authclient (`key=value` wire pairs from the master-protocol response).
Unknown keys land in UserInfo.Extra so a richer source schema does not lose data; `forward_*` keys auto-populate UserInfo.Forward. Boolean fields accept the canonical truthy values (1 / yes / y / true / t / on, case-insensitive); list fields (Groups, QuotaRules, AllowNets) accept comma-separated values and append, so a caller may invoke AssignField multiple times with the same key to merge entries (Dovecot allows `quota_rule=` to appear repeatedly).
func IsTruthy ¶ added in v1.39.0
IsTruthy reports whether s is one of the canonical truthy values auth-fields columns / wire pairs use (`1`, `yes`, `y`, `true`, `t`, `on`, case-insensitive). Anything else returns false — callers do not need to special-case empty strings or unknown values.
func MakeCacheKey ¶ added in v1.39.0
MakeCacheKey assembles the canonical cache key for a passdb / userdb lookup. Pinned to a single shape across drivers for predictability. Wire layer + chainAuthenticator both call this so cache hits are interchangeable between in-process and remote auth paths for the same (user, service).
func ParseFieldPair ¶ added in v1.39.0
ParseFieldPair splits one `key=value` token from the master-protocol wire into (key, value), undoing the escapes marshalUserInfo applies for TAB / LF / NUL / backslash. Tokens without `=` are treated as bare flags with empty value (mirroring Dovecot — `nodelay` on its own is equivalent to `nodelay=yes`).
func SplitCSV ¶ added in v1.39.0
SplitCSV splits a comma-separated value, trims whitespace around each entry, and drops empty entries. Returns nil for an empty or whitespace-only input so callers can assign the result directly to a `[]string` field without churning a slice the consumer must then filter.
func SplitMasterFromAuthid ¶ added in v1.39.0
SplitMasterFromAuthid extracts a (master, target) pair from an authid that uses the `target<sep>master` workaround for clients that cannot supply authzid in the PLAIN response (older Outlook, some mobile clients). Returns ("", authid) when sep is empty or not found — authid passes through unchanged.
A SASL response of the form `\0target*master\0password` is equivalent to the standards-compliant `target\0master\0password`.
Types ¶
type AuthResponse ¶
type AuthResponse struct {
Result AuthResult
Username string
Home string
MailLoc string
// Groups is the list of supplementary groups the user belongs to,
// sourced from the userdb `groups=` extra field. Used by ACL
// evaluation to match `group=` and `group-override=` entries.
// Empty when not configured — group= ACL entries have no effect.
Groups []string
// QuotaRules is the list of per-user quota rules sourced from the
// userdb `quota_rule=` extra field. Format: `*:storage=5G`.
QuotaRules []string
// VolatileDir is the VOLATILEDIR modifier from the mail location (or
// direct volatile_dir= userdb field). Carries the raw template string
// (%u/%n/%d/%h unexpanded) — callers expand it against the resolved
// home after auth completes.
VolatileDir string
// IndexDir is the INDEX= modifier from the mail location (or direct
// index_dir= userdb field). Carries the raw template string
// (%u/%n/%d/%h unexpanded). When set, per-folder index files live
// here instead of co-located with the mailbox under Home.
IndexDir string
// ControlDir is the CONTROL= modifier from the mail location (or direct
// control_dir= userdb field). Carries the raw template string
// (%u/%n/%d/%h unexpanded). When set, per-folder control files
// (yarilo-uidlist, subscriptions) live here instead of co-located
// with the mailbox under Home.
ControlDir string
// AltDir is the ALT= modifier from the mail location (or direct
// alt_dir= userdb field). Carries the raw template string
// (%u/%n/%d/%h unexpanded). When set, cold-tiered messages live
// under AltDir; reads check both primary and alt tiers.
AltDir string
Proxy bool
Host string
Port int
// Fields carries the passdb result as a key/value bag with
// prefix-derived scoping (see fields.go). Populated by passdb
// implementations that opt into Phase AUTH-2's wire surface;
// nil falls back to the typed-fields wire path.
Fields *Fields
}
AuthResponse is the final response sent back to the client session.
Phase AUTH-2 PR 1 adds Fields alongside the legacy typed members. When a passdb populates Fields, handleAuth emits the OK reply from the bag (with prefix gating: auth_* stripped, userdb_* passed through). When Fields is nil the typed fallback path runs — that's the pre-AUTH-2 wire shape, byte-compatible for the existing SQL passdb until PR 2 swaps the Passdb interface to take a shared Fields instance directly.
type AuthResult ¶
type AuthResult int
AuthResult is the outcome of a single authentication attempt.
const ( AuthOK AuthResult = iota AuthFail AuthTempFail )
type Authenticator ¶ added in v1.39.0
type Authenticator interface {
Authenticate(username, password, service, remoteIP string) (*AuthResponse, error)
}
Authenticator is the simpler legacy surface session paths (yarilo-imap, yarilo-pop3, etc.) use directly when they need to verify credentials in-process (without going over the yarilo-auth wire). Wraps a Passdb chain and projects the chain- internal Result back onto a wire-shaped AuthResponse so the caller does not need to know about Request / Result.
func NewAuthenticator ¶ added in v1.39.0
func NewAuthenticator(passdbs []Passdb, opts ...AuthenticatorOption) Authenticator
NewAuthenticator wraps one or more Passdb drivers into the session-friendly Authenticator surface. Passdb-only: userdb lookups are the session process's responsibility (via the master protocol).
Master-user impersonation is opt-in via WithAuthenticatorMasterUsers(true). When disabled (the default) the returned Authenticator deliberately does NOT implement MasterAuthenticator — so session-level type assertions in IMAP/POP3/Submission fail and any distinct SASL PLAIN authzid is rejected. Operators must explicitly flip the toggle (and configure masterdb / separator) to enable the feature.
type AuthenticatorOption ¶ added in v1.39.0
type AuthenticatorOption func(*chainAuthenticator)
AuthenticatorOption tunes a NewAuthenticator construction.
func WithAuthenticatorCache ¶ added in v1.39.0
func WithAuthenticatorCache(c *Cache) AuthenticatorOption
WithAuthenticatorCache attaches an auth cache. When set, every Authenticate / AuthenticateMaster call first consults the cache (key = `<service>\t<username>`) and short-circuits on hit. Misses run the full chain and seed the cache with the result. Nil cache = caching disabled (every call goes to the chain).
The cache is keyed by the passdb-side username; in a master-user flow the master and target produce independent cache entries.
func WithAuthenticatorMasterUserSeparator ¶ added in v1.39.0
func WithAuthenticatorMasterUserSeparator(sep string) AuthenticatorOption
WithAuthenticatorMasterUserSeparator enables the `target<sep>master` SASL workaround for legacy clients that cannot send authzid. Empty disables it. Mirrors WithMasterUserSeparator on the wire Server.
func WithAuthenticatorMasterUsers ¶ added in v1.39.0
func WithAuthenticatorMasterUsers(enabled bool) AuthenticatorOption
WithAuthenticatorMasterUsers flips the top-level master-user opt-in. When false (the default), NewAuthenticator returns an Authenticator-only wrapper — type-asserts to MasterAuthenticator at every protocol entry point fail, so any distinct SASL PLAIN authzid is rejected before the chain is consulted. When true, the chainAuthenticator's AuthenticateMaster method is exposed and the masterdb / separator options take effect.
func WithAuthenticatorMasterdb ¶ added in v1.39.0
func WithAuthenticatorMasterdb(passdbs []Passdb) AuthenticatorOption
WithAuthenticatorMasterdb attaches a dedicated masterdb chain. Mirrors WithMasterdb on the wire Server: AuthenticateMaster consults this chain first, falling through to the main passdb's per-user `master_user=yes` flag only when no masterdb entry knows the master.
type Cache ¶ added in v1.39.0
type Cache struct {
// contains filtered or unexported fields
}
Cache is an in-process LRU cache for verified auth lookups.
- Bytes-bounded: SizeBytes caps the total payload weight; LRU eviction makes room when an insert would exceed the cap.
- Two TTLs: positive entries (successful auth) live up to TTL, negative entries (failed lookup — unknown user, wrong password) live up to NegTTL. A separate, typically shorter NegTTL means a fresh password attempt can succeed soon after the user resets their password without waiting for the full positive TTL to expire.
- Password verify on hit: the cached entry stores an HMAC of the plain password (computed with a process-local random key). On lookup, the incoming password is HMAC-ed and compared in constant time. Negative entries store an empty password slot so any incoming password triggers re-lookup against the chain if the user happens to exist now.
Caching defence-in-depth: the HMAC key is generated per Cache construction and never persisted. A memory dump reveals the HMACs (which are not invertible) but not plain passwords. The key never crosses any wire — even the master-protocol CACHE-FLUSH only carries user masks, not entries.
Cache key format is decided by the caller (typically `service\tusername`); Cache treats keys as opaque.
func NewCache ¶ added in v1.39.0
NewCache returns a Cache bounded by sizeBytes total payload. A non-positive sizeBytes returns nil — caching is disabled and every helper that takes a *Cache no-ops on nil.
ttl is the positive-entry lifetime; negTTL is the negative-entry lifetime. ttl=0 disables positive caching; negTTL=0 disables negative caching. Setting ttl=0 with sizeBytes>0 means only negatives are cached (and vice versa).
func (*Cache) ClearByUserMask ¶ added in v1.39.0
ClearByUserMask evicts every entry whose stored username matches any of the supplied masks. Mask syntax: `*` matches zero or more chars, `?` matches one (admin CLI: `yarilo-admin auth cache flush [<user-mask>...]`).
Empty masks slice → behaves like Clear (full flush).
func (*Cache) Insert ¶ added in v1.39.0
Insert stores an entry under key. password is the plain credential (HMAC-ed before storage; never retained as plain). For negative entries pass an empty password — Insert stores no pwdMAC and Lookup treats every Fail entry as an unconditional short-circuit regardless of incoming password.
fields is the AuthResponse bag captured at the chain's reply; it's stored by reference so the caller must not mutate it after Insert returns. Pass a snapshot (Fields.Snapshot then a fresh Fields rebuilt) if mutation is unavoidable.
func (*Cache) Lookup ¶ added in v1.39.0
func (c *Cache) Lookup(key, password string) (*CacheEntry, bool)
Lookup checks the cache for key. On positive hit it verifies the incoming password against the stored HMAC in constant time; mismatch is treated as miss so a stale cached password cannot authenticate a new one. Negative hits return (entry, true) with Result=ResultFail and the caller short-circuits without touching the chain.
Expired entries are evicted lazily — Lookup removes them on touch so capacity accounting stays current. neg_expired_r equivalent: when an expired entry is encountered the caller gets (nil, false) and runs the chain; the eviction means a reinsert can land cleanly.
Returns (nil, false) for cache miss or password mismatch.
type CacheEntry ¶ added in v1.39.0
CacheEntry is the cached value. ResultOK carries Fields (the passdb result the chain produced) and pwdMAC (HMAC of the verified plain password). ResultFail carries neither — the presence of a Fail entry alone short-circuits the chain.
pwdMAC is set only on positive entries. Empty pwdMAC on a positive entry would let any caller pass on a cache hit, so callers MUST treat (Result=OK, len(pwdMAC)=0) as cache miss.
type Chain ¶
type Chain []Passdb
Chain composes multiple Passdb backends with first-hit-wins semantics. Each entry runs under a per-entry Fields.Snapshot so a driver's partial mutations are rolled back on ResultNext. Wire surface preserved from pre-PR-2: chain exhaust without a hit returns ResultFail (matches the pre-refactor Chain that returned AuthFail when every driver answered nil).
func (Chain) Authenticate ¶
Authenticate walks the chain. Snapshot before each driver, rollback on ResultNext, propagate immediately on OK / Fail / TempFail. Allocates the Fields bag on demand when the caller passed nil so drivers can always mutate without a nil-check.
type Fields ¶ added in v1.39.0
type Fields struct {
// contains filtered or unexported fields
}
Fields is an ordered key/value bag that carries the result of a passdb (and, eventually, userdb) lookup through the auth pipeline. Insertion order is preserved so the wire output is deterministic for any given mutation sequence — two processes that build the same Fields produce byte-identical OK responses.
Scope is derived from the key prefix at iteration / serialise time, not stored on the entry:
auth_* → ScopeInternal — never crosses the client wire
userdb_* → ScopeUserdb — passed through with the prefix
preserved so login pods can tell
them apart from passdb-only fields
(other) → ScopePassdb — passdb-only field, emitted on the
wire verbatim
Phase AUTH-2 PR 1 introduces Fields alongside the legacy typed AuthResponse fields. Both populate in parallel so the existing SQL passdb + handleAuth call sites stay byte-compatible with the pre-AUTH-2 wire. PR 2 swaps the Passdb interface to take a shared Fields instance so chains can mutate it; PR 3 wires userdb prefetch through the same bag.
func (*Fields) Delete ¶ added in v1.39.0
Delete removes a key. No-op when the key is absent. Subsequent keys shift to fill the gap so iteration order stays sequential.
func (*Fields) Each ¶ added in v1.39.0
Each iterates in insertion order. The visitor returns false to stop early; true to continue. nil bag yields nothing.
func (*Fields) Len ¶ added in v1.39.0
Len returns the number of fields. Returns 0 for a nil bag so callers do not have to nil-check before length-based decisions.
func (*Fields) Rollback ¶ added in v1.39.0
Rollback restores the bag to the state captured by snap. Calling Rollback with a nil snap is a no-op so callers do not need to branch on Snapshot returning nil.
func (*Fields) Set ¶ added in v1.39.0
Set adds or overwrites a key. Stable: rewriting a key keeps its original insertion-order position so the wire serialisation does not flip mid-stream.
func (*Fields) SetValidated ¶ added in v1.39.0
SetValidated parses value through the reserved-field registry before storing. When key matches a known reserved name (or `userdb_<base>` where base is reserved), the validator returns the canonical form and that goes into the bag. Unknown keys (including everything `forward_*`-prefixed and anything not in the registry) pass through verbatim — same behaviour as Set.
Returns a non-nil error when validation fails; the bag is NOT mutated in that case so callers can retry / surface the parse error without rolling back. Intended for admin / wire-side callers that ingest arbitrary input; driver-side passdb code keeps using Set (the driver knows its own schema).
func (*Fields) Snapshot ¶ added in v1.39.0
Snapshot captures the bag's state. Returns nil for a nil bag (Rollback is also nil-safe) so callers can use the pair without pre-allocation when the bag is optional.
func (*Fields) WireForm ¶ added in v1.39.0
WireForm renders Fields as the tab-delimited `key=value` sequence the client protocol's OK response carries. ScopeInternal entries are dropped unconditionally; ScopeUserdb and ScopePassdb entries are emitted in insertion order with values escape-encoded so embedded tabs / newlines / NULs cannot break the wire framing.
The first element returned is the response's user= token when `user=` is present in the bag — the rest of the auth pipeline expects the username token to lead. Callers that surface username via a different mechanism may pass it explicitly and drop the `user=` entry before serialising.
type MasterAuthenticator ¶ added in v1.39.0
type MasterAuthenticator interface {
AuthenticateMaster(authzid, authid, password, service, remoteIP string) (*AuthResponse, error)
}
MasterAuthenticator extends Authenticator with the SASL PLAIN authzid surface — i.e. master-user impersonation. Session-level callers (IMAP / POP3 / Submission) type-assert opts.Auth into this interface to decide whether to honour authzid; backends that only implement Authenticator (or stub test doubles) keep working unchanged but fall back to "authzid must equal authid" behaviour at the SASL layer.
- authzid — target identity the caller wants to log in AS. When empty (or equal to authid), this is a regular login and callers should typically dispatch via Authenticate instead.
- authid — the user supplying the password (the master in an impersonation request).
- password — the master's password.
- service — login service tag (imap / pop3 / submission / lmtp). Logged + forwarded to the chain unmodified.
type MasterServer ¶ added in v1.39.0
type MasterServer struct {
// contains filtered or unexported fields
}
MasterServer answers password-less userdb lookups and user enumeration over a separate TCP+mTLS listener. Wire-format mirrors Dovecot's auth-master-connection at the shape level (USER / PASS / LIST commands with ID-tracked responses) so admin tooling that already knows the Dovecot dialect maps over without surprises.
MasterServer is intentionally separate from the client-protocol Server: different audience (admins + backend-api vs login pods), different threat model (no untrusted credentials cross this socket), and different evolution cadence. The two share mTLS material today; splitting is a future operational knob.
func NewMasterServer ¶ added in v1.39.0
func NewMasterServer(userdb Userdb, opts ...MasterServerOption) *MasterServer
NewMasterServer constructs a MasterServer rooted at the given userdb. The userdb may be a UserdbChain composing multiple backends; LIST opportunistically uses UserdbIterator via type assertion when the supplied userdb implements it.
Passing a nil userdb is legal — every USER lookup will respond NOTFOUND, LIST will respond FAIL. Useful for deployments that expose only the wire surface (e.g. for connectivity checks) without configured backends.
func (*MasterServer) ListenAndServe ¶ added in v1.39.0
ListenAndServe accepts connections until ctx is cancelled. When tlsCfg is non-nil the listener uses mTLS (TLS 1.3, RequireAndVerifyClientCert is the caller's responsibility — set it on tlsCfg). Each connection is served by its own goroutine; active conns drain before the function returns.
type MasterServerOption ¶ added in v1.39.0
type MasterServerOption func(*MasterServer)
MasterServerOption tunes a NewMasterServer construction.
func WithMasterCache ¶ added in v1.39.0
func WithMasterCache(c *Cache) MasterServerOption
WithMasterCache attaches the auth cache that the master protocol's CACHE-FLUSH verb operates on. Pass the same *Cache instance the client-protocol Server received via WithCache so flushing through the admin socket actually empties the cache servicing login pods. Nil cache means CACHE-FLUSH responds FAIL.
func WithMasterTokenStore ¶ added in v1.57.0
func WithMasterTokenStore(ts TokenStore) MasterServerOption
WithMasterTokenStore attaches the shared token store. When set, the SESSION command issues tokens that the client-protocol VERIFY command can validate (both Server and MasterServer must receive the same TokenStore instance). When nil, SESSION responds FAIL.
type Passdb ¶
Passdb is the interface that passdb backends implement. Drivers receive a chain-wide Request whose Fields bag they mutate directly; the returned Result selects the chain's next move. error is reserved for unexpected technical failures that accompany ResultTempFail — the wire never carries the error text, only the temp_fail marker, so a driver-level "connection refused" is invisible to the login pod.
type PenaltyStore ¶ added in v1.39.0
type PenaltyStore interface {
PenaltyLookup(ip string) (int, error)
PenaltyUpdate(ip string, count int) error
}
PenaltyStore is the cross-process auth-fail backoff store the wire Server consults pre-passdb. Implemented by *anvil.Conn so every yarilo-auth pod shares the same counters; tests can plug in a stub. Nil-safe at the call sites — Lookup returns 0 and Update is a no-op when the store is not configured.
type PenaltyToSecsFunc ¶ added in v1.39.0
PenaltyToSecsFunc maps the penalty counter returned by PenaltyStore.Lookup to the sleep duration applied before the chain runs. Caller-supplied so a deployment can tune the exponential curve without recompiling protocol — typical implementations: anvil.PenaltyToSecs (2/4/8/15 cap), linear, or capped-linear. Nil falls back to no-sleep.
type PolicyChecker ¶ added in v1.39.0
type PolicyChecker interface {
CheckBefore(ctx context.Context, req PolicyRequest) (PolicyDecision, error)
CheckAfter(ctx context.Context, req PolicyRequest, success, policyReject bool) (PolicyDecision, error)
ReportAfter(ctx context.Context, req PolicyRequest, success, policyReject bool)
}
PolicyChecker is the policy-server hook surface the wire Server consults around the chain run. Implemented by *policy.Client (auth/policy package); tests can plug stubs.
Decision shape lives here so protocol does not import the policy package — single-direction dependency keeps the build graph clean.
type PolicyDecision ¶ added in v1.39.0
PolicyDecision is the parsed verdict. Continue=true means proceed; Reject=true means refuse; TarpitSecs>0 means sleep that many seconds then continue.
type PolicyMode ¶ added in v1.39.0
PolicyMode controls which lifecycle hooks fire. CheckBefore runs pre-passdb (block the chain on policy refusal); CheckAfter runs post-passdb with the outcome known (downgrade-success-to-fail use case); ReportAfter is fire-and-forget telemetry.
type PolicyRequest ¶ added in v1.39.0
type PolicyRequest struct {
Username string
Password string
RemoteIP string
Service string
DeviceID string
SessionID string
TLS bool
FailType string
}
PolicyRequest is the input to a policy call. Mirror of the shape policy.Client accepts; defined here so the wire Server can call without the policy package import.
type Request ¶ added in v1.39.0
type Request struct {
Username string
Password string
Service string
// RemoteIP is the client IP string (no port). Empty when the IP is
// unavailable (in-process chain, loopback test harnesses). Passdb
// drivers that enforce allow_nets MUST skip the check when empty.
RemoteIP string
// Fields is the chain-wide bag that accumulates passdb output.
// Driver implementations MAY assume non-nil; the canonical
// caller (Chain.Authenticate) allocates it when the Server-side
// authenticate entry-point starts the chain.
Fields *Fields
}
Request bundles every input a passdb needs to authenticate plus the shared Fields bag the chain mutates as it walks. Phase AUTH-2 PR 2 introduces this struct so passdb backends can write directly into the bag (the chain isolates each driver's mutations via Snapshot / Rollback on ResultNext). PR 3 will extend Request with userdb-prefetch metadata.
type Result ¶ added in v1.39.0
type Result int
Result classifies a passdb outcome. Separate from AuthResult (which is wire-shaped on AuthResponse) because ResultNext is a chain-internal signal — it never reaches the client wire.
const ( // ResultOK — user verified, Fields populated, chain stops. ResultOK Result = iota // ResultFail — credentials rejected (password mismatch, // disabled account, expired credential). Chain stops; the // wire returns FAIL. ResultFail // ResultNext — user unknown in this driver; chain rolls back // any partial Fields mutations and tries the next driver. ResultNext // ResultTempFail — backend technical failure (DB down, // network). Chain stops with a wire-level FAIL temp_fail. // Always accompanies a non-nil error so the server-side log // has the underlying cause. ResultTempFail )
func RunAuth ¶ added in v1.39.0
RunAuth executes the passdb chain. passdb is authoritative for the auth decision; userdb is never called here. Session processes obtain home/mail/quota via a separate USER command on the master socket after a successful VERIFY.
func RunMasterAuth ¶ added in v1.39.0
hasUserdbFields reports whether the bag already carries at least one userdb_-prefixed key — the marker for a prefetched userdb result that a downstream userdb.Lookup must not overwrite. RunMasterAuth handles a master-user impersonation request. req.Username MUST be the master when the function is called. On success req.Fields carries master_user/original_user/user. userdb is not consulted here — session processes call it via the master socket after VERIFY.
type SCRAMSha1Lookup ¶ added in v1.39.0
type SCRAMSha1Lookup interface {
LookupSCRAMSha1(username string) (*sasl.ScramCredentials, error)
}
SCRAMSha1Lookup is the SHA-1 counterpart of SCRAMSha256Lookup. Same semantics, same (creds, nil) / (nil, nil) / (nil, err) contract; only the digest family of the returned verifier differs. Provided for compatibility with legacy clients that only speak SCRAM-SHA-1 — new deployments should provision SCRAM-SHA-256 verifiers instead.
type SCRAMSha256Lookup ¶ added in v1.39.0
type SCRAMSha256Lookup interface {
LookupSCRAMSha256(username string) (*sasl.ScramCredentials, error)
}
SCRAMSha256Lookup exposes per-user SCRAM-SHA-256 verifiers to the session-layer SASL mech. Implementing this interface on the configured Authenticator (and on individual passdbs that carry verifiers in their backing store) lights up SCRAM-SHA-256 and SCRAM-SHA-256-PLUS advertisement in IMAP / POP3 / Submission.
LookupSCRAMSha256 returns:
- (creds, nil) — user exists and has a SCRAM-SHA-256 verifier. The SASL mech drives challenge-response from the returned StoredKey / ServerKey without ever seeing a plain password.
- (nil, nil) — user unknown OR stored credential is not a SCRAM verifier. The SASL mech fabricates a fake verifier so the exchange completes with a uniform auth-failed outcome and an attacker cannot probe for user existence via timing.
- (nil, err) — transient backend error. The session surfaces this as temp_fail.
Verifiers are produced by `yarilo-admin auth scram-verifier` (or any tool that emits the `{SCRAM-SHA-256}iter,salt,stored,server` blob).
type Scope ¶ added in v1.39.0
type Scope int
Scope classifies a field by its key prefix. Defined as a type so switch statements stay exhaustive when phase AUTH-2 PR 2 adds snapshot tracking.
const ( // ScopePassdb — passdb-side fields (no prefix). Crosses the // client wire as `key=value`. ScopePassdb Scope = iota // ScopeUserdb — fields prefetched by passdb for the userdb // layer. Crosses the client wire as `userdb_key=value` so // login pods can split the response into passdb / userdb // halves the way Dovecot's auth-client does. ScopeUserdb // ScopeInternal — internal-only metadata. Never crosses the // client wire. Used for state-tracking fields like // `auth_cache_key`, `auth_failure_attempted`, etc. in later // AUTH-N phases. ScopeInternal )
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server is the yarilo-auth TCP+mTLS server. Holds the passdb chain plus an optional userdb that enriches successful auth responses with userdb_* fields (Phase AUTH-2 PR 3) plus an optional masterdb chain + separator for master-user impersonation (Phase AUTH-3).
func NewServer ¶
func NewServer(passdbs []Passdb, opts ...ServerOption) *Server
NewServer creates an auth server with the given passdb chain. Optional userdb is attached via WithUserdb.
func (*Server) ListenAndServe ¶
ListenAndServe starts the auth TCP server. When tlsCfg is non-nil the listener uses mTLS (TLS 1.3, RequireAndVerifyClientCert). Blocks until ctx is cancelled; active sessions drain before the function returns.
type ServerOption ¶ added in v1.39.0
type ServerOption func(*Server)
ServerOption tunes a NewServer construction.
func WithCache ¶ added in v1.39.0
func WithCache(c *Cache) ServerOption
WithCache attaches an auth cache. handleAuth consults it before running the passdb chain — positive hit verifies the supplied password against the stored HMAC, negative hit short-circuits without password check. Misses run the chain and seed the cache with the result.
Nil cache disables caching. Pass the same *Cache instance to WithMasterCache on the matching MasterServer to expose CACHE-FLUSH for selective eviction.
func WithFailureDelay ¶ added in v1.39.0
func WithFailureDelay(d time.Duration) ServerOption
WithFailureDelay sets the timing-leak mitigation delay for client-visible auth failures (wrong password, unknown user, malformed SASL). The reply is held back by d before writing to the wire. Zero disables the delay (mostly for tests).
func WithInternalFailureDelay ¶ added in v1.39.0
func WithInternalFailureDelay(d time.Duration) ServerOption
WithInternalFailureDelay sets the delay applied when the failure is internal (passdb backend down, SQL refused, etc). Separate from WithFailureDelay because operators often want internal-error delays shorter than user-facing ones (the user will likely retry and another full failure delay would amplify outage symptoms).
func WithMasterUserSeparator ¶ added in v1.39.0
func WithMasterUserSeparator(sep string) ServerOption
WithMasterUserSeparator enables the `target<sep>master` workaround for SASL PLAIN clients that cannot supply authzid in the standard third-field position. Typical value is `*`. Empty disables the workaround — only RFC 4616 authzid is honoured.
func WithMasterUsers ¶ added in v1.39.0
func WithMasterUsers(enabled bool) ServerOption
WithMasterUsers is the top-level opt-in for master-user impersonation on the wire AUTH path. While false (the default) handleAuth ignores any SASL PLAIN authzid the client sends AND skips the separator workaround — every request routes through the regular passdb chain, indistinguishable from a build without master support. Set to true to expose the master flow at the wire level.
func WithMasterdb ¶ added in v1.39.0
func WithMasterdb(passdbs []Passdb) ServerOption
WithMasterdb attaches a dedicated masterdb chain. Master-user impersonation requests (PLAIN SASL responses where authzid is set and differs from authid) authenticate the authid against the masterdb first; if absent or not configured, the request falls through to the regular passdb and looks for a `master_user=yes` field on the result (per-user master flag). Either mechanism grants the impersonation.
func WithPenalty ¶ added in v1.39.0
func WithPenalty(store PenaltyStore, toSecs PenaltyToSecsFunc) ServerOption
WithPenalty attaches a cross-process penalty store. When set, handleAuth runs the following dance for every request:
- Lookup penalty for the remote IP, sleep toSecs(count) seconds (timing-leak protection equalises fast/slow paths regardless of penalty).
- Run the chain.
- On fail → Update(ip, count+1). On success → Update(ip, 0).
Nil store disables; nil toSecs disables the sleep but still records counters (useful for telemetry without enforcement).
func WithPolicy ¶ added in v1.39.0
func WithPolicy(checker PolicyChecker, mode PolicyMode) ServerOption
WithPolicy attaches a policy-server hook. The mode controls which lifecycle calls fire. Nil checker disables every hook.
func WithTokenStore ¶ added in v1.44.0
func WithTokenStore(ts TokenStore) ServerOption
WithTokenStore attaches the session token store. When set, every successful AUTH response carries a "token=<hex>" field that the backend must present via VERIFY before entering authenticated state. When nil, no token is issued and the VERIFY command always returns FAIL (standalone / test deployments that do not need the token handshake).
func WithUserdb ¶ added in v1.39.0
func WithUserdb(u Userdb) ServerOption
WithUserdb attaches a userdb backend to the Server. When set, every successful passdb result is enriched with userdb fields (with prefetch detection — see RunAuth). The userdb fields land in the response with the `userdb_` prefix preserved on the wire, so login pods can use them to set up the mail session without a separate master-protocol round-trip.
type Snapshot ¶ added in v1.39.0
type Snapshot struct {
// contains filtered or unexported fields
}
Snapshot captures the bag's current state so it can be restored later via Rollback. Used by Chain to isolate each passdb's mutations: snapshot before the call, rollback on ResultNext so the next driver sees a clean slate. The snapshot is read-only — every mutating method on Fields is safe to call between Snapshot and Rollback / discard.
Snapshot is O(n) in the current bag size (copies the keys, vals and idx maps); chain depth + bag size are both bounded by config so the cost stays trivial.
type TokenStore ¶ added in v1.44.0
type TokenStore interface {
Issue(username, sessionID, service string) (string, error)
Validate(tok string) (username, sessionID, service string, ok bool)
}
TokenStore is implemented by authtoken.Store. Abstracted as an interface so tests can provide a stub without importing the concrete package.
type UserInfo ¶ added in v1.39.0
type UserInfo struct {
// Username is the resolved canonical username this UserInfo
// describes. Mandatory.
Username string
// OriginalUser is the username the client supplied before any
// userdb-side translations (case folding, alias rewrite). Empty
// when no translation occurred.
OriginalUser string
// MasterUser is the master user when this lookup arrived via
// `master=` delegation. Populated only by Phase AUTH-3 master-
// user flows; the field exists in PR 1 so the schema is stable.
MasterUser string
// LoginUser is the post-impersonation effective user. For non-
// delegated lookups this equals Username; for master-user flows
// it is the target user. Phase AUTH-3 populates this.
LoginUser string
UID uint32
GID uint32
Home string
Chroot string
SystemGroupsUser string // override username for system group lookup
Groups []string // supplementary group names
ClientCertPresent bool // TLS client cert auth was used
// MailLocation overrides the global mail_location (Dovecot
// `mail` field).
MailLocation string
MailUID uint32 // distinct from system UID
MailGID uint32 // distinct from system GID
MailboxFormat string // maildir | sdbox | mdbox
MailAttributeDict string // dict URL for RFC 5464 METADATA
// VolatileDir is the VOLATILEDIR modifier extracted from MailLocation
// (or set directly via the `volatile_dir` userdb extra field). Carries
// the raw template string (%u/%n/%d/%h not yet expanded) so callers
// can expand it against the resolved home after userdb completes.
VolatileDir string
// IndexDir is the INDEX= modifier extracted from MailLocation (or set
// directly via the `index_dir` userdb extra field). Carries the raw
// template string (%u/%n/%d/%h not yet expanded). When set, per-folder
// index files (yarilo.index*, yarilo-acl) are stored here instead of
// co-located with the mailbox data under Home.
IndexDir string
// ControlDir is the CONTROL= modifier extracted from MailLocation (or
// set directly via the `control_dir` userdb extra field). Carries the
// raw template string (%u/%n/%d/%h not yet expanded). When set,
// per-folder control files (yarilo-uidlist, subscriptions) are stored
// here instead of co-located with the mailbox data under Home.
ControlDir string
// AltDir is the ALT= modifier extracted from MailLocation (or set
// directly via the `alt_dir` userdb extra field). Carries the raw
// template string (%u/%n/%d/%h not yet expanded). When set, messages
// that have been cold-tiered live under AltDir; reads check both
// primary (Home) and alt tiers.
AltDir string
// QuotaRules is the list of per-user quota rules (Dovecot
// `quota_rule=` can appear multiple times for nested roots).
QuotaRules []string
QuotaOverFlag string // operator-set override marker (string sentinel)
// AllowNets is the list of IP / CIDR strings the user is
// allowed to authenticate from. Empty means no IP restriction.
AllowNets []string
NoLogin bool // reject login outright
NoDelay bool // bypass auth-penalty backoff (Phase AUTH-4)
NoAuthenticate bool // auth disabled for this user
PassExpired bool // password expired — client must re-set
NoPassword bool // passdb without a password (e.g. OAuth)
Proxy bool
ProxyMaybe bool // proxy only when remote host differs
Host string // proxy backend host
Port int // proxy backend port (0 = default)
DestUser string // override target user in proxy
ProxyMech string // SASL mech to use against backend
ProxyTimeout int // seconds
ProxyRedirectReauth bool
ProxyNoPipelining bool
SSL string // yes | any | required
StartTLS bool
MailMaxUserIPConnections int // per-(user, IP)
MailMaxUserConnections int // per-user across IPs
Service string // IMAP | POP3 | LMTP | submission ...
LocalName string // SNI hostname the client connected to
// Forward carries `forward_*` fields that are passed through
// a proxy chain unchanged. Map key is the field name WITHOUT
// the `forward_` prefix.
Forward map[string]string
// Extra is the catch-all bag for fields not modelled as typed
// struct members. Anything a userdb driver returns that does
// not map to a typed field above ends up here, keyed by the
// raw field name. Wire-level prefix semantics (`userdb_*=`,
// `auth_*=`) land in Phase AUTH-2.
Extra map[string]string
// Password is the stored credential (or scheme-encoded hash)
// from a passdb lookup. The master-protocol wire serialiser
// in Phase AUTH-1 PR 2 strips this unconditionally so an
// admin-side USER request can never leak credentials. Present
// on the struct so passdb→userdb internal pipelines do not
// need a parallel type.
Password string
// CertName is the TLS client certificate Common Name. Set by
// the EXTERNAL SASL mechanism (Phase AUTH-5). Internal-only.
CertName string
// PolicyResponse is the verbatim body returned by the policy
// HTTP endpoint in Phase AUTH-6. Internal-only.
PolicyResponse string
}
UserInfo carries the full set of user fields a userdb lookup can return. Modelled after Dovecot 2.4's auth-fields plus the reserved field names every userdb driver writes (auth-fields.c, auth-request.c, userdb-passwd.c). The struct is intentionally comprehensive — adding fields later means breaking the protocol surface that pkg/authclient + yarilo-backend-api speak, so the shape is fixed up-front during Phase AUTH-1 and the prefix / snapshot / prefetch semantics in later phases extend BEHAVIOUR without touching DATA shape.
Zero values are the "field absent" marker; the master-protocol wire serialiser in Phase AUTH-1 PR 2 skips them. Sensitive fields (Password, CertName, PolicyResponse) are populated server-side but the wire layer filters them out unconditionally — they exist on the struct so internal pipelines can carry them without a parallel struct.
func ParseUserInfo ¶ added in v1.39.0
ParseUserInfo reconstructs a UserInfo from a list of wire-format `key=value` tokens (typically the tab-split portion of a master-protocol USER / PASS response after the leading `<verb>\t<id>\t<username>\t...`). The Username is set by the caller from the wire's username token; ParseUserInfo only fills the remainder.
func (*UserInfo) VisitFields ¶ added in v1.39.0
VisitFields calls fn for every non-zero typed field on UserInfo in canonical order. Internal-only fields (Password, CertName, PolicyResponse) are NEVER passed to fn — callers that serialise to a wire / bag get them stripped by construction.
Lists are emitted as a single comma-joined value; booleans are emitted only when true (value "yes"); numbers are decimal-rendered. Forward fields emit with the `forward_` prefix; Extra entries emit verbatim. Forward and Extra are iterated in lexicographic key order so two callers serialising the same UserInfo produce byte-identical output.
Shared by master.go's marshalUserInfo (no prefix, tab-joined wire form) and protocol.go's writeUserdbFields (`userdb_` prefix, Fields-bag form) so the field surface stays in lockstep.
type Userdb ¶ added in v1.39.0
Userdb is the contract every userdb backend implements. Unlike Passdb.Authenticate, Lookup takes no password — userdb answers "who is this user?" for admin tooling, master-protocol clients, and the prefetch shortcut (Phase AUTH-2) that lets a passdb short-circuit a redundant userdb round-trip.
Semantics on the return value match Passdb / Chain:
- (ui != nil, nil) — user found, ui populated
- (nil, nil) — user unknown in this backend; UserdbChain tries the next entry
- (nil, err != nil) — backend failure; UserdbChain stops and surfaces the error to the caller
type UserdbChain ¶ added in v1.39.0
type UserdbChain []Userdb
UserdbChain composes multiple Userdb backends with first-hit-wins semantics, mirroring the Passdb Chain. The first non-nil response from any backend in the chain is returned; an error from any backend short-circuits the chain (matches Dovecot's USERDB_RESULT_INTERNAL_FAILURE — "stop, do not try fallbacks").
type UserdbIterator ¶ added in v1.39.0
UserdbIterator is implemented by userdb backends that support listing every known user. Used by `yarilo-admin backend user iterate` and by the master-protocol `LIST` command. Optional — a backend that cannot enumerate (e.g. an LDAP search with no suitable filter) simply does not implement it.
type Validator ¶ added in v1.39.0
Validator parses a raw field value and returns its canonical string form. Returns an error when the value does not satisfy the field's type contract.
Canonicalisation: validators normalise equivalent inputs to a single representation. `Set("nologin", "TRUE")` then `Set("nologin", "yes")` then `Set("nologin", "1")` all land in the bag as `nologin=yes` — so two callers writing the "same" field produce byte-identical wire output.