Ruby on Rails Security Checklist for Heroku

Rails has many security features built-in - ensure they’re properly configured for Heroku deployment.

Expedited WAF for Rails

Expedited WAF adds network level request filtering, preventing malicious attacks from ever touching your application. This enables true defense in depth for your Rails app—even if your framework has a vulnerability or is improperly configured, Expedited WAF dynamically blocks threats before they reach your code.

From experience, we’ve seen many applications where attacks technically didn’t succeed, but burned through so many server resources that the application went down anyway. By filtering malicious requests at the network edge, Expedited WAF protects both your security and your availability.

Expedited WAF protects your Rails app on Heroku

New Security Features for Your Rails App

These capabilities aren’t available out of the box with Rails, but Expedited WAF adds them instantly:

Rails Core Security

  • Configure SECRET_KEY_BASE securely

    • Store in Heroku config: heroku config:set SECRET_KEY_BASE=$(rake secret)
    • Load from environment in config/secrets.yml or credentials
    • Never commit to version control
    • Rotate periodically
  • Enable force_ssl in production

    # config/environments/production.rb
    config.force_ssl = true
    config.ssl_options = {
      hsts: { expires: 1.year, subdomains: true, preload: true }
    }
    
  • Configure secure cookie settings

    # config/initializers/session_store.rb
    Rails.application.config.session_store :cookie_store,
      key: '_app_session',
      secure: Rails.env.production?,
      httponly: true,
      same_site: :lax,
      expire_after: 30.minutes  # Session timeout
    
  • Require re-authentication for sensitive operations

    • Password changes, email updates, payment actions should require current password
    • Use Devise’s :paranoid mode for additional protection
    • Consider time-based re-authentication for admin actions
  • Enable CSRF protection

    • Enabled by default - keep it!
    • Verify protect_from_forgery with: :exception in ApplicationController
    • Use form_authenticity_token in forms
    • Set config.action_controller.default_protect_from_forgery = true
  • Set allowed hosts

    # config/environments/production.rb
    config.hosts << "yourapp.herokuapp.com"
    config.hosts << "yourdomain.com"
    

Database Security

  • Use Rails 6+ encrypted credentials

    EDITOR=vim rails credentials:edit --environment production
    
    • Store database passwords, API keys
    • Deploy master.key via Heroku config
  • Configure Heroku Postgres properly

    # config/database.yml
    production:
      <<: *default
      url: <%= ENV['DATABASE_URL'] %>
      pool: <%= ENV['DB_POOL'] || 5 %>
      sslmode: require
    
  • Use parameterized queries

    • Rails ActiveRecord does this by default
    • Avoid raw SQL with string interpolation
    • Use where("name = ?", params[:name]) not where("name = '#{params[:name]}'")
  • Enable query logging carefully

    • Set config.active_record.log_level = :info (not :debug in production)
    • Don’t log sensitive data

Input & Output Protection

  • Avoid raw and .html_safe

    • Rails ERB escapes output by default - this is your primary XSS defense
    • Only use raw or .html_safe when absolutely necessary with sanitized content
    • Audit existing uses: grep -r "html_safe\|\.raw" app/
  • Sanitize user-generated HTML

    # When you must allow some HTML (comments, rich text)
    sanitize(user_content, tags: %w[p br strong em a ul ol li], attributes: %w[href])
    
    # Or use Rails::Html::SafeListSanitizer for more control
    Rails::Html::SafeListSanitizer.new.sanitize(content)
    
  • Validate and escape URL parameters

    • Never interpolate user input into redirects without validation
    • Use url_for or named routes instead of string concatenation

Defense in Depth: Even with proper Rails sanitization, Expedited WAF provides an additional layer of protection by filtering malicious HTML, JavaScript, and SQL injection attempts from form submissions at the network edge—before they ever reach your application code.

Authentication & Authorization

  • Use Devise for authentication

    • Configure secure password requirements
    • Enable account locking after failed attempts
    • Implement email confirmations
    • Use secure remember_me tokens
    • Consider argon2 gem as a modern alternative to bcrypt for password hashing
  • Implement authorization

    • Use Pundit or CanCanCan
    • Never rely on params for authorization
    • Always enforce authorization in controllers - checking permissions only in views is a critical security gap
    • Use authorize @resource in every controller action
  • Configure strong parameters

    def user_params
      params.require(:user).permit(:email, :name) # Never permit all
    end
    
  • Implement rate limiting

    • Use Rack::Attack
    # config/initializers/rack_attack.rb
    Rack::Attack.throttle('req/ip', limit: 300, period: 5.minutes) do |req|
      req.ip
    end
    
    Rack::Attack.throttle('logins/email', limit: 5, period: 20.seconds) do |req|
      req.params['email'] if req.path == '/login' && req.post?
    end
    

File Uploads & Assets

  • Use ActiveStorage with S3

    # config/storage.yml
    amazon:
      service: S3
      access_key_id: <%= ENV['AWS_ACCESS_KEY_ID'] %>
      secret_access_key: <%= ENV['AWS_SECRET_ACCESS_KEY'] %>
      region: us-east-1
      bucket: your-bucket
    
    # config/environments/production.rb
    config.active_storage.service = :amazon
    
  • Validate file uploads

    validates :avatar,
      content_type: ['image/png', 'image/jpg', 'image/jpeg'],
      size: { less_than: 5.megabytes }
    
  • Use signed URLs for file downloads

    # Generate expiring URLs - never expose permanent S3 links
    @document.file.url(expires_in: 5.minutes)
    
    # For sensitive files, add disposition to force download
    @document.file.url(expires_in: 5.minutes, disposition: 'attachment')
    
  • Never execute uploaded content

    • Store uploads outside the application directory (S3, not /public)
    • Never serve uploaded files with executable content types
    • Strip metadata and re-encode images to remove embedded scripts
  • Configure asset pipeline security

    # config/environments/production.rb
    config.public_file_server.enabled = false  # Let CDN/nginx serve
    config.assets.compile = false
    config.assets.digest = true
    

Security Headers

  • Configure security headers

    # config/initializers/content_security_policy.rb
    Rails.application.config.content_security_policy do |policy|
      policy.default_src :self
      policy.script_src  :self
      policy.style_src   :self
    end
    
    # config/initializers/permissions_policy.rb
    Rails.application.config.permissions_policy do |f|
      f.camera      :none
      f.gyroscope   :none
      f.microphone  :none
    end
    
  • Set X-Frame-Options

    • Enabled by default: config.action_dispatch.default_headers['X-Frame-Options'] = 'SAMEORIGIN'

Dependency Management

  • Keep Rails and gems updated

    bundle update rails
    bundle update
    bundle audit check --update
    
  • Use bundle audit

    gem install bundler-audit
    bundle audit
    
    • Add to CI/CD pipeline
    • Fail builds on critical vulnerabilities - don’t let vulnerable code deploy
    • Fix vulnerabilities promptly
  • Run security scans in CI

    # .github/workflows/security.yml
    - name: Security Audit
      run: |
        bundle audit check --update
        brakeman -q --no-summary --exit-on-warn
    
  • Pin gem versions

    • Use Gemfile.lock
    • Review dependencies before updating

Environment & Configuration

  • Store secrets in Heroku config

    heroku config:set SECRET_KEY_BASE='...'
    heroku config:set DATABASE_URL='...'
    heroku config:set RAILS_MASTER_KEY='...'
    
  • Configure production environment properly

    # config/environments/production.rb
    config.log_level = :info
    config.consider_all_requests_local = false
    config.action_controller.perform_caching = true
    config.cache_classes = true
    config.eager_load = true
    
  • Never commit secrets

    • Add config/master.key to .gitignore
    • Use encrypted credentials for sensitive data

Logging & Monitoring

  • Configure production logging

    # config/environments/production.rb
    config.log_level = :info
    config.log_tags = [:request_id]
    config.logger = ActiveSupport::Logger.new(STDOUT)
    
  • Don’t log sensitive parameters

    # config/initializers/filter_parameter_logging.rb
    Rails.application.config.filter_parameters += [
      :password, :password_confirmation, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv
    ]
    
  • Set up error tracking

    • Use Sentry, Rollbar, or Honeybadger
    • Configure in production only
  • Log authentication failures and suspicious activity

    # In your sessions controller or Devise config
    def create
      # ... authentication logic
    rescue AuthenticationError => e
      Rails.logger.warn "[SECURITY] Failed login attempt: #{request.remote_ip} - #{params[:email]}"
      # Track patterns: multiple failures from same IP or targeting same account
    end
    
    • Monitor for credential stuffing patterns
    • Alert on unusual login locations or times

Heroku Deployment

  • Use Puma web server

    # config/puma.rb
    workers Integer(ENV['WEB_CONCURRENCY'] || 2)
    threads_count = Integer(ENV['RAILS_MAX_THREADS'] || 5)
    threads threads_count, threads_count
    
    preload_app!
    
    on_worker_boot do
      ActiveRecord::Base.establish_connection
    end
    
    # Procfile
    web: bundle exec puma -C config/puma.rb
    
  • Specify Ruby version

    # Gemfile
    ruby '3.2.2'
    
  • Configure buildpacks if needed

    heroku buildpacks:add heroku/ruby
    heroku buildpacks:add heroku/nodejs  # If using Webpacker
    

API Security (Rails API mode)

  • Use Rails API mode securely

    class ApplicationController < ActionController::API
      include ActionController::HttpAuthentication::Token::ControllerMethods
    
      before_action :authenticate
    
      private
    
      def authenticate
        authenticate_or_request_with_http_token do |token, options|
          ActiveSupport::SecurityUtils.secure_compare(token, ENV['API_TOKEN'])
        end
      end
    end
    
  • Implement CORS properly

    # Gemfile
    gem 'rack-cors'
    
    # config/initializers/cors.rb
    Rails.application.config.middleware.insert_before 0, Rack::Cors do
      allow do
        origins 'yourdomain.com'
        resource '/api/*', headers: :any, methods: [:get, :post]
      end
    end
    
  • Use JWTs for API authentication

    • gem ‘jwt’
    • Implement token expiration
    • Refresh tokens securely

Testing

  • Write security tests

    • Test authentication/authorization
    • Test CSRF protection
    • Test input validation
  • Test on Heroku staging

    heroku create myapp-staging
    git push staging main
    

Pre-Deployment Checklist

  • Run brakeman security scanner

    gem install brakeman
    brakeman
    
  • Review Rails security guide

Additional Resources

Get Started

Install Expedited WAF Book a Security Review