From c4cf3ba7989e09cb0589bcdf910d50a6574b8647 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 17 Sep 2026 23:10:17 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat(ai):=20mcp-k8s=20=D1=87=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=B7=20ToolHive=20(read-only=20RBAC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MCP-сервер k8s из реестра ToolHive: ghcr.io/stackloklabs/mkp/server:0.4.3, transport streamable-http. Отдельный SA mcp-k8s и ClusterRole mcp-k8s-readonly (только get/list/watch). Core-группа перечислена поимённо, чтобы не открывать secrets; остальные группы явным списком, потому что apiGroups: [*] включает и core-группу. Наружу не выводим: без oidcConfigRef прокси ToolHive неаутентифицирован. --- kubernetes/apps/ai/kustomization.yaml | 1 + .../apps/ai/mcp-k8s/app/kustomization.yaml | 7 ++ kubernetes/apps/ai/mcp-k8s/app/mcpserver.yaml | 28 +++++++ kubernetes/apps/ai/mcp-k8s/app/rbac.yaml | 84 +++++++++++++++++++ .../apps/ai/mcp-k8s/app/serviceaccount.yaml | 8 ++ kubernetes/apps/ai/mcp-k8s/ks.yaml | 19 +++++ 6 files changed, 147 insertions(+) create mode 100644 kubernetes/apps/ai/mcp-k8s/app/kustomization.yaml create mode 100644 kubernetes/apps/ai/mcp-k8s/app/mcpserver.yaml create mode 100644 kubernetes/apps/ai/mcp-k8s/app/rbac.yaml create mode 100644 kubernetes/apps/ai/mcp-k8s/app/serviceaccount.yaml create mode 100644 kubernetes/apps/ai/mcp-k8s/ks.yaml diff --git a/kubernetes/apps/ai/kustomization.yaml b/kubernetes/apps/ai/kustomization.yaml index 2894bf3..d019a33 100644 --- a/kubernetes/apps/ai/kustomization.yaml +++ b/kubernetes/apps/ai/kustomization.yaml @@ -8,4 +8,5 @@ components: resources: - ./namespace.yaml + - ./mcp-k8s/ks.yaml - ./toolhive/ks.yaml diff --git a/kubernetes/apps/ai/mcp-k8s/app/kustomization.yaml b/kubernetes/apps/ai/mcp-k8s/app/kustomization.yaml new file mode 100644 index 0000000..1aff7a8 --- /dev/null +++ b/kubernetes/apps/ai/mcp-k8s/app/kustomization.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./serviceaccount.yaml + - ./rbac.yaml + - ./mcpserver.yaml diff --git a/kubernetes/apps/ai/mcp-k8s/app/mcpserver.yaml b/kubernetes/apps/ai/mcp-k8s/app/mcpserver.yaml new file mode 100644 index 0000000..e397001 --- /dev/null +++ b/kubernetes/apps/ai/mcp-k8s/app/mcpserver.yaml @@ -0,0 +1,28 @@ +--- +# Kubernetes MCP из реестра ToolHive (сервер k8s, проект StacklokLabs/mkp). +# Транспорт streamable-http — mkp отдаёт MCP по HTTP сам, прокси-обёртка над +# stdio не нужна. Порты оставлены дефолтными: proxyPort и mcpPort оба 8080. +# +# ВАЖНО: без oidcConfigRef прокси ToolHive работает без аутентификации — любой, +# кто достучится до его порта, получит доступ к API под правами SA ниже. +# Поэтому наружу сервер не выводим: пока агент вне кластера, адрес только +# внутрикластерный, а внешний доступ появится вместе с OIDC после Authelia. +apiVersion: toolhive.stacklok.dev/v1beta1 +kind: MCPServer +metadata: + name: mcp-k8s +spec: + image: ghcr.io/stackloklabs/mkp/server:0.4.3 + transport: streamable-http + # SA существует заранее (см. serviceaccount.yaml) — вместе с ним приезжает + # ClusterRoleBinding mcp-k8s-readonly. + serviceAccount: mcp-k8s + permissionProfile: + type: builtin + name: network + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + memory: 256Mi diff --git a/kubernetes/apps/ai/mcp-k8s/app/rbac.yaml b/kubernetes/apps/ai/mcp-k8s/app/rbac.yaml new file mode 100644 index 0000000..8ac8b7c --- /dev/null +++ b/kubernetes/apps/ai/mcp-k8s/app/rbac.yaml @@ -0,0 +1,84 @@ +--- +# Права MCP-сервера на API. Только чтение: mkp умеет и apply_resource, и +# delete_resource, и exec в поды — ограничение живёт исключительно здесь, +# поэтому список verbs закрыт чтением. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: mcp-k8s-readonly +rules: + # Core-группа перечислена поимённо: resources: ["*"] открыл бы и secrets, + # а cluster-secrets, токен Proxmox CSI и прочие креды агенту читать незачем. + - apiGroups: [""] + resources: + - configmaps + - endpoints + - events + - limitranges + - namespaces + - nodes + - persistentvolumeclaims + - persistentvolumes + - pods + - pods/log + - pods/status + - replicationcontrollers + - resourcequotas + - serviceaccounts + - services + verbs: ["get", "list", "watch"] + # Остальные группы перечислены явно, потому что apiGroups: ["*"] включает и + # core-группу — то есть снова открыл бы secrets. Список закрытый: не хватает + # группы — сервер просто не увидит её ресурсы, ничего не сломается. + - apiGroups: + - admissionregistration.k8s.io + - apiextensions.k8s.io + - apiregistration.k8s.io + - apps + - authentication.k8s.io + - authorization.k8s.io + - autoscaling + - batch + - certificates.k8s.io + - coordination.k8s.io + - discovery.k8s.io + - events.k8s.io + - flowcontrol.apiserver.k8s.io + - metrics.k8s.io + - networking.k8s.io + - node.k8s.io + - policy + - rbac.authorization.k8s.io + - scheduling.k8s.io + - storage.k8s.io + - acme.cert-manager.io + - cert-manager.io + - cilium.io + - dragonflydb.io + - external-secrets.io + - gateway.envoyproxy.io + - gateway.networking.k8s.io + - generators.external-secrets.io + - helm.toolkit.fluxcd.io + - image.toolkit.fluxcd.io + - kustomize.toolkit.fluxcd.io + - monitoring.coreos.com + - notification.toolkit.fluxcd.io + - snapshot.storage.k8s.io + - source.toolkit.fluxcd.io + - toolhive.stacklok.dev + resources: ["*"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: mcp-k8s-readonly +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: mcp-k8s-readonly +subjects: + - kind: ServiceAccount + name: mcp-k8s + namespace: ai diff --git a/kubernetes/apps/ai/mcp-k8s/app/serviceaccount.yaml b/kubernetes/apps/ai/mcp-k8s/app/serviceaccount.yaml new file mode 100644 index 0000000..29d62d5 --- /dev/null +++ b/kubernetes/apps/ai/mcp-k8s/app/serviceaccount.yaml @@ -0,0 +1,8 @@ +--- +# SA создаётся здесь намеренно: spec.serviceAccount в MCPServer означает «уже +# существующий» — если его не создать, оператор сделает свой и права выдавать +# будет некуда. +apiVersion: v1 +kind: ServiceAccount +metadata: + name: mcp-k8s diff --git a/kubernetes/apps/ai/mcp-k8s/ks.yaml b/kubernetes/apps/ai/mcp-k8s/ks.yaml new file mode 100644 index 0000000..005cb92 --- /dev/null +++ b/kubernetes/apps/ai/mcp-k8s/ks.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: mcp-k8s +spec: + interval: 1h + path: ./kubernetes/apps/ai/mcp-k8s/app + prune: true + sourceRef: + kind: GitRepository + name: flux-system + namespace: flux-system + targetNamespace: ai + wait: true + # Зависимость внутри своего ns, поэтому поле namespace не нужно (по CRD оно + # по умолчанию равно ns самого объекта-зависимости). + dependsOn: + - name: toolhive-operator -- 2.54.0 From c2c9b273f232aba8392092fd302aa8bd9834ff15 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 17 Sep 2026 23:17:35 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix(ai):=20mcp-k8s=20=E2=80=94=20containers?= =?UTF-8?q?/kubernetes-mcp-server=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=BE?= =?UTF-8?q?=20mkp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit У mkp нет ни одного безопасного режима, только RBAC. У containers/kubernetes-mcp-server есть штатный --read-only, --toolsets и denied_resources, официальный гайд по read-only SA и чарт. Образ quay.io/containers/kubernetes_mcp_server:v0.0.66 с дайджестом (тег проверен в API quay). RBAC: вместо самодельного списка — встроенная view (secrets в неё не входят) + отдельный ClusterRole на кластерные объекты и метрики. Аргументы: --port 8080 --read-only --toolsets core. --- kubernetes/apps/ai/mcp-k8s/app/mcpserver.yaml | 30 +++-- kubernetes/apps/ai/mcp-k8s/app/rbac.yaml | 109 ++++++------------ 2 files changed, 56 insertions(+), 83 deletions(-) diff --git a/kubernetes/apps/ai/mcp-k8s/app/mcpserver.yaml b/kubernetes/apps/ai/mcp-k8s/app/mcpserver.yaml index e397001..cbf9342 100644 --- a/kubernetes/apps/ai/mcp-k8s/app/mcpserver.yaml +++ b/kubernetes/apps/ai/mcp-k8s/app/mcpserver.yaml @@ -1,21 +1,31 @@ --- -# Kubernetes MCP из реестра ToolHive (сервер k8s, проект StacklokLabs/mkp). -# Транспорт streamable-http — mkp отдаёт MCP по HTTP сам, прокси-обёртка над -# stdio не нужна. Порты оставлены дефолтными: proxyPort и mcpPort оба 8080. +# Kubernetes MCP Server: containers/kubernetes-mcp-server (Apache-2.0, Go, без обёрток над kubectl). +# Транспорт streamable-http — сервер сам отдаёт MCP по HTTP на --port 8080, путь /mcp, +# ровно то, что ожидает прокси ToolHive (обёртка над stdio не нужна). # -# ВАЖНО: без oidcConfigRef прокси ToolHive работает без аутентификации — любой, -# кто достучится до его порта, получит доступ к API под правами SA ниже. -# Поэтому наружу сервер не выводим: пока агент вне кластера, адрес только -# внутрикластерный, а внешний доступ появится вместе с OIDC после Authelia. +# --read-only — штатный режим сервера: ни одной write-операции (create/update/delete). +# Это второй барьер после RBAC SA (см. rbac.yaml); третий — denied_resources в TOML, +# если понадобится спрятать ресурс от инструментов совсем. +# +# --toolsets core оставляет базовый набор: без helm (чарты ставит Flux, агенту там +# делать нечего), tekton, kubevirt, kiali и netobserv. +# +# ВАЖНО: без oidcConfigRef прокси ToolHive работает без аутентификации — любой, кто +# достучится до порта, получит доступ к API под правами SA ниже. Поэтому наружу сервер +# не выводим: внешний доступ появится вместе с OIDC после Authelia. apiVersion: toolhive.stacklok.dev/v1beta1 kind: MCPServer metadata: name: mcp-k8s spec: - image: ghcr.io/stackloklabs/mkp/server:0.4.3 + image: quay.io/containers/kubernetes_mcp_server:v0.0.66@sha256:6d650f4bd6ac303ad82713c997e73a2d001602f9bf17392c9b9a0e30e29c6423 transport: streamable-http - # SA существует заранее (см. serviceaccount.yaml) — вместе с ним приезжает - # ClusterRoleBinding mcp-k8s-readonly. + args: + - --port + - "8080" + - --read-only + - --toolsets + - core serviceAccount: mcp-k8s permissionProfile: type: builtin diff --git a/kubernetes/apps/ai/mcp-k8s/app/rbac.yaml b/kubernetes/apps/ai/mcp-k8s/app/rbac.yaml index 8ac8b7c..3092ce9 100644 --- a/kubernetes/apps/ai/mcp-k8s/app/rbac.yaml +++ b/kubernetes/apps/ai/mcp-k8s/app/rbac.yaml @@ -1,83 +1,46 @@ --- -# Права MCP-сервера на API. Только чтение: mkp умеет и apply_resource, и -# delete_resource, и exec в поды — ограничение живёт исключительно здесь, -# поэтому список verbs закрыт чтением. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: mcp-k8s-readonly -rules: - # Core-группа перечислена поимённо: resources: ["*"] открыл бы и secrets, - # а cluster-secrets, токен Proxmox CSI и прочие креды агенту читать незачем. - - apiGroups: [""] - resources: - - configmaps - - endpoints - - events - - limitranges - - namespaces - - nodes - - persistentvolumeclaims - - persistentvolumes - - pods - - pods/log - - pods/status - - replicationcontrollers - - resourcequotas - - serviceaccounts - - services - verbs: ["get", "list", "watch"] - # Остальные группы перечислены явно, потому что apiGroups: ["*"] включает и - # core-группу — то есть снова открыл бы secrets. Список закрытый: не хватает - # группы — сервер просто не увидит её ресурсы, ничего не сломается. - - apiGroups: - - admissionregistration.k8s.io - - apiextensions.k8s.io - - apiregistration.k8s.io - - apps - - authentication.k8s.io - - authorization.k8s.io - - autoscaling - - batch - - certificates.k8s.io - - coordination.k8s.io - - discovery.k8s.io - - events.k8s.io - - flowcontrol.apiserver.k8s.io - - metrics.k8s.io - - networking.k8s.io - - node.k8s.io - - policy - - rbac.authorization.k8s.io - - scheduling.k8s.io - - storage.k8s.io - - acme.cert-manager.io - - cert-manager.io - - cilium.io - - dragonflydb.io - - external-secrets.io - - gateway.envoyproxy.io - - gateway.networking.k8s.io - - generators.external-secrets.io - - helm.toolkit.fluxcd.io - - image.toolkit.fluxcd.io - - kustomize.toolkit.fluxcd.io - - monitoring.coreos.com - - notification.toolkit.fluxcd.io - - snapshot.storage.k8s.io - - source.toolkit.fluxcd.io - - toolhive.stacklok.dev - resources: ["*"] - verbs: ["get", "list", "watch"] ---- +# Базовое чтение даём встроенной ролью view — её же рекомендует официальный гайд +# containers/kubernetes-mcp-server (docs/getting-started-kubernetes.md). view +# описывает namespaced-ресурсы и secrets в неё не входят, поэтому именно она, а не +# самодельный список с resources: ["*"]. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: - name: mcp-k8s-readonly + name: mcp-k8s-view roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole - name: mcp-k8s-readonly + name: view +subjects: + - kind: ServiceAccount + name: mcp-k8s + namespace: ai +--- +# view включает только namespaced-ресурсы, поэтому кластерные объекты и метрики +# добавляем отдельно. Ни secrets, ни записи здесь нет. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: mcp-k8s-cluster-read +rules: + - apiGroups: [""] + resources: ["nodes", "namespaces", "persistentvolumes"] + verbs: ["get", "list", "watch"] + - apiGroups: ["storage.k8s.io"] + resources: ["storageclasses", "volumeattachments", "csidrivers", "csistoragecapacities"] + verbs: ["get", "list", "watch"] + - apiGroups: ["metrics.k8s.io"] + resources: ["*"] + verbs: ["get", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: mcp-k8s-cluster-read +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: mcp-k8s-cluster-read subjects: - kind: ServiceAccount name: mcp-k8s -- 2.54.0