mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 11:46:35 +00:00
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>
343 lines
12 KiB
Python
343 lines
12 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,
|
|
tls_mode="self_signed",
|
|
peer_enabled=True,
|
|
peer_mode="self_signed",
|
|
issuers=(ISSUER,),
|
|
):
|
|
calls: list[tuple[str, list]] = []
|
|
own_domain = domains.get(app)
|
|
|
|
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] == own_domain:
|
|
return [enabled if args[1] == "enabled" else mode]
|
|
if args[0] == app:
|
|
return [False if args[1] == "enabled" else "off"]
|
|
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),
|
|
):
|
|
variables = {} if tls_mode is None else {"TLS_MODE": tls_mode}
|
|
result = plugin.run([app], variables=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_own_tls_is_read_on_the_canonical_domain_not_the_vhost_sibling(self):
|
|
verdict, calls = self._resolve(
|
|
domains={"web-app-moodle": "moodle.example"},
|
|
enabled=True,
|
|
mode="self_signed",
|
|
)
|
|
self.assertIs(
|
|
verdict,
|
|
True,
|
|
"asked by application id, tls aligns to the vhost in `domain`; a "
|
|
"handler flushed after the onion sibling's vhost then reads TLS off "
|
|
"and the next stack deploy drops the CA mount",
|
|
)
|
|
self.assertEqual(calls[1], ("tls", ["moodle.example", "enabled"]))
|
|
|
|
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_domainless_ca_client_gets_the_ca_on_the_deployment_mode(self):
|
|
verdict, calls = self._resolve(
|
|
domains={},
|
|
enabled=True,
|
|
mode="self_signed",
|
|
services={"agent-broker": {"ca_client": True}, "socket-proxy": {}},
|
|
issuers=(),
|
|
)
|
|
self.assertIs(
|
|
verdict,
|
|
True,
|
|
"with no issuer to answer for it, a domainless ca_client still needs "
|
|
"the CA whenever the deployment itself is self-signed",
|
|
)
|
|
self.assertEqual([name for name, _ in calls], ["domains", "config"])
|
|
|
|
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}},
|
|
tls_mode="self_signed",
|
|
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_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",
|
|
issuers=(),
|
|
)
|
|
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={},
|
|
enabled=True,
|
|
mode="self_signed",
|
|
services={"agent-broker": {"ca_client": True}},
|
|
tls_mode="letsencrypt",
|
|
issuers=(),
|
|
)
|
|
self.assertIs(verdict, False)
|
|
|
|
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",
|
|
tls_mode="letsencrypt",
|
|
)
|
|
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",
|
|
tls_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=(),
|
|
tls_mode="letsencrypt",
|
|
)
|
|
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_a_ca_client_refuses_an_absent_tls_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}},
|
|
issuers=(),
|
|
tls_mode=None,
|
|
)
|
|
|
|
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()
|