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 theHTTPRouteandMCPServerRegistrationthat route MCP traffic through the gateway; it does not createAuthPolicyorRateLimitPolicyresources (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: truefrom anyServiceMonitor/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;watchonsecretsandconfigmaps- 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/listSecrets 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: 128Mirequests,cpu: 500m/memory: 512Milimits). 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¶
- NSA/CISA Cybersecurity Information Sheet on MCP security - the boundary and hardening recommendations this guide draws on.
- Gateway Integration guide
- TLS configuration · Metrics · Storage Version Migration