- Declare `__all__` in every package `__init__.py` whose `PROJECT_ROOT`
is imported by its modules (163 packages), so the constant counts as
exported; list every public name where the package defines more.
- Empty the 15 package `__init__.py` files whose `PROJECT_ROOT` no module
imports.
- Declare the public API of `utils.github.variant.axes` in `__all__`,
re-exports included, instead of silencing them with `noqa: F401`; the
list matches the one `feature/store-i18n` already carries.
- Use `import unittest.mock as mock` in `test_applications.py`, as the
rest of the suite does.
- Spell `_NON_PRINTABLE` in `utils/yaml_bootstrap.py` as the equivalent
BMP-only positive class; it matches exactly the same 2111 code points
as the old negated class across U+0000..U+10FFFF.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The seeded salt was the first 16 bytes of sha256(seed + secret). bcrypt
stores its salt in clear inside the hash, so every seeded hash published
a fast sha256 digest of the password under a seed that is public in the
repository (`web-app-wazuh:indexer-admin`). Anyone holding the hash could
test password guesses at sha256 speed instead of paying bcrypt's cost.
The salt is now sha256(seed)[:16]. The hash stays deterministic for one
password, still changes when the password rotates, and its salt carries
nothing computed from the secret. Existing Wazuh deployments get a new
hash once; `securityadmin.sh` re-applies `internal_users.yml` on every
deploy and the hash still verifies the unchanged password.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Combine main's WireGuard mesh axis with this branch's network axis in the CI
variant code. Every matrix row now carries both: network (clearnet, tor,
multi) from utils.github.variant.network and vpn (meshed or direct) from
utils.github.variant.vpn. Label parsing, which main moved to label.py, reads
the network glyph instead of the old tor flag and keeps the vpn glyph; the
selection token, plan table, matrix and trigger CLIs, the workflows and the
swarm routine forward both axes. The test of an incapable priority variant
now compares the set of network modes, since a swarm priority row also
covers both mesh arms.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Two bots bumped the one baudolo version independently. The pip pin
updater rewrites the literal package_name pin in
roles/svc-bkp-volume-2-local/tasks/01_install.yml and opens a PR scoped
to roles/<entry>/, and dependabot bumps the same package in
pyproject.toml. Each PR alone splits the pins that
tests/lint/repository/dependencies/test_baudolo_pin.py holds equal; #995
merged such a split and turned main's test-lint red until #996, and the
next upstream release (7.1.2 is tagged) would repeat it.
The version lookup now takes an optional distribution name and returns
its exact == pin from [project.dependencies]. The install task asks it
for backup-docker-to-local, so pyproject.toml is the only place the
version lives, dependabot is the only bot that moves it, and the pip
updater no longer finds a literal to bump. The lint now forbids a
literal pin in the task and requires the lookup.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The handover condition lived as the same inline Jinja in basic.conf.j2,
ws_generic.conf.j2 and Baserow's proxy.conf.j2. Move it into
lookup('network_handover', application_id, domain), which also refuses a
non-boolean flag, and use it at all three places.
A handover sibling still proxies its health, MCP and SSO endpoints to the
backend, and those requests carried the onion Host, which single-URL apps
reject: pretix answered /healthz/ready on the onion with 400 Unknown host
and failed the Prometheus blackbox probe. forwarded.conf.j2 now sends the
canonical domain as Host and X-Forwarded-Host on a handover sibling, so the
onion probe keeps measuring the backend through the onion vhost.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Matrix is the only single_mode role: on a multi node it is served on onion
only, so it has no TLS of its own, yet Synapse calls the clearnet Keycloak
issuer over self-signed TLS. ca_injected returned false, Synapse got no CA,
and OIDC discovery failed with CERTIFICATE_VERIFY_FAILED, which Synapse
reports as "504 Timeout waiting for response" and Element as "This login
provider is unavailable right now".
An application with a domain and self-signed TLS of its own still gets the
CA. Otherwise a ca_client service now gets it when the SSO issuer
application resolves TLS enabled and self-signed. The cherry-picked
store-i18n rule answered a domain-less client on the global TLS_MODE; the
issuer's own resolved mode honours per-app overrides on both sides,
answers false on a tor-only node, and true when TLS_ENABLED is off but the
issuer keeps an override, or when the client serves letsencrypt itself.
Matrix declares ca_client on its matrix service.
A later merge of feature/store-i18n conflicts in ca_injected.py and its
test; keep this side.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
sys-svc-compose writes compose.ca.override.yml for exactly lookup('ca_injected', application_id), which since 1254e27c2d also answers true for a service that declares ca_client and only calls the self-signed endpoint. compose_file_args rebuilt that predicate instead of asking it, and its copy still demanded a domain, so for a domainless ca_client the file was written and never reached --compose-file. Swarm takes its file list from that lookup; compose finds the file on disk by itself, which is why only the swarm jobs of run 36064589578 failed while the compose job passed.
The broker's container in that run carries no SSL_CERT_FILE and no CA mount, while openwebui, same job and same node, carries seven CA env vars and four binds. Every outbound call to Keycloak then fails the handshake, which the CLI test reports as 503 and Open WebUI relays as HTTP 400.
Two corrections in ca_injected itself. Its templar.template() call was dead: as_str strips the TrustedAsTemplate tag and ansible-core 2.19+ refuses to render an untrusted string, so the line could not resolve what it advertised, and tls.py and cert.py never template TLS_MODE either. And the ca_client branch short-circuited on the global TLS_MODE, skipping resolve_mode's per-app server.tls.mode override; it now applies only where there is no domain and so no per-app answer to get.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ca_injected asked three questions, the first being whether the application
has a domain. That decides whether it is SERVED over the self-signed CA. An
application that only CALLS one has no domain of its own, so the predicate
answered false, sys-svc-compose-ca skipped its stack, and the container ran
with an empty trust store.
svc-ai-agent-broker is such a caller: it authorizes every request against
Keycloak group paths over the public HTTPS base. Without the CA every
outbound call died in the TLS handshake, the broker answered 503 "access
check unavailable", Open WebUI turned that into HTTP 400, and the spec
failed on a model it had just listed. Its container was the only one in the
rescue artifact without SSL_CERT_FILE, while litellm and open-webui carried
it.
A service may now declare ca_client: true, and the predicate then answers on
the deployment's TLS mode alone, because the domain question is meaningless
for a client. The three served-side conditions are untouched, so no
application that already received the CA stops receiving it.
The alternative of pointing the broker at an internal Keycloak URL was
rejected: the role declares no keycloak service, so the URL would render
without a route.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The autoformat step of the gate wrapped the long assertions that
660ad2ff16 committed unformatted. No behaviour change.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`lookup('tls', ...)` aligns a cross-app domain to the consumer named by
`application_id` (utils/tls_common.py `align_domain_to_consumer`): a
clearnet-primary consumer gets the clearnet sibling of an onion-primary
target. The carrier key does not cover `application_id`, so a park that
renders inside a role hands that role's alignment to every later role
whose key matches.
The constructor and `tasks/utils/setup/role.yml` park at play scope,
where `application_id` is undefined. The re-park in
`roles/sys-token-store/tasks/write.yml` (67f6c8d7e8) and the nested-load
re-park render inside the caller: `svc-prx-openresty` at the preload
loader, `web-app-stalwart` or a provider role on the NFS host, the web
app itself under `sys-stk-front-base`. In a tor deploy that turned onion
targets into clearnet domains for onion-primary roles served from such a
park, and baked the caller's group into Nextcloud's sociallogin
`groupMapping`.
Drop `application_id` from the render's variables and from the
templar's for the duration of the render, so every park carries the
play-scope result the constructor parks have always carried. Rename the
nested re-park task so its log lines do not read as a role boundary.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The frontend and its sso, logout, dashboard and prometheus consumers were
gated on 'svc-net-tor' not in group_names, written when a Tor node served
onion only. On a multi node that removed the clearnet filer vhost, so the
storage specs of every SeaweedFS consumer failed on a clearnet pick with
ERR_SSL_UNRECOGNIZED_NAME_ALERT on filer.<domain>.
lookup('network_served', <network>, *role_ids) answers whether the node's
resolved NETWORK_MODE serves a network and every named role is deployed.
The gates use it instead of restating the mode resolution and the provider
check, so the frontend now drops only on a tor-only node. is_explicit_truth
registers the lookup in GATED_LOOKUPS so the static closure still reads
those flags as dynamic, and the dynamic-flag and universal-logout checks ask
is_explicit_truth instead of matching "in group_names" themselves.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Since 778c18fb23 the carrier key holds the tokens file's mtime and size.
sys-token-store changed that stat on every inclusion (00_core.yml
touched the file) and on every persisted token, and nothing re-parked a
carrier afterwards. Every later task's fresh worker then rendered the
applications SPOT cold, 30 to 80 s each. On main fea6f31d9f (run
38812964308) seven heavy jobs logged 194 to 287 such tasks and ran out
of the 20400 s budget; the same jobs before 778c18fb23 logged 65 to 83,
all of them real waits.
- 00_core.yml touches the store with modification_time and access_time
preserved, so it only creates a missing file. In swarm the controller
sees the store over NFS with attribute caching, and the touch alone
made the second pass go cold without any write.
- write.yml re-parks the carrier when the persist or the controller
mirror changed the store. Both run once, so every host that ran the
include gets the same verdict.
- tasks/utils/setup/role.yml re-parks after every role on every host,
so hosts that did not run the writing role recover too. Where the
key still matches this serves the carried payload; measured 0.1 s per
role in a local compose deploy.
- The tokens file enters the key as the SHA-256 of its content,
memoised per inode, mtime and size, so a touch no longer changes the
key. A stat that still matches serves the memo (1.67 us against
1.41 us for the old stat signature), and a stale stat keeps key and
YAML cache on the same old content instead of pairing new content
with an old key.
- applications_carrier opens the tokens file before it computes the
key, so on the NFS-mounted controller the parked key reflects the
revalidated attributes rather than a cached stat that changes a
minute later.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
No conflicts across 121 incoming commits. The one file both sides changed is
tasks/stages/01_constructor.yml: upstream parks an applications carrier for
the swarm bootstrap roles after the add_host tasks, and keys the carrier on
the tokens file as well. This branch's mesh bootstrap opens and drops its own
block-scoped carrier before that point, so both keep working as written.
Upstream also brings ruff 0.17.0, distro-derived env keys, a scoped act outer
teardown, the stalwart relay rule, the onion apex redirect and image and
version updates; ruff 0.17 reformats nothing on this branch.
PR #932 emits the node onion apex only for an onion DOMAIN_HOMEPAGE. On a multi node the homepage is the clearnet dashboard on purpose and the redirect template sends the apex to its onion sibling, so the merge dropped the apex redirect there. The apex is now kept when the homepage is an onion host or network_siblings lists an onion sibling this node serves; a target without one, such as a software domain off the primary, still gets none. The #932 test reads the primary domain instead of writing it out.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The asset host took the canonical CDN domain and only guessed its scheme from the suffix, so pages on the onion sibling loaded their libraries from the clearnet CDN, which their CSP and Tor reach do not allow. It now takes web-svc-cdn's url.base, which follows the network of the vhost being rendered; without the internal CDN it stays jsDelivr.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Keycloak runs once on clearnet, yet onion pages such as the dashboard embed it for the silent SSO check, and its frame-ancestors listed only clearnet sources, so the browser refused the frame. On the issuer's clearnet vhost each translatable ancestor now also appears on the node onion. Whether a token moves to the node onion is decided by its host, so the issuer keeps its one URL on onion vhosts wherever it is listed, not only through the logout source.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PR #932 emits the node onion apex only when DOMAIN_HOMEPAGE ends in .onion. On a multi node the homepage is the clearnet dashboard on purpose and the redirect template maps the target to its onion sibling, so merging main would drop the apex redirect without a textual conflict. This test goes red on that merge; relax the guard to a target that is onion or has an onion sibling rather than weakening the test.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
One oauth2-proxy applies one cookie-secure flag to every host it answers, so a secure clearnet instance drops the CSRF cookie of its plaintext onion sibling and the login loops. proxy_instances now yields one instance per network with its own compose service, sso or sso_onion host port and config file; each config takes cookie_secure, redirect_url and cookie_domains from its own network, and every vhost routes /oauth2/ to the instance of its network. The 29 gated roles gain a sso_onion port, and Bluesky heals the bind mount of every instance.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
network_url moves a URL into the network of the vhost being rendered, network_urls lists it once per network sibling with that sibling's scheme, and network_redirects fans a redirect source out to its siblings while the target stays canonical. Consumers that hard-code the canonical origin can now emit the sibling a request actually arrives on.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The onion pick only bypassed the SSO issuer, so a clearnet CDN, asset or dashboard host an onion page loads went through Tor, which the lab network cannot exit. browser_proxy_bypass derives the list from the deployed applications' domains plus the issuer; it is empty on a clearnet pick and on a tor-only node. Specs that passed their own proxy to newContext dropped the bypass and now inherit use.proxy.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On a Tor node current_play_redirect_domains schedules the bare node
onion as a redirect source targeting DOMAIN_HOMEPAGE. Without
web-app-dashboard, DOMAIN_HOMEPAGE falls back to SOFTWARE_DOMAIN
(infinito.nexus), so web-opt-rdr-domains rendered a 301 from
http://<node>.onion/ to http://infinito.nexus/: an onion visitor sent
to a clearnet host over plain HTTP.
The onion apex entry is now added only when DOMAIN_HOMEPAGE is an
onion host. Otherwise the onion apex gets no redirect and the default
server answers.
Closes#924
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brings web-app-wazuh to the reviewed state of #414. This branch was cut
from an earlier #414 head, which has since been rebased and gained its
review fixes, so both sides added the role and every role file conflicted
as add/add.
Conflicts, decided per file:
- roles/web-app-wazuh: #414's side. It is the reviewed version of the
same role, and this branch's own Wazuh changes are already part of it
(drop the Tor dual-stack workarounds, drop the first commit's
leftovers, ports 8096/8097, heap via service_memory_mb).
- roles/web-app-wazuh/templates/opensearch_dashboards.yml.j2: #414's file
with this branch's trust_dynamic_headers instead of base_redirect_url,
so the OIDC redirect follows the network sibling the request came in
on rather than pinning the clearnet URL.
- plugins/filter/bcrypt_hash.py and its test: #414's stable salt, which
the role's vars now pass.
- test-e2e-playwright keycloak.js: #414's leave-group helper, which fails
loudly on a missing user or group instead of returning silently.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflicts resolved by keeping both sides' intent:
- call-test-deploy.yml: main's re-enabled deploy concurrency group, keyed
on the network axis instead of the removed tor axis.
- sys-svc-proxy: main's shared headers/forwarded.conf.j2 now carries the
branch's per-vhost semantics (X-Forwarded-Host, TLS port and state read
from the vhost domain), so html, ws, upload and the Prometheus probes
all answer per network sibling.
- plugins/lookup/email.py: main's provider-agnostic mail lookup asks the
network registry instead of testing the .onion suffix, as the branch
does everywhere; container_extra_hosts.py likewise. The provider host's
NETWORK_MODE now decides whether it is served onion-first, since a Tor
node defaults to multi and keeps clearnet canonical.
- workspace common.sh: main's INFINITO_DOMAIN fallback without the
duplicated no-op line.
- README.md and requirement 035: both sides' rows (Wazuh plus main's
Pi-hole and Stalwart entries); adapter_candidate count 39.
Adapted to main where the branch's rules now reach main's code:
- six tests from main read the primary domain from default.env instead
of writing it out, as the hardcoded-domain lint requires;
- the chutney worker-check test passes ONION_SUFFIX from the network
registry, which the branch's svc-net-tor test script requires;
- web-app-wazuh moves its local http ports to 8096/8097 (8094/8095 now
belong to web-app-stalwart) and sizes the indexer heap through
service_memory_mb, the same fixes#414 carries;
- web-app-wazuh's README cosmos follows Stalwart as the mail provider.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The network probe used community.docker.docker_network_info, whose
get_network() scans GET /networks and returns the first entry whose Name
equals the requested name OR whose Id starts with it, in a list order that
changes between calls. In main run 38038541631 (Zammad#1, swarm) the
central redis overlay got the ID n8nwzb1q39m3u4k7awwwmt6yg, the probe for
the not yet created provider network "n8n" returned it as existing, the
create step was skipped, docker stack deploy attached hermes to the redis
overlay as "n8n", and the oauth2-proxy then reached the password-protected
central redis by its bare host name: "NOAUTH Authentication required".
The same scan can also return the ID-prefix twin of a network that does
exist, so comparing the returned name alone would turn that network into
"missing" and fail the create with "already exists".
Both probes now run `container network ls --filter name=<entity> --format
'{{json .}}'`, whose name filter never matches an ID. The new lookup
network_driver picks the entry named exactly after the entity and returns
its driver, '' when there is none; the drift checks, their messages and the
create steps all read that one entry. No docker module is left in the file,
so its two docker-module-uses-container-cli suppressions go too.
Verified: on the host daemon GET /networks/<id prefix> returns a network of
another name and docker network ls --filter name=<id prefix> returns
nothing. In ansible-playbook against real docker network ls output the
lookup returned the exact network's driver in 20 of 20 probes beside an
ID-prefix twin, an empty driver (create path) for a missing network and for
a name only contained in a sibling, and the drift path for a foreign
driver. Unit tests cover the same cases; with a substring match instead of
the exact name the two sibling cases fail.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`make quality-high` rewrote these files on every branch: the SuiteCRM
README and the root role table lacked the Redis service #881 added, the
session path test was not formatted, and an OpenTalk secret description
from #873 was not wrapped.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With three swarm replicas the PHP session files on the shared NFS volume
were read as empty under concurrent requests of one browser, so SuiteCRM
answered 403 on /api/graphql and dropped the session. Sessions now live in
Redis with session locking that waits like flock did; the phpredis version
is pinned in services.yml with its own update source, and the engine lookup
renders the save path.
Refs #879
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>