Manage Secrets and Config in Ruby Apps

Khimananda Oli 8 min read Programming and Languages
Manage Secrets and Config in Ruby Apps

By Khimananda Oli | Last reviewed: August 2026

Failing to properly manage secrets and config in Ruby apps is one of the most common causes of security breaches and compliance failures I see during audits. While Rails provides excellent built-in tooling like encrypted credentials, many teams still accidentally commit API keys or rely on fragile environment variable injection in production. This guide establishes a secure, scalable hierarchy for handling configuration that satisfies both developer ergonomics and SOC 2 evidence requirements.

Secrets Hierarchy: Local → Staging → ProductionLocal Development• .env (gitignored)• config/credentials/dev.yml.encDev-only dummy valuesStaging / CI• CI/CD Secret Store• ENV injection at runtimeEphemeral test credentialsProduction• Vault / AWS Secrets Mgr• Encrypted Credentials (read-only)Dynamic, rotated, auditedSecurity & Compliance LayerAudit Logs • Rotation Policies • Least Privilege IAM • Git History ScanningAll tiers must enforce encryption-at-rest and prevent plaintext leakageAligns with SOC 2 CC6.1, ISO 27001 A.8.24
Recommended hierarchy to manage secrets and config in Ruby apps across environments while maintaining audit readiness.

How do you manage secrets and config in Ruby apps using encrypted credentials?

Rails Encrypted Credentials remain the gold standard for managing application-level secrets that need to be version-controlled alongside your code. Unlike plain YAML files, these are AES-256-GCM encrypted and can only be decrypted with a master key. This approach solves the "works on my machine" problem while keeping sensitive data out of git history.

Setting up environment-specific credentials

In modern Rails (7+), you should use per-environment credentials rather than a single monolithic file. This limits the blast radius if a specific environment's master key is compromised and allows different team members to access only what they need.

# Generate production-specific credentials
EDITOR=vim bin/rails credentials:edit --environment production

# The structure inside config/credentials/production.yml.enc
aws:
  access_key_id: AKIAIOSFODNN7EXAMPLE
  secret_access_key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
stripe:
  api_key: sk_live_51HG4...
database:
  replica_password: super_secure_replica_pass

Access these values in your application code safely. Always provide fallbacks or explicit error handling to prevent cryptic nil errors during deployment.

# Safe access pattern with validation
class PaymentService
  def initialize
    @api_key = Rails.application.credentials.dig(:stripe, :api_key)
    raise MissingCredentialError, "Stripe API key missing" unless @api_key.present?
  end
end

A common mistake I encounter during secrets management reviews is teams storing the master key in the same repository as the encrypted file. The master key must live in a separate secure location—typically a secrets manager or distributed via a secure channel to authorized developers only.

When should you use environment variables versus encrypted files?

The decision between environment variables and encrypted credentials isn't binary; it's contextual. Environment variables excel at platform-specific configuration that changes between deployments without code changes, while encrypted files suit application secrets that travel with the codebase.

CriteriaEnvironment VariablesEncrypted CredentialsExternal Vault
Best ForPlatform config (DB host, port, region)App secrets (API keys, signing tokens)High-value secrets, dynamic creds, PII
Version ControlNo (injected at runtime)Yes (encrypted blob committed)No (fetched at runtime)
Rotation EaseRequires redeploy/restartRequires redeploy after re-encryptionTransparent, no app restart needed
Audit TrailPlatform-dependentGit log shows who editedFull access logging, policy enforcement
SOC 2 EvidenceWeak without external loggingModerate (git-based)Strong (native audit logs)
Developer FrictionLow (.env files locally)Medium (requires master key setup)Higher (Vault auth, token mgmt)

In practice, I recommend a hybrid approach. Use environment variables for infrastructure endpoints and non-sensitive toggles. Use encrypted credentials for third-party API keys that don't require frequent rotation. Reserve external vaults for database passwords, TLS certificates, and any secret that requires automatic rotation or fine-grained access policies.

How do you integrate HashiCorp Vault with Ruby applications?

For organizations requiring SOC 2 compliance or handling regulated data, an external secrets manager like HashiCorp Vault becomes necessary. Vault provides dynamic secrets, leasing, and detailed audit trails that flat files simply cannot offer. Integration with Ruby is straightforward using the official vault gem.

Dynamic Secret Lifecycle: Ruby ↔ VaultRuby AppVault ServerPostgreSQL1. Auth (AppRole/JWT)2. Client Token + Lease ID3. Create DB User + Pass4. Return Dynamic Creds5. Deliver Creds to App6. Renew Lease (before TTL)7. New Lease Duration8. Revoke User on Lease Expire
Dynamic secrets flow: Ruby authenticates, Vault generates ephemeral database credentials with automatic expiration and renewal.

Implementing the Vault client in Rails

Create a dedicated service object to handle Vault interactions. This isolates the complexity of authentication, lease management, and error handling from your business logic.

# app/services/vault_client.rb
class VaultClient
  def self.database_credentials(role: "app-readonly")
    vault = Vault::Client.new(
      address: ENV.fetch("VAULT_ADDR"),
      token: fetch_approle_token
    )
    
    secret = vault.logical.read("database/creds/#{role}")
    raise VaultError, "Failed to fetch DB creds" unless secret
    
    {
      username: secret.data[:username],
      password: secret.data[:password],
      lease_id: secret.lease_id,
      ttl: secret.lease_duration
    }
  rescue Vault::HTTPConnectionError => e
    Rails.logger.error("Vault connection failed: #{e.message}")
    raise
  end
  
  private_class_method def self.fetch_approle_token
    # Authenticate via AppRole and cache token in memory
    # Never store this token on disk
  end
end

For production resilience, implement lease renewal in a background thread or Sidekiq job. When a lease expires without renewal, Vault automatically revokes the credential at the database level, preventing unauthorized access even if the application is compromised.

What are the security risks of using dotenv in production Ruby apps?

The dotenv gem is ubiquitous in Ruby development, but using it in production introduces significant risk. While convenient for local development, relying on .env files in production violates core principles of secure configuration management and creates audit blind spots.

  • File permission leakage: .env files often inherit default umask permissions, making them readable by other users or processes on shared hosting environments.
  • No encryption at rest: Secrets sit in plaintext on disk. Any backup, snapshot, or forensic image captures them in cleartext.
  • Absent audit trail: There is no native mechanism to track who read or modified a .env file, making SOC 2 evidence collection impossible.
  • Deployment coupling: Updating a secret requires redeploying or restarting the application, increasing change failure rate and rollback complexity.
  • Git history contamination: Even if currently gitignored, accidental commits happen. Once in history, secrets remain exposed indefinitely unless rewritten.

If you must use environment variables in production, inject them through your platform's native mechanism (Kubernetes Secrets, AWS Parameter Store, systemd EnvironmentFile with restricted permissions) rather than loading from a file. For guidance on safe pipeline handling, review handling secrets in CI/CD pipelines safely.

How do you rotate secrets without downtime in Ruby applications?

Secret rotation is where theory meets operational reality. Many teams avoid rotation because they fear downtime, but unrotated secrets are a ticking clock for breach impact. The key is designing your application to support graceful credential transitions.

Implementing dual-credential support

Before rotating any secret, ensure your application can accept both old and new values simultaneously. This eliminates the coordination problem between deployment and secret update.

# config/initializers/stripe.rb
# Support both current and previous key during rotation window
STRIPE_KEYS = [
  Rails.application.credentials.dig(:stripe, :api_key),
  Rails.application.credentials.dig(:stripe, :api_key_previous)
].compact

# Webhook signature verification accepts either key
Stripe::Webhook::Signature.verify_header(
  payload,
  sig_header,
  STRIPE_KEYS,
  tolerance: Stripe::Webhook::DEFAULT_TOLERANCE
)

For database credentials managed by Vault, rotation is automatic and transparent. The application requests a new lease before the old one expires, and Vault handles the user creation and revocation at the database level. Your Ruby code never needs to know the actual password—it only manages the lease lifecycle.

Establish a rotation schedule aligned with your compliance framework. SOC 2 typically expects quarterly rotation for high-privilege credentials, while ISO 27001 focuses more on event-driven rotation after personnel changes or suspected compromise. Document this policy and automate evidence collection to satisfy auditors without manual effort.

Secure Configuration Management Next Steps

To effectively manage secrets and config in Ruby apps, start by auditing your current state: scan git history for leaked credentials, inventory all environment variables, and classify secrets by sensitivity tier. Implement encrypted credentials immediately for application secrets, migrate production infrastructure secrets to a vault within 90 days, and establish automated rotation for all high-value credentials. If your team needs help designing a compliant secrets architecture or preparing for an upcoming audit, reach out to discuss your specific requirements. Secure configuration isn't a feature you add later—it's the foundation your production reliability depends on.

Frequently Asked Questions

Use environment variables via dotenv-rails for local development and a dedicated secrets manager like AWS Secrets Manager or Vault for production. Never commit credentials to version control.

Add dotenv-rails to your Gemfile and create a .env file. Variables load automatically in development and test environments without extra configuration.

Encrypted credentials work well for team-shared config, but environment variables remain standard for cloud-native deployments and containerized infrastructure in 2026.

Implement dual-read logic supporting both old and new secret values simultaneously. Deploy the update, verify functionality, then remove legacy secret references in a follow-up release.

The config gem provides structured, environment-aware settings with validation. It supports YAML files, environment variable overrides, and nested configuration namespaces effectively.

Configure Rails filtered_parameters to mask sensitive fields. Use structured logging libraries that automatically redact values matching secret key patterns before output.

Yes, the vault-ruby gem integrates HashiCorp Vault directly. Authenticate via AppRole or Kubernetes service accounts and fetch secrets dynamically at application boot time.

Use the config gem's validation feature or write custom initializers that raise errors immediately if critical environment variables are missing or malformed.

Dotenv loads .env files simply while figaro enforces required keys and generates application.yml. Dotenv sees wider adoption and better Rails 7+ compatibility currently.

Use separate secret stores per environment with distinct access policies. Never reuse production credentials in staging; instead replicate structure with different values.

Yes, environment variables load at process boot. Restart Puma or Unicorn workers to pick up changes unless using a dynamic secrets client.

Define secrets in docker-compose.yml using the secrets key or pass env_file references. Mount secret files as read-only volumes for sensitive certificates.

Set file permissions to 600 so only the owner can read it. Add .env to .gitignore immediately upon creation to prevent accidental commits.

Run ENV.fetch('KEY_NAME') in Rails console to test presence. Check dotenv loading order and verify variable spelling matches exactly including case sensitivity.

No, Redis lacks encryption at rest by default and exposes data through CLI tools. Use purpose-built secrets managers instead of caching layers for credential storage.