Commit Graph
177 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 5.5 57d6ece9b0 feat(cdn): serve the CDN same-origin on every front-proxy vhost
Values an app renders once (env files, branding config, web manifests)
cannot follow the network a page is served on, so onion pages loaded
logos and icons from the clearnet CDN host (Mobilizon, Wazuh, Mini-QR in
run 40243434327). Like the object-store route from 40b178d9a4, every
front-proxy vhost now aliases the CDN directory under /_infinito/cdn/,
with nosniff and a sandboxing CSP so a mirrored HTML or SVG file cannot
script the app's origin. lookup('cdn') and lookup('design') add
relative_urls for consumers that want host-relative URLs; the absolute
urls stay for server-side users. The include lives outside the location
snippets, so test_vhost_cross_cutting_includes requires it of every
override, the compose synapse vhost included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 03:29:40 +01:00
kevinveenbirkenbachandClaude Opus 5.5 99123762df Merge branch 'feature/updater' into feature/store-i18n
16 conflicts, resolved per hunk:

- sys-stk-front-proxy: tasks/main.yml keeps the network-sibling loop;
  the single stack_host_contents write of http.conf plus vhost and the
  front_proxy_vhost_changed gate move into tasks/vhost.yml, with the
  http.conf decided per sibling domain so an onion vhost gets no HTTPS
  redirect.
- sys-front-tls: 01_http.yml is gone as the updater intends; the
  cert-renew include in main.yml gates on the vhost domain's TLS.
- sys-svc-compose: the updater's resolve_host_cid without the remote
  echo and the prepared pending flag in up.yml, written with
  IS_SWARM_MODE and the repeated-when block grouping.
- web-app-nextcloud: the updater's post-upgrade gate and pending-only
  plugin configuration, which already carry the pinned-plugin handling.
- web-app-xwiki: one XWIKI_CORE_VERSION for the readiness probe and
  style.css.j2; OIDC/LDAP follow xwiki_extensions_ready at bootstrap,
  the S3 blob store stays off there as TestBlobstoreRenderOrder demands.
- roles_with_service: onion swap and dashboard card key combined, the
  canonical domain is the canonical URL's host in both branches.
- sys-ctl-hlth-csp: --clearnet-timeout and --concurrency together;
  run_checker forwards its options to build_docker_cmd, keeping the
  script under the 500-line limit.
- web-app-matrix: Element branding and web_client_location side by side.
- web-app-stalwart/README.md: the generated Cosmos section is dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 23:47:07 +01:00
kevinveenbirkenbachandClaude Opus 5.5 afb82f57a1 feat(web-app-dashboard): let a role name the canonical domain its card opens
The dashboard card of every app opened its primary domain, so Matrix led to the Synapse status page and Stalwart to its admin UI. A role can now declare services.dashboard.card: <key of meta/domains.yml canonical>; docker_cards builds the card URL with lookup('canonical_url', app, key), the Apps menu reuses it, and roles_with_service('dashboard') hands the same domain to the tile e2e spec. Matrix sets card: element, Stalwart card: webmail. Without card nothing changes, and the prometheus/matomo target lists are untouched. A lint requires the key to resolve in the role's own domains.canonical.

The Matrix ansible flavor also sets Synapse's web_client_location to Element through matrix_synapse_configuration_extension_yaml, as the compose flavor already does, so matrix.<domain>/ redirects there. Merge note: feature/iam-profile-rbac defines the same extension variable inside the OIDC block; the next merge must combine both into one block (agreed with that session).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 21:01:06 +01:00
kevinveenbirkenbach 2c04cce617 Merge branch 'feature/updater' into feature/store-i18n
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	plugins/lookup/deployment.py
2026-10-25 09:32:11 +01:00
kevinveenbirkenbachandClaude Opus 5.5 ca4f53d0e4 fix(mcp): keep a persistent host's providers through a partial deploy
mcp_servers narrowed the MCP providers to application_closure(deployment.whitelist), which on a persistent host drops every provider an earlier run deployed and a partial run does not touch. sys-svc-mcp-reconcile then converges each client present on the host against that narrowed set: on echoserver the --id web-opt-rdr-domains run rdr-3 removed the Prometheus registration from Flowise.

deployment now reports ephemeral (runtime act or github), and the closure narrows only there, where a whitelist is all a run deploys; a persistent host narrows nothing, as a full deploy already did with its empty whitelist, so its clients keep every provider it serves. CI keeps its narrowing unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 09:24:28 +01:00
kevinveenbirkenbachandClaude Opus 5.5 2597ce1b90 fix(ca_injected): read an application's own TLS on its canonical domain
Asked by application id, lookup('tls') aligns the domain to the vhost in `domain`. sys-svc-cdn reloads the compose handlers inside every sibling of the vhost loop, so a handler flushed after the loop still saw the application's onion sibling there: on prometheus' second stack deploy (the MCP adapter rebuild) ca_injected answered False, the swarm CA override was skipped and compose_file_args dropped it from docker stack deploy. Blackbox lost the CA mount and the docs healthz probe failed with an unknown authority (API#1, Jenkins#1), and the spec flip recreated every task at once, so the sso-proxy reached central redis before its sidecar registered and the converge gate failed on NOAUTH (OnlyOffice#1, Nextcloud#1, GitLab#1, Mattermost#1; run 40131272595).

The CA mount belongs to the containers, not to a vhost, so the own-TLS check now asks with the canonical domain, which no alignment touches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 08:44:25 +01:00
kevinveenbirkenbach f69170274c Merge branch 'feature/updater' into feature/store-i18n
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	roles/web-opt-rdr-domains/README.md
#	roles/web-opt-rdr-domains/tasks/main.yml
#	roles/web-svc-design/tasks/00_core.yml
#	tasks/stages/01_constructor.yml
#	tasks/stages/02_server.yml
#	tests/unit/python/plugins/lookup/test_current_play_redirect_domains.py
2026-10-25 08:31:09 +01:00
kevinveenbirkenbachandClaude Opus 5.5 6bb1dafcc5 refactor(web-opt-rdr-domains): read host-wide redirects from the role config
Host-wide redirects such as a legacy zone came from the top-level inventory variable redirect_domain_mappings, which only the role default declared. Once the role input moved to web_opt_rdr_domains_mappings nothing declared it any more and the inventory validation refused the deploy, because it does not see the lookup's vars_ read. The redirects now live where the role's other settings are: services.rdr-domains.redirects in meta/services.yml, empty by default and set per inventory under applications.web-opt-rdr-domains, and current_play_redirect_domains reads them through the config lookup. They still win over the application aliases, and a caller that includes the role without its own web_opt_rdr_domains_mappings still renders the merged CURRENT_PLAY_REDIRECT_DOMAINS.

Inventories move their redirect_domain_mappings list unchanged to applications.web-opt-rdr-domains.services.rdr-domains.redirects.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 08:11:33 +01:00
kevinveenbirkenbachandClaude Opus 5.5 086abc6338 fix(playwright): reach other applications through lookup('landing_url')
The playwright env renders once per network pass. lookup('tls', app, 'url.base') aligns another application's URL to the pass network even where that application hands the network over, so a spec expected a host the browser is redirected away from (fediwall on its onion pass expected the Mastodon and Friendica onion siblings).

lookup('landing_url', app) returns the aligned URL, or the canonical one behind a handover. Every cross-application URL in the 79 playwright.env.j2 templates goes through it now, fediwall's inline handover expressions included, and a lint rejects both the raw tls form and the inline one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 22:25:39 +02:00
kevinveenbirkenbachandClaude Opus 5.5 e0e2c0c944 fix(lookup): fan a redirect out only to the networks its target is served in
network_redirects gave every network sibling of a source the mapping, and redirect-domain.conf.j2 falls back to the canonical target when the target has no sibling in the source's network. Since e4fd4552f0 schedules the clearnet apex entry without web-opt-rdr-domains in the round, its fan-out rendered a vhost for the bare node onion pointing at the clearnet homepage in every multi-network round without an onion dashboard: the CSP health check of API#1 and OnlyOffice#1 in run 40109131688 failed with '<node onion>: left .onion: ended at https://dashboard.<domain>/'.

The canonical sibling of a source keeps its mapping as before; a further sibling is fanned out only when the target is served in that sibling's network. A dropped onion source has no vhost and the default server answers it with 404 inside the onion instead of redirecting to clearnet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 21:19:45 +02:00
kevinveenbirkenbachandClaude Opus 5.5 e4fd4552f0 fix(lookup): keep the clearnet apex redirect on a multi-network node
current_play_redirect_domains gated both apex entries on web-opt-rdr-domains being in lookup('deployment').deployed whenever the host serves Tor. On a multi node that also dropped the clearnet DOMAIN_PRIMARY -> DOMAIN_HOMEPAGE entry for every isolated variant without that role, so the apex had no vhost and answered with a TLS handshake reject; the Matrix federation delegation on the apex depends on that vhost in swarm. A clearnet node, and main, always schedules it.

The clearnet entry is now scheduled whenever the node's resolved NETWORK_MODE serves clearnet, and the deployment gate keeps guarding only the onion-apex entry. A tor-only node keeps the old behaviour, and the operator's production inventory runs no Tor host, so production is unaffected. Tests cover multi without the role (clearnet apex only) and tor-only (no apex).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 16:16:38 +02:00
kevinveenbirkenbachandClaude Opus 5.5 3643ce7b22 fix(playwright): check injected assets only on targets that render in the pass network
On the +tor pass, web-svc-design 'loads shared CSS', web-svc-logout 'loads logout.js' and matomo 'tracker injected' expected the onion CDN and matomo hosts for every target, but three kinds of target render on clearnet there: the SSO issuer is never aligned to the pass network, a handover role's onion sibling only answers 302 to its canonical URL, and an IdP-gated role sends the anonymous visitor to the issuer (rescue proxy log of fork run 40042690056, job Pretix#0: onion 302, clearnet page, cdn.main.infinito.test). The dashboard tile test failed the same way for the four handover tiles.

roles_with_service takes an opt-in network= and keeps a role only when its canonical domain lies in that network and is not a handover sibling; the three env templates pass the network of the pass domain, so the clearnet pass still covers every target and each network's CSP is checked against its own hosts only. The design and matomo specs skip IdP-gated targets the way the logout spec already does. The dashboard exposes handover_domain and expects a handover tile to land on the canonical domain, while href and iframe checks stay on canonical_domain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 15:48:09 +02:00
kevinveenbirkenbachandClaude Opus 5.5 e611e5ea05 fix(objstore): name the consumer when resolving its onion origin
sys-svc-objstore includes the provider role with application_id set to "{{ _provider_role }}", which is itself lookup('objstore', ...). For a consumer served only on onion the objstore lookup sets proxy_required and calls lookup('tls', consumer_id); without a consumer, align_domain_to_consumer renders variables['application_id'], which runs the objstore lookup again, and the chain repeats until Ansible stops with "Recursive loop detected" / "Maximum depth exceeded" (fork run 40042690056, jobs Moodle#2 and Nextcloud#12, log line 286323). Only web-app-matrix sets single_mode, so only its multi-mode onion deploys hit it.

The tls lookup takes a consumer= kwarg and hands it to align_domain_to_consumer, the way canonical_url already does, and the objstore lookup names its consumer. The default stays empty, so no other caller changes. Regression tests cover both lookups; an onion consumer now resolves to http://matrix.<onion>/objstore/matrix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 15:26:22 +02:00
kevinveenbirkenbachandClaude Opus 5 0af8ee5992 Merge branch 'feature/updater' into feature/store-i18n
Five conflicts, resolved by what each side is for rather than by taking a
side:

- sys-service/01_prune.yml and sys-service-terminator/02_unlock.yml take
  the incoming single `systemctl` invocation over four module loops. The
  unlock side also drops an unmask step: the list it runs on is built with
  `reject('search', ' masked')`, so it never held a masked unit and the
  step did nothing.
- sys-stk-front-proxy/tasks/main.yml keeps this branch's version. The block
  the other side still carries there moved into vhost.yml, which runs it
  once per network sibling; taking theirs would have duplicated it.
  vhost.yml holds every task of that block, the global injection and the
  TLS layer included.
- web-app-matomo/08_reinject_consumers.yml likewise keeps the per-vhost
  loop, which re-renders the injection for every network sibling and passes
  matomo_token_override.
- utils/cache/domains.py combines both: the incoming side rendered
  DOMAIN_PRIMARY with the real templar and variables, which is right, but
  added a second renderer beside the module's own. _render_plain now takes
  that context optionally, so the other two call sites stay as they were.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-24 01:57:56 +02:00
kevinveenbirkenbachandClaude Opus 5.5 47160d755e fix(tls): serve every vhost the certificate of its own domain
ssl_credentials.j2 resolved the certificate by application_id, so every vhost of an application got the lineage of its primary domain. echoserver serves webmail.infinito.nexus the CN=academy.infinito.nexus certificate that names mail.infinito.nexus but not webmail, and browsers refuse it with a hostname mismatch. The template and the letsencrypt existence check now resolve by the vhost's domain, so a secondary vhost points at its own live/<domain> link and a missing link triggers the issuance instead of a mismatch. Primary vhosts resolve to the same files as before in every TLS mode and flavor.

The LDAP stream passes its primary domain explicitly. Onion domains stay on the application lineage, and the app-scoped self-signed path always names the application leaf, because neither has a per-domain certificate.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-23 23:41:33 +02:00
kevinveenbirkenbachandClaude Opus 5.5 ef246a6e03 refactor(unit_name): build unit names from SYS_SERVICE_VERSION_SUFFIX
The lookup assembled '.<version>.<software domain>' itself, a second copy of the infix the unlock and prune phases now read from the SPOT. It reads SYS_SERVICE_VERSION_SUFFIX instead and refuses one that does not start with a dot or carries an empty segment. The direct version-plugin fallback goes with it: it existed because the plugin rendered the untrusted literal "{{ lookup('version') }}", while the SPOT arrives as a trusted group_vars string. Every unit name rendered from group_vars is byte-identical before and after.

The timer description names the versioned service it starts instead of the legacy unversioned name.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-23 23:34:13 +02:00
kevinveenbirkenbach 59c5667f1f Merge branch 'feature/updater' into feature/store-i18n 2026-10-23 22:24:51 +02:00
kevinveenbirkenbachandClaude Opus 5.5 8c8e6a1242 feat(web-opt-rdr-domains): redirect a whole legacy zone with one wildcard source
redirect_domain_mappings now renders its source and target, so an inventory can map *.{{ OLD_DOMAIN }} and the apex onto a target; keep_path: false drops the request URI from the 301. Wildcards get no www. variant, sys-dns-wildcards adds the *.<zone> A/AAAA records for every zone with a wildcard source next to DOMAIN_PRIMARY, and the CSP health check skips wildcard names.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-23 20:17:19 +02:00
kevinveenbirkenbachandClaude Opus 5.5 9152c884d8 fix(tls): serve every redirect source the certificate that names it
The cert lookup handed an alias or redirect source the live directory of its app's canonical domain or of its target, so social.cymais.cloud got the infinito.nexus lineage and failed with 'Hostname mismatch'. With the san and dedicated flavors a domain term now resolves to its own live/<domain>, which cert_link_domains points at the lineage that covers it; application terms keep the canonical path. resolve_term follows redirect chains and fails on a cycle.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-23 20:14:21 +02:00
kevinveenbirkenbach a04f2c9895 Merge branch 'feature/updater' into feature/store-i18n 2026-10-23 17:32:32 +02:00
kevinveenbirkenbachandClaude Opus 5 677a63931c Merge branch 'feature/network/multi-network-serving' into feature/store-i18n
Resolves 68 conflicted files. The governing rule was our structure with their
network semantics: the tor boolean becomes a network mode (clearnet | tor |
multi) throughout the variant layer, while this branch's architecture and mesh
axes, its extracted modules and its i18n content stay.

Where the two sides covered different ground, both are kept rather than one
picked:

- ca_injected answers a ca_client on the deployment's TLS mode OR on an SSO
  issuer that resolves self-signed. 3a3e9ed2c9 records why: an issuer is not
  an object store, so keeping only one side drops the other's coverage.
- get_merged_domains keeps the loud refusal when svc-net-tor is deployed
  without a provisioned node, now gated on their tor_serves_host; their
  node_onion_of returns an empty string there, which would have served every
  onion-exclusive app on clearnet in silence.
- sso_cookie_domains stays the single source for oauth2-proxy's cookie scope.
  Their per-network branch omitted the issuer domain on the onion arm, which
  breaks the callback leg.
- The clearnet CSP batch carries --clearnet-timeout again; their batch dict
  hardcoded zero while the role still passes the flag.
- The dashboard keeps its i18n content dir, image, version and design
  resolution; their side reduced the logo sources to the bare service
  provider values.

Their side wins where it is measurably better: the onion mirror rewrite is
gated per vhost rather than per app, the Playwright onion decision reads the
pass's own domain instead of the role's canonical one, and the matomo
reinjection and dashboard config render per network sibling.

Eight files were broken by the merge without ever carrying a conflict marker,
because git marks only the lines both sides touched:

- utils/cache/domains.py lost the onionize block that sat between two hunks
- roles/sys-front-inj-all/templates/body_filter.lua.j2 called it
- web-app-bluesky, keycloak sso_proxy: the get_entity_name filter was renamed
- trigger/__main__.py, playwright_coverage.py and its test: the deleted tor
  module
- cli/administration/deploy/ci/timings.py read Label.tor
- plugins/filter/csp_filters.py called _aligned_url with four of six arguments
- tests/unit/python/utils/cache/test_domains.py referenced a renamed class
- tests/.../sys-ctl-hlth-csp/files/test_script.py mocked a function main no
  longer calls

Verified: the variant layer (155 tests, 65 subtests), ca_injected (18),
the CI layer (175), the domains cache including the restored guard (32),
retrigger inputs (9), timings and the CSP health script (53). make
quality-high clears autoformat; its remaining findings are tracked separately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-23 09:53:41 +02:00
kevinveenbirkenbachandClaude Opus 5.5 bfdf88fae0 fix(domains): read an application's domains where the cache puts them
meta/domains.yml lands on the top-level `domains` key of the merged
application, yet three readers still looked under `server.domains`,
which no role fills any more:

- current_play_redirect_domains built every mapping from an empty
  config, so each app redirected to itself and was dropped. Neither
  `domains.aliases` nor AUTO_BUILD_ALIASES produced a single redirect,
  and echoserver's update check listed nextcloud.infinito.nexus and the
  other entity domains as dropped by the upgrade.
- csp_skip_domains never found the canonical of a disabled service.
- the logout filter's fallback returned no domains.

The unit tests fed the old layout and passed regardless. They now use
the merged one, and a new test builds the redirect from keycloak's real
role defaults.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 22:18:07 +02:00
kevinveenbirkenbachandClaude Opus 5 cbb336ed7d Merge branch 'main' into feature/store-i18n
Eight conflicts, none resolved by taking a side wholesale.

compose_file_args.py: main's removal of the dead templar parameter kept, its _value_has_domain/_has_domain helpers dropped. They served only the inlined CA check that the ca_injected lookup replaces, and the call site already carried main's signature.

web-app-penpot/01_administrator.yml: main's side. PR #1170 moves the admin password from the environment and sh -lc into stdin, and the test_quote_in_shell lint it ships enforces that.

web-app-mattermost/meta/services.yml: this side. The source build exists so the image is multi-arch; main's bump to mattermost/mattermost-team-edition:12.0.1 would drop that silently. The version lag is a follow-up.

web-app-bigbluebutton: main's b83b361ea9 runs BBB v3.0.39, where upstream replaced etherpad with bbb-shared-notes-server, so the ep_redis_publisher patch, the postgres entrypoint and the initdb hook go with their files. html5_settings and its render task stay: git log -S shows main carries no commit for them, they came from 76e9441117 on this side. vars/main.yml takes main's BBB_PINNED_SERVICES with this side's entity_name filter, because get_entity_name no longer exists here. The README keeps this side's feature bullets with main's etherpad-free wording; main's Cosmos section is dropped, the branch does not carry that section.

tests/integration/iam/passwords/test_quote_in_shell.py: this side's STDIN_BLOCK_RE, which the merged file uses at line 116.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-22 19:34:17 +02:00
kevinveenbirkenbach 17918992c7 Merge branch 'main' into feature/network/multi-network-serving
# Conflicts:
#	plugins/lookup/compose_file_args.py
#	roles/web-app-dashboard/tasks/02_manager_ops.yml
#	roles/web-app-dashboard/vars/main.yml
2026-10-22 18:52:10 +02:00
kevinveenbirkenbachandClaude Opus 5 c6436924e9 fix(objstore): keep the server-side endpoint on the provider's host
40b178d9a4 rebased all four public_* values onto the consumer's own origin, but only two of them face a browser. public_endpoint is the client endpoint an SDK inside the container dials: funkwhale's AWS_S3_ENDPOINT_URL and decidim's AWS_ENDPOINT. A container has no Tor resolver, so an onion-only consumer would have handed boto3 an address it cannot resolve, turning funkwhale's CERTIFICATE_VERIFY_FAILED into a connect failure.

public_bucket and public_url keep the rebase, since those are the base URLs the browser dials. public_domain keeps the provider host and has no direct consumer.

1e61debad2 established why funkwhale signs against the host it dials: query-string SigV4 binds the Host, so the signing host and the fetch host must be the same string. funkwhale is the only role that sets AWS_QUERYSTRING_AUTH, and it reads public_endpoint, so no rebased consumer signs and the same-origin route cannot produce SignatureDoesNotMatch. Attachment fetches on an onion pick stay failing, which that commit records as the accepted state.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-22 18:28:18 +02:00
kevinveenbirkenbachandClaude Opus 5 3a3e9ed2c9 fix(ca_injected): give the CA to a ca_client whose own TLS is off
Funkwhale is served on onion only, so resolve_enabled returns False for its primary and it resolves no TLS mode of its own. Its api container still signs and fetches media against the object store over https, and without the root CA botocore ends the theming converge with 'SSL validation failed for https://api.seaweedfs.s3.<domain>/funkwhale/dynamic_preferences/ui__logo/... CERTIFICATE_VERIFY_FAILED: unable to get local issuer certificate'. That is the job's only fatal.

The ca_client declaration already answered an application with no domain at all on the deployment mode. Resolving no mode of its own has a second shape, an application that has a domain but whose own TLS is disabled, and the onion is exactly that. Both branches now share _declares_ca_client and _on_the_deployment_mode. An application whose own TLS is enabled keeps deciding on its own resolved mode, which test_a_ca_client_with_a_domain_follows_its_own_tls_mode still pins.

web-app-funkwhale declares ca_client on its api service, the container env.j2 hands AWS_CA_BUNDLE to.

feature/network/multi-network-serving carries 69aee158f6, which answers a ca_client on the resolved mode of the SSO issuer it calls so Synapse reaches a clearnet Keycloak. An issuer is not an object store, so the two cover different peers and a merge that keeps only one side drops the other's coverage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-22 18:25:33 +02:00
kevinveenbirkenbachandClaude Opus 5 40b178d9a4 fix(objstore): serve browser assets from the consumer's own origin
web-svc-seaweedfs publishes no onion vhost by the tor-contract in its meta/services.yml, so a consumer served only on onion domains was handed an asset origin on the provider's clearnet host. A Tor-proxied browser resolves that name through nothing: BookWyrm logged 19 CSP violations for api.seaweedfs.s3.<domain>/bookwyrm/static, which also took down its theme link and logo specs because bookwyrm.js never loaded.

The lookup now rebases public_domain, public_bucket, public_endpoint and public_url onto the consumer's own origin plus /objstore whenever it is onion-only and the provider is not, and exposes proxy_required, proxy_prefix, upstream_domain and upstream_scheme. A self-guarding nginx fragment routes that prefix over the loopback to the provider's own vhost, so no vhost joins the object-store network and the proxy never becomes a consumer with a bucket it does not use. The location carries the bucket and proxy_pass repeats it; a prefix-only route would serve every bucket of the store under each consumer's origin.

BookWyrm additionally pins AWS_S3_URL_PROTOCOL, because upstream derives PROTOCOL from DOMAIN and calls everything but localhost https, so its asset URLs stayed https where the page is http.

The vhost cross-cutting lint gains an APPLIES predicate: the route is required of an override only when the role declares an object-store engine, which takes the finding from 15 overrides down to the 5 that consume one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-22 17:04:07 +02:00
kevinveenbirkenbachandClaude Opus 5.5 b83b361ea9 fix(web-app-bigbluebutton): run BBB v3.0.39 with the shared-notes server
The version-source update in #1146 moved the bigbluebutton/docker ref from
v3.0.23 to v3.0.39. That tag changes three things the role did not
handle, and it exposed a fourth defect in compose_file_args.

1. generate-compose renders etherpad and bbb-pads only behind
   USE_INSECURE_ETHERPAD and otherwise emits bbb-shared-notes-server with
   a `build:` block and no `image:` (upstream adds the image only after
   the tag, in 9a6a9a9a4c). compose_mods drops `build` only where an image
   is set, so the guard "Fail if any service still builds locally" stopped
   every BigBlueButton deploy (main run 39749899667, Nextcloud#11:
   `rc: 0`, `stdout: '    build:'`, bootstrap_compose_project.yml:35).
   meta/services.yml now pins alangecker/bbb-shared-notes-server:v3.0.39,
   compose_mods takes `pinned` (BBB_PINNED_SERVICES, every service the
   role config pins an image for) and drops their `build`, and the
   etherpad leftovers go (ep_redis_publisher patch, etherpad postgres
   entrypoint and initdb hook, etherpad database tasks, ETHERPAD_* env,
   etherpad_api_key secret).

2. The notes server connects to postgres once at start and exits with 2
   when it is not up yet; upstream gives it no restart policy. The
   override sets the role's restart policy, so it retries until postgres
   accepts connections.

3. Greenlight moves from v3.8.0 to v3.9.0.1, which sets
   `config.hosts = URL_HOST`, and the template passes
   `URL_HOST: https://${DOMAIN}`. Rails compares hostnames, so every
   request was blocked (`Blocked hosts: bbb.talk.infinito.test`, 403).
   Greenlight documents URL_HOST as the bare hostname; the override sets
   it to the role domain.

4. compose_file_args decided whether a role ships a compose override by
   templating "{{ application_id | abs_role_path_by_application_id }}"
   through the templar. ansible-core 2.21 does not render an untrusted
   string, so the check saw the literal and always returned False: the CA
   override generation and the pull handler never read BBB's
   compose.override.yml. That was harmless while compose.yml alone was
   valid; with the notes-server image living in the override, `compose
   config` failed ("has neither an image nor a build context"). The check
   now resolves the role path for the application id it is given. BBB is
   the only role that ships an override, so no other role's file list
   changes.

Existing installations drop the etherpad and bbb-pads containers on the
next `compose up --remove-orphans`; the old `etherpad` database stays in
PostgreSQL untouched.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 16:56:48 +02:00
kevinveenbirkenbach 396c937033 Merge branch 'main' into feature/store-i18n
# Conflicts:
#	cli/build/docs/integration_matrix/__init__.py
#	tests/lint/ansible/roles/meta/__init__.py
#	utils/github/variant/axes.py
2026-10-21 00:09:53 +02:00
kevinveenbirkenbach fdb6e668b2 Merge branch 'main' into feature/network/multi-network-serving
# Conflicts:
#	utils/github/variant/axes.py
2026-10-20 22:56:45 +02:00
kevinveenbirkenbachandClaude Opus 5.5 6e776a5780 refactor(python): clear the open CodeQL code-quality alerts
- 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>
2026-10-20 21:40:58 +02:00
kevinveenbirkenbachandClaude Opus 5 f46c4d0bab fix(storage): keep the NFS client off the ports the platform publishes
A swarm job lost every published port of the mail stack without anything
reporting it. dockerd aborted the whole ingress program:

  Failed to add ingress: failed to program ingress ports:
  failed to bind host port 0.0.0.0:995/tcp: address already in use

/proc/net/tcp named the squatter: 10.100.1.1:995 -> 10.100.1.11: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 pop3s. Afterwards none of 25/465/993/995/4190/8095 was bound while a
sibling service's port was, and stalwart still reported 1/1 healthy
because its healthcheck runs inside the container. The e2e CLI probe
found it two hours later as ECONNREFUSED on SMTP 25.

993 and 995 are the only published ports repository-wide inside that
window, so this is luck of the draw on any NFS-backed swarm host that
runs the mail role.

insecure on the kernel export is not separate hygiene, it is the other
half: kernel nfsd defaults to secure and answers a mount from an
unprivileged source port with EACCES, so noresvport without it would
break every kernel-flavor mount. It reaches the kernel flavor only,
since templates/exports.j2 is reached from tasks/flavor/kernel/main.yml
alone. The ganesha flavor needs nothing: ganesha.conf.j2 sets no
Privileged_Port and the default 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. The role already
anticipates that, releasing DIR_BACKUPS first when the check-mode probe
reports a change. Nothing bind-mounts that path into a container, so
the exposure is a host-side job holding it at that instant, which fails
loudly rather than silently.

Not yet proven in effect: a source port is chosen when the RPC
connection is established, so a plain remount can leave a running mount
on its old reserved port until the next real mount. Reading
/proc/net/tcp after the first deploy on an existing client is what
confirms it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-20 19:38:01 +02:00
kevinveenbirkenbach feb5630f19 Merge remote-tracking branch 'origin/main' into feature/network/multi-network-serving 2026-10-20 17:14:44 +02:00
kevinveenbirkenbach c3398ef317 Merge branch 'main' into feature/store-i18n 2026-10-19 03:25:44 +02:00
kevinveenbirkenbach 15930945fe Merge branch 'main' into feature/store-i18n 2026-10-19 01:54:37 +02:00
kevinveenbirkenbachandClaude Opus 5.5 7f28564548 Merge branch 'main' into feature/network/multi-network-serving
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>
2026-10-19 00:15:05 +02:00
kevinveenbirkenbachandClaude Opus 5 a10c0db10e fix(svc-bkp-volume-2-local): exclude a volume by the name docker gives it
A meta/volumes.yml entry that declares backup: false without pinning a
name: had its semantic key emitted into --volumes-no-backup-required.
Docker names such a volume <project>_<key>, and baudolo compares the string
exactly, so the exclusion matched nothing and the volume was copied anyway.

web-app-docs hit it in CI: docs_sites was emitted, the live volume is
docs_docs_sites, and the 17 GB regenerable sphinx tree was backed up into
the 30 GB container root. That filled it and took four jobs of run
39137606155 down with three different symptoms: no space left during the
copy, rsync reading the tree while the builder still wrote to it, and
hlth-disc-space then refusing to start on a full filesystem.

deployed_volume_name() now derives the name the way compose does: the
pinned name: when there is one, otherwise the role's entity name, which is
the compose project, joined to the key. The docstring claimed the old
behaviour was correct and is fixed too.

Three roles were silently still backed up and are now excluded as declared:
web-app-docs, web-app-jellyfin and web-svc-api. The nine entries that pin a
name are unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-18 23:08:45 +02:00
kevinveenbirkenbachandClaude Opus 5.5 474849a39c fix(svc-bkp-volume-2-local): read the baudolo version from pyproject.toml
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>
2026-10-18 21:44:40 +02:00
kevinveenbirkenbachandClaude Opus 5.5 4c6f9d5cd7 refactor(networks): decide a handover sibling in one lookup and proxy it as the canonical host
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>
2026-10-18 15:19:57 +02:00
kevinveenbirkenbachandClaude Opus 5 bab08063f3 feat(plugins): ask for a container-network origin instead of assembling one
A sibling container is reached at http://<entity name>:<internal port>,
and that string was written out per call site: a literal protocol, an
entity_name interpolation for the host, and a lookup('config', ...,
'services.<key>.ports.internal.http') for the port. Nine of them across
seven roles, each needing its own `# nocheck: literal-protocol-lookup`,
because the sibling lint rightly objects to a protocol glued in front of
a lookup - the protocol is supposed to come from wherever the TLS
posture is decided.

A container network has no TLS posture. It speaks plaintext by
construction, which is a property of the network rather than of any
caller, so stating it nine times stated it eight times too often.
lookup('container_url', application_id, service_key) holds all three
facts, and because no literal protocol is left at the call site the
suppression markers go with them.

The port is deliberately the internal one. A sibling that goes out to
the published port leaves the container network and comes back through
the ingress, which the dns-search suffix turns into the public FQDN.
The lookup refuses a service that declares no internal http port rather
than falling back to anything.

test_container_url_lookup.py keeps the shape from coming back: a
protocol, an interpolation mentioning entity_name, a colon, a second
interpolation. Nothing else matches that, and `# nocheck:
container-url-lookup` covers an origin this lookup genuinely cannot
express - a port that is not ports.internal.http, or a host that is not
the application's entity.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-18 02:13:05 +02:00
kevinveenbirkenbachandClaude Opus 5.5 69aee158f6 fix(ca_injected): answer a ca_client on the issuer it calls, and let Matrix opt in
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>
2026-10-16 22:35:41 +02:00
kevinveenbirkenbachandClaude Opus 5 df0e98f146 fix(ca): pass the CA override to the deployment that was granted it
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>
2026-10-16 22:02:57 +02:00
kevinveenbirkenbachandClaude Opus 5 b2e594073e fix(ca): give the CA to a service that only calls the self-signed endpoint
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>
2026-10-16 22:02:56 +02:00
kevinveenbirkenbachandClaude Opus 5.5 8e7dd508c9 style(tests): apply the formatter to the network_served tests
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>
2026-10-16 21:40:19 +02:00
evangelostsak 6f24461720 refactor: drop rationale and narration from tests/unit/python/plugins/lookup/test_mesh_ranges.py 2026-10-16 19:10:01 +02:00
evangelostsak d480c2785e refactor: drop rationale and narration from tests/unit/python/plugins/lookup/test_mesh_address.py 2026-10-16 19:10:01 +02:00
kevinveenbirkenbachandClaude Opus 5.5 660ad2ff16 fix(web-app-seaweedfs): keep the frontend on every node that serves clearnet
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>
2026-10-16 13:35:29 +02:00
kevinveenbirkenbachandClaude Opus 5 adacac7f29 fix(mcp): present the account's login in basic auth, not the users-dict key
The WordPress MCP endpoint answered 401 to every authenticated call, and
web-app-hermes and web-app-openclaw failed their CLI tests as cascades of the
same refusal. The WordPress container's own Apache log settles it: 121 POST,
100 GET and 21 HEAD on the MCP path, each with %u = mcp-web-app-wordpress and
status 401. A populated %u rules out a stripped Authorization header, and the
body is infinito_mcp_unauthorized, which only the mu-plugin's rest_pre_dispatch
guard emits - a wrong password for an existing user would surface WP core's
incorrect_password instead. So WordPress never resolved a user, because the
deploy sent the users-dict key while the login the deploy creates is
mcp-wordpress.

7916c0177e introduced the split when it capped usernames, deliberately keeping
the key because meta/mcp.yml's owner and the token store wire to it.
web-app-wordpress is the only role with auth: basic_auth - every other MCP
provider uses bearer, where the owner never travels - which is why only this
one broke.

resolve_credential now also returns the login, mcp_servers emits it on every
discovered entry, and the filter encodes login:token. It raises when no login
resolves rather than authenticating as the empty user, so the failure is loud
instead of another 401. The new mcp_client_login lookup shares
resolve_provider_credential with mcp_client_bearer so a second resolver cannot
drift from the first, and a test pins the two to the same function object.
test.env.j2 is the one place that hand-builds the server dict instead of
reading MCP_DISCOVERED_SERVERS, so it passes login as well.

The lint closes the class: auth: basic_auth requires credential.owner to be a
key some role's meta/users.yml declares. Without it a future basic-auth
provider sourced from credentials would hit the same fallback silently. It
imports BASIC_AUTHS from the filter so the rule and the renderer cannot drift
onto different spellings.

Tool names were verified against the adapter version the addon actually
installs, v0.7.0, not the v0.5.0 recorded in meta/mcp.yml: the sanitizer turns
the three registered abilities into infinito-get-post,
infinito-list-categories and infinito-search-posts, matching the rendered
allowlist exactly. Adding login to a discovered entry changes nothing for the
five consumers - three of them do not list basic_auth in supported_auths and
never see this server at all, and the other two read named keys only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-16 12:37:30 +02:00
kevinveenbirkenbachandClaude Opus 5 5b69b5c7c6 Merge branch 'feature/designer' into feature/store-i18n
Six conflicts, all from git pairing store-i18n's renamed roles with their designer originals.

web-app-meta/meta/services.yml and web-app-sphinx/vars/main.yml: store-i18n renamed web-app-sphinx to web-app-meta and dropped its custom build, so the role keeps the prebuilt ghcr.io/infinito-nexus/meta image and the old sphinx role stays deleted.

svc-ai-tika/templates/compose.yml.j2: a bogus rename pairing dropped sphinx build lines into the tika service. Rejected; the template is byte-identical to its pre-merge state again.

web-app-docs/templates/Dockerfile.j2: the designer file renders sphinx under the design title and reads SPHINX_IMAGE, SPHINX_OUTPUT_DIR and SPHINX_EXEC_DIR, none of which exist in web-app-docs, which builds from files/Dockerfile. Rejected.

web-app-docs/templates/playwright.env.j2 and files/playwright/test-design.js: both sides add independently, so both are kept. Of the designer's three new env reads only designTitle survives; iconUrl and faviconUrl are shadowed inside the test by stricter requireDotenvValue reads and were dead at module scope.

plugins/lookup/email.py merges clean toward designer, which is intended rather than a loss: 08ef66d8de (2026-10-08) replaces the submission_via_relay port-25 rule of 233f15f3e0 (2026-07-01) with per-bot app passwords under SSO, and carries the matching 04_manage_user/app_password tasks.

Checked after resolving: no duplicate YAML keys across the 17 merged YAML files, and no nocheck line dropped above a renamed key.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-15 22:21:50 +02:00
kevinveenbirkenbachandClaude Opus 5 1e61debad2 fix(objstore): sign funkwhale's media URLs against the host the browser dials
funkwhale has two S3 surfaces sharing one private bucket. Audio goes out as X-Accel-Redirect and the front nginx fetches it server-side, but attachments (instance logo, avatars, covers) land in the API JSON as absolute URLs the BROWSER dials. AWS_S3_ENDPOINT_URL drives the boto3 client, so with the internal endpoint django-storages signed URLs for http://seaweedfs:8333, a host no browser can reach.

Point the client at the new bucket-less public_endpoint key. Measured against the live gateway: a URL signed for seaweedfs:8333 and sent to the public host answers 403 SignatureDoesNotMatch, while one signed for the public host reaches bucket lookup — query-string SigV4 binds the Host, so the signing host and the fetch host must be the same string. AWS_S3_CUSTOM_DOMAIN cannot serve this: storages/backends/s3.py:689 returns UNSIGNED without a CloudFront signer, and the bucket also holds the music library that music/views.py exists to gate.

AWS_CA_BUNDLE follows, because the public endpoint is TLS on the local CA and botocore reads its trust store only from that variable (configprovider.py:59) — sys-svc-compose-ca injects SSL_CERT_FILE, REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE and NODE_EXTRA_CA_CERTS, never this one. It takes the bundle rather than the bare root cert, since AWS_CA_BUNDLE replaces the default store.

AWS_QUERYSTRING_AUTH=True fixes a pre-existing break: upstream defaults it to `not PROXY_MEDIA`, which produced unsigned URLs against a private bucket. The new front nginx.conf.j2 serves /_protected/media, which the stock template resolves with `alias ${MEDIA_ROOT}/$1;` and therefore 404s for every S3-backed listen.

web-svc-seaweedfs keeps no onion vhost. Its tor-contract reason now states the mechanical cause instead of the old claim that the role is never a Tor-browser surface, which decidim and funkwhale have made false: the node hidden service publishes http only while sys-front-tls keys listen/ssl on application_id, so an onion vhost would listen on 443 against a port-80 hidden service. Attachment fetches therefore still fail on an onion pick, and the spec is left to fail there rather than record the miss and pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-15 22:14:38 +02:00