Ruby on Rails PCI Compliance

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

Leave a Comment

icon 1,650 PCI scans performed this month
check icon Business in Austin, TX completed their PCI SAQ A-EP