Mutual TLS (mTLS) & Zero-Trust Networking
The gateway implements mutual TLS for subgraph communication, providing cryptographic authentication for every service-to-service call. This follows the Zero-Trust principle: never trust, always verify.
Architecture
┌──────────────┐ mTLS ┌──────────────┐
│ │◄──────────────────────►│ │
│ Gateway │ SPIFFE SVID Identity │ Subgraph │
│ (Router) │ X.509 Certificates │ (Users) │
│ │ ECDSA P-256 │ │
├──────────────┤ └──────────────┘
│ Ephemeral CA │ mTLS ┌──────────────┐
│ SVID Rotation│◄──────────────────────►│ │
│ Trust Bundle │ Both sides verify │ Subgraph │
│ │ each other's cert │ (Products) │
└──────────────┘ └──────────────┘Both the router and subgraphs present certificates and verify each other's identity. There are no plaintext connections.
Security Properties
| Property | Description |
|---|---|
| Mutual Authentication | Both client and server present and verify X.509 certificates |
| SPIFFE Identity | Workload identities follow the SPIFFE standard: spiffe://{domain}/ns/{namespace}/sa/{service} |
| Short-Lived Certificates | Default TTL of 1 hour; automatically rotated at 50% lifetime |
| Ephemeral CA | In-process Certificate Authority using ECDSA P-256 — no external PKI needed for development |
| ECDSA P-256 | Modern elliptic curve cryptography for key generation |
| TLS 1.2+ | Minimum TLS version enforced on all connections |
Quick Start
1. Enable mTLS in router.yaml
# Global mTLS configuration
mtls:
enabled: true
trust_domain: "cluster.local"
service_name: "router"
namespace: "default"
cert_ttl_secs: 3600 # 1 hour
trust_bundle_path: "/etc/certs/trust-bundle.pem" # Optional
verify_hostname: true
allow_fallback: false # Strict mode — no plain HTTP
# Per-subgraph mTLS overrides
subgraphs:
- name: "users"
url: "https://users-service:4001/graphql"
mtls:
enabled: true
trust_domain: "cluster.local"
service_name: "users-service"2. Ephemeral CA (Development / Testing)
For local development, the gateway automatically creates an ephemeral CA at startup:
use grpc_graphql_gateway::mtls::{MtlsConfig, MtlsProvider};
let config = MtlsConfig {
enabled: true,
trust_domain: "dev.local".to_string(),
service_name: "gateway".to_string(),
namespace: "default".to_string(),
cert_ttl_secs: 3600,
..Default::default()
};
let provider = MtlsProvider::new(config)?;
// Provider automatically generates CA + initial SVID3. External CA (Production)
For production, provide your own CA certificate and key:
mtls:
enabled: true
trust_domain: "production.example.com"
ca_cert_path: "/etc/certs/ca.pem"
ca_key_path: "/etc/certs/ca-key.pem"
trust_bundle_path: "/etc/certs/trust-bundle.pem"use grpc_graphql_gateway::mtls::CertificateAuthority;
let ca = CertificateAuthority::from_files(
"/etc/certs/ca.pem",
"/etc/certs/ca-key.pem",
"production.example.com",
)?;SPIFFE Identity Model
Every workload gets a SPIFFE Verifiable Identity Document (SVID) with a URI like:
spiffe://cluster.local/ns/default/sa/router
spiffe://cluster.local/ns/default/sa/users-service
spiffe://cluster.local/ns/payments/sa/billing-serviceComponents
| Component | Example | Description |
|---|---|---|
| Trust Domain | cluster.local |
Organizational boundary |
| Namespace | default |
Kubernetes namespace or logical group |
| Service Account | router |
Unique service identifier |
Certificate Lifecycle
┌──────────┐ Issue ┌──────────┐
│ │───────────────►│ │
│ CA │ │ SVID │ TTL: 1 hour
│ │◄───────────────│ │ Auto-rotate at 30 min
│ │ Rotate │ │
└──────────┘ └──────────┘
│
▼
┌──────────────────┐
│ reqwest::Client │
│ with mTLS ID │
└──────────────────┘Automatic Rotation
The MtlsProvider starts a background task that:
- Checks every 30 seconds if the current SVID needs renewal
- Renews at 50% lifetime (i.e., 30 minutes for a 1-hour TTL)
- Emergency rotation if the certificate has already expired
- Atomic swap — in-flight requests continue with the old cert; new requests get the fresh one
use std::sync::Arc;
let provider = Arc::new(MtlsProvider::new(config)?);
let rotation_handle = provider.start_rotation_task();
// Provider will automatically rotate SVIDs in the backgroundHealth Check Integration
The mTLS status is available for health probes:
let status = provider.status().await;
println!("Trust Domain: {}", status.trust_domain);
println!("SPIFFE ID: {}", status.spiffe_id);
println!("Cert Valid: {}", status.cert_valid);
println!("Remaining: {}s", status.remaining_secs);
println!("Needs Renewal:{}", status.needs_renewal);The MtlsStatus struct:
pub struct MtlsStatus {
pub enabled: bool,
pub trust_domain: String,
pub spiffe_id: String,
pub cert_serial: String,
pub cert_valid: bool,
pub remaining_secs: u64,
pub needs_renewal: bool,
}Building mTLS Clients
The provider builds reqwest::Client instances pre-configured with:
- The current SVID as the TLS client identity
- The CA trust bundle for verifying subgraph certificates
- TLS 1.2 minimum version
- Optional hostname verification
// Build a client with the current SVID
let client = provider.build_client().await?;
// Use it for subgraph requests
let response = client
.post("https://users-service:4001/graphql")
.json(&graphql_request)
.send()
.await?;Issuing Subgraph SVIDs
For testing or development, the gateway can issue SVIDs for subgraphs:
use grpc_graphql_gateway::mtls::{issue_subgraph_svid, export_svid};
use std::time::Duration;
let ca = provider.ca();
// Issue an SVID for the users subgraph
let svid = issue_subgraph_svid(
ca,
"users-service",
"default",
Duration::from_secs(3600),
)?;
// Export to files for the subgraph to use
export_svid(&svid, "/etc/certs/users-cert.pem", "/etc/certs/users-key.pem")?;
// Export trust bundle for subgraphs to verify the gateway
ca.export_trust_bundle("/etc/certs/trust-bundle.pem")?;Configuration Reference
MtlsConfig
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool |
false |
Enable/disable mTLS |
trust_domain |
String |
"cluster.local" |
SPIFFE trust domain |
service_name |
String |
"router" |
Service identity name |
namespace |
String |
"default" |
Kubernetes namespace |
cert_ttl_secs |
u64 |
3600 |
Certificate TTL in seconds |
ca_cert_path |
Option<String> |
None |
External CA cert (PEM). Ephemeral CA if not set |
ca_key_path |
Option<String> |
None |
External CA key (PEM). Ephemeral CA if not set |
trust_bundle_path |
Option<String> |
None |
Path to export trust bundle |
allow_fallback |
bool |
false |
Allow plain HTTP if mTLS fails |
verify_hostname |
bool |
true |
Verify subgraph TLS hostname |
Production Recommendations
1. Use an External CA
In production, provide your own CA rather than relying on the ephemeral CA:
mtls:
ca_cert_path: "/etc/certs/ca.pem"
ca_key_path: "/etc/certs/ca-key.pem"2. Short TTLs
Keep certificate TTLs short (1 hour or less). The automatic rotation handles renewals seamlessly:
mtls:
cert_ttl_secs: 3600 # 1 hour3. Never Allow Fallback in Production
mtls:
allow_fallback: false # Strict mode4. Export Trust Bundles for Subgraphs
Subgraphs need the CA trust bundle to verify the gateway's certificate:
mtls:
trust_bundle_path: "/shared/certs/trust-bundle.pem"5. Monitor Certificate Health
Integrate MtlsStatus into your health check endpoint to detect certificate issues before they cause outages.
6. Kubernetes Integration
In Kubernetes, mount cert paths from Secrets or use a sidecar like SPIRE for production-grade SPIFFE identity management:
# Kubernetes deployment snippet
volumes:
- name: certs
secret:
secretName: gateway-mtls-certs
containers:
- name: gateway
volumeMounts:
- name: certs
mountPath: /etc/certs
readOnly: truePrerequisites
- OpenSSL must be installed on the system (used for certificate generation via CLI)
- For production, pre-generated CA certificates are recommended over the ephemeral CA