شَرطة واحدة، مستأجران اثنان، ومفتاح توقيع واحد
اثنان من مستأجرينا كانا المستأجر نفسه. كانت لهما أسماء مختلفة، وعمليات تسجيل مختلفة، وصفوف فوترة مختلفة. كما تشاركا قاعدة بيانات ومفتاح توقيع رموز، ولم يكن لدى أيٍّ منا نحن الثلاثة، المستأجران الاثنان أو نحن، أدنى فكرة عن ذلك. هذه قصة الدالة المكوّنة من سطر واحد التي دمجتهما، ولماذا بدت كل طبقة في النظام صحيحة تمامًا أثناء حدوث ذلك، ولماذا يُعدّ التدقيق الأمني قبل الإطلاق أرخص تأمين ستشتريه على الإطلاق.
للمصادقة متعددة المستأجرين مهمة واحدة لا يجوز لها أن تخطئ فيها: إبقاء المستأجرين منفصلين. يجب ألا يكون مستخدمو Acme، وجلسات Acme، وقبل كل شيء مفتاح توقيع Acme، في متناول أي شخص آخر أبدًا. مفتاح التوقيع هو جوهرة التاج. من يستطيع التوقيع بمفتاح Acme يستطيع سَكّ رمز سيقبله خادم المصادقة الخاص بـ Acme نفسه باعتباره أصيلًا، لأي مستخدم، وبأي دور، دون الحاجة إلى كلمة مرور. لذا فإن كل مورد لكل مستأجر نُنشئه، وكل جدول تخزين وكل مفتاح، نُسنده إلى نطاق اسمي خاص باستخدام الـ slug الخاص بالمستأجر. اضبط هذا الإسناد بدقة وسيغدو المستأجرون جزرًا منفصلة. أخطئ فيه على نحو خفي وسيصيران بهدوء المكان نفسه.
الخطأ المكوّن من سطر واحد
إليك الدالة التي تُحوّل الـ slug الخاص بالمستأجر إلى البادئة التي نُسمّي بها تخزينه ومفاتيحه. اقرأ تعليق التوثيق. إنه يوثّق الخطأ وكأنه ميزة.
/// Derive the table name prefix from a tenant slug by stripping hyphens.
/// E.g. "acme-corp" → "acmecorp".
public static string GetTablePrefix(string tenantSlug)
{
return tenantSlug.Replace("-", "");
}
Replace("-", ""). إنها تُزيل الشرطات. كان القصد تنظيميًا: تتدفق الـ slugs إلى أسماء جداول Azure Table وأسماء مفاتيح Vault، التي لها قواعدها الخاصة بالأحرف، فقمنا بتنظيفها. المشكلة أن إزالة الأحرف تحويلٌ فاقد للبيانات، والتحويل الفاقد للبيانات على مُعرّف ليس تحويلًا أحاديًا. كلٌّ من acme-corp وacmecorp يخرج باعتباره acmecorp. وكذلك ac-me-corp وacme--corp. slugs مختلفة، ونطاق اسمي واحد.
ذلك النطاق الاسمي هو كل شيء في المراحل اللاحقة. جدول المستخدمين هو {prefix}-Users. ومفتاح التوقيع لكل مستأجر هو، حرفيًا، هذا:
private string GetKeyName() => $"signing-{ShardRouter.GetTablePrefix(_tenantContext.Slug)}";
لذا فإن acme-corp وacmecorp لا يتشاركان قائمة مستخدمين فحسب. إنهما يوقّعان رموزهما بمفتاح signing-acmecorp نفسه في Vault. الرمز المسكوك لأحدهما هو، بايتًا ببايت، رمز موقَّع بصلاحية للآخر. إذا استطعت تسجيل slug ينهار إلى البادئة نفسها لمستأجر قائم، فبإمكانك أن تُصدر لنفسك رموزًا يثق بها خادم المصادقة الخاص بهم ثقة كاملة. استيلاء على الحساب عبر المستأجرين، والثغرة هي ببساطة "سجّل بشرطة".
لماذا لم يُمسك بالخطأ أي شيء
الجانب المُقلق هو مدى اعتيادية كل طبقة في مظهرها. تَحقّق التسجيل من الـ slug فرأى نصًا جديدًا غير مستخدَم. أنشأت عملية التهيئة سجلَّ مستأجر مُسنَدًا إلى الـ slug الكامل، acme-corp، وهو فعلًا متمايز عن acmecorp في مستوى التحكم. اشتقّت عملية التحقق من الرمز المفتاحَ من الـ slug وتحقّقت بسعادة. أدّى كل مكوّن مهمته بصورة صحيحة على مُدخلاته الخاصة.
لم يقارن أي شيء في النظام قط بادئتَيْ slug اثنين، لأنه لم يكن هناك مكوّن واحد يملك القاعدة الثابتة "الـ slug يُسنَد إلى نطاق اسمي واحد بالضبط". عاش التصادم في الفجوة بين الـ slug، الذي يُسنِد إليه مستوى التحكم، والبادئة، التي يُسنِد إليها التخزين ومفاتيح Vault. لم يكن أحد واقفًا في تلك الفجوة. هذه هي بصمة أخطر صنف من الأخطاء: ليس تحقّقًا نسيه أحد، بل افتراض لم يعلم أحد أنه يفترضه.
التصادم الثاني، بلا مقابل
كان للتحويل الفاقد للبيانات نفسه ضحية ثانية. تُسمّى مستأجرات النظام الداخلية لدينا باللاحقة: {slug}-admin يحتوي على فريق البوابة الخاص بالمستأجر، و{slug}-sandbox يحتوي على بيئته الاختبارية. مرِّر هذه عبر مُزيل الشرطات نفسه فيُصبح acme-admin هو acmeadmin. ما يعني أن عميلًا سجّل الـ slug acmeadmin سينهار إلى بادئة مستأجر الإدارة الخاص بـ Acme، المكان الوحيد الذي يُفترض أن يكون أكثر امتيازًا من العميل، لا أن يُشارَك مع واحد.
دالة إزالة واحدة، وحدّان مختلفان من حدود العزل مهدَّدان: مستأجر إلى مستأجر، ومستأجر إلى مستوى التحكم الخاص به. حين يهدّد سطر واحد حدّين غير مرتبطين، فتلك هي العلامة على أنه خطأ في الجذر لا في السطح. لا بدّ أن يقع الإصلاح على التحويل، لا على أيٍّ من العَرَضَين.
الإصلاح: أحادي بحكم البنية
الغريزة تدفع إلى جعل دالة البادئة أذكى. اهرب من الشرطات، جزّئ الـ slug بدالة تجزئة، رمّزه بترميز base32. كلٌّ من هذه لا يزال تحويلًا عليك أن تُثبت أنه أحادي إلى الأبد، في مواجهة كل تغيير مستقبلي، على يد شخص قد لا يعرف لماذا يهمّ ذلك. الإصلاح الأرخص والأكثر متانة بكثير هو إزالة حرية التحويل تمامًا: قيِّد المُدخل بحيث يكون التحويل هو دالة الهوية.
// Lowercase alphanumeric ONLY, no hyphens. Forbidding hyphens makes prefix == slug,
// so distinct slugs can never share a data store or signing key.
if (!slug.All(c => (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')))
return false;
أصبحت الـ slugs الآن أحرفًا صغيرة وأرقامًا، لا شيء غير ذلك. ومع عدم وجود شرطات لإزالتها، لم يَعُد لدى GetTablePrefix ما يفعله، وprefix == slug يصحّ بحكم البنية، ولن يستطيع slugان متمايزان مشاركة نطاق اسمي مجددًا أبدًا. كما نرفض الأسماء المحجوزة وأي slug ينتهي بـ admin أو sandbox، ما يُغلق تصادم مستأجر النظام في السطر نفسه. تَحقّق الصلاحية هو النقطة الضيقة التي يُصبح فيها الـ slug نطاقًا اسميًا لأول مرة، فهذا بالضبط حيث ينتمي ضمان التطابق واحدًا لواحد.
ثم نؤكّد القاعدة نفسها مرة أخرى، عند نقطة الاختناق في التهيئة. هناك بابان يمكن للـ slug الدخول عبرهما، التسجيل الذاتي والتهيئة المدفوعة من الإدارة، والقاعدة الأمنية الثابتة المُدافَع عنها عند باب واحد فقط من بابين هي قاعدة غير مُدافَع عنها عند أيٍّ منهما.
لماذا نُخبرك بهذا
اكتشفنا هذا في تدقيق أمني قبل الإطلاق لمنتجنا نفسه، قبل وجود أي عميل دافع واحد، وهو الوقت المقبول الوحيد لاكتشافه. لم يُدمَج أي مستأجر قط في الإنتاج. لكنه خطأ يبعث على التواضع، لأنه ليس تحقّقًا مفقودًا ولا خوارزمية ضعيفة. إنه دالة مساعدة تفعل شيئًا معقولًا تمامًا، تنظيف نص، في موضع يتبيّن فيه أن "معقول" و"أحادي" كلمتان مختلفتان.
عاش الدرس بعد الإصلاح. أي دالة تُحوّل مُدخلًا يتحكم فيه المستخدم إلى اسم حدّ أمني، جدول أو مفتاح أو نطاق اسمي أو مسار، يجب أن تكون أحادية، وينبغي أن تفرض ذلك عند أضيق نقطة يُنشأ فيها التطابق، لا أن تأمل أن ينجو عبر كل طبقة لاحقة. خطوة التطبيع، تحويل إلى أحرف صغيرة أو اقتطاع أو إزالة أو طيّ، هي بالضبط الموضع الذي تتحول فيه هويتان بهدوء إلى هوية واحدة.
إنه المبدأ نفسه الذي بُني عليه المنتج بأكمله. الأمان ليس فئة تترقى إليها؛ إنه الأرضية. كل ضمان عزل، وكل مفتاح توقيع، وكل ميزة أمنية نُطلقها، SSO وSAML، وSCIM، وMFA، وتصدير التدقيق، مُفعّلة في كل خطة، لأن البديل هو من نوع الأشياء التي تجدها في الثانية صباحًا بدلًا من أن تجدها في تدقيق. انظر ما المُتضمَّن.