diff --git a/bot_bottle/egress_addon_core.py b/bot_bottle/egress_addon_core.py index 354876f..8b8280e 100644 --- a/bot_bottle/egress_addon_core.py +++ b/bot_bottle/egress_addon_core.py @@ -418,10 +418,11 @@ class PolicyResolverLike(typing.Protocol): # to begin with — the failure mode that made a bricked registration read like # a misconfigured allowlist. DENY_UNATTRIBUTED = ( - "egress: this bottle is not registered with the orchestrator, so it has " - "no egress policy at all and every host is denied. The bottle's registry " - "row is missing or ambiguous — it was torn down, or another bottle claimed " - "its source IP. Relaunch the bottle; this is not an allowlist problem." + "egress: this request was not attributed to any bottle, so no egress " + "policy applies and every host is denied. Either the bottle's registry " + "row is missing/ambiguous (torn down, or another bottle claimed its " + "source IP), or the request carried no matching identity token — check " + "that the caller's proxy URL includes it. This is not an allowlist problem." ) DENY_UNPARSEABLE = ( "egress: this bottle's egress policy could not be parsed, so it is being " diff --git a/tests/unit/test_egress_multitenant.py b/tests/unit/test_egress_multitenant.py index 01219d7..6d99e0d 100644 --- a/tests/unit/test_egress_multitenant.py +++ b/tests/unit/test_egress_multitenant.py @@ -130,13 +130,17 @@ class TestDenyReasonNamesTheRealFault(unittest.TestCase): cfg = resolve_client_config(resolver, "10.243.0.1") # type: ignore[arg-type] return decide(cfg.routes, host, "/v1/x", {}, deny_reason=cfg.deny_reason).reason - def test_unattributed_says_unregistered_not_allowlist(self) -> None: + def test_unattributed_says_unattributed_not_allowlist(self) -> None: reason = self._reason(_FakeResolver(result=None)) self.assertEqual(DENY_UNATTRIBUTED, reason) # The misleading claim is the one that must be gone: the host was # never "not in the allowlist" — there was no allowlist at all. self.assertNotIn("is not in the bottle's egress.routes allowlist", reason) - self.assertIn("not registered", reason) + # Both causes must be named. `/resolve` fail-closes on a missing row + # *and* on a token mismatch, and the message pointing only at the row + # sent us hunting for a deregistered bottle that was registered fine. + self.assertIn("registry row", reason) + self.assertIn("identity token", reason) def test_resolver_error_says_orchestrator_unreachable(self) -> None: self.assertEqual(DENY_RESOLVER_ERROR, self._reason(_FakeResolver(raises=True)))