From 2508ba5fb4feba720acf5a538549b3e8d66e1006 Mon Sep 17 00:00:00 2001 From: Cristi Bleotiu Date: Wed, 19 Aug 2026 16:42:04 +0300 Subject: [PATCH 1/2] feat: recognize R1 MeshDB service images What changed: - recognize exact R1 MeshDB and legacy image repositories - reject reserved database users before certificate side effects - pin the local verification bed to the signed digest and 500ms bound Why: - allow the final service image without changing persisted CockroachDB contracts --- .../deeploy-crdb-verify-full/README.md | 9 +- .../validate_workflow.py | 16 +++- extensions/business/deeploy/deeploy_mixin.py | 24 +++-- .../deeploy/tests/test_create_requests.py | 89 ++++++++++++++++++- .../deeploy/tests/test_update_requests.py | 5 +- 5 files changed, 123 insertions(+), 20 deletions(-) diff --git a/docker-compose/deeploy-crdb-verify-full/README.md b/docker-compose/deeploy-crdb-verify-full/README.md index d9ebf739..a6215f58 100644 --- a/docker-compose/deeploy-crdb-verify-full/README.md +++ b/docker-compose/deeploy-crdb-verify-full/README.md @@ -1,8 +1,8 @@ -# CockroachDB Verify-Full Testbed +# R1 MeshDB Verify-Full Testbed This local-only bed starts three real edge runtimes on a private broker. The -validator then uses the edge certificate-preparation helper and the published -CockroachDB service image to run a three-node SQL cluster on the same isolated +validator then uses the edge certificate-preparation helper and the exact +published R1 MeshDB digest to run a three-node SQL cluster on the same isolated network. No dAuth, Cloudflare, remote node, or live Deeploy resource is used. Run from the `edge_node` worktree root: @@ -15,7 +15,8 @@ PYTHONPATH=/mnt/c/repos/naeural_client:. /home/bleot/venvs/umbrella313/bin/pytho docker compose -f docker-compose/deeploy-crdb-verify-full.yaml down -v --remove-orphans ``` -The validator checks DNS hostname verification, wrong-host/wrong-CA/plaintext +The validator checks the production `CRDB_MAX_OFFSET=500ms` bound, DNS hostname +verification, wrong-host/wrong-CA/plaintext rejection, password authentication, three-node membership, data persistence across full certificate regeneration, delayed third-node convergence, and a packet capture with unique SQL canaries. It deletes raw captures, certificates, diff --git a/docker-compose/deeploy-crdb-verify-full/validate_workflow.py b/docker-compose/deeploy-crdb-verify-full/validate_workflow.py index 6abde10e..6dc13be1 100644 --- a/docker-compose/deeploy-crdb-verify-full/validate_workflow.py +++ b/docker-compose/deeploy-crdb-verify-full/validate_workflow.py @@ -13,9 +13,13 @@ NETWORK = "deeploy-crdb-verify-full" -IMAGE = os.environ.get("CRDB_IMAGE", "ghcr.io/ratio1/deeploy-cockroachdb-service:main") +IMAGE = os.environ.get( + "CRDB_IMAGE", + "ghcr.io/ratio1/r1-meshdb@sha256:3be00a63467628d0f5c3382be8ae7a885c5b658762dfd095fba0cb0b5549fab4", +) CLIENT_IMAGE = os.environ.get("CRDB_CLIENT_IMAGE", "postgres:16-alpine") SNIFFER_IMAGE = os.environ.get("CRDB_SNIFFER_IMAGE", "nicolaka/netshoot:v0.13") +MAX_OFFSET = os.environ.get("CRDB_MAX_OFFSET", "500ms") HOSTNAME = "crdb-client.test" NODES = ["verify_crdb_1", "verify_crdb_2", "verify_crdb_3"] ALIASES = ["roach1", "roach2", "roach3"] @@ -23,6 +27,11 @@ CLIENT = "verify_crdb_client" PASSWORD = "verify_full_disposable_password" +if "@sha256:" not in IMAGE: + raise ValueError("CRDB_IMAGE must be an immutable digest reference") +if MAX_OFFSET != "500ms": + raise ValueError("CRDB_MAX_OFFSET must remain at the production 500ms bound") + def run(args, *, check=True, capture=False, input_text=None, timeout=None): result = subprocess.run( @@ -114,7 +123,7 @@ def start_node(root, index): "start", "--certs-dir=/certs", "--store=/cockroach/cockroach-data", "--listen-addr=0.0.0.0:26257", f"--advertise-addr={alias}:26257", "--http-addr=0.0.0.0:8080", "--join=roach1:26257,roach2:26257,roach3:26257", - "--max-offset=2s", "--cache=.1", "--max-sql-memory=.1", + f"--max-offset={MAX_OFFSET}", "--cache=.1", "--max-sql-memory=.1", ) @@ -249,7 +258,8 @@ def main(): print( "verify-full workflow ok " - f"(nodes=3 rows=1000 ca_before={first_fingerprint[:12]} ca_after={second_fingerprint[:12]} capture_bytes={capture.stat().st_size})" + f"(nodes=3 rows=1000 max_offset={MAX_OFFSET} ca_before={first_fingerprint[:12]} " + f"ca_after={second_fingerprint[:12]} capture_bytes={capture.stat().st_size})" ) finally: cleanup(root) diff --git a/extensions/business/deeploy/deeploy_mixin.py b/extensions/business/deeploy/deeploy_mixin.py index cdf6d4cb..5b2ae1ce 100644 --- a/extensions/business/deeploy/deeploy_mixin.py +++ b/extensions/business/deeploy/deeploy_mixin.py @@ -92,7 +92,10 @@ "ACCESS_KEY", "ACCESSKEY", ) -COCKROACHDB_IMAGE_MARKER = "deeploy-cockroachdb-service" +COCKROACHDB_IMAGE_REPOSITORIES = frozenset(( + "ghcr.io/ratio1/deeploy-cockroachdb-service", + "ghcr.io/ratio1/r1-meshdb", +)) COCKROACHDB_CERT_ENV_KEYS = ( "CRDB_CA_CRT", "CRDB_NODE_CRT", @@ -107,6 +110,7 @@ "CRDB_USER", "CRDB_PASSWORD", ) +COCKROACHDB_RESERVED_USERS = frozenset(("root", "admin", "node", "public")) COCKROACHDB_MIN_TARGET_NODES = 3 COCKROACHDB_ALLOCATION_PARAM = "deeploy_cockroachdb" COCKROACHDB_CERT_REGENERATION_REQUEST_KEY = "cockroachdb_certificate_regeneration_id" @@ -3375,8 +3379,13 @@ def _is_cockroachdb_plugin_instance(self, instance): if not isinstance(instance, dict): return False image = instance.get("IMAGE") - if isinstance(image, str) and COCKROACHDB_IMAGE_MARKER in image.lower(): - return True + if isinstance(image, str): + repository = image.strip().lower().split("@", 1)[0] + last_slash = repository.rfind("/") + last_colon = repository.rfind(":") + if last_colon > last_slash: + repository = repository[:last_colon] + return repository in COCKROACHDB_IMAGE_REPOSITORIES env = instance.get("ENV") if isinstance(env, dict) and ( "CRDB_NODE_COUNT" in env or "CRDB_HOSTNAMES" in env @@ -3620,8 +3629,8 @@ def _validate_cockroachdb_auth_env(self, env): raise ValueError( "CockroachDB {} must be a SQL identifier: letters, digits, and underscores, not starting with a digit.".format(key) ) - if env.get("CRDB_USER", "").lower() == "root": - raise ValueError("CockroachDB CRDB_USER must not be root.") + if env.get("CRDB_USER", "").lower() in COCKROACHDB_RESERVED_USERS: + raise ValueError("CockroachDB CRDB_USER is reserved.") return True def _cockroachdb_cert_bundle_complete(self, instance, target_nodes): @@ -4082,10 +4091,8 @@ def _prepare_cockroachdb_secure_config_for_pipeline(self, pipeline, target_nodes continue for instance in instances: if self._is_cockroachdb_plugin_instance(instance): + self._validate_cockroachdb_auth_env(instance.get("ENV")) has_cockroachdb = True - break - if has_cockroachdb: - break if not has_cockroachdb: return pipeline @@ -4111,7 +4118,6 @@ def _prepare_cockroachdb_secure_config_for_pipeline(self, pipeline, target_nodes if not self._is_cockroachdb_plugin_instance(instance): continue env = instance.setdefault("ENV", {}) - self._validate_cockroachdb_auth_env(env) hostnames = [ item.strip() for item in str(env.get("CRDB_HOSTNAMES", "")).split(",") diff --git a/extensions/business/deeploy/tests/test_create_requests.py b/extensions/business/deeploy/tests/test_create_requests.py index cdf4a1dc..7823773a 100644 --- a/extensions/business/deeploy/tests/test_create_requests.py +++ b/extensions/business/deeploy/tests/test_create_requests.py @@ -118,6 +118,55 @@ def test_managed_service_kind_resolves_explicit_legacy_and_persisted_cockroachdb "cockroachdb", ) + def test_managed_service_kind_recognizes_exact_r1_meshdb_and_legacy_repositories(self): + plugin = make_deeploy_plugin() + digest = "sha256:" + ("a" * 64) + accepted = ( + "ghcr.io/ratio1/r1-meshdb:v1.0.0", + f"ghcr.io/ratio1/r1-meshdb@{digest}", + f"ghcr.io/ratio1/r1-meshdb:v1.0.0@{digest}", + "ghcr.io/ratio1/deeploy-cockroachdb-service:main", + f"ghcr.io/ratio1/deeploy-cockroachdb-service@{digest}", + f"ghcr.io/ratio1/deeploy-cockroachdb-service:main@{digest}", + ) + rejected = ( + "ghcr.io/example/r1-meshdb:latest", + "ghcr.io/ratio1/r1-meshdb-helper:latest", + "ghcr.io/ratio1/deeploy-cockroachdb-service2:main", + ) + + for image in accepted: + with self.subTest(image=image): + self.assertTrue(plugin._is_cockroachdb_plugin_instance({"IMAGE": image})) + for image in rejected: + with self.subTest(image=image): + self.assertFalse(plugin._is_cockroachdb_plugin_instance({"IMAGE": image})) + misleading = { + "IMAGE": image, + "plugin_name": "cockroachdb", + "ENV": {"CRDB_NODE_COUNT": "3"}, + } + self.assertFalse(plugin._is_cockroachdb_plugin_instance(misleading)) + + candidate = make_plugin_entry("CONTAINER_APP_RUNNER", IMAGE=accepted[1]) + self.assertEqual( + plugin._resolve_deeploy_service_kind( + inputs=make_inputs(service_kind="cockroachdb", plugins=[candidate]), + ), + "cockroachdb", + ) + + near_match = make_plugin_entry( + "CONTAINER_APP_RUNNER", + plugin_name="cockroachdb", + IMAGE=rejected[1], + ENV={"CRDB_NODE_COUNT": "3"}, + ) + with self.assertRaisesRegex(ValueError, "service kind.*does not match"): + plugin._resolve_deeploy_service_kind( + inputs=make_inputs(service_kind="cockroachdb", plugins=[near_match]), + ) + def test_managed_service_kind_rejects_unsupported_or_conflicting_identity(self): plugin = make_deeploy_plugin() postgres_plugin = make_plugin_entry( @@ -774,7 +823,7 @@ def test_cockroachdb_pipeline_secure_config_regenerates_for_legacy_scale_up(self self.assertIn("CRDB_NODE_KEY", by_node["0xai_node_d"]["ENV"]) self.assertEqual(by_node["0xai_node_d"]["ENV"]["CF_TUNNEL_TOKEN"], "token-d") - def test_cockroachdb_secure_config_rejects_root_or_unsafe_identifiers(self): + def test_cockroachdb_secure_config_rejects_reserved_or_unsafe_identifiers(self): plugin = make_deeploy_plugin() inputs = make_inputs( plugins=[ @@ -794,8 +843,42 @@ def test_cockroachdb_secure_config_rejects_root_or_unsafe_identifiers(self): plugin._prepare_cockroachdb_secure_config(inputs, ["0xai_node_a", "0xai_node_b", "0xai_node_c"]) inputs[DEEPLOY_KEYS.PLUGINS][0]["ENV"]["CRDB_DATABASE"] = "appdb" - with self.assertRaisesRegex(ValueError, "must not be root"): - plugin._prepare_cockroachdb_secure_config(inputs, ["0xai_node_a", "0xai_node_b", "0xai_node_c"]) + for reserved_user in ("root", "ROOT", "admin", "AdMiN", "node", "public"): + with self.subTest(reserved_user=reserved_user): + inputs[DEEPLOY_KEYS.PLUGINS][0]["ENV"]["CRDB_USER"] = reserved_user + with self.assertRaisesRegex(ValueError, "reserved"): + plugin._prepare_cockroachdb_secure_config( + inputs, + ["0xai_node_a", "0xai_node_b", "0xai_node_c"], + ) + + def test_legacy_pipeline_rejects_reserved_user_before_certificate_generation(self): + plugin = make_deeploy_plugin() + generation_calls = [] + plugin._generate_cockroachdb_cert_bundle = ( + lambda *args, **kwargs: generation_calls.append((args, kwargs)) + ) + pipeline = { + NetMonCt.PLUGINS: [{ + "SIGNATURE": "CONTAINER_APP_RUNNER", + "INSTANCES": [{ + "IMAGE": "ghcr.io/ratio1/deeploy-cockroachdb-service:main", + "ENV": { + "CRDB_DATABASE": "appdb", + "CRDB_USER": "admin", + "CRDB_PASSWORD": "secret-password", + }, + }], + }], + } + + with self.assertRaisesRegex(ValueError, "reserved"): + plugin._prepare_cockroachdb_secure_config_for_pipeline( + pipeline, + ["0xai_node_a", "0xai_node_b", "0xai_node_c"], + ) + + self.assertEqual(generation_calls, []) def test_deeploy_status_payload_keeps_cockroachdb_config_unredacted(self): payload = { diff --git a/extensions/business/deeploy/tests/test_update_requests.py b/extensions/business/deeploy/tests/test_update_requests.py index 8681657b..e3120bbb 100644 --- a/extensions/business/deeploy/tests/test_update_requests.py +++ b/extensions/business/deeploy/tests/test_update_requests.py @@ -1274,7 +1274,10 @@ def test_process_service_update_without_resolved_id_fails_before_side_effects(se fixture_plugin = make_deeploy_plugin() nodes, discovered_instances, request_plugin = self._make_four_replica_cockroach_update_fixture(fixture_plugin) request_plugin.pop(DEEPLOY_KEYS.PLUGIN_INSTANCE_ID) - request_plugin["IMAGE"] = "repo/reconfigured-service:latest" + request_plugin["IMAGE"] = ( + "ghcr.io/ratio1/r1-meshdb@sha256:" + "3be00a63467628d0f5c3382be8ae7a885c5b658762dfd095fba0cb0b5549fab4" + ) plugin, called = self._make_process_update_plugin( discovered_instances=discovered_instances, nodes=nodes, From 5c1adb53cd6e6dd457a950711247b8367962ac05 Mon Sep 17 00:00:00 2001 From: Cristi Bleotiu Date: Thu, 20 Aug 2026 23:09:12 +0300 Subject: [PATCH 2/2] fix: reject reserved database users before payment What changed: - validate reserved CockroachDB users immediately after service-kind resolution - cover create and update ordering before payment and node lookup Why: - prevent invalid managed-service requests from reaching downstream side effects --- .../business/deeploy/deeploy_manager_api.py | 2 + extensions/business/deeploy/deeploy_mixin.py | 12 +++++ .../deeploy/tests/test_process_request.py | 53 +++++++++++++++++++ .../deeploy/tests/test_update_requests.py | 46 ++++++++++++++++ 4 files changed, 113 insertions(+) diff --git a/extensions/business/deeploy/deeploy_manager_api.py b/extensions/business/deeploy/deeploy_manager_api.py index ba4decd4..822cd0f4 100644 --- a/extensions/business/deeploy/deeploy_manager_api.py +++ b/extensions/business/deeploy/deeploy_manager_api.py @@ -870,6 +870,7 @@ def _process_pipeline_request( if job_app_type not in JOB_APP_TYPES_ALL: job_app_type = JOB_APP_TYPES.NATIVE service_kind = self._resolve_deeploy_service_kind(inputs=inputs) + self._validate_managed_service_request_admission(service_kind, inputs) self.P(f"Resolved job app type: {job_app_type}") # persist job type so downstream mixins can adjust validations (e.g. native app resource checks) inputs[DEEPLOY_KEYS.JOB_APP_TYPE] = job_app_type @@ -937,6 +938,7 @@ def _process_pipeline_request( deeploy_specs=deeploy_specs_for_update, discovered_plugin_instances=discovered_plugin_instances, ) + self._validate_managed_service_request_admission(service_kind, inputs) self.P( f"Discovered {len(discovered_plugin_instances)} live plugin instance record(s) " f"for update job_id={job_id}, app_id={app_id}." diff --git a/extensions/business/deeploy/deeploy_mixin.py b/extensions/business/deeploy/deeploy_mixin.py index 5b2ae1ce..26f63c03 100644 --- a/extensions/business/deeploy/deeploy_mixin.py +++ b/extensions/business/deeploy/deeploy_mixin.py @@ -3477,6 +3477,18 @@ def _resolve_deeploy_service_kind( raise ValueError("Deeploy service kind does not match the requested plugin configuration.") return resolved_kind + def _validate_managed_service_request_admission(self, service_kind, inputs): + if service_kind != MANAGED_SERVICE_KIND_COCKROACHDB: + return True + for plugin_instance in inputs.get(DEEPLOY_KEYS.PLUGINS) or []: + if not self._is_cockroachdb_plugin_instance(plugin_instance): + continue + env = plugin_instance.get("ENV") + user = env.get("CRDB_USER") if isinstance(env, dict) else None + if isinstance(user, str) and user.lower() in COCKROACHDB_RESERVED_USERS: + raise ValueError("CockroachDB CRDB_USER is reserved.") + return True + def _validate_managed_service_target_change( self, service_kind, diff --git a/extensions/business/deeploy/tests/test_process_request.py b/extensions/business/deeploy/tests/test_process_request.py index bb6b6101..35743dda 100644 --- a/extensions/business/deeploy/tests/test_process_request.py +++ b/extensions/business/deeploy/tests/test_process_request.py @@ -186,6 +186,59 @@ def check_and_deploy_pipelines(**kwargs): self.assertIn("token-b", str(res[DEEPLOY_KEYS.REQUEST])) self.assertIsInstance(res[DEEPLOY_KEYS.REQUEST]["PER_NODE_CONFIG"], dict) + def test_create_rejects_reserved_cockroachdb_user_before_payment_or_node_lookup(self): + plugin = _ProcessRequestStub.__new__(_ProcessRequestStub) + plugin.ct = ct + plugin.bc = _BCStub() + plugin.deepcopy = copy.deepcopy + plugin.sanitize_name = lambda value: str(value).replace("/", "_").replace(" ", "_") + plugin.uuid = lambda size=7: "abc1234"[:size] + plugin.cfg_deeploy_verbose = 0 + plugin.queued_persistence = None + phase_calls = [] + plugin.deeploy_check_payment_and_job_owner = ( + lambda *args, **kwargs: phase_calls.append("payment") or True + ) + plugin._check_nodes_availability = ( + lambda inputs, **kwargs: phase_calls.append("nodes") + or list(inputs[DEEPLOY_KEYS.TARGET_NODES]) + ) + plugin.check_and_deploy_pipelines = ( + lambda **kwargs: phase_calls.append("deploy") + or ({}, DEEPLOY_STATUS.COMMAND_DELIVERED, {}, {}) + ) + + response = plugin._process_pipeline_request( + { + DEEPLOY_KEYS.APP_ALIAS: "cockroachdb", + DEEPLOY_KEYS.TARGET_NODES: ["0xai_node_a", "0xai_node_b", "0xai_node_c"], + DEEPLOY_KEYS.PIPELINE_INPUT_TYPE: "void", + DEEPLOY_KEYS.PIPELINE_INPUT_URI: None, + DEEPLOY_KEYS.PIPELINE_PARAMS: {}, + DEEPLOY_KEYS.CHAINSTORE_RESPONSE: False, + DEEPLOY_KEYS.JOB_APP_TYPE: "service", + DEEPLOY_KEYS.JOB_ID: 97, + DEEPLOY_KEYS.RETURN_REQUEST: False, + DEEPLOY_KEYS.PLUGINS: [{ + DEEPLOY_KEYS.PLUGIN_SIGNATURE: "CONTAINER_APP_RUNNER", + "IMAGE": "ghcr.io/ratio1/r1-meshdb@sha256:" + ("a" * 64), + "CONTAINER_RESOURCES": {"cpu": 1, "memory": "2g", "storage": "8g"}, + "ENV": { + "CRDB_DATABASE": "appdb", + "CRDB_USER": "AdMiN", + "CRDB_PASSWORD": "secret-password", + }, + }], + }, + is_create=True, + async_mode=True, + ) + + self.assertEqual(response[DEEPLOY_KEYS.STATUS], DEEPLOY_STATUS.FAIL) + self.assertIn("reserved", response[DEEPLOY_KEYS.ERROR]) + self.assertEqual(phase_calls, []) + self.assertEqual(plugin.bc.submitted, []) + def test_error_handler_redacts_secret_request_values(self): plugin = _ProcessRequestStub.__new__(_ProcessRequestStub) plugin.deepcopy = copy.deepcopy diff --git a/extensions/business/deeploy/tests/test_update_requests.py b/extensions/business/deeploy/tests/test_update_requests.py index e3120bbb..25ae1e56 100644 --- a/extensions/business/deeploy/tests/test_update_requests.py +++ b/extensions/business/deeploy/tests/test_update_requests.py @@ -1318,6 +1318,52 @@ def test_process_service_update_without_resolved_id_fails_before_side_effects(se self.assertEqual(called["delete"], 0) self.assertEqual(called["deploy"], 0) + def test_process_service_update_rejects_reserved_user_before_payment_or_node_lookup(self): + fixture_plugin = make_deeploy_plugin() + nodes, discovered_instances, request_plugin = self._make_four_replica_cockroach_update_fixture( + fixture_plugin + ) + request_plugin["ENV"]["CRDB_USER"] = "PUBLIC" + plugin, called = self._make_process_update_plugin( + discovered_instances=discovered_instances, + nodes=nodes, + deeploy_specs={ + DEEPLOY_KEYS.JOB_ID: 11, + DEEPLOY_KEYS.JOB_APP_TYPE: JOB_APP_TYPES.SERVICE, + DEEPLOY_KEYS.CURRENT_TARGET_NODES: nodes, + }, + ) + phase_calls = [] + plugin.deeploy_check_payment_and_job_owner = ( + lambda *args, **kwargs: phase_calls.append("payment") or True + ) + plugin._check_nodes_availability = ( + lambda inputs: phase_calls.append("nodes") or nodes + ) + + response = plugin._process_pipeline_request( + { + DEEPLOY_KEYS.APP_ID: "cockroachdb_422ce92", + DEEPLOY_KEYS.APP_ALIAS: "cockroachdb", + DEEPLOY_KEYS.JOB_ID: 11, + DEEPLOY_KEYS.JOB_APP_TYPE: JOB_APP_TYPES.SERVICE, + DEEPLOY_KEYS.PIPELINE_INPUT_TYPE: "void", + DEEPLOY_KEYS.CHAINSTORE_RESPONSE: False, + DEEPLOY_KEYS.TARGET_NODES: nodes, + DEEPLOY_KEYS.TARGET_NODES_COUNT: len(nodes), + DEEPLOY_KEYS.PLUGINS: [request_plugin], + }, + is_create=False, + async_mode=True, + ) + + self.assertEqual(response[DEEPLOY_KEYS.STATUS], "failed") + self.assertIn("reserved", response[DEEPLOY_KEYS.ERROR]) + self.assertEqual(phase_calls, []) + self.assertEqual(called["delete"], 0) + self.assertEqual(called["deploy"], 0) + self.assertEqual(called["queued"], 0) + def test_prepare_single_plugin_instance_update_falls_back_to_instance_conf(self): plugin = make_deeploy_plugin() fallback_instance = {