يفشل SSH العام عبر مُرحِّل عند حدّين غير مترابطين، وإصلاح أحدهما يكشف عادةً الآخر. إما أن تُرجع الحافة استجابة HTTP بدل تمرير تدفق، وإما أن تُضيف الترويسة قبل الحمولة ترويسةً لم يوافق الخادم الخلفي على قراءتها. ثلاث حالات تحدد أيّ الأمرين تراه: إلى ماذا يشير السجل العام فعلاً، وماذا يرسل كتلة التدفق إلى المنبع، وأي اسم يتحقق منه عميل SSH. عالِجها بهذا الترتيب.

حدّان، عرضان

يبدو الفشل عند الحافة كصفحة حالة HTTP تصل حيث ينبغي أن تصل لافتة SSH. يكتمل الاتصال على مستوى TCP وتكون البايتات الأولى نصاً يخص خادم ويب. ذلك تضارب بروتوكولات، لا حزمة مفقودة. ويخبرك أيضاً أن الحافة تخدم عمداً شيئاً آخر لذلك الاسم.

يبدو الفشل عند المنبع مختلفاً. يصل الاتصال إلى sshd، ويقرأ sshd بايتات ليست تبادل التعريف الذي يتوقعه، فتموت الجلسة أو تتعلّق دون أن تقدّم مفتاحاً قط.

يسهل الخلط بين الاثنين لأن كليهما ينتج عميلاً يتصل ثم يفشل. أول استجابة بروتوكول تتلقاها هي ما يميّز بينهما، ولهذا يجب أن تُدرَج في التذكرة.

ترويسة PROXY رسالة، لا مفتاح تبديل

في وحدة التدفق، تأمر proxy_protocol on NGINX بكتابة ترويسة بروتوكول PROXY نحو المنبع قبل الحمولة. وهي لا تمكّن تمرير TCP، ولا علاقة لها بتشفير SSH. إنها ملاحظة موجَّهة إلى المستقبل. وإن لم يستطع المستقبل قراءتها، صارت حطاماً في بداية التدفق.

على الخدمة المتلقية أن تستهلك تلك الترويسة عمداً. يتوقع sshd العادي تبادل تعريف SSH كأول بايتات على الاتصال، لذا تكسر مقدمة غير متوقعة المصافحة بطريقة تشبه خادماً تالفاً.

stream {
    server {
        listen 2222;
        proxy_pass 10.77.0.20:22;
    }
}

تلك القطعة هي كل ما يلزم لمُرحِّل عامل لخادم خلفي يمكن الوصول إليه عبر VPN. لا يظهر أي توجيه PROXY لأن المستقبل لا يتحدث البروتوكول، وهذا الغياب قرار لا سهو.

افصل أيضاً الاتجاهين. ضبط proxy_protocol على سطر listen يعني أن NGINX يتوقع أن يرسل عميله هو الترويسة أولاً؛ وتمكينه لعميل SSH عادي ينتج عكس المشكلة التي يحاول الناس حلها عادةً تماماً.

ما يبلغه اسم المضيف العام فعلاً

صفحة HTTP 400 داخل جلسة SSH دليل على الحافة. شيء في المسار يتحدث HTTP، ولن يغيّر ذلك أي قدر من العمل على المفتاح أو الحساب في الخادم الخلفي. توقّف عن تغيير الخادم الخلفي حتى تُحسم الحافة.

يحمل سجل Cloudflare العادي المُوكَّل حركة HTTP وHTTPS المدعومة على منافذ محددة. وتشغيل التوكيل لاسم مضيف لا ينشئ مُرحِّل TCP عاماً، ونقل SSH إلى المنفذ 443 لا يجعله HTTPS.

يوجد اتجاهان قابلان للعمل. يمكن لسجل DNS فقط أن يشير إلى مُرحِّلك الخاص بـ TCP، ما يضع قرار التعرّض في يديك. بدلاً من ذلك، استخدم منتجاً يوثّق نقل SSH صراحةً، مع إعدادات المصدر والعميل والوصول الخاصة به.

أياً كان اختيارك، لا يزال المُرحِّل يحتاج إلى مسار VPN إلى الخادم الخلفي واتصال مسموح به ومسار عودة. والمسار ليس اختيارياً لأن الجانب العام يعمل.

أبقِ هوية SSH ثابتة عبر اسم مستعار

يريد المستخدمون عادةً مواصلة كتابة اسم المضيف الحالي. يمكن لاسم مستعار في إعداد SSH أن يفعل ذلك دون تغيير أي شيء في المُرحِّل، ويُبقي التغيير على جانب العميل حيث ينتمي.

Host git.example.com
    HostName ssh-entry.example.net
    Port 2222
    User git
    HostKeyAlias git.example.com

بدون HostKeyAlias، يبحث OpenSSH عن مفتاح المضيف تحت الاسم الذي يتصل به، فينتقل الإدخال ويرى كل مستخدم تحذيراً بتغيّر المفتاح عند أول استخدام. وضبط الاسم المستعار يُبقي إدخالات known-hosts مستقرة بينما يذهب الاتصال إلى المُرحِّل.

ينطبق هذا الاسم المستعار على المراجع القائمة على SSH فقط. ولا يراجعه عنوان Git من نوع HTTPS، ويجب أن يطابق عنوان المرجع المعتاد وسيلة النقل التي جهّزتها فعلاً.

ما يجب ألا تفعله هو تعطيل التحقق من مفتاح المضيف لإسكات التحذير. فذلك يزيل حماية لم يطلب إصلاح التمرير لمسّها، ويحوّل إزعاجاً شكلياً إلى تعرّض حقيقي.

تحقّق من كل قفزة على حدة

اختبر المستمع العام بمفرده، ثم اتصال المُرحِّل بالخادم الخلفي من مضيف المُرحِّل، وبعد ذلك فقط العميل من طرف إلى طرف. كل واحد من تلك الاختبارات الثلاثة يستبعد طبقة مختلفة. وتخطّي واحد يعني أن النتيجتين المتبقيتين لا يمكن تفسيرهما. ثلاث ملاحظات، وثلاثة مواضع يمكن أن يسكنها الفشل.

احتفظ بعنوان الوجهة والمنفذ وأول بايتات البروتوكول مع كل نتيجة. ذلك الثلاثي يميّز خطأ الحافة عن مشكلة توجيه VPN عن فشل مصادقة SSH، وهو الفرق بين إصلاح وتخمين.

وأخيراً، وثّق التعرّض المقصود. مُرحِّل TCP ينقل SSH إلى خادم خلفي خاص هو فتح متعمد في المحيط العام، ويستحق المراجعة نفسها التي يستحقها أي خدمة واردة أخرى.

مقالات ذات صلة