spree-security について
Spree 6の拡張開発者向けセキュリティスキルです。シークレット、APIアクセス、スタッフセッション、顧客向けエンドポイントを扱い、認証情報、決済、店舗間データのリスクを抑える方法を説明します。
主な機能
- 01シークレットと暗号化キーの管理
- 02APIキーのスコープ設定
- 03スタッフセッションの保護
- 04店舗単位のレコードアクセス
- 05一括代入の制御
活用シーン
- 01顧客データやゲートウェイ情報の暗号化キーを設定
- 02APIキーのスコープで連携権限を制限
- 03カスタムコントローラーの店舗間データ露出を点検
- 04APIリソースの書き込み可能属性を設定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ spree/ agent-skills --skill spree-securityインストール後、Claude Code で/spree-securityを入力して読み込まれたか確認します。
詳細
Spree のセキュリティ
Spree は Rails のセキュリティモデルを継承し、決済フロー、顧客の個人識別情報(PII)、スタッフの認証情報、送信 Webhook など、eコマース特有の攻撃対象領域を追加します。このスキルでは、Spree 6 を拡張する開発者が適切に対処すべき内容を説明します。誰が何をできるか(ロール、権限キー、スコープ、ストアフロントの所有権、SSO)については、spree-auth-permissions を参照してください。
4 行でわかる脅威モデル
- Store API:インターネットに公開され、信頼できないクライアントから呼び出されます。リスク:カートや注文に対する IDOR、カタログコンテンツを介した XSS、カート/認証エンドポイントの悪用。
- Admin API + ダッシュボード:スタッフの認証情報がフィッシングされ、インテグレーションからキーが漏えいします。リスク:過剰なスコープを持つシークレットキー、カスタムコントローラーでのストア間の情報漏えい、ロールの権限の肥大化。
- 決済:金銭と PCI に関わります。リスク:ログや DB へのカード情報の記録、ゲートウェイ Webhook の未検証、返金の悪用。
- 送信トラフィック:Webhook とインテグレーションが対象です。リスク:SSRF、受信先の未検証。
シークレット
- Rails の暗号化済みクレデンシャルまたは環境変数に保管し、リポジトリには絶対に含めないでください。
VITE_*変数はダッシュボードのバンドルにコンパイルされるため、絶対にシークレットを設定しないでください。 - シークレットが漏えいした場合: まずプロバイダー側でローテーションし、その後クレデンシャル/環境変数を更新してデプロイし、最後に Git の履歴を消去します(
git filter-repo)。先に履歴を消去すると、漏えいしたキーはローテーションするまで使い続けられます。 secret_key_baseは環境ごとに固定してください。 シークレット API キーはこれをキーとする HMAC-SHA256 ダイジェストとして保存されるため、これをローテーションするとすべてのsk_キーが無効になります。また、JWT 署名の最後のフォールバックとしても使われます。SPREE_JWT_SECRET_KEY(またはクレデンシャルのjwt_secret_key)で専用の JWT シークレットを設定してください。- Active Record の暗号化。 Spree は、キーが設定されている場合に限り、
Spree::WebhookEndpoint#secret_keyとSpree::GatewayCustomer#profile_id(決定論的)、およびSpree::UserIdentity#access_token/#refresh_token(OAuth トークン)を暗号化します。キーがない場合は平文になり、スターターは起動時に警告をログに出力します。 次の 3 つの環境変数をすべて設定してください:ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY、ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY、ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT。create-spree-appは開発用のキー一式を.envに書き込みます。古いプロジェクトの.envに追加するにはspree encryption initを使います(既存のキーは上書きせず、--forceもありません。その後、spree updateでコンテナを再作成してください。eject 済みの場合はspree devを使います。spree restartでは古い環境変数が維持されます)。本番環境用の新しいキー一式は、spree encryption init --printまたはbin/rails db:encryption:initで出力できます。環境ごとに別のキー一式を使い、シークレットマネージャーにバックアップしてください。- スターターの
config/application.rbは、環境変数(なければactive_record_encryptionクレデンシャルの設定)をconfig.active_record.encryptionにコピーします。これは重要です。Webhook シークレットとゲートウェイ顧客のencrypts呼び出しはRails.configuration.active_record.encryptionを参照するため、このスニペットがない状態でキーをクレデンシャルのみに保存すると、この 2 つのカラムは平文のままになります。古いスターターを使ったアプリには、このスニペットが必要です(spree-upgrade-5-to-6を参照)。 - データを暗号化した後は、キーを変更したり失ったりしないでください。 Rails ではプライマリーキーをローテーションできますが、Webhook シークレットとゲートウェイ顧客 ID に使う決定論的キー/ソルトはローテーションできません。
- 既存データの暗号化を有効にする場合:ID トークンは読み取り可能な状態を維持し(
support_unencrypted_data: true)、次回の書き込み時に暗号化されます。Webhook シークレットとゲートウェイ顧客 ID はそうなりません。config.active_record.encryption.support_unencrypted_data = trueとextend_queries = trueを設定し、キーを設定してデプロイした後、[Spree::WebhookEndpoint, Spree::GatewayCustomer, Spree::UserIdentity].each { |m| m.find_each(&:encrypt) }を実行し、その後両方の設定を削除してください。
- 決済方法とインテグレーションの設定は暗号化されません。 ゲートウェイの認証情報はシリアライズされた
preferencesカラムに保存されるため、データベースとそのバックアップには有効なシークレットが含まれているものとして扱ってください。有効なゲートウェイキーは、シードではなくダッシュボード(設定 → 決済)から入力してください。
spree/agent-skills プラグインには、エージェントが既知の形式のシークレット(Stripe のライブキー、AWS キー、PAT)を書き込もうとしたときに警告するフックが付属しています。これは警告用のトリップワイヤーであり、レビューの代わりにはなりません。
API キー
pk_* Publishable. Safe in browser and mobile code. Store API only.
sk_* Secret. Server-to-server only. Never ship it in a bundle, app or VITE_* variable.
- 最小限のスコープ。
spree api-key create --type secret --scopes read_orders,write_fulfillmentsを実行してください。インテグレーションにwrite_allを付与しないでください。スコープ一覧はspree-api-v3/references/scopes.mdにあります。 - スコープとチャネルの紐付けは変更できません。アクセスをローテーションまたは変更するには、新しいキーを発行してデプロイした後、古いキーを失効させます(設定 → API キー、または
spree api-key revoke)。 - シークレットキーで行った書き込みは、そのキーに帰属します(
*_type: api_key)。そのため、アクターフィールドとWebhookDeliveryログを通じて、漏えいしたキーによる操作を監査できます。 - ストアフロントの公開可能キーをそのチャネルに紐付けることで、買い物客が
X-Spree-Channelを送信してチャネルを切り替えられないようにします。 pk_の漏えいは軽微な問題です。レート制限を設定してローテーションしてください。sk_の漏えいは情報漏えいインシデントです。キーを失効させてから監査してください。
ダッシュボードとスタッフセッション
React ダッシュボード(@spree/dashboard。spree_dashboard によって /dashboard で配信)は、API キーを保持しません。
- スタッフがサインインすると、JWT はメモリ内にのみ保存されます(デフォルトの有効期間は 5 分、
admin_jwt_expiration)。リフレッシュトークンは、/api/v3/admin/authをスコープとする HttpOnly の署名付き Cookie です。JavaScript から読み取ることはできず、リフレッシュのたびにローテーションされます。 - HTTPS では Cookie は
SameSite=None; Secureです。通常の HTTP ではSameSite=Laxにフォールバックします(開発環境のみ)。 - 同一オリジン(デフォルト:Rails が
/dashboardでダッシュボードを配信)の場合、CORS や Cookie の設定は不要です。 - クロスオリジン(CDN 上のダッシュボード)の場合、両側で HTTPS を使用し、さらに Settings → Allowed Origins(
Spree::AllowedOrigin、/api/v3/admin/allowed_origins)にダッシュボードのオリジンを追加します。スターターのconfig/initializers/cors.rbは、credentials: trueを指定して/api/v3/admin/*の許可オリジンをそのテーブルで確認します。Cookie の CSRF 保護は SameSite と厳格なオリジン許可リストによって行われるため、リストは正確かつ短く保ってください。 - UI での表示制御は認可ではありません。 ダッシュボードでは
GET /api/v3/admin/meの権限キーに基づいてボタンを非表示にしますが、それらを強制するのは API のゲートです。ダッシュボードプラグインのアクションはすべて、scoped_resourceを宣言するエンドポイントを呼び出す必要があります(spree-auth-permissionsを参照)。このゲートを通過した後のレコード単位の拒否はSpree.ability_classによって行われます。カスタムサブクラスでスタッフのアクセスをさらに制限できますが、キーゲートを緩めることはありません。また、シークレットキー(Spree::ApiKeyAbility)はこのゲートを回避します。 - スタッフ SSO:
OidcStrategyを登録し、必要に応じてSpree.admin_authentication_strategiesから:emailを削除します。スタッフアカウントは IdP から自動プロビジョニングされません。 - スタッフ管理のガードレールは組み込みです(詳細は
spree-auth-permissionsを参照)。adminロールを付与または削除できるのは管理者のみで、ストアには少なくとも 1 人の管理者が必要です。招待にはロールを指定する必要があります。招待の承認リンクは、書き込み権限で保護された別のエンドポイントから取得し(一覧から取得することはできません)、再送時にはトークンがローテーションされます。スタッフのパスワードリセットリンクは、ダッシュボードのオリジンのみを参照します。ログイン、現在のパスワードの確認、招待の承認では、共通のロックアウト(Spree::Authentication::Lockout)を使用します。追加するパスワード確認も、必ずこれを経由させてください。 - スタッフにはそれぞれ個別のアカウントと、最小権限のロールを付与してください。
admin@のログインを共有しないでください。
Mission Control(/jobs)
ジョブ UI は HTTP Basic 認証で保護されています。本番環境では MISSION_CONTROL_USER と MISSION_CONTROL_PASSWORD を設定してください。これらが設定されていない場合、ダッシュボードはロックされたままです。スターターのローカルデフォルト(spree / spree123)が適用されるのは開発環境とテスト環境のみです。本番環境の環境ファイルにコピーしないでください。
Store API:IDOR とストア間の情報漏えい
- カートと注文は、その所有者(JWT)またはゲストトークン(
X-Spree-Token)を持つユーザーが読み取れます。トークンを持つ人は誰でも、そのカートや注文を読み取れるため、トークンをログに記録したり、共有する URL に含めたりしないでください。 - プレフィックス付き ID は秘密情報ではありません(連番キーをエンコードしたものです)。すべての検索で認可を確認し、ID を推測しにくいことを前提にしないでください。
- カスタム Store コントローラーでは、
storefront_access_policy.scope(Model.for_store(current_store))またはcurrent_user.<association>を通じて読み取ってください。Model.find(params[:id])は使用しないでください。 - カスタム Admin コントローラーでは、継承した
scope(ストアでスコープ済み)を使用し、scoped_resourceを宣言してください。マルチストアアプリはデータベースを共有しているため、コントローラー内のSpree::Order.findはストア間の情報漏えいにつながります。 - 書き込み本文内のすべての ID も検索対象です。 参照先の ID(
stock_location_id、reason_id、カスタムフィールドの親、翻訳対象、価格リストのバリアント、プロモーションルールの商品)を、current_store(セラー向けの画面ではセラーも含む)を通じて解決し、親レコードはクエリパラメーターや本文パラメーターではなくルートから取得してください。コアのエンドポイントではこの処理を行っていますが、パラメーターから生の*_idを割り当てるカスタムコントローラーを作ると、ストア間の情報漏えいの穴が再び開きます。 - 開発環境とテスト環境では、
Spree::StoreScopeGuardが、ストア所有テーブル(store_idを持つ任意のspree_*テーブル)に対するSELECTのうち、ストアでスコープされていないもの、または ID で絞り込まれていないものを検出します。API v3 のすべてのリクエストと、Spree::Current.storeを設定するすべての作業単位(ジョブ、Webhook コントローラー、コンソールスクリプト、テスト)を、Spree::Currentがリセットされるまで監視します。モード:SPREE_STORE_SCOPE_GUARD/Spree::Config[:store_scope_guard]=log(デフォルト)、raise(Spree 独自の API テストスイート)、off。本番環境では有効になりません。意図的にグローバル検索を行う場合は、Spree::StoreScopeGuard.skip { … }で囲んでください。警告を真剣に受け止めてください。
一括代入
API v3 のコントローラーは、resource_permitted_attributes(カスタムコントローラー)と各モデルの additional_permitted_attributes を使って許可リストを作成します。Spree::PermittedAttributes モジュールはありません。拡張機能が追加した項目が失われるため、permitted_params をオーバーライドしないでください。
# Make an extension column writable on the existing endpoints (never `<<`, the array is frozen):
Spree::Product.additional_permitted_attributes += [:brand_id]
Spree::Address.additional_permitted_attributes += [{ tag_ids: [] }]
# Custom controllers list their own:
def resource_permitted_attributes = %i[name rating body]
params.permit! は使用しないでください。顧客向けエンドポイントでは、所有権や権限に関わるカラム(customer_id、store_id、role_ids、status)を許可しないでください。これらはサーバー側で設定してください。
インジェクションと XSS
- SQL: パラメーター化された
where('x > ?', v)またはハッシュ条件を使用し、文字列補間は決して使用しないでください。Ransack は許可リストに登録された属性のみをフィルタリングするため、安全です。不明なq[...]述語は黙って破棄されます。新しいフィルターはSpree.ransack.add_attribute(Model, :attr)で公開し、シークレットやダイジェストを許可リストに登録しないでください。フィルターははい/いいえの判定に使えるため、Store API と Seller API の許可リストはスタッフ向けよりも狭く設定されています(:store/:sellerをキーとするstorefront_ransackable_associations、private_ransackable_attributes/private_ransackable_scopes)。許可リストを広げる際は、非公開データをストアフロント用リストに含めないでください。Store/Seller のResourceControllerを継承するコントローラーでは、自動的に設定されます(ransack_auth_object)。顧客向けコントローラーで独自にクエリを実装する場合は、ransack(params, auth_object: :store)を明示的に呼び出してください。 - リッチテキスト: 商品、カテゴリー、コレクション、販売者の説明は、
Spree::RichTextSanitizerにより保存時にサニタイズされます(has_spree_rich_text/sanitizes_rich_text経由)。許可リストはダッシュボードの Tiptap エディターが生成する内容をカバーしています:p br hr h1–h6 strong em s u code pre blockquote ul ol li a img。data:/javascript:URL は除去されます。API はdescriptionをプレーンテキストとして、description_htmlをサニタイズ済みマークアップとして返します。- 許可リストを意図的に広げるには、イニシャライザーに
Spree::RichTextSanitizer.allowed_tags += %w[table thead tbody tr th td]を記述します。 - コールバックをスキップする書き込み(
update_columns、update_all、生 SQL、モデルを経由しない一括インポート)はサニタイズされません。Spree::RichTextSanitizer.sanitize(html)を自分で呼び出してください。 - モデルに新しいリッチテキスト列を追加する場合:
include Spree::SanitizableRichTextとhas_spree_rich_text :bodyを追加します。翻訳属性には、Translationクラスにもsanitizes_rich_textが必要です。
- 許可リストを意図的に広げるには、イニシャライザーに
- ストアフロント:
*_htmlは、サーバー側でサニタイズされたこれらのフィールドに限り、フレームワークの生 HTML 出力方法(たとえば React のdangerouslySetInnerHTML)でレンダリングしてください。それ以外はすべてエスケープしてください。 - CSV エクスポート: スタッフがスプレッドシートで開くファイルに書き込む前に、
=、+、-、@で始まるセルを無害化してください。
CORS と CSP
# config/initializers/cors.rb: storefront on another origin (the admin block comes from the starter)
allow do
origins 'https://shop.example.com'
resource '/api/v3/store/*', headers: :any, methods: %i[get post patch put delete options]
end
credentials: trueとorigins '*'を決して併用しないでください。管理画面と、オリジンをまたぐ販売者パネルでは、Allowed Origins テーブルを使用する必要があります。- HTML をレンダリングするアプリに CSP を設定してください。Next.js のストアフロントの場合は Rails ではなく Next アプリに設定します。Rails 側は主に JSON とダッシュボードを配信します。
Webhook
アウトバウンド(Spree → お客様の受信先)。各配信には、エンドポイントのシークレットを使用した "#{timestamp}.#{raw_body}" の HMAC-SHA256 の 16 進数値である X-Spree-Webhook-Signature と、X-Spree-Webhook-Timestamp、X-Spree-Webhook-Event が含まれます。受信側は次の処理を行う必要があります。
- JSON を解析する前に、未加工の本文バイト列を使って検証します。
- 定時間比較関数(
ActiveSupport::SecurityUtils.secure_compare、crypto.timingSafeEqual)で比較します。 - 約 5 分より古いタイムスタンプは拒否します(リプレイ対策)。
- 冪等に処理します。2xx 以外の応答、タイムアウト、接続エラーは
Spree::WebhookDeliveryJobによってバックオフ付きで再試行されます(合計 5 回。同じイベントidが使われます)。手動で再配信した場合も、同じイベントが再度送信されます。
@spree/sdk/webhooks は verifyWebhookSignature(rawBody, signature, timestamp, secret, tolerance = 300) をエクスポートします。spree-events-webhooks を参照してください。
SSRF。 開発環境以外では、配信は SsrfFilter を経由し、プライベート、ループバック、リンクローカルの宛先がブロックされます。開発環境では localhost の受信先を使用できるよう、このフィルターを回避します。Rails.env.development? の分岐を他のコードパスにコピーしないでください。webhooks_verify_ssl は開発環境以外ではデフォルトで有効です。有効のままにしてください。Webhook エンドポイントの作成には write_webhooks が必要です。この権限は意図的に settings とは分離されています。
write_webhooks/read_webhooks で許可されない操作。 customer.password_reset_requested には有効なリセットトークンが含まれます。このイベントが届くのは、イベント名を指定したエンドポイントのみです(* や customer.* には届きません)。このイベントの購読、または受信するエンドポイントの転送先変更には、write_customers も必要です。配信ログでは、認証情報(カートの token、リセット/検証トークン、ダウンロード URL、決済セッションのシークレット、ギフトカードの code)は [REDACTED] として保存されます。また Admin API が配信の payload を返すのは、基になるレコードを読み取れる呼び出し元のみです(それ以外の場合は null)。
インバウンド(ゲートウェイ → Spree。例:/api/v3/webhooks/payments/...):レコードは接頭辞付き ID のみで検索されます(リクエストでストアを指定することはできません)。リクエストは、その決済方法/インテグレーションのストア内で実行され、プロバイダーはレコード固有のシークレットを使って署名を検証します。署名が無効な場合は 401 を返します。認証手段は署名のみです。カスタムプロバイダーを作成する場合は、処理を行う前に署名を検証してください(spree-providers、spree-payments を参照)。
データプライバシー(GDPR)
- データ主体からのリクエストは
Spree::DataRequestレコードです。顧客はPOST /api/v3/store/customers/me/data_requests { kind: "access" | "erasure" }でリクエストを作成します(消去にはcurrent_passwordが必要です)。スタッフはGET /api/v3/admin/customers/:id/exportとPOST /api/v3/admin/customers/:id/anonymizeを使用します。エクスポートはバックグラウンドで作成され、有効期限付きの署名済みリンクとしてメールで送信されます。 - 消去とは匿名化を意味します。これは
Spree::Customers::Anonymizeワークフローで実行されます。アカウント、住所録、注文の住所スナップショット、保存済みカード、ID、セッションの情報を消去し、同意レコードからメールアドレス、IP、ユーザーエージェントを削除します(レコード自体は保持されます。下記の「同意」を参照)。財務記録(注文、決済、税明細、明細項目に加え、税務管轄判定のための国、州、切り詰められた郵便番号)は保持され、customer.anonymizedが公開されます。 - 個人データを保持するテーブルを追加する場合は、同じ変更で匿名化処理も拡張してください。 Core には、個人データのカラムが匿名化の対象に含まれていない場合に失敗するスキーマガードの spec があります。
- フック:
Rails.application.config.after_initialize do Spree.hooks.register('customers.anonymize.validate', 'MyApp::LegalHold') # workflow.reject!('Under legal hold') Spree.hooks.register('data_requests.fulfill.extend_payload', 'MyApp::LoyaltyExport') # return a Hash to merge end - 同意:
Spree::ConsentRecordは、同意イベント(目的、ソース、時刻、ドキュメントのダイジェスト)を記録します。顧客にはemail_marketing_consent_updated_at/_sourceもあります。同意レコードは消去後も保持され(証明として目的、ソース、タイムスタンプを維持)、メールアドレス、IP、ユーザーエージェントは消去されます。また、顧客がオプトインしていた場合、消去時にemail_marketingの撤回レコード(ソースはanonymization)が追加されます。Cookie の同意はストアフロントの責任です。 - スタッフによる
DataRequestとConsentRecordへのアクセスには、read_customers/write_customersが適用されます。
PCI の対象範囲
Spree は PAN を保存も送信もしません。カードデータは、ゲートウェイのホステッドフィールド(spree_stripe 経由の Stripe Elements / Payment Element、Adyen Drop-in、PayPal)を通じて、ブラウザーからゲートウェイへ送られます。Spree が受け取るのはトークンと決済セッションのみです。Spree::CreditCard に保持されるのはブランド、末尾 4 桁、有効期限、ゲートウェイ参照情報であり、カード番号全体や CVC は保持されません。
- カードデータ用のカラムは追加しないでください。また、API 経由でカードの生データを中継しないでください。その必要があると思われる場合は、代わりにゲートウェイのトークン化を使用してください。
- ホステッドフィールドのみを使用する場合、通常は SAQ A に該当します。カードデータを独自に収集すると、SAQ D に該当します。
- パラメーターのフィルタリングでは、すでに
number、verification_valueなどが対象になっています。独自の機密パラメーター名を追加する場合は、Rails.application.config.filter_parametersを拡張してください。
レート制限
API には次のスロットリングが組み込まれています。公開可能キー + IP アドレスごと(300 回/分)、シークレットキーごと(600 回/分)、ログイン、登録、トークン更新、パスワードリセットの IP アドレスごとの制限です。カウンターは Rails.cache に保存されるため、複数プロセスでデプロイする場合は共有ストア(Solid Cache または Redis)が必要です。大量の不正アクセスを防ぐため、前段に CDN または WAF を配置してください。表については spree-api-v3 を参照してください。
依存関係の衛生管理
CI で、ストアフロントとダッシュボードに対して bundle audit、brakeman、pnpm audit を実行してください。このプラグインには CI ワークフローは含まれていません。
デプロイチェックリスト
- シークレットは credentials/env に設定します。
VITE_*、リポジトリ、シードにはシークレットを含めません。 -
secret_key_baseを固定し、SPREE_JWT_SECRET_KEYを設定します。3 つのACTIVE_RECORD_ENCRYPTION_*キーを設定し(本番環境専用の値を設定してバックアップします)、config.active_record.encryptionに読み込みます。 -
config.force_ssl = trueを設定します。API、ダッシュボード、ストアフロントで HTTPS を使用します。 - Allowed Origins には、実際のダッシュボード/販売者パネルのオリジンのみを指定します。ストアフロントの CORS には、オリジンを明示的に指定します。
-
MISSION_CONTROL_USER/MISSION_CONTROL_PASSWORDを設定します。 - インテグレーションでは、必要最小限のスコープを持つシークレットキーを使用し、それぞれの目的に応じた名前を付けます。
write_allは緊急時の使用に限定します。 - スタッフには個別のアカウントと最小権限のロールを割り当てます。組織に IdP がある場合は、SSO を必須にします。
- Webhook 受信側で HMAC を検証し、タイムスタンプを確認して、冪等性を確保します。
- カスタムコントローラーは、ストア単位(Admin)または所有者単位(Store)でスコープを制限します。
- 追加した個人データ用テーブルも匿名化の対象に含めます。
- レート制限用に共有キャッシュストアを使用します。前段に CDN/WAF を配置します。
- データベースのバックアップは暗号化し、データベースとは別の場所に保存します。
詳細情報
- PCI:
node_modules/@spree/docs/dist/developer/security/pci_compliance.md - データプライバシー:
node_modules/@spree/docs/dist/developer/core-concepts/data-privacy.md - ダッシュボードのデプロイと認証:
node_modules/@spree/docs/dist/developer/dashboard/deployment.md - Admin API の認証:
node_modules/@spree/docs/dist/api-reference/admin-api/authentication.md - バックグラウンドジョブ / Mission Control:
node_modules/@spree/docs/dist/developer/deployment/background_jobs.md - Rails セキュリティガイド:https://guides.rubyonrails.org/security.html
- 関連スキル:
spree-auth-permissions、spree-api-v3、spree-events-webhooks、spree-payments、spree-deployment。