diff --git a/charts/kubernetes-mcp-server/README.md b/charts/kubernetes-mcp-server/README.md index 597934bb7..0489095ed 100644 --- a/charts/kubernetes-mcp-server/README.md +++ b/charts/kubernetes-mcp-server/README.md @@ -1,7 +1,7 @@ # Kubernetes MCP Server Helm Chart Kubernetes MCP Server exposes Kubernetes cluster inspection and automation through the Model Context Protocol. -This chart deploys the official `ghcr.io/containers/kubernetes-mcp-server:v0.0.62` image in HTTP mode with in-cluster authentication, +This chart deploys the official `ghcr.io/containers/kubernetes-mcp-server:v0.0.63` image in HTTP mode with in-cluster authentication, read-only safety flags, and least-privilege RBAC by default. ## Install @@ -35,6 +35,10 @@ rbac: The chart blocks full write plus destructive mode unless `mcp.allowUnsafeWriteAccess=true` is set. The chart is stateless by default. If persistence is enabled and the Deployment is scaled above one replica, use `ReadWriteMany` storage or disable persistence to avoid sharing one `ReadWriteOnce` PVC across pods. +Ingress class rendering is optional. Set `ingress.ingressClassName: ""` to omit `spec.ingressClassName`. +When `networkPolicy.enabled=true`, ingress is restricted to the configured peers. +Setting `networkPolicy.enabled=true` enables egress isolation with built-in DNS and HTTPS allowances; `networkPolicy.extraEgress` appends API server or proxy rules. +Set `networkPolicy.dnsEgressPeers` when your cluster DNS pods do not use the default kube-system/kube-dns labels. ## Security Scan: `kubernetes-mcp-server` diff --git a/charts/kubernetes-mcp-server/templates/NOTES.txt b/charts/kubernetes-mcp-server/templates/NOTES.txt index 69d8bc4a2..f531b7741 100644 --- a/charts/kubernetes-mcp-server/templates/NOTES.txt +++ b/charts/kubernetes-mcp-server/templates/NOTES.txt @@ -1,39 +1,95 @@ {{/* SPDX-License-Identifier: Apache-2.0 */}} Kubernetes MCP Server has been deployed. -Access: +1. Access -- Service: http://{{ include "kubernetes-mcp-server.fullname" . }}.{{ .Release.Namespace }}.svc:{{ .Values.service.port }} -- Port-forward: kubectl port-forward svc/{{ include "kubernetes-mcp-server.fullname" . }} 8080:{{ .Values.service.port }} -n {{ .Release.Namespace }} -- Local MCP URL: http://127.0.0.1:8080 +- Service DNS: http://{{ include "kubernetes-mcp-server.fullname" . }}.{{ .Release.Namespace }}.svc:{{ .Values.service.port }} +- Port-forward: + kubectl port-forward svc/{{ include "kubernetes-mcp-server.fullname" . }} 8080:{{ .Values.service.port }} -n {{ .Release.Namespace }} +- Local MCP URL: + http://127.0.0.1:8080 -Safety mode: +2. Runtime Mode - readOnly: {{ .Values.mcp.readOnly }} - disableDestructive: {{ .Values.mcp.disableDestructive }} +- allowUnsafeWriteAccess: {{ .Values.mcp.allowUnsafeWriteAccess }} - stateless: {{ .Values.mcp.stateless }} - clusterProvider: {{ .Values.mcp.clusterProvider }} - disableMultiCluster: {{ .Values.mcp.disableMultiCluster }} +- listOutput: {{ .Values.mcp.listOutput }} +- logLevel: {{ .Values.mcp.logLevel }} - toolsets: {{ join "," .Values.mcp.toolsets }} -RBAC: +3. RBAC {{- if and .Values.serviceAccount.create .Values.rbac.create }} - ServiceAccount: {{ include "kubernetes-mcp-server.serviceAccountName" . }} - ClusterRoleBinding target: {{ .Values.rbac.clusterRoleName }} +- Verify read access: + kubectl auth can-i list pods --as=system:serviceaccount:{{ .Release.Namespace }}:{{ include "kubernetes-mcp-server.serviceAccountName" . }} +- Verify destructive access remains denied with default values: + kubectl auth can-i delete pods --as=system:serviceaccount:{{ .Release.Namespace }}:{{ include "kubernetes-mcp-server.serviceAccountName" . }} {{- else }} -- Chart-managed RBAC is disabled. Confirm the ServiceAccount can access the intended resources. +- Chart-managed RBAC is disabled. +- Confirm the selected ServiceAccount can access only the intended resources. {{- end }} -Troubleshooting: +4. Configuration File -- kubectl get pod -l app.kubernetes.io/instance={{ .Release.Name }} -n {{ .Release.Namespace }} -- kubectl logs deploy/{{ include "kubernetes-mcp-server.fullname" . }} -n {{ .Release.Namespace }} -- kubectl auth can-i list pods --as=system:serviceaccount:{{ .Release.Namespace }}:{{ include "kubernetes-mcp-server.serviceAccountName" . }} +{{- if .Values.mcp.configToml }} +- TOML config is mounted from ConfigMap {{ include "kubernetes-mcp-server.fullname" . }}. +- Inspect it with: + kubectl get configmap {{ include "kubernetes-mcp-server.fullname" . }} -n {{ .Release.Namespace }} -o yaml +{{- else }} +- No TOML config is mounted; runtime options are generated from mcp.* values. +{{- end }} + +5. Exposure + +{{- if .Values.ingress.enabled }} +- Ingress is enabled. +- Keep this endpoint behind trusted authentication and network controls. +{{- else }} +- Ingress is disabled. +{{- end }} +{{- if .Values.gateway.enabled }} +- Gateway API HTTPRoute is enabled. +{{- else }} +- Gateway API HTTPRoute is disabled. +{{- end }} +{{- if .Values.networkPolicy.enabled }} +- NetworkPolicy is enabled for inbound MCP traffic. +{{- else }} +- NetworkPolicy is disabled. +{{- end }} + +6. Persistence And Scaling + +{{- if .Values.persistence.enabled }} +- Persistence is enabled at {{ .Values.persistence.mountPath }}. +- PVC source: {{ default (include "kubernetes-mcp-server.fullname" .) .Values.persistence.existingClaim }} +- For replicaCount > 1, use ReadWriteMany storage or an externally managed RWX claim. +{{- else }} +- Persistence is disabled; the default deployment is stateless. +{{- end }} +- replicaCount: {{ .Values.replicaCount }} + +7. Troubleshooting + +- Pods: + kubectl get pod -l app.kubernetes.io/instance={{ .Release.Name }} -n {{ .Release.Namespace }} +- Logs: + kubectl logs deploy/{{ include "kubernetes-mcp-server.fullname" . }} -n {{ .Release.Namespace }} +- Service endpoints: + kubectl get endpoints {{ include "kubernetes-mcp-server.fullname" . }} -n {{ .Release.Namespace }} +- Rendered arguments: + kubectl get deploy {{ include "kubernetes-mcp-server.fullname" . }} -n {{ .Release.Namespace }} -o jsonpath='{.spec.template.spec.containers[0].args}' -Resources: +8. Resources - Chart documentation: https://helmforge.dev/docs/charts/kubernetes-mcp-server -- Upstream: https://github.com/containers/kubernetes-mcp-server +- Upstream project: https://github.com/containers/kubernetes-mcp-server +- Security guidance: keep write-enabled releases isolated to trusted namespaces and clients. Happy Helmforging :) diff --git a/charts/kubernetes-mcp-server/templates/_helpers.tpl b/charts/kubernetes-mcp-server/templates/_helpers.tpl index 8d69420e8..78c05fa67 100644 --- a/charts/kubernetes-mcp-server/templates/_helpers.tpl +++ b/charts/kubernetes-mcp-server/templates/_helpers.tpl @@ -16,3 +16,19 @@ app.kubernetes.io/part-of: helmforge {{- end -}} {{- define "kubernetes-mcp-server.serviceAccountName" -}}{{- if .Values.serviceAccount.create -}}{{- default (include "kubernetes-mcp-server.fullname" .) .Values.serviceAccount.name -}}{{- else -}}{{- default "default" .Values.serviceAccount.name -}}{{- end -}}{{- end -}} {{- define "kubernetes-mcp-server.image" -}}{{- printf "%s:%s" .Values.image.repository .Values.image.tag -}}{{- end -}} + +{{- define "kubernetes-mcp-server.validate" -}} +{{- if and (not .Values.mcp.readOnly) (not .Values.mcp.disableDestructive) (not .Values.mcp.allowUnsafeWriteAccess) -}} +{{- fail "write access with destructive tools requires mcp.allowUnsafeWriteAccess=true" -}} +{{- end -}} +{{- if and (gt (int .Values.replicaCount) 1) .Values.persistence.enabled (not .Values.persistence.existingClaim) (not (has "ReadWriteMany" .Values.persistence.accessModes)) -}} +{{- fail "replicaCount > 1 with persistence.enabled requires persistence.accessModes to include ReadWriteMany or persistence.enabled=false" -}} +{{- end -}} +{{- $podLabels := .Values.podLabels | default dict -}} +{{- if hasKey $podLabels "app.kubernetes.io/name" -}} +{{- fail "podLabels must not override the selector label app.kubernetes.io/name" -}} +{{- end -}} +{{- if hasKey $podLabels "app.kubernetes.io/instance" -}} +{{- fail "podLabels must not override the selector label app.kubernetes.io/instance" -}} +{{- end -}} +{{- end -}} diff --git a/charts/kubernetes-mcp-server/templates/deployment.yaml b/charts/kubernetes-mcp-server/templates/deployment.yaml index 28516005b..95b0e3363 100644 --- a/charts/kubernetes-mcp-server/templates/deployment.yaml +++ b/charts/kubernetes-mcp-server/templates/deployment.yaml @@ -13,7 +13,7 @@ spec: template: metadata: labels: - {{- include "kubernetes-mcp-server.labels" . | nindent 8 }} + {{- include "kubernetes-mcp-server.selectorLabels" . | nindent 8 }} {{- with .Values.podLabels }} {{- toYaml . | nindent 8 }} {{- end }} diff --git a/charts/kubernetes-mcp-server/templates/ingress.yaml b/charts/kubernetes-mcp-server/templates/ingress.yaml index 40952d058..b3fd2d853 100644 --- a/charts/kubernetes-mcp-server/templates/ingress.yaml +++ b/charts/kubernetes-mcp-server/templates/ingress.yaml @@ -11,7 +11,9 @@ metadata: {{- toYaml . | nindent 4 }} {{- end }} spec: - ingressClassName: {{ .Values.ingress.ingressClassName | quote }} + {{- with .Values.ingress.ingressClassName }} + ingressClassName: {{ . | quote }} + {{- end }} {{- with .Values.ingress.tls }} tls: {{- toYaml . | nindent 4 }} diff --git a/charts/kubernetes-mcp-server/templates/networkpolicy.yaml b/charts/kubernetes-mcp-server/templates/networkpolicy.yaml index cdb8061b4..d1988ba9d 100644 --- a/charts/kubernetes-mcp-server/templates/networkpolicy.yaml +++ b/charts/kubernetes-mcp-server/templates/networkpolicy.yaml @@ -1,5 +1,7 @@ {{/* SPDX-License-Identifier: Apache-2.0 */}} {{- if .Values.networkPolicy.enabled }} +{{- $extraEgress := .Values.networkPolicy.extraEgress | default list }} +{{- $dnsEgressPeers := .Values.networkPolicy.dnsEgressPeers | default list }} apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: @@ -12,6 +14,7 @@ spec: {{- include "kubernetes-mcp-server.selectorLabels" . | nindent 6 }} policyTypes: - Ingress + - Egress ingress: - from: {{- if .Values.networkPolicy.ingressFrom }} @@ -22,4 +25,23 @@ spec: ports: - protocol: TCP port: http + egress: + - to: + {{- toYaml $dnsEgressPeers | nindent 8 }} + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + - to: + - ipBlock: + cidr: 0.0.0.0/0 + - ipBlock: + cidr: ::/0 + ports: + - protocol: TCP + port: 443 + {{- with $extraEgress }} + {{- toYaml . | nindent 4 }} + {{- end }} {{- end }} diff --git a/charts/kubernetes-mcp-server/templates/validate.yaml b/charts/kubernetes-mcp-server/templates/validate.yaml index 4577664aa..cd06c1fca 100644 --- a/charts/kubernetes-mcp-server/templates/validate.yaml +++ b/charts/kubernetes-mcp-server/templates/validate.yaml @@ -1,7 +1,2 @@ {{/* SPDX-License-Identifier: Apache-2.0 */}} -{{- if and (not .Values.mcp.readOnly) (not .Values.mcp.disableDestructive) (not .Values.mcp.allowUnsafeWriteAccess) -}} -{{- fail "write access with destructive tools requires mcp.allowUnsafeWriteAccess=true" -}} -{{- end -}} -{{- if and (gt (int .Values.replicaCount) 1) .Values.persistence.enabled (not (has "ReadWriteMany" .Values.persistence.accessModes)) -}} -{{- fail "replicaCount > 1 with persistence.enabled requires persistence.accessModes to include ReadWriteMany or persistence.enabled=false" -}} -{{- end -}} +{{- include "kubernetes-mcp-server.validate" . -}} diff --git a/charts/kubernetes-mcp-server/tests/networkpolicy_test.yaml b/charts/kubernetes-mcp-server/tests/networkpolicy_test.yaml index 730b0b0c8..f74e27098 100644 --- a/charts/kubernetes-mcp-server/tests/networkpolicy_test.yaml +++ b/charts/kubernetes-mcp-server/tests/networkpolicy_test.yaml @@ -14,3 +14,43 @@ tests: content: protocol: TCP port: http + - contains: + path: spec.policyTypes + content: Egress + - equal: + path: spec.egress[0].ports[0].port + value: 53 + - equal: + path: spec.egress[1].ports[0].port + value: 443 + - it: appends extra egress rules after default DNS and HTTPS + set: + networkPolicy: + enabled: true + extraEgress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + ports: + - protocol: TCP + port: 6443 + asserts: + - contains: + path: spec.policyTypes + content: Egress + - equal: + path: spec.egress[0].ports[0].port + value: 53 + - equal: + path: spec.egress[0].to[0].namespaceSelector.matchLabels["kubernetes.io/metadata.name"] + value: kube-system + - equal: + path: spec.egress[0].to[0].podSelector.matchLabels["k8s-app"] + value: kube-dns + - equal: + path: spec.egress[1].ports[0].port + value: 443 + - equal: + path: spec.egress[2].ports[0].port + value: 6443 diff --git a/charts/kubernetes-mcp-server/tests/templates_test.yaml b/charts/kubernetes-mcp-server/tests/templates_test.yaml index a757bcb19..e183c75f8 100644 --- a/charts/kubernetes-mcp-server/tests/templates_test.yaml +++ b/charts/kubernetes-mcp-server/tests/templates_test.yaml @@ -14,6 +14,11 @@ tests: - equal: path: spec.template.spec.containers[0].image value: ghcr.io/containers/kubernetes-mcp-server:v0.0.63 + - equal: + path: spec.template.metadata.labels["app.kubernetes.io/name"] + value: kubernetes-mcp-server + - notExists: + path: spec.template.metadata.labels["helm.sh/chart"] - it: renders service template: templates/service.yaml asserts: @@ -41,6 +46,17 @@ tests: - equal: path: metadata.annotations["cert-manager.io/cluster-issuer"] value: letsencrypt + - it: omits empty ingress class name + template: templates/ingress.yaml + set: + ingress.enabled: true + ingress.ingressClassName: "" + ingress.hosts[0].host: kubernetes-mcp-server.example.com + ingress.hosts[0].paths[0].path: / + ingress.hosts[0].paths[0].pathType: Prefix + asserts: + - notExists: + path: spec.ingressClassName - it: renders HTTPRoute hostnames template: templates/httproute.yaml set: diff --git a/charts/kubernetes-mcp-server/tests/validation_test.yaml b/charts/kubernetes-mcp-server/tests/validation_test.yaml index 97bfa7b2e..8f998e022 100644 --- a/charts/kubernetes-mcp-server/tests/validation_test.yaml +++ b/charts/kubernetes-mcp-server/tests/validation_test.yaml @@ -18,3 +18,18 @@ tests: asserts: - failedTemplate: errorMessage: "replicaCount > 1 with persistence.enabled requires persistence.accessModes to include ReadWriteMany or persistence.enabled=false" + - it: allows scaling with an existing persistence claim + set: + replicaCount: 2 + persistence.enabled: true + persistence.existingClaim: kubernetes-mcp-data-rwx + persistence.accessModes[0]: ReadWriteOnce + asserts: + - notFailedTemplate: {} + - it: fails when podLabels override selector labels + set: + podLabels: + app.kubernetes.io/instance: custom + asserts: + - failedTemplate: + errorMessage: "podLabels must not override the selector label app.kubernetes.io/instance" diff --git a/charts/kubernetes-mcp-server/values.schema.json b/charts/kubernetes-mcp-server/values.schema.json index 37df18ec0..31bb70521 100644 --- a/charts/kubernetes-mcp-server/values.schema.json +++ b/charts/kubernetes-mcp-server/values.schema.json @@ -12,7 +12,7 @@ "type": "object", "properties": { "repository": { "type": "string", "default": "ghcr.io/containers/kubernetes-mcp-server" }, - "tag": { "type": "string", "default": "v0.0.62" }, + "tag": { "type": "string", "default": "v0.0.63" }, "pullPolicy": { "type": "string", "enum": ["Always", "IfNotPresent", "Never"], "default": "IfNotPresent" } } }, @@ -68,7 +68,17 @@ "type": "object", "properties": { "enabled": { "type": "boolean", "default": false }, - "ingressFrom": { "type": "array", "items": { "type": "object" } } + "ingressFrom": { "type": "array", "items": { "type": "object" } }, + "dnsEgressPeers": { + "type": "array", + "description": "DNS peers allowed by the built-in egress allowance", + "items": { "type": "object" } + }, + "extraEgress": { + "type": "array", + "description": "Additional egress rules appended after the built-in DNS and HTTPS allowances", + "items": { "type": "object" } + } } }, "probes": { "$ref": "#/definitions/probes" }, diff --git a/charts/kubernetes-mcp-server/values.yaml b/charts/kubernetes-mcp-server/values.yaml index 6ea32f4a8..45095dcac 100644 --- a/charts/kubernetes-mcp-server/values.yaml +++ b/charts/kubernetes-mcp-server/values.yaml @@ -70,6 +70,16 @@ pdb: networkPolicy: enabled: false ingressFrom: [] + # -- DNS peers allowed when egress isolation is enabled. + dnsEgressPeers: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + podSelector: + matchLabels: + k8s-app: kube-dns + # -- Additional egress rules appended after the built-in DNS and HTTPS allowances. + extraEgress: [] probes: startup: { enabled: true, initialDelaySeconds: 5, periodSeconds: 10, timeoutSeconds: 3, failureThreshold: 60 } liveness: { enabled: true, initialDelaySeconds: 0, periodSeconds: 20, timeoutSeconds: 5, failureThreshold: 3 }