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.
New Security Features for Your Rails App
These capabilities aren’t available out of the box with Rails, but Expedited WAF adds them instantly:
- Block IP Addresses - Stop malicious actors by IP or CIDR range
- Block User Agents - Filter out bad bots and scrapers
- Block by Geolocation - Restrict traffic by country or region
- DDoS Protection - Stop attacks with CAPTCHA challenges
- Block Anonymous Proxies - Prevent traffic from VPNs and proxy networks
- Form Input Filtering - Automatically sanitize and filter malicious HTML, scripts, and SQL injection attempts from form submissions before they reach your Rails app
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
- Store in Heroku config:
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 timeoutRequire re-authentication for sensitive operations
- Password changes, email updates, payment actions should require current password
- Use Devise’s
:paranoidmode for additional protection - Consider time-based re-authentication for admin actions
Enable CSRF protection
- Enabled by default - keep it!
- Verify
protect_from_forgery with: :exceptionin ApplicationController - Use
form_authenticity_tokenin 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: requireUse parameterized queries
- Rails ActiveRecord does this by default
- Avoid raw SQL with string interpolation
- Use
where("name = ?", params[:name])notwhere("name = '#{params[:name]}'")
Enable query logging carefully
- Set
config.active_record.log_level = :info(not :debug in production) - Don’t log sensitive data
- Set
Input & Output Protection
Avoid
rawand.html_safe- Rails ERB escapes output by default - this is your primary XSS defense
- Only use
rawor.html_safewhen 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_foror 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
argon2gem 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 @resourcein every controller action
Configure strong parameters
def user_params params.require(:user).permit(:email, :name) # Never permit all endImplement 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 = :amazonValidate 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 endSet X-Frame-Options
- Enabled by default:
config.action_dispatch.default_headers['X-Frame-Options'] = 'SAMEORIGIN'
- Enabled by default:
Dependency Management
Keep Rails and gems updated
bundle update rails bundle update bundle audit check --updateUse 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-warnPin 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 = trueNever 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.rbSpecify 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 endImplement 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 endUse 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 brakemanReview Rails security guide