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:
| Feature | Description |
|---|---|
| Direct gRPC | Unix socket communicationβno HTTP exposure |
| Process Attestation | SPIRE verifies your actual Ruby process |
| X.509 & JWT SVIDs | Both mTLS certificates and API tokens |
| Auto-Rotation | Callback hooks for credential updates |
| Thread-Safe | Production-ready for multi-threaded apps |
| Zero Dependencies | Only 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:
- Process attestation - SPIRE verifies
/opt/puppetlabs/puppet/bin/ruby, not shell - Deferred execution - Secrets fetched on agent, not compiled in catalog
- Zero persistent credentials - JWTs expire, no long-lived tokens
- 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 SVIDs | JWT SVIDs |
|---|---|
| mTLS between services | HTTP API authentication |
| Certificate-based | Token-based |
| Works with Envoy, Istio | Works with API gateways |
| Mutual authentication | Standard OAuth2 format |
Completing the SPIFFE Ecosystem
The gem joins official clients for other languages:
| Language | Client Library | Status |
|---|---|---|
| Go | go-spiffe | β Official |
| Python | py-spiffe | β Official |
| Java | java-spiffe | β Official |
| Rust | spiffe-rs | β Official |
| Ruby | spiffe-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
| Issue | Solution |
|---|---|
| Socket not found | systemctl status spire-agent or set SPIFFE_ENDPOINT_SOCKET |
| No identity issued | Verify workload entry: spire-server entry show |
| Permission denied | Add user to spire group: sudo usermod -a -G spire $USER |
| Connection timeout | Check 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:
- π― First Ruby client for SPIFFE Workload APIβcompletes the ecosystem
- π Proper attestation replaces insecure subprocess workarounds
- π Puppet transformation enables Zero Trust configuration management (production-validated with hundreds of nodes)
- π 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:
- π¦ GitHub Repository β Source and examples
- π RubyGems Package β Installation
- π SPIFFE Docs β Standards and guides
Production Examples:
- π Puppet + SPIRE + Vault Integration β Complete implementation
- π Full function source in the gem repository
SPIFFE Ecosystem:
- Workload API Spec β Official specification
- go-spiffe β Go implementation
- py-spiffe β Python implementation
- java-spiffe β Java implementation
Related Posts:
- Elevating CI/CD Pipeline Security β SPIRE for GitHub Actions
Comments