# Kubernetes (kubectl + kubeconfig)

> Paste a view-only ServiceAccount kubeconfig and the agent runs kubectl reads against the cluster; which verbs and flags pass, what Connect and verify runs, and the save-time checks.

_How-to · Updated 2026-10-27 · Describes release 0.1.164_

## Overview {#overview}

A kubectl connection gives the agents direct, read-only cluster access from a pasted kubeconfig, with no MCP server in between. The **Incident** agent reads pods, events, rollouts and logs through it (Kubernetes is one of the five categories that agent needs), the **Compliance** agent reads cluster state for its controls, and the estate x-ray harvests the inventory from it. A second, write-role kubectl connection is how an approved deployment restart runs.

The binary runs on the platform host, or inside a [private-network agent](/help/private-access.md) for a cluster whose API is private; in that case the agent's image must include `kubectl`.

## Setup {#setup}

### Before you start {#before-you-start}

- The `datasources.manage` permission; the built-in owner and operator roles carry it.
- A kubeconfig for a view-only ServiceAccount that may also `get` and `list` `nodes` ([Installation](#installation)).
- The API server reachable from the platform, or a [private-network agent](/help/private-access.md) in the cluster's network.

### Installation {#installation}

Create a dedicated ServiceAccount bound to the built-in `view` role in the namespaces the agent may read, plus `get` and `list` on `nodes`, which the connection check needs; the default `view` role alone does not allow the node probe. Export a kubeconfig whose current context names that account, the cluster's CA embedded as `certificate-authority-data` and, when the account is namespace-bound, the namespace. [Connect Kubernetes](/help/setup-kubernetes.md) has a copy-and-run command that does exactly this for one namespace and prints `aiops-readonly.kubeconfig`. Exclude Secrets, `pods/exec` and every write from the role; the platform refuses writes anyway, but RBAC is the real boundary. A token from `kubectl create token` expires; decide how it is renewed.

### Configuration {#configuration}

1. Open **Settings · Data in → Data sources** and select **Connect datasource**.
2. Under **Type**, choose **Kubernetes (kubectl + kubeconfig)**. Enter a **Connection name**; the connected list shows it, and per-connection agent tools are named after it.
3. Fill the fields:

| Field (portal label) | API key | Value |
|---|---|---|
| kubeconfig YAML | `kubeconfig` | The complete file, line breaks preserved; a secret, parsed as YAML on save |
| Namespaces the service account may read, when it is not allowed to list them cluster-wide; comma-separated (optional) | `namespaces` | `payments, checkout` when the account is bound with RoleBindings only |

4. Optionally, open **Optional · describe this environment** and fill **What should AIOps know about it?** with the cluster, the namespaces and the critical services.
5. Under **Use this connection for**, choose **Investigate only** and select **Connect and verify**.

The same connection through the API (`POST /api/aiops/{tenant_id}/datasources`, requires `datasources.manage`; the kubeconfig goes in `secret_values`, and one posted in `config` is moved there):

```json
{
  "type_key": "kubectl",
  "name": "prod-eks",
  "description": "Production cluster; checkout and payments run in the payments namespace.",
  "config": {
    "namespaces": "payments, checkout"
  },
  "secret_values": {
    "kubeconfig": "apiVersion: v1\nkind: Config\nclusters:\n- name: estate\n  cluster:\n    server: https://<api-server>\n    certificate-authority-data: <base64>\ncontexts:\n- name: estate\n  context:\n    cluster: estate\n    user: aiops-reader\n    namespace: payments\ncurrent-context: estate\nusers:\n- name: aiops-reader\n  user:\n    token: <service-account token>\n"
  },
  "role": "read",
  "enabled": true
}
```

The save validates the paste: `'kubeconfig' is not valid YAML (…) — paste the full file preserving line breaks` or `'kubeconfig' does not parse as a YAML mapping — line breaks were probably lost in the paste` is a `400` at save time, not a failure mid-investigation. The accepted kubeconfig forms are a user with a static `token` (a ServiceAccount token) or with `client-certificate-data` and `client-key-data`, and a cluster with `certificate-authority-data`; everything the file needs is inside it. A kubeconfig whose user runs a credential plugin (`exec`, such as `aws eks get-token` or kubelogin, or the older `auth-provider`), or that names a file on the platform host (`tokenFile`, `client-certificate`, `client-key`, `certificate-authority` as a path), is refused the same way with `This kubeconfig runs a credential plugin (users[].user.exec or auth-provider) or names a file on the platform host (tokenFile, client-certificate, client-key, certificate-authority); the platform does neither. Use a kubeconfig with a static token or a service-account token and embedded certificate data (certificate-authority-data, client-*-data).` The platform image ships no such plugin, kubectl would otherwise read the named file from the platform host (a `tokenFile` is sent to the kubeconfig's server as the bearer token), and a connection saved before this check gets the same message from every tool call and Test instead of running kubectl. The pasted kubeconfig is also the only identity kubectl can use: the platform strips the pod's in-cluster API discovery from every connector process, so a kubeconfig that resolves to no server (`{}`, a file without `current-context`) fails against `localhost:8080` instead of answering with the platform's own service account, and a connection saved with a blank kubeconfig answers `the connection's 'kubeconfig' is blank — paste it and save the connection before use` instead of running kubectl.

### Validation {#validation}

**Connect and verify**, and **Test** on the row later, looks for `kubectl` on the host's PATH, tries `kubectl --version`, then runs `kubectl get nodes --request-timeout=5s -o name` with the kubeconfig written to a private file and passed through `KUBECONFIG`. **You should see** a dialog titled `<connection name> is connected` with **Connected — verified from the control plane**, and later in the row's **Last test** `ok · binary found: /usr/local/bin/kubectl — … — 'kubectl get nodes --request-timeout=5s -o name' ok (node/<name>)`, the last line of the real output. A kubeconfig that does not parse, an API server that does not answer and a ServiceAccount without node read all fail here. When a private-network agent serves the workspace, the platform does not run the command itself: the row shows the check as queued, the agent runs it, and the result reads **Connected — verified by your private-network agent**.

### If something fails {#if-something-fails}

The dialog shows **Needs a fix**, what to fix and the check's own detail; the row is saved either way. Select **Edit**, correct the field the detail names and save again, or select **Retry** once a cause outside the form (network, vendor-side permission) is fixed. Every verdict this connector produces, and what to do about it, is in [Troubleshooting](#troubleshooting) below.

## Data collected {#data-collected}

### Tools the agent gets {#tools}

- `<name>_exec` — runs `kubectl` with the argument vector the agent passes; a leading `kubectl` in the vector is dropped. The kubeconfig's current context is preconfigured, and the tool description tells the agent not to pass `--context` or `--kubeconfig` unless the connection notes name a context. Output is the merged stdout and stderr, `exit code <n>` first on a failure, capped at 20,000 characters; a command is killed after 60 seconds (`timed out after 60s`), and a missing binary at run time reads `cannot run 'kubectl': …`.

A kubectl connection registers no HTTP tool. The estate x-ray uses the same tool under its own, stricter read set, which does not include `logs` or `events`.

### Read surface {#read-surface}

An investigation runs a kubectl call only when the platform's own kubectl adapter verifies it as a read, independently of the connection's verb list. What passes: the verbs `get`, `describe`, `top`, `explain`, `api-resources`, `api-versions`, `cluster-info` (not `cluster-info dump`), `version`, `logs`, `events`, `help` and `auth can-i`, optionally preceded by the global flags `--context`, `--namespace` or `-n`, `--request-timeout` and `--v` with their values. What is refused, never offered for approval: every other verb (`apply`, `delete`, `scale`, `rollout restart`, `exec`, `cp`, `patch`, `edit`), `diff` although the catalog lists it among the read verbs, any other leading flag, and anywhere in the vector `--raw`, `--subresource`, `--server` or `-s`, `--kubeconfig`, `--token`, `--as`, `--client-*`, `--certificate-*`, `--insecure-*`, `--username` and `--password`, so only the saved credential and the saved cluster are ever used. The refusal reads `Investigation connections are read-only. This operation was not executed; prepare an incident change using a write connection and obtain approval.`

The catalog's own list — `get`, `describe`, `top`, `explain`, `api-resources`, `api-versions`, `cluster-info`, `version`, `logs`, `events`, `diff` — is the approval gate's read set; the verified adapter above is the floor underneath it. The full rule table is under **The read/write gate in detail** in [Connections and remediation access](/help/read-and-write-roles.md#read-role-vs-write-role).

### Inventory and namespaces {#inventory}

The estate inventory first lists each kind cluster-wide. When that is `Forbidden`, it reads `kubectl get <kind> -n <namespace>` for every namespace in **Namespaces**, else the current context's namespace, and its notes say `namespace-scoped read: N namespace(s) from …`. Nodes or namespaces that stay unreadable leave the pods without a host and the namespace-selector NetworkPolicies flagged, and a blast radius touching them is reported as estimated with that coverage gap.

### Approved fixes {#approved-fixes}

A deployment restart runs through a second kubectl connection saved as **Approved changes only (separate credential)** with **Allow use for approved plans** ticked, on a ServiceAccount scoped to the namespace and the verbs the fix needs. The namespace in that kubeconfig's context is the scope a restart may target; a context without one confirms none. How approvals and tiers apply is in [Set up write connections](/help/setup-write-connections.md).

## Troubleshooting {#troubleshooting}

| What you see | Cause | What to do |
|---|---|---|
| `400 'kubeconfig' is not valid YAML …` or `… does not parse as a YAML mapping …` on save | The paste lost its line breaks. | Paste the complete file into the multi-line field. |
| `This kubeconfig runs a credential plugin … or names a file on the platform host …` on save, Test or a tool call | The kubeconfig's user authenticates through `exec` or `auth-provider` (an EKS, GKE or AKS default kubeconfig), or a user or cluster points at a file path (`tokenFile`, `client-certificate`, `client-key`, `certificate-authority`) instead of embedding the data. | Export a kubeconfig with a ServiceAccount token and `certificate-authority-data` ([Installation](#installation)); `kubectl config set-cluster … --embed-certs=true` embeds a CA. |
| `the connection's 'kubeconfig' is blank …` on Test or a tool call | The connection was saved without its kubeconfig. | Paste the file and save the connection. |
| `… dial tcp [::1]:8080: connect: connection refused` | The kubeconfig resolves to no server: no `current-context`, or a context whose cluster has no `server`. | Paste the complete file (`kubectl config view --minify --flatten`). |
| `failed · binary 'kubectl' not found on PATH` | The host that runs the check has no kubectl. | Ask your platform operator; a private-network agent needs it in its image. |
| `failed · 'kubectl get nodes --request-timeout=5s -o name' failed (exit 1): …` | The tail of the output names it: a connection refused or timeout (API server unreachable), `Unauthorized` (expired or wrong token), `Forbidden` (no node read). | Fix the kubeconfig or the RBAC at the source and **Test** again. |
| `failed · 'kubectl get nodes --request-timeout=5s -o name' timed out after 5s` | The API server does not answer the platform. | For a private API, install a [private-network agent](/help/private-access.md) inside that network. |
| The row shows the check queued for the private agent and never returns | No live agent, or the agent's image lacks kubectl. | Check the agent's heartbeat under **Connect**, and its image. |
| A `kubectl delete`, `apply`, `exec` or `rollout restart` answers `Investigation connections are read-only.` | The verified read adapter. | Expected; a restart runs through the write connection after approval. |
| `kubectl diff` or `kubectl get --raw /…` is refused although `diff` is a read verb | The adapter verifies a fixed verb set and refuses `--raw`. | Use `get` with `-o yaml` instead. |
| The agent sees one namespace only | The ServiceAccount is bound with RoleBindings and **Namespaces** is empty, so the inventory fell back to the context's namespace. | List the readable namespaces in **Namespaces**. |
| An x-ray denies `kubectl logs` | The x-ray read set excludes `logs` and `events` on purpose. | Expected; investigations may read logs. |

The generic verdict strings are explained in [Test a connection and read the failure](/help/manage-connections.md#troubleshooting). Every connection status word is in the [status and error reference](/help/status-and-error-reference.md#connection-statuses).

## Next steps {#next-steps}

- Select **Discover this connection** in the success dialog to find the services and dependencies behind it: [Launch a scan](/help/estate-harvest-and-xray.md#launch-a-scan).
- See what the inventory reads per namespace: [Inventory and namespaces](#inventory).
- Run approved fixes through a separate write connection: [Approved fixes](#approved-fixes).
- See what else each agent needs in [Choose your connection](/help/choose-your-connection.md#agent-needs).
