Skip to main content

Integrating Defakto with Envoy

The Defakto agent implements the Envoy Secret Discovery Service (SDS) API on the same Unix socket as the SPIFFE Workload API. This lets Envoy sidecars obtain X.509 SVIDs and trust bundles directly from the agent without any additional secrets management.

How it works

The Defakto agent serves two SDS secret names over the agent socket:

Secret nameContent
defaultThe workload's X.509 SVID (certificate + private key)
ROOTCAThe trust bundle (CA certificates) for the trust domain

Envoy references these names in tls_certificate_sds_secret_configs and validation_context_sds_secret_config respectively. The agent automatically rotates SVIDs before they expire; Envoy receives the update over the same long-lived gRPC stream with no restart required.

Compatibility

Envoy versionStatus
1.18 and laterCompatible
Earlier than 1.18Not supported

Compatibility specifically depends on the following Envoy features, all of which are stable from 1.18:

  • envoy.service.secret.v3.SecretDiscoveryService — the Defakto agent only serves xDS v3 SDS; Envoy versions without stable v3 support cannot connect
  • typed_extension_protocol_options — required to configure the SDS cluster as a gRPC (HTTP/2) upstream (added in 1.14, stable in 1.18)
  • combined_validation_context — required to validate peer SVIDs against the trust bundle fetched via SDS (added in 1.15, stable in 1.18)

Prerequisites

  • A Kubernetes cluster with the Defakto agent installed (via the spirl-system Helm chart)
  • The SPIFFE CSI Driver enabled (installed by default with the spirl-system chart)
  • Envoy 1.18 or later

Complete example

The envoy/mtls-demo directory in the defakto-examples repository contains a Terraform configuration that deploys a full server and client setup in an existing cluster. It assumes the Defakto agent is already running.

Deploying a workload with an Envoy sidecar

Below is an example setup where Envoy is deployed as a sidecar alongside some application container called my-app. Envoy handles all mTLS on behalf of the application. The app itself connects over plain HTTP internally and has no knowledge of certificates.

Here is an example Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
name: my-service
namespace: my-namespace
spec:
selector:
matchLabels:
app: my-service
template:
metadata:
labels:
app: my-service
k8s.spirl.com/spiffe-csi: enabled
spec:
serviceAccountName: my-service
containers:
- name: app
image: my-app:latest
- name: envoy
image: envoyproxy/envoy:v1.32-latest
args: ["envoy", "-c", "/etc/envoy/envoy.yaml"]
volumeMounts:
- name: envoy-config
mountPath: /etc/envoy
readOnly: true
volumes:
- name: envoy-config
configMap:
name: my-service-envoy-config

The k8s.spirl.com/spiffe-csi: enabled label on the pod triggers the Defakto Admission Controller to inject the agent socket into every container in the pod at /spirl-agent-socket/agent.sock. Envoy uses this socket to reach the SDS API and obtain its SVID.

Configuring Envoy

Every pod using Envoy for mTLS needs the same two things in its config: a node block (required for gRPC-based SDS) and a static cluster pointing to the agent socket. What differs between a server and a client is whether they expose a downstream mTLS listener, initiate upstream mTLS connections, or both.

Save the appropriate config below as envoy.yaml and load it as a ConfigMap:

kubectl create configmap my-service-envoy-config \
--from-file=envoy.yaml \
-n my-namespace

Server config

Accepts incoming mTLS connections on port 8443, verifies the caller's SVID, and forwards plain HTTP to the application on port 8080:

node:
id: my-service
cluster: my-service

admin:
address:
socket_address: { address: 127.0.0.1, port_value: 9901 }

static_resources:
clusters:
- name: spiffe_agent_sds
type: STATIC
typed_extension_protocol_options:
envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
"@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
explicit_http_config:
http2_protocol_options: {}
load_assignment:
cluster_name: spiffe_agent_sds
endpoints:
- lb_endpoints:
- endpoint:
address:
pipe:
path: /spirl-agent-socket/agent.sock

- name: local_app
type: STATIC
load_assignment:
cluster_name: local_app
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: { address: 127.0.0.1, port_value: 8080 }

listeners:
- name: ingress_mtls
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
require_client_certificate: true
common_tls_context:
tls_certificate_sds_secret_configs:
- name: default
sds_config:
resource_api_version: V3
api_config_source:
api_type: GRPC
transport_api_version: V3
grpc_services:
- envoy_grpc:
cluster_name: spiffe_agent_sds
combined_validation_context:
default_validation_context:
match_subject_alt_names:
- exact: "PEER_SPIFFE_ID"
validation_context_sds_secret_config:
name: ROOTCA
sds_config:
resource_api_version: V3
api_config_source:
api_type: GRPC
transport_api_version: V3
grpc_services:
- envoy_grpc:
cluster_name: spiffe_agent_sds
filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
route_config:
virtual_hosts:
- name: local
domains: ["*"]
routes:
- match: { prefix: "/" }
route: { cluster: local_app }
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
note

Replace PEER_SPIFFE_ID with the actual SPIFFE ID of the client. The SPIFFE ID format depends on the path template configured for your cluster. To find the actual SPIFFE ID a workload will receive, check the cert loaded by Envoy after it starts:

kubectl port-forward -n my-namespace deploy/my-service 9901:9901
curl -s localhost:9901/certs | python3 -m json.tool | grep uri

Client config

Accepts plain HTTP from the application on port 9090 and proxies it to the server over mTLS, verifying the server's SVID.

The upstream cluster references the server by its Kubernetes Service DNS name. A Service exposing the server's mTLS port (8443) must exist — see the complete example for the full manifest. Replace <cluster-domain> with your cluster's DNS domain (typically cluster.local):

node:
id: my-client
cluster: my-client

admin:
address:
socket_address: { address: 127.0.0.1, port_value: 9901 }

static_resources:
clusters:
- name: spiffe_agent_sds
type: STATIC
typed_extension_protocol_options:
envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
"@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
explicit_http_config:
http2_protocol_options: {}
load_assignment:
cluster_name: spiffe_agent_sds
endpoints:
- lb_endpoints:
- endpoint:
address:
pipe:
path: /spirl-agent-socket/agent.sock

- name: my_service_mtls
type: STRICT_DNS
load_assignment:
cluster_name: my_service_mtls
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: { address: my-service.my-namespace.svc.<cluster-domain>, port_value: 8443 }
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
common_tls_context:
tls_certificate_sds_secret_configs:
- name: default
sds_config:
resource_api_version: V3
api_config_source:
api_type: GRPC
transport_api_version: V3
grpc_services:
- envoy_grpc:
cluster_name: spiffe_agent_sds
combined_validation_context:
default_validation_context:
match_subject_alt_names:
- exact: "PEER_SPIFFE_ID"
validation_context_sds_secret_config:
name: ROOTCA
sds_config:
resource_api_version: V3
api_config_source:
api_type: GRPC
transport_api_version: V3
grpc_services:
- envoy_grpc:
cluster_name: spiffe_agent_sds

listeners:
- name: egress_proxy
address:
socket_address: { address: 127.0.0.1, port_value: 9090 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: egress_http
route_config:
virtual_hosts:
- name: my_service
domains: ["*"]
routes:
- match: { prefix: "/" }
route: { cluster: my_service_mtls }
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
note

Replace PEER_SPIFFE_ID with the actual SPIFFE ID of the server. The SPIFFE ID format depends on the path template configured for your cluster. To find the actual SPIFFE ID a workload will receive, check the cert loaded by Envoy after it starts:

kubectl port-forward -n my-namespace deploy/my-service 9901:9901
curl -s localhost:9901/certs | python3 -m json.tool | grep uri

Verifying the connection

Envoy exposes SDS statistics via its admin API (port 9901 by default). Port-forward to a running pod and check the stats:

kubectl port-forward -n my-namespace deploy/my-service 9901:9901
curl -s localhost:9901/stats | grep -E 'sds\.(default|ROOTCA)|spiffe_agent_sds'

A healthy connection shows:

  • sds.default.update_success incrementing (SVID delivered)
  • sds.ROOTCA.update_success incrementing (trust bundle delivered)
  • listener.0.0.0.0_8443.server_ssl_socket_factory.ssl_context_update_by_sds non-zero (TLS context loaded from SDS)

If upstream_cx_connect_fail is non-zero, verify the socket path inside the Envoy container:

kubectl exec -n my-namespace deploy/my-service -c envoy -- ls /spirl-agent-socket/

The socket file agent.sock must be present.