SPIFFE for Ruby: Bringing Zero Trust Identity to Ruby Applications


The Missing Piece: Ruby in the SPIFFE Ecosystem

SPIFFE (Secure Production Identity Framework For Everyone) has become the industry standard for Zero Trust workload identity, with IETF backing since 2024 and adoption by major cloud-native projects. Official client libraries exist for Go, Python, Java, and Rustβ€”but until now, Ruby was left out.

This gap is significant because Ruby powers:

  • πŸ—οΈ Configuration management (Puppet, Chef)
  • 🌐 Web applications (Rails, Sinatra)
  • βš™οΈ DevOps tooling (CI/CD scripts, automation)

Without a native client, Ruby applications relied on insecure workarounds like subprocess calls that break process-based attestationβ€”the core security principle of SPIFFE.


Introducing spiffe-workload: First Ruby Client for SPIFFE

The spiffe-workload gem fills this ecosystem gap with a production-ready implementation:

FeatureDescription
Direct gRPCUnix socket communicationβ€”no HTTP exposure
Process AttestationSPIRE verifies your actual Ruby process
X.509 & JWT SVIDsBoth mTLS certificates and API tokens
Auto-RotationCallback hooks for credential updates
Thread-SafeProduction-ready for multi-threaded apps
Zero DependenciesOnly requires grpc and openssl

Why This Matters: Subprocess vs. Native

The Problem with Shell Commands:

# ❌ BROKEN: SPIRE attests /bin/sh, not your Ruby app
svid = `spire-agent api fetch x509`

This approach:

  • Attests the wrong process (shell, not Ruby)
  • Allows any process to impersonate your app
  • Breaks Zero Trust security model

The Solution with Native Library:

# βœ… CORRECT: SPIRE attests your Ruby process
require 'spiffe'
client = Spiffe.workload_api_client
svid = client.x509_svid  # Cryptographically bound to THIS process

Quick Start: 3 Steps to Zero Trust Identity

1. Install

gem install spiffe-workload

2. Register with SPIRE

spire-server entry create \
  -parentID spiffe://example.org/agent/myhost \
  -spiffeID spiffe://example.org/myapp \
  -selector unix:uid:$(id -u) \
  -selector unix:path:/usr/bin/ruby

3. Use in Your Code

require 'spiffe'

# Get your identity
client = Spiffe.workload_api_client
svid = client.x509_svid
puts "Identity: #{svid.spiffe_id}"

# Use for mTLS
http = Net::HTTP.new('api.example.com', 443)
http.use_ssl = true
http.ssl_context = client.tls_context
response = http.get('/data')

client.shutdown

Real-World Example: Puppet + SPIRE + Vault Integration

The Challenge: Configuration management systems traditionally store secrets in Hiera or version controlβ€”a critical security vulnerability.

The Solution: A production-ready Puppet function that orchestrates the complete flow: SPIRE JWT β†’ Vault auth β†’ Secret retrieval.

The Complete Implementation

# modules/spire_vault/lib/puppet/functions/spire_vault/lookup.rb
Puppet::Functions.create_function(:'spire_vault::lookup') do
  def lookup(secret_path, options = {})
    # Step 1: Fetch JWT from SPIRE using spiffe-workload gem
    client = Spiffe.workload_api_client(socket_path: spire_socket)
    jwt_svid = client.jwt_svid(audience: jwt_audience)

    # Step 2: Authenticate to Vault with JWT
    vault_token = vault_login(jwt_svid.token, jwt_role)

    # Step 3: Fetch secret from Vault
    secret_data = vault_read(secret_path, vault_token)

    # Step 4: Return field or entire secret
    options['field'] ? secret_data[options['field']] : secret_data
  end
end

Usage in Puppet Manifests

# Pattern 1: Extract specific field (most common)
file { '/etc/app/db_password':
  content => Deferred('spire_vault::lookup', [
    'kv/data/myapp/database',
    { 'field' => 'password' }
  ]),
  mode => '0600',
}

# Pattern 2: Multiple secrets in template
file { '/etc/app/config':
  content => Deferred('inline_epp', [
    'DB_HOST=<%= $host %>, DB_PASS=<%= $pass %>',
    {
      'host' => Deferred('spire_vault::lookup', ['kv/data/db', {'field' => 'host'}]),
      'pass' => Deferred('spire_vault::lookup', ['kv/data/db', {'field' => 'password'}])
    }
  ]),
}

SPIRE Workload Registration

# Puppet agent identity in SPIRE
apiVersion: spire.spiffe.io/v1alpha1
kind: ClusterStaticEntry
spec:
  spiffeID: 'spiffe://example.com/puppet/agent'
  selectors:
    - 'unix:uid:0' # Root user
    - 'unix:path:/opt/puppetlabs/puppet/bin/ruby' # Puppet Ruby process

Impact:

  • βœ… Zero secrets in Git - No Hiera eyaml or encrypted data
  • βœ… Per-node identity - Each agent attested by SPIRE
  • βœ… 15-30 min TTLs - Automatic JWT rotation
  • βœ… Vault policies - Fine-grained access control by SPIFFE ID

This pattern is in production use managing secrets across hundreds of Puppet-managed nodes.

Key Advantages:

  1. Process attestation - SPIRE verifies /opt/puppetlabs/puppet/bin/ruby, not shell
  2. Deferred execution - Secrets fetched on agent, not compiled in catalog
  3. Zero persistent credentials - JWTs expire, no long-lived tokens
  4. Audit trail - Complete chain: SPIFFE ID β†’ Vault role β†’ Secret access

Key Features

πŸ”„ Automatic Rotation

client = Spiffe.workload_api_client

client.on_x509_svid_update do |new_svid|
  puts "Credential rotated: #{new_svid.leaf_certificate.not_after}"
  @http_client.ssl_context = client.tls_context
end

Thread.new { client.watch_x509_svids }

🎫 JWT SVIDs for APIs

jwt = client.jwt_svid(audience: 'api.example.com')

request = Net::HTTP::Get.new('/data')
request['Authorization'] = "Bearer #{jwt.token}"
# Token automatically expires in 15-30 minutes

πŸ” Both Identity Types

X.509 SVIDsJWT SVIDs
mTLS between servicesHTTP API authentication
Certificate-basedToken-based
Works with Envoy, IstioWorks with API gateways
Mutual authenticationStandard OAuth2 format

Completing the SPIFFE Ecosystem

The gem joins official clients for other languages:

LanguageClient LibraryStatus
Gogo-spiffeβœ… Official
Pythonpy-spiffeβœ… Official
Javajava-spiffeβœ… Official
Rustspiffe-rsβœ… Official
Rubyspiffe-workloadπŸ†• First Implementation

Architecture: gRPC over Unix sockets

# Simplified implementation
@channel = GRPC::Core::Channel.new("unix://#{@socket_path}")
@stub = SpiffeWorkloadAPI::Stub.new(nil, nil, channel_override: @channel)

def x509_svid
  response = @stub.fetch_x509_svid(X509SVIDRequest.new)
  X509SVIDWrapper.new(response.svids.first)
end

Performance: 5-10ms per SVID fetch (cached socket), thread-safe for production use.


Troubleshooting Quick Reference

IssueSolution
Socket not foundsystemctl status spire-agent or set SPIFFE_ENDPOINT_SOCKET
No identity issuedVerify workload entry: spire-server entry show
Permission deniedAdd user to spire group: sudo usermod -a -G spire $USER
Connection timeoutCheck SPIRE agent logs: journalctl -u spire-agent -f

Production Best Practices

# Always use error handling and cleanup
begin
  client = Spiffe.workload_api_client(timeout: 10)

  # Retry transient failures
  retries = 3
  begin
    svid = client.x509_svid
  rescue Spiffe::WorkloadAPIError => e
    retries -= 1
    raise unless retries > 0
    sleep 2 ** (3 - retries)
    retry
  end

  # Use SVID...
ensure
  client&.shutdown  # Always clean up
end

Checklist:

  • βœ… Set TTLs to 15-30 minutes in SPIRE
  • βœ… Implement rotation callbacks for long-running apps
  • βœ… Never log tokens/certificates
  • βœ… Handle agent unavailability gracefully
  • βœ… Monitor SPIRE agent health

Why This Matters

Closing the Ruby Gap: Before this gem, Ruby was the only major language without native SPIFFE support. With SPIFFE adoption accelerating (IETF backing, WebAssembly integration) and Ruby powering critical infrastructure, this gap was a security liability.

Key Impact:

  1. 🎯 First Ruby client for SPIFFE Workload APIβ€”completes the ecosystem
  2. πŸ” Proper attestation replaces insecure subprocess workarounds
  3. 🎭 Puppet transformation enables Zero Trust configuration management (production-validated with hundreds of nodes)
  4. πŸš€ Production-ready with thread safety, rotation, and comprehensive error handling

Real-World Validation: The Puppet + SPIRE + Vault integration shown above is running in production, demonstrating that Ruby applications can now participate fully in Zero Trust architectures alongside Go, Python, and Java services.


Getting Started

# 1. Install
gem install spiffe-workload

# 2. Register with SPIRE
spire-server entry create \
  -spiffeID spiffe://example.org/myapp \
  -selector unix:path:/usr/bin/ruby

# 3. Use in your code
require 'spiffe'
svid = Spiffe.workload_api_client.x509_svid
puts "Identity: #{svid.spiffe_id}"

Resources

Project Links:

Production Examples:

SPIFFE Ecosystem:

Related Posts:

Comments

πŸ‘€ 0