Slide 1

Slide 1 text

全ての永続トークンを、 生まれる前に消し去りたい! Ryo Kajiwara (sylph01) @ Kaigi on Rails 2026, 2026/10/16 1

Slide 2

Slide 2 text

Slides are available from the QR code → or from my Speaker Deck (username sylph01 ) 2

Slide 3

Slide 3 text

whoami 梶原 龍 / sylph01 GitHub @sylph01, Twitter @s01, bsky @sylph01.s01.ninja やせいのプログラマ。暗号とか認証認可とかできます 愛媛県松山市に住んでいます 今の現場(どちらも認証基盤担当) codeTakt (今回のトークに関連する作業をした) Leaner Technologies いろいろします(背景参照) 3

Slide 4

Slide 4 text

本発表の目的 皆さんの運用するWebサービスにおいて、無期限あるいは長寿命の、非 常に強い権限を持つトークンを廃止する、またそのための手段を共有す る。 4

Slide 5

Slide 5 text

いつものDisclaimer は今回はそんなに当てはまらない。フロンティアの暗号技術を導入す るわけではないし、暗号関連の機能を自作するわけではないので。 … So, do try this in production, but still be aware of your risks. 5

Slide 6

Slide 6 text

6

Slide 7

Slide 7 text

問題空間の定義 ここでいう「永続トークン」とは何か? サービスに埋め込まれるアクセストークン 往々にして強い権限を持つ 7

Slide 8

Slide 8 text

The One Ring 管理者権限とはThe One Ring (指輪物語のアレ) The One Ringが地上に存在している間、悪意のある人間はThe One Ringを悪用でき、世界は危機に晒され続けている The One RingはMount Doomに投げ捨てろ そのために約1000ページかけた大冒険をする必要はない image: https://www.needpix.com/photo/742654/ring-gold-middle-earth-golden-ring-metallic-map-cgi-jewellery-free-pictures 8

Slide 9

Slide 9 text

永続的に利用可能なトークン は、それ自体がセキュリティ ホールである 9

Slide 10

Slide 10 text

補: トークンのrevoke 実は標準がある。RFC 7009があなたの求めるMount Doomかもしれ ません self-containedなJWTによるアクセストークンは寿命を待つしかない exp なしの無期限だったら…??? 普通の方法では破壊できないなら生まれる前に消し去るしかない 10

Slide 11

Slide 11 text

トークン vs パスワード どちらもcredentialの一種で、どちらも一致を確認して行動が可能になる 性質のものだが… パスワードは「認証に使う合言葉」 トークンは「認可の表明」 原義には実質は「切符」 「切符」には認証の結果許可されたもの(認可)のみ付与され ていてアイデンティティの確認(認証)はついてこない image: https://www.flickr.com/photos/kolix/2539213620 by K.L. (CC BY-NC-SA 2.0). ニューヨーク地下鉄のtoken 11

Slide 12

Slide 12 text

えっ、でもパスワードの定期更新はや めろって話じゃなかったっけ? の話をしているものと思いますが… パスワードリセットの強制は悪いパスワードの選択を誘導するため 避けるべきとされている 当然トークンもパスワードと同程度以上に予測困難である必要はあ る NIST SP 800-63B-4 12

Slide 13

Slide 13 text

えっ、でもパスワードの定期更新はや めろって話じゃなかったっけ? パスワードは意識されるけどトークンは意識されないことが多いの が怖い 特に自動化用の長寿命トークンは放置されがち 13

Slide 14

Slide 14 text

トークンのセキュリティ影響の広さ ( 同時に有効なトークン数) x (有効期限) x (送られるサーバー数) 同時に有効なトークンのうち1つでも取れたら攻撃成立 有効期限が無期限なら攻撃可能な期間は無限大 送られるサーバーの数が多いほどサーバーの不備や通信の不備に弱 くなる 14

Slide 15

Slide 15 text

15

Slide 16

Slide 16 text

どうすればいい? 16

Slide 17

Slide 17 text

アクセストークンの前提 ここからはアクセストークンについて、 OAuthで払い出されて 「認可の表明」の機能を持つもの という前提を持つこととし、OAuthの道具を使った解決の話をします。 17

Slide 18

Slide 18 text

そのトークンは誰の権限? ユーザーの権限 自分自身のデータの操作をするアクセストークン アプリの権限・システムの権限 複数のユーザーのデータの操作をするアクセストークン 18

Slide 19

Slide 19 text

そのトークンで操作をするのは誰? 人間が同期的にアクションをする 人間が離れて(unattended)非同期的にアクションをする 19

Slide 20

Slide 20 text

どのOAuthのモード? 人間が待っている user AuthCode Grant 人間が不在 offline_access system Client Credentials Grant Client Credentials Grant 20

Slide 21

Slide 21 text

人間が待っている + user : AuthCode 「いつものOAuth」。いつものやつなので深入りはしない。 21

Slide 22

Slide 22 text

人間が不在 + user: offline_access 「いわゆるrefresh token」。 22

Slide 23

Slide 23 text

も永続トークンでは? refresh token 確かにそうだが、アクセストークンの有効期限が無期限よりはマシ。 アクセストークンは複数通信で、複数のリソースサーバーに対して利用 される。 refresh tokenはauthorization serverとの間でだけ利用され、実装によって はsingle-useにできる。 の は だったハズ。「実装によって」と書いているのは標準ではsingle-useは強制ではないため Google refresh token single use 23

Slide 24

Slide 24 text

offline_access を特別扱いする理由 確かにAuthCode Grantの一部だが… OAuthでは定義されておらず、OpenID Connectで導入されたscope OIDC Core §11 オフラインアクセスが要求された場合、(他の方法で許可を確 認できない限り) prompt パラメータが consent である必要が ある(MUST) 本質的に違うgrantと言ってよい トークンが「ログアウトを経ても生き延びる」という意味 24

Slide 25

Slide 25 text

強い権限の offline_access をやめろ が広い、unattended、ログアウトを経ても生き延びる One Ringを置いてテーブルを離れている状態 offline_access は 「ユーザー権限の永続トークンをくれ」に等 しい refresh tokenがほしいからといって反射的につけるものではない scope 25

Slide 26

Slide 26 text

じゃあ管理画面はどうやって作ればいいの? 普通はトークンが介入しない。同一アプリケーションなので。 管理UIとAPIが分離されているような場合 管理者のuser identity、AuthCode Grant 管理者自身の sub claim 管理権限を表現するscope セッションとともに有効期限が切れる 26

Slide 27

Slide 27 text

じゃあ管理画面はどうやって作ればいいの? 管理者自身の sub 「 sub が管理者であること」「APIパラメータによって誰に操 作をしたか」がわかる auditが可能 セッションとともに有効期限が切れる adminのoffline_accessをやめろ 27

Slide 28

Slide 28 text

厄介なことが起きるケース: 非同期処理 一括処理を実行→jobをenqueue→Sidekiqがあとで拾う その頃にはadminのaccess tokenがない じゃあadminのaccess tokenをpersistしちゃう? こうすると強い権限を持つトークンが永続化されてしまう。 28

Slide 29

Slide 29 text

厄介なことが起きるケース: 非同期処理 このようにする。 adminの権限でjobを発火 jobが実際に使うトークンはclient credentials 発火したことの説明責任はadminのものとして監査ログに記録 認可の主体と説明責任の主体を分離する 29

Slide 30

Slide 30 text

Client Credentials Grant OAuth ってAuthCode Grantだけじゃなくてえ… (A) Client Authentication Client Authorization Server (B) Access Token 30

Slide 31

Slide 31 text

31

Slide 32

Slide 32 text

におけるクライアント認証 OAuth confidential client クライアント認証に必要なシークレットを保持できるもの サーバーサイドアプリなど public client confidential client でないもの、つまりClient Secretなどを保持で きないもの ネイティブアプリ、ブラウザベースアプリ(SPA)など 32

Slide 33

Slide 33 text

におけるクライアント認証 OAuth (本筋から外れるおまけ) ネイティブアプリにclient secretを埋め込むな 特にLLMの普及した現在リバースエンジニアリングは容易です Authorization Serverがpublic clientを認証しつつAuthorization Code Grantを行う方法として Proof Key for Code Exchange (PKCE) という 手法があります 33

Slide 34

Slide 34 text

におけるクライアント認証 OAuth 共通のシークレット(shared secret)を用いる方法 client_secret_post , client_secret_basic , client_secret_jwt 秘密情報を共有しない方法 公開鍵暗号を使う方式 private_key_jwt , tls_client_auth , self_signed_tls_client_auth https://qiita.com/TakahikoKawasaki/items/63ed4a9d8d6e5109e401 34

Slide 35

Slide 35 text

client_secret も永続トークンでは? パスワード vs トークンでいうところのパスワード 送信される先がAuthorization Serverだけで限定的 35

Slide 36

Slide 36 text

に頼らない方式 shared secret FAPI (Financial-grade API) ではshared secretに頼らない方式が必要。 private_key_jwt トークンリクエストのペイロードを秘密鍵で署名 認可サーバーは登録済みの公開鍵を使って署名検証 mTLS(mutual TLS; クライアント証明書を使ったTLS接続) RFC 7523: JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants 36

Slide 37

Slide 37 text

におけるmTLSの注意点 Rails アプリは一般にロードバランサーやリバースプロキシなどのTLSを 終端するエンドポイントの後ろで動いていることが多い。このとき、ロ ードバランサー/リバースプロキシがヘッダを使ってRailsアプリにmTLS の情報を伝える場合厄介なことが起きる。 Rails 37

Slide 38

Slide 38 text

Rails におけるmTLSの注意点: ALBの例 の下で動くRailsアプリは X-Amzn-Mtls-Clientcert などのヘッ ダからmTLSの情報を得る。 ユーザー指定のヘッダが直接Railsアプリに届いてしまうと mTLSの保証が無効化される ヘッダがingressで剥がされていない場合 ALBを経由せずにRailsアプリに直接アクセスできる場合 ALB 38

Slide 39

Slide 39 text

Rails におけるmTLSの注意点: ALBの例 ALB の動作モード passthrough: バックエンドアプリは X-Amzn-Mtls-Clientcert から証明書を取り出してアプリ側でクライアント認証する必要 がある。これにはcertificate chainの検証も含む verify: ALBがtrust store相手に証明書を検証し、パースされたメ タデータがヘッダに乗ってくる verifyモードのほうが考えることが少ない 39

Slide 40

Slide 40 text

の侵害を想定する場合 RP ハードウェアで鍵を保管した上での private_key_jwt やSenderConstrained Access Token (Proof-of-Possession, DPoP) 秘密鍵をコピーされることを防げる Attested Workload Identity SPIFFE 40

Slide 41

Slide 41 text

ハードウェアで鍵を保管する 公開鍵/秘密鍵ペアを生成・保管することのできるチップ 操作はAPIを通してのみ可能、鍵本体はアクセス不可能 Trusted Platform Module (TPM) Secure Enclave Hardware Security Module (HSM) External hardware 41

Slide 42

Slide 42 text

Sender-Constrained Access Token 「送信者制限つきアクセストークン」 ただトークンを提示するだけ(Bearer)ではなく、そのトークンを使 用していい送信者であることを証明する必要がある private_key_jwt によるクライアント認証はトークンの取得を保 護、Sender-Constrained ATはトークンの使用を保護 42

Slide 43

Slide 43 text

DPoP (RFC 9449) クライアントは鍵ペアを生成 トークンリクエストに {公開鍵, ペイロード, 署名} の組 = DPoP proof JWT を渡す 認可サーバーは発行したトークンと公開鍵を関連づける 各リソースリクエストはアクセストークンに加えて {公開鍵, ペイロ ード, 署名} を送る リソースサーバーは署名検証 + トークンと公開鍵の関連付けを 確認する アクセストークンだけ取っても秘密鍵がないので何もできない! 43

Slide 44

Slide 44 text

SPIFFE サービス間認証の方法として、ワークロードに対してIDを与えてそ れを検証するための仕組み リファレンス実装としてSPIREというのがある IDを払い出すためには共有や事前払い出しの秘密を使うのではな く、ワークロードの偽装困難な性質を検証することで秘密に頼らず ワークロードを認証する ノードが何者であるか(クラウドやKubernetes上の性質)、プ ロセスが何者であるか(agentから見たワークロードの性質) 深入りすると多分単独でトーク1個成立すると思います… 44

Slide 45

Slide 45 text

と の関連は? SPIFFE OAuth 現在のところはSPIFFEを単体で使えば目的は満たせる。 draft-ietf-oauth-spiffe-client-auth という、SPIFFEをOAuthのクライアン ト認証の方式として利用できるようにすることを目指すInternet-Draftが ある。2026/06時点でWG Itemであり-02。 45

Slide 46

Slide 46 text

それでも完全な防護にはならない 46

Slide 47

Slide 47 text

権限の最小化が必要 でやることを減らす ではなく、Rings of PowerにはThe Three, The Seven, The Nineもある 強い操作だけ違う権限にする operator scope The One Ring が権限分散の例として適切かどうかは諸説ある。The Nineの所有者はNazgûlになってしまったので… The Nine 47

Slide 48

Slide 48 text

の最小化も必要 audience 取るときにどのリソースサーバーに対してのトークンが 欲しいかを知らせることができる RFC 8707: 48

Slide 49

Slide 49 text

その他 強い操作をするマシンを外からアクセスできないようにするなども 考えられる 49

Slide 50

Slide 50 text

50

Slide 51

Slide 51 text

トークンの更新が競合した場合どうす る? 複数ワーカーが存在して、キャッシュしているトークンの有効期限 が切れたときにどうなる? 一般的な 並列処理(parallellism)・並行処理(concurrency) の話 51

Slide 52

Slide 52 text

correctness race 複数の更新が競合したときどれが有効になる? 52

Slide 53

Slide 53 text

の場合 refresh token かどうかは実装次第 なら1つだけ成功して残りは失敗となるが… single-use single-use 53

Slide 54

Slide 54 text

の場合 refresh token 結論: 複数ワーカーでrefresh tokenを使うな。 refresh tokenはユーザーの文脈で使うもの どうしても複数マシンで自動化したい場合は複数セッション作るこ とがありうるが、それだけ同時に有効なトークンは増えてリスクも 増える 54

Slide 55

Slide 55 text

の場合 Client Credentials Grant 全部のリクエストが成功して有効なトークンが返るのでcorrectness race はない。 一方でそのぶんだけ有効なトークンが払い出されることにはなるが… 55

Slide 56

Slide 56 text

thundering herd problem 複数ワーカーが同時に参照・更新操作を行おうとするが、一つのワーカ ーだけが有効な操作を行うことができ、残りのワーカーの操作が失敗す る、という問題。キャッシュに関しては特に cache stampede ともい う。 一定のサイクルで更新するようにしていると更新も同時に発生しこの問 題が起きる。 56

Slide 57

Slide 57 text

thundering herd problem 「有効期限が切れたら更新」ではなく、ある程度のrandom jitterを 設けてrefreshする 同時に更新が走ることを防ぐ Rails.cache.write / fetch の race_condition_ttl 最初のリクエストは更新が通常通り走る その次以降のリクエストについて、 race_condition_ttl で 設定した期間は最初のリクエストの値をキャッシュして返す 57

Slide 58

Slide 58 text

トークンのキャッシュ方法 でもいいが最近はSolid Cacheがある Solid Cache with Active Record Encryptionがおすすめ DBに書かれるということはbackup, read replicaなどにも書かれ るので暗号化が重要 設定に encrypt: true → bin/rails db:encryption:init Redis store 58

Slide 59

Slide 59 text

Rails.cache.fetch( ACCESS_TOKEN_CACHE_KEY, race_condition_ttl: RACE_CONDITION_TTL ) do |_name, options| # block runs when entry does not exist response = request_token # call token request jitter = rand(0.7..0.9) # random value from 70 to 90 percent options.expires_in = (response[:expires_in] * jitter).seconds response.fetch(:access_token) { raise TokenRequestError } end 59

Slide 60

Slide 60 text

応用: Sender-Constrained vs ワーカー 間共有トークン? セキュリティのためにはSender-Constrainedにしたいが、一方で: トークンを複数ワーカーで共有するとsender-constrainedにできな い 特にハードウェアによる保証ができない ワーカー個別にsender-constrainedにするとcache hit rateが悪化する tokenをハンドルするプロセスがcallをproxyすることもできる が、それでもそのプロセスに負荷が集中する 60

Slide 61

Slide 61 text

61

Slide 62

Slide 62 text

っていうけど実際やってんの? つい最近やった。 62

Slide 63

Slide 63 text

実はClient Credentials Grant自体は別の用途で先に作ってた 63

Slide 64

Slide 64 text

そのうえで永続トークンを使ってた部分のCCG置き換えを8月〜9月にか けてやった(上記は一例、他のサービスに対しても同様のPRが複数) race condition対策の例は実際に入ってるものにかなり近い。 64

Slide 65

Slide 65 text

どこまでやってる? 今のところは client_secret_post private_key_jwt までは比較的すぐにやれるんじゃないかと思う DPoPはAS側の対応がかなり要りそう 65

Slide 66

Slide 66 text

66

Slide 67

Slide 67 text

まとめ 67

Slide 68

Slide 68 text

を使おう Client Credentials Grant システム間連携自動化のトークンはほとんどこれ 68

Slide 69

Slide 69 text

を正しく使おう Refresh Token ユーザー文脈、強い権限を与えすぎない 69

Slide 70

Slide 70 text

強いクライアント認証を使おう 共有シークレットに頼らない方式もあります 70

Slide 71

Slide 71 text

権限を最小化しよう 万一突破されたときの最後の防御はこれ 71

Slide 72

Slide 72 text

を捨てよう The One Ring あるいは、生まれる前に消し去ってしまえばよい 72

Slide 73

Slide 73 text

Questions? / Comments? Twitter: @s01 or Fediverse: @[email protected] 73