Skip to content

Production readiness

The MCP Lifecycle Operator ships with defaults tuned for a fast getting-started experience: permissive network access, an allow-all egress posture for managed pods, and simple examples that skip external dependencies such as cert-manager. Those defaults are deliberately not what you want in production.

This page is aimed at platform and cluster operators. It is a hub that gathers the hardening and day-2 topics the operator supports, grouped by category. Each category links out to the detailed page where one already exists, and covers the configuration inline where it does not.

Note

These settings are independent - adopt them incrementally. Nothing here changes the operator's default behaviour; you opt in per concern.

Readiness checklist

Area Default Production recommendation Details
Managed-pod network isolation Ingress open to any pod; egress allow-all Restrict spec.network.ingressFrom / egressTo Network isolation
Gateway exposure ClusterIP Service per server Shared gateway via httproute or kuadrant Gateway integration
TLS posture Go defaults Pin TLS_MIN_VERSION / groups to policy TLS & transport security
Metrics endpoint Served over HTTPS on 8443 Scrape over TLS; avoid insecureSkipVerify Metrics & monitoring security
Operator hardening Non-root, read-only rootfs, all caps dropped Keep the shipped securityContext; scope RBAC Operator hardening
Availability & resources 1 replica, modest requests/limits Size for load; plan storage migrations Availability & operations

Network isolation

The operator creates a NetworkPolicy for every managed MCP server pod. Out of the box that policy allows any pod in the cluster to reach the server port and lets the server pod egress anywhere. For production, and to align with the NSA CSI MCP Security guidance on designing for boundaries, restrict both directions through spec.network.

apiVersion: mcp.x-k8s.io/v1beta1
kind: MCPServer
metadata:
  name: my-mcp-server
  namespace: default
spec:
  # ...
  network:
    # Only pods in the server's namespace carrying this label may reach it.
    # Add a namespaceSelector to admit clients from other namespaces.
    ingressFrom:
      - podSelector:
          matchLabels:
            role: mcp-client
    # The server pod may only egress to this CIDR on 443/TCP.
    egressTo:
      - ipBlock:
          cidr: 10.0.0.0/8
    egressPorts:
      - port: 443
        protocol: TCP
Field Effect when set Effect when empty (default)
network.ingressFrom Only the listed NetworkPolicyPeers can reach the server port Any pod in the cluster can connect
network.egressTo Server pod may only reach the listed destinations Egress to all destinations allowed
network.egressPorts Restricts egress to the listed ports All ports allowed to whatever egressTo permits
network.dnsEgressPeer Scopes the automatic DNS rule to a specific destination DNS (port 53) allowed to any destination

NetworkPolicies are additive

These rows describe only the policy the operator manages. Kubernetes unions every NetworkPolicy that selects a pod, so another policy in the namespace can still admit broader ingress or egress than what you configure here. Audit the other policies that select the server pod when you rely on these restrictions.

Default egress is allow-all (unrestricted outbound)

When an MCPServer sets neither egressTo nor egressPorts, the managed policy declares Egress in policyTypes with a single empty rule (egress: [{}]), which permits unrestricted outbound traffic to any destination on any port. This is the default because MCP servers commonly need to reach arbitrary external endpoints (the APIs they front), and standard Kubernetes NetworkPolicy cannot select DNS names or reliably represent dynamically addressed destinations - a narrower default rule would break legitimate traffic. Because NetworkPolicies are additive, another policy cannot narrow an allow-all rule; only a separate network control can. To restrict a server's outbound traffic, set spec.network.egressTo / egressPorts (which also documents the intended destinations), or govern egress with a dedicated network control outside this operator.

DNS is always permitted when egress is restricted

As soon as egressTo or egressPorts is set, the operator prepends an egress rule allowing UDP and TCP port 53 so pods can still resolve names. Use dnsEgressPeer to narrow that rule to your cluster's DNS service. dnsEgressPeer on its own does not activate egress restrictions.

Restricted default posture needs an explicit ingressFrom

The operator can be started with --network-policy-default-posture=restricted. Under that posture an MCPServer that omits spec.network.ingressFrom gets a deny-all ingress policy instead of the open default. This deny only takes effect where a NetworkPolicy-enforcing network plugin (CNI) is installed - without one the policy has no effect and untrusted pods can still reach the server. It is also additive with other NetworkPolicies: Kubernetes unions the ingress rules of every policy that selects the pod, so a deny-all here does not override an allow rule from another applicable policy. Where the policy is enforced, the operator completes the MCP handshake by connecting to the server pod, so a deny-all ingress also blocks the operator itself (and any gateway): the server never reaches Verified and no status.address is published. Under restricted posture always set ingressFrom with peers that match the operator pods and every gateway source that must reach the server - the operator never adds an implicit peer for itself. This flag only changes the default for unconfigured dimensions; an explicit spec.network value is always honored regardless of posture.

Observing the effective posture

The open default is not silent. When the operator starts with the default --network-policy-default-posture=open, it logs a WARNING: advisory noting that servers without ingressFrom are reachable from any namespace and pointing at the restricted alternative. Independently of the flag, every MCPServer reports its applied posture in the NetworkPolicyRestricted status condition: status: "True" (reason Restricted) once both ingress sources and egress are constrained, and status: "False" (reason Unrestricted, IngressUnrestricted, or EgressUnrestricted) while either direction is still open. Watch that condition (kubectl get mcpserver -o jsonpath='{.status.conditions}') to find servers still running with open ingress. The condition is informational and never gates readiness.

Peers use the standard Kubernetes NetworkPolicyPeer shape (podSelector, namespaceSelector, ipBlock); ipBlock cannot be combined with the selector fields. These constraints are checked during reconciliation, not by an admission webhook: an MCPServer with an invalid CIDR is still admitted by the API server and then reports a ValidationError status condition, so validate config before applying rather than relying on kubectl apply to reject it.

Gateway integration

Each MCP server is backed by a ClusterIP Service, reachable only from inside the cluster. To let clients outside the cluster reach servers through a shared ingress point - instead of wiring up external exposure per server - route them through a gateway. The operator ships two providers:

  • httproute - the Kubernetes Gateway API.
  • kuadrant - the Kuadrant MCP Gateway. The provider creates the HTTPRoute and MCPServerRegistration that route MCP traffic through the gateway; it does not create AuthPolicy or RateLimitPolicy resources (configure those separately - see the tip below).

Set spec.gateway.provider and the operator manages an MCPGatewayBinding for you. Both providers require spec.gateway.configRef pointing at a ConfigMap with the gateway details; reconciliation fails without it. See the full Gateway Integration guide for provider configuration and examples.

Tip

For internet-facing deployments prefer the kuadrant provider, but expose an MCP server only after the applicable AuthPolicy and RateLimitPolicy resources are configured and verified at the gateway. Remember to supply its required configRef.

TLS & transport security

The controller manager serves its webhook and metrics endpoints over TLS, and opens an outbound TLS connection to each managed server to verify the MCP handshake. Both surfaces read the same profile from TLS_MIN_VERSION, TLS_CIPHER_SUITES, and TLS_GROUPS.

In production, pin these to your organization's crypto policy (or let a platform TLS profile such as an OpenShift TLSSecurityProfile supply them) instead of relying on Go defaults. The outbound client path enforces a TLS 1.2 floor that a permissive profile cannot lower.

See the TLS configuration page for the full variable reference, TLS 1.3 named groups, and post-quantum (X25519MLKEM768) availability.

Metrics & monitoring security

The operator exposes Prometheus metrics over HTTPS on port 8443. In production:

  • Scrape the endpoint over TLS and provision the scraper with the CA so you can drop insecureSkipVerify: true from any ServiceMonitor/PodMonitor. cert-manager is the usual way to issue the serving certificate.
  • Restrict who can reach 8443 (RBAC on the metrics reader, and/or a NetworkPolicy on the operator namespace).

See the Metrics page for the full list of exposed metrics and their labels.

Operator hardening

The shipped controller Deployment already runs the manager container with a locked-down container-level securityContext (under spec.template.spec.containers[]) - keep it:

securityContext:
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true
  runAsNonRoot: true
  capabilities:
    drop:
      - ALL
  seccompProfile:
    type: RuntimeDefault

Additional hardening for production:

  • RBAC - the operator installs with the ClusterRole it needs to manage MCP servers cluster-wide. If you run it scoped to specific namespaces, tighten the role bindings accordingly and review the granted verbs against your least-privilege baseline. The ClusterRole also grants cluster-wide get;list;watch on secrets and configmaps - see Cluster-wide Secret and ConfigMap access.
  • Namespace isolation - apply a NetworkPolicy to the operator's own namespace so only your monitoring stack can reach the metrics port.
  • Image provenance - pin the operator image by digest and verify signatures where your supply-chain policy requires it.

Cluster-wide Secret and ConfigMap access

The operator's ClusterRole grants cluster-wide get;list;watch on secrets and configmaps. This cannot be narrowed: MCPServers may live in any namespace, so the role cannot be namespace-scoped, and the operator watches Secret and ConfigMap metadata for drift, which requires list;watch (Kubernetes RBAC cannot scope list/watch by label or field). Treat the operator namespace as privileged.

  • Blast radius - a compromised operator token can get/list Secrets and ConfigMaps cluster-wide.
  • Caching - Secret and ConfigMap contents are never held in the operator cache: both are read uncached via the API reader, and only their metadata is watched to trigger reconciles on drift. Pods are the only object type given a dedicated content cache, and it is label-scoped to managed Pods and stripped to the fields diagnostics need (see internal/controller/cache.go).
  • Compensating controls - restrict and network-isolate the operator namespace, keep the shipped restricted securityContext (above), and audit access to the operator ServiceAccount.

Availability & operations

  • Replicas & resources - the operator ships with a single replica and modest requests/limits (cpu: 10m / memory: 128Mi requests, cpu: 500m / memory: 512Mi limits). Size these for the number of managed servers in your cluster and load-test before committing to limits; see Performance testing.
  • Storage-version migrations - when upgrading across API storage versions, drive the migration deliberately rather than relying on lazy conversion. See the Storage Version Migration page.

Performance testing

Sizing and scaling recommendations should be backed by load and stress testing (operator under many managed servers, CPU/memory under load). This is tracked upstream and is not yet part of this guide.

Further reading