Every Rails application needs secrets: API keys, database passwords, encryption keys, and service tokens. Rails 8 credentials provide a built-in, encrypted solution that eliminates the need for external secrets managers while keeping sensitive data secure in version control.
The Problem with Environment Variables
Environment variables work, but they create operational headaches. Teams share .env.example files that drift out of sync. New developers spend hours tracking down missing keys. Production deployments fail because someone forgot to set STRIPE_SECRET_KEY. Rails credentials solve this by encrypting secrets into a single file that lives in the repository.
Understanding the Credentials System
Rails 8 credentials consist of two files per environment: an encrypted YAML file and a master key. The encrypted file (credentials.yml.enc) is safe to commit. The master key (master.key) stays out of version control and unlocks the secrets.
The system supports environment-specific credentials, allowing different secrets for development, staging, and production while sharing common structure and workflow.
Setting Up Environment-Specific Credentials
To create credentials for a specific environment, use the credentials command with the environment flag:
# Terminal commands for each environment
RAILS_ENV=development bin/rails credentials:edit
RAILS_ENV=staging bin/rails credentials:edit
RAILS_ENV=production bin/rails credentials:editThis creates separate encrypted files and keys for each environment:
# config/credentials/development.yml.enc (encrypted)
# config/credentials/development.key (DO NOT COMMIT)
# config/credentials/staging.yml.enc (encrypted)
# config/credentials/staging.key (DO NOT COMMIT)
# config/credentials/production.yml.enc (encrypted)
# config/credentials/production.key (DO NOT COMMIT)Structure credentials with nested keys for organization:
# Decrypted view of config/credentials/production.yml.enc
# Database
mysql:
password: super_secret_production_password
replica_password: replica_password_here
# External Services
stripe:
secret_key: sk_live_abc123
webhook_secret: whsec_xyz789
publishable_key: pk_live_def456
aws:
access_key_id: AKIAIOSFODNN7EXAMPLE
secret_access_key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
region: us-east-1
bucket: myapp-production
# Application Secrets
secret_key_base: a1b2c3d4e5f6...
# Feature Flags
features:
new_checkout: true
beta_api: falseAccessing Credentials in Code
Rails provides a clean interface for reading credentials throughout the application:
# app/services/payment_processor.rb
class PaymentProcessor
def initialize
@client = Stripe::Client.new(
api_key: Rails.application.credentials.stripe[:secret_key]
)
end
def process(amount:, token:)
@client.charges.create(
amount: amount,
currency: 'usd',
source: token
)
end
end
# app/models/document.rb
class Document < ApplicationRecord
has_one_attached :file
def storage_config
{
access_key_id: Rails.application.credentials.dig(:aws, :access_key_id),
secret_access_key: Rails.application.credentials.dig(:aws, :secret_access_key),
region: Rails.application.credentials.dig(:aws, :region)
}
end
endThe dig method safely navigates nested credentials, returning nil if any key in the chain is missing. For required credentials, access them directly to get a clear error when missing:
# config/initializers/stripe.rb
Stripe.api_key = Rails.application.credentials.stripe.fetch(:secret_key)
# Raises KeyError if stripe.secret_key is missing
# "key not found: :secret_key"Database Configuration with Credentials
Integrate credentials directly into database configuration for secure password management:
# config/database.yml
default: &default
adapter: mysql2
encoding: utf8mb4
collation: utf8mb4_unicode_ci
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
username: <%= Rails.application.credentials.dig(:mysql, :username) || 'root' %>
password: <%= Rails.application.credentials.dig(:mysql, :password) %>
development:
<<: *default
database: myapp_development
production:
<<: *default
database: myapp_production
host: <%= Rails.application.credentials.dig(:mysql, :host) %>
password: <%= Rails.application.credentials.mysql![:password] %>The bang method (mysql!) raises an error if the key is missing, preventing silent failures in production.
Deploying with Kamal
Kamal needs the master key to decrypt credentials on production servers. Add it to the encrypted secrets in the deploy configuration:
# config/deploy.yml
service: myapp
image: myapp/web
servers:
web:
hosts:
- 192.168.1.1
labels:
traefik.http.routers.myapp.rule: Host(`myapp.com`)
env:
clear:
RAILS_ENV: production
RAILS_LOG_TO_STDOUT: true
secret:
- RAILS_MASTER_KEY
registry:
server: ghcr.io
username: myapp
password:
- KAMAL_REGISTRY_PASSWORDSet the master key using Kamal's secrets management:
# Terminal
kamal env push RAILS_MASTER_KEY=$(cat config/credentials/production.key)Alternatively, set RAILS_MASTER_KEY as an environment variable on the server. Rails automatically uses this to decrypt production credentials.
Validating Credentials on Boot
Catch missing credentials early by validating them during application initialization:
# config/initializers/credentials_validator.rb
Rails.application.config.after_initialize do
required_credentials = [
[:secret_key_base],
[:mysql, :password],
[:stripe, :secret_key],
[:aws, :access_key_id],
[:aws, :secret_access_key]
]
missing = required_credentials.reject do |keys|
Rails.application.credentials.dig(*keys).present?
end
if missing.any? && !Rails.env.test?
raise <<~ERROR
Missing required credentials:
#{missing.map { |keys| " - #{keys.join('.')}" }.join("\n")}
Run: RAILS_ENV=#{Rails.env} bin/rails credentials:edit
ERROR
end
endThis initializer halts boot with a clear error message listing exactly which credentials need attention.
Rotating Credentials Safely
When a key is compromised or during routine rotation, update credentials without downtime:
# app/services/credential_rotator.rb
class CredentialRotator
def self.stripe_key_valid?(key)
client = Stripe::Client.new(api_key: key)
client.accounts.retrieve
true
rescue Stripe::AuthenticationError
false
end
def self.current_stripe_key
primary = Rails.application.credentials.dig(:stripe, :secret_key)
fallback = Rails.application.credentials.dig(:stripe, :secret_key_previous)
return primary if stripe_key_valid?(primary)
return fallback if fallback && stripe_key_valid?(fallback)
raise "No valid Stripe key available"
end
endDuring rotation, add the new key as primary and keep the old key as a fallback. Once all systems use the new key, remove the fallback in a subsequent deploy.
Common Mistakes to Avoid
Never commit master keys to version control. Add them to .gitignore immediately:
# .gitignore
config/master.key
config/credentials/*.keyAvoid accessing credentials in class bodies, as this runs during boot before credentials load:
# Bad - runs at class load time
class PaymentService
API_KEY = Rails.application.credentials.stripe[:secret_key]
end
# Good - runs when method is called
class PaymentService
def api_key
Rails.application.credentials.stripe[:secret_key]
end
endSummary
Rails 8 credentials provide encrypted, version-controlled secrets management without external dependencies. Environment-specific files keep development and production secrets separate. Kamal deployment requires only setting RAILS_MASTER_KEY. Validate credentials on boot to catch configuration errors before they cause runtime failures. With proper setup, credentials eliminate entire categories of deployment bugs while keeping sensitive data secure.