Skip to content

v2.4.1

Choose a tag to compare

@github-actions github-actions released this 04 Oct 13:51
· 269 commits to develop since this release
e648424

yarilo 2.4.1

The first stable release. Until now the project has been a beta: the configuration keys, the on-disk format and the meaning of the version number could all move without notice. From this release they cannot.

yarilo is a mail server written in Go — IMAP, POP3, LMTP delivery, Submission, Sieve and ManageSieve, with JMAP alongside them. It runs as a set of single-purpose processes rather than one binary with modes, and every deployment shape is a configuration, not a build.

Upgrading from 2.3.x is not a drop-in. Read Upgrading from 2.3.x before you run helm upgrade: several defaults changed, and some configurations that loaded before now refuse to start.

What is stable, and what is merely present

Stable means the promises below hold. It does not mean everything is finished:

IMAP4rev2 / IMAP4rev1 complete, with the extension set listed in the README
POP3, LMTP, Submission complete
Sieve, ManageSieve complete, including the vendor extensions
JMAP partial
Other Users namespace not implemented

JMAP reads, synchronises and writes keywords: the session resource, Mailbox/{get,query,changes}, Email/{get,query,set,changes}, Thread/{get,changes}, SearchSnippet/get, Quota/{get,changes,query,queryChanges} (read-only), blob download and back-references, with real state strings. Email/set updates keywords only; create and destroy are refused with an answer naming what to use instead. Mailbox/set, blob upload and push are not implemented. Email/queryChanges and Mailbox/queryChanges answer cannotCalculateChanges by design, and canCalculateChanges is advertised as false.

Shared mailboxes work through owner-templated shared namespaces (user/%u/) and public namespaces. A namespace of type: other (other_users is accepted as an alias) is answered with NO and a reason; it needs cross-pod routing that is not built yet.

That is a statement about absence, not about breakage. What is served is under the same promises as everything else.

The three contracts

Configuration keys. A key does not change meaning. A key that is removed is answered rather than ignored: either it logs a warning at start saying what took its place, or, where silently ignoring it would change what the deployment does, the configuration refuses to start and the message says what to use instead. A rename keeps the old spelling working as an alias; both spellings set to different values refuse startup, both set to the same value are accepted. One pair is stricter: auth_allow_cleartext and disable_plaintext_auth are one setting with opposite senses, so carrying both refuses startup even when they agree.

On-disk format. Within a major version the format does not change, and the previous minor can read what a newer one wrote. That is what makes a rollback available during an incident; see Rolling back.

The version number.

  • a patch changes behaviour only where the old behaviour was a defect;
  • a minor adds a feature or changes a default, and a changed default is named in these notes under Upgrading. Keys keep their meaning, and two neighbouring minors read each other's data;
  • a major is anything a rollback could not read, or a key that changed meaning.

Upgrading from 2.3.x

Breaking changes

  1. Internal TLS is on by default. One switch, internalTLS.enabled: true; per-component flags are refused. The chart makes a CA and one certificate per role unless you give it internalTLS.ca.secretName or cert-manager. Each internal listener accepts only the roles meant to talk to it.
    • Upgrading a release that ran with internal TLS off is a short outage: while the rollout runs, new pods speak TLS and old ones do not, and calls between them fail until the last pod has restarted. Either plan it as a window, or set internalTLS.enabled: false for this upgrade and switch it on later in a window of your choosing.
    • GitOps renderers (Argo CD, helm template) cannot see existing Secrets: set internalTLS.ca.secretName to a CA made once outside the chart, or use cert-manager, or every render makes a new CA.
  2. Certificates without a role are accepted in this release only. A peer certificate whose SAN names no role is accepted and logged once per listener and certificate, so a rolling upgrade from one shared certificate keeps working. The next release refuses it.
  3. NetworkPolicy is on by default. The chart renders one ingress policy per internal listener, admitting only the roles that talk to it. It needs a CNI that enforces policies (Calico, Cilium, …); on one that does not, the policies are silently inert. Operator tools outside the chart's pods (a smoketest, yarilo-migrate) need networkPolicy.adminFrom; the MTA-facing and telemetry ports take networkPolicy.mtaFrom and networkPolicy.telemetryFrom (empty: open to everyone).
  4. The AUTH protocol changed on the wire, so all components must run the same version. The initial response is now base64, as the protocol always documented. During a rolling upgrade, a login proxy and yarilo-auth on different versions refuse each other's logins until both are on 2.4.1. Plan the upgrade as a window, or keep it short.
  5. An empty admin API token stops the service at start. backend_api and the director's admin API refuse to start without a token; running without one takes an explicit auth_disabled: true (local development only), and setting both is refused. The chart always sets a token, so a Helm deployment is unaffected; a hand-written yarilo.yaml may not be.
  6. A shared or public namespace with ACL off refuses to start. With acl.enabled: false every authenticated user could reach every mailbox of such a namespace. The message names the namespace and the key.
  7. sasl-login listens on 12345, not 12325. Point your MTA's smtpd_sasl_path at the new port in the same upgrade. auth_service.sasl_listen is gone; it refuses to load, and the message points at sasl_login.listen.
  8. lmtp_user_concurrency_limit: 0 refuses to start. The default is 10 concurrent deliveries per user, counted across the cluster through yarilo-warden and enforced by yarilo-lmtp-login at RCPT TO (451 4.3.0 past it). Unlimited is -1. With warden unreachable a delivery is accepted without the limit and a warning is logged.
  9. auth_allow_cleartext: false now refuses, where it used to only hide mechanisms. On a connection that is neither TLS, nor behind a trusted HAProxy, nor local, PLAIN and LOGIN are refused until STARTTLS. The default (true) is unchanged.
  10. Removed keys that now refuse to load: director_service.lmtp_listen, director_service.lmtp_backend_port, auth_service.sasl_listen, services.<listener>.connection_limit, telemetry.telemetry_pprof_heap_enabled. Each message names what replaced it.

Redis with a password, in this order

The chart now passes a Redis password to every process that opens Redis, from redis.passwordSecret (name, key) into YARILO_REDIS_PASSWORD; a Redis dict of your own takes password: "${YARILO_REDIS_PASSWORD}" in its settings. Do not put the password in a Redis URL: the URL is rendered into the ConfigMap as written.

To turn a password on without a window:

  1. create the Secret and set redis.passwordSecret, then upgrade. The clients now authenticate; a Redis that has no password yet accepts that (AUTH default <password> against a nopass default user);
  2. then set the password on Redis itself.

The other order locks every component out of Redis until the upgrade lands.

Behaviour that changed without a key

  • A message delivered without a Message-ID gets one (protocol.lmtp.lmtp_add_message_id, default true).
  • A deployment that set only protocol.submission.hostname was giving that name to the LMTP banner and to Received as well; those now take the installation's hostname, which defaults to each process's own host name until you set it. Set hostname on a cluster.

New configuration in this release

Every key helm/values.yaml gained since 2.3.266, by section, with its default. The documentation site has the full reference for each.

Top level and chart

Key Default What it does
hostname the host's own name What this installation calls itself: the domain of a synthesised Message-ID, the LMTP banner, Received. Set it on a cluster.
config_schema_version, chart_version written by the chart Chart-managed; do not set. A process warns when its ConfigMap was rendered by an older chart, and names the settings that are being defaulted.
internalTLS.enabled true Internal mTLS with per-role certificates (above).
internalTLS.ca.secretName — A CA Secret made outside the chart; without it and without cert-manager the chart makes one.
internalTLS.certManager.enabled, .issuerRef.kind, .issuerRef.name false, Issuer, — One cert-manager Certificate per role instead of chart-made Secrets.
internalTLS.certGeneration 0 Raise to re-issue every chart-made certificate.
networkPolicy.enabled true Ingress policies per internal listener (above).
networkPolicy.adminFrom, .mtaFrom, .telemetryFrom [] Peers outside the chart for the admin, MTA-facing and telemetry ports; empty mtaFrom/telemetryFrom admit everyone.
redis.passwordSecret.name, .key —, password The Redis password, from a Secret (above).
virtual_definitions {} One file per mailbox of a shared virtual namespace, mounted read-only; a namespace must point at them.
namespaces[].mailboxes — Mailboxes a namespace creates or subscribes on its own (auto: create / subscribe).

Storage

Key Default What it does
storage.mail_fsync optimized What reaches the disk before a delivery is answered: never, optimized (the body) or always (plus index, list and directory).
storage.storage_lock_method flock How a write to a shared file excludes another writer: flock, fcntl (NFS with NLM) or dotlock.
storage.storage_lock_stale_timeout 180 Seconds an unchanged dotlock is left before a waiter takes it over; -1 never.
storage.mail_cache_purge_min_size 32k A cache file smaller than this is never purged on its own; 0 purges any size.
storage.mail_cache_purge_delete_percentage 20 Expunged records against live ones, in percent, that purges the cache.
storage.mail_cache_purge_continued_percentage 200 Continued records against live ones, in percent, that purges the cache.
dicts.metadata.settings.lock_method — (flock) Write lock for the annotations file; match storage_lock_method.

Protocols

Key Default What it does
protocol.lmtp.add_message_id true Synthesise a Message-ID for a message that arrives without one; an existing header is never rewritten.
protocol.lmtp.lda_mailbox_autocreate false A delivery to a folder that does not exist makes it.
protocol.lmtp.lda_mailbox_autosubscribe false Subscribes a folder lda_mailbox_autocreate made; refused without it.
protocol.lmtp.quota_full_tempfail false A full mailbox answers 452 4.2.2 (retry) instead of 552 5.2.2 (bounce).
protocol.lmtp.lmtp_max_recipients 0 (unlimited) Recipients per LMTP transaction, advertised as LIMITS RCPTMAX.
protocol.lmtp.proxy.lmtp_proxy_timeout 125 lmtp-login's cap in seconds on reaching a backend and finishing one delivery; past it the MTA gets 451.
protocol.managesieve.managesieve_max_line_length 65536 Bytes in one ManageSieve command line or literal, a script's body aside (that is sieve_max_script_size); a larger literal is refused before it is read.
login.login_proxy_timeout 30 Seconds a login proxy may spend reaching a backend and bringing the session up; a userdb proxy_timeout overrides it per user.

Components

Key Default What it does
components.auth.auth_max_attempts 3 Failed authentications allowed on one connection before it is closed.
components.auth.wait_pool_size 64 Connections kept for waiting lock acquires.
components.director.director_domain_expire 900 Seconds a domain keeps its backend with no session on it (domain assignment policy).
components.director.director_domain_rebalance_percent, director_domain_rebalance_interval, director_domain_rebalance_cooldown 20, 60, 600 When a domain moves from the busiest backend to the quietest, how often that is judged, and how long a moved domain is left alone; 0 percent never moves one.
components.quotaStatus.quota_status_success, quota_status_toolarge, quota_status_overquota OK, — (the over-quota action), 554 5.2.2 %{error} quota-status answers for a recipient that fits, a message that is too large, and a full mailbox.
components.warden.service.event_queue_size 4096 Warden's queue of session events; when full, the oldest are dropped and counted.
components.dict.enabled, replicas, listen, max_conns true, 1, :9107, 8 The dict service and the connections a session keeps to one named dict.
components.dict.internalTLS.secretName, resources, affinity, nodeSelector, tolerations chart-managed / empty Pod settings of the dict service.

Rolling back

Two routes, and they are not the same:

  • helm rollback moves the chart and the image together;
  • --set image.tag=<older> moves the image alone, leaving the newer chart in place, so an older binary receives keys it does not know.

A rollback to 2.3.x is verified after this release, not before it, and this section will carry the result: what the candidate wrote, what 2.3.266 read back, and the digests before and after. Until then, treat a rollback across this upgrade as untested. Two things are already known to stand in the way of 2.3.266:

  • with redis.passwordSecret set and a password on Redis, 2.3.266 cannot authenticate to Redis;
  • IMAP METADATA annotations now live in a file in each user's home (%h/yarilo-metadata.json, the metadata dict); 2.3.266 reads its old shared location and will not see them. Nothing is deleted.

Changes since 2.3.266

Every commit: v2.3.266...v2.4.1.