FusionDirectory's image runs PHP 7.4, which has been out of security
support since 2022, and upstream ships no PHP 8 build. The runtime
health check of #1265 therefore reports every deployment of it, and in
CI a red FusionDirectory job makes the matrix's fail-fast cancel every
other job of the run (seen on #1265's run 41101611445: one red job, 79
cancelled). There is nothing to fix on our side short of upstream
shipping a supported runtime.
- meta/services.yml: lifecycle beta -> eol. Default CI (tested envelope
alpha..maintenance) no longer deploys the role, the updater and the
image mirror skip it (mirrored image set 188 -> 187).
- README.md: end-of-life banner as lifecycle.md requires for eol: the
project neither maintains nor tests the role, operators use it at
their own risk, upstream FusionDirectory for support.
- Root README.md: make docs drops the role from the generated table of
the tested lifecycle envelope.
Production deploys of the role keep running the runtime check, so an
operator who still deploys it sees the PHP 7.4 finding.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main CI run 40976399687, socialhome#0 (swarm, tor, centos): the CLI tests
of svc-net-tor (onion:25 "socks CONNECT failed with reply 0x01") and
web-app-stalwart (probe_mail relay: ConnectionRefusedError on port 25)
failed. dockerd on the manager had aborted the whole ingress program of
the mail stack:
Failed to add ingress: failed to program ingress ports:
failed to bind host port 0.0.0.0:993/tcp: address already in use
/proc/net/tcp named the squatter: 192.168.244.10:993 -> :2049, the
node's own NFS client. The mount carries no noresvport, so the kernel
drew a source port from the 665-1023 reserved window and landed on
imaps. None of 25/465/993/995/4190/8095 got a DNAT rule or a
docker-proxy listener while every other published port did, and
stalwart still reported healthy because its healthcheck runs inside the
container. 993 and 995 are the only published ports inside that window,
so this is luck of the draw on any NFS-backed swarm host that runs the
mail role.
mount_opts now emits noresvport. insecure on the kernel-flavor export is
the other half: kernel nfsd defaults to secure and answers a mount from
an unprivileged source port with EACCES. It reaches the kernel flavor
only; ganesha already accepts unprivileged clients.
What it costs: the next deploy of every existing NFS client sees a
pending opts change and remounts the shared state; a host-side job
holding that path at that instant fails loudly. A plain remount may keep
a running mount on its old reserved port until the next real mount.
Port of f46c4d0bab from feature/store-i18n, applied identically so the
later store-i18n -> main merge stays conflict-free.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeQL py/unused-global-variable flagged two test package inits
(code-scanning alerts #1068 and #1072):
- tests/unit/python/roles/web-app-fider/__init__.py exports PROJECT_ROOT
to test_tenant_bootstrap_gate.py through `from . import PROJECT_ROOT`
but declared no __all__. Declare it, as every other test package
that re-exports PROJECT_ROOT does.
- tests/unit/python/roles/sys-front-tls/__init__.py defined a
PROJECT_ROOT nothing imports: the only test lives in templates/, whose
own __init__.py provides it. Leave the role-level package init empty,
like the other role-level test packages.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mailu is superseded by web-app-stalwart and the project no longer
maintains it. Its OIDC flavor runs the archived heviat/Mailu-OIDC images
(the webmail ships Roundcube 1.6.9 and PHP 8.3.15), and its unbound
sidecar fails DNS since the root KSK rollover (#1303). Rather than fix
a role on its way out, mark it end of life:
- meta/services.yml: lifecycle deprecated -> eol. The updater and the
image mirror now skip the role (the mirrored image set drops from
189 to 188 entries), and default CI keeps excluding it as before.
- README.md: the banner now states that the project neither maintains
nor tests the role, that operators use it at their own risk, and
links upstream Mailu for support, as lifecycle.md requires for eol.
The migration pointer to web-app-stalwart stays.
- web-app-stalwart README: "deprecated" becomes "end-of-life".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hlth-runtime set system_service_force_flush_final: false. In debug mode
(CI) that makes sys-service start the unit as soon as the role is
included, so whether the check saw the app containers depended on where
the role landed in the play: in CI run 41023277637 SuiteCRM#0 and
Nextcloud#4 had their containers up first (suitecrm started 11:46:28,
check 11:49:02), while FusionDirectory#0 in the run before checked
before its containers existed and passed without inspecting anything.
In production (SYS_SERVICE_ALL_ENABLED false) the unit never ran during
a deploy at all, only from its timer.
Put it on the final run list like every other health check that
inspects containers (hlth-container, hlth-volumes, hlth-csp,
hlth-webserver, hlth-disc-space, hlth-btrfs): sys-service-terminator
starts it once all apps are deployed, so a runtime below its floor
fails the deploy deterministically.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Addresses two CodeRabbit findings on PR #1261.
1. Scoped IPv6 (thread 4421596356): ipaddress.ip_network accepts a scope
ID, so `fe80::1%eth0` resolved to `fe80::1%eth0/128` and the vhost
would carry `allow fe80::1%eth0/128;`, which nginx refuses to load.
kc_admin_networks now raises AnsibleFilterError for any entry whose
network address carries a scope ID. test_admin_allowlist.py covers a
scoped address and a scoped CIDR; both fail against the previous
filter.
2. Module docstring (thread 4421596351): removed; it described the
module instead of documenting parameters or a trip-wire.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Compose gates web, worker and cron on the seeder via depends_on
service_completed_successfully. Swarm ignores depends_on, so nothing
waited for the one-shot seeder there. In CI (store-i18n run 40979252521,
OpenProject#0 swarm) an overlay flap reset the seeder's Postgres
connection; Postgres rolled back RootSeeder's transaction, but the
seeder never noticed and sat idle on the dead socket forever
("Executing database seed..."). No UserCustomFieldSection rows were
committed, so /my/account rendered without fieldsets and the deploy
still reported green.
- 01_manager_ops.yml: in swarm, after the handler flush deploys the
stack, poll job_state.sh on the seeder service until its newest task
is Complete (fail on Shutdown, give up after 20 minutes), mirroring
web-app-zammad's init-job wait. Compose keeps relying on depends_on.
- env.j2: add libpq TCP keepalives (idle 60s, interval 10s, count 6)
and tcp_user_timeout=120000 to DATABASE_URL, so a connection whose
peer vanished errors out within about two minutes instead of hanging.
The seeder then exits non-zero and swarm's on-failure restart policy
reruns the seed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rsync 3.5.0 (Debian security update 3.5.0+ds1-0+deb13u1) refuses a
destination symlink owned by neither root nor the calling user.
svc-opt-ssd-hdd turns a volume's `_data` into a symlink to /mnt/hdd or
/mnt/ssd. The restore drill empties the volume and runs `compose create`;
docker repopulates it and lchowns the `_data` symlink to the image
directory's owner (e.g. www-data). The restore then failed with
"refusing to follow a symlink owned by an untrusted user" and
"change_dir#1 .../_data/ failed: Too many levels of symbolic links",
the drill left its subjects down, and the dependent checks timed out
(CI run 40979252521, ssd-hdd#0, on feature/store-i18n; recover.py is
identical on main).
VolumeRecovery now resolves the mountpoint before handing it to rsync:
os.path.realpath locally, `ssh <host> realpath -e -- <mountpoint>` for
--docker-host, so rsync writes into the real directory and never meets
the symlink. `--insecure-links` was not used because older rsync builds
on other distributions lack it. Two new unit tests (local symlinked
mountpoint, remote resolution); both fail against the previous code.
Root cause and fix by the i18n session (feature/store-i18n triage of
ssd-hdd#0), ported to main.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shared OpenResty nginx.conf took worker_processes and worker_connections
from WEBSERVER_WORKER_*, which are the min() of svc-prx-openresty's limits and
the current play's application_id. The file renders once, in the scope of
whichever role loads the proxy first, so that role's limits capped every vhost:
on feature/store-i18n web-svc-mirror (pids_limit 64) loaded it first, every
vhost got worker_connections 64, and openresty raised 555 "64
worker_connections are not enough while connecting to upstream" alerts while a
single openwebui page fires ~150-170 chunk requests (CI run 40979252521,
homeassistant#1). Main carries the same wiring.
Add WEBSERVER_PROXY_WORKER_PROCESSES and WEBSERVER_PROXY_WORKER_CONNECTIONS,
computed from svc-prx-openresty's own openresty service only (same
resource_filter fallbacks, at least one worker), and render nginx.conf from
them. WEBSERVER_WORKER_* keep pairing the proxy with the play for app-internal
nginx such as Nextcloud's. The values can only stay equal or rise.
Found by the i18n session's triage of homeassistant#1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both deploy steps set `disable: ${{ matrix.disable || inputs.disable }}`.
A row carries tokens of its own whenever its network mode needs them (a
clearnet row disables Tor), so the caller's list never reached the
deploy. On feature/store-i18n (CI run 40979252521) the workspace swarm
zombie for svc-db-mariadb/postgres deployed 19 apps instead of 7 on its
clearnet row (fedora: 4h06 for one step, job cancelled at 345 min). main
carries the same expression; it bites as soon as a caller's disable list
meets a clearnet row.
The steps now pass format('{0} {1}', matrix.disable, inputs.disable);
every reader of `disable` splits on spaces and commas. Both input
descriptions follow. test_deploy_disable_merge.py reads the live
expressions; against the old fallback it fails two subtests.
Ported from feature/store-i18n 57e574ac2d (store-i18n's extra `network:`
step env stays there).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The role builds akaunting/docker at master. Upstream moved that
Dockerfile to `FROM php:8.2.35-apache` (akaunting/docker#123, merged
2026-10-26), a release with the fixes for CVE-2026-109618 and
CVE-2026-109619, so the to-do that asked to move off PHP 8.1.34 no
longer describes the image the role builds.
Closes#1119
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4a56b4c19c set services.element.force_verification to false only for
the SSO variants 0 and 2, on the reasoning that only the SSO specs log
the same account in twice. The deploy's second pass logs the variant 1
administrator (ansible flavor, SSO off, password login) in again. PASS 1
bootstraps the account's cross-signing identity, so in PASS 2 Element
stops on "Confirm your digital identity" ("Use another device" / "Can't
confirm?" / "Remove this device", no skip), and the password-login spec
times out at _shared.js:436 (fork run 40979252521, job 123823820263,
Discourse#1 with web-app-matrix variant 1, all three retries).
Variant 1 now sets force_verification to false like the other two. The
services.yml default stays true, so production keeps forced
verification. No variant exercises true end to end any more; the
follow-up issue restores one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1259 moved the interim CVE-2026-109618 mitigation (memory_limit = -1)
out of the shared hardening.ini into memory-unlimited.ini and mounted it
only in the roles whose images it had inventoried. Its branch predates
#1263, which gave FusionDirectory the php_hardening mount, so after both
merged FusionDirectory ran with the image default of 128M again.
A conf.d mount would not have fixed it: the nfrastack image writes
php_admin_value[memory_limit] into its FPM pool
(/etc/php7/pools.d/www/www-settings.conf) from PHP_MEMORY_LIMIT, and a
pool php_admin_value outranks every conf.d ini. Measured on main: CLI
128M, FPM 128M. So even before #1259 the hardening ini only ever lifted
the CLI limit.
nfrastack/fusiondirectory:1.5 runs PHP 7.4.33 on Alpine 3.15, end of
life and never patched, so the role carries the mitigation until it
moves to a PHP 8 image.
- Set PHP_MEMORY_LIMIT=-1 in templates/env.j2. The image seds it into
php.ini and the pool, so CLI and FPM both report -1.
- The root TODO lists FusionDirectory with the image and the reason it
uses the variable instead of the mount; the role TODO names which
setting carries which CVE mitigation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At BBB boot, bbb-graphql-server's start.sh runs pg_terminate_backend and
drops bbb_graphql. When that kills apps-akka's first HikariCP connection
while the pool detects the driver's default transaction isolation,
HikariCP 4.0.3 swallows the 08xxx error, marks detection as done and
keeps isolation -1 for the pool's lifetime. Every later connection setup
then calls setTransactionIsolation(-1), which pgjdbc rejects with
"Transaction isolation level -1 not supported", so meetings never reach
the database and the client shows "Oops" (about 1 in 8 boots).
Pin the isolation explicitly so HikariCP never falls back to the
detected value: slick-hikaricp reads postgres.transactionIsolation,
prepends TRANSACTION_ and hands it to HikariCP, hence the value must be
exactly READ_COMMITTED (the PostgreSQL default, so normal boots behave
as before). The flag goes into apps-akka's own compose environment as
JAVA_OPTS, which the sbt-native-packager start script honours, instead
of env.j2, because compose_mods pins env.j2 onto every bbb service and
would hand the flag to bbb-web and fsesl-akka as well.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Addresses three CodeRabbit findings on PR #1265.
1. LAM hardening invisible to the probe (thread 4413764929): the runtime
check probes PHP with `php -r`, the CLI SAPI. web-app-lam mounted the
hardening ini only into /etc/php/8.4/apache2/conf.d, which the Debian
CLI does not scan, so the probe read unhardened CLI settings and the
check failed for LAM. The php_hardening entry now also mounts the ini
into /etc/php/8.4/cli/conf.d. No other role mounts it into a Debian
per-SAPI directory; the rest use the shared scan dir of the official
image or of Alpine. The TODO entry now names the remaining gap: the
probe reads only the CLI copy.
2. Explanatory comment above HEALTH_RUNTIME_FLOORS (thread 4413764949):
removed. The README already states the end-of-life rule and now also
defines a floor as the lowest patched release of each line.
3. `apk --no-cache add` missed (thread 4415018662): PACKAGE_INSTALL_PATTERN
required `add` right after `apk`, so `RUN apk --no-cache add php7`
passed as an image without PHP. The apk branch now accepts options
before `add`, like the apt branch. New tests cover the option form and
guard that `apk` in a path or a comment is no package install.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>