Both mail providers published "v=DMARC1; p=reject; ruf=...; adkim=s;
aspf=s" without a rua tag, so the domain owner received no aggregate
reports and could not see who sends in the domain's name or whether
legitimate mail fails alignment.
services.<entity>.dmarc.rua_user in web-app-stalwart and web-app-mailu
now names the users-catalogue key whose mailbox receives the aggregate
reports. It is set to administrator, the one user every deployment
provisions with a mailbox (user-administrator), and an inventory may
name another key. The address is built as <username>@<mail domain>,
which is the exact account or alias the provider creates, so the record
never points at an address the server does not serve. An assert in the
DNS task fails the deploy before any record is written when the user
carries no mailbox account (Stalwart), or neither a mailbox account nor
a forward alias (Mailu), so reports cannot bounce. Mailu's DMARC_RUA
env, which only feeds the record its admin UI suggests, now carries the
same local part instead of the unprovisioned "admin".
production_facts.py takes a required --dmarc-rua. check_dmarc parses
the record's tags and fails unless rua names that mailbox (size
suffixes and several URIs are accepted). The policy check now reads the
p tag instead of matching the substring "p=", which "sp=" also matched.
Closes#1125
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stalwart v1.0.0 rejects unknown JMAP properties ("Invalid property" ->
notCreated/notUpdated), validates OIDC tokens more strictly and refuses to
open a data store from before 1.0. Port the role and protect existing
installs:
- Milter: hostname/timeoutConnect/timeoutCommand/timeoutData become
host/connectTimeout/commandTimeout/dataTimeout; the existing milter is
looked up by host.
- Onion relay route: address becomes host.
- OIDC directory: requireAudience is gone. Set skipAudienceValidation and
unscopedTokens=interoperable on create and update, so Keycloak tokens
(no Stalwart audience, no mail scope) keep working for the WebUI and
Roundcube XOAUTH2.
- WebUI: send oauthClientId only with SSO enabled; without SSO the WebUI
falls back to its own client metadata document.
- Store guard: before anything changes, a running Stalwart whose table u
still has the 0.16 bigint layout stops the deploy and points to the
stalwart-migrate steps, which the README now documents under
"Upgrading from 0.16".
- Updater: pin the WebUI git_tags source to v2.* to match server 1.x.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stalwart server v1.0.0 and WebUI v2.0.0 only work as a pair:
- WebUI v2.0.0 declares minServerVersion 1.0.0.
- Server v1.0.0 rejects a bundle without the manifest that only WebUI 2.x
ships ("Application bundle does not contain a manifest").
The separate bumps in #1227 (server only) and #1223 (WebUI only) were
each broken on their own, so this commit carries both.
WebUI v2.0.0 also dropped the hardcoded 'stalwart-webui' fallback client
id. Without an oauth-client-id meta tag (or CIMD support at the IdP) it
shows "Sign-in is not configured" and never redirects. That is the
test-sso.js failure in Jenkins#0 of #1223. Set the Application record's
oauthClientId to the configured Keycloak client so the server injects the
meta tag on unpack.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Proxy:
- server_tokens off, X-Powered-By stripped from upstream responses,
HSTS max-age raised to one year.
- No global X-Content-Type-Options: under nosniff, ORB (step 3.4)
blocks no-cors cross-origin responses typed text/html, e.g. tracking
beacons answered with an empty 204.
PHP (CVE-2026-109618, CVE-2026-109619):
- New role sys-svc-compose-php installs a shared ini (expose_php Off,
session.upload_progress.enabled 0, fatal_error_backtraces 0 for
PHP 8.5, interim memory_limit -1) and a PHP-FPM [www] drop-in
(pm.max_requests = PHP_FPM_MAX_REQUESTS) once per stack host,
before 04_files renders any compose file.
- The host file names carry a sha1 prefix of the controller-side
source, so a content change alters every PHP app's compose.yml and
recreates it, also on remote deploys where compose_volumes cannot
read the stack host file and hashes the path instead.
- 20 PHP roles mount the ini as a compose/swarm config into their
image's scan dir; moodle, nextcloud and shopware also mount the FPM
drop-in. Magento keeps its own pool: its php-fpm.conf has no
include= and is rendered without notify.
- Joomla's php-upload.ini now loads as zzz-joomla-uploads.ini so its
own memory_limit wins: the media API compares Content-Length with
toBytes(memory_limit) and rejects every upload at -1.
TODOs record the memory_limit revert condition and accepted
trade-offs, Akaunting on PHP 8.1, FusionDirectory on PHP 7.4 and the
public Keycloak admin console.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Stalwart bans an address for good once it passes a failure rate: the four
*BanPeriod fields of its Security settings are null, and block_ip stores a
ban without a period with expires_at.unwrap_or(u64::MAX). Many clients reach
Stalwart through one address (the reverse proxy, published ports, Tor, the
swarm ingress), so one burst of failed logins locked that path out until
someone deleted the BlockedIp entry by hand.
The role now sets authBanPeriod, abuseBanPeriod, loiterBanPeriod and
scanBanPeriod to services.stalwart.ban_period_seconds, one hour by default,
and reloads the settings, as Stalwart's own security test does after it
changes a ban period. It reads the settings first and leaves them alone when
they already match, so a redeploy changes nothing.
Closes#916
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Under SSO the role tried to let applications relay without SMTP AUTH by
widening the relay rule to the private networks. The rule compared the
peer address to the text of a CIDR, which never matches, so an
application could reach local mailboxes but got "550 5.1.2 Relay not
allowed" for every external recipient. Repairing the comparison would
have opened a relay, because published ports, Tor and the swarm mesh all
hand outside clients an address inside those networks.
Stalwart checks app passwords before it asks the external directory, so
a bot can authenticate while the directory is OIDC. With SSO enabled the
role now creates one app password per bot, acting for that account as
the recovery admin, and stores it in the token store the SMTP password
is already read from. A bot gets a new one when its entry is deleted in
Stalwart or its token is removed.
The relay rule is back to authenticated sessions only. The lookup loses
its SSO relay branch and the role its submission_via_relay flag, so an
SSO deployment submits on 465 with AUTH like every other one.
The role's CLI test now fails when an unauthenticated session is given
an external recipient, or when the bot credential is refused one.
Closes#793
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Docker updater watches image plus version, the repository updater
repository plus ref. A dozen pins fell outside both - an app_version, a
release, a tag in a registry neither speaks - so nothing ever told anyone
they had gone stale.
Such a pin now names its upstream next to itself in an `update:` block:
git_tags, registry_tags, npm, http_regex or a role-local script, with
`match` selecting a tag flavour and `strip` removing a prefix the pin does
not carry. utils/update/source.py resolves them beside the two existing
updaters, `python -m cli.contributing.update.source` bumps them, the
update-version-sources job opens one PR per role, and
tests/external/update/source warns while a bump is pending.
Two lints hold the rule: every semver pin is watched by one of the three
updaters or opted out with a reason, and a semver only ever sits behind
`version`, `<what>_version` or `ref` - which renames moodle's release to
app_version and simpleicons' local_version to local_tag, the latter being
the tag of a locally built image rather than an upstream version. A
declaration naming a key the entity does not carry, or a type the resolver
does not know, fails the lint instead of resolving nothing in silence.
Picked from feature/store-i18n (94d607fd0a) onto current main. Two things
differ from the original: svc-ai-lmstudio no longer carries an app_version
pin, so its declaration is dropped, and web-app-stalwart's webui_version,
added since, declares the git tags of the webui repository.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Deprecated roles are no longer tested, but Stalwart variant 1 still switched its mailu service key on so CI co-deployed Mailu for the migration e2e. The override and the static mailu key, whose only purpose was that pull, are gone, and the generated cosmos drops the mailu node.
The migration gate keeps only the check that services.stalwart.migration.import_mailu stays declared and off by default. The operator-side import (12_import_mailu.yml, migrate_from_mailu.py) still runs when web-app-mailu is in the inventory; the e2e harness under files/test/ and templates/test.env.j2 now skips itself and is left for removal.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>