The pattern
Without a service mesh, every service has to:
- Load its TLS cert + key on startup.
- Verify the peer’s cert against a CA bundle.
- Handle cert rotation gracefully.
- Run a mTLS-aware HTTP client for outbound calls.
That’s per-service code. It drifts. It’s the most-skipped part of zero-trust deployments.
With Envoy + SPIRE, all of the above moves into the sidecar. The application speaks plaintext HTTP to its sidecar; the sidecar handles TLS + mTLS + cert rotation transparently.
The pieces
┌─────────────────────────┐ ┌─────────────────────────┐
│ Service A pod │ │ Service B pod │
│ ┌─────────┐ ┌─────────┐ │ │ ┌─────────┐ ┌─────────┐ │
│ │ app │→│ envoy │ │ ←──┼─│ envoy │→│ app │ │
│ │ (Go) │←│ sidecar │ │ ───┼─│ sidecar │←│ (Go) │ │
│ └─────────┘ └────┬────┘ │ │ └────┬────┘ └─────────┘ │
└──────────────────┼──────┘ └──────┼──────────────────┘
│ │
┌────┴──────────────────┴────┐
│ SPIRE Workload API socket │
│ (mounted from node DaemonSet)
└────────────────────────────┘
Each Envoy fetches its own SVID via the SPIRE Workload API. SVIDs rotate every ~hour. Envoy reloads them transparently without dropping connections.
The Envoy config
The interesting bits in the Envoy bootstrap:
clusters:
- name: spire_agent
type: STATIC
load_assignment:
cluster_name: spire_agent
endpoints:
- lb_endpoints:
- endpoint:
address:
pipe:
path: /run/spire/sockets/agent.sock
http2_protocol_options: {}
# Per-listener mTLS using a SPIFFE-aware transport socket
listeners:
- address: { socket_address: { address: 0.0.0.0, port_value: 8443 } }
filter_chains:
- transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
common_tls_context:
tls_certificate_sds_secret_configs:
- name: "spiffe://genie.example/ns/prod/sa/kyc-orchestrator"
sds_config:
api_config_source:
api_type: GRPC
grpc_services:
- envoy_grpc: { cluster_name: spire_agent }
SDS (Secret Discovery Service) is the gRPC API Envoy uses to ask SPIRE for the current cert. Envoy refreshes automatically.
What the application doesn’t have to do
- Load TLS material.
- Watch for file changes (no more inotify on cert files).
- Handle TLS handshake errors at the Go level.
- Track per-peer trust relationships.
The Go application speaks plaintext to localhost on a sidecar-controlled port. The sidecar does the rest.
What it still has to do
- Authorisation. SPIFFE handles “is this really service A?” (authentication). “Is service A allowed to do X?” is still your application’s RBAC.
The combination is the right shape: SPIFFE for identity, your application policy stack for authorisation, audit log for after-the-fact.
For Genie’s deployment plan, this is the target end-state for the agent-to-MCP-server traffic — mTLS via Envoy + SPIRE rather than per-service TLS code. The roadmap item; the patterns translate cleanly once the SPIRE control plane is up.