January 29, 2026 · 10 min read
The Rails SaaS Security Checklist for 2026: Ship Fast Without Getting Hacked
Jeronim Morina
Founder, Omaship
Most Rails SaaS apps get hacked not because Rails is insecure, but because founders skip the basics. Here is the checklist that actually matters — and what you can safely ignore.
Rails is one of the most secure web frameworks out of the box. CSRF protection, parameterized queries, output escaping, encrypted cookies — it handles the OWASP top 10 better than most frameworks without you doing anything. But "out of the box" only covers the framework layer. Your app, your deployment, and your habits are where the real risks live.
This is not a 50-page compliance document. It is a practical checklist for indie hackers and small teams who want to ship fast without leaving the front door open.
Authentication: get this right first
Authentication is where most SaaS security stories begin. Rails 8 shipped a built-in authentication generator that gives you a solid foundation: session-based auth, bcrypt password hashing, password resets, and secure cookie handling. For most SaaS apps, this is all you need.
The built-in generator is intentionally simple. It gives you code you own, in your app, that you can read and understand. No dependency to keep updated. No abstraction layer hiding security-critical logic. This is a feature, not a limitation.
Devise is still a valid choice if you need OAuth, two-factor authentication, or account lockout out of the box. But understand the tradeoff: you are adding a dependency that touches your most security-critical code path. If you use Devise, keep it updated and audit your configuration.
What actually matters for authentication security:
- Use bcrypt with a cost factor of at least 12. Rails defaults to 10. Bump it. The extra milliseconds per login are worth it.
- Set secure cookie flags.
config.force_ssl = truein production handles theSecureflag. SethttponlyandSameSite=Laxon session cookies. - Rotate session tokens on login. Call
reset_sessionbefore setting the new session to prevent session fixation. - Rate-limit login attempts. Use Rack::Attack or a similar middleware. Five attempts per minute per IP is a reasonable starting point.
- Use constant-time comparison for tokens.
ActiveSupport::SecurityUtils.secure_compareexists for this. Use it for API keys, reset tokens, and any secret comparison.
Authorization: the part everyone forgets
Authentication tells you who someone is. Authorization tells you what they can do. Most SaaS security breaches are authorization failures: user A can see user B's data because nobody checked.
Pick a pattern and stick with it. Pundit and Action Policy are the two most common choices. Both work well. Pundit is simpler, Action Policy is more structured. For most SaaS apps, Pundit is enough.
The critical rule: every controller action that touches data must check authorization. No exceptions. If you find yourself writing @thing = Thing.find(params[:id]) without scoping to the current user or checking a policy, you have an IDOR vulnerability.
- Scope all queries to the current tenant. Use
current_user.thingsinstead ofThing.find. Better yet, use a default scope or a current tenant pattern. - Test authorization explicitly. Write tests that prove user A cannot access user B's resources. These tests catch regressions that functional tests miss.
- Use Pundit's
after_action :verify_authorized. This raises an error if you forget to callauthorizein any action. It turns authorization from "opt-in" to "opt-out."
CSRF, XSS, and SQL injection: what Rails handles vs. what you still need to do
Rails handles these three attack classes better than almost any other framework. But "handles" does not mean "eliminates."
CSRF: Rails includes CSRF tokens in every form automatically. You are protected as long as you do not disable protect_from_forgery and you use Rails form helpers. If you build a JSON API, use token-based authentication instead of cookies, or add CSRF tokens to your API requests.
XSS: ERB escapes output by default. You are safe unless you use raw, html_safe, or . Audit your codebase for these. Every instance is a potential XSS vector. If you need to render user-provided HTML, sanitize it with Rails::HTML5::SafeListSanitizer.
SQL injection: Active Record parameterizes queries automatically. You are safe unless you interpolate user input into SQL strings. Never write where("name = '#{params[:name]}'"). Always use where(name: params[:name]) or where("name = ?", params[:name]).
- Grep your codebase for
html_safeandraw. Each one should have a comment explaining why it is safe. - Grep for string interpolation in
whereclauses. This is the most common Rails SQL injection vector. - Keep
protect_from_forgeryenabled. If you need to skip it for an API endpoint, use a separate API controller with token auth.
Secrets management: stop committing credentials
Rails credentials (config/credentials.yml.enc) solve the secrets problem elegantly. Your secrets are encrypted at rest, checked into version control safely, and decrypted at runtime with a master key that lives outside the repo.
- Use Rails credentials for all secrets. API keys, SMTP passwords, Stripe keys — all of them. Not ENV vars scattered across your deployment config.
- Use per-environment credentials.
config/credentials/production.yml.enckeeps production secrets separate from development ones. - Never commit
master.keyorproduction.key. Check your.gitignore. Rails adds these by default, but verify. - Rotate secrets when team members leave. This is the step everyone skips. If someone with access to production credentials leaves, rotate them.
- In CI/CD, pass the master key as a secret environment variable. GitHub Actions secrets, not committed files.
Dependency scanning: catch vulnerabilities before production
Your app is only as secure as its weakest dependency. Ruby gems and JavaScript packages are constantly disclosing vulnerabilities. Automated scanning catches them before they hit production.
- Run Brakeman in CI. Brakeman is a static analysis tool that catches Rails-specific security issues: SQL injection, XSS, mass assignment, file access, and more. It is fast, free, and has almost no false positives. There is no reason to skip it.
- Run
bundle auditin CI. This checks yourGemfile.lockagainst the Ruby Advisory Database. It catches known CVEs in your gem dependencies. - Run
importmap auditif you use importmaps. This is the JavaScript equivalent ofbundle audit, checking your pinned JavaScript dependencies for known vulnerabilities. - Pin your dependencies. Use exact versions in
Gemfile.lock(which Bundler does automatically) and pin JavaScript versions in your importmap. - Update dependencies regularly. A monthly
bundle updatewith a test run catches most issues before they become urgent.
HTTPS, CSP, and security headers
HTTPS is non-negotiable in 2026. Every production Rails app should have config.force_ssl = true. This enables HSTS, redirects HTTP to HTTPS, and sets the Secure flag on cookies. One line, multiple security wins.
- Enable
force_sslin production. This is the single highest-value security line in your Rails config. - Set a Content Security Policy. Rails has a built-in CSP DSL in
config/initializers/content_security_policy.rb. Start restrictive and loosen as needed. A basic CSP that blocks inline scripts stops most XSS attacks even if your output escaping fails. - Set
X-Frame-Options: DENY. Prevents clickjacking. Rails sets this toSAMEORIGINby default. Tighten toDENYunless you need to embed your app in iframes. - Set
X-Content-Type-Options: nosniff. Rails sets this by default. Verify it is present. - Set
Referrer-Policy: strict-origin-when-cross-origin. Prevents leaking sensitive URL parameters to third-party sites.
Database security
SQLite in production is a legitimate choice in 2026. Rails 8 supports it with Solid Queue, Solid Cache, and Solid Cable. For single-server SaaS apps, it simplifies your stack significantly. But the security considerations differ from PostgreSQL.
- SQLite: protect the database file. It is just a file on disk. Set proper file permissions (
chmod 600). Keep it outside the web root. Back it up encrypted. - PostgreSQL: use a strong password and SSL connections. Do not use the default
postgresuser for your app. Create a dedicated user with minimal privileges. - Encrypt sensitive columns. Rails has built-in encrypted attributes via
encryptsin Active Record. Use them for PII: email addresses, phone numbers, anything subject to privacy regulations. - Back up regularly and test restores. A backup you have never tested is not a backup. Automate it and verify monthly.
Deployment security: Kamal and the server
Kamal gives you zero-downtime deploys to any VPS. But the server itself needs hardening. A $5 VPS with default settings is an invitation.
- SSH keys only, no password auth. Disable
PasswordAuthenticationinsshd_config. If you cannot SSH in with a key, you should not be SSH-ing in. - Change the default SSH port. Security through obscurity is not real security, but it eliminates 99% of automated scans. Pick a port above 1024.
- Set up a firewall.
ufwon Ubuntu: allow your SSH port, 80, 443. Deny everything else. Three commands, significant risk reduction. - Install fail2ban. It bans IPs after repeated failed SSH attempts. Default configuration is fine for most setups.
- Keep the server updated. Enable unattended security upgrades on Ubuntu/Debian. One-time setup, automatic patching.
- Use a non-root deploy user. Kamal can run as a non-root user with Docker access. Do not deploy as root.
CI/CD security: harden your pipeline
Your CI/CD pipeline has access to your production secrets and your codebase. It is a high-value target.
- Pin GitHub Actions to commit SHAs, not tags. Tags can be moved. A compromised action can steal your secrets. Use
uses: actions/checkout@abcdef1234567890instead ofuses: actions/checkout@v4. - Use least-privilege GitHub tokens. The default
GITHUB_TOKENhas broad permissions. Scope it down with thepermissionskey in your workflow. - Never echo secrets in CI logs. GitHub masks known secrets, but derived values (like URLs with embedded tokens) are not masked. Be careful with
set -xin scripts. - Separate CI and CD secrets. Your test suite does not need your production deploy key. Use GitHub environments to scope secrets to specific workflows.
- Review Dependabot PRs before merging. Automated dependency updates are good, but blindly merging them is not. A malicious version bump is a supply chain attack.
Monitoring and incident response
Security without monitoring is hope-driven development. You need to know when something goes wrong.
- Set up error tracking. Sentry, Honeybadger, or Appsignal. Unhandled exceptions in production can indicate exploitation attempts. You need to see them immediately.
- Add audit logging for sensitive actions. Log who changed what and when: password changes, role changes, data exports, admin actions. Use
Rails.loggerwith structured logging or a dedicated audit table. - Rate-limit sensitive endpoints. Login, password reset, API endpoints. Rack::Attack makes this straightforward in Rails.
- Set up uptime monitoring. A free tier from UptimeRobot or similar is enough. If your app goes down unexpectedly, you want to know before your users tell you.
- Have a response plan. It does not need to be a formal document. Just know: who gets notified, how do you rotate compromised secrets, and how do you communicate with affected users.
The "good enough" security stack for indie hackers
You do not need a SOC 2 audit to launch. You do not need a full-time security team. Here is the pragmatic stack that covers 95% of real-world threats:
| Layer | Tool / approach | Effort |
|---|---|---|
| Authentication | Rails 8 built-in auth + Rack::Attack | 30 minutes |
| Authorization | Pundit + scoped queries | 1-2 hours |
| Static analysis | Brakeman in CI | 10 minutes |
| Dependency audit | bundle audit + importmap audit in CI | 10 minutes |
| HTTPS / headers | force_ssl + CSP initializer | 15 minutes |
| Server hardening | SSH keys + ufw + fail2ban | 30 minutes |
| Secrets | Rails credentials (per-environment) | 15 minutes |
| Monitoring | Sentry + UptimeRobot | 20 minutes |
| CI/CD | Pinned actions + scoped secrets | 15 minutes |
Total setup time: about half a day. That is a small investment for a foundation that handles the vast majority of real-world threats. You can launch with this stack and iterate on more advanced measures (SOC 2, penetration testing, bug bounties) as your business grows.
How Omaship handles this out of the box
Omaship is built with the philosophy that security should be a default, not an afterthought. Here is what you get without any additional configuration:
- Brakeman runs in CI on every push. Security static analysis is not optional — it is part of the test suite. If Brakeman finds an issue, the build fails.
- Importmap audit in CI. JavaScript dependency vulnerabilities are caught before they reach production.
- Rails 8 authentication generator. Secure session-based auth with bcrypt, already wired up and tested.
- Force SSL enabled in production. HTTPS, HSTS, and secure cookies are on by default.
- Kamal deployment with SSH key auth. No password-based deployment. No credentials in plaintext configs.
- Per-environment credentials. Production secrets are separated from development secrets. Master keys are excluded from version control.
- Secure CI/CD pipeline. GitHub Actions with scoped permissions and secrets management.
You still need to set up authorization (Pundit or equivalent), server hardening (ufw, fail2ban), and monitoring (Sentry or similar). But the foundation is solid, and the most common security mistakes are already prevented.
The bottom line
Security is not about being paranoid. It is about being methodical. Rails gives you a strong foundation. The checklist above fills the gaps between "Rails defaults" and "production-ready SaaS."
Ship fast, but ship with the basics covered. Authentication, authorization, dependency scanning, HTTPS, and server hardening. Half a day of setup. Years of not getting hacked.
The best security is the kind you set up once, automate, and forget about. Brakeman in CI catches code issues. Bundle audit catches dependency issues. Force SSL and CSP headers catch transport and injection issues. Rate limiting catches abuse. That is the stack. It is not glamorous, but it works.
Start with a secure foundation
Omaship ships with Brakeman CI, importmap audit, Rails 8 auth, force SSL, and secure Kamal deployment out of the box. No security setup required on day one.