لنفرض أن لديك سيرفراً واحداً بعنوان IP واحد، وعليه متجر الشركة ومدونتها وواجهة API، ولكل منها نطاق مختلف، فكيف يعرف السيرفر أي شهادة يرسل عندما يصله اتصال على المنفذ 443، والاتصال لم يقل بعد أي موقع يريد؟ وهل الأفضل أن تصدر شهادة لكل نطاق، أم شهادة واحدة تحمل الأسماء كلها، أم شهادة شاملة Wildcard تغطي كل النطاقات الفرعية؟ وقد شرحنا في دليل الشهادات الرقمية ما بداخل الشهادة وكيف تفحص في المصافحة، وهذا المقال يكمل من هناك ويركز على سؤال واحد: سيرفر واحد وأسماء كثيرة.
وسوف نناقش في هذا المقال ما يلي:
- كيف تعمل إشارة اسم الخادم Server Name Indication واختصاراً SNI، وماذا يحدث للكلاينت الذي لا يرسلها.
- الطرق الثلاث لتغطية أسماء كثيرة: شهادة لكل اسم، والشهادة متعددة الأسماء SAN، والشهادة الشاملة Wildcard، ومتى تناسب كل واحدة.
- حدود Let's Encrypt في عدد الأسماء والشهادات.
- نطاق الضرر Blast Radius عندما تنتهي شهادة تحمل أسماء كثيرة أو يتسرب مفتاحها.
- ما الذي تكشفه الشهادة والاتصال عن أسمائك: سجلات الشفافية، وإشارة SNI على الشبكة، ووضع Encrypted Client Hello اليوم.
- التوجيه بإشارة SNI في الطبقة الرابعة دون فك التشفير.
- تجربة عملية بمخرجات حقيقية على Nginx وHAProxy وOpenSSL مع جهة إصدار محلية.
كيف يعرف السيرفر أي شهادة يرسل؟
في أيام HTTP العادي لم تكن هناك مشكلة، فالمتصفح يرسل اسم الموقع في الترويسة Host داخل الطلب، والسيرفر يقرؤها ويختار الموقع. أما في HTTPS فالشهادة يجب أن تصل قبل أي طلب، لأن الطلب نفسه سوف يمر مشفراً بعد المصافحة، وبالتالي لا يرى السيرفر الترويسة Host إلا بعد أن يكون قد اختار الشهادة وأرسلها. وكان الحل البسيط قديماً أن تعطي كل موقع HTTPS عنوان IP مستقلاً، وهذا الحل غير مناسب إطلاقاً اليوم لأن عناوين IPv4 قليلة ومكلفة.
والحل الذي يعمل به الإنترنت اليوم هو الامتداد server_name في رسالة ClientHello، المعرف في RFC 6066 القسم 3، وهو ما نسميه إشارة اسم الخادم SNI، فالكلاينت يكتب فيه اسم النطاق الذي يريده في أول رسالة من المصافحة، والسيرفر يقرؤه ويختار الشهادة المطابقة قبل أن يرد، وقد رأينا مكانه في خطوات المصافحة في قسم مصافحة TLS 1.3. الصورة التالية تبين الفكرة:

وقد يتساءل البعض: ماذا يحدث إذا لم يرسل الكلاينت إشارة SNI أصلاً؟ والإجابة أن السيرفر يرسل الشهادة الافتراضية Default Certificate، وهي في Nginx شهادة الـ server المعلم بـ default_server على هذا المنفذ، وفي HAProxy الشهادة الأولى في سطر bind كما شرحنا في قسم الشهادة الافتراضية في دليل HAProxy. وهذه الحالة أكثر مما تتوقع، وأهم أسبابها ما يلي:
- الاتصال بعنوان IP مباشرة: مثل
https://203.0.113.10/، فالمعيار نفسه يمنع وضع عنوان IP في إشارة SNI، ولذلك لا ترسل المتصفحات وcurl أي اسم في هذه الحالة. - أدوات الفحص ومراقبة المنافذ: بعض فاحصات الصحة Health Checks وماسحات المنافذ تتصل دون اسم.
- برامج قديمة جداً: مكتبات TLS قديمة في أجهزة مدمجة أو أنظمة لم تحدث منذ سنوات.
- اسم لا يعرفه السيرفر: هنا يرسل الكلاينت إشارة SNI ولكن لا توجد شهادة تطابقها، فيحصل أيضاً على الشهادة الافتراضية.
لذلك اجعل الشهادة الافتراضية شهادة لا تحمل أي اسم حقيقي، مثل CN=invalid، ولا تجعلها شهادة موقعك الرئيسي، والسبب أن الشهادة الافتراضية تعطى لكل من يطرق الباب بعنوان IP، فإذا كانت تحمل أسماء نطاقاتك فأنت تخبر كل ماسح في الإنترنت بأسماء مواقعك على هذا العنوان.
ثلاث طرق لتغطية أسماء كثيرة
بعد أن عرف السيرفر الاسم من إشارة SNI، يبقى أن تقرر كيف تكون الشهادات نفسها، ونوعا الشهادة متعددة الأسماء والشاملة معرفان باختصار في قسم أنواع الشهادات، أما هنا فالسؤال عن الثمن الذي تدفعه مع كل طريقة. الصورة التالية تضع الطرق الثلاث جنباً إلى جنب:

والجدول التالي يلخص الفروق بين الطرق الثلاث:
| السؤال | شهادة لكل اسم | شهادة متعددة الأسماء SAN | الشهادة الشاملة Wildcard |
|---|---|---|---|
| عدد المفاتيح الخاصة | مفتاح لكل اسم | مفتاح واحد لكل الأسماء | مفتاح واحد لكل النطاقات الفرعية |
| إذا فشل التجديد | يتوقف اسم واحد | تتوقف كل الأسماء معاً | تتوقف كل النطاقات الفرعية معاً |
| إذا تسرب المفتاح | تلغي شهادة اسم واحد | تلغي الشهادة لكل الأسماء | يستطيع المهاجم انتحال أي اسم من المستوى نفسه |
| ما يراه الغريب في الشهادة | اسم واحد | كل الأسماء في القائمة | النمط فقط مثل *.apps.example.com |
| إضافة اسم جديد | شهادة جديدة | إعادة إصدار الشهادة كلها | لا تحتاج شيئاً إذا كان في المستوى نفسه |
| التحقق في Let's Encrypt | HTTP-01 أو DNS-01 | HTTP-01 أو DNS-01 لكل اسم | DNS-01 فقط |
متى تناسب كل طريقة؟
لا توجد طريقة صحيحة دائماً، وإنما تختار بحسب من يملك الأسماء ومن يدير السيرفرات، والحالات الشائعة كما يلي:
- نطاقات العملاء في منصة SaaS: شهادة لكل نطاق دائماً، والسبب أن كل عميل يملك نطاقه وقد يغادر في أي يوم، ووضع نطاقات عملاء مختلفين في شهادة SAN واحدة يعني أن كل عميل يرى أسماء الآخرين، وأن خروج عميل واحد يجبرك على إعادة إصدار شهادة الجميع. وقد شرحنا طرق إصدار شهادة نطاق العميل في قسم النطاق الفرعي للعميل وشهادته، وما يدعمه موزع الحمل المدار عند كل مزود في قسم الشهادات على موزع الحمل المدار.
- الاستضافة المشتركة Shared Hosting: المنطق نفسه، فكل موقع لصاحب مختلف، لذلك شهادة لكل موقع مع تجديد آلي عبر ACME.
- أسماء سيرفر البريد: شهادة SAN واحدة مناسبة هنا، مثل
mail.example.comوsmtp.example.comوimap.example.com، لأنها الخدمة نفسها على السيرفر نفسه، وتنتهي وتتجدد معاً على أي حال. - النطاق الرئيسي مع www:
example.comوwww.example.comفي شهادة SAN واحدة هي الحالة الأكثر شيوعاً على الإطلاق، وهي مناسبة تماماً. - بيئات مؤقتة كثيرة لفريق واحد: مثل
pr-123.preview.example.comلكل طلب دمج Pull Request، وهنا تناسب الشهادة الشاملة*.preview.example.com، فلا تصدر شهادة لكل بيئة، ولا تظهر أسماء البيئات في سجلات الشفافية. - الخدمات الداخلية: أسماء مثل
grafana.corp.example.comلا يجب أن تظهر في شهادة عامة، كما سنرى في قسم الخصوصية، لذلك استخدم لها جهة إصدار داخلية مثل step-ca، أو mkcert على جهاز المطور. - Kubernetes Ingress: تضع في الجزء
tlsمن كل Ingress أسماء النطاقات واسم ال Secret الذي يحمل الشهادة، ووثائق Kubernetes تنص على أن الأسماء المختلفة تتشارك المنفذ 443 ويختار ال Ingress Controller بينها بحسب إشارة SNI، فالقرار هو نفسه: Secret لكل خدمة، أو Secret واحد بشهادة SAN أو شاملة لعدة خدمات.
حدود Let's Encrypt
إذا كنت تعتمد على Let's Encrypt فهناك حدود تؤثر في اختيارك، والجدول التالي منقول من صفحة حدود الإصدار Rate Limits كما كانت في أكتوبر 2026، وتاريخ آخر تعديل عليها 5 أغسطس 2026:
| الحد | القيمة |
|---|---|
| الأسماء في الشهادة الواحدة | حتى 100 اسم أو عنوان IP بحسب ملف الشهادة Profile |
| الشهادات لكل نطاق مسجل Registered Domain | 50 شهادة كل 7 أيام |
| الشهادات للمجموعة نفسها من الأسماء | 5 شهادات كل 7 أيام |
| الطلبات Orders لكل حساب | 300 طلب كل 3 ساعات |
| فشل التحقق | 5 مرات لكل اسم ولكل حساب في الساعة |
ولاحظ أن حد الخمسين شهادة لكل نطاق مسجل يحسب على مستوى example.com كله، فإذا كانت منصتك تعطي كل عميل نطاقاً فرعياً مثل client1.saas.example وأصدرت شهادة لكل واحد فسوف تصل إلى الحد بسرعة، وهذه من الحالات التي تناسبها الشهادة الشاملة *.saas.example. وتنص الصفحة نفسها على أن الأفضل من ناحية الأداء أن تقلل عدد الأسماء في الشهادة الواحدة متى استطعت، وأخطاء الحدود وكيف تقرؤها مشروحة في قسم too many certificates في دليل Caddy.
شهادة واحدة لكل شيء: نطاق الضرر
الشهادة متعددة الأسماء مريحة لأنها ملف واحد وتجديد واحد، ولكن هذه الراحة هي نفسها المشكلة، فكل ما يحدث لها يحدث لكل الأسماء في الوقت نفسه، ونطاق الضرر Blast Radius يظهر في ثلاث حالات:
- فشل التجديد: إذا فشل التجديد لاسم واحد في القائمة، مثلاً لأن سجل DNS لنطاق قديم تغير ففشل التحقق منه، فإن الشهادة الجديدة لا تصدر أصلاً، وتنتهي القديمة على كل الأسماء معاً.
- تسرب المفتاح: المفتاح الخاص للشهادة موجود على كل سيرفر يقدم أي اسم منها، فإذا اخترق أضعف هذه السيرفرات فقد تسرب المفتاح الذي يثبت هوية كل الأسماء.
- الإلغاء Revocation: الإلغاء يكون للشهادة كاملة وليس لاسم واحد فيها، فإلغاؤها بسبب مشكلة في اسم واحد يوقف كل الأسماء حتى تصدر شهادة جديدة.
الصورة التالية تبين نتيجة التجربة العملية التي سوف نراها بعد قليل:

لذلك لا تضع في شهادة SAN واحدة إلا أسماء تعيش وتموت معاً، أي الخدمة نفسها على السيرفرات نفسها وتحت إدارة الفريق نفسه، وراقب تاريخ انتهاء كل شهادة بغض النظر عن طريقة التجديد، كما في خطوة متى تنتهي الشهادة.
ما الذي تكشفه عن أسمائك؟
هناك مكانان يقرأ منهما الغريب أسماء نطاقاتك دون أي صلاحية: الشهادة نفسها، والاتصال على الشبكة.
قائمة SAN يقرؤها أي أحد
الشهادة بيانات عامة يرسلها السيرفر لكل من يتصل به، فإذا وضعت في شهادة المتجر اسم jenkins.corp.example.com لأنه على السيرفر نفسه، فأي زائر للمتجر يستطيع أن يقرأ هذا الاسم بأمر واحد، كما سوف نرى في التجربة. والأهم أن كل شهادة عامة تسجل قبل إصدارها في سجلات الشفافية Certificate Transparency التي شرحناها في قسم سجلات الشفافية، وهذه السجلات عامة ولا يحذف منها شيء، وتستطيع البحث فيها باسم نطاقك في crt.sh، وهي أيضاً من أول ما يبحث فيه المخترق عندما يجمع معلومات عن هدفه. والنتيجة أن الشهادة العامة لاسم داخلي تنشر هذا الاسم للأبد، حتى لو حذفت الشهادة من السيرفر بعد يوم.
وهذا سبب يجعل الشهادة الشاملة Wildcard مفيدة أحياناً، فهي تكشف النمط *.apps.example.com فقط وليس أسماء الخدمات تحته. ولكن لا تعتمد على إخفاء الاسم كوسيلة حماية، فالأسماء تتسرب من سجلات DNS ومن الروابط ومن إشارة SNI نفسها، وإنما احم الخدمة الداخلية بأن تكون في شبكة خاصة لا تصلها من الإنترنت أصلاً.
إشارة SNI ظاهرة على الشبكة
رسالة ClientHello ترسل قبل الاتفاق على أي مفتاح، ولذلك فإن إشارة SNI فيها تمر دون تشفير، ويقرؤها كل من يرى الحركة: مزود الإنترنت، والشبكة في المقهى، وجدار الحماية في الشركة، وقد ذكرنا ذلك في قسم HTTPS. وهذا بالضبط ما يستخدمه HAProxy في التوجيه في الطبقة الرابعة كما سوف نرى في القسم التالي، وما تستخدمه جدران الحماية لحجب المواقع بالاسم.
هل يحل Encrypted Client Hello المشكلة؟
الحل الذي عملت عليه IETF سنوات هو تشفير رسالة ClientHello نفسها، واسمه Encrypted Client Hello واختصاراً ECH، وفكرته أن الكلاينت يرسل رسالتين داخل بعض: رسالة خارجية Outer ClientHello فيها اسم عام لمزود الخدمة، ورسالة داخلية Inner ClientHello فيها الاسم الحقيقي مشفرة بمفتاح عام ينشره الموقع في سجل HTTPS في DNS. ووضعه في أكتوبر 2026 كما يلي:
- المعيار: نشر في مارس 2026 كـ RFC 9849 في مسار المعايير Proposed Standard، وطريقة نشر مفتاح ECH في سجلات SVCB وHTTPS في RFC 9848.
- Chrome: مفعل افتراضياً منذ الإصدار 117 على سطح المكتب وAndroid بحسب صفحة الميزة في Chrome Platform Status، ويمكن للشركات تعطيله بالسياسة
EncryptedClientHelloEnabled. - Firefox: متاح منذ الإصدار 119 كما في ملاحظات إصدار Firefox 119، ويعتمد بحسب صفحة دعم Mozilla على DNS عبر HTTPS واختصاراً DoH للحصول على المفتاح.
- السيرفرات: OpenSSL أضاف دعم ECH في الإصدار 4.0.0 بتاريخ 14 أبريل 2026 كما في سجل أخبار OpenSSL، وNginx أضاف التوجيه ssl_ech_file في الإصدار 1.29.4 مع فرع ECH في OpenSSL كما في سجل تغييرات Nginx.
ولاحظ أن ECH لا يعمل إلا إذا اجتمع كل ذلك: متصفح يدعمه، وDNS مشفر يصل منه سجل HTTPS، وسيرفر أو مزود CDN ينشر المفتاح ويفك الرسالة الداخلية، والأداة التي استخدمناها في التجربة، أي OpenSSL 3.5، لا تدعمه أصلاً. وحتى مع ECH تبقى سجلات الشفافية تنشر أسماء الشهادات العامة، فهو يخفي اسم الموقع عن الشبكة وليس عن العالم. وإذا كنت تعتمد على قراءة إشارة SNI في جدار الحماية أو في موزع حمل من الطبقة الرابعة فاختبر سلوكه مع ECH، والسبب أن الموزع سوف يرى الاسم العام الخارجي فقط.
التوجيه بإشارة SNI دون فك التشفير
بما أن إشارة SNI تمر دون تشفير، فإن موزع حمل من الطبقة الرابعة يستطيع أن يقرأها ويقرر إلى أي سيرفر يمرر الاتصال، دون أن يملك الشهادة أو المفتاح، وقد شرحنا الفرق بين الطبقتين وما يراه كل منهما في قسم الطبقة الرابعة والطبقة السابعة. الصورة التالية تبين هذا الشكل:

ويفيد هذا الشكل، الذي يسمى أيضاً TLS Passthrough، في الحالات التالية:
- المفتاح يجب أن يبقى عند صاحبه: مثل خدمة عميل أو فريق آخر يدير شهادته بنفسه، فلا يوضع مفتاحها على موزع الحمل المشترك.
- بروتوكولات غير HTTP فوق TLS: مثل قاعدة بيانات أو وسيط رسائل خلف المنفذ نفسه، فالموزع لا يحتاج أن يفهم البروتوكول.
- مدخل واحد Ingress بعنوان ثابت: كما في تصميم المدخل والمخرج، مع ترك إنهاء TLS لكل خدمة خلفه.
أما ثمنه فإن الموزع لا يرى المسار ولا الترويسات، فلا يستطيع التوجيه بهما ولا إضافة ترويسة X-Forwarded-For، ويحتاج السيرفر الخلفي إلى PROXY Protocol إذا أراد عنوان الزائر الحقيقي.
وماذا عن آلاف النطاقات؟
إذا كان العملاء يضيفون نطاقاتهم بأنفسهم فلن تكتب سطراً في الإعداد لكل نطاق، وإنما تحتاج سيرفراً يصدر الشهادة عند أول اتصال بالاسم الجديد بعد أن يسأل نظامك هل هذا النطاق مسجل عندك، وهي ميزة On-Demand TLS في Caddy التي شرحنا استخدامها مع نقطة السؤال ask في قسم CNAME إلينا والتحقق عبر HTTP، والإعداد العام لـ Caddy في دليل Caddy. ولاحظ أن هذه الميزة تعتمد على إشارة SNI بالكامل، فالاسم الذي تصدر له الشهادة هو الاسم الذي وصل في ClientHello.
تجربة عملية: ثلاثة نطاقات على سيرفر واحد
سوف نبني في هذه التجربة جهة إصدار محلية، ونشغل Nginx بثلاثة نطاقات مرة بشهادة لكل نطاق ومرة بشهادة SAN واحدة، ثم نقرأ الشهادة كما يقرؤها الغريب، ونكسر الشهادة بانتهاء صلاحيتها، ونختبر حدود الشهادة الشاملة، ثم نضع HAProxy أمام السيرفرين يوجه بإشارة SNI دون أن يفك التشفير. والتجربة خفيفة عمداً، فكل ال Containers من Docker Images صغيرة مبنية على Alpine، ولا يوجد اتصال بأي جهة إصدار عامة، والنطاقات تحت lab.test لا وجود لها خارج الشبكة الداخلية لـ Docker. والإصدارات المستخدمة كما يلي:
- Alpine 3.22.6 مع OpenSSL 3.5.9 وcurl 8.14.1 في Container الأدوات.
- Nginx 1.31.6 من
nginx:1.31.6-alpine. - HAProxy 3.4.6 من
haproxy:3.4.6-alpine. - Docker Engine 29.8.0.
الخطوة 1: جهة إصدار محلية والشهادات
نبدأ بشبكة Docker وContainer للأدوات فيه OpenSSL وcurl، ومجلد التجربة مربوط عليه:
mkdir -p lab-sni/certs lab-sni/nginx-sep lab-sni/nginx-san lab-sni/haproxy && cd lab-sni
docker network create lab-sni-net
docker run -d --name lab-sni-tools --network lab-sni-net --memory 128m -v "$PWD:/lab" alpine:3.22 sleep 7200
docker exec lab-sni-tools sh -c 'apk add -q openssl curl && openssl version && cat /etc/alpine-release'والمخرج سوف يكون كما يلي:
OpenSSL 3.5.9 29 Sep 2026 (Library: OpenSSL 3.5.9 29 Sep 2026)
3.22.6بعد ذلك نكتب السكربت gen.sh الذي يبني جهة الإصدار ويصدر كل شهادات التجربة، والتفاصيل خطوة بخطوة لبناء جهة إصدار مشروحة في تجربة دليل الشهادات، لذلك جمعناها هنا في دالة واحدة:
#!/bin/sh
# Builds a small local CA and the lab certificates (run inside lab-sni-tools)
set -e
cd /lab/certs
# 1) the local CA
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -days 30 \
-subj "/CN=ArabRoot Lab CA" -keyout ca.key -out ca.crt 2>/dev/null
# helper: issue NAME CN "SAN list" [extra x509 options]
issue() {
name=$1; cn=$2; san=$3; shift 3
openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -subj "/CN=$cn" \
-keyout $name.key -out $name.csr 2>/dev/null
printf 'subjectAltName=%s\nextendedKeyUsage=serverAuth\nbasicConstraints=CA:FALSE\n' "$san" > $name.ext
openssl x509 -req -in $name.csr -CA ca.crt -CAkey ca.key -extfile $name.ext "$@" -out $name.crt 2>/dev/null
cat $name.crt ca.crt > $name.chain.crt
rm $name.csr $name.ext
}
# 2) one certificate per name
issue shop shop.lab.test DNS:shop.lab.test -days 30
issue blog blog.lab.test DNS:blog.lab.test -days 30
issue api api.lab.test DNS:api.lab.test -days 30
# 3) one SAN certificate for all names, plus an internal name someone added "for convenience"
issue san shop.lab.test DNS:shop.lab.test,DNS:blog.lab.test,DNS:api.lab.test,DNS:jenkins.corp.lab.test -days 30
# 4) the same SAN certificate, but its renewal never happened
issue san-expired shop.lab.test DNS:shop.lab.test,DNS:blog.lab.test,DNS:api.lab.test,DNS:jenkins.corp.lab.test \
-not_before 20260801000000Z -not_after 20261001000000Z
# 5) a wildcard certificate
issue wild '*.apps.lab.test' 'DNS:*.apps.lab.test' -days 30
# 6) the default certificate for clients that send no SNI or an unknown name (self-signed, no real name)
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -days 30 \
-subj "/CN=invalid" -keyout default.key -out default.crt 2>/dev/null
lsفي الإعداد أعلاه لاحظ التالي:
- الشهادة
sanتحمل الأسماء الثلاثة واسماً داخلياً رابعاًjenkins.corp.lab.test، وهذا يحدث كثيراً في الواقع عندما يضيف أحدهم اسماً «لأنه على السيرفر نفسه». - الشهادة
san-expiredهي الشهادة نفسها بتاريخ انتهاء في 1 أكتوبر 2026، وقد أنشأناها بالخيارين-not_beforeو-not_afterفي openssl x509، وهذه طريقة لمحاكاة تجديد فشل دون أن ننتظر شهراً. - الشهادة الافتراضية
defaultموقعة ذاتياً وتحملCN=invalidفقط، فلا تكشف أي اسم.
ونشغل السكربت كما يلي:
docker exec lab-sni-tools sh /lab/gen.shوالمخرج سوف يكون كما يلي:
api.chain.crt
api.crt
api.key
blog.chain.crt
blog.crt
blog.key
ca.crt
ca.key
default.crt
default.key
san-expired.chain.crt
san-expired.crt
san-expired.key
san.chain.crt
san.crt
san.key
shop.chain.crt
shop.crt
shop.key
wild.chain.crt
wild.crt
wild.keyالخطوة 2: شهادة لكل نطاق والاختيار بإشارة SNI
الملف nginx-sep/default.conf فيه أربعة server على المنفذ نفسه، الأول هو الافتراضي والبقية لكل نطاق شهادته:
# One certificate per name: nginx picks the server block by SNI
server {
listen 443 ssl default_server;
server_name _;
ssl_certificate /certs/default.crt;
ssl_certificate_key /certs/default.key;
return 421 "no site for this name\n";
}
server {
listen 443 ssl;
server_name shop.lab.test;
ssl_certificate /certs/shop.chain.crt;
ssl_certificate_key /certs/shop.key;
return 200 "shop\n";
}
server {
listen 443 ssl;
server_name blog.lab.test;
ssl_certificate /certs/blog.chain.crt;
ssl_certificate_key /certs/blog.key;
return 200 "blog\n";
}
server {
listen 443 ssl;
server_name api.lab.test;
ssl_certificate /certs/api.chain.crt;
ssl_certificate_key /certs/api.key;
return 200 "api\n";
}ولاحظ أن ال server الافتراضي يرد بالرمز 421 Misdirected Request، وهو الرمز المناسب لطلب وصل إلى سيرفر لا يخدم هذا الاسم، وهذا أفضل من أن يعرض موقعاً آخر بالخطأ. ونشغل Nginx كما يلي:
docker run -d --name lab-sni-nginx-sep --network lab-sni-net --memory 64m \
-v "$PWD/certs:/certs:ro" -v "$PWD/nginx-sep/default.conf:/etc/nginx/conf.d/default.conf:ro" \
nginx:1.31.6-alpineالآن نطلب كل نطاق بالخيار -servername الذي يضع الاسم في إشارة SNI، ثم نتصل مرة بالخيار -noservername الذي يمنع إرسالها، ومرة دون أي خيار منهما:
docker exec lab-sni-tools sh -c '
for n in shop.lab.test blog.lab.test api.lab.test; do
echo "== -servername $n"
openssl s_client -connect lab-sni-nginx-sep:443 -servername $n -CAfile /lab/certs/ca.crt </dev/null 2>/dev/null | grep -E "^subject=|^issuer=|Verify return code"
done
echo "== -noservername"
openssl s_client -connect lab-sni-nginx-sep:443 -noservername -CAfile /lab/certs/ca.crt </dev/null 2>/dev/null | grep -E "^subject=|^issuer=|Verify return code"
echo "== no -servername at all"
openssl s_client -connect lab-sni-nginx-sep:443 -CAfile /lab/certs/ca.crt </dev/null 2>/dev/null | grep -E "^subject=|^issuer=|Verify return code"
'والمخرج سوف يكون كما يلي:
== -servername shop.lab.test
subject=CN=shop.lab.test
issuer=CN=ArabRoot Lab CA
Verify return code: 0 (ok)
== -servername blog.lab.test
subject=CN=blog.lab.test
issuer=CN=ArabRoot Lab CA
Verify return code: 0 (ok)
== -servername api.lab.test
subject=CN=api.lab.test
issuer=CN=ArabRoot Lab CA
Verify return code: 0 (ok)
== -noservername
subject=CN=invalid
issuer=CN=invalid
Verify return code: 18 (self-signed certificate)
== no -servername at all
subject=CN=invalid
issuer=CN=invalid
Verify return code: 18 (self-signed certificate)في المخرج أعلاه لاحظ التالي:
- كل اسم حصل على شهادته، والعنوان والمنفذ واحد في الحالات الثلاث، والفرق الوحيد هو الاسم في إشارة SNI.
- دون إشارة SNI وصلت الشهادة الافتراضية
CN=invalid، وفشل التحقق منها لأنها موقعة ذاتياً. - الحالة الأخيرة هي الخطأ الشائع في التشخيص، فبحسب وثائق s_client إذا لم تكتب
-servernameيرسل OpenSSL في إشارة SNI الاسم الذي كتبته في-connectإذا كان بشكل اسم DNS، وهنا كانlab-sni-nginx-sep، وهو اسم لا يعرفه Nginx فأرسل الشهادة الافتراضية. لذلك إذا فحصت شهادة موقع بعنوان IP أو باسم السيرفر الداخلي فسوف ترى شهادة خاطئة، وتظن أن الموقع معطل وهو سليم.
الخطوة 3: ماذا يرى المتصفح دون إشارة SNI؟
الآن نكرر الطلب بـ curl، مرة بالأسماء ومرة بعنوان IP مباشرة، ومرة باسم غير موجود:
docker exec lab-sni-tools sh -c '
IP=$(getent hosts lab-sni-nginx-sep | cut -d" " -f1); echo "nginx-sep ip: $IP"
for n in shop blog api; do curl -s --cacert /lab/certs/ca.crt --resolve $n.lab.test:443:$IP https://$n.lab.test/; done
echo "== by IP, no SNI"
curl -s --cacert /lab/certs/ca.crt https://$IP/ ; echo "exit=$?"
curl -sk -w "status=%{http_code}\n" https://$IP/
echo "== unknown name"
curl -s --cacert /lab/certs/ca.crt --resolve old.lab.test:443:$IP https://old.lab.test/; echo "exit=$?"
curl -sv --cacert /lab/certs/ca.crt --resolve old.lab.test:443:$IP https://old.lab.test/ 2>&1 | grep -iE "subject:|SSL certificate problem|curl: \("
'والمخرج سوف يكون كما يلي:
nginx-sep ip: 172.22.0.3
shop
blog
api
== by IP, no SNI
exit=60
no site for this name
status=421
== unknown name
exit=60
* SSL certificate problem: self-signed certificateفي المخرج أعلاه لاحظ أن الطلب بعنوان IP فشل في التحقق بالرمز 60، لأن curl لا يرسل عنوان IP في إشارة SNI فوصلت الشهادة الافتراضية، ثم عندما تجاوزنا التحقق بالخيار -k رد ال server الافتراضي بالرمز 421 ولم يعرض أي موقع. والاسم غير الموجود old.lab.test وصل إلى الشهادة الافتراضية نفسها، وهذا ما يحدث عندما يبقى سجل DNS لنطاق قديم يشير إلى سيرفرك بعد أن حذفت موقعه.
الخطوة 4: شهادة SAN واحدة لكل النطاقات
الآن نشغل Nginx ثانياً بشهادة SAN واحدة، والملف nginx-san/default.conf يضع الشهادة على مستوى http فترثها كل ال server:
# One SAN certificate shared by every name
ssl_certificate /certs/san.chain.crt;
ssl_certificate_key /certs/san.key;
server { listen 443 ssl default_server; server_name shop.lab.test; return 200 "shop\n"; }
server { listen 443 ssl; server_name blog.lab.test; return 200 "blog\n"; }
server { listen 443 ssl; server_name api.lab.test; return 200 "api\n"; }docker run -d --name lab-sni-nginx-san --network lab-sni-net --memory 64m \
-v "$PWD/certs:/certs:ro" -v "$PWD/nginx-san/default.conf:/etc/nginx/conf.d/default.conf:ro" \
nginx:1.31.6-alpineونطلب الأسماء الثلاثة ونطبع الرقم التسلسلي والبصمة لكل شهادة تصل، ثم نتصرف كغريب لا يعرف إلا المتجر، فنتصل به ونقرأ شهادته:
docker exec lab-sni-tools sh -c '
for n in shop.lab.test blog.lab.test api.lab.test; do
echo "== -servername $n"
openssl s_client -connect lab-sni-nginx-san:443 -servername $n -CAfile /lab/certs/ca.crt </dev/null 2>/dev/null | openssl x509 -noout -subject -serial -fingerprint -sha256
done
echo "== outsider: connect to the shop and read the certificate"
openssl s_client -connect lab-sni-nginx-san:443 -servername shop.lab.test </dev/null 2>/dev/null | openssl x509 -noout -text | sed -n "/Subject:/p;/Validity/,/Not After/p;/Subject Alternative Name/,+1p"
'والمخرج سوف يكون كما يلي:
== -servername shop.lab.test
subject=CN=shop.lab.test
serial=70D6167C18EA152ED5AFEECD90098442CF861CF8
sha256 Fingerprint=57:28:D2:8C:01:51:FE:A3:5E:DF:BE:F0:C1:A0:E5:63:C0:5B:FD:84:4E:53:05:6F:54:3F:B6:59:74:38:1C:98
== -servername blog.lab.test
subject=CN=shop.lab.test
serial=70D6167C18EA152ED5AFEECD90098442CF861CF8
sha256 Fingerprint=57:28:D2:8C:01:51:FE:A3:5E:DF:BE:F0:C1:A0:E5:63:C0:5B:FD:84:4E:53:05:6F:54:3F:B6:59:74:38:1C:98
== -servername api.lab.test
subject=CN=shop.lab.test
serial=70D6167C18EA152ED5AFEECD90098442CF861CF8
sha256 Fingerprint=57:28:D2:8C:01:51:FE:A3:5E:DF:BE:F0:C1:A0:E5:63:C0:5B:FD:84:4E:53:05:6F:54:3F:B6:59:74:38:1C:98
== outsider: connect to the shop and read the certificate
Validity
Not Before: Oct 13 21:37:11 2026 GMT
Not After : Nov 12 21:37:11 2026 GMT
Subject: CN=shop.lab.test
X509v3 Subject Alternative Name:
DNS:shop.lab.test, DNS:blog.lab.test, DNS:api.lab.test, DNS:jenkins.corp.lab.testفي المخرج أعلاه لاحظ التالي:
- الأسماء الثلاثة تصلها الشهادة نفسها بالرقم التسلسلي نفسه والبصمة نفسها، أي أن إشارة SNI ما زالت تختار ال
serverالصحيح للطلب، ولكنها لم تعد تغير الشهادة. - الحقل
Subjectيقولshop.lab.testحتى لمن طلب المدونة، وهذا لا يؤثر على التحقق، لأن المتصفحات تقرأ الأسماء من SAN وليس من CN كما ذكرنا في دليل الشهادات. - الغريب الذي اتصل بالمتجر فقط عرف الآن أن المدونة وواجهة API على السيرفر نفسه، والأخطر أنه عرف اسم
jenkins.corp.lab.test، أي أن لديك Jenkins داخلياً واسمه الكامل، دون أن يرسل أي طلب إليه. ولو كانت هذه شهادة عامة لكانت القائمة نفسها في سجلات الشفافية منذ لحظة إصدارها.
الخطوة 5: نكسرها، شهادة SAN منتهية
الآن نحاكي تجديداً فشل، فنعيد تشغيل lab-sni-nginx-san بإعداد يشير إلى san-expired بدلاً من san، وهذا هو التغيير الوحيد في الملف:
mkdir -p nginx-san-expired
sed 's/san\./san-expired./' nginx-san/default.conf > nginx-san-expired/default.conf
docker rm -f lab-sni-nginx-san
docker run -d --name lab-sni-nginx-san --network lab-sni-net --memory 64m \
-v "$PWD/certs:/certs:ro" -v "$PWD/nginx-san-expired/default.conf:/etc/nginx/conf.d/default.conf:ro" \
nginx:1.31.6-alpineثم نطلب الأسماء الثلاثة بـ curl، ونفحص الشهادة بـ s_client:
docker exec lab-sni-tools sh -c '
IP=$(getent hosts lab-sni-nginx-san | cut -d" " -f1); date -u
for n in shop blog api; do echo "== $n"; curl -s --cacert /lab/certs/ca.crt --resolve $n.lab.test:443:$IP https://$n.lab.test/ -o /dev/null -w "exit=%{exitcode}\n" ; curl -sS --cacert /lab/certs/ca.crt --resolve $n.lab.test:443:$IP https://$n.lab.test/ 2>&1 >/dev/null; done
echo "== s_client"
openssl s_client -connect lab-sni-nginx-san:443 -servername blog.lab.test -CAfile /lab/certs/ca.crt </dev/null 2>/dev/null | grep -E "Verify return code"
openssl s_client -connect lab-sni-nginx-san:443 -servername blog.lab.test </dev/null 2>/dev/null | openssl x509 -noout -enddate -checkend 0
'والمخرج سوف يكون كما يلي، وقد حذفنا منه فقط الفقرة الإرشادية ذات الأسطر الأربعة التي يطبعها curl بعد كل خطأ ورابط More details here الذي يسبقها، لأنها تتكرر كما هي بعد كل اسم:
Tue Oct 13 21:39:00 UTC 2026
== shop
exit=60
curl: (60) SSL certificate problem: certificate has expired
== blog
exit=60
curl: (60) SSL certificate problem: certificate has expired
== api
exit=60
curl: (60) SSL certificate problem: certificate has expired
== s_client
Verify return code: 10 (certificate has expired)
notAfter=Oct 1 00:00:00 2026 GMT
Certificate will expireفي المخرج أعلاه لاحظ أن المواقع الثلاثة توقفت في اللحظة نفسها بالخطأ نفسه، وأن Nginx بدأ دون أي تحذير في سجله رغم أن الشهادة منتهية منذ أسبوعين، فهو لا يفحص تاريخ الشهادة عند التشغيل، ولذلك لا تعتمد على أن السيرفر سوف ينبهك. ولاحظ أيضاً أن -checkend 0 يطبع Certificate will expire للشهادة المنتهية أيضاً، والمهم فيه رمز الخروج وليس النص.
وللمقارنة أعدنا السيرفر الأول إلى شهادة لكل اسم، ولكن مع شهادة منتهية للمدونة وحدها، صدرت بالطريقة نفسها:
docker exec lab-sni-tools sh -c 'cd /lab/certs && openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -subj "/CN=blog.lab.test" -keyout blog-expired.key -out b.csr 2>/dev/null && printf "subjectAltName=DNS:blog.lab.test\nextendedKeyUsage=serverAuth\n" > b.ext && openssl x509 -req -in b.csr -CA ca.crt -CAkey ca.key -extfile b.ext -not_before 20260801000000Z -not_after 20261001000000Z -out blog-expired.crt 2>/dev/null && cat blog-expired.crt ca.crt > blog-expired.chain.crt && rm b.csr b.ext'
mkdir -p nginx-sep-expired
sed 's#/certs/blog\.#/certs/blog-expired.#' nginx-sep/default.conf > nginx-sep-expired/default.conf
docker rm -f lab-sni-nginx-sep
docker run -d --name lab-sni-nginx-sep --network lab-sni-net --memory 64m \
-v "$PWD/certs:/certs:ro" -v "$PWD/nginx-sep-expired/default.conf:/etc/nginx/conf.d/default.conf:ro" \
nginx:1.31.6-alpine
docker exec lab-sni-tools sh -c 'IP=$(getent hosts lab-sni-nginx-sep | cut -d" " -f1); for n in shop blog api; do printf "%s: " $n; curl -sS --cacert /lab/certs/ca.crt --resolve $n.lab.test:443:$IP https://$n.lab.test/ 2>&1 | head -1; done'والمخرج سوف يكون كما يلي:
shop: shop
blog: curl: (60) SSL certificate problem: certificate has expired
api: apiوهذا هو الفرق في نطاق الضرر كما في الصورة السابقة: شهادة منفصلة انتهت فتوقفت المدونة وحدها، وشهادة SAN انتهت فتوقف كل شيء. وبعد ذلك أعدنا السيرفرين إلى إعدادهما السليم nginx-sep وnginx-san بالطريقة نفسها قبل الخطوة التالية.
الخطوة 6: حدود الشهادة الشاملة
الشهادة wild تحمل الاسم *.apps.lab.test، وسوف نسأل OpenSSL هل تصلح لأربعة أسماء مختلفة بالخيار -verify_hostname، وهو الفحص نفسه الذي يجريه الكلاينت في المصافحة:
docker exec lab-sni-tools sh -c 'cd /lab/certs
openssl x509 -in wild.crt -noout -ext subjectAltName
for h in shop.apps.lab.test apps.lab.test a.b.apps.lab.test shop.lab.test; do
echo "== $h"; openssl verify -CAfile ca.crt -verify_hostname $h wild.crt 2>&1
done'والمخرج سوف يكون كما يلي:
X509v3 Subject Alternative Name:
DNS:*.apps.lab.test
== shop.apps.lab.test
wild.crt: OK
== apps.lab.test
CN=*.apps.lab.test
error 62 at 0 depth lookup: hostname mismatch
error wild.crt: verification failed
== a.b.apps.lab.test
CN=*.apps.lab.test
error 62 at 0 depth lookup: hostname mismatch
error wild.crt: verification failed
== shop.lab.test
CN=*.apps.lab.test
error 62 at 0 depth lookup: hostname mismatch
error wild.crt: verification failedفي المخرج أعلاه لاحظ أن النجمة طابقت shop.apps.lab.test فقط، ولم تطابق النطاق نفسه apps.lab.test، ولا المستوى الثاني a.b.apps.lab.test، والسبب أن RFC 9525 القسم 6.3 يشترط أن تكون النجمة وحدها هي الجزء الأيسر من الاسم، وأنها تطابق جزءاً Label واحداً فقط. فإذا احتجت النطاق نفسه مع نطاقاته الفرعية فاطلب شهادة فيها الاسمان معاً apps.lab.test و*.apps.lab.test، وإذا احتجت مستويين فأنت تحتاج شهادة شاملة لكل مستوى، مثل *.eu.apps.lab.test.
وانتبه إلى خطرين في الشهادة الشاملة، الأول أن مفتاحها يثبت هوية كل اسم ممكن تحتها، بما في ذلك أسماء لم تنشئها بعد، فمن يسرقه من أي سيرفر يستطيع أن ينتحل login.apps.example.com حتى لو لم يكن لديك موقع بهذا الاسم، لذلك لا تنسخ مفتاح شهادة شاملة إلى سيرفرات لا تحتاجه. والثاني أن Let's Encrypt لا تصدرها إلا بالتحقق عبر DNS، أي أن برنامج التجديد يحتاج توكن يكتب في DNS نطاقك، وهذا التوكن نفسه هدف ثمين، فأعطه صلاحية على منطقة التحقق فقط إذا كان مزود DNS يسمح بذلك.
الخطوة 7: HAProxy يوجه بإشارة SNI دون أن يملك أي شهادة
الآن نضع HAProxy في وضع TCP أمام السيرفرين، فيمرر shop وblog إلى السيرفر الأول، وapi إلى الثاني الذي يحمل شهادة SAN، وكل ما عداها إلى الأول. والملف haproxy/haproxy.cfg كما يلي:
global
log stdout format raw local0
defaults
mode tcp
log global
timeout connect 5s
timeout client 30s
timeout server 30s
frontend fe_tls
bind :443
# wait for the ClientHello (up to 5s) so the SNI can be read; HAProxy holds no certificate
tcp-request inspect-delay 5s
tcp-request content capture req.ssl_sni len 64
tcp-request content accept if { req.ssl_hello_type 1 }
log-format "%ci:%cp sni=%[capture.req.hdr(0)] -> %b/%s bytes=%B"
use_backend be_sites if { req.ssl_sni -i shop.lab.test blog.lab.test }
use_backend be_api if { req.ssl_sni -i api.lab.test }
default_backend be_sites
backend be_sites
server sep lab-sni-nginx-sep:443
backend be_api
server san lab-sni-nginx-san:443في الإعداد أعلاه لاحظ التالي:
- السطر
bind :443لا يحمل الخيارsslولاcrt، أي أن HAProxy لا يملك أي شهادة ولا يفك التشفير. tcp-request inspect-delayمعreq.ssl_hello_type 1يجعلان HAProxy ينتظر حتى تصل رسالة ClientHello كاملة قبل أن يقرر، وإلا فقد يقرر قبل أن تصل إشارة SNI، كما في وثائق req.ssl_sni.- السطر
captureيحفظ الاسم لنطبعه في السجل، وهذا يثبت أن الاسم مقروء لمن يرى الحركة دون أي مفتاح.
docker run -d --name lab-sni-haproxy --network lab-sni-net --memory 64m \
-v "$PWD/haproxy:/usr/local/etc/haproxy:ro" haproxy:3.4.6-alpine
docker exec lab-sni-haproxy ls /usr/local/etc/haproxyhaproxy.cfgثم نطلب الأسماء الثلاثة عبر HAProxy، ونتصل مرة دون إشارة SNI، ونطلب الصفحات بـ curl، ثم نقرأ سجل HAProxy:
docker exec lab-sni-tools sh -c '
for n in shop.lab.test blog.lab.test api.lab.test; do
printf "%s -> " $n; openssl s_client -connect lab-sni-haproxy:443 -servername $n -CAfile /lab/certs/ca.crt </dev/null 2>/dev/null | openssl x509 -noout -subject -serial | tr "\n" " "; echo
done
printf "no SNI -> "; openssl s_client -connect lab-sni-haproxy:443 -noservername </dev/null 2>/dev/null | openssl x509 -noout -subject
IP=$(getent hosts lab-sni-haproxy | cut -d" " -f1)
for n in shop blog api; do curl -s --cacert /lab/certs/ca.crt --resolve $n.lab.test:443:$IP https://$n.lab.test/; done'
docker logs lab-sni-haproxy 2>&1 | grep sni=والمخرج سوف يكون كما يلي:
shop.lab.test -> subject=CN=shop.lab.test serial=511ACE40D829DAF60E5B6F4A5DAE9CDD87A164CF
blog.lab.test -> subject=CN=blog.lab.test serial=2C21F1614E1C6FB368E982302C5178CD6D19323F
api.lab.test -> subject=CN=shop.lab.test serial=70D6167C18EA152ED5AFEECD90098442CF861CF8
no SNI -> subject=CN=invalid
shop
blog
api
172.22.0.2:45752 sni=shop.lab.test -> be_sites/sep bytes=2875
172.22.0.2:45766 sni=blog.lab.test -> be_sites/sep bytes=2875
172.22.0.2:45778 sni=api.lab.test -> be_api/san bytes=2929
172.22.0.2:45784 sni=- -> be_sites/sep bytes=2381
172.22.0.2:45800 sni=shop.lab.test -> be_sites/sep bytes=3111
172.22.0.2:45814 sni=blog.lab.test -> be_sites/sep bytes=3111
172.22.0.2:45816 sni=api.lab.test -> be_api/san bytes=3163في المخرج أعلاه لاحظ التالي:
- كل اسم وصل إلى شهادته الصحيحة من السيرفر الخلفي، و
api.lab.testحصل على شهادة SAN من السيرفر الثاني برقمها التسلسلي نفسه الذي رأيناه في الخطوة 4، أي أن HAProxy مرر الاتصال المشفر كما هو. - الاتصال دون إشارة SNI ذهب إلى
default_backendفوصلت الشهادة الافتراضية للسيرفر الأول، والسجل كتبsni=-. - السجل يحمل اسم كل موقع مع أن HAProxy لا يملك أي مفتاح، وهذا ما يراه أي جهاز في الطريق، وهو أيضاً ما سوف يختفي إذا استخدم الكلاينت ECH، فيرى HAProxy الاسم الخارجي العام فقط.
والتوجيه بالمسار لا يعمل في هذا الوضع لأن المسار داخل البايتات المشفرة، وقد رأينا ذلك بالتفصيل في تجربة مقارنة Reverse Proxy وموزع الحمل والبوابة.
إزالة التجربة
نحذف ما أنشأناه بالاسم فقط، ثم Docker Image الخاصة بـ HAProxy إذا لم تكن تستخدمها في شيء آخر:
docker rm -f lab-sni-haproxy lab-sni-nginx-sep lab-sni-nginx-san lab-sni-tools
docker network rm lab-sni-net
docker rmi haproxy:3.4.6-alpineأخطاء شائعة
- فحص الشهادة دون
-servername: كما رأينا في الخطوة 2، تصلك الشهادة الافتراضية فتشخص مشكلة غير موجودة، لذلك اكتب الاسم دائماً فيopenssl s_client، ولاحظ أن curl يرسله تلقائياً من الرابط ما لم يكن الرابط عنوان IP. - جعل شهادة الموقع الرئيسي هي الافتراضية: فيرى كل ماسح يطرق عنوان IP اسم موقعك، وتظهر الشهادة الخطأ لكل نطاق قديم ما زال يشير إلى السيرفر بدلاً من رد واضح.
- إضافة أسماء داخلية إلى شهادة عامة: الاسم يبقى في سجلات الشفافية للأبد، فاستخدم جهة إصدار داخلية للأسماء الداخلية.
- شهادة SAN لأسماء لا تعيش معاً: نطاقات عملاء مختلفين أو خدمات لفرق مختلفة في شهادة واحدة، فيوقف فشل واحد الجميع، ويرى كل عميل أسماء الآخرين.
- توقع أن
*.example.comتغطيexample.comأوa.b.example.com: النجمة لمستوى واحد فقط كما رأينا في الخطوة 6. - نسخ مفتاح الشهادة الشاملة إلى كل السيرفرات: أضعف سيرفر يملك المفتاح يكفي لانتحال كل نطاق فرعي، فأنه TLS في مكان واحد أو اجعل لكل مجموعة سيرفرات شهادتها.
- التوجيه في الطبقة الرابعة بالمسار: الموزع لا يرى إلا إشارة SNI، فإذا احتجت المسار فيجب أن ينهي الموزع التشفير.
تدريب عملي
- اكتب في crt.sh اسم نطاق شركتك، واقرأ الأسماء التي ظهرت في شهاداتها، وهل بينها أسماء داخلية أو بيئات اختبار قديمة؟
- في التجربة، أضف إلى
haproxy.cfgسطراً يرفض الاتصال الذي لا يحمل إشارة SNI بدلاً من تمريره إلىdefault_backend، وتأكد من النتيجة بـ-noservernameومن السجل.
الخلاصة
وصلنا لنهاية الموضوع، وأهم ما فيه:
- إشارة SNI في ClientHello هي التي تسمح لسيرفر واحد بعنوان واحد أن يقدم لكل نطاق شهادته، ومن لا يرسلها، مثل من يتصل بعنوان IP، يحصل على الشهادة الافتراضية، فاجعلها شهادة لا تحمل أي اسم حقيقي.
- شهادة لكل اسم تحصر الضرر في اسم واحد وتناسب نطاقات العملاء والمواقع المستقلة، وشهادة SAN تناسب أسماء الخدمة الواحدة التي تعيش وتموت معاً، والشهادة الشاملة تناسب النطاقات الفرعية الكثيرة لفريق واحد وتغطي مستوى واحداً فقط.
- شهادة SAN المنتهية توقف كل أسمائها في اللحظة نفسها، ومفتاحها المسروق يكشفها كلها، والسيرفر لن ينبهك، فراقب تاريخ الانتهاء بنفسك.
- كل اسم في شهادة عامة منشور في سجلات الشفافية للأبد، وإشارة SNI مقروءة على الشبكة، وECH أصبح معياراً في مارس 2026 ولكنه يحتاج متصفحاً وDNS مشفراً وسيرفراً يدعمه.
- موزع الحمل في الطبقة الرابعة يستطيع التوجيه بإشارة SNI دون أي شهادة، فيبقى المفتاح في السيرفر الخلفي، مقابل ألا يرى المسار ولا الترويسات.
دليلالشهادات الرقمية TLS للمبتدئين وصيغ ملفاتهاكيف يعرف المتصفح أنه يتحدث مع الموقع الحقيقي، وماذا يوجد داخل الشهادة، ولماذا تأتي ملفاتها بأكثر من صيغة.
دليلتكامل منصة SaaS مع أنظمة العميل دون خلط المستأجرينكيف تربط منصتك متعددة المستأجرين بنطاق العميل وبريده وأنظمته دون أن تفتح باباً على بقية العملاء.
دليلReverse Proxy أم Load Balancer أم API Gateway؟بماذا تقرر كل طبقة أمام تطبيقك، ومتى تضيف موزع الحمل أو بوابة ال API، مع تجربة تقيس ما تضيفه كل طبقة.
دليلHAProxy: توزيع الحمل Load Balancing أمام خدماتكعندما يعمل تطبيقك على أكثر من سيرفر، يوزع HAProxy الطلبات بينها ويستبعد المعطل منها تلقائياً مع شهادات HTTPS.
دليلتثبيت Caddy على خادمك: Reverse Proxy بشهادات HTTPS تلقائيةكيف تثبت Caddy على سيرفرك ليمنح كل موقع شهادة HTTPS تلقائياً، وتربط به تطبيقاتك بملف إعداد من بضعة أسطر.سجل التحديثات
- أكتوبر 2026: كتابة المقال، وتنفيذ التجربة في 13 أكتوبر 2026 على Docker Engine 29.8.0 مع Alpine 3.22.6 وOpenSSL 3.5.9 وcurl 8.14.1، وNginx 1.31.6، وHAProxy 3.4.6، وجهة إصدار محلية دون أي جهة عامة، وحدود Let's Encrypt من صفحتها المحدثة في 5 أغسطس 2026، ووضع ECH من RFC 9849 وصفحات Chrome وFirefox وOpenSSL وNginx كما كانت في 14 أكتوبر 2026.