Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions docker-compose/deeploy-crdb-verify-full/README.md
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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,
Expand Down
16 changes: 13 additions & 3 deletions docker-compose/deeploy-crdb-verify-full/validate_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,25 @@


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"]
RELAY = "verify_crdb_relay"
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(
Expand Down Expand Up @@ -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",
)


Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions extensions/business/deeploy/deeploy_manager_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}."
Expand Down
36 changes: 27 additions & 9 deletions extensions/business/deeploy/deeploy_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -3468,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,
Expand Down Expand Up @@ -3620,8 +3641,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):
Expand Down Expand Up @@ -4082,10 +4103,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
Expand All @@ -4111,7 +4130,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(",")
Expand Down
89 changes: 86 additions & 3 deletions extensions/business/deeploy/tests/test_create_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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=[
Expand All @@ -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 = {
Expand Down
53 changes: 53 additions & 0 deletions extensions/business/deeploy/tests/test_process_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading