DEV Community

Cover image for K8s MCP Symfony — Diagnose Kubernetes with ChatGPT, safely
sebk69
sebk69

Posted on

K8s MCP Symfony — Diagnose Kubernetes with ChatGPT, safely

K8s MCP Symfony — Diagnose Kubernetes with ChatGPT, safely

Modern Kubernetes clusters expose a lot of useful information.

But let’s be honest:

  • Troubleshooting often requires many kubectl commands
  • Pod status, events and logs are spread across different resources
  • Giving an AI assistant unrestricted access to a cluster is dangerous
  • Copying logs and manifests manually quickly becomes painful

So I built K8s MCP Symfony.

It is a read-only Model Context Protocol server that lets ChatGPT and other MCP clients inspect and diagnose Kubernetes clusters.

The project is built with Symfony and secured with OAuth 2.1.


What does it provide?

The first release exposes a focused set of Kubernetes tools:

  • k8s_get_cluster_info
  • k8s_list_namespaces
  • k8s_list_nodes
  • k8s_list_pods
  • k8s_get_pod
  • k8s_get_pod_logs
  • k8s_list_events
  • k8s_diagnose_pod

For example, you can ask your assistant:

Diagnose the pod checkout-api-7d9c8f8f56-k2m4p
in the production namespace.
Enter fullscreen mode Exit fullscreen mode

The server can correlate:

  • Pod status
  • Container states
  • Kubernetes events
  • Current or previous logs
  • Restart information

Instead of manually executing several commands, the assistant receives structured evidence through MCP.


Architecture: simple by design

ChatGPT or MCP client
        ↓
OAuth 2.1 access token
        ↓
K8s MCP Symfony
        ↓
Read-only Kubernetes API
Enter fullscreen mode Exit fullscreen mode

The Symfony application implements the MCP JSON-RPC endpoint and translates tool calls into controlled Kubernetes API requests.

It does not execute kubectl.

It communicates directly with the Kubernetes API using a dedicated service account.


Security first

Connecting an AI assistant to Kubernetes requires strict boundaries.

K8s MCP Symfony is read-only by design.

Kubernetes API restrictions

Only HTTP GET requests are allowed.

Resources must be present in an explicit allowlist.

Dangerous subresources are rejected, including:

  • exec
  • attach
  • portforward
  • proxy
  • ephemeralcontainers
  • Service-account token creation

The service account only receives Kubernetes permissions such as:

verbs:
  - get
  - list
  - watch
Enter fullscreen mode Exit fullscreen mode

There is no permission to create, update, patch or delete resources.

Namespace restrictions

The server can also be restricted to selected namespaces:

K8S_ALLOWED_NAMESPACES=production,staging
Enter fullscreen mode Exit fullscreen mode

This prevents the MCP client from exploring unrelated namespaces.

OAuth scopes

Calling Kubernetes tools requires an OAuth access token.

The default scopes are:

openid
k8s.read
Enter fullscreen mode Exit fullscreen mode

Tools that expose logs also require:

k8s.logs
Enter fullscreen mode Exit fullscreen mode

This makes it possible to authorize cluster inspection without automatically granting access to application logs.

Automatic redaction

Every Kubernetes response is sanitized before being returned.

The sanitizer removes or hides:

  • Secret values
  • Service-account tokens
  • Passwords
  • API keys
  • Private keys
  • Authorization headers
  • Database URLs
  • Sensitive environment variables
  • Managed fields
  • Last-applied configurations

Secret objects can expose their metadata, but never their data or stringData content.

Logs and responses are also bounded to avoid sending unlimited amounts of cluster data to the MCP client.


Quick start

Clone the repository:

git clone https://git.small-project.dev/pub/apps/k8s-mcp-symfony.git
cd k8s-mcp-symfony
Enter fullscreen mode Exit fullscreen mode

Create your local configuration:

cp .env .env.local
composer install
Enter fullscreen mode Exit fullscreen mode

Configure the MCP endpoint and OAuth provider:

MCP_PUBLIC_URL=https://k8s-mcp.example.com/mcp
MCP_OAUTH_RESOURCE=https://k8s-mcp.example.com/mcp

MCP_OAUTH_ISSUER=https://sso.example.com/realms/mcp
MCP_OAUTH_JWKS_URI=https://sso.example.com/realms/mcp/protocol/openid-connect/certs

MCP_OAUTH_READ_SCOPE=k8s.read
MCP_OAUTH_LOGS_SCOPE=k8s.logs
Enter fullscreen mode Exit fullscreen mode

Then configure Kubernetes access:

K8S_API_URL=https://kubernetes.default.svc
K8S_API_TOKEN=
K8S_ALLOWED_NAMESPACES=production,staging
K8S_LOGS_ENABLED=true
Enter fullscreen mode Exit fullscreen mode

Start the application:

docker compose up --build
Enter fullscreen mode Exit fullscreen mode

The local MCP endpoint is available at:

http://localhost:8080/mcp
Enter fullscreen mode Exit fullscreen mode

Kubernetes deployment

The repository contains Kubernetes templates for:

  • Service account
  • Read-only ClusterRole
  • ClusterRoleBinding
  • Deployment
  • Service
  • Ingress

The application can therefore run directly inside the cluster and use its mounted service-account token.

Release pipelines validate the PHP project, run PHPStan and PHPUnit, build the container image and deploy tagged versions to Kubernetes.

A release can be created with:

bin/release --patch
Enter fullscreen mode Exit fullscreen mode

Why Symfony?

Symfony provides everything needed for a small remote MCP service:

  • HTTP routing
  • Dependency injection
  • HTTP client
  • Environment configuration
  • Testable services
  • Structured JSON responses

The MCP implementation remains small while the Kubernetes and security layers stay isolated and testable.


Current limits

The project intentionally does not modify the cluster.

You cannot use it to:

  • Restart a Deployment
  • Delete a Pod
  • Scale a workload
  • Apply a manifest
  • Execute a command inside a container

These operations should remain behind a separate and much stricter approval process.

The goal of this server is diagnosis, not autonomous cluster administration.


Links

Git repository:

https://git.small-project.dev/pub/apps/k8s-mcp-symfony

License:

GNU GPL v3.


Feedback welcome

The first release focuses on safe pod and cluster diagnostics.

Which Kubernetes diagnostic tool should be added next?

  • Deployment rollout analysis?
  • Service and endpoint diagnostics?
  • PVC and storage inspection?
  • Resource usage and capacity reports?
  • Ingress and certificate diagnostics?

Top comments (2)

Collapse
 
sebk69 profile image
sebk69

Thanks for the feedback, you were absolutely right about the main issue: an application-level namespace allowlist is not enough if the ServiceAccount still has overly broad permissions in Kubernetes.

I reworked the implementation accordingly.

Namespace restrictions are no longer enforced by the application. K8S_ALLOWED_NAMESPACES has been removed entirely. The application now only validates the namespace format, while the actual authorization decision is delegated to Kubernetes RBAC.

The RBAC model has also been split more strictly:

  • a ClusterRole only for truly cluster-scoped resources;
  • a Role + RoleBinding in each namespace the MCP is allowed to access;
  • no access to Secrets or ConfigMaps.

The allowed namespaces are now defined only at deployment time through KUBE_ACCESS_NAMESPACES, and that value is never exposed to the application runtime.

I also added negative tests covering cross-namespace access, Secrets, ConfigMaps, sensitive subresources such as exec, attach, portforward, etc., as well as expired or audience-mismatched ServiceAccount tokens.

I then verified the permissions against the real cluster: pods, pods/log, events, and the diagnostic operations all work through the deployed RBAC permissions.

So the security model is now much clearer:

  • OAuth scopes = what the MCP caller is allowed to request
  • Kubernetes RBAC = what the MCP process is actually allowed to read

There is still one improvement I have not implemented yet: using separate Kubernetes credentials for log access, so that pods/log can be isolated even further.

Thanks again for the feedback, it definitely helped improve the security model.

Collapse
 
mads_hansen_27b33ebfee4c9 profile image
Mads Hansen

This is a strong boundary, especially separating k8s.logs from general read access. I’d harden it one layer lower too: if deployment uses a ClusterRoleBinding while namespace isolation lives only in K8S_ALLOWED_NAMESPACES, an application bug or bypass still inherits cluster-wide read. A minimal Role + RoleBinding in each allowed namespace makes Kubernetes enforce the same boundary.

Logs deserve separate upstream credentials as well—not only a separate OAuth scope—so a request lacking k8s.logs cannot reach pods/log even if routing or scope checks regress. Useful negative tests would cover another namespace, Secrets/ConfigMaps, every rejected subresource, and an expired/audience-mismatched projected service-account token. I’d also cap list/watch selectors and request budgets: read-only Kubernetes calls can still create significant data exposure and API-server load.