Integrating Red Hat OpenShift Service Mesh with Zero Trust Workload Identity Manager in a single-cluster {#zero-trust-manager-mesh-integration_{context}}
Deploy and configure SPIFFE Runtime Environment as the certificate authority (CA) for Red Hat OpenShift Service Mesh workloads, replacing the Istio built-in CA with SPIFFE-compliant identities and automatically rotated short-lived certificates.
SPIRE integration with Red Hat OpenShift Service Mesh
Red Hat OpenShift Service Mesh integrates with Zero Trust Workload Identity Manager so Envoy sidecars obtain mTLS certificates from Secure Production Identity Framework for Everyone (SPIFFE) instead of Istio’s built-in CA, enabling cryptographically verified workload identities.
SPIRE provides cryptographic workload identities based on the Secure Production Identity Framework for Everyone (SPIFFE) standard. This integration enables a zero-trust security model where workload identities are cryptographically verified rather than relying on network-based authentication.
Component overview
The following table summarizes the main components in a single-cluster SPIFFE and Red Hat OpenShift Service Mesh integration and what each one does.
| Component | Purpose |
|---|---|
| Zero Trust Workload Identity Manager | Manages SPIRE deployment on OpenShift Container Platform |
| SPIRE Server | Certificate Authority; issues SVIDs |
| SPIRE Agent | Runs on each node; provides SDS API to workloads |
| SPIFFE CSI Driver | Mounts SPIRE socket into pods |
| ClusterSPIFFEID | Registers which pods get which identities |
| Red Hat OpenShift Service Mesh Operator | Manages Istio deployment |
| Istiod | Istio control plane |
| Envoy Sidecar | Proxy in each pod; uses SPIRE for certificates |
SPIRE integration architecture components
Learn about the key components in the SPIRE integration architecture and how they work together to enable zero-trust workload identity and automated certificate management for secure mTLS connections in Red Hat OpenShift Service Mesh.
- Zero Trust Workload Identity Manager
- Manages the SPIRE deployment lifecycle on OpenShift Container Platform, including custom resources for SPIRE Server, SPIRE Agent, and related components.
- SPIRE Server
- Acts as the certificate authority that issues SPIFFE Verifiable Identity Documents (SVIDs) to authenticated workloads.
- SPIRE Agent
- Runs as a DaemonSet on each cluster node, providing the Envoy Secret Discovery Service (SDS) API to workloads on that node.
- SPIFFE CSI Driver
- Mounts the SPIRE Agent UNIX domain socket into pods, enabling secure communication between Envoy sidecars and the SPIRE Agent.
- Red Hat OpenShift Service Mesh
- Manages the Istio deployment through the
servicemeshoperator3Operator. - Istiod
- The Istio control plane that configures Envoy proxies but delegates certificate issuance to SPIRE.
- Envoy sidecar
- The proxy injected into each workload pod that uses SPIRE-issued certificates for mTLS connections.
Deploying SPIRE operands for Red Hat OpenShift Service Mesh integration
Deploy SPIRE operands by creating the ZeroTrustWorkloadIdentityManager custom resource (CR) and related SPIRE operand CRs together. A running SPIRE deployment is required before you configure Red Hat OpenShift Service Mesh to use SPIRE-issued certificates for workload mTLS.
Prerequisites
- You have installed Zero Trust Workload Identity Manager.
- The OpenShift CLI (
oc) is configured with access to the cluster. - You have permissions to create custom resources in the
zero-trust-workload-identity-managernamespace.
Procedure
- Set the environment variables by running the following commands:
$ export TRUST_DOMAIN=ocp.one$ export ZTWIM_NS=zero-trust-workload-identity-manager$ export JWT_ISSUER="https://oidc-discovery.$(oc get ingresses.config/cluster -o jsonpath={.spec.domain})"
- Deploy all SPIRE operand CRs, including the
ZeroTrustWorkloadIdentityManagerCR:-
Create the
ZeroTrustWorkloadIdentityManagerCR:$ oc apply -f - <<EOFapiVersion: operator.openshift.io/v1alpha1kind: ZeroTrustWorkloadIdentityManagermetadata:name: clusterlabels:app.kubernetes.io/name: zero-trust-workload-identity-managerapp.kubernetes.io/managed-by: zero-trust-workload-identity-managerspec:trustDomain: ${TRUST_DOMAIN}clusterName: ""bundleConfigMap: "spire-bundle"EOF -
Create the
SpireServerCR:$ cat <<EOF | oc apply -f -apiVersion: operator.openshift.io/v1alpha1kind: SpireServermetadata:name: clusterspec:logLevel: "info"logFormat: "text"jwtIssuer: $JWT_ISSUERcaValidity: "24h"defaultX509Validity: "1h"defaultJWTValidity: "5m"caKeytype: “rsa-2048”jwtKeyType: "rsa-2048"keyManager: “”caSubject:country: "US"organization: "RH"commonName: "SPIRE Server CA"persistence:size: "5Gi"accessMode: "ReadWriteOnce"datastore:databaseType: "sqlite3"connectionString: "/run/spire/data/datastore.sqlite3"tlsSecretName: ""maxOpenConns: 100maxIdleConns: 10connMaxLifetime: 0disableMigration: "false"EOF -
Wait for the SPIRE Server to become ready by running the following commands:
$ until oc get statefulset/spire-server -n "${ZTWIM_NS}" &> /dev/null; do sleep 3; done$ kubectl rollout status statefulset/spire-server -n "${ZTWIM_NS}" --timeout=300s -
Create the
SpireAgentCR:$ cat <<EOF | oc apply -f -apiVersion: operator.openshift.io/v1alpha1kind: SpireAgentmetadata:name: clusterspec:socketPath: "/run/spire/agent-sockets"logLevel: "info"logFormat: "text"nodeAttestor:k8sPSATEnabled: "true"workloadAttestors:k8sEnabled: "true"workloadAttestorsVerification:type: "auto"hostCertBasePath: "/etc/kubernetes"hostCertFileName: "kubelet-ca.crt"disableContainerSelectors: "false"useNewContainerLocator: "true"EOF -
Wait for the SPIRE Agent to become ready by running the following commands:
$ until oc get daemonset/spire-agent -n "${ZTWIM_NS}" &> /dev/null; do sleep 3; done$ kubectl rollout status daemonset/spire-agent -n "${ZTWIM_NS}" --timeout=300s -
Deploy the
SpiffeCSIDriverCR:$ cat <<EOF | oc apply -f -apiVersion: operator.openshift.io/v1alpha1kind: SpiffeCSIDrivermetadata:name: clusterspec:agentSocketPath: '/run/spire/agent-sockets'pluginName: "csi.spiffe.io"EOF -
Wait for the SPIFFE CSI Driver to become ready by running the following commands:
$ until oc get daemonset/spire-spiffe-csi-driver -n "${ZTWIM_NS}" &> /dev/null; do sleep 3; done$ kubectl rollout status daemonset/spire-spiffe-csi-driver -n "${ZTWIM_NS}" --timeout=300s -
Deploy the
SpireOIDCDiscoveryProviderCR:$ export OIDC_DISCOVERY_CONFIG_MAP=spire-spiffe-oidc-discovery-provider$ cat <<EOF | oc apply -f -apiVersion: operator.openshift.io/v1alpha1kind: SpireOIDCDiscoveryProvidermetadata:name: clusterspec:logLevel: "info"logFormat: "text"csiDriverName: "csi.spiffe.io"jwtIssuer: $JWT_ISSUERreplicaCount: 1managedRoute: "true"EOF -
Wait for the OIDC Discovery Provider to be created by running the following commands:
$ until oc get deployment spire-spiffe-oidc-discovery-provider -n "${ZTWIM_NS}" &> /dev/null; do sleep 3; done$ oc wait --for=condition=Available deployment/spire-spiffe-oidc-discovery-provider -n "${ZTWIM_NS}" --timeout=300s
-
Verification
-
Verify that Zero Trust Workload Identity Manager is installed:
-
Deploy the client workload and try to fetch a workload SVID:
$ cat <<EOF | oc apply -f -apiVersion: apps/v1kind: Deploymentmetadata:name: ztwim-clientnamespace: defaultlabels:app: ztwim-clientspec:selector:matchLabels:app: ztwim-clienttemplate:metadata:labels:app: ztwim-clientspec:containers:- name: clientimage: ghcr.io/spiffe/spire-agent:1.5.1command: ["/opt/spire/bin/spire-agent"]args: [ "api", "watch", "-socketPath", "/run/spire/sockets/spire-agent.sock" ]volumeMounts:- mountPath: /run/spire/socketsname: spiffe-workload-apireadOnly: truevolumes:- name: spiffe-workload-apicsi:driver: csi.spiffe.ioreadOnly: trueEOF -
Wait for the client deployment to become ready by running the following command:
$ until oc get deployment ztwim-client -n default &> /dev/null; do sleep 3; done$ oc wait --for=condition=Available deployment/ztwim-client -n default --timeout=300s$ sleep 5
-
-
Verify that the x509 SVID is available by running the following command:
$ oc exec -it \"$(oc get \pods -o=jsonpath='{.items[0].metadata.name}' \-l app=ztwim-client \-n default \)" -n default -- \/opt/spire/bin/spire-agent \api fetch -socketPath /run/spire/sockets/spire-agent.sockThe expected output is an SVID like the following example:
Received 1 svid after 29.636075msSPIFFE ID: spiffe://ocp.one/ns/default/sa/defaultSVID Valid After: 2025-10-21 14:04:03 +0000 UTCSVID Valid Until: 2025-10-21 15:04:13 +0000 UTCCA #1 Valid After: 2025-10-21 07:38:03 +0000 UTCCA #1 Valid Until: 2025-10-22 07:38:13 +0000 UTC -
Verify that the JSON Web Token (JWT) SVID is available by running the following command:
$ oc exec -it \"$(oc get \pods -o=jsonpath='{.items[0].metadata.name}' \-l app=ztwim-client \-n default \)" -n default -- \/opt/spire/bin/spire-agent \api fetch jwt -audience=sample-aud -socketPath /run/spire/sockets/spire-agent.sockThe expected output is a JWT SVID like the following example:
token(spiffe://ocp.one/ns/default/sa/default):eyJhbGciOiJSUzI1NiIsImtpZCI6Ij....IsImbundle(spiffe://ocp.one):{"keys": [{"kty": "RSA","kid": "6k9PfhrAdfajT6jvLvR6bdomFvQxMeGf","n": "wEYTV0ri4OOcdgEVgzN0...KhUEGf0NKxnuaeGQ","e": "AQAB"}]} -
Remove the client workload by running the following command:
$ oc delete deployment ztwim-client -n default
Deploying Red Hat OpenShift Service Mesh for SPIRE integration
Deploy Red Hat OpenShift Service Mesh by creating the IstioCNI and Istio CRs with SPIRE integration settings so Envoy sidecars obtain SPIRE-issued certificates for workload mTLS after the SPIRE stack is running.
Prerequisites
- You have installed Zero Trust Workload Identity Manager.
- The OpenShift CLI (
oc) is configured with access to the cluster. - You have permissions to create namespaces and custom resources in the
istio-cniandistio-systemnamespaces. - You have permissions to read secrets in the
zero-trust-workload-identity-managernamespace.
Procedure
-
Set the Istio environment variables by running the following commands:
$ export ZTWIM_NS=zero-trust-workload-identity-manager$ export TRUST_DOMAIN=ocp.one$ export JWT_ISSUER="https://oidc-discovery.$(oc get ingresses.config/cluster -o jsonpath={.spec.domain})"$ export OSSM_NS=istio-system$ export OSSM_CNI=istio-cni$ export VERIFY_NS=verify-ossm-ztwim$ export EXTRA_ROOT_CA="$(oc get secret oidc-serving-cert \-n ${ZTWIM_NS} -o json | \jq -r '.data."tls.crt"' | \base64 -d | \sed 's/^/ /')" -
Create the
IstioCNICR to deploy Istio CNI by running the following commands:$ oc new-project "${OSSM_CNI}" 2>/dev/null || oc project "${OSSM_CNI}"$ oc apply -f - <<EOFapiVersion: sailoperator.io/v1kind: IstioCNImetadata:name: defaultspec:version: <version>namespace: ${OSSM_CNI}EOFwhere:
spec.version- Replace
<version>with the Istio version supported by your Red Hat OpenShift Service Mesh Operator. You can find supported versions by runningoc get IstioCNI -o jsonpath='{.items[*].spec.version}'after the Operator is installed.
-
Wait for Istio CNI to become ready by running the following commands:
$ until oc get daemonset/istio-cni-node -n "${OSSM_CNI}" &> /dev/null; do sleep 3; done$ kubectl rollout status daemonset/istio-cni-node -n "${OSSM_CNI}" --timeout=300sThe
untilloop waits for the Red Hat OpenShift Service Mesh Operator to create theistio-cni-nodeDaemonSet. Theoc rollout statuscommand waits for the DaemonSet pods to become ready. -
Install the Istio CR with SPIRE integration by running the following commands:
$ oc new-project "${OSSM_NS}" 2>/dev/null$ cat <<EOF | oc apply -f -apiVersion: sailoperator.io/v1kind: Istiometadata:name: defaultspec:namespace: istio-systemupdateStrategy:type: InPlacevalues:pilot:jwksResolverExtraRootCA: |${EXTRA_ROOT_CA}env:PILOT_JWT_ENABLE_REMOTE_JWKS: "true"meshConfig:trustDomain: $TRUST_DOMAINdefaultConfig:proxyMetadata:WORKLOAD_IDENTITY_SOCKET_FILE: "spire-agent.sock"sidecarInjectorWebhook:templates:spire: |spec:initContainers:- name: istio-proxyvolumeMounts:- name: workload-socketmountPath: /run/secrets/workload-spiffe-udsreadOnly: truevolumes:- name: workload-socketcsi:driver: "csi.spiffe.io"readOnly: truespireGateway: |spec:containers:- name: istio-proxyvolumeMounts:- name: workload-socketmountPath: /run/secrets/workload-spiffe-udsreadOnly: truevolumes:- name: workload-socketcsi:driver: "csi.spiffe.io"readOnly: trueEOF -
Wait for all of the resources to become ready by running the following commands:
$ until oc get deployment istiod -n "${OSSM_NS}" &> /dev/null; do sleep 3; done$ oc wait --for=condition=Available deployment/istiod -n "${OSSM_NS}" --timeout=300s
Verification
- Verify that Istio is integrated with SPIRE:
-
Create a test workload with the
spireinjection template by running the following commands:$ oc new-project "${VERIFY_NS}" 2>/dev/null -
Enable the sidecar injection by running the following command:
$ oc label namespace "${VERIFY_NS}" istio-injection=enabled -
Create the
httpbinworkload:$ cat <<EOF | oc apply -f -apiVersion: apps/v1kind: Deploymentmetadata:name: httpbinnamespace: ${VERIFY_NS}spec:replicas: 1selector:matchLabels:app: httpbinversion: v1template:metadata:annotations:inject.istio.io/templates: "sidecar,spire"spiffe.io/audience: "test-audience"labels:app: httpbinversion: v1spec:containers:- image: docker.io/mccutchen/go-httpbin:v2.15.0imagePullPolicy: IfNotPresentname: httpbinports:- containerPort: 8080EOF -
Wait for all of the resources to become ready by running the following commands:
$ until oc get deployment httpbin -n "${VERIFY_NS}" &> /dev/null; do sleep 3; done$ oc wait --for=condition=Available deployment/httpbin -n "${VERIFY_NS}" --timeout=300s -
Verify the SPIRE workload identity by running the following command:
$ HTTPBIN_POD=$(oc get pod -l app=httpbin -n "${VERIFY_NS}" -o jsonpath="{.items[0].metadata.name}")$ istioctl proxy-config secret "$HTTPBIN_POD" \-n "${VERIFY_NS}" -o json \| jq -r '.dynamicActiveSecrets[0].secret.tlsCertificate.certificateChain.inlineBytes' \| base64 --decode > chain.pemopenssl x509 -in chain.pem -text | grep SPIREExample outputIssuer: C=US, O=RH, CN=<APP_DOMAIN>/serialNumber=...Subject: C=US, O=SPIREIf you see
SPIREin bothIssuerandSubject, the integration is working. Envoy is getting its certificates from SPIRE, not from Istio’s built-in CA. -
Remove the namespace by running the following command:
$ oc delete namespace "${VERIFY_NS}"
-