Featured

Multi-Cluster Networking: mTLS with Gateway API

Management cluster queries metrics from test/prod clusters. Secured with mTLS via Gateway API. The implementation: separate Gateways, client certificates, ReferenceGrants.

By Jurg van Vliet

Published Nov 17, 2025

The Cross-Cluster Requirement

We run multiple Kubernetes clusters: local (Kind), test (Scaleway), and production (Scaleway). Each cluster has Prometheus scraping local metrics.

For centralised monitoring, our management cluster runs Grafana and Mimir. Grafana needs to query Prometheus in each cluster. This is cross-cluster networking, and it needs to be secure.

Requirements:

  • Grafana queries Prometheus in test/production clusters
  • Connections must be encrypted (TLS)
  • Connections must be mutually authenticated (client certs)
  • No public exposure of Prometheus endpoints
  • Manageable certificate lifecycle

Gateway API with cert-manager solves this cleanly.

The Gateway API Pattern

We use separate Gateways for public and internal traffic:

Public Gateway: Serves application traffic, uses Let's Encrypt certificates, no client authentication required.

Internal Gateway: Serves cluster-to-cluster traffic, requires mutual TLS (both server and client certs), not publicly accessible.

This separation means internal endpoints aren't accidentally exposed. Different Gateway, different TLS configuration, different security posture.

Implementation with cert-manager

Step 1: Create cluster CA

Each cluster has a cert-manager ClusterIssuer for internal certificates:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: internal-ca
spec:
  ca:
    secretName: internal-ca-secret

The CA certificate is generated once and stored in internal-ca-secret. This CA issues certificates for both servers (Prometheus endpoints) and clients (Grafana).

Step 2: Server certificate for Prometheus

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: prometheus-server-cert
  namespace: monitoring
spec:
  secretName: prometheus-tls
  issuerRef:
    name: internal-ca
    kind: ClusterIssuer
  dnsNames:
  - prometheus.monitoring.svc.cluster.local
  - prometheus.example.com

cert-manager creates prometheus-tls secret with certificate and key.

Step 3: Client certificate for Grafana

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: grafana-client-cert
  namespace: monitoring
spec:
  secretName: grafana-client-tls
  issuerRef:
    name: internal-ca
    kind: ClusterIssuer
  commonName: grafana-client
  usages:
  - client auth

Step 4: Gateway configuration for mTLS

This is where Gateway API shows its value:

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: internal-gateway
  namespace: monitoring
spec:
  gatewayClassName: envoy-gateway
  listeners:
  - name: prometheus-mtls
    protocol: HTTPS
    port: 9090
    hostname: "prometheus.example.com"
    tls:
      mode: Terminate
      certificateRefs:
      - name: prometheus-tls
        kind: Secret

For client validation, we use Envoy Gateway's BackendTLSPolicy (Gateway API extension):

apiVersion: gateway.envoyproxy.io/v1alpha1
kind: ClientTrafficPolicy
metadata:
  name: mtls-validation
  namespace: monitoring
spec:
  targetRef:
    group: gateway.networking.k8s.io
    kind: Gateway
    name: internal-gateway
  tls:
    clientValidation:
      caCertificateRefs:
      - name: internal-ca-secret
        group: ""
        kind: Secret

This configuration:

  • Terminates TLS with the server certificate
  • Requires client certificate for authentication
  • Validates client cert against the cluster CA

Step 5: HTTPRoute to Prometheus

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: prometheus-route
  namespace: monitoring
spec:
  parentRefs:
  - name: internal-gateway
    namespace: monitoring
  hostnames:
  - "prometheus.example.com"
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /
    backendRefs:
    - name: prometheus
      port: 9090

ReferenceGrants for Cross-Namespace Access

Gateway API enforces security: a Gateway in namespace A can't reference a Secret in namespace B without explicit permission.

Problem: Gateway is in monitoring namespace. CA certificate might be in cert-manager namespace.

Solution: ReferenceGrant

apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
  name: allow-gateway-to-cert-manager
  namespace: cert-manager
spec:
  from:
  - group: gateway.networking.k8s.io
    kind: Gateway
    namespace: monitoring
  to:
  - group: ""
    kind: Secret

This grants the Gateway in monitoring namespace permission to reference Secrets in cert-manager namespace. Explicit, auditable, secure.

What We Learned

Separation is cleaner than complex policies: We initially tried to use one Gateway with different TLS policies per route. This was complicated and error-prone. Separate Gateways—one public, one internal—is simpler.

ReferenceGrants add friction (intentionally): Having to explicitly grant cross-namespace access feels like extra configuration. But it prevents accidental exposure. The friction is security.

Certificate rotation is automatic: cert-manager handles renewal. Certificates rotate before expiry without manual intervention. This is better than static certificates that require runbooks.

Debugging TLS is still hard: When mTLS doesn't work, error messages are often cryptic. We added detailed logging and tested thoroughly in test environment before production.

Gateway API abstracts the proxy: We run Envoy Gateway, but the HTTPRoute definitions don't depend on Envoy specifics (except TLS validation configuration). If we need to switch to Istio or Cilium later, most configuration remains unchanged.

Sources:

#gatewayapi #mtls #networking #kubernetes #security