自社の認証サーバーを攻撃者の目で読み込み、結果を文書にまとめました。番号付きの 16 セクションで、SAML から SCIM、管理 API、バックアップツールまでを扱い、すべての指摘に深刻度を付けています。最も深刻な 8 件は一晩で修正しましたが、テストスイートはその晩の前も後も 367 件中 367 件が成功と報告していました。8 件は無関係なバグではありませんでした。サブシステムをまたいで繰り返された、3 つの癖だったのです。
パスワードを 5 回間違えるとアカウントはロックされます。チェックは正しく、しきい値も正しかったのに、1 万回の誤ったパスワードを送っても一度も発動しませんでした。一斉に送りさえすれば。カウンターは保護のない読み込み・変更・書き込みだったため、同時に届いた試行が互いのインクリメントを上書きし、数値はほとんど増えなかったのです。修正は、インクリメント操作を持たないキーバリューストアでアトミックなカウンターを作る方法そのものです。そしてそこから得た発想の転換が、誰かが並列で数えていると想定せよ、ということでした。
シングルサインオンは 20 年以上前に決着した標準です。それでも SAML は今なお、商談を止めたり営業への問い合わせを強いたりする項目であり続けています。エンタープライズ認証がいかにして高額なアップセルになったのか。その世界を築いたのは誰で、それを警告したのは誰だったのか。その堀は実際どれほど広く、何でできているのか。そして、その半分を静かに干上がらせつつあるひとつの力について。
監視は、障害のときにしか実行されないコードです。壊れていることに気づくわけにはいかない、まさにその瞬間にだけ動きます。そこで私たちは、すべてのアラートルールに対して本物のシグナルを流し、通知が実際にキャプチャ用のリレーに届くまで合格としないハーネスを作りました。最初の実行で、看板となるエラー率アラートが完全に死んでいることがわかり、さらに悪いものも見つかりました。保持期間の設定が、気づかないうちにログへ即時期限切れを刻んでいたのです。そのため、取り込みは正常に見えるのに、ある種類のアラートはまるごと空のデータに対して評価されていました。
100 MB を超えるドキュメント用スクリーンショットを Cloudflare R2 に移し、アプリが実行時に自分のホスト名から導き出す CDN ベースの裏に置きました。ブラウザ上では問題なく動きましたが、オリジンのトラフィックは少しも減りませんでした。クローラーが見るページは 127.0.0.1 上でプリレンダリングされており、そこではホスト名が何の手がかりにもならないからです。解決策は、HTML がビルドから持ち出し、エッジがリクエストごとに埋めるプレースホルダーです。そしてそうせざるを得ない理由は、読み取り専用のファイルシステムにあります。
JIT(ジャストインタイム)プロビジョニングはデフォルトでオンになっていました。そのため、フェデレーションされた ID プロバイダーは、ある人物が存在すると主張するだけで、顧客のテナントにアカウントを作成できました。このデフォルトをオフにするのは 1 単語の変更です。本当の問題は、そのカラムを一度も書き込んだことのない保存済みのすべての接続について、動作を黙って変えることなくオフにすることでした。それを解決した仕掛けは、デフォルトが API の一部である理由も説明してくれます。
Backend for Frontend(BFF)は OAuth トークンをブラウザから取り除き、ブラウザには不透明な Cookie だけを持たせます。1 つの BFF で多数のテナントを捌くのは、ほとんどが配管の問題です。ただしバックチャネルログアウトが来るまでは。これは Cookie もセッションもブラウザもないサーバー間の POST で、テナントを特定する手がかりは、まだ検証していないトークンの中のクレームだけです。
未署名のイメージが絶対に動かないよう、Kubernetes クラスターに署名を検証するアドミッション Webhook を追加しました。数時間後、ある Deployment には 2,242 個の ReplicaSet ができていて、3 秒ごとに新しいものが生まれ、利用可能な Pod は 1 つもありませんでした。Webhook のログを見ると、すべてのリクエストを許可していました。被害の原因は、「mutating admission webhook」という言葉のうち、私たちが読み飛ばすようになっていた 1 語にありました。
再訪したユーザーは、ログイン画面が出るまで毎回きっかり 10 秒、「Loading…」を眺めていました。決め手はそのきりのいい数字です。SSO ライブラリが、X-Frame-Options: DENY と frame-ancestors 'none' を返す認証ホストに向けて、サイレントリニュー用の隠し iframe を開いていました。そのためフレームは読み込まれず、エラーも出ず、ライブラリはタイムアウトいっぱいまで待ち続けたのです。クリックジャッキング対策と OIDC のサイレントリニュー。それぞれは正しいのに、両者の継ぎ目で止まっていました。
保存時にすべてのメールアドレスを暗号化したところ、管理画面の検索ボックスがひっそりとオートコンプリートしなくなりました。エラーもログも出ません。ブラインドインデックスを使えば暗号文に対する完全一致検索は取り戻せますが、HMAC の役目はまさに、前方一致検索に必要な順序性を破壊することです。本記事では、すべてのプレフィックスをそれぞれ独立した値としてインデックスすることで入力補完を取り戻した設計と、検索可能暗号は暗号の問題ではなくシステムの問題だと教えてくれた 3 つの落とし穴を紹介します。
JWT の署名を RS256 から ES256 に移行したところ、発行したトークンはどれも暗号学的には正しいのに、どれも拒否されました。それも 3 回、3 つの異なる理由で。JWS が生の R‖S を求めている場所に DER を渡したこと、128 トークンに 1 つが失敗する整数のパディング、そして足元で変わった base64 の方言です。壊れたバイト列の読み方と、署名の長さだけで犯人のレイヤーを言い当てる方法を紹介します。
私たちの JWT を 1 つ検証するためにサービスが読む 2 つのドキュメント、ディスカバリードキュメントと JWKS は、私たちが配信するものの中で最もリクエストが多く、最も秘密性の低いものです。それなのに、取得のたびにオリジンまで届いていました。Cloudflare でキャッシュするのは簡単な部分です。難しいのは、古くなった公開鍵のキャッシュが有効なトークンを決して拒否してはならないという点です。積極的にキャッシュした JWKS を安全にする、ローテーションに強い手順を紹介します。
Duende の LITE は年額 $5,750 ですが、クライアントアプリは 2 つまでで、SAML はまったく含まれていません。エンタープライズ SSO や 3 つ目のアプリが必要になった瞬間、年額 $12,500 の STANDARD に移ることになります。実際のプロダクトにとって Duende が本当はいくらかかるのか、そして誰も警告してくれないクライアント数への課金について見ていきます。
私たちの認証サーバーが発行するすべてのトークンには、1 つの秘密鍵が署名しています。その鍵は生涯で 2 度引っ越しました。プロセスの中で生まれ、出られない Vault へと追放され、そしてまったく別の、より小さく速い種類の鍵として生まれ変わったのです。どの引っ越しも、トークンが飛び交っている稼働中の発行者の上で行われました。鍵の保管場所とアルゴリズムを変えるには実際に何が必要なのか、そして「よりセキュア」なコミットに紛れ込んでいた静かなリグレッションについて書きます。
あなたの SP はアサーションの署名を検証し、証明書をピン留めし、ユーザーをログインさせました。すべてのチェックに通りました。それでも私は、1 時間前に捕捉したログインをリプレイできました。手口は署名を破ることではありませんでした。署名がそもそもカバーしていないフィールドを削除することでした。
2 要素目の確認はログインページで行い、トークンエンドポイントでは一度も再確認していませんでした。そのため、正しいパスワードと手で書き換えた戻り先 URL があれば、MFA を素通りできたのです。その修正と、そこから学んだルールを紹介します。MFA はログイン画面の 1 ステップではなく、発行するセッションが持つ性質です。
SCIM は標準化された CRUD API で、その最も重要な操作は、退職した従業員のアカウントを無効にするものです。ベンダーはこれに 1 接続あたり月額 125 ドルの値をつけるか、SSO と並べてエンタープライズプランに置いています。SCIM 接続の提供に実際どれだけのコストがかかるのか、そしてデプロビジョニングに課金することがなぜセキュリティに課金することなのかをお話しします。
Azure Table Storage と DynamoDB には削除マーカーがありません。削除された行は単に消えるので、増分バックアップは削除をまったく認識できません。そのバックアップをリストアすると、オフボーディングしたユーザーが戻ってきます。ローテーションして捨てたはずのパスワードと一緒に。削除を第一級の扱いにしなければならない理由と、それを実現するたった 1 つのテーブルを紹介します。
私たちのレートリミッターとロックアウトカウンターは、クライアントの IP をキーにしていました。nginx の背後では、その IP はクライアントが自由に設定できるヘッダーから来ていました。つまりヘッダーを 1 つ変え続けるだけで、すべてのリクエストが新しい IP を手に入れ、IP 単位の制御はすべて崩れました。1 行で済む偽装と、Kubernetes 上では見た目以上に厄介な修正についてお話しします。
Azure Table Storage が提供するのは、パーティションキーと行キー、それ以外はほぼ何もありません。JOIN もなく、セカンダリインデックスと呼べるものもなく、インクリメントもありません。それでも私たちはマルチテナントの認証システムをその上で動かしています。それを可能にしているパターンと、SQL と比べて何を手放すのかを紹介します。
メールアドレスごとのレート制限がない、認証不要のパスワードリセットのエンドポイントは、あなただけの問題ではありません。他人の受信箱に向ければ、あなたのリセットフォームはスパム砲台になり、そのツケはあなたの送信者レピュテーションに回ります。どのメールアドレスにアカウントがあるかを漏らさずに直す方法を書きます。
ほとんどの認証ベンダーは、SSO、SCIM、MFA、監査ログをエンタープライズプランの柵の向こうに囲い込んでいます。私たちはそのすべてを $0 のプランに入れました。機能を無償で提供しても成り立つユニットエコノミクスを紹介します。壁は最初から機能ではなく、アクティブユーザー数とサポートなのです。
シングルサインオンとは、ID プロバイダーを信頼することです。私たちはそれを 1 つ余計に信頼していました。戻ってきたフェデレーションユーザーをアサーション内のメールアドレスで特定していたので、どの接続も他人のアドレスを主張して、その人のアカウントに入れたのです。修正はバリデーションの追加ではありませんでした。アイデンティティの結合キーを、メールアドレスから、主張する側が偽造できないプロバイダーごとのサブジェクトに変えることでした。
Azure Container Apps なら、アイドル状態の認証クラスターに払い続けるお金をなくせるはずでした。私たちは移行のコストを見積もり、そして計画を打ち切りました。理由は価格でもコールドスタートでもなく、リーダー選出を行う認証バックプレーンが本当に必要とする唯一の数を、サーバーレスが持っていないからです。
「機能税」を実際の請求書として示します。3 つの購入シナリオ(最初のエンタープライズ案件、数社の顧客、SSO が当然の前提になった段階)を各ベンダーの公開価格で試算しました。さらに、誰も警告してくれない静かな追加料金、つまり統合の途中で OAuth クライアントが足りなくなる問題も取り上げます。
3 レプリカのクラスターが、何度も自分自身と意見を食い違わせていました。原因は私たち自身のコードにありました。ピアディスカバリーの処理に、マルチキャストが失敗するとディスカバリーを無効にする catch ブロックがあり、マネージド Kubernetes ではマルチキャストは必ず失敗するのです。ゴシップを丸ごと削除し、代わりに Blob のリースでリーダーを選出するようにした理由と、推測するしかないメンバーシップより Storage Explorer で開けるメンバーシップのほうが優れている理由を紹介します。
ログイン機能なら週末で作れます。請求書が届くのはその後です。署名検証、SCIM によるデプロビジョニング、MFA、監査ログ、鍵のローテーション、侵害時の責任、そしてティア 0 のシステムのオンコール。自作が正解になる場合も含めて、自作か購入かを正直に検討します。
OIDC か SAML か。実際に選べることはほとんどありません。それぞれのプロトコルの正体、どちらがどんな場面で勝つのか、SAML の自前実装を危険にしている XML 署名の落とし穴、そして B2B プロダクトが結局は両方を必要とする理由を解説します。
Authagonal のサポートシステムを紹介します。更新ボタン不要のリアルタイムメッセージング、顧客の最初のメッセージで言語を固定する透明な双方向 AI 翻訳、そしてメールになってもすべて翻訳されたまま続くスレッド。シンプルで、見た目が良く、しかも本当に使えます。
私たちのテナントのうち 2 つが、人知れず同じテナントになっていました。データベースも、トークン署名鍵も同じで、名前だけが違う。原因は、ハイフンを取り除いてスラッグをサニタイズする 1 行のヘルパーで、一意の識別子を一意でないものに変えていました。情報が欠落する文字列変換がどのようにしてテナント間の乗っ取りになったのか、そしてなぜ皆さんより先に私たちが見つけられたのかをお話しします。
私たちは認証を売っているので、最も重要なテストは顧客になるテストです。1 回の Playwright 実行でテナントをサインアップし、すべての機能を設定し、本物のアプリをそこに向け、正しいクレームで本物のユーザーをログインさせ、さらにバックアップ、リストア、自己削除まで行います。徹底したエンドツーエンドテストが、本番リリースへの自信をどう支えているかを紹介します。
私たちのスタッフ用コンソールは Authagonal 自身を通して認証しており、Entra の SSO と SCIM がプロビジョニングを担っています。自分が自分の顧客になると、どのテストスイートも書こうと思いつかないエッジケースが表に出てきます。そのうち重要だったものを紹介します。
Terraform の state は、クラウド全体の地図であり、大量のシークレットの置き場でもあります。そしてデフォルトでは、パブリックなエンドポイントの向こうに置かれています。私たちがゼロからリモート state をブートストラップし、プライベートエンドポイントの背後に置き、VPN ゲートウェイの税を払う代わりにゼロトラストのコネクター経由でアクセスするようにした方法を紹介します。その間、一度も自分たちを締め出すことはありませんでした。
「全員がパスワードをリセットする」のは移行時の選択であって、物理法則ではありません。ID プロバイダーを乗り換えるときに、ユーザーの既存のパスワードハッシュを引き継いで切り替えを誰にも気づかせない方法と、それが本当にできない唯一のケースを紹介します。
認証業界が課金しているのはコストではなく、許可です。SSO、MFA、プロビジョニングといった、提供コストが実質ゼロの機能を、プランの壁や接続単位の料金の向こうに閉じ込めています。料金ページが、あなたに決して気づいてほしくない区別についてお話しします。
セルフホストの Duende IdentityServer からマネージドな認証へ移行する。SQL の設定と ASP.NET Identity から実際に何が移行できるのか、そして背負わなくてよくなる運用コストについて。
Auth0 からの移行に実際に何が伴うのかを実践的に見ていきます。簡単な部分、手間のかかる部分、そして Auth0 が本当に渡してくれない 1 つのものについてです。