كل عملية تحقق من رمز كانت تصل إلى خادمنا الأصلي. الآن يعيش JWKS على الحافة.
للتحقق من أحد رموز JWT لدينا، تجلب الخدمة مستندين عامّين من جهة الإصدار: مستند الاكتشاف عند /.well-known/openid-configuration، وJWKS الذي يشير إليه — أي مجموعة المفاتيح العامة التي وُقّعت بها الرموز. وهذان هما أكثر ما نقدّمه طلباً وأقلّه سرّيةً. إنهما مفاتيح عامة، عامة بحكم تعريفها، وبياناتٌ وصفية لا تتغير إلا حين نُدوّر. وحتى وقت قريب، كانت كل عملية من عمليات الجلب تلك تعود طوال الطريق إلى أصلنا (origin). ولأننا متعددو المستأجرين، كان الأمر أسوأ مما توحي به تلك الجملة.
كان الأصل يؤدي عملاً لا سبب له أن يؤديه
كل مستأجر جهة إصدار قائمة بذاتها، لها مستند .well-known خاص بها وJWKS خاص بها. وكل طرف معتمِد، وكل خادم موارد، وكل SDK يتحقق من رمز يسحب تلك المستندات — عند بدء بارد، وعند انتهاء ذاكرته المؤقتة القصيرة، مرةً لكل نسخة خدمة. اضرب جهاتِ الإصدار لكل مستأجر في المُتحقِّقين لكل خدمة في مرّات فشل الذاكرة المؤقتة، فتحصل على سيلٍ جارف من الطلبات، ينتهي كله عند التطبيق، وكله من أجل مستندات متطابقة بايتاً ببايت لكل طالب ولا تتغير إلا حين نُدوّر مفتاحاً.
هذه هي المواصفات النموذجية لشيء ينبغي تخزينه مؤقتاً: عام، ومتطابق، ونادر التغيّر. لكن التطبيق بدلاً من ذلك كان يولّد المفاتيح العامة، ديناميكياً، على المسار الساخن لعملية التحقق من رموز الجميع. لم يكن هناك أي مبرر لحساب المفاتيح مع كل طلب.
وضعهما على الحافة
التغيير نفسه صغير: اضبط ترويسات Cache-Control صادقة على استجابتَي الاكتشاف وJWKS لكي يخزّنهما Cloudflare مؤقتاً على الحافة. الآن يصل المُتحقِّق إلى أقرب نقطة تواجد لـ Cloudflare، ويقدّم الأصل كل مستند مرةً واحدة تقريباً لكل PoP لكل TTL بدلاً من مرة واحدة لكل عملية تحقق. الاكتشاف قابل للتخزين المؤقت ببساطة — فهو لا يتغير تقريباً أبداً. أما JWKS فهو الذي يجب أن تفكر فيه ملياً، لأن JWKS هو المستند الوحيد الذي وظيفته بأكملها أن يتغير بالضبط حين تُدوّر مفتاحاً، وتخزينُ شيء يجب أن يكون طازجاً في اللحظة الوحيدة التي يتغير فيها هو المكان الذي يؤذي فيه الناس أنفسهم.
كيف يعضّك JWKS المُخزَّن مؤقتاً
تدوير المفتاح مع مجموعة مفاتيح مخزّنة مؤقتاً فخّ، وهو يفشل في أسوأ اتجاه ممكن. دوّرْ إلى مفتاح توقيع جديد، وابدأ بإصدار رموز به، فإذا بمُتحقِّق جلب JWKS قبل ظهور المفتاح الجديد يحمل نسخة قديمة. يصل رمز حاملاً معرّف المفتاح الجديد؛ فيبحث عنه المُتحقِّق، ولا يجده، فيرفض الرمز. الرمز صالح تماماً. والتوقيع حقيقي. والمستخدم لم يرتكب أي خطأ. ذاكرتُك المؤقتة على الحافة نفسها أعادت للتو 401 لعملية تسجيل دخول مشروعة.
لاحظ في أي اتجاه يفشل ذلك. مفتاحٌ خاص قديم لا يعني شيئاً — أنت ببساطة لم تبدأ بعد باستخدام الجديد. أما مجموعةُ مفاتيح عامة قديمة فهي انقطاعٌ تُلحِقه بنفسك، وأنت من أدخل الذاكرة المؤقتة التي سبّبته عمداً، لتوفير حركة المرور إلى الأصل. سلامة المخطط بأكمله تحيا أو تموت على التدوير.
النمط الآمن للتدوير
السلامة كلها في ترتيب العمليات، وهذا الترتيب هو نقيض التبديل الذرّي. ثلاث قواعد:
- انشر قبل أن توقّع. يدخل المفتاح الجديد إلى JWKS أولاً، ولا توقّع به رمزاً واحداً حتى يكون ذلك JWKS — بمعرّف المفتاح الجديد وكل شيء — قد بقي حيّاً على الحافة لمدة TTL كاملة واحدة للذاكرة المؤقتة على الأقل. فبحلول الوقت الذي يمكن فيه لأي رمز يحمل المفتاح الجديد أن يصل إلى مُتحقِّق، تكون مجموعة المفاتيح المخزّنة لدى المُتحقِّق تحتوي على المفتاح بالفعل.
- أحِلْه إلى التقاعد بعد الانتهاء، لا عند التدوير. يبقى المفتاح القديم منشوراً في JWKS حتى ينتهي آخر رمز وقّعه على الإطلاق. القديم والجديد يتعايشان طوال فترة التداخل كاملةً. اسحب المفتاح القديم لحظةَ التدوير فتترك كل رمز وقّعه ولا يزال صالحاً عالقاً بلا سند.
- TTL أقصر من فترة التداخل. يجب أن يكون
max-ageللذاكرة المؤقتة على الحافة، مضافاً إليه هامشٌ لانحراف الساعة، أقصرَ من نافذة تداخل المفاتيح. يُسمح للذاكرة المؤقتة أن تكون قديمة؛ لكن لا يُسمح لها أبداً أن تكون قديمة لمدة أطول من النافذة التي يكون فيها كلا المفتاحين صالحاً. تلك المتباينة الواحدة هي ما يجعل JWKS المُخزَّن مؤقتاً بقوة آمناً.
وحين يُجمَع ذلك معاً، يصير التدوير تلاشياً متبادلاً (crossfade) بترتيب تشغيل ثابت: أضِف المفتاح الجديد، وانتظر TTL كي تراه الحافة وكل مُتحقِّق، ثم ابدأ بالتوقيع به، وانتظر حتى تتقادم الرموز القديمة وتزول، ثم أسقِط المفتاح القديم. كل خطوة آمنة في وجه ذاكرة مؤقتة متأخرة بمقدار TTL، لأن TTL اختير ليكون أصغر من كل نافذة قد تتأخر خلفها الذاكرة المؤقتة.
الدرس، مُستخلَصاً
التخزين المؤقت عادةً قرارٌ يتعلق بزمن الاستجابة. أما تخزين المفاتيح العامة مؤقتاً فقرارٌ يتعلق بالصحة، لأن الشيء الذي تخزّنه هو الشيء الذي يقرر أي الرموز حقيقي. يمكنك تماماً أن تضعه على الحافة — بل ينبغي لك، لأنه عام ومطلوب بكثافة — لكن فقط بعد أن يُعبَّر عن التدوير بوصفه تداخلاً مرتّباً بدلاً من تبديل، وفقط بعد أن يكون TTL للذاكرة المؤقتة أقصرَ بشكل مُثبَت من ذلك التداخل. أتقِنْ ذلك، فتكفّ أكثرُ المستندات ازدحاماً وأكثرُها قابليةً للتخزين المؤقت في نظام مصادقتك عن ملامسة أصلك، ولا يلاحظ أحدٌ الفرق أبداً، وهذا هو بيت القصيد تماماً.