Ecstaticloud
Initializing0%
Ecstaticloud Logo
Back to Insights
CybersecurityOctober 17, 2026

Zero-Trust Service Identity: Implementing SPIFFE/SPIRE in Multi-Cloud Kubernetes

As multi-cloud Kubernetes deployments scale, traditional perimeter security fails to protect microservices from sophisticated insider and lateral movement attacks. Discover how implementing short-lived cryptographic identities using SPIFFE/SPIRE eliminates static secrets and hardens cross-cloud workloads.

The enterprise perimeter is officially dead. As organizations transition from monolithic data centers to multi-cloud Kubernetes architectures spanning AWS EKS, GCP GKE, and Azure AKS, the traditional "hard shell, soft center" security posture collapses. Relying on network-level primitives like IP addresses, CIDR blocks, Kubernetes NetworkPolicies, or cloud-native IAM mechanisms (e.g., AWS IRSA or GCP Workload Identity) leaves critical security gaps when workloads cross cluster boundaries or cloud providers.

Cloud IAM solutions are platform-bound silos. IRSA cannot natively identify a microservice running on GKE trying to access a Kafka cluster on EKS without complex, fragile OIDC federation setups for every single service. Furthermore, static API keys, database credentials, and long-lived TLS certificates stored in Kubernetes Secrets remain a prime target for lateral movement attacks.

To achieve a true Zero-Trust Architecture (ZTA), service identity must be decoupled from the network topology and cloud provider lock-in. Identity must be cryptographic, short-lived, dynamically attested, and platform-agnostic.

This is precisely what SPIFFE (Secure Production Identity Framework for Everyone) and SPIRE (SPIFFE Runtime Environment) deliver. In this post, we will dissect the mechanics of SPIFFE/SPIRE and execute an end-to-end multi-cloud Kubernetes implementation.


1. The SPIFFE/SPIRE Architecture

SPIFFE defines an open standard for identifying software workloads. SPIRE is the reference implementation designed to issue these identities dynamically based on platform attestation.

                  +-----------------------------------+
                  |        SPIRE Server               |
                  |  - Issues SVIDs                   |
                  |  - Maintains Trust Domain         |
                  |  - Manages CA Keys & Federation   |
                  +-----------------+-----------------+
                                    |
            +-----------------------+-----------------------+
            | mTLS / gRPC                                   | mTLS / gRPC
            v                                               v
+-----------------------+                       +-----------------------+
|  EKS Node             |                       |  GKE Node             |
|  +-----------------+  |                       |  +-----------------+  |
|  |   SPIRE Agent   |  |                       |  |   SPIRE Agent   |  |
|  +--------+--------+  |                       |  +--------+--------+  |
|           | UDS       |                       |           | UDS       |
|  +--------v--------+  |                       |  +--------v--------+  |
|  |  Payment Pod    |  |                       |  |  Order Pod      |  |
|  +-----------------+  |                       |  +-----------------+  |
+-----------------------+                       +-----------------------+

Core Concepts

  1. SPIFFE ID: A uniform resource identifier (URI) that uniquely identifies a workload. Format: spiffe://<trust-domain>/<workload-path> Example: spiffe://ecstaticloud.internal/ns/production/sa/payment-service

  2. SPIFFE Verifiable Identity Document (SVID): The cryptographic token representing the SPIFFE ID. SVIDs are issued as X.509 certificates (for mTLS) or JWT tokens (for HTTP authorization headers).

  3. SPIFFE Workload API: A local Unix Domain Socket (UDS) exposed by the SPIRE Agent. Workloads fetch their SVIDs directly from this socket without needing any static authentication credentials.

  4. Trust Domain: A logical boundary managing its own keys and authorization policies.


2. Multi-Tiered Attestation Mechanism

SPIRE guarantees workload identity through a two-tiered attestation model: Node Attestation and Workload Attestation.

+-------------------------------------------------------------------------------+
|                            Attestation Flow                                   |
+-------------------------------------------------------------------------------+

 [ Node Boot ]
      |
      v
 1. Node Attestation ----> AWS/GCP/K8s PSAT API
      |                   (Validates Instance / Node Identity)
      v
 [ SPIRE Agent Obtains Agent SVID ]
      |
      v
 [ Pod Start ]
      |
      v
 2. Workload Attestation -> Queries Kubelet / CGroup / Container Runtime
      |                   (Validates Namespace, ServiceAccount, Pod UID, Image)
      v
 3. Issue SVID ----------> Returns X.509 SVID over Unix Domain Socket

Tier 1: Node Attestation

When a SPIRE Agent boots on a Kubernetes worker node, it proves its host's identity to the central SPIRE Server using node attestors:

  • AWS: Uses AWS Instance Identity Documents (IID).
  • GCP: Uses Google Compute Engine Instance Identity Tokens.
  • Kubernetes: Uses Projected Service Account Tokens (k8s_psat).

Tier 2: Workload Attestation

Once the Agent is authenticated, local workloads (Pods) request an identity from the local SPIRE Agent over the Unix Domain Socket /tmp/spire-agent/public/api.sock.

The Agent inspects the caller process by querying the Linux kernel (PIDs, CGroups) and the local Kubelet daemon to evaluate dynamic selectors:

  • k8s:ns: Namespace of the pod.
  • k8s:sa: Service Account assigned to the pod.
  • k8s:pod-label: Specific metadata key-value pairs.
  • k8s:container-image-sign-verifier: Cryptographic image digest signatures.

If the attributes match a registered identity rule, the SPIRE Agent constructs and signs an SVID (or requests the Server to sign it) and hands it back to the pod in-memory.


3. Production Multi-Cloud Kubernetes Deployment

Let's walk through building a multi-cloud SPIFFE/SPIRE infrastructure bridging AWS EKS and GCP GKE under a unified trust configuration.

Step 1: Deploying SPIRE Server with High Availability

The SPIRE Server requires a shared datastore (e.g., PostgreSQL) for HA deployments across availability zones.

Here is the declarative configuration for spire-server.conf:

# spire-server.conf
server {
  bind_address = "0.0.0.0"
  bind_port = "8081"
  trust_domain = "ecstaticloud.internal"
  data_dir = "/run/spire/data"
  log_level = "DEBUG"
  ca_ttl = "168h"
  default_x509_svid_ttl = "1h"
}

plugins {
  DataStore "sql" {
    plugin_data {
      database_type = "postgres"
      connection_string = "postgres://spire_admin:SecurePassword123!@spire-db.internal:5432/spire?sslmode=verify-full"
    }
  }

  KeyManager "disk" {
    plugin_data {
      keys_path = "/run/spire/data/keys.json"
    }
  }

  # Enable Kubernetes PSAT Node Attestation
  NodeAttestor "k8s_psat" {
    plugin_data {
      clusters = {
        "aws-eks-cluster-1" = {
          service_account_allow_list = ["spire:spire-agent"]
        }
        "gcp-gke-cluster-1" = {
          service_account_allow_list = ["spire:spire-agent"]
        }
      }
    }
  }

  UpstreamAuthority "aws_pca" {
    plugin_data {
      region = "us-east-1"
      certificate_authority_arn = "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/abc-123"
    }
  }
}

Step 2: Deploying SPIRE Agent DaemonSet

The SPIRE Agent runs as a DaemonSet on every node across both EKS and GKE clusters. It mounts a host path socket directory to serve the local Workload API.

# spire-agent-daemonset.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: spire-agent
  namespace: spire
  labels:
    app: spire-agent
spec:
  selector:
    matchLabels:
      app: spire-agent
  template:
    metadata:
      labels:
        app: spire-agent
    spec:
      hostPID: true
      hostNetwork: true
      dnsPolicy: ClusterFirstWithHostNet
      serviceAccountName: spire-agent
      containers:
        - name: spire-agent
          image: ghcr.io/spiffe/spire-agent:1.8.5
          args: ["-config", "/run/spire/config/spire-agent.conf"]
          volumeMounts:
            - name: spire-config
              mountPath: /run/spire/config
              readOnly: true
            - name: spire-agent-socket
              mountPath: /run/spire/sockets
            - name: kubelet-socket
              mountPath: /var/run/kmsplugin
              readOnly: true
          livenessProbe:
            exec:
              command: ["/opt/spire/bin/spire-agent", "healthcheck", "-socketPath", "/run/spire/sockets/agent.sock"]
            initialDelaySeconds: 15
            periodSeconds: 60
      volumes:
        - name: spire-config
          configMap:
            name: spire-agent
        - name: spire-agent-socket
          hostPath:
            path: /run/spire/sockets
            type: DirectoryOrCreate
        - name: kubelet-socket
          hostPath:
            path: /var/run/kmsplugin
            type: Directory

And its associated configuration (spire-agent.conf):

# spire-agent.conf
agent {
  data_dir = "/run/spire/agent/data"
  log_level = "INFO"
  server_address = "spire-server.spire.svc.cluster.local"
  server_port = "8081"
  socket_path = "/run/spire/sockets/agent.sock"
  trust_domain = "ecstaticloud.internal"
}

plugins {
  NodeAttestor "k8s_psat" {
    plugin_data {
      cluster = "aws-eks-cluster-1"
    }
  }

  WorkloadAttestor "k8s" {
    plugin_data {
      # Path to the Kubelet socket for workload verification
      kubelet_read_only_port = 10255
    }
  }

  KeyManager "memory" {}
}

4. Workload Registration via SPIRE Controller Manager

Rather than manually registering identity mapping rules via CLI commands, modern production setups utilize the SPIRE Controller Manager. It listens to Kubernetes Custom Resources (SpiffeID) to declaratively automate workload identity assignment.

Declarative CRD Mapping

# payment-service-spiffeid.yaml
apiVersion: spire.spiffe.io/v1alpha1
kind: SpiffeID
metadata:
  name: payment-service-identity
  namespace: production
spec:
  spiffeId: spiffe://ecstaticloud.internal/ns/production/sa/payment-service-sa
  dnsNames:
    - payment.production.svc.cluster.local
  selector:
    podLabel:
      app: payment-processor
    namespace: production
    serviceAccount: payment-service-sa

When this manifest is applied:

  1. SPIRE Controller Manager detects the resource.
  2. It calls the SPIRE Server API to construct an entry linking the selectors (k8s:ns:production, k8s:sa:payment-service-sa, k8s:pod-label:app:payment-processor) to the target spiffeID.
  3. Any pod matching these criteria can now retrieve its X.509 SVID directly from its node's SPIRE Agent socket.

5. Consuming Identity: Two Technical Approaches

Once SPIRE is running, how do application microservices actually use these identities?

Option A: Zero-Code Sidecar Integration via Envoy Proxy & SDS

The cleanest enterprise architecture delegates cryptographic identity management to an Envoy sidecar proxy using the Secret Discovery Service (SDS) protocol over the SPIFFE Workload API Unix socket.

+-----------------------------------------------------------------------+
|  Pod: payment-processor                                               |
|                                                                       |
|  +--------------------+                   +------------------------+  |
|  | Application Container|  HTTP (localhost) | Envoy Sidecar Proxy    |  |
|  | (Plain Text)       |------------------>| - Handles mTLS         |  |
|  +--------------------+                   | - Fetches SVID via SDS |  |
|                                           +-----------+------------+  |
+-------------------------------------------------------|---------------+
                                                        | SDS over UDS
                                                        v
                                            /run/spire/sockets/agent.sock

Envoy SDS snippet (envoy-config.yaml):

static_resources:
  listeners:
    - name: ingress_mtls_listener
      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://ecstaticloud.internal/ns/production/sa/payment-service-sa"
                    sds_config:
                      api_config_source:
                        api_type: GRPC
                        grpc_services:
                          - envoy_grpc:
                              cluster_name: spire_agent
                validation_context_sds_secret_config:
                  name: "spiffe://ecstaticloud.internal"
                  sds_config:
                    api_config_source:
                      api_type: GRPC
                      grpc_services:
                        - envoy_grpc:
                            cluster_name: spire_agent
  clusters:
    - name: spire_agent
      type: STATIC
      connect_timeout: 0.25s
      http2_protocol_options: {}
      load_assignment:
        cluster_name: spire_agent
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    pipe:
                      path: /run/spire/sockets/agent.sock

Option B: Programmatic Integration via Go SPIFFE SDK

For low-latency or high-performance services where sidecar proxies add unacceptable memory/latency overhead, consume the Workload API natively inside application code.

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"

	"github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig"
	"github.com/spiffe/go-spiffe/v2/workloadapi"
)

const socketPath = "unix:///run/spire/sockets/agent.sock"

func main() {
	ctx := context.Background()

	// Create a Workload API X.509 Source
	source, err := workloadapi.NewX509Source(ctx, workloadapi.WithClientOptions(
		workloadapi.WithAddr(socketPath),
	))
	if err != nil {
		log.Fatalf("Unable to create X509Source: %v", err)
	}
	defer source.Close()

	// Authorize specific client SPIFFE ID during mTLS handshake
	allowedID := "spiffe://ecstaticloud.internal/ns/production/sa/checkout-service-sa"
	tlsConfig := tlsconfig.MTLSServerConfig(source, source, tlsconfig.AuthorizeID(allowedID))

	server := &http.Server{
		Addr:      ":8443",
		TLSConfig: tlsConfig,
	}

	http.HandleFunc("/charge", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Payment processed securely via Zero-Trust mTLS!")
	})

	log.Println("Starting mTLS server authenticated by SPIFFE...")
	log.Fatal(server.ListenAndServeTLS("", ""))
}

With this SDK integration:

  • The application automatically streams X.509 SVIDs into memory.
  • Certificate Rotation occurs dynamically without process restarts as SPIRE handles background rotation (e.g., every 30 minutes).

6. Cross-Cloud SPIFFE Trust Domain Federation

When running native services across two independent clouds (AWS EKS and GCP GKE), each cluster should typically act as its own autonomous Trust Domain.

To allow microservices in aws.ecstaticloud.internal to execute secure mTLS calls to services in gcp.ecstaticloud.internal, we use SPIFFE Federation.

+--------------------------------------+         +--------------------------------------+
| AWS Trust Domain                     |         | GCP Trust Domain                     |
| spiffe://aws.ecstaticloud.internal   |         | spiffe://gcp.ecstaticloud.internal   |
|                                      |         |                                      |
| +----------------------------------+ |         | +----------------------------------+ |
| | SPIRE Server (AWS)               | |         | | SPIRE Server (GCP)               | |
| | - Serves Bundle Endpoint         |<===========>| - Serves Bundle Endpoint         | |
| +----------------------------------+ |  HTTPS  | +----------------------------------+ |
|                                      |  Bundle |                                      |
| +----------------------------------+ | Exch.   | +----------------------------------+ |
| | AWS Workload                     | |         | | GCP Workload                     | |
| | SVID: spiffe://aws.../sa/billing | |         | | SVID: spiffe://gcp.../sa/ledger  | |
| +----------------------------------+ |         | +----------------------------------+ |
+--------------------------------------+         +--------------------------------------+

Server Federation Configuration

To configure SPIRE Server on AWS to federate with GCP:

# aws-spire-server.conf (Fragment)
server {
  trust_domain = "aws.ecstaticloud.internal"
  
  federation {
    bundle_endpoint {
      address = "0.0.0.0"
      port = 8443
    }
  }
}

plugins {
  DataStore "sql" { ... }
  
  # Register foreign GCP Trust Domain
  Federation "gcp.ecstaticloud.internal" {
    plugin_data {
      bundle_endpoint_url = "https://spire-federation.gcp.ecstaticloud.internal:8443"
      bundle_endpoint_profile "https_spiffe" {
        endpoint_spiffe_id = "spiffe://gcp.ecstaticloud.internal/spire/server"
      }
    }
  }
}

Dynamic Trust Synchronization

  1. The AWS SPIRE Server securely polls the GCP SPIRE Server bundle endpoint over HTTPS using SPIFFE authentication.
  2. Root CA certificates are synchronized dynamically without exposing private keys.
  3. Workloads in AWS receive trust bundles containing public keys for both aws.ecstaticloud.internal and gcp.ecstaticloud.internal.
  4. Envoy proxies evaluate federated cross-cloud calls seamlessly during TLS handshakes.

7. Hardening, Operations, and Debugging

Deploying SPIFFE/SPIRE at scale requires intentional platform engineering. Here are crucial production guidelines:

1. Attestation Security Traps

  • Avoid Overly Permissive Selectors: Never register a SPIFFE ID using solely k8s:ns. A compromised pod inside that namespace could spoof identity for any other pod in that namespace. Always enforce multi-tuple selectors:
    k8s:ns + k8s:sa + k8s:pod-label
    
  • Enforce Signed Container Digests: Complement Kubernetes attestation with container image digests or Cosign signature validation rules (k8s:image-id) to prevent supply-chain image poisoning attacks.

2. SVID Lifecycle & Short TTLs

Set short X.509 SVID lifespans (e.g., 1 Hour). Short-lived credentials render stolen certificates practically useless to attackers, eliminating the need for complex, latency-heavy Certificate Revocation Lists (CRLs) or OCSP stapling mechanisms.

3. Monitoring & Observability

Expose Prometheus metrics from both SPIRE Server and Agents on port :9088. Critical metrics to monitor:

  • spire_agent_workload_api_connection_errors: Spikes indicate permission issues accessing socket /run/spire/sockets/agent.sock.
  • spire_server_ca_days_left: Certificate Authority expiration tracking.
  • spire_agent_svid_rotation_latency: Latency metrics when agent renews workload certificates.

4. Debugging SPIRE Workloads

When a pod fails to fetch its SVID, use the spire-agent CLI to simulate workload attestation for a specific PID:

# Exec into SPIRE Agent Pod
kubectl exec -ti spire-agent-xyz -n spire -- /bin/sh

# Query identities currently issued by the local agent
/opt/spire/bin/spire-agent api fetch -socketPath /run/spire/sockets/agent.sock

# Validate registration entries on SPIRE Server
/opt/spire/bin/spire-server entry show -spiffeID spiffe://ecstaticloud.internal/ns/production/sa/payment-service-sa

Summary

Moving to a Zero-Trust architecture across heterogeneous clouds requires abandoning static secrets and provider-specific IAM dependencies.

By standardizing on SPIFFE/SPIRE, software identity becomes dynamic, cryptographically verifiable, and seamlessly portable across AWS, GCP, and on-premises environments. Workloads authenticate directly via local platforms, credentials rotate automatically in-memory, and security enforcement shifts from rigid IP boundaries to identity-based access control.