mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 19:56:53 +00:00
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>
250 lines
8.2 KiB
Python
250 lines
8.2 KiB
Python
"""Unit tests for the ca_injected lookup plugin.
|
|
|
|
Pins the predicate that decides whether the self-signed root CA is bind-mounted
|
|
into an application's containers. Anything reading the mounted file must agree
|
|
with the handler gate that creates the mount, so every term is pinned here,
|
|
including their short-circuit order.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import unittest
|
|
import unittest.mock as mock
|
|
|
|
from ansible.errors import AnsibleError
|
|
|
|
from . import PROJECT_ROOT
|
|
|
|
MODULE = "lookup_ca_injected"
|
|
ISSUER = "web-app-keycloak"
|
|
|
|
|
|
def _load_lookup():
|
|
spec = importlib.util.spec_from_file_location(
|
|
MODULE, str(PROJECT_ROOT / "plugins/lookup/ca_injected.py")
|
|
)
|
|
mod = importlib.util.module_from_spec(spec)
|
|
assert spec and spec.loader
|
|
spec.loader.exec_module(mod)
|
|
return mod
|
|
|
|
|
|
class _DummyTemplar:
|
|
def template(self, value):
|
|
return value
|
|
|
|
|
|
class TestCaInjectedLookup(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.mod = _load_lookup()
|
|
|
|
def _resolve(
|
|
self,
|
|
*,
|
|
domains,
|
|
enabled,
|
|
mode,
|
|
app="web-app-moodle",
|
|
services=None,
|
|
peer_enabled=True,
|
|
peer_mode="self_signed",
|
|
issuers=(ISSUER,),
|
|
):
|
|
calls: list[tuple[str, list]] = []
|
|
|
|
def fake_get(name, **_kwargs):
|
|
def run(args, variables=None):
|
|
calls.append((name, list(args)))
|
|
if name == "config":
|
|
return [services or {}]
|
|
if name == "domains":
|
|
return [domains]
|
|
if args[0] == app:
|
|
return [enabled if args[1] == "enabled" else mode]
|
|
if isinstance(peer_mode, Exception) and args[1] == "mode":
|
|
raise peer_mode
|
|
return [peer_enabled if args[1] == "enabled" else peer_mode]
|
|
|
|
return mock.Mock(run=run)
|
|
|
|
plugin = self.mod.LookupModule()
|
|
plugin._loader = None
|
|
plugin._templar = _DummyTemplar()
|
|
with (
|
|
mock.patch.object(self.mod.lookup_loader, "get", side_effect=fake_get),
|
|
mock.patch.object(self.mod, "issuer_applications", return_value=issuers),
|
|
):
|
|
result = plugin.run([app], variables={})
|
|
return result[0], calls
|
|
|
|
def test_a_self_signed_app_with_a_domain_gets_the_ca(self):
|
|
verdict, _ = self._resolve(
|
|
domains={"web-app-moodle": "moodle.example"},
|
|
enabled=True,
|
|
mode="self_signed",
|
|
)
|
|
self.assertIs(verdict, True)
|
|
|
|
def test_letsencrypt_does_not_get_the_ca(self):
|
|
verdict, _ = self._resolve(
|
|
domains={"web-app-moodle": "moodle.example"},
|
|
enabled=True,
|
|
mode="letsencrypt",
|
|
)
|
|
self.assertIs(verdict, False)
|
|
|
|
def test_tls_disabled_does_not_get_the_ca(self):
|
|
verdict, calls = self._resolve(
|
|
domains={"web-app-moodle": "moodle.example"},
|
|
enabled=False,
|
|
mode="self_signed",
|
|
)
|
|
self.assertIs(verdict, False)
|
|
self.assertEqual([name for name, _ in calls], ["domains", "tls", "config"])
|
|
|
|
def test_an_app_without_a_domain_never_asks_about_tls(self):
|
|
verdict, calls = self._resolve(domains={}, enabled=True, mode="self_signed")
|
|
self.assertIs(verdict, False)
|
|
self.assertEqual([name for name, _ in calls], ["domains", "config"])
|
|
|
|
def test_a_domainless_ca_client_gets_the_ca_from_a_self_signed_issuer(self):
|
|
verdict, calls = self._resolve(
|
|
domains={},
|
|
enabled=True,
|
|
mode="self_signed",
|
|
services={"agent-broker": {"ca_client": True}, "socket-proxy": {}},
|
|
)
|
|
self.assertIs(
|
|
verdict,
|
|
True,
|
|
"a container that only calls the self-signed endpoint has no domain "
|
|
"of its own, and without the CA every outbound call fails the "
|
|
"handshake",
|
|
)
|
|
self.assertEqual(
|
|
calls,
|
|
[
|
|
("domains", []),
|
|
("config", ["web-app-moodle", "services", {}]),
|
|
("tls", [ISSUER, "enabled"]),
|
|
("tls", [ISSUER, "mode"]),
|
|
],
|
|
)
|
|
|
|
def test_a_ca_client_with_a_domain_never_answers_on_the_deployment_mode(self):
|
|
verdict, calls = self._resolve(
|
|
domains={"web-app-moodle": "moodle.example"},
|
|
enabled=True,
|
|
mode="letsencrypt",
|
|
services={"agent-broker": {"ca_client": True}},
|
|
peer_mode="letsencrypt",
|
|
)
|
|
self.assertIs(
|
|
verdict,
|
|
False,
|
|
"an app that has a domain is served over its own resolved mode, which "
|
|
"may override the deployment default; the ca_client answer comes from "
|
|
"the issuer's resolved mode, never from the global TLS_MODE",
|
|
)
|
|
self.assertEqual(
|
|
[name for name, _ in calls],
|
|
["domains", "tls", "tls", "config", "tls", "tls"],
|
|
)
|
|
|
|
def test_a_ca_client_with_its_own_letsencrypt_still_trusts_a_self_signed_issuer(
|
|
self,
|
|
):
|
|
verdict, _ = self._resolve(
|
|
domains={"web-app-matrix": "matrix.example"},
|
|
enabled=True,
|
|
mode="letsencrypt",
|
|
app="web-app-matrix",
|
|
services={"matrix": {"ca_client": True}},
|
|
)
|
|
self.assertIs(
|
|
verdict,
|
|
True,
|
|
"a client served over its own letsencrypt certificate still calls the "
|
|
"issuer, whose per-app override may be self-signed",
|
|
)
|
|
|
|
def test_an_onion_only_ca_client_gets_the_ca_from_a_self_signed_issuer(self):
|
|
verdict, _ = self._resolve(
|
|
domains={"web-app-matrix": "matrix.example.onion"},
|
|
enabled=False,
|
|
mode="off",
|
|
app="web-app-matrix",
|
|
services={"matrix": {"ca_client": True}},
|
|
)
|
|
self.assertIs(
|
|
verdict,
|
|
True,
|
|
"an onion-only role has no TLS of its own but still calls the "
|
|
"self-signed clearnet issuer",
|
|
)
|
|
|
|
def test_a_ca_client_gets_nothing_when_the_issuer_serves_no_tls(self):
|
|
verdict, _ = self._resolve(
|
|
domains={"web-app-matrix": "matrix.example.onion"},
|
|
enabled=False,
|
|
mode="off",
|
|
app="web-app-matrix",
|
|
services={"matrix": {"ca_client": True}},
|
|
peer_enabled=False,
|
|
peer_mode="off",
|
|
)
|
|
self.assertIs(
|
|
verdict,
|
|
False,
|
|
"on a tor-only node the issuer is served over plain http, so there is "
|
|
"no self-signed endpoint to trust",
|
|
)
|
|
|
|
def test_a_ca_client_gets_nothing_when_the_issuer_is_not_self_signed(self):
|
|
verdict, _ = self._resolve(
|
|
domains={},
|
|
enabled=True,
|
|
mode="self_signed",
|
|
services={"agent-broker": {"ca_client": True}},
|
|
peer_mode="letsencrypt",
|
|
)
|
|
self.assertIs(verdict, False)
|
|
|
|
def test_a_ca_client_without_an_issuer_gets_nothing(self):
|
|
verdict, _ = self._resolve(
|
|
domains={},
|
|
enabled=True,
|
|
mode="self_signed",
|
|
services={"agent-broker": {"ca_client": True}},
|
|
issuers=(),
|
|
)
|
|
self.assertIs(verdict, False)
|
|
|
|
def test_a_ca_client_keeps_the_refusal_of_an_unresolvable_issuer_mode(self):
|
|
with self.assertRaises(
|
|
AnsibleError,
|
|
msg="a missing mode read as 'not self signed' withholds the CA in "
|
|
"silence, and every handshake then fails far from here",
|
|
):
|
|
self._resolve(
|
|
domains={},
|
|
enabled=True,
|
|
mode="self_signed",
|
|
services={"agent-broker": {"ca_client": True}},
|
|
peer_mode=AnsibleError("tls: required variable 'TLS_MODE' is missing"),
|
|
)
|
|
|
|
def test_the_term_is_required(self):
|
|
plugin = self.mod.LookupModule()
|
|
plugin._templar = _DummyTemplar()
|
|
with self.assertRaises(AnsibleError):
|
|
plugin.run([], variables={})
|
|
with self.assertRaises(AnsibleError):
|
|
plugin.run([" "], variables={})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|