Fider v0.39.0 accepts an OAuth redirect only when its origin equals
OAuthBaseURL (isKnownOAuthOrigin compares u.Scheme + "://" + u.Host with
it), and in single-host mode OAuthBaseURL is BASE_URL unchanged. The tls
lookup's url.base ends with "/", so the comparison never matched and
every Keycloak login was rejected. Main run 41925343136 (2df12002c0),
Fider#0 compose clearnet centos/ext4: playwright.spec.js:56 and :88 find
no .c-menu-user after SSO, and the rescue fider.log shows 11x "OAuth
callback state for provider _keycloak points at an unknown origin.
Aborting sign in process." The image is the mirrored getfider/fider
stable, refreshed 35 hours before the run.
The tls lookup now also returns url.origin, the base URL without its
slash (the lookup hunk of 44aaa68924 on feature/store-i18n), and the
Fider env passes it as BASE_URL (d531d35823 there). url.base keeps its
value, so no other consumer changes, and both lines match the
store-i18n side for a conflict-free merge.
| 🎯 Fix | 💥 Break | 🌱 Efficiency | ⚡ Speed | 🛡️ Security | 🏆 Score | Gate |
| --- | --- | --- | --- | --- | --- | --- |
| 88 % | 3 % | – | – | ~100 % | 85 | ⛔ fix 88 % < 90 |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fork run 41911075483 (feature/store-i18n 2fd6c6d310) lost both workspace
jobs, debian:13 and ubuntu:26.04, in make security-apparmor-teardown:
common.sh: line 75: /proc/sys/kernel/apparmor_restrict_unprivileged_unconfined: Read-only file system
Inside the workspace container /proc/sys is mounted read-only, and the
write that #1371 added ran under set -e. A sysctl that cannot be written
now leaves the restriction as it is with a log line instead of failing
the teardown, and only a successful write records the previous value.
Restore keeps its record when it cannot write the sysctl back.
The tests call the helpers under set -euo pipefail like teardown.sh and
use /proc/sys/kernel/ostype as a sysctl nobody can write; both new tests
fail against the previous common.sh.
| 🎯 Fix | 💥 Break | 🌱 Efficiency | ⚡ Speed | 🛡️ Security | 🏆 Score | Gate |
| --- | --- | --- | --- | --- | --- | --- |
| 95 % | 3 % | – | – | ~100 % | 92 | ✅ 90/5, efficiency and speed <= 100, security 100 % |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
followed_tags caught OSError, ValueError, KeyError and RuntimeError, but
http.client.IncompleteRead, BadStatusLine and LineTooLong derive from
http.client.HTTPException only. One truncated digest response therefore
escaped to collect_updates (docker.py:344, no try) and aborted the whole
Docker update run instead of printing the follow warning for that image.
Every other fetch site of the updater already catches HTTPException
(docker.py, pip.py).
Reported by CodeRabbit on #1370.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tests/lint/ansible/roles/test_file_spot.py rejects a hard-coded
"roles/a/tasks/main.yml" in test_stage.py and asks for the
ROLE_FILE_TASKS_MAIN constant. The fixture only needs some nested file,
not a role, so it uses pkg/a/data.yml and pkg/a/.git instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On runner image 20261101 kernel.apparmor_restrict_unprivileged_unconfined
is on, so the kernel turns runc's change_onexec into the stack
docker-default//&unconfined. docker-default grants no signal to an
unconfined peer, and every in-container kill() is denied; run 41743807934
lost four package builds that way (journal: apparmor="DENIED"
operation="signal" comm="rsync" signal=usr2).
teardown.sh now writes 0 to the sysctl and records the value it found
under /run/infinito; restore.sh writes that value back and drops the
record. A missing sysctl or one already at 0 is left alone, and a second
teardown keeps the first record. CI runs teardown before every deploy,
workspace and dns job; dev hosts through make environment-bootstrap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run 41743807934 lost Matrix#2, Workspace (debian:13), Gitea#0 and Wazuh#0
to the full job budget right after "Preparing build workspace". On runner
image 20261101 the kernel stacks every container onto
docker-default//&unconfined, which refuses every in-container kill(); the
host journal logs apparmor="DENIED" operation="signal" comm="rsync"
signal=usr2. rsync's local copy waits for exactly that SIGUSR2 and sleeps
until the job is killed.
scripts/install/stage.sh copies the tree through a tar pipe, which sends
no signal; --format=posix keeps nanosecond mtimes. package.sh uses it on
the arch and debian paths and installs tar instead of rsync.
Copying this repository both ways gives identical content, modes, types,
link targets and mtimes on all 11306 entries. test_stage.py runs stage.sh
under a seccomp filter that fails every signal syscall with EACCES; the
old rsync copy hangs past its timeout under the same filter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Update run 41722395224 proposed no sys-os bump although fedora:latest
points at 45 and a local dry run proposes 44 -> 45. followed_tags turned
every failed digest lookup into "no candidate" without a word, so the run
cannot tell a rate limit from a pin that is current.
tag_digest now retries an HTTP 429 twice with a short backoff, as the tag
listing does, and followed_tags reports on stderr what it compared and
which tags it kept, or the error that made it propose nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_context.sh runs under set -euo pipefail and tested membership with
`printf '%s\n' "${_names}" | grep -qxF <name>`. grep -q exits on the first
match; when printf still has lines to write it dies of SIGPIPE (141), and
pipefail turns the matched test into false. resolve_db_service then fell
through to the central <dep>_<dep> name or printed nothing, and
has_swarm_service reported a deployed service as missing, so
skip_if_no_swarm_service let the converge and chaos gates exit 0 untested.
The race grows with the stack's service count and with CPU load (store-i18n
gate pass 11: 'mariadb_mariadb' != 'magento_database').
The three tests read the listing from a here-string, which is no pipeline.
Two regression tests put the match first in a 10000-name listing and fail
on every run against the old helper. Root cause found by i18n 2.0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DEV_PYTHON_REQUIRES is shorter than the old name, so ruff format joins the
dict comprehension in test_install.py onto one line, and the CI format
check would report the drift.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main moved buildkit to v0.34.2 and ollama to 0.40.14. Both image_tag
suppressions named the old tags, so they no longer apply and the
suppression lint rejects them; they are dropped, as the docker updater
does on a bump. Whether the new tags still carry CRITICAL findings shows
in the next scan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The runtime image pins lived in meta/distros.yml, which neither the docker
updater, the image mirror nor the vulnerability scan reads, so a pin would
only ever move by hand. The new role sys-os carries them in its
meta/services.yml under the distro id; utils/distros.py renders the
workspace matrix from there with the stdlib block reader, and
meta/distros.yml keeps the distro attributes only.
A new release now arrives as an updater pull request. follow: latest holds
debian, ubuntu and fedora to the release their latest tag points at, by
Docker Hub digest: Ubuntu moves between LTS releases only (26.10 shares the
digest of rolling, not latest) and Fedora skips rawhide (46 shares the
digest of rawhide, latest is 45). Arch stays rolling; CentOS stream tags
carry no number the updater can order.
dockerhub_repo and is_dockerhub move to utils/docker/image/ref.py beside
split_registry_and_name, so utils/update/follow.py can use them without an
import cycle and utils/update/docker.py stays under the size cap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1347 passed the supported Python range to the remote install.sh as
INFINITO_PYTHON_REQUIRES. Every INFINITO_* name is a key of the generated
.env, so test_every_referenced_infinito_key_is_registered fails on main
for this one, which no registry entry can carry: its value is derived
from pyproject.toml's requires-python, the single source, and a default
in default.env would be a second copy of the range. The task environment
and the script now call it DEV_PYTHON_REQUIRES; behaviour is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
76adaeb9e8 wrote the regex_search group reference as '\1' inside the folded
task scalar. Jinja unescapes string literals, so the filter received \x01,
rejected it as an unknown argument and failed the dev-python task on every
remote host. The literal is now '\\1', and the unit test renders the task's
environment expression the way Ansible does instead of only checking that
the variable is named.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bf9c5be173 capped the bootstrap at 3.14 with constants inside install.sh, a
second copy of the range pyproject.toml already declares. requires-python is
now the single source (>=3.11,<3.15): install.sh derives its floor and cap
from it, in the repository or, on a remote host where the script runs alone,
from INFINITO_PYTHON_REQUIRES that the dev-python role reads from the same
file; any other form fails loudly. The Dockerfile copies pyproject.toml
before the bootstrap, and pip now refuses 3.15 for the package as well.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
compose.yml first renders before the role writes its config and secret
files, so the content-hash names from the compose_volumes lookup describe
the previous deploy and the next unchanged deploy recreates the stack once
more. A flush handler now recomputes only those <entity>_<key>_<sha8> names
from the files with the lookup's own naming function, on the controller like
the render, so render and flush always agree. Re-rendering the template in
the handler would run in the scope of the include that loaded it and lose
role switches set only on the notifying task.
Fixes#1212 (based on 6b2589a632, PR #1231)
Co-authored-by: Prageeth Panicker <prage.pani@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A truncated chunked OSV download raises http.client.IncompleteRead, an
HTTPException rather than an OSError, so it skipped the .part cleanup and
the error line; the handler now catches both. The runtime check's warn
summary no longer claims the mirror scan tracks every warning, since host
findings and images outside the mirror never reach it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fedora 45 defaults to Python 3.15. ansible-core 2.21, which the unpinned
`ansible` extra still resolves to (ansible 14.5.0 pins ~=2.21.5), calls
dataclasses._is_type, which 3.15 removed, so the Workspace (fedora:latest)
job died before the first playbook (run 41491308729).
install.sh now picks and installs the highest Python between
PYTHON_MIN_MINOR and PYTHON_MAX_MINOR (3.11 to 3.14) on apt, rpm and
pick_python_bin, and fails loudly when only newer interpreters exist. On
Fedora 45 it installs python3.14. Raise PYTHON_MAX_MINOR once ansible ships
ansible-core 2.22.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The make test-security report now holds every finding at the threshold, a
suppressed one with its suppressed_until day, and the scan fails only on the
unsuppressed rows. The per-image issue keeps listing a suppressed CVE with
that day, unsuppressed and CRITICAL rows first, so the issue stays open
while a suppression runs instead of closing until it expires. The job
summary counts the failing findings and names the suppressed total apart.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A suppression entry under <service>.security.suppressions now names either
one CVE (id) or one image tag (image_tag), each with until. An image_tag
entry accepts every finding of the service's image while the owner runs that
tag; each owner of a shared digest is judged by its own tag (Config.Image at
runtime, the pinned version in the mirror scan), so a container on any other
tag stays unsuppressed. Host suppressions are the id entries of the engine
services; an image_tag entry there covers the engine's own image.
The cve lint requires image_tag to equal the service's version, and the
Docker image updater drops a service's image_tag entries with their markers
when it moves the service to another version, so a tag suppression never
outlives its tag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The daily mirror waited about 70 s before each of its 195 copies (50 images
per hour), roughly 3.8 h of pauses, and logged no single rate-limit error.
37 of the copies come from ghcr.io, registry.gitlab.com, quay.io,
registry.opencode.de and mcr.microsoft.com, which the Docker Hub pull limit
does not cover, and --only-missing also waited before images it then skipped.
The throttle now waits only before an actual copy from docker.io, and the
schedule allows 150 Docker Hub copies per hour (24 s apart).
Part of #1324
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The scan threshold becomes a severity-to-verdict mapping in
roles/sys-ctl-hlth-runtime/meta/services.yml, CRITICAL: fail and HIGH: warn,
for production and CI alike. A finding mapped to fail fails the service and
the deploy, a warn prints a warning line and passes, a suppressed one prints
an info line. Engine errors, missing databases and PHP hardening violations
still fail. The old list form is rejected with a config error. The daily
mirror scan stays strict: every unsuppressed fail or warn finding is a row,
so it fails while an image is red.
Every run publishes one list of judged findings four ways:
- report.json of the latest run (summary + records) under
DIR_HEALTH_RUNTIME_REPORT, written atomically;
- findings.jsonl under DIR_HEALTH_RUNTIME_LOG, one record per finding,
appended per run and rotated weekly by a logrotate drop-in;
- one JSON line per target plus a summary line on stdout for the journal;
- infinito_hlth_runtime.prom in DIR_METRICS_TEXTFILE when web-app-prometheus
is deployed: findings per target, kind, image, severity, scanner and
verdict (no CVE label), last run time and success, scanner errors.
Each record carries run_id, timestamp, host, environment, target, kind,
image, scanner, id, severity, package, installed_version, fixed_version,
suppressed, suppressed_until and verdict. The paths are SPOT variables in
group_vars/all/05_paths.yml, the script takes them as arguments.
Requirement 070 and the role README describe the mapping, the outputs and
the per-image vulnerability issue of the previous commit.
Part of #1324
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dry-run mirror scan (run 41286420589) reported 10,179 CVE/image pairs
across 135 red digests; one issue per pair would flood the tracker. The
sync now keeps exactly one open issue per pinned image:
- The body lists every CVE row (id, severity, engine, role, service,
package, installed, fixed), CRITICAL first, and stops below 60,000
characters with the number of rows left out and a pointer to the
vulnerability-findings artifact. On the dry-run data: 135 issues, 8
capped, the largest body 59,872 characters.
- A further open issue with the same title closes as a duplicate of the
oldest one.
- When an image's CVE table changed, the issue gets a "Still unresolved"
comment with the table and an updated body; an unchanged table leaves the
issue alone, so a daily run does not add 135 identical comments.
Part of #1324
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeQL (py/call-to-non-callable, alerts 1073 and 1074 on PR #1326) could not
see the __call__ of the HangingEdit class defined inside the test method and
flagged both gh() calls in sync(). FakeGh now takes the verbs to hang as a
parameter, so the test double stays one module-level class.
Part of #1324
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeRabbit review of PR #1326:
- fetch_databases downloads each OSV archive to all.zip.part and replaces
all.zip only after the transfer completes. An OSError removes the partial
file and comes back as an `osv <ecosystem>: <error>` line instead of a
traceback.
- run_gh bounds every gh call with a 120 s timeout. A failed or hung create,
edit or close becomes a `failed <action>: <error>` line while the other
issues and the close pass still run, and the command exits 1.
- The suppression lint checks every list item of a suppressions block, so an
entry that starts with `until:` still needs its `# nocheck: cve <reason>`
marker.
Part of #1324
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The small GitHub runner shape (AWS, 72G root) ran out of disk in swarm and compose rows (#1329: WordPress#2 of run 41261531965, Nextcloud#6 and #7 of run 41262286962) while an empty instance-store disk nvme1n1 sat unused. mount_spare_disk.sh formats and mounts it at /mnt, but only on a GitHub-hosted runner outside act, and only a whole unpartitioned disk without any signature, mount or swap use of at least 75G; every decline leaves a notice. relocate_docker_dataroot.sh then bind-mounts /var/lib/containerd from the spare disk and moves the Docker data-root to /mnt/docker-host (not /mnt/docker, which is the nested daemon's volume that dedicated.sh wipes). It refuses before stopping anything when images, volumes, build cache, foreign containerd namespaces or shims exist or daemon.json is unreadable, and rolls back the bind mount and daemon.json on any later failure or cancel. Swap goes on / once containerd lives on /mnt, dump_state.sh logs disk size, model and free space per storage root, and the swarm watchdog samples / and /var/lib/containerd every 10 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR #1326 CI failed in two places:
- The vulnerability-db job stopped on `unable to create listing temp file:
open /tmp/grype-db-listing…: permission denied`. The repository runs the
engines as `docker run --user <uid>:<gid>`, and the Grype and Syft images
set no User and WorkingDir /tmp, which only root may write. The dry-run
mirror scan failed the same way: 156 Syft errors and the Grype database.
Every engine now gets TMPDIR pointing at its writable /work mount.
- Every workspace job died in `make system-purge`: `git clean -fdX` could
not remove build/vulnerability-db, because the job bound that directory
as its own mount inside the mounted checkout. On GitHub the workspace job
now keeps its databases under ${RUNNER_TEMP}, outside the checkout; act
keeps the shared host directory.
Part of #1324
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under act the deploy job pinned an engine but skipped the cache restores, so
every local swarm-* run stopped on a missing Grype database. The swarm
matrix also ran inside the act job container, where the shared host
directory did not exist, and the workspace job would have kept a copy of
about 5 GB in every worktree.
- scripts/tests/deploy/act/workflow.sh requires
INFINITO_VULNERABILITY_DB_HOST_PATH, creates it on the host and binds it
into the job container at the same path, because that container drives
the host daemon, which resolves every stack and swarm node bind on the
host.
- call-test-deploy.yml pins engine and database path in one step that runs
only outside act; under act the deploy draws an engine and refreshes the
shared directory. call-test-workspace.yml takes the shared path under act.
- The refresh lock moves into the shared directory, so host deploys and act
runs serialize on one file, and a root refresh hands its files to the
directory owner. hand_over skips paths a mocked download never created,
which failed four TestRefresh cases when the gate ran as root.
- A missing INFINITO_VULNERABILITY_DB_HOST_PATH stops compose up with a
"run 'make dotenv'" hint instead of a KeyError.
- Rationale paragraphs in the database and compose docstrings are dropped.
Part of #1324
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The daily mirror workflow gains a scan job after the sync. It runs
make test-security against the GHCR mirror, writes the failing findings per
image and per CVE into the job summary and uploads the report as the
vulnerability-findings artifact.
- One issue per CVE and image, labelled vulnerability, is opened, updated
while the finding stays and closed once no engine reports it. Issues are
only closed when the scan reported no error, so an image that failed to
scan keeps its issues.
- workflow_dispatch and workflow_call take an issues switch; false is a dry
run that only produces the summary and the artifact. Scheduled runs always
sync the issues.
- Only the scan job holds issues: write.
Part of #1324
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each CI deploy row now draws one vulnerability engine (trivy, grype or osv)
as a fifth rotation axis, shown in the job name next to distro and
filesystem (🔬 axis, 🔺 trivy, 🍇 grype, 🐙 osv). The deploy-time scan of
sys-ctl-hlth-runtime runs that engine behind Syft and the Grype runtime pass.
- A vulnerability-db job keeps one Actions cache per database and day and
downloads only the missing ones. Every deploy job restores Grype plus its
drawn engine, fails on a cache miss and never downloads a database itself.
- Local stacks share one host directory, INFINITO_VULNERABILITY_DB_HOST_PATH,
mounted read-only at INFINITO_VULNERABILITY_DB_DIR in compose.yml and in
every swarm node. A local deploy without a pinned engine draws one and
refreshes only the databases it reads, at most once a day, under a lock.
- A pinned engine whose database is missing stops the deploy before it
starts.
Part of #1324
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The runtime check judged PHP by its upstream version and failed every
container on an end-of-life line. Distribution builds backport fixes without
raising that number, so main run 41154895895 failed Checkmk#0 on
`PHP 8.1.2-1ubuntu2.28 runs the end-of-life line 8.1`, a build Ubuntu lists
as fixed for CVE-2026-109618 and CVE-2026-109619.
- The role now runs Trivy, Grype and OSV-Scanner, with a Syft SBOM per
target, against the host and the images of the running containers, in
bounded parallel lanes. Targets, engines and the failing severities are
lists in meta/services.yml; the default is every target and engine at
CRITICAL and HIGH with a fix available.
- Grype always scans the binary-classified packages of the SBOM, so a PHP
built from source in an official php image (8.4.26) still fails on the
CVEs fixed in 8.4.27, which Trivy and OSV do not see.
- OSV reads an SBOM copy whose distro purls carry the source package name.
- A CVE is suppressed only in the role owning the image, under
services.<service>.security.suppressions with id, until and a
`# nocheck: cve <reason>` marker; a lint rejects bare, malformed and
expired entries.
- The deploy-time run fails on its first finding: sys-service gains
system_service_run_once and sys-service-terminator skips the retries
only for those units. The timer runs daily at 00:00 UTC.
- The PHP version floors are removed; the php_hardening check stays.
- make test-security replaces make security-runtimes: Syft builds an SBOM
of every pinned image straight from the GHCR mirror, cached by digest,
and the three engines scan it with the role's threshold and suppressions.
Part of #1324
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The access-log map read $request_uri, which keeps percent-encoding and
repeated slashes. A request such as /%70assword/reset/<token>,
/password%2Freset/<token> or //password//reset/<token> missed the
token-route expression, fell through to the plain-path branch and was
logged with the token in plain text, while the backend still decoded it
to its reset handler.
The map now reads $uri: decoded, slash-merged (merge_slashes is not
overridden anywhere) and without the query string, so the existing
expressions match every spelling of a token route. Trade-off: after an
internal redirect the log shows the internal URI; with the generic
vhost's error_page (ws_generic.conf.j2) a 5xx line now carries
/500.html instead of the client's path, with the status unchanged.
Nothing in the repository parses the proxy access log.
The unit test resolves the map against the variable it reads, modelling
$uri as nginx builds it, and adds the three encoded spellings above.
Negative control: with the map back on $request_uri, exactly those three
cases fail (3 failed, 8 passed).
Reported by CodeRabbit on #1322.
Part of #1192
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The front proxy logged the full request line: production had no
access_log, so nginx fell back to `combined` ($request, Referer,
$remote_user), and the debug format logged $request and the Referer.
Query-string tokens (?access_token=, ?key=, ?token=) and token routes
such as /password/reset/{token} and /i/confirm-email/{user}/{random}
ended up in plain text in the container log.
nginx.conf.j2 now declares a map from $request_uri to
$infinito_access_log_path:
- the path is written without the query string;
- the segments after a known token route (password/reset,
password-reset, reset-password, confirm-email,
lostpassword/reset/form, email-login, activate-account; matched
case-insensitively at any depth) are written as [redacted], so the
request, its status and timings stay visible without the token.
Both modes write an explicit access_log to /dev/stdout:
- production uses the new `redacted` format, laid out like `combined`
(remote address, time, "method path protocol", status, bytes,
user agent) with Referer and basic-auth user written as "-", so
combined-style parsers keep working;
- debug keeps its fields (host, timings, upstream time, lengths,
X-Forwarded-For, scheme, server name) but logs
"method path protocol" instead of $request and drops the Referer.
The path comes from $request_uri instead of $uri so internal rewrites
(error_page, try_files) do not replace the path the client requested.
Nothing in the repository parses the proxy access log (Wazuh, the
health checks and the Prometheus log_by_lua metrics do not read it),
so no consumer needs to change. The error log still carries
`request:` and `referrer:` on error-level entries; that is outside
this change.
A unit test renders nginx.conf.j2 in both modes and asserts that every
access_log uses a format declared there, that no format references
$request, $request_uri, $args, $query_string, $is_args, $arg_*,
$http_referer or $remote_user, and, by resolving the map as nginx does,
that query strings are dropped, token routes are redacted and
lookalike paths stay unchanged.
Verified live with web-svc-html in production mode (deploy failed=0, so
openresty accepted the config). A probe request to
/password/reset/<probe>?token=<probe> was logged as
"GET /password/reset/[redacted] HTTP/1.1" 404 62 "-" "curl/8.14.1",
and /plain/page as "GET /plain/page HTTP/1.1". Negative control: the
same request against the previous config logged the probe path token
and the query token in plain text. The debug format with the fix is
covered by the unit test only.
Closes#1192
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shared proxy's nginx.conf derived worker_connections from the
openresty container's pids_limit. pids_limit caps processes and threads,
worker_connections caps simultaneous sockets per worker (client and
upstream), so with pids_limit 256 and one worker the whole host's proxy
held about 256 sockets, roughly 128 proxied requests in flight across
every vhost. Raising pids_limit to buy connections would loosen the
fork-bomb guard.
svc-prx-openresty now declares services.openresty.worker_connections
(4096 per worker, the common nginx production range, enough for several
chunk-heavy apps such as OpenWebUI at ~150-170 requests per page load).
WEBSERVER_PROXY_WORKER_CONNECTIONS reads it through lookup('config'),
without a default, so a missing key fails loudly. pids_limit stays 256.
WEBSERVER_PROXY_WORKER_RLIMIT_NOFILE is derived as 2x worker_connections
(8192), one descriptor for each client and upstream socket of a proxied
request, and nginx.conf renders it as worker_rlimit_nofile. The openresty
container pins its nofile ulimit (soft and hard) to the same value:
workers call setrlimit as root without CAP_SYS_RESOURCE, so they can only
lower the hard limit, and the inherited hard limit depends on the host
daemon (Docker >= 25 defaults to 1024:524288, DiD nests whatever the
outer container has). An explicit ulimit makes the rlimit succeed on
every host instead of logging a setrlimit alert.
The unit test pins worker_connections 4096, worker_rlimit_nofile 8192 and
the container ulimit, and fails when worker_connections follows
pids_limit again.
Live proof (compose deploy of web-svc-html, both plays failed=0): the
deployed nginx.conf renders worker_rlimit_nofile 8192 and
worker_connections 4096; openresty's master, worker and cache manager
run with "Max open files 8192 8192"; the openresty log carries no
setrlimit or "worker_connections are not enough" line.
Closes#1300
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_sso.py, test_sso_oidc_plugin.py and test_sso_proxy_consumers.py
evict utils, utils.cache, utils.cache.applications and the roles
packages from sys.modules to import their plugin against fresh modules,
and never put the originals back. Ansible's plugin loader keeps the
applications lookup it loaded earlier in sys.modules, so that lookup
stayed bound to the evicted utils.cache.applications, whose
_MERGED_APPLICATIONS_CACHE utils.cache._reset_cache_for_tests() no
longer reaches.
On main this turned test_webserver_worker_limits red whenever one xdist
worker ran a test that loads the applications lookup through Ansible,
then one of these sso tests, then the worker-limits test: the stale
cache served the cpus 0.5 payload of test_a_sub_cpu_proxy_still_runs_one_worker,
so the later methods rendered worker_processes 1 instead of 4 (main run
41139196554, Workspace centos and fedora, second make test on gw1).
_load_module now runs inside mock.patch.dict(sys.modules): the plugin
keeps the fresh modules it was built against, and sys.modules returns to
its prior state afterwards. This is the change store-i18n carries since
ec0fde6561, ported unchanged so both lines stay identical.
Evidence:
- Before: pytest -n 0 test_tor_ports.py test_sso_oidc_plugin.py
test_webserver_worker_limits.py -> "3 failed, 33 passed", the CI
failure set ('1' != '4' twice, worker_processes 1 vs 4).
- After: test_tor_ports.py, the four sso test files and
test_webserver_worker_limits.py -> "68 passed".
(cherry picked from commit ec0fde6561)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>