The Complete Guide to Rails Credentials and Secrets Management
Managing secrets well is a common hallmark of production grade Rails applications. Get it wrong and you're one misplaced commit away from exposing API keys, database passwords, or encryption tokens. Get it right and your team can deploy confidently, rotate credentials with minimal disruption, and be better prepared for security audits.
I've worked with Rails credentials across dozens of production systems, from startups shipping their first feature to enterprise teams managing multi-environment deployments across AWS and GCP. This guide covers everything I've learned about how Rails handles secrets, the mistakes I see teams make repeatedly, and the practices that actually hold up under pressure.
How Rails Credentials Work
Rails introduced encrypted credentials in Rails 5.2 as the successor to the older secrets.yml and encrypted secrets workflows. The concept is straightforward: your secrets live in an encrypted YAML file that's safe to commit to version control. Only someone with the master key can decrypt it.
As of Rails 8.1.2 (released January 2026), the credentials system has matured significantly. Rails 8.0 removed the legacy config.read_encrypted_secrets toggle, making encrypted credentials the standard built-in encrypted secrets mechanism in Rails, typically combined with environment variables for infrastructure-level configuration.
The basics
When you run rails new, Rails generates two files:
config/credentials.yml.enc, your encrypted secrets fileconfig/master.key, the key used to encrypt and decrypt it
The .enc file is safe to commit. The .key file is not. Rails adds config/master.key to .gitignore automatically.
To edit your credentials:
EDITOR="code --wait" bin/rails credentials:edit
Rails opens the decrypted credentials for editing (often via a temporary plaintext file) and re-encrypts when you save and close. Treat any plaintext artifacts, temporary files, editor swap files, clipboard history, as sensitive, especially on shared machines.
Inside, you'll find standard YAML:
# config/credentials.yml.enc (decrypted)
secret_key_base: 14b6462b8dc4366e1266baab0...
aws:
access_key_id: AKIAIOSFODNN7EXAMPLE
secret_access_key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
stripe:
publishable_key: pk_live_...
secret_key: sk_live_...
Access them in your application code:
Rails.application.credentials.aws[:access_key_id]
Rails.application.credentials.dig(:stripe, :secret_key)
Per-environment credentials
Since Rails 6, you can create environment-specific credential files. This is critical for any team running separate staging and production configurations.
# Create production-specific credentials
EDITOR="code --wait" bin/rails credentials:edit --environment production
# Create staging-specific credentials
EDITOR="code --wait" bin/rails credentials:edit --environment staging
This generates:
config/credentials/production.yml.enc+config/credentials/production.keyconfig/credentials/staging.yml.enc+config/credentials/staging.key
By default, Rails uses config/credentials/#{Rails.env}.yml.enc if it exists; otherwise it falls back to the global config/credentials.yml.enc. This means you can keep shared configuration in the global file and override per environment where needed.
What's coming in Rails 8.2: combined credentials
There is ongoing work on Rails edge toward a unified credentials API, often discussed as Rails.app.creds. Until Rails 8.2 ships as stable, treat the names and behaviour below as provisional, link to the official API docs once available. The intent is to check ENV first, then fall back to encrypted credentials, eliminating one of the most common pain points, the boilerplate of checking both sources:
# Before (Rails 8.1 and earlier)
api_key = ENV["STRIPE_API_KEY"] || Rails.application.credentials.dig(:stripe, :api_key)
# After (Rails 8.2, API subject to change)
api_key = Rails.app.creds.require(:stripe_api_key)
The require method raises if the value is missing from both sources. There's also option for values with defaults:
redis_url = Rails.app.creds.option(:redis_url, default: "redis://localhost:6379")
Under the hood, Rails.app.creds builds on a chained configuration approach (ActiveSupport::CombinedConfiguration), which checks multiple credential backends in order. This may be extensible to external secret managers:
# config/initializers/credentials.rb (example, verify against final Rails 8.2 API)
Rails.app.creds = ActiveSupport::CombinedConfiguration.new(
Rails.app.envs, # Check ENV first
VaultConfiguration.new, # Then HashiCorp Vault
Rails.app.credentials # Finally, encrypted credentials
)
This is a significant step forward for teams that need to bridge between environment variables and encrypted credentials, especially during migrations or when working with managed infrastructure that injects secrets via ENV.
Common Mistakes (and How to Avoid Them)
After reviewing and auditing credentials setups for teams of all sizes, these are the mistakes I encounter most frequently.
1. Committing the master key
This happens more often than you'd expect. A developer removes config/master.key from .gitignore, commits, pushes, and now your encryption key is in your Git history, even if you immediately revert.
The fix: If this happens, treat the key as compromised. Generate new credentials with a new master key and rotate every secret that was in the old file. Rails credentials use AES-128-GCM (as defined in ActiveSupport::EncryptedFile), it's not getting brute-forced, but an exposed key means all your encrypted secrets are readable. Check your Git history for any exposure, and consider using tools like git-secrets or trufflehog to scan for accidental leaks in CI.
2. Sharing keys over insecure channels
Slack messages, emails, and shared Google Docs are not appropriate channels for master keys. Once a key hits a third-party service, you've lost control of who can access it and whether it's been logged or cached.
The fix: Use a password manager that supports team sharing, 1Password, Bitwarden, or your organisation's vault solution. Share the key there, and document in your project's README exactly where team members can find it.
3. Losing the master key entirely
If you lose config/master.key and don't have RAILS_MASTER_KEY set anywhere, your encrypted credentials are permanently inaccessible. There is no recovery mechanism. Rails credentials use AES-128-GCM authenticated encryption, without the key, the file cannot be decrypted in practice.
The fix: Store your master key in at least two secure locations: your team's password manager and your deployment infrastructure's secret store. Document both locations.
4. Using the same credentials across all environments
Running production API keys in development and test environments increases your attack surface unnecessarily and makes it easy to accidentally trigger production side effects during testing.
The fix: Use per-environment credential files. Keep development and test credentials pointing to sandbox or mock services. Production credentials should only exist in the production-specific file.
5. Not understanding the decryption key resolution
Rails gets the decryption key from ENV['RAILS_MASTER_KEY'] if set. Otherwise, it reads from config.credentials.key_path (by default config/credentials/#{Rails.env}.key if present, else config/master.key).
If you're using per-environment credentials but only setting RAILS_MASTER_KEY, Rails will try to decrypt the environment-specific file with that key. If the keys don't match, you'll get a decryption error that can be confusing to debug.
The fix: Be explicit about which key maps to which file. In CI, set environment-specific variables like RAILS_MASTER_KEY to the correct key for the environment you're testing or deploying.
Integrating With AWS and GCP Secrets Managers
Rails credentials work well for many teams, but as applications scale, you may need to integrate with external secret managers for compliance, auditing, or infrastructure reasons.
AWS Secrets Manager
A common pattern is to fetch secrets from AWS Secrets Manager at boot time and merge them into your application configuration:
# config/initializers/aws_secrets.rb
if Rails.env.production?
require "aws-sdk-secretsmanager"
client = Aws::SecretsManager::Client.new(region: "eu-west-2")
begin
response = client.get_secret_value(secret_id: "myapp/production")
secrets = JSON.parse(response.secret_string)
# Make secrets available via ENV-like access
secrets.each { |key, value| ENV[key] ||= value }
rescue Aws::SecretsManager::Errors::ServiceError => e
Rails.logger.error("Failed to fetch secrets: #{e.message}")
raise "Cannot start without secrets" # Fail closed
end
end
Production considerations: Add timeouts, retries, and fail-closed behaviour. Avoid overwriting ENV values that are already set. For high-traffic applications, consider caching fetched secrets rather than hitting the API on every boot, and prefer a dedicated config object over mutating ENV in multi-threaded contexts.
If you adopt Rails 8.2's combined credentials when it ships, you may be able to plug AWS directly into the credential backend chain without changing application code.
GCP Secret Manager
The approach with Google Cloud is similar:
# config/initializers/gcp_secrets.rb
if Rails.env.production?
require "google/cloud/secret_manager"
client = Google::Cloud::SecretManager.secret_manager_service
project_id = "your-project-id"
begin
secret_name = "projects/#{project_id}/secrets/myapp-production/versions/latest"
response = client.access_secret_version(name: secret_name)
secrets = JSON.parse(response.payload.data)
secrets.each { |key, value| ENV[key] ||= value }
rescue Google::Cloud::Error => e
Rails.logger.error("Failed to fetch GCP secrets: #{e.message}")
raise "Cannot start without secrets"
end
end
When to use external managers vs Rails credentials
Rails credentials are sufficient for most small to mid-sized teams. Consider external secret managers when you need automatic rotation policies, granular access audit logging, compliance with SOC2/PCI DSS/HIPAA requirements around secret access, or when multiple services (not just your Rails app) need access to the same secrets.
The two approaches are not mutually exclusive. Many teams use Rails credentials for application-specific configuration and an external manager for shared infrastructure secrets like database passwords and third-party API keys.
CI/CD Workflows for Secrets
Your CI/CD pipeline needs access to your secrets to run tests and deploy, but how you provide that access matters enormously.
GitHub Actions
Store your master key as a GitHub Actions secret and inject it as an environment variable:
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
env:
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
RAILS_ENV: production
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4"
bundler-cache: true
- name: Run tests
run: bin/rails test
- name: Deploy
run: bin/kamal deploy
Kamal 2 secrets management
If you're deploying with Kamal 2 (the default deployment tool for Rails 8), secrets are managed through the .kamal/secrets file. This replaced the older .env approach and is typically committed to version control. It's designed to contain references to secrets sourced from your environment or external vaults, but avoid hardcoding literal secret values there:
# .kamal/secrets
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
RAILS_MASTER_KEY=$(cat config/master.key)
DB_PASSWORD=$DB_PASSWORD
Kamal also supports fetching secrets directly from password managers:
# .kamal/secrets (using 1Password)
SECRETS=$(kamal secrets fetch --adapter 1password --account myaccount --from MyVault/MyApp RAILS_MASTER_KEY DB_PASSWORD)
RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY $SECRETS)
DB_PASSWORD=$(kamal secrets extract DB_PASSWORD $SECRETS)
Kamal includes built-in helpers for fetching secrets from common password managers (currently 1Password, LastPass, and Bitwarden). For other backends like AWS Secrets Manager or Doppler, teams often use custom scripts or community adapters, verify against the version of Kamal you're running. In typical setups, secrets are resolved as part of the deploy workflow rather than a separate manual push step, but verify your Kamal version and deploy pipeline.
One important detail: secrets in .kamal/secrets are injected as environment variables into your Docker containers. Anyone with root or privileged access to your server, or container exec permissions, can read them via env or printenv inside the container. This is standard for containerised deployments, but it's worth understanding when designing your security model.
CircleCI, GitLab CI, and others
The pattern is the same across CI providers: store the master key in the platform's secret management, inject it as RAILS_MASTER_KEY, and ensure it's never logged or exposed in build output.
Key rules for CI/CD secrets:
- Never echo or print secret values in build logs
- Use the CI platform's built-in secret masking where available
- Rotate keys if a build log containing secrets is ever exposed
- Restrict who can modify CI secrets to the same group that manages production access
Security Best Practices
Rotate regularly
Treat your master key like a password. Rotate it periodically, at minimum when someone with access leaves the team. The process is straightforward:
- Decrypt your current credentials and copy the plaintext (be mindful of editor swap files and clipboard managers)
- Delete the old
.encand.keyfiles - Run
bin/rails credentials:editto generate new files with a new key - Paste your secrets back in and close the editor
- Distribute the new key through your secure channel
- Update CI/CD and deployment configurations
- Clear your clipboard history if applicable
Audit access
Know who has access to your master key at all times. Maintain a list, review it quarterly, and revoke access when it's no longer needed.
Use config.require_master_key
In your production environment config, ensure Rails fails loudly if the master key is missing rather than silently falling back:
# config/environments/production.rb
config.require_master_key = true
This prevents your application from booting without credentials, which is far preferable to discovering the problem at runtime when a feature that depends on a missing API key fails.
Filter credentials from logs
Rails ships with a default filter list in the generated filter_parameter_logging.rb initializer (see the Rails configuration guide for details). Extend it for your application's specific secrets:
# config/initializers/filter_parameter_logging.rb
Rails.application.config.filter_parameters += [
:api_key, :private_key, :webhook_secret
]
Don't store secrets in Docker images
If you're building Docker images for deployment, ensure your Dockerfile doesn't copy master keys or credential keys into the image. In modern Rails-generated Docker setups (Rails 7.1+), asset precompilation uses SECRET_KEY_BASE_DUMMY=1 to avoid needing production secrets at build time, but custom Dockerfiles may not include this.
Check your .dockerignore:
config/master.key
config/credentials/*.key
.kamal/secrets
Consider encryption at rest for your database
Rails credentials protect secrets in your codebase, but once they're loaded into memory and used, for example, to connect to your database, the connection itself needs to be secure. Use SSL/TLS for database connections, encrypt sensitive columns with Active Record Encryption (introduced in Rails 7.0), and ensure your infrastructure encrypts data at rest.
Version Compatibility Quick Reference
| Rails Version | Credentials Support | Notes |
|---|---|---|
| 5.2 | Introduced encrypted credentials (global) | secrets.yml still supported alongside |
| 6.0 – 7.x | Multi-environment credentials | Per-env .yml.enc files via --environment flag |
| 8.0 | Multi-environment credentials | read_encrypted_secrets removed |
| 8.1.2 (current) | Multi-environment credentials | Stable, production-ready |
| 8.2 (edge, provisional) | Combined credentials API | ENV + credentials unified lookup (not yet released) |
Ruby 4.0 was released on December 25, 2025. Rails 8.0 and 8.1 require Ruby 3.2 or newer. Rails credentials rely on Active Support's encryption features and should work on supported Ruby versions, however, there are known compatibility discussions around Ruby 4.0 (see rails/rails#56457), so confirm your Rails version's Ruby support policy before standardising on Ruby 4.0 in production. Ruby 3.4 is widely used with Rails 8.x and is a conservative choice if you want to avoid Ruby 4.0's newness.
Final Thoughts
Rails credentials are not complicated, but they do require intentionality. The encryption is solid (AES-128-GCM via ActiveSupport::EncryptedFile), the tooling is mature, and with Kamal 2's secrets management and the upcoming Rails.app.creds in 8.2, the ecosystem continues to get better at making secure practices the path of least resistance.
In practice, many production teams use a combination: Rails credentials for values they want versioned alongside the codebase, and environment variables (or an external secret manager) for infrastructure-level secrets. Neither approach is exclusive, the key is choosing deliberately and documenting your team's conventions.
The most common problems I see aren't technical, they're organisational. Teams that struggle with credentials typically lack a clear policy for who has access, how keys are shared, and what happens when someone leaves. Solve those problems first, and the technical implementation follows naturally.
If your team is running a production Rails application and you're not confident in your secrets management, or you're preparing for a security audit and need your credentials setup reviewed, I can help.
Need a credentials and security audit for your Rails application?
I help teams review, modernise, and secure their Rails credentials and deployment pipelines, without disrupting production. Whether you're migrating from secrets.yml, integrating with AWS/GCP, or setting up Kamal 2 for the first time, I can get your secrets management to a place where your team can deploy with confidence.