From 9678dbb856f434f51b1840714cda10932f0501dd Mon Sep 17 00:00:00 2001 From: Aditya Aggarwal <42476079+aditya-786@users.noreply.github.com> Date: Sun, 28 Jun 2026 13:06:44 +0530 Subject: [PATCH] feat(cilium): support secure TLS connection to the hubble relay The Cilium provider connected to the Hubble relay over plaintext only (grpc.insecure_channel), so it could not talk to a relay that requires TLS or mutual TLS. Add optional TLS via four auth-config fields: use_tls turns on a secure channel, ca_certificate verifies the server (system trust store when omitted), and client_certificate plus client_key enable mutual TLS. Default behavior is unchanged. The channel construction is extracted into build_cilium_channel. Closes #4264 --- .../documentation/cilium-provider.mdx | 4 ++ .../cilium-snippet-autogenerated.mdx | 10 ++-- .../cilium_provider/cilium_provider.py | 54 ++++++++++++++++++- .../cilium_provider/secure_channel.py | 24 +++++++++ tests/test_cilium_secure_channel.py | 54 +++++++++++++++++++ 5 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 keep/providers/cilium_provider/secure_channel.py create mode 100644 tests/test_cilium_secure_channel.py diff --git a/docs/providers/documentation/cilium-provider.mdx b/docs/providers/documentation/cilium-provider.mdx index 2ae0c5fdd9..533ae61e92 100644 --- a/docs/providers/documentation/cilium-provider.mdx +++ b/docs/providers/documentation/cilium-provider.mdx @@ -40,6 +40,10 @@ The Cilium provider leverages Hubble's network flow data to automatically discov | Parameter | Description | Example | |-----------|-------------|----------| | `cilium_base_endpoint` | The base endpoint of the Cilium Hubble relay | `localhost:4245` | +| `use_tls` | Connect to the Hubble relay over TLS | `true` | +| `ca_certificate` | CA certificate (PEM) used to verify the Hubble relay server | `-----BEGIN CERTIFICATE-----` | +| `client_certificate` | Client certificate (PEM) for mutual TLS | `-----BEGIN CERTIFICATE-----` | +| `client_key` | Client private key (PEM) for mutual TLS | `-----BEGIN PRIVATE KEY-----` | ## Outputs diff --git a/docs/snippets/providers/cilium-snippet-autogenerated.mdx b/docs/snippets/providers/cilium-snippet-autogenerated.mdx index 79376178e5..119a1c9228 100644 --- a/docs/snippets/providers/cilium-snippet-autogenerated.mdx +++ b/docs/snippets/providers/cilium-snippet-autogenerated.mdx @@ -1,9 +1,13 @@ -{/* This snippet is automatically generated using scripts/docs_render_provider_snippets.py +{/* This snippet is automatically generated using scripts/docs_render_provider_snippets.py Do not edit it manually, as it will be overwritten */} ## Authentication This provider requires authentication. - **cilium_base_endpoint**: The base endpoint of the cilium hubble relay (required: True, sensitive: False) +- **use_tls**: Connect to the hubble relay over TLS (required: False, sensitive: False) +- **ca_certificate**: CA certificate (PEM) used to verify the hubble relay server (required: False, sensitive: True) +- **client_certificate**: Client certificate (PEM) for mutual TLS (required: False, sensitive: True) +- **client_key**: Client private key (PEM) for mutual TLS (required: False, sensitive: True) ## In workflows @@ -14,6 +18,6 @@ This provider can't be used as a "step" or "action" in workflows. If you want to ## Topology -This provider pulls [topology](/overview/servicetopology) to Keep. It could be used in [correlations](/overview/correlation-topology) -and [mapping](/overview/enrichment/mapping#mapping-with-topology-data), and as a context +This provider pulls [topology](/overview/servicetopology) to Keep. It could be used in [correlations](/overview/correlation-topology) +and [mapping](/overview/enrichment/mapping#mapping-with-topology-data), and as a context for [alerts](/alerts/sidebar#7-alert-topology-view) and [incidents](/overview#17-incident-topology). \ No newline at end of file diff --git a/keep/providers/cilium_provider/cilium_provider.py b/keep/providers/cilium_provider/cilium_provider.py index c10efe088d..e8b3648e73 100644 --- a/keep/providers/cilium_provider/cilium_provider.py +++ b/keep/providers/cilium_provider/cilium_provider.py @@ -1,12 +1,12 @@ import dataclasses from collections import defaultdict -import grpc import pydantic from keep.api.models.db.topology import TopologyServiceInDto from keep.contextmanager.contextmanager import ContextManager from keep.providers.base.base_provider import BaseTopologyProvider +from keep.providers.cilium_provider.secure_channel import build_cilium_channel from keep.providers.models.provider_config import ProviderConfig from keep.validation.fields import NoSchemeUrl @@ -25,6 +25,50 @@ class CiliumProviderAuthConfig: } ) + use_tls: bool = dataclasses.field( + default=False, + metadata={ + "name": "use_tls", + "description": "Connect to the hubble relay over TLS", + "required": False, + "sensitive": False, + "type": "switch", + }, + ) + + ca_certificate: str = dataclasses.field( + default="", + metadata={ + "name": "ca_certificate", + "description": "CA certificate (PEM) used to verify the hubble relay server", + "required": False, + "sensitive": True, + "type": "file", + }, + ) + + client_certificate: str = dataclasses.field( + default="", + metadata={ + "name": "client_certificate", + "description": "Client certificate (PEM) for mutual TLS", + "required": False, + "sensitive": True, + "type": "file", + }, + ) + + client_key: str = dataclasses.field( + default="", + metadata={ + "name": "client_key", + "description": "Client private key (PEM) for mutual TLS", + "required": False, + "sensitive": True, + "type": "file", + }, + ) + class CiliumProvider(BaseTopologyProvider): """Manage Cilium provider.""" @@ -88,7 +132,13 @@ def pull_topology(self) -> list[TopologyServiceInDto]: ObserverStub, ) - channel = grpc.insecure_channel(self.authentication_config.cilium_base_endpoint) + channel = build_cilium_channel( + self.authentication_config.cilium_base_endpoint, + use_tls=self.authentication_config.use_tls, + ca_certificate=self.authentication_config.ca_certificate, + client_certificate=self.authentication_config.client_certificate, + client_key=self.authentication_config.client_key, + ) stub = ObserverStub(channel) # Create a request for the last 1000 flows diff --git a/keep/providers/cilium_provider/secure_channel.py b/keep/providers/cilium_provider/secure_channel.py new file mode 100644 index 0000000000..e779386cee --- /dev/null +++ b/keep/providers/cilium_provider/secure_channel.py @@ -0,0 +1,24 @@ +import grpc + + +def build_cilium_channel( + endpoint: str, + use_tls: bool = False, + ca_certificate: str = "", + client_certificate: str = "", + client_key: str = "", +) -> grpc.Channel: + """Build a gRPC channel to the Hubble relay, secured with TLS when enabled. + + When use_tls is set, the connection uses TLS; supplying client_certificate and + client_key enables mutual TLS, and ca_certificate verifies the server (falling + back to the system trust store when omitted). + """ + if not use_tls: + return grpc.insecure_channel(endpoint) + credentials = grpc.ssl_channel_credentials( + root_certificates=ca_certificate.encode() if ca_certificate else None, + certificate_chain=client_certificate.encode() if client_certificate else None, + private_key=client_key.encode() if client_key else None, + ) + return grpc.secure_channel(endpoint, credentials) diff --git a/tests/test_cilium_secure_channel.py b/tests/test_cilium_secure_channel.py new file mode 100644 index 0000000000..63ac278ccf --- /dev/null +++ b/tests/test_cilium_secure_channel.py @@ -0,0 +1,54 @@ +from unittest.mock import patch + +from keep.providers.cilium_provider.secure_channel import build_cilium_channel + +PATCH_GRPC = "keep.providers.cilium_provider.secure_channel.grpc" + + +def test_insecure_channel_when_tls_disabled(): + with patch(PATCH_GRPC) as mock_grpc: + channel = build_cilium_channel("localhost:4245", use_tls=False) + mock_grpc.insecure_channel.assert_called_once_with("localhost:4245") + mock_grpc.secure_channel.assert_not_called() + assert channel is mock_grpc.insecure_channel.return_value + + +def test_mutual_tls_passes_encoded_pem(): + with patch(PATCH_GRPC) as mock_grpc: + channel = build_cilium_channel( + "relay:4245", + use_tls=True, + ca_certificate="CA_PEM", + client_certificate="CERT_PEM", + client_key="KEY_PEM", + ) + mock_grpc.ssl_channel_credentials.assert_called_once_with( + root_certificates=b"CA_PEM", + certificate_chain=b"CERT_PEM", + private_key=b"KEY_PEM", + ) + mock_grpc.secure_channel.assert_called_once_with( + "relay:4245", mock_grpc.ssl_channel_credentials.return_value + ) + mock_grpc.insecure_channel.assert_not_called() + assert channel is mock_grpc.secure_channel.return_value + + +def test_server_only_tls_omits_client_material(): + with patch(PATCH_GRPC) as mock_grpc: + build_cilium_channel("relay:4245", use_tls=True, ca_certificate="CA_PEM") + mock_grpc.ssl_channel_credentials.assert_called_once_with( + root_certificates=b"CA_PEM", + certificate_chain=None, + private_key=None, + ) + + +def test_tls_without_ca_uses_system_trust(): + with patch(PATCH_GRPC) as mock_grpc: + build_cilium_channel("relay:4245", use_tls=True) + mock_grpc.ssl_channel_credentials.assert_called_once_with( + root_certificates=None, + certificate_chain=None, + private_key=None, + )