Written by Technical Team | Last updated 18.09.2025 | 12 minute read
A strong security posture starts long before the first controller is scaffolded. Architecture and configuration decisions set the guard rails (pun intended) that either prevent or enable vulnerabilities later. Treat your Rails application as one component within a broader system of cloud infrastructure, networks, build pipelines and people. From day one, define a threat model: who could attack you, what they want, and where the most valuable assets sit. For a typical Rails product those assets include customer data in PostgreSQL, API credentials and signing keys, session cookies, and admin interfaces. By explicitly modelling threats—unauthorised data exfiltration, account takeover, code execution on application servers—you make your security priorities explicit, budget for them, and measure them.
Production configuration must be locked down. Force HTTPS end to end, not just at the load balancer. Set config.force_ssl = true, enable HSTS with a reasonably long max-age, and ensure TLS is used for every upstream hop, including connections to Redis, PostgreSQL and object storage. Cookies should be marked Secure, HttpOnly and with an appropriate SameSite setting (usually Lax for session cookies, Strict for highly sensitive flows such as admin). Rails’ per-environment secrets keep production credentials out of development and test; go further by integrating with an external secrets manager and rotating keys on a schedule. The goal is a configuration regime that is boringly predictable: identical between staging and production, codified as code, and reviewed like any other change.
Rails has a powerful and convenient router; use it deliberately. Disable or remove any default routes that expose metadata or testing endpoints. Constrain routes by HTTP verb, and prefer idempotent semantics for reads. At the edge, apply rate limits to high-risk endpoints such as sign-in, password reset and file upload. Rack::Attack can throttle based on unique identifiers—but make sure you’re using stable keys (e.g., user id or IP in combination with a request fingerprint) and that you log throttled attempts. For APIs, disable unneeded HTTP methods, enforce strict content types, and ensure CORS is minimal and explicit.
Finally, secure the execution environment. Containers should run as a non-root user, with read-only roots where feasible and only the capabilities actually needed by the process. Keep your Ruby interpreter and system packages patched. The application user should have no shell access to production hosts, and production consoles must be gated behind multi-factor authentication and audited. File permissions on config/credentials/*.key and any SSH keys used by deployment should be restricted to the deploy user only. Where you use background processing (e.g., Sidekiq), run workers with separate credentials and queues so that compromise of a low-risk job cannot trivially escalate to access high-risk data.
Rails gives you a head start against the classic web threats, but it is entirely possible to subvert those protections with sloppy code. The priority is to lean into Rails’ safe defaults and avoid “raw” escape hatches unless you have a compelling, reviewed reason. Injection attacks start when untrusted data is concatenated into an executable context—SQL strings, shell commands, template code or JSON that a parser interprets as instructions. XSS begins when untrusted input is rendered as executable script in a browser. CSRF occurs when an attacker tricks a victim’s browser into making a state-changing request to your site using their valid session. The common defence theme is to keep data and code separate, use framework primitives that do the escaping for you, and verify intent before action.
For SQL, always bind parameters. Active Record query methods (where, find_by, exists?, update_all) accept interpolation safely when you pass arguments separately rather than using Ruby string interpolation. Avoid building dynamic order clauses from user input; maintain an explicit allow-list of sortable columns and directions, and translate user choices to database tokens internally. If you find yourself writing connection.execute(“… #{params[:x]} …”), you’re probably creating an injection hazard. The same goes for sanitize_sql—it is not a substitute for proper binding and can be misused. When you absolutely must interpolate dynamic SQL (e.g., complex reporting), isolate it behind a small, well-tested service with exhaustive inputs validation.
XSS prevention in Rails builds on automatic HTML escaping in ERB and safe HTML helpers. Treat any use of raw, .html_safe, or sanitize with suspicion and code review. If content originates from users (comments, bios, markdown), process it through a robust renderer that outputs safe HTML with a strict allow-list of tags and attributes. Complement this with a Content Security Policy that blocks inline scripts, uses nonces for legitimate scripts, and restricts connect, img, font and frame sources to those you truly need. CSP both reduces the blast radius of an XSS bug and acts as a canary if it starts blocking unexpected script loads in production.
CSRF defences are built in to Rails, but they still require discipline. Ensure you’re calling protect_from_forgery in your controllers or, in modern Rails, not disabling the default CSRF protection. All forms should include the authenticity token, and all non-idempotent JSON endpoints should either validate the token or implement a secure mechanism like double submit for same-site cookies. Be careful when exposing APIs to third-party front ends: if they run in browsers with your cookies, they need CSRF defence; if they use token-based authentication, design them to be cookie-less and enforce CORS preflight with strict origins.
Some vulnerabilities arise at the margins. Server-side request forgery (SSRF) can occur when your app fetches remote URLs supplied by users—think “fetch metadata for this link” or “download an avatar from this URL”. Disallow private IP ranges, limit redirects, set small timeouts, and ideally proxy such requests through a hardened service with a static allow-list of hosts. Deserialisation hazards crop up when you parse YAML or Marshal data from untrusted sources; use JSON for external inputs and Psych.safe_load with an explicit permitted_classes list when you must accept YAML.
To keep teams aligned and code reviews focused, codify the most common Rails pitfalls and their safe alternatives:
Nothing undermines trust like an account-takeover story. Authentication must be more than a username and a password field wired to Devise defaults. Use a slow, memory-hard password hash with a high work factor (e.g., bcrypt with a cost tuned to your servers). Offer multi-factor authentication options that balance usability with security: authenticator apps are table stakes for administrator roles; WebAuthn provides phishing-resistant factors for everyone else. Password reset flows should be rate-limited, use single-use, short-lived tokens, and never disclose whether an email address exists in your system. For social logins or single sign-on, implement OAuth 2/OIDC with well-known providers and PKCE for public clients.
Authorisation determines what an authenticated user is allowed to do. In Rails this often lives in Pundit or CanCanCan policies; wherever it lives, keep it centralised and explicit. Avoid duplicating checks in controllers and views. Multi-tenant applications deserve special attention: every query must be scoped to the tenant and enforced at the database layer whenever feasible (e.g., PostgreSQL Row Level Security or foreign key constraints keyed to the tenant id). Do not rely solely on default_scope; it is too easy to bypass with an eager load or raw query. Where users can act “as” other users (impersonation), require a privileged role, log it, and badge the UI clearly to prevent confusion and accidental changes.
Sessions embody the user’s identity in a browser. Cookies carrying session ids must be Secure, HttpOnly and use SameSite=Lax unless you have a clear reason to be stricter or looser. Rotate session ids on sign-in and privilege elevation, and invalidate all sessions on password change. Keep the session payload small; avoid storing PII or authorisation decisions in the session—derive them server side each request. For APIs, favour short-lived access tokens with refresh tokens bound to device or client id. If you adopt JWTs, set tight expiry, include aud, iss and a unique jti, and maintain a server-side deny-list for revocation. Rate-limit sensitive endpoints and introduce progressive delays or CAPTCHAs on suspicious behaviour to dampen credential-stuffing attacks.
Data security is broader than AES and TLS, though both remain essential. Encrypt data in transit everywhere and use modern ciphers by default; TLS termination at the load balancer is not enough if your app then connects to PostgreSQL or Redis unencrypted. At rest, encrypt database volumes and object storage, and treat signing keys and secrets as the crown jewels. Rails makes it easy to store credentials in config/credentials.yml.enc, but that only relocates the problem: how do you protect, rotate and audit the master key? In production, store master keys in a secrets manager, not in environment variables passed to dozens of servers. Automate rotation of API keys for third-party services; when rotation is manual, build it into your operational checklists and incident playbooks.
Handle personally identifiable information (PII) with data minimisation and compartmentalisation. Do you actually need the full date of birth or would age band suffice? If you store national identifiers, separate the columns, encrypt them application-side before they hit the database, and gate access to decryption behind explicit roles with audit trails. In PostgreSQL, use separate schemas or databases for operational data and analytics extracts, with different credentials and least-privilege grants. When exporting data, generate CSVs or reports asynchronously, storing them in a private bucket with short-lived, signed URLs. Avoid caching sensitive pages at the edge, and scrub sensitive fields from your logs with filter_parameters so they never appear in plain text.
File uploads and document processing deserve a careful design. With Active Storage, prefer cloud storage over the local filesystem, and configure strict content-type allow-lists. Use serverside virus scanning—either a managed service or ClamAV in a hardened container—and strip dangerous metadata from images and office documents. Do not rely on client-supplied filenames or content types; validate and normalise them server side. When generating derivatives (thumbnails, PDFs), use libraries that avoid calling external shell tools where possible. If you must shell out to ImageMagick or Ghostscript, pass arguments in array form, set resource limits, and process in a sandboxed worker with no network access.
Your application’s “supply chain”—Ruby, Rails, gems, OS packages and build tools—is a significant source of risk. Keep dependencies lean and pinned. Prefer maintained gems with active security posture and avoid pulling in large frameworks for trivial features. Automate dependency scanning in CI and treat “known vulnerable dependency” alerts as you would failing tests. Brakeman is a staple for static analysis of Rails projects; combine it with dependency audits and a lightweight dynamic scanner in staging for basic coverage. Beyond scanning, verify what you build: use checksummed gem sources, lockfiles committed to version control, and reproducible builds so you can prove that the code you reviewed is the code you shipped.
To embed these practices in day-to-day engineering, make them explicit and testable:
Security is a practice, not a project, and that means operations. Start with observability: structured, centralised logs that include request ids, user ids (where lawful), IP addresses, rate-limit decisions and security headers. Filter secrets and PII at the logger, not after the fact. Monitor for anomalies—sudden spikes in 401s on the sign-in endpoint, an uptick in CSRF failures, or repeated throttling on a specific IP range. Surface security metrics on a dashboard the whole team sees daily, alongside performance and error rates. Strong signals sadly aren’t enough without response. Define playbooks for the events you will actually see: suspected credential stuffing, an XSS discovery, lost device with a logged-in admin session, compromised API key. A good playbook includes detection criteria, communication steps (internal and customer-facing), immediate containment actions (e.g., revoke tokens, rotate credentials), and follow-up tasks (root cause, fixes, and a blameless post-mortem).
Compliance should support, not distort, your security agenda. Understand the specific obligations that apply to your product—GDPR for personal data of EU/UK residents, PCI DSS if you process card data, sector-specific rules if you handle health or financial information. Map those obligations to concrete Rails artefacts: data subject access requests become export scripts that honour tenancy and redaction rules; lawful basis and consent become database columns and audit trails; “privacy by design” becomes actively choosing to hash or tokenise identifiers. The best approach is to treat compliance as codified security and privacy requirements that you test in CI, not a binder on a shelf. When the inevitable audit arrives, you will have evidence in code and logs, and your team will have the muscle memory to demonstrate how the controls work in practice.
Is your team looking for help with Ruby on Rails development? Click the button below.
Get in touch