إعداد mTLS في nginx بشهادات عميل
احمِ لوحة الإدارة عبر mTLS: أنشئ CA خاصاً باستخدام openssl، وأصدر شهادة لكل مستخدم، واضبط nginx لرفض أي طلب بلا شهادة عميل صالحة.
ما الذي يفعله mTLS
يجعل TLS المتبادل، الذي يُكتب عادةً mTLS، nginx يطلب شهادة من كل عميل ويرفض الطلب عندما تكون الشهادة مفقودة أو عندما لا تكون صادرة عن مرجع مصادقة (CA) تتحكم فيه. يحدث هذا التحقق داخل مصافحة TLS (أمان طبقة النقل)، لذلك لا يصل المتصل الذي لا يملك شهادة عميل صالحة إلى تطبيقك مطلقاً. تكمن الفائدة في ذلك: يمكن أن تكون لوحة الإدارة أو نقطة نهاية المقاييس متاحة على الإنترنت العام من دون صفحة تسجيل دخول، ومن دون أي شيء يمكن لروبوت تخمينه.
الإعداد صغير. مرجع مصادقة خاص واحد تُنشئه باستخدام openssl، وشهادة واحدة لكل شخص، وثلاثة توجيهات في كتلة خادم nginx. لكن ما يحدد قدرة هذا الإعداد على الاستمرار عاماً كاملاً هو الجانب التشغيلي، لذلك يغطي معظم هذا الدليل مدد الصلاحية، والإبطال، والشهادات الخاصة بكل شخص، وما يجب فعله عندما يُرفض عميل ولا يستطيع أحد معرفة السبب.
سلسلتان، لا سلسلة واحدة
توجد سلسلتا شهادات في إعداد mTLS، ولا علاقة لإحداهما بالأخرى. ودمجهما هو أول خطأ يرتكبه معظم الناس.
السلسلة الأولى تخص الخادم. يقدّم VPS الخاص بك شهادة لـ admin.example.com صادرةً عن CA عامة مثل Let's Encrypt، ويتحقق المتصفح منها مقابل مخزن الجذر الذي يأتي مع نظام التشغيل. لا يغيّر mTLS شيئاً في هذا الجزء. إذا أصدر certbot هذه الشهادة لك اليوم، فاتركها كما هي تماماً: راجع إصدار شهادة Let's Encrypt لـ nginx باستخدام certbot.
السلسلة الثانية تخص العميل. تنشئ CA صغيرة خاصة بك، وتوقّع شهادة لكل شخص يحتاج إلى الوصول، وتخبر nginx بأن يثق بهذه CA وحدها عند التحقق من العملاء. لا يعرف أي مخزن جذور عام CA الخاصة بك، ولا يحتاج إلى معرفتها. الطرف الوحيد الذي يجب أن يثق بها هو nginx، من خلال ملف ssl_client_certificate.
لذلك، لا يؤثر ssl_client_certificate إطلاقاً في الشهادة التي يقدّمها nginx، كما أن سلسلة Let's Encrypt لا تؤثر في العملاء المسموح لهم بالدخول. إن توجيه ssl_client_certificate إلى fullchain.pem لا يؤدي إلى ما يبدو أنه يؤدي إليه: يحدّد هذا التوجيه جهات الإصدار التي قد تأتي منها شهادة العميل، أي الطرف الآخر من الاتصال. أما جعل الخادم نفسه يثق بـCA الخاصة بك لتنفيذ الاتصالات الصادرة، فهو مهمة منفصلة، موضّحة في إضافة CA الخاصة بك إلى مخزن الثقة في Ubuntu، كما أن مخزن الثقة في النظام ليس المخزن الذي يقرأ منه nginx عند التحقق من العميل.
أنشئ CA للعملاء باستخدام openssl
أنشئ CA في مكان غير خادم الويب. يحتاج nginx إلى الشهادة العامة لـCA فقط. يوقّع المفتاح الخاص لـCA شهادات العملاء الجديدة، لذلك فإن تركه على خادم متصل بالإنترنت يعني أن أي اختراق يمنح المهاجم القدرة على إنشاء عملاء صالحين لنفسه متى شاء.
mkdir -p ~/client-ca/certs ~/client-ca/newcerts ~/client-ca/private ~/client-ca/csr
cd ~/client-ca
chmod 700 private
touch index.txt
echo 1000 > serial
echo 1000 > crlnumberتُشكّل index.txt وserial وcrlnumber قاعدة بيانات CA. يرفض openssl ca العمل من دونها. وهي تتيح أيضاً الإلغاء لاحقاً، لأن قائمة الإلغاء تسمي الأرقام التسلسلية؛ لذلك يجب أن يتذكر CA الرقم التسلسلي الذي مُنح لكل جهة.
أنشئ ~/client-ca/openssl.cnf. اضبط dir على المسار الفعلي لهذا الدليل، لأن openssl ca لا يوسّع ~.
[ ca ]
default_ca = client_ca
[ client_ca ]
dir = /home/you/client-ca
database = $dir/index.txt
new_certs_dir = $dir/newcerts
certificate = $dir/ca.crt
private_key = $dir/private/ca.key
serial = $dir/serial
crlnumber = $dir/crlnumber
default_md = sha256
default_days = 365
default_crl_days = 30
policy = policy_loose
rand_serial = no
unique_subject = no
email_in_dn = no
[ policy_loose ]
commonName = supplied
countryName = optional
stateOrProvinceName = optional
organizationName = optional
organizationalUnitName = optional
emailAddress = optional
[ client_ext ]
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuerأنشئ الآن مفتاح CA وشهادته الموقعة ذاتياً:
openssl genrsa -aes256 -out private/ca.key 4096
chmod 600 private/ca.key
openssl req -x509 -new -key private/ca.key -sha256 -days 3650 \
-subj "/O=Example Ops/CN=Example Ops Client CA" \
-addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
-addext "keyUsage=critical,keyCertSign,cRLSign" \
-out ca.crtيضع -aes256 عبارة مرور على مفتاح CA، لذلك يطلبها في كل عملية توقيع. هذا هو الغرض منه. تحقق مما أنشأته:
openssl x509 -in ca.crt -noout -subject -dates -ext basicConstraintsيجب أن يكون الحقل subject هو CA الخاص بك، وأن تمتد مدة الصلاحية عشر سنوات. يجب أن يقرأ سطر الامتداد CA:TRUE, pathlen:0. يعني pathlen:0 أن هذا CA يمكنه توقيع الشهادات النهائية، ولا يمكنه توقيع CA آخر؛ وبذلك تبقى السلسلة بعمق مستوى واحد تماماً، ويمكنك ترك ssl_verify_depth دون تغيير.
إصدار شهادة عميل لكل شخص
استخدم شهادة واحدة لكل شخص. لا تستخدم شهادة مشتركة لفريق، لأن إلغاء الشهادة المشتركة يمنع الجميع من الوصول، ولأنها لا توضح من أجرى الاتصال.
openssl genrsa -out private/alice.key 2048
openssl req -new -key private/alice.key -out csr/alice.csr \
-subj "/O=Example Ops/CN=alice"
openssl ca -config openssl.cnf -extensions client_ext \
-days 365 -notext -in csr/alice.csr -out certs/alice.crtيطبع openssl ca الشهادة التي يوشك على توقيعها، ويطلب عبارة مرور CA، ثم يطلب التأكيد مرتين، وبعد ذلك يضيف سطراً إلى index.txt. أضف -batch عند استخدامه في نص برمجي. يهم قسم client_ext بسبب سطر واحد فيه: extendedKeyUsage = clientAuth. تُرفض الشهادة التي يحتوي تعداد extended key usage فيها على serverAuth فقط، لأنها غير صالحة لمصادقة العميل. لذلك حدّد الغرض بدلاً من الاعتماد على التخمين.
تحقق من تطابق الزوج مع CA قبل تسليمه:
openssl verify -CAfile ca.crt certs/alice.crtيطبع ذلك certs/alice.crt: OK. أي مخرجات أخرى تعني أن الشهادة وCA غير متطابقين، ولن يعالج أي إعداد لـ nginx المشكلة.
اجمع المفتاح والشهادة في ملف واحد يمكن للمتصفح استيراده:
openssl pkcs12 -export -inkey private/alice.key -in certs/alice.crt \
-name "alice at example ops" -out alice.p12يطلب التصدير كلمة مرور تحمي الملف أثناء نقله. أرسل الملف وكلمة المرور عبر قناتين مختلفتين، وسلّم المستخدمين .p12 بدلاً من .key مجرداً. يمكنك إضافة -certfile ca.crt لإدراج CA في الحزمة، لكن nginx لا يحتاج إليها: يحتفظ nginx بالفعل بـ ca.crt، ولذلك تتحقق الشهادة الموقعة مباشرة من CA من تلقاء نفسها.
يكتب OpenSSL 3، الذي يأتي مع Ubuntu 24.04، ملفات PKCS#12 باستخدام تشفير حديث، وتستطيع المتصفحات وأنظمة التشغيل المستخدمة اعتباراً من August 2026 قراءتها. إذا رفض مستورد قديم الملف، فأعد تصديره مع إضافة -legacy، إذ يعود ذلك إلى الخوارزميات الأقدم التي يتوقعها المستورد. اقرأ الرسالة التي يعرضها المستورد قبل استخدام ذلك الخيار.
تهيئة nginx باستخدام ssl_client_certificate وssl_verify_client
انسخ شهادة CA، وشهادة CA فقط، إلى الخادم.
scp ca.crt user@admin.example.com:/tmp/client-ca.crt
ssh user@admin.example.com \
'sudo install -o root -g root -m 644 /tmp/client-ca.crt /etc/nginx/client-ca.crt'الوضع 644 صحيح هنا. شهادة CA معلومات عامة. يبقى مفتاح CA على محطة عملك.
أضف بعد ذلك 3 توجيهات إلى كتلة الخادم التي تنهي TLS بالفعل:
server {
listen 443 ssl;
server_name admin.example.com;
ssl_certificate /etc/letsencrypt/live/admin.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/admin.example.com/privkey.pem;
ssl_client_certificate /etc/nginx/client-ca.crt;
ssl_verify_client on;
ssl_verify_depth 1;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}ssl_verify_depth 1 هي القيمة الافتراضية في nginx، وتعني أن شهادة العميل يجب أن تكون موقَّعة مباشرةً من CA الموجودة في ذلك الملف. ارفعها فقط إذا أضفت CA وسيطة. يرسل nginx أيضاً أسماء الموضوعات من ssl_client_certificate إلى العميل أثناء المصافحة، وبذلك يعرف المتصفح أيّاً من شهاداته يعرضه. ولهذا السلوك استخدم ssl_client_certificate بدلاً من ssl_trusted_certificate، إذ تتحقق بالطريقة نفسها لكنها لا ترسل أي قائمة.
تتضمن Ubuntu 24.04 الإصدار nginx 1.24، حيث يُضاف HTTP/2 إلى سطر listen باستخدام listen 443 ssl http2;. في nginx 1.25.1 والإصدارات الأحدث، أصبحت هذه الصيغة مهجورة، وأصبح HTTP/2 توجيهاً مستقلاً هو http2 on;. لا يغيّر أي من الخيارين التحقق من الشهادة.
أعد التحميل واقرأ النتيجة:
sudo nginx -t && sudo systemctl reload nginx
curl -i https://admin.example.com/يطبع nginx -t كلاً من syntax is ok وtest is successful. لا يحمل استدعاء curl أي شهادة، لذلك ينبغي أن يعيد 400 Bad Request مع المحتوى No required SSL certificate was sent. هذا يعني أن nginx رفض الطلب عند بوابته الخاصة، أي إن الإعداد فعّال ولم يُطلب من التطبيق معالجة الطلب. جرّب الآن بالطريقة الصحيحة:
curl --cert certs/alice.crt --key private/alice.key https://admin.example.com/ينبغي أن يعيد ذلك أي محتوى يقدّمه تطبيقك.
لماذا يجب وضع البوابة في كتلة الخادم
تُبادَل الشهادة أثناء مصافحة TLS، قبل أن يقرأ nginx سطر الطلب. لذلك لا يعرف nginx في تلك اللحظة أي location سيستقبل الطلب. إن وضع ssl_verify_client on; داخل location يطلب من العميل إعادة التفاوض في منتصف الاتصال. أزال TLS 1.3 إعادة التفاوض، كما يمنعها HTTP/2. لذلك يفشل هذا النمط في حزمة برمجية حديثة بدلاً من عرض طلب الشهادة.
حدّد النطاق بنفسك. اطلب الشهادة على مستوى الخادم، ثم اتخذ القرار لكل location:
ssl_verify_client optional;
location /metrics {
if ($ssl_client_verify != SUCCESS) { return 403; }
proxy_pass http://127.0.0.1:9090;
}
location /healthz {
proxy_pass http://127.0.0.1:8080;
}يحتوي $ssl_client_verify على SUCCESS، أو NONE إذا لم يرسل العميل شيئاً، أو FAILED: متبوعاً بسبب. عند استخدام optional، يطلب nginx شهادة ويتحقق منها فقط إذا وصلت. وهذا ما يتيح عمل مسار /healthz العام المذكور أعلاه، مع إبقاء /metrics مغلقاً. إذا أُرسلت شهادة وفشل التحقق منها، يرفضها nginx في تلك المرحلة أيضاً. إذا أردت فحص شهادة فاشلة بنفسك بدلاً من ذلك، فاستخدم optional_no_ca. عندها يجب أن يتعامل اختبارك مع كل قيمة غير SUCCESS على أنها رفض.
يستخدم nginx رموز حالة غير قياسية لهذا الغرض، ويمكن لـ error_page التقاطها كي يحصل الزائر المرفوض على تفسير بدلاً من استجابة 400 مجردة:
error_page 495 496 = @needcert;
location @needcert {
default_type text/plain;
return 200 "This host requires a client certificate. Ask ops for one.\n";
}يعني 495 أن شهادة العميل فشلت عملية التحقق. ويعني 496 أن العميل لم يقدّم شهادة. اجعل هذه الصفحة نصاً عادياً، لأن من يقرأها لا يملك جلسة ولا حساباً.
كيف أُثبّت شهادة العميل في المتصفح؟
يحتفظ Firefox بمخزن شهادات خاص به: افتح Settings، ثم Privacy and Security، ثم View Certificates، ثم علامة التبويب Your Certificates، ثم Import، واختر ملف .p12 وأدخل كلمة المرور الخاصة به.
يستخدم Chrome وEdge مخزن نظام التشغيل في Windows وmacOS، لذلك يؤدي فتح ملف .p12 إلى تشغيل معالج استيراد النظام. في Linux، يقرأ Chrome قاعدة بيانات NSS (خدمات أمان الشبكة) منفصلة في مجلدك الرئيسي، وتكون أداة سطر الأوامر هي الطريقة الموثوقة:
sudo apt install -y libnss3-tools
pk12util -d sql:$HOME/.pki/nssdb -i alice.p12افتح الموقع بعد ذلك، وسيطلب المتصفح تحديد الشهادة التي تريد إرسالها. يتذكر Chrome هذا الاختيار طوال جلسة المتصفح، لذا أعد تشغيل المتصفح عندما تريد ظهور الطلب مرة أخرى. تُخزَّن الشهادة في ملف تعريف متصفح واحد على جهاز واحد، لذلك لا تظهر الشهادة المستوردة إلى Firefox في Chrome، كما لا يظهر أي منهما على هاتفك.
الاختبار باستخدام curl --cert
صحّح الأخطاء باستخدام curl، لأنّه يعرض الإجراءات التي نفّذها.
curl -v --cert certs/alice.crt --key private/alice.key https://admin.example.com/يمكنك دمج الشهادة والمفتاح في ملف PEM واحد وتمريره إلى --cert alice.pem. إذا كان المفتاح محمياً بعبارة مرور، فسيطلب curl إدخالها. ويقبل أيضاً --cert alice.pem:passphrase، لكن عبارة المرور ستظهر عندئذ في سجل أوامر الصدفة، لذلك استخدم المطالبة التفاعلية.
من المفيد إجراء اختبارين قبل إلقاء اللوم على nginx. أولاً، يجب أن تكون الشهادة والمفتاح زوجاً متطابقاً:
openssl x509 -noout -pubkey -in certs/alice.crt | openssl sha256
openssl pkey -pubout -in private/alice.key | openssl sha256تدل قيمتا التجزئة المتطابقتان على أن الملفين ينتميان إلى بعضهما. أما اختلافهما فيعني أنك خلطت بين ملفي شخصين مختلفين، ولن يذكر لك أي عميل هذا السبب.
ثانياً، يجب أن يطلب الخادم شهادة CA الخاصة بك:
openssl s_client -connect admin.example.com:443 -servername admin.example.com </dev/nullابحث عن كتلة Acceptable client certificate CA names في الناتج، وابحث داخلها عن subject الخاص بـCA لديك. إذا كانت هذه الكتلة مفقودة تماماً، فهذا يعني أن nginx لا يطلب شهادة في server block الذي أجاب عن الطلب. عندئذ تكون توجيهاتك قد وُضعت في server block آخر، وغالباً في الخادم الافتراضي.
تمرير CN الخاص بالعميل إلى التطبيق
توضح الشهادة هوية المتصل، لكن التطبيق الموجود خلف الـproxy لا يستطيع رؤية طبقة TLS، لذلك يجب على nginx تمرير الاسم إليه.
map $ssl_client_s_dn $client_cn {
default "";
"~,?CN=(?<cn>[^,]+)" $cn;
}يحتوي $ssl_client_s_dn على الاسم المميز للموضوع بصيغة RFC 2253، وتكون صيغته مثل CN=alice,O=Example Ops. تستخرج map حقل CN إلى $client_cn. اجعل CN اسم مستخدم عاديًا، لأن الفاصلة داخل CN تُهَرَّب في هذه الصيغة، والتعبير النمطي الصغير أعلاه لا يعالج هذا الهروب.
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Client-Cert-CN $client_cn;
proxy_set_header X-Client-Cert-Serial $ssl_client_serial;
}يستبدل proxy_set_header أي رأس يحمل الاسم نفسه أرسله المتصل، لذلك لا يمكن لأحد تزوير X-Client-Cert-CN من خلال هذا الموقع. ويحافظ شرطان على ذلك. يرث nginx قيمة proxy_set_header من المستوى الخارجي فقط عندما لا يعرّف المستوى الداخلي أي قيمة خاصة به، لذلك يفقد موقع ثانٍ يحتوي على سطر proxy_set_header واحد بصمت كل الرؤوس التي عُيّنت في المستوى الأعلى، بما فيها هذا الرأس. ويجب ألا يكون التطبيق قابلاً للوصول إلا عبر nginx، ما يعني ربطه بـ127.0.0.1 بدلاً من 0.0.0.0، لأن تطبيقًا يستمع على منفذ عام سيقرأ الرأس المزوّر مباشرة من الإنترنت. يوضّح شرح إعداد reverse proxy في nginx سطرًا بسطر جانب الـproxy من ذلك. وإذا كان التطبيق يحتاج إلى الشهادة كاملة بدلاً من الاسم، فإن $ssl_client_escaped_cert يحملها بترميز URL وبصيغة آمنة داخل رأس.
كيف أُلغي شهادة عميل واحدة؟
يغادر أحد الأشخاص، أو يُفقد حاسوب محمول. تلغي تلك الشهادة وحدها، ويواصل الآخرون العمل. وهذا هو سبب إصدار شهادة واحدة لكل شخص.
cd ~/client-ca
openssl ca -config openssl.cnf -revoke certs/alice.crt
openssl ca -config openssl.cnf -gencrl -out crl.pemيغيّر الأمر الأول حالة ذلك الرقم التسلسلي في index.txt من V إلى R. يكتب الأمر الثاني قائمة إبطال الشهادات (CRL)، وهي ملف موقّع يتضمن أرقام الشهادات التسلسلية المُلغاة. انقل الملف إلى الخادم، ووجّه nginx إليه باستخدام ssl_crl /etc/nginx/client-ca.crl; بجانب التوجيهات الأخرى.
scp crl.pem user@admin.example.com:/tmp/client-ca.crl
ssh user@admin.example.com 'sudo install -m 644 /tmp/client-ca.crl /etc/nginx/client-ca.crl && sudo nginx -t && sudo systemctl reload nginx'إليك المشكلة التي تغلق البوابة أمام الجميع. تحتوي CRL على تاريخ nextUpdate، وتحدده default_crl_days، وقيمته 30 في الإعداد أعلاه. بعد انقضاء ذلك التاريخ، يتعامل OpenSSL مع القائمة على أنها قديمة، ويفشل التحقق من كل شهادة عميل بسبب CRL has expired، وليس من الشهادة المُلغاة فقط. يقرأ nginx الملف عند تحميل إعداداته، لذلك لا يغيّر تحديث CRL على القرص شيئاً حتى تعيد تحميل الإعدادات. أعد إنشاء القائمة وأعد تحميل الإعدادات وفق جدول زمني يقع بهامش مريح داخل هذه المدة، مثل التجديد أسبوعياً مع مدة قدرها 30 يوماً، وتحقق من التواريخ قبل نسخ الملف:
openssl crl -in crl.pem -noout -lastupdate -nextupdateلعدد قليل من المستخدمين، يوجد خيار أبسط. أنت تدير CA، لذلك يمكن لـ nginx رفض رقم تسلسلي مباشرة وتجاوز آلية CRL:
map $ssl_client_serial $revoked {
default 0;
"1002" 1;
}استخدم ذلك مع if ($revoked) { return 403; } في location. لا يوجد له تاريخ انتهاء يمكن نسيانه. لكنه لا ينتقل إلى خوادم أخرى، لذلك لن تعرف أي خدمة أخرى تثق بـ CA لديك شيئاً عنه. إذا كان لديك nginx واحد أمام تطبيق واحد، فهذا هو الخيار البسيط والصريح. انتقل إلى CRL عندما يصبح لديك أكثر من بوابة.
ما المدة المناسبة لصلاحية شهادات العميل؟
اجعل صلاحية شهادات العميل سنة واحدة، أو أقل إذا كنت تستطيع تحمّل جهد إعادة إصدارها. انتهاء الصلاحية هو موضع الفشل الهادئ هنا، لأن حامِل الشهادة لا يتلقى أي تحذير مسبق. قد يفتح لوحة التحكم ذات صباح، فيرفض nginx الاتصال، ويصف المتصفح سبب الرفض بعباراته الخاصة، ونادراً ما تتضمن كلمة «منتهية الصلاحية». اجعل صلاحية CA عشر سنوات، وسجّل تاريخ انتهائها في مكان ستقرأه فعلاً، لأن انتهاء صلاحية شهادة CA يوقف التحقق من كل الشهادات التابعة لها في اليوم نفسه.
يوجد أمران يساعدانك على اكتشاف ذلك مسبقاً:
openssl x509 -in certs/alice.crt -noout -subject -serial -enddate
awk -F'\t' '{print $1, $2, $4}' ~/client-ca/index.txtالعمود الأول في index.txt هو الحالة: V للشهادة السارية، وR للشهادة الملغاة، وE للشهادة منتهية الصلاحية. يعرض العمود الثاني تاريخ الانتهاء بتنسيق YYMMDDHHMMSSZ، بينما يعرض العمود الرابع الرقم التسلسلي. هذا الملف هو سجلك الوحيد لمعرفة حامل كل شهادة، لذا احتفظ بنسخة احتياطية منه مع مفتاح CA، وتعامل مع كليهما على أنهما أسرار.
التجديد يعني إصدار شهادة جديدة، وليس تمديد الشهادة الحالية. أنشئ مفتاحاً جديداً وCSR (طلب توقيع الشهادة)، ثم وقّع الشهادة وسلّمها إلى صاحبها. ألغِ الشهادة القديمة بعد أن يؤكد أن الشهادة الجديدة تعمل.
ما الذي يحمي منه mTLS، وما الذي لا يحمي منه
ما يزيله هو الوصول غير الموثَّق. يرفض الماسح الذي يعثر على اسم مضيفك أثناء عملية المصافحة، لذلك لا يرسل طلب HTTP، ولا يرى نموذج تسجيل الدخول، ولا يتمكن من تجربة كلمة مرور مسروقة عليه. لا يوجد ما يمكن استخدامه في حشو بيانات الاعتماد. وتصبح ثغرة في تدفق تسجيل الدخول للتطبيق غير قابلة للوصول أمام أي شخص لا يملك شهادة. كما أنه يلغي السر المشترك الذي يلصقه الأشخاص في المحادثات، لأن المفتاح الخاص ملف يصعب نسخه بالخطأ.
أما ما لا يعالجه فهو اختراق العميل. تملك البرمجيات الخبيثة على حاسوب محمول ملف المفتاح، وتملك عبارة المرور لحظة إدخال المالك لها. ويبدو ذلك المهاجم للخادم تماماً كمستخدم شرعي، لأن الشهادة تثبت حيازة ملف، ولا تثبت وجود شخص. وتظل كلمة مرور .p12 وتشفير القرص بالكامل مهمَّين.
وهو ليس آلية تفويض أيضاً. تصل كل شهادة صالحة إلى كل ما يقدمه server block ذلك، ما لم تتحقق من $client_cn وتتخذ إجراءً بناءً على قيمته. وبالإعداد الافتراضي، تكون صلاحيات حاملي شهادتين متطابقة.
كما أنه يحمي المسار عبر nginx فقط. إذا كان التطبيق يستمع أيضاً على منفذ عام، فإن mTLS أمامه يصبح إجراءً شكلياً: اربط التطبيق بـ127.0.0.1 وأبقِ المنفذ الخاص به محجوباً في جدار الحماية. أما المدخل الآخر إلى الخادم نفسه فهو SSH، ويستحق القدر نفسه من الاهتمام، كما هو موضح في تقوية الوصول إلى SSH على VPS الخاص بك.
هناك حد أخير، ويظهر أثره في اليوم الذي تفعّل فيه ذلك. يتوقف كل ما لا يستطيع تقديم شهادة عن العمل: أداة مراقبة التوافر، أو webhook من مزود دفع، أو قارئ RSS، أو تطبيق جوّال لا يتوفر له مخزن شهادات يمكنك الوصول إليه. قرر كيفية التعامل مع هذه الحالات قبل ضبط ssl_verify_client on، لأن الفشل يكون كاملاً، ويحدث بصمت من جانبها.
اقرأ ما يعرضه العميل عند رفضه
تختلف الرسالة التي يعرضها العميل المرفوض باختلاف المتصفح وإصدار curl ومكتبة TLS المستخدمة تحته. لذلك اقرأ ما يطبعه عميلك بدلاً من مطابقته مع رسالة مكتوبة في مكان آخر. توجد التفاصيل المفيدة على الخادم.
sudo tail -n 50 /var/log/nginx/error.logيترك رفض الشهادة سطراً يحتوي على client SSL certificate verify error متبوعاً بالسبب الذي قدّمه OpenSSL. هذا السبب هو المعلومة التي يجب التصرف بناءً عليها. عادةً يكون السبب واحداً من عدة أسباب محددة. صدرت الشهادة من CA مختلف عن الملف المحدد في ssl_client_certificate. انتهت فترة صلاحية الشهادة أو لم تبدأ بعد. تجاوزت CRL الموجودة على الخادم تاريخ nextUpdate، ولذلك أصبحت ترفض جميع العملاء بدلاً من عميل واحد.
عندما لا يعرض المتصفح شهادة على الإطلاق، تكون المشكلة قد حدثت قبل مرحلة التحقق. يرسل nginx أسماء الجهات المصدرة المقبولة أثناء عملية المصافحة، ولم يجد المتصفح في مخزنه شيئاً يطابقها، لذلك لم يكن لديه ما يعرضه. استورد .p12 مرة أخرى إلى ملف التعريف الذي تستخدمه فعلياً للتصفح.
هناك حالة أخرى تستحق الذكر. إذا اختبرت باستخدام شهادة عميل موقعة ذاتياً فقط، بدلاً من شهادة وقّعتها CA، فلن ينجح التحقق، لأن nginx يتحقق من التوقيع مقابل ملف CA، والشهادة الموقعة ذاتياً ليست موجودة فيه. آلية إنشاء الشهادة هي نفسها الموضحة في إنشاء شهادة موقعة ذاتياً على Ubuntu. يحتاج mTLS فقط إلى الخطوة الإضافية التي توقّع فيها CA الشهادة.
FAQ
هل ما زلت أحتاج إلى شهادة Let's Encrypt إذا استخدمت mTLS؟
نعم. الشهادتان مستقلتان عن بعضهما. يقدّم الخادم شهادته الخاصة حتى يثق المتصفح باسم المضيف، ولا تزال هذه الشهادة مطالبة بأن تصدر عن CA يعرفه المتصفح مسبقاً. أما CA الخاص بالعملاء فهو سلسلة خاصة منفصلة، وتُستخدم فقط للتحقق من هوية المتصل. لا يغيّر ضبط ssl_client_certificate شيئاً في الشهادة التي يقدّمها nginx، ويجب ألا يشير إلى سلسلة Let's Encrypt الخاصة بك.
لماذا لا يطلب مني المتصفح أبداً اختيار شهادة؟
يرسل nginx قائمة بالجهات المصدرة المقبولة أثناء المصافحة، ويكوّنها من الملف الموجود في ssl_client_certificate. لا يعرض المتصفح إلا الشهادات التي تظهر الجهة المصدرة لها في تلك القائمة. لذلك يعني عدم ظهور المطالبة أن المتصفح لا يحتوي على شهادة صادرة عن CA الخاص بك: إما أن الاستيراد تم في ملف تعريف مختلف للمتصفح، أو أن الشهادة وقّعتها CA مختلفة عن تلك المثبتة على الخادم. شغّل openssl s_client -connect admin.example.com:443 وابحث في الناتج عن أسماء CA لشهادات العملاء المقبولة لمعرفة CA التي يطلبها الخادم فعلياً.
هل يمكنني طلب شهادة عميل على عنوان URL واحد فقط؟
ليس باستخدام ssl_verify_client on داخل location. تُتبادل الشهادة أثناء المصافحة، قبل أن يعرف nginx مسار الطلب، كما أن إعادة المصافحة التي كانت ستوفر حلاً لذلك أُزيلت من TLS 1.3 ومُنع استخدامها في HTTP/2. اضبط ssl_verify_client optional; في كتلة الخادم، ثم اختبر $ssl_client_verify في كل location محمي وأعد 403 عندما لا تكون قيمته SUCCESS.
كيف ألغي الوصول لشخص واحد؟
ألغِ تلك الشهادة باستخدام openssl ca -revoke، وأعد إنشاء القائمة باستخدام openssl ca -gencrl، وانسخها إلى الخادم، ثم أعد تحميل nginx حتى يقرأ الملف الجديد. لن يتأثر أي شخص آخر، لكن ذلك لا يعمل إلا إذا كانت لكل شخص شهادته الخاصة، لا شهادة مشتركة. راقب تاريخ nextUpdate الخاص بـCRL، لأن انتهاء صلاحية CRL يؤدي إلى فشل التحقق لكل العملاء، وليس للعملاء الذين أُلغيت شهاداتهم فقط.
هل يستبدل mTLS صفحة تسجيل الدخول؟
بالنسبة إلى الوصول، نعم: من دون شهادة لا يصل أي شيء إلى التطبيق، لذلك لا توجد استمارة يمكن مهاجمتها ولا كلمة مرور يمكن تخمينها. أما بالنسبة إلى الهوية داخل التطبيق، فلا. تثبت الشهادة أن المتصل يملك ملف مفتاح، ولذلك يُعد الكمبيوتر المحمول المسروق مستخدماً صالحاً. مرّر CN إلى الخدمة في الاتجاه الخلفي، واحتفظ بالحسابات والصلاحيات التي يوفّرها التطبيق أصلاً، وتعامل مع الشهادة باعتبارها بوابة أمامها.