Rails 8 Credentials Management Guide

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:edit

This 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: false

Accessing 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
end

The 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_PASSWORD

Set 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
end

This 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
end

During 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/*.key

Avoid 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
end

Summary

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.

11 claps
← Back to Blog