From d2a391e6d7baffd846767f0d9c45a124628e92cb Mon Sep 17 00:00:00 2001 From: Tyler Diderich Date: Mon, 17 Mar 2025 13:30:24 -0500 Subject: [PATCH 1/4] k8s init --- kubernetes/README.md | 92 ++++++++++++++ kubernetes/custom-integration-kubernetes.star | 119 ++++++++++++++++++ 2 files changed, 211 insertions(+) create mode 100644 kubernetes/README.md create mode 100644 kubernetes/custom-integration-kubernetes.star diff --git a/kubernetes/README.md b/kubernetes/README.md new file mode 100644 index 0000000..6d47dc6 --- /dev/null +++ b/kubernetes/README.md @@ -0,0 +1,92 @@ +# Custom Integration: Kubernetes + +## runZero requirements + +- Superuser access to the [Custom Integrations configuration](https://console.runzero.com/custom-integrations) in runZero. + +## Kubernetes requirements + +- `kubectl` installed and configured to interact with your Kubernetes cluster. +- Cluster admin privileges to retrieve node, pod, and cluster metadata. +- A Kubernetes service account with the necessary permissions to query the API. +- A **Bearer Token** for API authentication. +- An active `kubectl proxy` session to securely interact with the cluster API. + +## Steps + +### Kubernetes configuration + +1. **Create a Service Account and Bind Permissions** + Run the following commands to create a service account, bind it to the `cluster-reader` role, and retrieve the bearer token: + + ```sh + kubectl create serviceaccount rz-integration + kubectl create clusterrolebinding rz-integration-binding --clusterrole=view --serviceaccount=default:rz-integration + ``` + +2. **Retrieve the Bearer Token** + ```sh + kubectl create token rz-integration --duration=48h + ``` + + Copy and securely store this token, as it will be used to authenticate API requests. + +3. **Start the Kubernetes API Proxy** + ```sh + kubectl proxy --port=8001 + ``` + + This will make the Kubernetes API available at `http://127.0.0.1:8001` on your local machine. This is required since the API calls require a valid certificate. + +4. **Test API Access** + Validate that your token works and that the API is accessible: + + ```sh + curl -H "Authorization: Bearer YOUR_K8S_BEARER_TOKEN" -H "Accept: application/json" http://127.0.0.1:8001/api/v1/nodes + ``` + + You should receive a JSON response containing details about your cluster nodes. + +--- + +### runZero configuration + +1. **(OPTIONAL) - Customize the script** + - Modify API calls as needed to adjust asset metadata. + - Edit which attributes are ingested into runZero. + +2. **[Create a Credential for the Custom Integration](https://console.runzero.com/credentials)** + - Select **Custom Integration Script Secrets** as the credential type. + - Use the `access_key` field for **your Kubernetes API URL** (`http://127.0.0.1:8001`). + - Use the `access_secret` field for **your Bearer Token** (retrieved in step 2). + +3. **[Create the Custom Integration](https://console.runzero.com/custom-integrations/new)** + - Name the integration (e.g., **"Kubernetes"**). + - Add an icon if desired. + - Toggle **Enable custom integration script** and paste the finalized script. + - Click **Validate** to check for syntax errors. + - Click **Save** to store the integration. + +4. **[Create the Custom Integration task](https://console.runzero.com/ingest/custom/)** + - Select the Credential and Custom Integration from steps 2 and 3. + - Set up the task schedule for periodic asset ingestion. + - Choose the runZero Explorer where the integration should execute. + - Click **Save** to start the first ingestion task. + +--- + +### What's next? + +- The task will appear on the [tasks](https://console.runzero.com/tasks) page and run according to schedule. +- The integration will update existing assets or create new ones based on Kubernetes metadata. +- You can search for Kubernetes assets in runZero using: + + ``` + custom_integration:Kubernetes + ``` + +- Use runZero's asset search to filter by node roles, pod namespaces, or other collected metadata. + +--- + +🚀 **Your Kubernetes assets are now being ingested into runZero!** 🚀 diff --git a/kubernetes/custom-integration-kubernetes.star b/kubernetes/custom-integration-kubernetes.star new file mode 100644 index 0000000..fb4676c --- /dev/null +++ b/kubernetes/custom-integration-kubernetes.star @@ -0,0 +1,119 @@ +load('runzero.types', 'ImportAsset', 'NetworkInterface') +load('json', json_encode='encode', json_decode='decode') +load('http', http_post='post', http_get='get', 'url_encode') +load('net', 'ip_address') + +def k8s_api_request(hostname, token, endpoint): + """Generic function to interact with Kubernetes API""" + headers = { + "Authorization": "Bearer " + token, + "Accept": "application/json" + } + response = http_get(hostname + endpoint, headers=headers) + if response.status_code != 200: + print("Error fetching data:", response.status_code) + return None + return json_decode(response.body) + +def discover_k8s_nodes(hostname, token): + """Fetches Kubernetes nodes and converts them into runZero assets""" + nodes = k8s_api_request(hostname, token, "/api/v1/nodes") + if not nodes: + return [] + + assets = [] + for node in nodes.get("items", []): + metadata = node.get("metadata", {}) + status = node.get("status", {}) + node_name = metadata.get("name", "") + node_ips = [addr["address"] for addr in status.get("addresses", []) if addr["type"] == "InternalIP"] + + ip4s = [ip_address(ip) for ip in node_ips if "." in ip] + ip6s = [ip_address(ip) for ip in node_ips if ":" in ip] + + assets.append( + ImportAsset( + id="k8s-node-" + node_name, + hostnames=[node_name], + networkInterfaces=[NetworkInterface(ipv4Addresses=ip4s, ipv6Addresses=ip6s)], + os="Kubernetes", + osVersion=status.get("nodeInfo", {}).get("kubeletVersion", ""), + customAttributes={ + "k8s_node_uid": metadata.get("uid", ""), + "k8s_node_name": node_name, + "k8s_roles": metadata.get("labels", {}).get("kubernetes.io/role", ""), + "k8s_creation_timestamp": metadata.get("creationTimestamp", ""), + "k8s_kernel_version": status.get("nodeInfo", {}).get("kernelVersion", ""), + "k8s_os_image": status.get("nodeInfo", {}).get("osImage", ""), + "k8s_architecture": status.get("nodeInfo", {}).get("architecture", ""), + "k8s_container_runtime": status.get("nodeInfo", {}).get("containerRuntimeVersion", ""), + "k8s_capacity": status.get("capacity", {}), + "k8s_allocatable": status.get("allocatable", {}), + "k8s_conditions": status.get("conditions", []), + "k8s_pod_cidr": node.get("spec", {}).get("podCIDR", ""), + "k8s_pod_cidrs": node.get("spec", {}).get("podCIDRs", []), + "k8s_provider_id": node.get("spec", {}).get("providerID", ""), + "k8s_daemon_endpoints": status.get("daemonEndpoints", {}), + "k8s_images": status.get("images", []) + } + ) + ) + return assets + +def discover_k8s_pods(hostname, token): + """Fetches Kubernetes Pods and their metadata""" + pods = k8s_api_request(hostname, token, "/api/v1/pods") + if not pods: + return [] + + pod_assets = [] + for pod in pods.get("items", []): + metadata = pod.get("metadata", {}) + status = pod.get("status", {}) + spec = pod.get("spec", {}) + + pod_name = metadata.get("name", "") + namespace = metadata.get("namespace", "") + node_name = spec.get("nodeName", "Unknown") + + pod_ips = [addr["ip"] for addr in status.get("podIPs", [])] + ip4s = [ip_address(ip) for ip in pod_ips if "." in ip] + ip6s = [ip_address(ip) for ip in pod_ips if ":" in ip] + + container_images = [c["image"] for c in spec.get("containers", [])] + container_statuses = status.get("containerStatuses", []) + + pod_assets.append( + ImportAsset( + id="k8s-pod-" + pod_name, + hostnames=[pod_name], + networkInterfaces=[NetworkInterface(ipv4Addresses=ip4s, ipv6Addresses=ip6s)], + os="Containerized", + customAttributes={ + "k8s_pod_uid": metadata.get("uid", ""), + "k8s_pod_name": pod_name, + "k8s_namespace": namespace, + "k8s_node": node_name, + "k8s_pod_status": status.get("phase", ""), + "k8s_pod_start_time": status.get("startTime", ""), + "k8s_pod_conditions": status.get("conditions", []), + "k8s_pod_owner": metadata.get("ownerReferences", []), + "k8s_pod_ip": status.get("podIP", ""), + "k8s_pod_host_ip": status.get("hostIP", ""), + "k8s_container_images": ",".join(container_images), + "k8s_container_statuses": container_statuses + } + ) + ) + + return pod_assets + +def main(*args, **kwargs): + """Entry point for the integration""" + hostname = kwargs["access_key"] + token = kwargs["access_secret"] + node_assets = discover_k8s_nodes(hostname, token) + pod_assets = discover_k8s_pods(hostname, token) + + all_assets = node_assets + pod_assets + return all_assets if all_assets else None From ebd31809db140b698abab8231e57033dc4ecfb74 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 8 Apr 2025 16:25:00 +0000 Subject: [PATCH 2/4] Auto: update integrations JSON and README --- README.md | 1 + docs/integrations.json | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 02648f3..ec944da 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ If you need help setting up a custom integration, you can create an [issue](http - [Drata](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/drata/) - [JAMF](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/jamf/) - [Kandji](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/kandji/) +- [Kubernetes](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/kubernetes/) - [Lima Charlie](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/lima-charlie/) - [Tanium](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/tanium/) ## Export from runZero diff --git a/docs/integrations.json b/docs/integrations.json index 8e99131..378c286 100644 --- a/docs/integrations.json +++ b/docs/integrations.json @@ -1,6 +1,6 @@ { - "lastUpdated": "2025-04-08T16:09:55.802912Z", - "totalIntegrations": 10, + "lastUpdated": "2025-04-08T16:25:00.764802Z", + "totalIntegrations": 11, "integrationDetails": [ { "name": "Automox", @@ -38,6 +38,12 @@ "readme": "https://github.com/runZeroInc/runzero-custom-integrations/blob/main/digital-ocean/README.md", "integration": "https://github.com/runZeroInc/runzero-custom-integrations/blob/main/digital-ocean/custom-integration-digital-ocean.star" }, + { + "name": "Kubernetes", + "type": "inbound", + "readme": "https://github.com/runZeroInc/runzero-custom-integrations/blob/main/kubernetes/README.md", + "integration": "https://github.com/runZeroInc/runzero-custom-integrations/blob/main/kubernetes/custom-integration-kubernetes.star" + }, { "name": "Kandji", "type": "inbound", From f69e71ba7c93fb0feb9fca168cc08b60c21bb6aa Mon Sep 17 00:00:00 2001 From: Tyler Diderich Date: Thu, 17 Apr 2025 12:19:29 -0500 Subject: [PATCH 3/4] Trigger workflow From f3f6c9414f78fa9099b0f61902ae729411196eaa Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 17 Apr 2025 17:20:38 +0000 Subject: [PATCH 4/4] Auto: update integrations JSON and README --- docs/integrations.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/integrations.json b/docs/integrations.json index e137533..33c17c7 100644 --- a/docs/integrations.json +++ b/docs/integrations.json @@ -1,6 +1,6 @@ { - "lastUpdated": "2025-04-17T10:26:35.663903Z", - "totalIntegrations": 11, + "lastUpdated": "2025-04-17T17:20:37.983979Z", + "totalIntegrations": 12, "integrationDetails": [ { "name": "Automox",