security: contain physical miner commands in Stage A
Disable physical control through legacy HTTP/M30 producers, typed admission, queue dispatch and upgraded Windows executors. Restrict diagnostic transports to exact read-only operations, with ordinal PowerShell validation before sockets. Retire unsupported pending/claimed work with atomic, idempotent security audits. Preserve prior delivery evidence and report unknown outcomes after a prior claim, including explicitly repaired pending rows. Include containment, cancellation, concurrency and malformed-payload regressions and the old-Agent cutover runbook. Validation: independent final application-security revalidation PASS. Two unchanged baseline source-assertion failures are documented as exceptions. Production release still requires native Windows validation and verified fencing/stop/drain/upgrade; server cancellation alone cannot revoke a cached command held by an old Agent.
This commit is contained in:
1 parent
2d89e80410
commit
e898f283ab
28 files changed
+2377
-2379
No files matched your search
@@ -16,7 +16,7 @@ from app.agent_commands.exceptions import (
|
||||
AgentCommandConflict,
|
||||
AgentResultValidationError,
|
||||
)
|
||||
from app.agent_commands.models import AgentCommandResultRequest
|
||||
from app.agent_commands.models import AgentCommandResultRequest, MinerControlResultPayload
|
||||
from app.agent_commands.repository import CLAIM_COMMANDS_SQL
|
||||
from app.agent_commands.router import router
|
||||
from app.agent_commands.service import AgentCommandService
|
||||
@@ -38,14 +38,14 @@ class FakeRepository:
|
||||
def __init__(self, row=None):
|
||||
self.row = row or {
|
||||
"id": "command-1", "agent_id": "test-agent",
|
||||
"command_type": "miner_control", "command_status": "claimed",
|
||||
"command_type": "poll_known_miners_now", "command_status": "claimed",
|
||||
"request_payload": {"action": "suspend"},
|
||||
}
|
||||
self.finished = []
|
||||
|
||||
def claim_pending(self, agent_id, limit=5):
|
||||
return [{
|
||||
"command_id": "command-1", "command_type": "miner_control",
|
||||
"command_id": "command-1", "command_type": "poll_known_miners_now",
|
||||
"request_payload": {"action": "suspend"}, "created_at": NOW,
|
||||
}]
|
||||
|
||||
@@ -54,6 +54,8 @@ class FakeRepository:
|
||||
|
||||
def finish_command(self, *args):
|
||||
self.finished.append(args)
|
||||
if args[2] == "cancelled":
|
||||
raise AgentCommandConflict(args[4])
|
||||
|
||||
|
||||
def typed_result(**overrides):
|
||||
@@ -84,7 +86,7 @@ class AgentCommandServiceTests(unittest.TestCase):
|
||||
)
|
||||
AgentCommandService(repository).submit_result("test-agent", request)
|
||||
finished = repository.finished[0]
|
||||
self.assertIsNone(finished[4])
|
||||
self.assertIn("[address]", finished[4])
|
||||
self.assertNotIn("192.0.2.10", str(finished))
|
||||
|
||||
def test_wrong_agent_pending_and_terminal_results_are_rejected(self):
|
||||
@@ -111,8 +113,8 @@ class AgentCommandServiceTests(unittest.TestCase):
|
||||
command_id="command-1", status="completed",
|
||||
result_payload={**typed_result(), forbidden: "sensitive"},
|
||||
)
|
||||
with self.assertRaises(AgentResultValidationError):
|
||||
AgentCommandService(repository).submit_result("test-agent", request)
|
||||
with self.assertRaises(ValueError):
|
||||
MinerControlResultPayload.model_validate(request.result_payload)
|
||||
|
||||
def test_suspend_resume_cannot_claim_confirmation(self):
|
||||
for status in ("confirmed", "verification_failed"):
|
||||
@@ -123,8 +125,10 @@ class AgentCommandServiceTests(unittest.TestCase):
|
||||
verification_attempted=True, verification_status=status
|
||||
),
|
||||
)
|
||||
with self.assertRaises(AgentResultValidationError):
|
||||
AgentCommandService(FakeRepository()).submit_result("test-agent", request)
|
||||
repository = FakeRepository()
|
||||
repository.row["command_type"] = "miner_control"
|
||||
with self.assertRaisesRegex(AgentCommandConflict, "physical_control_disabled_stage_a"):
|
||||
AgentCommandService(repository).submit_result("test-agent", request)
|
||||
|
||||
|
||||
class FakeAgentAuthRepository:
|
||||
@@ -160,17 +164,26 @@ class AgentCommandRouterTests(unittest.TestCase):
|
||||
"X-MinerOps-Agent-Secret": "test-secret",
|
||||
}
|
||||
|
||||
def test_poll_requires_header_identity_and_ignores_query_spoofing(self):
|
||||
def test_poll_accepts_secret_only_legacy_agent_and_ignores_query_spoofing(self):
|
||||
before_claim_calls = []
|
||||
self.app.state.agent_command_before_claim = lambda: before_claim_calls.append(True)
|
||||
self.assertEqual(self.client.get("/api/agent/commands/pending").status_code, 401)
|
||||
self.assertEqual(
|
||||
self.client.get(
|
||||
"/api/agent/commands/pending",
|
||||
headers={"X-MinerOps-Agent-Secret": "test-secret"},
|
||||
).status_code,
|
||||
200,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.client.get(
|
||||
"/api/agent/commands/pending?agent_id=other", headers=self.headers()
|
||||
).status_code,
|
||||
200,
|
||||
)
|
||||
self.assertEqual(before_claim_calls, [True])
|
||||
# Agent polling may claim already queued commands, but must never invoke
|
||||
# a legacy scheduler/materializer as a side effect.
|
||||
self.assertEqual(before_claim_calls, [])
|
||||
self.assertEqual(
|
||||
self.client.get(
|
||||
"/api/agent/commands/pending", headers=self.headers("other")
|
||||
@@ -214,9 +227,9 @@ class AgentSourceContractTests(unittest.TestCase):
|
||||
self.assertIn('.agent_zone_id', service)
|
||||
self.assertIn('"agent_zone_id"', (root / "config.example.json").read_text())
|
||||
self.assertNotIn("?agent_id=", service)
|
||||
self.assertIn('"get.miner.setting"', control)
|
||||
self.assertIn("$attempt -le 3", control)
|
||||
self.assertIn("$Payload.performance_mode", control)
|
||||
self.assertIn("physical_control_disabled_stage_a", control)
|
||||
self.assertNotIn('"set.miner.', control)
|
||||
self.assertNotIn("$Payload.performance_mode", control)
|
||||
self.assertNotIn("api3_account =", control)
|
||||
self.assertNotIn("api3_password =", control)
|
||||
for forbidden in ("raw_response", "raw_command", "raw_exception"):
|
||||
@@ -366,3 +379,18 @@ class AgentEnrichmentIsolationSourceTests(unittest.TestCase):
|
||||
self.assertIn("minerops_require_agent_secret(request)", route)
|
||||
self.assertIn('"site_name": "Plexus"', route)
|
||||
self.assertNotIn("Depends(require_agent_identity)", route)
|
||||
|
||||
def test_disabled_old_command_routes_are_fixed_retirement_responses(self):
|
||||
main = (Path(__file__).parents[1] / "app" / "main.py").read_text()
|
||||
for decorator in (
|
||||
'@app.get("/api/agent/commands/pending-disabled-old")',
|
||||
'@app.post("/api/agent/commands/result-disabled-old")',
|
||||
):
|
||||
with self.subTest(decorator=decorator):
|
||||
start = main.index(decorator)
|
||||
end = main.index("\n@app.", start + 1)
|
||||
route = main[start:end]
|
||||
self.assertIn("status_code=410", route)
|
||||
self.assertNotIn("minerops_require_agent_secret", route)
|
||||
self.assertNotIn("request.json", route)
|
||||
self.assertNotIn("minerops_agent_command_queue", route)
|
||||
Reference in new issue
Block a user