Containers on Red Hat OpenShift can get automatic cryptographic identities through zero trust workload identity manager, but workloads running inside a virtual machine (VM) cannot. In this tutorial, I demonstrate how I bridged that gap using a virtual socket (VSOCK) and a dedicated in-VM SPIRE agent to give every workload — whether it's running as a container or as an application inside a VM — a short-lived, automatically rotating SPIFFE identity.
Why an application in a VM doesn't get a SPIRE identity
If you're running Red Hat OpenShift Virtualization alongside containerized workloads, you've likely encountered this kind of friction. Your containers enjoy automatic cryptographic identities through the zero trust workload identity manager. An application running inside a VM is stuck with static credentials, manual rotation, and an inconsistent security posture. Zero trust workload identity manager has no way to reach inside the guest OS to issue it an identity.
Zero trust workload identity manager works seamlessly for containers because the SPIRE agent runs as a DaemonSet on each node and the container storage interface (CSI) driver plug-in (spiffe-csi-driver) mounts its Unix socket to every pod. The SPIRE agent performs node attestation, then communicates with the SPIRE server to fetch cryptographic identities for workloads on its node. It issues these identities once workload attestation succeeds. But a VM is a complete computer running inside a computer with its own kernel, its own filesystem, its own process table.
Even though the VM runs inside a virt-launcher pod, the VM guest OS cannot see the mounted socket. The mount exists in the pod's filesystem. As a result, the virt-launcher pod itself can receive a cryptographic identity, but the workloads running inside the VM cannot.
You can't copy a Unix domain socket into the VM, either. A socket is kernel state (buffers, queues) connected to a running process, not a portable file. It only exists in the kernel that created it.
So how do we give VM workloads the same zero-trust identity model that containers already enjoy?
Solution: VSOCK + a dedicated in-VM SPIRE agent
The basic idea is straightforward: Run a dedicated SPIRE agent inside each VM and connect it to the SPIRE server on the cluster using VSOCK, the secure communication channel built into the Linux kernel specifically for VM-to-host communication.
Why VSOCK instead of TCP?
- Network isolation: VSOCK is invisible to IP routing, port scanning, or any network-based attack.
- VM isolation: Each VM gets its own context ID (CID), so VMs can't snoop on each other.
- No exposure: If we used TCP over the pod network, then the connection would be visible to anything on the cluster's virtual network, which provides opportunity for interception or spoofing.
Two socat
instances form a transparent tunnel. The SPIRE agent inside the VM sends TCP traffic to localhost:8081
, and socat
relays it over VSOCK to the host node. The second socat
instance forwards it to the SPIRE server. Both the agent and server speak plain TCP, completely unaware that VSOCK sits in the middle.
Why per-VM agents?
A single SPIRE agent on the host can't distinguish between applications running inside a VM. From the host's perspective, the entire VM is just one QEMU process. To give each app (Redis, Postgres, and so on) its own identity, we need an agent inside the VM that can inspect the VM's /proc
filesystem and see the individual processes.
This follows the same model that zero trust workload identity manager uses for Kubernetes nodes: Each node gets an agent, and the agent serves workloads on that node. We treat each VM like a node.
Deployment schema
Each application inside the VM gets its own unique cryptographic identity (an SVID) that rotates automatically. In this demonstration, time to live (TTL) is kept very short (120s for Redis, 180s for PostgreSQL) so rotation can be observed in real time.
Prerequisites
Before starting, you need:
- Red Hat OpenShift cluster with the zero trust workload identity manager operator installed
- Red Hat OpenShift Virtualization operator installed with a running VM (I used RHEL 9)
- The
oc
andvirtctl
commands
Set these environment variables for your session:
export KUBECONFIG="/path/to/your/kubeconfig"
export APP_DOMAIN="apps.$(oc get dns cluster -o jsonpath='{ .spec.baseDomain }')"
export VM_NAME="your-vm-name"
export VM_NAMESPACE="openshift-cnv"
export SPIRE_NAMESPACE="zero-trust-workload-identity-manager"
export SPIRE_SERVER_POD="spire-server-0"
Step 1: Enable VSOCK on the cluster and VM
VSOCK must be enabled at two levels: The cluster feature gate and the individual VM spec. To enable the VSOCK feature gate at the cluster level:
oc annotate hyperconverged kubevirt-hyperconverged \
-n openshift-cnv \ 'kubevirt.kubevirt.io/jsonpatch=[{"op":"add","path":"/spec/configuration/developerConfiguration/featureGates/-","value":"VSOCK"}]' \--overwrite
Enable VSOCK on your specific VM and restart it:
oc patch vm ${VM_NAME} -n ${VM_NAMESPACE} \
--type=merge -p \ '{"spec":{"template":{"spec":{"domain":{"devices":{"autoattachVSOCK":true}}}}}}'
oc virt stop ${VM_NAME} -n ${VM_NAMESPACE}
oc virt start ${VM_NAME} -n ${VM_NAMESPACE}
After the restart, verify that VSOCK is available inside the VM by checking for /dev/vsock
:
ls -l /dev/vsock
As output, you see the character device. If it's missing, confirm the feature gate annotation was applied correctly and that the VM was restarted.
Step 2: Deploy the host-side VSOCK bridge
This pod runs on the same node as your VM and forwards VSOCK connections to the SPIRE server over TCP:
NODE=$(oc get vmi ${VM_NAME} -n ${VM_NAMESPACE} -o jsonpath='{.status.nodeName}')
SPIRE_POD_IP=$(oc get pod ${SPIRE_SERVER_POD} -n ${SPIRE_NAMESPACE} -o jsonpath='{.status.podIP}')
cat < bundle.pem
Copy the bundle to the VM at /opt/spire/bundle.pem
, then create the agent configuration at /opt/spire/conf/agent/agent.conf
:
agent {
data_dir = "/var/lib/spire/agent"
log_level = "DEBUG"
server_address = "127.0.0.1"
server_port = "8081"
socket_path = "/run/spire/sockets/agent.sock"
trust_domain = "apps.your-cluster.example.com" #Must match the trust domain configured during zero trust workload identity manager installation
trust_bundle_path = "/opt/spire/bundle.pem"
}
plugins {
NodeAttestor "join_token" {
plugin_data {}
}
KeyManager "disk" {
plugin_data {
directory = "/var/lib/spire/agent"
}
}
WorkloadAttestor "unix" {
plugin_data {}
}
}
Two configuration details are critical here. First, server_address
points to localhost because socat
handles the VSOCK relay transparently. Second, the Unix WorkloadAttestor lets the agent identify processes by UID, which is how we give each application its own identity.
Step 4: Attest the agent and register workloads
Generate a join token on your workstation:
oc exec -n ${SPIRE_NAMESPACE} ${SPIRE_SERVER_POD} -- \
./spire-server token generate \
-spiffeID "spiffe://${APP_DOMAIN}/vm/${VM_NAME}" \
-ttl 600000
Start the agent inside the VM with that token:
sudo mkdir -p /run/spire/sockets /var/lib/spire/agent
sudo /usr/local/bin/spire-agent run \
-config /opt/spire/conf/agent/agent.conf \
-joinToken > /tmp/spire-agent.log 2>&1 &
Watch the logs with tail -f /tmp/spire-agent.log
. The agent is connected and ready when you see Node attestation was successful
and Starting Workload and SDS APIs
.
Now register your workloads. On your workstation:
AGENT_ID="spiffe://${APP_DOMAIN}/spire/agent/join_token/"
oc exec -n ${SPIRE_NAMESPACE} ${SPIRE_SERVER_POD} -- \
./spire-server entry create \
-parentID "$AGENT_ID" \
-spiffeID "spiffe://${APP_DOMAIN}/vm/${VM_NAME}/redis" \
-selector unix:uid:994 \
-x509SVIDTTL 120
oc exec -n ${SPIRE_NAMESPACE} ${SPIRE_SERVER_POD} -- \
./spire-server entry create \
-parentID "$AGENT_ID" \
-spiffeID "spiffe://${APP_DOMAIN}/vm/${VM_NAME}/postgres" \
-selector unix:uid:26 \
-x509SVIDTTL 180
Each registration entry tells the SPIRE Server: "any process running as this UID, attested by this VM's agent, gets this SPIFFE ID." The agent verifies the caller's UID via SO_PEERCRED on the Unix socket, which cannot be spoofed by userspace.
Step 5: Verify identity issuance and rotation
Back in the VM, fetch SVIDs as each application user:
sudo -u redis /usr/local/bin/spire-agent api fetch x509 \
-socketPath /run/spire/sockets/agent.sock
Output:
Received 1 svid after 6.123148ms
SPIFFE ID: spiffe://apps.gcp26feb.gcp.devcluster.openshift.com/vm/rhel9-magenta-gull-92/redis
SVID Valid After: 2026-02-27 08:55:07 +0000 UTC
SVID Valid Until: 2026-02-27 09:55:17 +0000 UTC
Each application gets its own unique X.509 certificate with the SPIFFE ID embedded in the subject alternative name (SAN) field. You can confirm this with the `openssl` command:
openssl x509 -in /tmp/redis-svid/svid.0.pem -noout -text | grep -A1 "Subject Alternative Name"
The output:
X509v3 Subject Alternative Name:
URI:spiffe://apps.gcp26feb.gcp.devcluster.openshift.com/vm/rhel9-magenta-gull-92/redis
With the short TTLs I configured (120s for Redis, 180s for PostgreSQL), you can watch automatic rotation happen in real time. The agent renews SVIDs at roughly 50% of their TTL without any application intervention. Wait 70 seconds, re-fetch, and compare to see a completely new certificate with different serial numbers and validity dates.
What this proves
This proof-of-concetp validates four key things:
- VSOCK provides an isolated channel between VMs and the SPIRE server that never touches the cluster network.
- A per-VM SPIRE agent distinguishes individual applications inside the VM using Unix UID-based attestation.
- Multiple workloads get unique, short-lived identities that rotate automatically without application changes.
- The same trust domain spans containers and VMs, enabling unified zero-trust policies across your entire platform.
Moving toward production
For a production deployment, I'd replace several PoC-specific choices:
VM attestation
- Proof of concept:
join_token
(one-time use) - Production: x509pop attestation or custom KubeVirt attestor plug-in (supports re-attestation)
Registration
- Proof of concept: Manual command-line entries
- Production: Explore the possibility of automating with spire-controller-manager
Host bridge
- Proof of concept: Manual pod deployment
- Production: Managed by zero trust workload identity manager operator, auto-deployed per node
VM bridge
- Proof of concept: Manual
socat
- Production: systemd service with
cloud-init
Installation of per-VM SPIRE Agent
- Proof of concept: Manual
- Production: Explore the possibility of automating it with Red Hat Ansible Automation Platform
SPIRE Server Service Discovery
- Proof of concept: Connects directly to SPIRE server's pod IP
- Production: Connect using Kubernetes Service for SPIRE server
Get started
If you're running mixed container and VM workloads on OpenShift and want to extend zero-trust workload identity to your VMs:
- Install the zero trust workload identity manager operator from OperatorHub.
- Follow the steps in this tutorial to bridge SPIRE into your VMs with VSOCK.
- Explore the SPIFFE/SPIRE documentation for deeper understanding of workload identity standards.
- Check out the OpenShift Virtualization documentation for more on running VMs alongside containers.
Zero-trust shouldn't stop at the VM boundary. With VSOCK and a dedicated in-VM SPIRE Agent, it doesn't have to.
Facts Only
Red Hat OpenShift Virtualization enables containers and virtual machines to run on the same cluster.
Containers receive cryptographic identities via the zero trust workload identity manager and a SPIRE agent DaemonSet.
The spiffe-csi-driver mounts a Unix socket to pods to facilitate identity issuance.
Virtual machines run with their own kernels and filesystems, preventing them from accessing the virt-launcher pod's Unix sockets.
VSOCK is a Linux kernel communication channel for VM-to-host interaction.
The proposed solution involves running a SPIRE agent inside the VM and using socat to tunnel TCP traffic over VSOCK to the SPIRE server.
The VM agent uses a Unix WorkloadAttestor to identify processes based on User IDs (UID).
SVIDs (SPIFFE Verifiable Identity Documents) are issued as X.509 certificates with short time-to-live (TTL) values.
The proof-of-concept used RHEL 9 VMs, Redis (UID 994), and PostgreSQL (UID 26).
Implementation requires enabling the VSOCK feature gate in the hyperconverged operator and the VM specification.
Executive Summary
Workloads running in virtual machines on Red Hat OpenShift lack the automatic, rotating cryptographic identities provided to containers by the zero trust workload identity manager. Because VMs operate with isolated kernels and filesystems, they cannot access the Unix sockets used by the SPIRE agent to attest container workloads. This creates a security disparity where VM-based applications often rely on static credentials and manual rotation.
To bridge this gap, a specialized architecture employs VSOCK—a secure, non-IP communication channel—to connect a dedicated SPIRE agent residing inside the guest VM to the cluster's SPIRE server. By treating each VM as a distinct node and utilizing UID-based attestation, individual processes within the VM can obtain unique, short-lived SVIDs. While the current demonstration relies on manual configuration and join tokens, a production-ready version would require automated deployment via tools like Ansible and more robust attestation methods to ensure scalable, secure identity management across mixed container and VM environments.
Full Take
This is a technical architectural guide functioning as an educational bridge for infrastructure engineers. It operates in CONSTRUCTIVE MODE, moving from a recognized friction point (the "VM identity gap") to a functional proof-of-concept.
The core strength of this approach is the recognition that identity must be tied to the process, not the wrapper. By treating the VM as a node and deploying an internal agent, it maintains the SPIFFE philosophy of workload attestation rather than settling for a coarse "VM-level" identity. The choice of VSOCK is a critical security detail; by bypassing the IP stack, it reduces the attack surface and prevents lateral movement or spoofing via the cluster network.
However, we should question the operational overhead introduced here. Moving from a seamless DaemonSet model (for containers) to a per-VM agent model increases the management surface. The "production" section admits to several manual gaps—specifically regarding agent installation and registration—suggesting that while the cryptographic path is solved, the lifecycle management path is still theoretical.
To extend this thinking:
1. If the VM agent is compromised, does the trust domain provide enough isolation to prevent the issuance of fraudulent identities for other VMs?
2. How does this model scale when managing thousands of VMs with varying OS distributions and UID schemes?
3. Could a specialized KubeVirt attestor eliminate the need for the internal agent entirely?
The narrative is clean and technically grounded. It does not employ manipulation patterns; rather, it identifies a specific limitation of a product suite and proposes a workaround.
Counterstrike Scan: A bad actor would push this by fabricating a critical vulnerability in VM networking to force a rushed adoption of a proprietary "secure" agent. This content does not match that pattern; it provides a transparent, step-by-step implementation for those who already opt into this architecture.
Sentinel — Human
This is a detailed, technically dense tutorial demonstrating a novel integration between container identity (SPIRE) and virtual machine identities using kernel-level socket communication for a specific virtualization platform.
