Skip to content

DRAFT: Kubernetes integration #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/)
- [NinjaOne](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/ninjaone/)
- [Tanium](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/tanium/)
Expand Down
10 changes: 8 additions & 2 deletions docs/integrations.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -44,6 +44,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",
Expand Down
92 changes: 92 additions & 0 deletions kubernetes/README.md
Original file line number Diff line number Diff line change
@@ -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!** 🚀
119 changes: 119 additions & 0 deletions kubernetes/custom-integration-kubernetes.star
Original file line number Diff line number Diff line change
@@ -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