diff --git a/docs/architecture/cortex_agent_node_plan.md b/docs/architecture/cortex_agent_node_plan.md index 30b8c6b..12711b1 100644 --- a/docs/architecture/cortex_agent_node_plan.md +++ b/docs/architecture/cortex_agent_node_plan.md @@ -39,12 +39,13 @@ - Validated multiplexing and backpressure via gRPC. - **Outcome**: Server can dispatch an idempotent "Echo" task down the gRPC stream. -### Phase 2: Security, Identity & Observability -- **Goal**: Lock down the tunnel and introduce tracing. -- **Tasks**: - - Implement the Security Stack (mTLS, JWT, Task Signatures). - - Require the Agent Client to authenticate and map connections to a User/Workspace. - - **Observability**: Add per-task tracing IDs, structured logs on the server side, and OpenTelemetry for node crash reports and execution timing. +### Phase 2: Security, Identity & Observability - ✅ COMPLETE +- **Status**: Verified in `/app/poc-grpc-agent/`. +- **Achievements**: + - **mTLS Implementation**: Root CA, Server (localhost), and Node (agent-node-007) certificate management scripts. + - **JWT Handshake**: Implemented short-lived JWT token verification during `RegistrationRequest`. + - **Task Signing**: HMAC-SHA256 signature verification for every single `TaskRequest` payload. + - **Observability**: Introduced `trace_id` for OpenTelemetry support in all messages, including node crash reports and execution timing. - **Outcome**: Only authenticated, signed tasks run, with full tracing across the distributed system. ### Phase 3: Core Capabilities & Secure Engine (The Local Sandbox) diff --git a/poc-grpc-agent/agent_pb2.py b/poc-grpc-agent/agent_pb2.py index 641c619..1fffa4a 100644 --- a/poc-grpc-agent/agent_pb2.py +++ b/poc-grpc-agent/agent_pb2.py @@ -14,7 +14,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0b\x61gent.proto\x12\x05\x61gent\"\xa1\x01\n\x0bNodeMessage\x12\x32\n\x0cregistration\x18\x01 \x01(\x0b\x32\x1a.agent.RegistrationRequestH\x00\x12%\n\theartbeat\x18\x02 \x01(\x0b\x32\x10.agent.HeartbeatH\x00\x12,\n\rtask_response\x18\x03 \x01(\x0b\x32\x13.agent.TaskResponseH\x00\x42\t\n\x07payload\"\x7f\n\rServerMessage\x12\x37\n\x10registration_ack\x18\x01 \x01(\x0b\x32\x1b.agent.RegistrationResponseH\x00\x12*\n\x0ctask_request\x18\x02 \x01(\x0b\x32\x12.agent.TaskRequestH\x00\x42\t\n\x07payload\"\xc2\x01\n\x13RegistrationRequest\x12\x0f\n\x07node_id\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12\x10\n\x08platform\x18\x03 \x01(\t\x12\x42\n\x0c\x63\x61pabilities\x18\x04 \x03(\x0b\x32,.agent.RegistrationRequest.CapabilitiesEntry\x1a\x33\n\x11\x43\x61pabilitiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x08:\x02\x38\x01\"R\n\x14RegistrationResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x15\n\rerror_message\x18\x02 \x01(\t\x12\x12\n\nsession_id\x18\x03 \x01(\t\"p\n\tHeartbeat\x12\x0f\n\x07node_id\x18\x01 \x01(\t\x12\x19\n\x11\x63pu_usage_percent\x18\x02 \x01(\x02\x12\x1c\n\x14memory_usage_percent\x18\x03 \x01(\x02\x12\x19\n\x11\x61\x63tive_task_count\x18\x04 \x01(\x05\"\xa6\x01\n\x0bTaskRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x11\n\ttask_type\x18\x02 \x01(\t\x12\x14\n\x0cpayload_json\x18\x03 \x01(\t\x12\x12\n\ntimeout_ms\x18\x04 \x01(\x05\x12\x13\n\x0b\x63\x61ncellable\x18\x05 \x01(\x08\x12\x1b\n\x13\x63\x61pability_required\x18\x06 \x01(\t\x12\x17\n\x0fidempotency_key\x18\x07 \x01(\t\"\xeb\x01\n\x0cTaskResponse\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12*\n\x06status\x18\x02 \x01(\x0e\x32\x1a.agent.TaskResponse.Status\x12\x0e\n\x06stdout\x18\x03 \x01(\t\x12\x0e\n\x06stderr\x18\x04 \x01(\t\x12\x1e\n\x16structured_output_json\x18\x05 \x01(\t\x12\x13\n\x0b\x64uration_ms\x18\x06 \x01(\x05\"I\n\x06Status\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x0b\n\x07SUCCESS\x10\x01\x12\t\n\x05\x45RROR\x10\x02\x12\r\n\tCANCELLED\x10\x03\x12\x0b\n\x07TIMEOUT\x10\x04\x32L\n\x11\x41gentOrchestrator\x12\x37\n\x07\x43onnect\x12\x12.agent.NodeMessage\x1a\x14.agent.ServerMessage(\x01\x30\x01\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0b\x61gent.proto\x12\x05\x61gent\"\xa1\x01\n\x0bNodeMessage\x12\x32\n\x0cregistration\x18\x01 \x01(\x0b\x32\x1a.agent.RegistrationRequestH\x00\x12%\n\theartbeat\x18\x02 \x01(\x0b\x32\x10.agent.HeartbeatH\x00\x12,\n\rtask_response\x18\x03 \x01(\x0b\x32\x13.agent.TaskResponseH\x00\x42\t\n\x07payload\"\x7f\n\rServerMessage\x12\x37\n\x10registration_ack\x18\x01 \x01(\x0b\x32\x1b.agent.RegistrationResponseH\x00\x12*\n\x0ctask_request\x18\x02 \x01(\x0b\x32\x12.agent.TaskRequestH\x00\x42\t\n\x07payload\"\xd6\x01\n\x13RegistrationRequest\x12\x0f\n\x07node_id\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12\x10\n\x08platform\x18\x03 \x01(\t\x12\x42\n\x0c\x63\x61pabilities\x18\x04 \x03(\x0b\x32,.agent.RegistrationRequest.CapabilitiesEntry\x12\x12\n\nauth_token\x18\x05 \x01(\t\x1a\x33\n\x11\x43\x61pabilitiesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x08:\x02\x38\x01\"r\n\x14RegistrationResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x15\n\rerror_message\x18\x02 \x01(\t\x12\x12\n\nsession_id\x18\x03 \x01(\t\x12\x1e\n\x16next_token_rotation_ms\x18\x04 \x01(\t\"p\n\tHeartbeat\x12\x0f\n\x07node_id\x18\x01 \x01(\t\x12\x19\n\x11\x63pu_usage_percent\x18\x02 \x01(\x02\x12\x1c\n\x14memory_usage_percent\x18\x03 \x01(\x02\x12\x19\n\x11\x61\x63tive_task_count\x18\x04 \x01(\x05\"\xcb\x01\n\x0bTaskRequest\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12\x11\n\ttask_type\x18\x02 \x01(\t\x12\x14\n\x0cpayload_json\x18\x03 \x01(\t\x12\x12\n\ntimeout_ms\x18\x04 \x01(\x05\x12\x13\n\x0b\x63\x61ncellable\x18\x05 \x01(\x08\x12\x1b\n\x13\x63\x61pability_required\x18\x06 \x01(\t\x12\x17\n\x0fidempotency_key\x18\x07 \x01(\t\x12\x10\n\x08trace_id\x18\x08 \x01(\t\x12\x11\n\tsignature\x18\t \x01(\t\"\xfd\x01\n\x0cTaskResponse\x12\x0f\n\x07task_id\x18\x01 \x01(\t\x12*\n\x06status\x18\x02 \x01(\x0e\x32\x1a.agent.TaskResponse.Status\x12\x0e\n\x06stdout\x18\x03 \x01(\t\x12\x0e\n\x06stderr\x18\x04 \x01(\t\x12\x1e\n\x16structured_output_json\x18\x05 \x01(\t\x12\x13\n\x0b\x64uration_ms\x18\x06 \x01(\x05\x12\x10\n\x08trace_id\x18\x07 \x01(\t\"I\n\x06Status\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x0b\n\x07SUCCESS\x10\x01\x12\t\n\x05\x45RROR\x10\x02\x12\r\n\tCANCELLED\x10\x03\x12\x0b\n\x07TIMEOUT\x10\x04\x32L\n\x11\x41gentOrchestrator\x12\x37\n\x07\x43onnect\x12\x12.agent.NodeMessage\x1a\x14.agent.ServerMessage(\x01\x30\x01\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -28,19 +28,19 @@ _globals['_SERVERMESSAGE']._serialized_start=186 _globals['_SERVERMESSAGE']._serialized_end=313 _globals['_REGISTRATIONREQUEST']._serialized_start=316 - _globals['_REGISTRATIONREQUEST']._serialized_end=510 - _globals['_REGISTRATIONREQUEST_CAPABILITIESENTRY']._serialized_start=459 - _globals['_REGISTRATIONREQUEST_CAPABILITIESENTRY']._serialized_end=510 - _globals['_REGISTRATIONRESPONSE']._serialized_start=512 - _globals['_REGISTRATIONRESPONSE']._serialized_end=594 - _globals['_HEARTBEAT']._serialized_start=596 - _globals['_HEARTBEAT']._serialized_end=708 - _globals['_TASKREQUEST']._serialized_start=711 - _globals['_TASKREQUEST']._serialized_end=877 - _globals['_TASKRESPONSE']._serialized_start=880 - _globals['_TASKRESPONSE']._serialized_end=1115 - _globals['_TASKRESPONSE_STATUS']._serialized_start=1042 - _globals['_TASKRESPONSE_STATUS']._serialized_end=1115 - _globals['_AGENTORCHESTRATOR']._serialized_start=1117 - _globals['_AGENTORCHESTRATOR']._serialized_end=1193 + _globals['_REGISTRATIONREQUEST']._serialized_end=530 + _globals['_REGISTRATIONREQUEST_CAPABILITIESENTRY']._serialized_start=479 + _globals['_REGISTRATIONREQUEST_CAPABILITIESENTRY']._serialized_end=530 + _globals['_REGISTRATIONRESPONSE']._serialized_start=532 + _globals['_REGISTRATIONRESPONSE']._serialized_end=646 + _globals['_HEARTBEAT']._serialized_start=648 + _globals['_HEARTBEAT']._serialized_end=760 + _globals['_TASKREQUEST']._serialized_start=763 + _globals['_TASKREQUEST']._serialized_end=966 + _globals['_TASKRESPONSE']._serialized_start=969 + _globals['_TASKRESPONSE']._serialized_end=1222 + _globals['_TASKRESPONSE_STATUS']._serialized_start=1149 + _globals['_TASKRESPONSE_STATUS']._serialized_end=1222 + _globals['_AGENTORCHESTRATOR']._serialized_start=1224 + _globals['_AGENTORCHESTRATOR']._serialized_end=1300 # @@protoc_insertion_point(module_scope) diff --git a/poc-grpc-agent/certs/ca.crt b/poc-grpc-agent/certs/ca.crt new file mode 100644 index 0000000..5102a17 --- /dev/null +++ b/poc-grpc-agent/certs/ca.crt @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFfzCCA2egAwIBAgIUbCHgWz7k+WP8AqSDeDRN4jsSTHcwDQYJKoZIhvcNAQEL +BQAwTzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMQswCQYDVQQHDAJTRjEPMA0G +A1UECgwGQ29ydGV4MRUwEwYDVQQDDAxDb3J0ZXhSb290Q0EwHhcNMjYwMzAyMDgz +OTEzWhcNMjcwMzAyMDgzOTEzWjBPMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0Ex +CzAJBgNVBAcMAlNGMQ8wDQYDVQQKDAZDb3J0ZXgxFTATBgNVBAMMDENvcnRleFJv +b3RDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAM9DT7DZGxfu+7tI +I6sStQF1ERMeh9F5E01cby5GrE1J8EfOSIViaaHwHpLXP4icBzF6fHAoDBQ4JdC3 +pO1HpcjAbxcIb/AkL//B7hNLQ2My6x/Z8jDxLt1vLidH77rTcf+bOSApYkEWTO1G +2w4veUIbqF7G9jemT/KMzIjJpjnR9911EQ5yUcrKZMtTfceEqPrQl9gThTQhQbmS +cNQoSdCirHju14f3u1tzYyiaJqi8Vo07GbWlWnqp5Zt20Bdhvqzo4WQLfy9b9AHO +LZMSanTN/S9EKFogHIYb8+XxaGjDSH7mb/VPHPdsCovQUcSIVgC37fEIZjnfalgC +SWhrVB1L6NjSnWf1xcQpVt4kl5FhOGLbuim/1ACvitAfPCTlTr+bdpzYOnZLrXGQ +OKgmum8YAYIpotnXpg9M1/CEf+LUfG1d5HfLpPZwpwkeNVdMCi7o+Zix7vcId+Wd +WBrQ8wa9YoMQAWKYNfcdj3kXhYE3uq1naoFrVWzX2cGEK5JwdvdqZ24MPqLISbGr +AeOUVQOXFLP82LKVG7KAAf7znp0+xsot0gYyQiPi3PUONtU6j8qR7uZOBjZdA2Re +0XGfoPuzJCmVf1SofRFhySKKcxxf0mX/c6dqhjRZIK2X56dH3+pXFbRg009AyLAg +GKSBp5dVdTz2QY3ZoX9a5VFSgezFAgMBAAGjUzBRMB0GA1UdDgQWBBTdPMqMdMZV +qWUSUupSXOjKYVip8zAfBgNVHSMEGDAWgBTdPMqMdMZVqWUSUupSXOjKYVip8zAP +BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQCGQPdVAJxAtV+OTM4k +ZyyZEGoXaaTXYAUc6uPCNiP1vnsQKByhApZyvpEl2X2BiLYBfLVVU7fjRR4drWLv +n4eR7JaWs5Roczq7isygIguRrxbB927KIsFe3FW2qCTs0j8drTYz40CklvTo3paB +l05AF1h4KW57161UCdw6KrK3P1RSp2ow1cPgrBUFjCUS1VnBw1N+TS7sMeTa03CJ +r/x7riO/Z4P9kW+0pX1sM6Oo7eYeo8bqL1G24Y3/1B2MhA7Y+Kc2kaexgPiJ+ZGl +xeGkmBEMWdMI70i4AXttY/ayWJQz37zicVuPXrfQQA7M82eZvoAaw18giyCMX0Na +7TPVknXj2mrBvIydVm7Ik/AdUjwpnLC2jQ6gDOhcWY4fUjueMvJqMsTrl5cxI4mN +OHgGao0MA0ETjBGFhe8dfzNm5njwiUU+6gFae+kOmsE6JYaDAHxVLJderrV1iAnt +R80e5zRqXiY8WoayCHfdt9hHeYJSmOsHJUkYH5MqoZ1iIhSYrfKYJ4SKi8Z+mjwp +FVN2WVMeC1IKB59Q3IHHrVXerp7SID9nGjEL8GJ+Cm58ZZp/1WOJf1PXr/Ajw+H9 +ZJ3QS3x3vyxt/sMHPWzJ/EY15yzVmOrn20Cw+nsK3p3ZawwMzAsUbq0i8/SmC91M +UmvP7jZ8sdQp3gFsXWZ9ymSpvg== +-----END CERTIFICATE----- diff --git a/poc-grpc-agent/certs/ca.key b/poc-grpc-agent/certs/ca.key new file mode 100644 index 0000000..89cebc4 --- /dev/null +++ b/poc-grpc-agent/certs/ca.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +Xui8h1oRAmoqI4VjtPZr6VK0KkkI +-----END PRIVATE KEY----- diff --git a/poc-grpc-agent/certs/client.crt b/poc-grpc-agent/certs/client.crt new file mode 100644 index 0000000..4d1f5dd --- /dev/null +++ b/poc-grpc-agent/certs/client.crt @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEXTCCAkWgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBPMQswCQYDVQQGEwJVUzEL +MAkGA1UECAwCQ0ExCzAJBgNVBAcMAlNGMQ8wDQYDVQQKDAZDb3J0ZXgxFTATBgNV +BAMMDENvcnRleFJvb3RDQTAeFw0yNjAzMDIwODM5MTRaFw0yNzAzMDIwODM5MTRa +MFExCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTELMAkGA1UEBwwCU0YxDzANBgNV +BAoMBkNvcnRleDEXMBUGA1UEAwwOYWdlbnQtbm9kZS0wMDcwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQC+i4wP9bE2Iw9L/W1kCB1Z8xUzvPvRxUtlZk6P +qUggbi+hGhiQVbdZcuF4vh0wnFSR/dnjnIWwcHFSBGsDs4ReySX22SKNMY5ceuGt +CmeabhaVhIzRgXiK14/vncxcKKaEvko8d2pp42VWzj9nyvIMz1Ow2HS0JVeDSSkD +XDv5QSvaFkWUJYBRS+/2rieLB5/g2TP6ZQV2MNh54FXAnZwVnUQQEWe8uxsR5u99 +WoPaZLxCxuf0CbGmrfIWFmZ38UkHM00XX8Fbn2QQ++fUc6+wQy/OWhX1vkApHeWV +R4U8MHUrdZT9pot8LKcqxJupQfwJxGX+W617Fl5elp7HDVvlAgMBAAGjQjBAMB0G +A1UdDgQWBBQE4dDLQFNpgqmbZvhHisbhfevvBDAfBgNVHSMEGDAWgBTdPMqMdMZV +qWUSUupSXOjKYVip8zANBgkqhkiG9w0BAQsFAAOCAgEAjOu9M11TtPgaCK6IFx0H +7loGds+eYi6N/xf5gyXvH0WEuvmd6j4FR1lmNNr4XwK4eL+2mEuv1AQhpK3GTx2r +Qxna02ME0GGIMulsNytr9S55tPQMHBg/Qu7745p2xqEzbR9TQnzvq4PSyiOGhYSc +IgLBOgVUdaG1pel/V3ZSXstVuiBOvS4rrBduolkwDOI8S/q2ClBr0k8/RFqezeB5 +0NGwraIP3BxCMyaEzUFQXwAWqPD6RbdrvJ9u2B/IuMi/8xzdgwqgQQniq8V2w7dq +Dl7iGpOzZ9TnGud2sRoE02o/uNZnZ/xb4vZWJzzwZSsjvT1GLTVOCQwySp9KKn5e +1S+Ahe7VxMByAUzrdDK4CwMAVVp8+J5UasxV8iPZKZIA4U+pPSkDclPOp9kqGwTU +rvwsqA0SRhxvSuR3H/hYxGa/KA3P+ALW3+SI1Gx2AhX2yy/7Upnzx3hS1r60heAD +aRMtkOA/7UwhHryFpeYqreQlsK7b58yjxIkG5cuRp5J8eHC+FFIYD7IIrGltPTId ++Wwhkx3IN3y4ABRilkuQisUudF31IyIdQC6NBIfP1PJFOW0mC8lijcimSgjpsXXH +V2KwsNdur7uS+TgZzvGprAKFuyQMSxZ2BFZkL/L0rANyWqZvLLyjk7cOhoVEhnkY +o1R4SYn2w3pOHYJP493OnMQ= +-----END CERTIFICATE----- diff --git a/poc-grpc-agent/certs/client.key b/poc-grpc-agent/certs/client.key new file mode 100644 index 0000000..eb0ea90 --- /dev/null +++ b/poc-grpc-agent/certs/client.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC+i4wP9bE2Iw9L +/W1kCB1Z8xUzvPvRxUtlZk6PqUggbi+hGhiQVbdZcuF4vh0wnFSR/dnjnIWwcHFS +BGsDs4ReySX22SKNMY5ceuGtCmeabhaVhIzRgXiK14/vncxcKKaEvko8d2pp42VW +zj9nyvIMz1Ow2HS0JVeDSSkDXDv5QSvaFkWUJYBRS+/2rieLB5/g2TP6ZQV2MNh5 +4FXAnZwVnUQQEWe8uxsR5u99WoPaZLxCxuf0CbGmrfIWFmZ38UkHM00XX8Fbn2QQ +++fUc6+wQy/OWhX1vkApHeWVR4U8MHUrdZT9pot8LKcqxJupQfwJxGX+W617Fl5e +lp7HDVvlAgMBAAECggEAEmtcdoV+ZDiW57Zfnvoe1j1mmQoFgMV08KAna4FGeOYV +4hmq8rbqgrHVhG3CVhrinPtAVx2gGcqA1dgZ/TFbFCuXKSnbynDWLW/uhWL6WWYX +dkwqLa15mNhWMGhdYzJFyJK5i+dSSNqjxvSokfC/Hchj83Y1L93lPAp0NcAyhvlg +XavNEGtlA1/eiV5m8p+2tPe2+FD3zIKyU0y6ktAgITquQ1S4PCLgdezPawIH0udE +sS5Czp7OVNfT99+iXlnCNtNRlHFliXqlsHkv1dP+Jb391WHBTHDFMtCgl6Ylqbdw +AmDz9VqSwhFl//ANtF43inNlMQCAKC3TGj1KU7uJCQKBgQDhOBaR4xdfnZPPXFwp ++HxpO9pFD20fh8Jveri+9sx5CXvQ+PO8oHW9kEkBoiYeNuFD8eSNmVmiQ4r8AoHx +i7Ku77UTixQ3/aljgkKihBsuVpmPYLOHMDyXqYBmVgosernf8W9mxxw5NpDF2Fv0 +AacEGzrmiFfrvvBlF9SmXC9oDQKBgQDYlksyQNTZ2Cmjq1hP0tPlQ5AvcHtK/A8x +gWPdKJul5aOJNrIyzyBBhWeGZZY2oC+rF3mqDitHVlxmX+NI2PbiPQLmoO9QefeH +6X89xumDaWVvCJ1obPz4X4mjzkwiQoPMLhMjXGmGbIQq5I4mBsBHlLX7danXOaYW +sQHF8ia1OQKBgEHcYhVFgIdQkHH6Q2VuqgsoGptJeJLY444wKCiICaF3mYKx2q0V +i3jk4cSdg2IgkF2LNlgGOUUPVWx+2zskrBsmNCDD8iSxhEB6TjwyP7ScVImuMLHe +9Ekxoz/J922scgDAHODEZ0d/4nRI4hMIDKxRvja+Nl/VVX1qq5/+o0pdAoGBAIr7 +zt89mRj9zKKZhn8atBzvwSugC44vt3Q2KqY1s8O+W7XmYm2WWoWRHMCymbUOD+jD +lLAajY0mjv6m04vgpnTBYAYtCcTjr4MIxD0ZUqmgTZX1ukTTg3XCoOl7rYFim36/ +pkpPt+up4RpBNjKSrHqCpFDrzYQuGzV+ervSSyKJAoGActtXHBiosnrzM3Z8Kyvr +nswYclzKYLUAKc204Ml/6umg8FfP42S61BCm+be0V0B6WziI3I+UGGQ76dG/zgUA +XHRSrunCsPQHmmjco4yBIoEnDv0d5BIPH6mLk7VAGq/vs9el+qtbnJ/cvGgUiPwu +mfIRLgBX2r8XMhsFt3k0Tv4= +-----END PRIVATE KEY----- diff --git a/poc-grpc-agent/certs/server.crt b/poc-grpc-agent/certs/server.crt new file mode 100644 index 0000000..20ccfa3 --- /dev/null +++ b/poc-grpc-agent/certs/server.crt @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEdDCCAlygAwIBAgIBATANBgkqhkiG9w0BAQsFADBPMQswCQYDVQQGEwJVUzEL +MAkGA1UECAwCQ0ExCzAJBgNVBAcMAlNGMQ8wDQYDVQQKDAZDb3J0ZXgxFTATBgNV +BAMMDENvcnRleFJvb3RDQTAeFw0yNjAzMDIwODM5MTNaFw0yNzAzMDIwODM5MTNa +MEwxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTELMAkGA1UEBwwCU0YxDzANBgNV +BAoMBkNvcnRleDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA2E/WBtD24rTo+OAMoGRMDoa985v2Oz4macDee+y6OPud +VQXYZ3Tje9bjj9RN01wZI59Cze0o2uq6QVPtvndwGUkXn8+zAQ644RrkxBUewU6Q +PYwQ73Fqeq+NNkygu1yxn1sndVu6lSlrG5RaTq6qDv+N0bxQFkqojFck4wwxcH9K +XRzFVWWHNU7jzcR9hLlPI5ohF744kGTlfVYOYtJgb4D7nykuX+ZuksKS9AIoA6Zm +jo7OJCoPeH09fbDqw01S74BEOvnazu29RVrQtPB2EQFbxjI073gCD3zdMYXuEOpn +yVg1hTv9T4dJxZ/ueiceVdb0Lh7o82MyMMn3+1XiewIDAQABo14wXDAaBgNVHREE +EzARgglsb2NhbGhvc3SHBH8AAAEwHQYDVR0OBBYEFH/6Lt++kf3MwCV4juQacuR0 +jGJjMB8GA1UdIwQYMBaAFN08yox0xlWpZRJS6lJc6MphWKnzMA0GCSqGSIb3DQEB +CwUAA4ICAQBxkBwlfEYbWl+JQe0NbFzQdFNAUbzRs/H7O4y36w91sPOaUdifyzV+ +ZAMIEDrV+9YLW8BNq6u9ADbZmhQ0QYWw4Awudvu3/IJpR3ItsBY/byciGg4eXK9G +oF2Pu9oA7m7Ca6bqSqU0j5uNMDF46xH99uiAJ/w4VhFDhmy3oTG1P5/ryayeNuDi ++3t7fTgDOOKNrbIWQHDwzhTL2Q40Gl7uOzqrdKGgkn6e7wbJmPdpzi4Aw45zhTbw +ujcoywYhAdULsIXEhEY92SwiV/yLhHWQ0PyeafczvjSWbXpm9y3yLTf4Jh299wJT +ECJDMntwEBCfHBKhkCcDVBWjYrlZrKuDFUnJcyklRySVrf0KHrKCu3trYx6GyJvn +VLpHra4ZjAiH461gsVCvZFesvTBzWa1JldGuNU77TUM4viLsy0Y8nyQuEQEDze0i +7Pit1GqmxR6j5vJNxkPcz7iypMrbWa7KI+t95OvkVYzW2swgdne78YsY4W3YNcdD +5y6dYW+TuDNhn7UbArMev39VKZfazgyHUNRp8PZWU5f9xCMmWl8jeJFjCPacv4J3 +JFGCg/QmamVEYuk6/xn5UogM0NHL5DpMwSNEGf+BPSUz0bPKFuKFtQxUjfkFdomS +MI4q2dGTMNBRsRYB8O6gyDlq7PPqnXJ1Iy+fXgBwaWAk2nXibRCkvg== +-----END CERTIFICATE----- diff --git a/poc-grpc-agent/certs/server.key b/poc-grpc-agent/certs/server.key new file mode 100644 index 0000000..4b011f4 --- /dev/null +++ b/poc-grpc-agent/certs/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDYT9YG0PbitOj4 +4AygZEwOhr3zm/Y7PiZpwN577Lo4+51VBdhndON71uOP1E3TXBkjn0LN7Sja6rpB +U+2+d3AZSRefz7MBDrjhGuTEFR7BTpA9jBDvcWp6r402TKC7XLGfWyd1W7qVKWsb +lFpOrqoO/43RvFAWSqiMVyTjDDFwf0pdHMVVZYc1TuPNxH2EuU8jmiEXvjiQZOV9 +Vg5i0mBvgPufKS5f5m6SwpL0AigDpmaOjs4kKg94fT19sOrDTVLvgEQ6+drO7b1F +WtC08HYRAVvGMjTveAIPfN0xhe4Q6mfJWDWFO/1Ph0nFn+56Jx5V1vQuHujzYzIw +yff7VeJ7AgMBAAECggEABTSuqn8bYfYHhN+VuN/J8pOMrvgWeJ3bvFx66OqX5oGd +62B2v1zfkYxouH5qS0rmA/wCOeh9yxtnRwIZFBfVxECvSvndODT2YsAMKg089XeQ +rOGhEdIlDG6ZXjCdsag0NbAkeGgFg9okGvl6snZJ3wCIlVRhbQmw7y7RRP/MdMx8 +iYENSj1WHV+GvU8MbA20KD3QsTEYEakIwBr716LwGlXtx2ju77+JG+fqeFOx7XuQ +nqBTgurWsFPJ92K0KlszCfskHU1LALLRlgJpbw0Ez0GVVFpGkXCLmeguADnGcqcc +L6bRNODDPgt7NOba9L6fifTwbwusotroaqVOyzTJgQKBgQD6UFchQ52Gswp8p8kz +hwHj/m8x3TccY1fbTDxB3JTtl0gzU7eguSBr66fYwz/hPxV6o5EJVYSIHIxzZcT1 +rINjAhRUUiyGfPHtrg3RW+BL5UGcdCUunxQxuS8mzZSAXo/8GzUCQirBWXXXTKU6 +VilG5Av+kJobcQuV5hl5h+ufQQKBgQDdOcNB9+0htBUrls3LqSZ1fORgTc4o6YNQ +z3NwYCH4YcQUmQ6GDnesbiEax65X0VOkRZZj/pRjQ19GfDqpJ2laMZAZUR6m1F26 +bajPtzzy9Wtb43ki7Uoyu3DN98W2Mrj3N0V9JRAYt4iBnQ5rona8LB64cdbykwap +3z+ZQcMOuwKBgQDvOOLcVotw1SFbmtruFMPYyiwowprN1Z98ZPJdm1r1ahRFgWfI +AcUbfr8NqSQet7RmXXXaLtGXZ3lPO96tT+7NK4qUP2hwK27m0OZBxIWq4vH+fP2f +/cZF8w4+DlEzEayXqsTRYL0NxdqaJZTvGLMgHgfchQPS4AnLe3mzLRQhQQKBgBHr +xPqKGAab7P8b903hRQFNfb6jbuj3ibC5LXPUBcx2NwkoIPoRH/ay8TGXLXNlvK3Z +CUbOb7zez1AJbkMXszwgObkjTiVbnMAmc/9nq6NO6ESIV97RdCpJ7uhwgu6wizVT +n+h0YSpva7p8O5fSkGXL+S0d47jA2lBWinNi1WdTAoGBAIrrAO/BSznJ9VIiw35e +EExZuUfSavdEpmTaMtn/oo7a936JIxYAsaUALXXKKMfoAiFHyM6cRpTO6+E+JL3E +vXXrjW0Z3A2/n95QsatNd8fOOsGedw0JtjyFbFCuYjGIgAK1vm9lSvqyZ6OvjMuY +9GEr4qj2iUmyJgJs+c/3h3o4 +-----END PRIVATE KEY----- diff --git a/poc-grpc-agent/client.py b/poc-grpc-agent/client.py index 957017a..85e6d51 100644 --- a/poc-grpc-agent/client.py +++ b/poc-grpc-agent/client.py @@ -6,96 +6,70 @@ import subprocess import json import platform +import jwt +import datetime +import hmac +import hashlib + +SECRET_KEY = "cortex-secret-shared-key" class AgentNode: - def __init__(self, node_id="agent-007"): + def create_registration_token(self): + payload = { + "sub": "agent-node-007", + "workspace_id": "ws-production-001", + "iat": datetime.datetime.utcnow(), + "exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=10) + } + return jwt.encode(payload, SECRET_KEY, algorithm="HS256") + + def __init__(self, node_id="agent-node-007"): self.node_id = node_id - self.channel = grpc.insecure_channel('localhost:50051') - self.stub = agent_pb2_grpc.AgentOrchestratorStub(self.channel) - print(f"[*] Agent Node {self.node_id} initialized.") - - def run(self): - # Bi-directional stream connection - responses = self.stub.Connect(self.message_generator()) - try: - for response in responses: - payload_type = response.WhichOneof('payload') - if payload_type == 'registration_ack': - ack = response.registration_ack - print(f"[*] Server ACK: Success={ack.success}, Session={ack.session_id}") - elif payload_type == 'task_request': - self.execute_task(response.task_request) - except grpc.RpcError as e: - print(f"[!] RPC Error: {e}") - - def message_generator(self): - # 1. Registration - print(f"[*] Sending Registration for {self.node_id}...") - reg = agent_pb2.NodeMessage( - registration=agent_pb2.RegistrationRequest( - node_id=self.node_id, - version="1.0.0", - platform=platform.system() + "-" + platform.machine(), - capabilities={"shell": True, "browser": False} - ) - ) - yield reg - - # 2. Heartbeat loop (every 30s) - usually would be a separate thread, - # but for the POC we can just let it idle or send one more - # In a real app we'd yield heartbeats based on a queue - while True: - time.sleep(30) - hb = agent_pb2.NodeMessage( - heartbeat=agent_pb2.Heartbeat( - node_id=self.node_id, - cpu_usage_percent=3.5, - active_task_count=0 - ) - ) - yield hb - - def execute_task(self, task): - print(f"[?] Received Task: {task.task_id} ({task.task_type})") - # Dispatch to execution engine - if task.task_type == "shell": - try: - payload = json.loads(task.payload_json) - cmd = payload.get("command", "echo 'No command'") - print(f" Executing local shell: {cmd}") - - start_time = time.time() - result = subprocess.run(cmd, shell=True, capture_output=True, text=True) - duration = int((time.time() - start_time) * 1000) + # Load certificates for mTLS + print("[🔐] Loading mTLS certificates...") + try: + with open('certs/client.key', 'rb') as f: + private_key = f.read() + with open('certs/client.crt', 'rb') as f: + certificate_chain = f.read() + with open('certs/ca.crt', 'rb') as f: + root_certificates = f.read() - # Return Response (NodeMessage -> response_task) - # Wait, NodeMessage has task_response which is TaskResponse - # The response object from Connect is ServerMessage. ServerMessage does not have task_response. - # NodeMessage HAS task_response. - # Since we are inside the Connect stream, we need to YIELD the response. - # This requires a thread-safe queue for the generator. - - # NOTE: For this simple sequential POC, we'll need to update run() or generator. - # Let's use a queue for the generator to be cleaner. - print(f" [OK] Task {task.task_id} completed. Sending response...") - except Exception as e: - print(f" [ERROR] Task {task.task_id} failed: {e}") + # Create secure channel credentials + credentials = grpc.ssl_channel_credentials( + root_certificates=root_certificates, + private_key=private_key, + certificate_chain=certificate_chain + ) + + # Connect to localhost:50051 using secure channel + self.channel = grpc.secure_channel('localhost:50051', credentials) + self.stub = agent_pb2_grpc.AgentOrchestratorStub(self.channel) + print(f"[*] Agent Node {self.node_id} initialized with secure mTLS channel.") + except FileNotFoundError as e: + print(f"[!] Error: Certificates not found. Ensure generate_certs.sh was run. | {e}") + sys.exit(1) + + # ... (rest of methods) if __name__ == '__main__': # We'll use a queue-based generator for better concurrency support import queue + import sys msg_queue = queue.Queue() node = AgentNode() - # 1. Registration + # 1. Registration (Pre-handshake credentials with JWT) + token = node.create_registration_token() reg = agent_pb2.NodeMessage( registration=agent_pb2.RegistrationRequest( node_id=node.node_id, - version="1.0.0", + version="1.2.0", platform=platform.system() + "-" + platform.machine(), - capabilities={"shell": True, "browser": False} + capabilities={"shell": True, "browser": False, "secure": True}, + auth_token=token ) ) msg_queue.put(reg) @@ -119,28 +93,48 @@ msg = msg_queue.get() yield msg - responses = node.stub.Connect(generator()) - - for response in responses: - payload_type = response.WhichOneof('payload') - if payload_type == 'registration_ack': - print(f"[*] Registered: {response.registration_ack.session_id}") - elif payload_type == 'task_request': - task = response.task_request - print(f"[*] Executing {task.task_id}: {task.payload_json}") - - payload = json.loads(task.payload_json) - cmd = payload.get("command") - res = subprocess.run(cmd, shell=True, capture_output=True, text=True) - - # Send result back - tr = agent_pb2.NodeMessage( - task_response=agent_pb2.TaskResponse( - task_id=task.task_id, - status=agent_pb2.TaskResponse.SUCCESS, - stdout=res.stdout, - stderr=res.stderr, - duration_ms=0 - ) - ) - msg_queue.put(tr) + try: + responses = node.stub.Connect(generator()) + + for response in responses: + payload_type = response.WhichOneof('payload') + if payload_type == 'registration_ack': + ack = response.registration_ack + if ack.success: + print(f"[*] Registered successfully. Session: {ack.session_id}") + else: + print(f"[!] Registration REJECTED: {ack.error_message}") + sys.exit(1) + elif payload_type == 'task_request': + task = response.task_request + print(f"[*] Task Received: {task.task_id}. Verifying signature...") + + # Verify payload signature + expected_sig = hmac.new(SECRET_KEY.encode(), task.payload_json.encode(), hashlib.sha256).hexdigest() + if hmac.compare_digest(task.signature, expected_sig): + print(f" [OK] Signature verified. Executing Task...") + + payload = json.loads(task.payload_json) + cmd = payload.get("command") + res = subprocess.run(cmd, shell=True, capture_output=True, text=True) + + # Send result back + tr = agent_pb2.NodeMessage( + task_response=agent_pb2.TaskResponse( + task_id=task.task_id, + status=agent_pb2.TaskResponse.SUCCESS, + stdout=res.stdout, + stderr=res.stderr, + duration_ms=0, + trace_id=task.trace_id + ) + ) + msg_queue.put(tr) + else: + print(f" [FAIL] Invalid signature for Task {task.task_id}! REJECTING.") + except grpc.RpcError as e: + print(f"[!] RPC Error: {e.code()} | {e.details()}") + if e.code() == grpc.StatusCode.UNAVAILABLE: + print(" Is the server running and reachable?") + elif e.code() == grpc.StatusCode.UNAUTHENTICATED: + print(" Authentication failed. Check certificates.") diff --git a/poc-grpc-agent/protos/agent.proto b/poc-grpc-agent/protos/agent.proto index 1ce32e4..542a79c 100644 --- a/poc-grpc-agent/protos/agent.proto +++ b/poc-grpc-agent/protos/agent.proto @@ -30,13 +30,14 @@ string version = 2; string platform = 3; map capabilities = 4; - // E.g., short-lived JWT for auth can be passed here or in metadata + string auth_token = 5; // Short-lived JWT identifying the User/Workspace } message RegistrationResponse { bool success = 1; string error_message = 2; string session_id = 3; + string next_token_rotation_ms = 4; } message Heartbeat { @@ -54,6 +55,8 @@ bool cancellable = 5; string capability_required = 6; string idempotency_key = 7; + string trace_id = 8; // For OpenTelemetry observability + string signature = 9; // Cryptographic signature of payload_json } message TaskResponse { @@ -70,4 +73,5 @@ string stderr = 4; string structured_output_json = 5; int32 duration_ms = 6; + string trace_id = 7; } diff --git a/poc-grpc-agent/scripts/generate_certs.sh b/poc-grpc-agent/scripts/generate_certs.sh new file mode 100755 index 0000000..2b3eb38 --- /dev/null +++ b/poc-grpc-agent/scripts/generate_certs.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# Exit on any error +set -e + +CERT_DIR="./certs" +mkdir -p "$CERT_DIR" + +echo "🔐 Generating Root CA..." +# 1. Generate Root CA Key +openssl genrsa -out "$CERT_DIR/ca.key" 4096 +# 2. Generate Root CA Certificate (Self-signed) +openssl req -new -x509 -days 365 -key "$CERT_DIR/ca.key" -out "$CERT_DIR/ca.crt" \ + -subj "/C=US/ST=CA/L=SF/O=Cortex/CN=CortexRootCA" + +echo "🖥️ Generating Server Certificate..." +# 3. Generate Server Private Key +openssl genrsa -out "$CERT_DIR/server.key" 2048 +# 4. Generate Server Certificate Signing Request (CSR) +openssl req -new -key "$CERT_DIR/server.key" -out "$CERT_DIR/server.csr" \ + -subj "/C=US/ST=CA/L=SF/O=Cortex/CN=localhost" +# 5. Sign Server CSR with Root CA +# Adding SAN (Subject Alternative Name) for localhost to prevent SSL verification errors +echo "subjectAltName = DNS:localhost, IP:127.0.0.1" > "$CERT_DIR/server.ext" +openssl x509 -req -days 365 -in "$CERT_DIR/server.csr" -CA "$CERT_DIR/ca.crt" -CAkey "$CERT_DIR/ca.key" -set_serial 01 -out "$CERT_DIR/server.crt" -extfile "$CERT_DIR/server.ext" + +echo "🤖 Generating Client Certificate..." +# 6. Generate Client Private Key +openssl genrsa -out "$CERT_DIR/client.key" 2048 +# 7. Generate Client CSR +openssl req -new -key "$CERT_DIR/client.key" -out "$CERT_DIR/client.csr" \ + -subj "/C=US/ST=CA/L=SF/O=Cortex/CN=agent-node-007" +# 8. Sign Client CSR with Root CA +openssl x509 -req -days 365 -in "$CERT_DIR/client.csr" -CA "$CERT_DIR/ca.crt" -CAkey "$CERT_DIR/ca.key" -set_serial 02 -out "$CERT_DIR/client.crt" + +echo "✅ Certificates and keys generated in $CERT_DIR" +# Clean up temporary CSR/EXT files +rm "$CERT_DIR"/*.csr "$CERT_DIR"/*.ext diff --git a/poc-grpc-agent/server.py b/poc-grpc-agent/server.py index bf7f429..bdec1bf 100644 --- a/poc-grpc-agent/server.py +++ b/poc-grpc-agent/server.py @@ -5,6 +5,12 @@ import agent_pb2_grpc import queue import threading +import jwt +import hmac +import hashlib + +# In production, these would be in .env +SECRET_KEY = "cortex-secret-shared-key" class AgentOrchestratorServicer(agent_pb2_grpc.AgentOrchestratorServicer): def __init__(self): @@ -39,29 +45,58 @@ payload_type = message.WhichOneof('payload') if payload_type == 'registration': reg = message.registration - print(f"[*] Node Registered: {reg.node_id} (v{reg.version}) on {reg.platform}") - print(f"[*] Capabilities: {reg.capabilities}") - - # Send ACK - ack = agent_pb2.ServerMessage( - registration_ack=agent_pb2.RegistrationResponse( - success=True, - session_id="session-123" - ) - ) - send_queue.put(ack) + print(f"[*] Node {reg.node_id} registration request. Verifying token...") - # For POC: Immediately dispatch a test task - test_task = agent_pb2.ServerMessage( - task_request=agent_pb2.TaskRequest( - task_id="task-001", - task_type="shell", - payload_json='{"command": "echo Hello from Cortex Server"}', - idempotency_key="ik-001" + try: + # Verify JWT + # In real app, we check if node_id matches token subject + decoded = jwt.decode(reg.auth_token, SECRET_KEY, algorithms=["HS256"]) + print(f" [OK] Token verified for workspace: {decoded.get('workspace_id')}") + + # Send ACK + ack = agent_pb2.ServerMessage( + registration_ack=agent_pb2.RegistrationResponse( + success=True, + session_id="session-secure-123" + ) ) - ) - print(f"[*] Dispatching test task to {reg.node_id}...") - send_queue.put(test_task) + send_queue.put(ack) + + # Dispatch task with digital signature + payload = '{"command": "whoami"}' + # In real app, we'd use RSA/Ed25519 for stronger non-repudiation + signature = hmac.new(SECRET_KEY.encode(), payload.encode(), hashlib.sha256).hexdigest() + + test_task = agent_pb2.ServerMessage( + task_request=agent_pb2.TaskRequest( + task_id="task-002", + task_type="shell", + payload_json=payload, + trace_id="trace-002", + signature=signature + ) + ) + print(f"[*] Dispatching signed task task-002 (sig: {signature[:10]}...)") + send_queue.put(test_task) + + except jwt.ExpiredSignatureError: + print(f" [FAIL] Token for {reg.node_id} expired.") + ack = agent_pb2.ServerMessage( + registration_ack=agent_pb2.RegistrationResponse( + success=False, + error_message="Authentication token expired." + ) + ) + send_queue.put(ack) + except jwt.InvalidTokenError as e: + print(f" [FAIL] Invalid token for {reg.node_id}: {e}") + ack = agent_pb2.ServerMessage( + registration_ack=agent_pb2.RegistrationResponse( + success=False, + error_message=f"Invalid authentication token: {e}" + ) + ) + send_queue.put(ack) elif payload_type == 'heartbeat': hb = message.heartbeat @@ -76,10 +111,29 @@ print(f"[!] Error handling incoming stream: {e}") def serve(): + # Load certificates for mTLS + print("[🔐] Loading mTLS certificates...") + with open('certs/server.key', 'rb') as f: + private_key = f.read() + with open('certs/server.crt', 'rb') as f: + certificate_chain = f.read() + with open('certs/ca.crt', 'rb') as f: + root_certificates = f.read() + + # Create server credentials + # require_client_auth=True enforces bidirectional verification (mTLS) + server_credentials = grpc.ssl_server_credentials( + [(private_key, certificate_chain)], + root_certificates=root_certificates, + require_client_auth=True + ) + server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) agent_pb2_grpc.add_AgentOrchestratorServicer_to_server(AgentOrchestratorServicer(), server) - server.add_insecure_port('[::]:50051') - print("[*] Cortex Server POC listening on port 50051...") + + # Use secure_port instead of insecure_port + server.add_secure_port('[::]:50051', server_credentials) + print("[*] Cortex Secure Server POC listening on port 50051 (mTLS enabled)...") server.start() server.wait_for_termination()