Secure a Spree deployment — Rails credentials and env-var hygiene, Devise auth (Spree v5 ships it in-core; `spree_auth_devise` is archived), CanCanCan authorization rules, Doorkeeper OAuth2 scopes, Storefront publishable key vs admin API key, webhook HMAC verification, OWASP Top 10 for Rails (mass assignment, CSRF, SQL injection via Ransack, XSS, IDOR through prefixed IDs), PCI scope (Spree never touches raw cards thanks to gateway tokenization), and multi-store data isolation. Use when auditing a Spree app, hardening a deploy, or addressing a security incident.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Secure a Spree deployment — Rails credentials and env-var hygiene, Devise auth (Spree v5 ships it in-core; `spree_auth_devise` is archived), CanCanCan authorization rules, Doorkeeper OAuth2 scopes, Storefront publishable key vs admin API key, webhook HMAC verification, OWASP Top 10 for Rails (mass assignment, CSRF, SQL injection via Ransack, XSS, IDOR through prefixed IDs), PCI scope (Spree never touches raw cards thanks to gateway tokenization), and multi-store data isolation. Use when auditing a Spree app, hardening a deploy, or addressing a security incident.
Rails default — required for HTML, exempted for API
spree_auth_devise Is Deprecated
The standalone spree_auth_devise gem is archived as of Feb 2026. Spree v5+ ships Devise auth in the core gem. Do NOT install the old gem on new projects — it conflicts and creates a maintenance burden.
CanCanCan Permission Model
# app/models/spree/ability.rbclassSpree::AbilityincludeCanCan::Abilitydefinitialize(user)
user ||= Spree.user_class.new
if user.has_spree_role?(:admin)
can :manage, :allelsif user.has_spree_role?(:order_manager)
can :manage, Spree::Orderelse
can :read, [Spree::Product, Spree::Taxon]
endendend
Extend via decorator — don't replace.
API Auth Tiers (v3)
Token type
Scope
Where stored
Publishable key (pk_…)
Read-only public catalog + cart endpoints
Browser env (NEXT_PUBLIC_*) — safe
User JWT
Customer's account, their orders
httpOnly cookie server-side
Cart token (order_token)
Anonymous cart only
httpOnly cookie server-side
Admin API key
Per-user admin scope
Server env vars or vault — never to browser
OAuth2 access token (admin scope)
App integration
Server-to-server only
Webhook HMAC Verification
Webhooks 2.0 signs with HMAC-SHA256 over the raw body using a per-endpoint shared secret. Always verify:
Without this, attackers can filter by sensitive columns (passwords, tokens).
Prefixed IDs (v3)
API v3's prefixed IDs (prod_…, ord_…) are opaque strings — they don't expose row counts (sequential integers do). But always pair with ownership checks:
order = current_store.orders.find(params[:id]) # scoped lookup
Not:
order = Spree::Order.find(params[:id]) # IDOR risk
PCI Scope
Spree's never touches raw card numbers when configured correctly:
Stripe Elements / Adyen Drop-in / PayPal Buttons collect card data in the gateway's iframe
Spree stores only the gateway's token (tok_…, pm_…, etc.)
This keeps the merchant in SAQ-A scope, the lightest PCI tier
Don't build a custom card form that sends params[:card_number] to Rails. That escalates PCI scope dramatically.
Multi-Store Data Isolation
As covered in spree-multi-store, always scope queries by current_store. A single missed scope can leak another tenant's orders.
Store it in your platform's secret manager (Heroku config, AWS Secrets Manager, etc.)
Never commit config/master.key — gitignore it
Rotate periodically: re-encrypt credentials with a new key, update env
CSRF Exemption for APIs
classSpree::Api::V3::BaseController < ActionController::API# ActionController::API doesn't include CSRF protection — correct for APIsend
But never disable CSRF on HTML controllers (admin UI). Default Rails CSRF tokens protect admin from cross-origin attacks.
Rate Limiting
Spree's per-endpoint API rate limiting is built in (v5+). For brute-force protection on admin login, layer rack-attack:
# config/initializers/rack_attack.rbRack::Attack.throttle('admin_login', limit:5, period:15.minutes) do |req|
req.ip if req.path == '/admin/login' && req.post?
end
Implementation Guidance
Pre-Production Security Checklist
config.force_ssl = true + HSTS headers
RAILS_MASTER_KEY in vault (not in code)
Devise password policy raised from defaults
CanCanCan abilities reviewed; no can :manage, :all outside admin role
Ransack ransackable_attributes locked per exposed model
bundler-audit and brakeman clean in CI
Webhook signature verification on every receiver
OAuth2 application secrets rotated quarterly
API key issuance auditable — Spree stores them; track in logs
Multi-store scope test — assert customer can't fetch other stores' orders
PCI scope verified — gateway iframes only; never custom card form
Admin 2FA for all admin users
config.filter_parameters includes password, token, secret, credit_card
CSP headers for the storefront (Content-Security-Policy)