Ruby on Rails PCI Compliance
Bottom Line Up Front
Ruby on Rails applications handling payment card data need specific configurations and controls to meet PCI DSS requirements. Your Rails app’s architecture determines whether you’re building for SAQ A-EP (tokenized payments with redirect), SAQ D (storing card data), or somewhere in between. The framework’s conventions make some requirements easier — strong parameter filtering, encrypted credentials, and built-in CSRF protection — but you’ll need to implement additional controls for secure coding, logging, encryption, and access management to achieve full compliance.
Technical Overview
Rails follows convention-over-configuration principles that align well with PCI security requirements when properly implemented. The framework provides several security features out of the box: parameter filtering for sensitive data, encrypted credentials management, secure session handling, and protection against common web vulnerabilities.
Architecture Considerations
Your Rails application’s placement within the Cardholder Data Environment (CDE) determines your compliance scope. Three common architectures:
Fully Segmented (SAQ A/A-EP eligible):
- Rails app redirects to hosted payment page
- No card data touches your servers
- JavaScript tokenization (Stripe Elements, Square, etc.)
API Integration (SAQ D):
- Rails app receives card data via forms
- Transmits to payment gateway API
- Requires full CDE controls
Hybrid with Tokenization:
- Initial card capture for tokenization
- Subsequent transactions use tokens only
- Reduced ongoing scope
The framework integrates naturally with cloud platforms (AWS, Google Cloud, Azure) that offer PCI-compliant infrastructure. Your deployment model — containerized, traditional servers, or platform-as-a-service — impacts which controls you inherit versus implement.
Defense-in-Depth Positioning
Rails applications typically sit at the application layer but must integrate with multiple security layers:
- Network layer: WAF, firewall rules, network segmentation
- Infrastructure layer: Hardened OS, patched dependencies, secure configurations
- Application layer: Your Rails code and security controls
- Data layer: Database encryption, access controls, key management
PCI DSS Requirements Addressed
Rails applications primarily address requirements in the secure coding and application security domains:
Requirement 6: Develop and Maintain Secure Systems
Rails helps meet Requirement 6.5 (common vulnerabilities) through built-in protections:
| Vulnerability | Rails Protection | Additional Steps Needed |
|---|---|---|
| SQL Injection | ActiveRecord parameterization | Audit raw SQL usage |
| XSS | Auto-escaping in views | Configure CSP headers |
| CSRF | Token verification by default | Ensure not disabled |
| Broken Authentication | has_secure_password | Implement account lockout |
| Sensitive Data Exposure | Parameter filtering | Add field-level encryption |
Requirement 6.3 mandates secure software development lifecycle. Your Rails development must include:
- Code reviews before production deployment
- Separation of development, testing, and production environments
- Change control procedures for all code changes
Requirement 8: Identify and Authenticate Access
Rails authentication typically uses Devise, Authlogic, or custom implementations. For PCI compliance:
- 8.2.3: Passwords must meet complexity requirements (implement in User model validations)
- 8.2.4: Password changes every 90 days (add password_changed_at tracking)
- 8.2.5: No password reuse for last four passwords (store password history)
- 8.3: Multi-factor authentication for all CDE access (integrate with Authy, Google Authenticator)
Requirement 10: Track and Monitor All Access
Rails logging requires enhancement for PCI compliance:
- 10.2: Log all access to cardholder data
- 10.3: Record required event details (user ID, timestamp, success/failure)
- 10.5: Secure logs against tampering
Implementation Guide
Initial Security Hardening
Start with `config/application.rb` and environment-specific configurations:
“`ruby
config/application.rb
module YourApp
class Application < Rails::Application
# Force SSL in production
config.force_ssl = true
# Security headers
config.middleware.use Rack::Protection
# Parameter filtering for PCI data
config.filter_parameters += [:card_number, :cvv, :expiry, :pan]
# Disable unused middleware
config.middleware.delete ActionDispatch::Cookies unless needed
config.middleware.delete ActionDispatch::Session::CookieStore unless needed
end
end
```
Secure Headers Configuration
Add comprehensive security headers using Secure Headers gem:
“`ruby
Gemfile
gem ‘secure_headers’
config/initializers/secure_headers.rb
SecureHeaders::Configuration.default do |config|
config.x_frame_options = “DENY”
config.x_content_type_options = “nosniff”
config.x_xss_protection = “1; mode=block”
config.hsts = {
max_age: 31536000,
include_subdomains: true,
preload: true
}
config.csp = {
default_src: %w(‘self’),
script_src: %w(‘self’ ‘unsafe-inline’), # Tighten for production
style_src: %w(‘self’ ‘unsafe-inline’),
img_src: %w(‘self’ data: https:),
font_src: %w(‘self’),
connect_src: %w(‘self’),
form_action: %w(‘self’),
base_uri: %w(‘self’),
frame_ancestors: %w(‘none’)
}
end
“`
Database Encryption Implementation
For storing sensitive authentication data (if permitted under Requirement 3.4):
“`ruby
Gemfile
gem ‘attr_encrypted’
app/models/payment_method.rb
class PaymentMethod < ApplicationRecord
# Encrypt specific fields at rest
attr_encrypted :account_number, key: Rails.application.credentials.encryption_key
attr_encrypted :routing_number, key: Rails.application.credentials.encryption_key
# Never store CVV or PIN data
attr_accessor :cvv # Memory only, never persisted
# Mask for display
def masked_account_number
return nil unless account_number.present?
"” (account_number.length – 4) + account_number.last(4)
end
end
“`
Audit Logging Implementation
Create comprehensive audit logging for Requirement 10:
“`ruby
app/models/concerns/auditable.rb
module Auditable
extend ActiveSupport::Concern
included do
after_create :log_create
after_update :log_update
after_destroy :log_destroy
end
private
def log_create
AuditLog.create!(
user_id: Current.user&.id,
action: ‘create’,
auditable_type: self.class.name,
auditable_id: self.id,
ip_address: Current.ip_address,
user_agent: Current.user_agent,
changes: self.attributes
)
end
def log_update
return unless changed?
# Filter sensitive changes
filtered_changes = changes.except(*Rails.application.config.filter_parameters)
AuditLog.create!(
user_id: Current.user&.id,
action: ‘update’,
auditable_type: self.class.name,
auditable_id: self.id,
ip_address: Current.ip_address,
user_agent: Current.user_agent,
changes: filtered_changes
)
end
end
“`
Payment Integration with Scope Reduction
Implement tokenization to minimize CDE scope:
“`ruby
app/controllers/payments_controller.rb
class PaymentsController < ApplicationController def create # Never log full card data Rails.logger.info("Payment attempt for user: #{current_user.id}") # Use payment gateway's tokenization response = PaymentGateway.tokenize( card_number: params[:card_number], expiry_month: params[:expiry_month], expiry_year: params[:expiry_year], cvv: params[:cvv] ) if response.success? # Store only the token current_user.payment_methods.create!( token: response.token, card_type: response.card_type, last_four: response.last_four, expiry_month: params[:expiry_month], expiry_year: params[:expiry_year] ) # Clear sensitive params from memory params.delete(:card_number) params.delete(:cvv) redirect_to success_path else flash[:error] = "Payment method could not be saved" render :new end end end ```
Environment Configuration
Separate configurations by environment with proper secrets management:
“`ruby
config/environments/production.rb
Rails.application.configure do
# Force SSL
config.force_ssl = true
# Session security
config.session_store :cookie_store,
key: ‘_secure_session’,
secure: true,
httponly: true,
expire_after: 15.minutes,
same_site: :strict
# Log level that excludes sensitive data
config.log_level = :info
config.log_tags = [:request_id, :remote_ip]
# Asset compilation settings
config.assets.compile = false
config.public_file_server.enabled = false
end
“`
Testing and Validation
Security Testing Framework
Implement automated security testing in your CI/CD pipeline:
“`ruby
spec/security/parameter_filtering_spec.rb
RSpec.describe “Parameter Filtering” do
it “filters card numbers from logs” do
post “/payments”, params: {
card_number: “4111111111111111”,
amount: 100
}
log_content = File.read(Rails.root.join(“log/test.log”))
expect(log_content).not_to include(“4111111111111111”)
expect(log_content).to include(“[FILTERED]”)
end
end
spec/security/authentication_spec.rb
RSpec.describe “Authentication Security” do
it “enforces password complexity” do
user = User.new(email: “test@example.com”, password: “simple”)
expect(user).not_to be_valid
expect(user.errors[:password]).to include(“must contain uppercase, lowercase, number, and special character”)
end
it “locks account after failed attempts” do
user = create(:user)
6.times do
post “/login”, params: { email: user.email, password: “wrong” }
end
expect(user.reload).to be_locked
end
end
“`
Compliance Validation Checklist
Create automated tests that mirror QSA validation:
“`ruby
lib/tasks/pci_compliance.rake
namespace :pci do
desc “Run PCI compliance checks”
task compliance_check: :environment do
puts “Running PCI compliance validations…”
# Check SSL/TLS configuration
if Rails.application.config.force_ssl
puts “✓ SSL enforced in production”
else
puts “✗ SSL not enforced – Required by PCI DSS 4.1”
end
# Check parameter filtering
sensitive_params = [:card_number, :cvv, :pan, :account_number]
missing = sensitive_params – Rails.application.config.filter_parameters
if missing.empty?
puts “✓ All sensitive parameters filtered”
else
puts “✗ Unfiltered parameters: #{missing.join(‘, ‘)}”
end
# Check authentication requirements
if User.validators_on(:password).any? { |v| v.is_a?(PasswordComplexityValidator) }
puts “✓ Password complexity enforced”
else
puts “✗ Password complexity not enforced – Required by PCI DSS 8.2.3”
end
end
end
“`
Vulnerability Scanning Integration
Configure your ASV scanning to handle Rails applications:
“`yaml
.github/workflows/security.yml
name: Security Scanning
on: [push, pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
– uses: actions/checkout@v2
– name: Run Brakeman
run: |
gem install brakeman
brakeman -f json -o brakeman-report.json
– name: Run bundler-audit
run: |
gem install bundler-audit
bundle audit check –update
– name: Check for secrets
run: |
gem install git-secrets
git secrets –scan
“`
Operational Maintenance
Log Management and Review
Implement centralized logging with proper retention:
“`ruby
config/initializers/logger.rb
if Rails.env.production?
# Send to centralized logging system
require ‘remote_syslog_logger’
Rails.logger = RemoteSyslogLogger.new(
ENV[‘SYSLOG_HOST’],
ENV[‘SYSLOG_PORT’],
program: “rails-#{Rails.env}”
)
# Separate security events log
SECURITY_LOGGER = RemoteSyslogLogger.new(
ENV[‘SYSLOG_HOST’],
ENV[‘SYSLOG_PORT’],
program: “rails-security”
)
end
“`
Dependency Management
Maintain secure dependencies per Requirement 6.2:
“`ruby
Gemfile
group :development do
gem ‘bundler-audit’
gem ‘brakeman’
end
Run weekly in CI/CD:
bundle audit check –update
brakeman -q -f html -o brakeman.html
“`
Change Management Process
Implement code review requirements for all CDE-affecting changes:
“`ruby
.github/PULL_REQUEST_TEMPLATE.md
Security Checklist
- [ ] No sensitive data in logs
- [ ] Input validation on all user inputs
- [ ] Authentication/authorization checks in place
- [ ] Database queries use parameterization
- [ ] Security tests written and passing
PCI Impact
- [ ] Changes affect cardholder data flow
- [ ] Changes affect authentication/authorization
- [ ] Changes affect logging/monitoring
- [ ] No impact on PCI compliance
“`
Troubleshooting
Common Implementation Issues
Session Fixation in Rails < 7:
“`ruby
config/initializers/session_store.rb
Rails.application.config.session_store :cookie_store,
key: ‘_app_session’,
secure: Rails.env.production?,
httponly: true,
same_site: :lax
“`
Mass Assignment Protection:
“`ruby
Always use strong parameters
def payment_params
params.require(:payment).permit(:amount, :currency)
# Never permit :card_number, :cvv directly
end
“`
CORS Configuration for API-Only Apps:
“`ruby
config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins ENV[‘ALLOWED_ORIGINS’].split(‘,’)
resource ‘/api/*’,
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options],
credentials: true,
max_age: 86400
end
end
“`
Performance Considerations
Encryption and logging add overhead. Optimize with:
- Asynchronous logging using Sidekiq/Resque
- Database connection pooling for encrypted queries
- CDN for static assets to reduce application load
- Caching strategies that don’t expose sensitive data
FAQ
Q: Can I store card data in my Rails application’s database?
A: Only if absolutely necessary and you’re validated for SAQ D compliance. The current standard requires any stored card data to be encrypted using industry-standard algorithms (AES-256), with keys managed separately from the