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>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-10-22 18:25:33 +02:00
1 parent 3bd0b62b97
commit 3a3e9ed2c9
3 files changed
+50 -7

No files matched your search

+23 -7
View File
@@ -11,11 +11,19 @@ lived as four hand-copied expressions - both `sys-svc-compose` handlers,
per place that can drift.
Those three ask whether the application is *served* over the self-signed CA.
An application that only *calls* one has no domain of its own and would be
An application that only *calls* one resolves no mode of its own and would be
skipped, so a service may declare ``ca_client: true`` and be answered on the
deployment's TLS mode alone. Without it such a container verifies against an
empty trust store and fails every outbound call at the TLS handshake.
Resolving no mode of its own covers two shapes, and the declaration answers
for both: an application with no domain at all, and one whose own TLS is
disabled. The second is the onion case, where ``resolve_enabled`` returns
False for a ``.onion`` primary; the application has a domain, is served
without TLS, and still needs the CA for its outbound calls. An application
whose own TLS *is* enabled keeps deciding on its own resolved mode, which may
override the deployment default, so the declaration never answers for it.
Returns a plain bool, so callers read as a predicate:
when: lookup('ca_injected', application_id)
@@ -59,22 +67,30 @@ class LookupModule(LookupBase):
args, variables=variables or {}
)[0]
if not has_domain(_run("domains", []), application_id):
def _declares_ca_client() -> bool:
services = _run("config", [application_id, "services", {}]) or {}
if not any(
return any(
_to_bool(service.get("ca_client", False))
for service in services.values()
if isinstance(service, dict)
):
return [False]
)
def _on_the_deployment_mode() -> bool:
mode = as_str(require(variables or {}, "TLS_MODE", str))
if mode not in AVAILABLE_FLAVORS:
raise AnsibleError(
f"ca_injected: TLS_MODE must be one of "
f"{sorted(AVAILABLE_FLAVORS)}, got '{mode}'"
)
return [mode == SELF_SIGNED_MODE]
return mode == SELF_SIGNED_MODE
if not has_domain(_run("domains", []), application_id):
if not _declares_ca_client():
return [False]
return [_on_the_deployment_mode()]
if not _to_bool(_run("tls", [application_id, "enabled"])):
return [False]
if not _declares_ca_client():
return [False]
return [_on_the_deployment_mode()]
return [_run("tls", [application_id, "mode"]) == SELF_SIGNED_MODE]
@@ -37,6 +37,7 @@ api:
path: api/v1/instance/nodeinfo/2.0
name: funkwhale-api
image: funkwhale/api
ca_client: true
version: 2.0.12
min_storage: 588MB
ports:
@@ -140,6 +140,32 @@ class TestCaInjectedLookup(unittest.TestCase):
)
self.assertEqual([name for name, _ in calls], ["domains", "tls", "tls"])
def test_an_onion_served_ca_client_gets_the_ca_on_the_deployment_mode(self):
verdict, _ = self._resolve(
domains={"web-app-moodle": "abc.onion"},
enabled=False,
mode="self_signed",
services={"api": {"ca_client": True}},
tls_mode="self_signed",
)
self.assertIs(
verdict,
True,
"resolve_enabled returns False for a .onion primary, so an onion app "
"has a domain and no TLS of its own; without the CA its outbound call "
"to a self-signed endpoint dies at the handshake",
)
def test_an_onion_served_app_without_the_declaration_still_gets_nothing(self):
verdict, _ = self._resolve(
domains={"web-app-moodle": "abc.onion"},
enabled=False,
mode="self_signed",
services={"api": {}},
tls_mode="self_signed",
)
self.assertIs(verdict, False)
def test_a_ca_client_gets_nothing_when_the_ca_is_not_self_signed(self):
verdict, _ = self._resolve(
domains={},