IP المصدر خلف Cloudflare يتسرّب عبر النطاق الفرعي deploy — ثلاثة مستودعات في أربع عشرة ثانية، أُصلحت دفعةً واحدة
في تلك الظهيرة حدّقت في GitHub قليلاً. أغلقت ثلاثة مستودعات طلبات الدمج عند 16:20:31 و16:20:38 و16:20:45 — بفارق أربع عشرة ثانية.
smarts #38: deploy.smarts.md ← deploy.smartshow2claude #13: deploy.how2claude.com ← deploy.how2claudepickful #118: deploy.pickful.ai / deploy.pickful.xyz ← deploy.pickful / deploy.pickful-alphaكانت الإصلاحات الثلاثة من النوع نفسه. كنت أتحدث مع Claude عن خلل صغير غير ذي صلة في النشر داخل مستودع how2claude. ألقى نظرة عابرة على config/deploy.yml فقال لي إنّ IP خادم المصدر لتلك الآلة ظاهر أمام الجميع في DNS العام.
كل مشاريعي خلف Cloudflare. السحابة البرتقالية الصغيرة في لوحة Cloudflare تعني أنّ السجلّ مُمَرَّر عبر الوكيل — يصل طلب HTTP أولاً إلى عقدة edge في Cloudflare ثم إلى المصدر عندي. لا يظهر IP المصدر في ردود DNS؛ ما تُعيده DNS هو IP من نوع anycast تابع لـCloudflare. هذا الجزء كان سليماً.
لكنّي أنشر باستخدام Kamal. يقوم Kamal بـSSH إلى الخادم ليُشغّل docker. لا يستطيع SSH العبور خلال HTTP proxy، لذا احتجت إلى hostname غير مُمَرَّر عبر Cloudflare لأقوم بـSSH عبره. كان إعدادي حينها:
# config/deploy.yml
servers:
web:
- deploy.how2claude.com # ← سجلّ A مباشر إلى IP المصدر، سحابة رمادية (DNS only)
في Cloudflare كان how2claude.com وwww.how2claude.com برتقاليَّين. أما deploy.how2claude.com فكان رمادياً. يجب أن يكون رمادياً، وإلا لما وصل SSH إلى الجهاز.
الرمادي يعني DNS عاماً. يستطيع أيّ شخص أن يُشغّل:
$ dig deploy.how2claude.com +short
<IP المصدر عندي>
ثم إنّ نمط التسمية deploy.<domain> بحد ذاته علامة فارقة — امسح النطاقات الفرعية deploy.* على قائمة من نطاقات SaaS الشائعة، وسيكون لديك حصاد من IP خوادم المصدر التي كان يُفترض أن تختبئ خلف Cloudflare.
تجاوزٌ لـWAF، تجاوزٌ لتحديد المعدل، تجاوزٌ لحماية DDoS. على بُعد أمر dig واحد.
كنّا نتحدث في موضوع آخر. طلبت منه أن يُراجع وثيقة نشر كتبتها للتو؛ فتح config/deploy.yml للمقارنة، ووصل إلى السطر السابع — - deploy.how2claude.com — توقّف لحظة، ثم قال شيئاً قريباً من هذا:
هذا الـhostname يُحلّ عبر DNS العام، أليس كذلك؟ يعني أنّ أيّ مَن يُشغّل استعلام DNS يحصل على IP المصدر، وبالتالي يُتجاوَز وكيل edge الخاص بـCloudflare.
تجمّدتُ ثانيةً. هذا الأمر كان عليّ أن أنتبه إليه — كان يكفي أن أنظر مرّةً ثانية إلى أيقونة السحابة الرمادية أثناء إعداد Cloudflare وأسأل نفسي ما الذي تعنيه — لكنّي لم أفعل. كنت أتعامل مع ذلك الـhostname كأنّه شيء "داخلي"، لمجرّد أنّ بادئته deploy.. منحَه دماغي بهدوء خصوصيةً لم تكن له يوماً.
Claude لا يحمل هذا الانحياز. يقرأ سلسلة نصية في حقل yaml ويطرح السؤال الميكانيكي: كيف تتحوّل هذه السلسلة إلى IP؟ الجواب: DNS عام. الاستنتاج: IP المصدر عام.
/etc/hostsتبيّن أنّ الإصلاح بسيط لدرجة محرجة. يطلب Kamal من عميل SSH المحلي أن يحلّ الـhostname، أي يكفي أن يُحَلّ هذا الـhostname على جهازي وحده — لا حاجة لأن يُحَلّ في الإنترنت كلّه.
اقصُر الـhostname في الـyaml:
servers:
web:
- deploy.how2claude # ← انتبه: من دون .com
ثم أضِف سطراً إلى /etc/hosts عندي:
198.51.100.42 deploy.how2claude
تحتاج عقد تشغيل CI التي تنفّذ النشر إلى السطر نفسه (متغيّر بيئة، أو مباشرةً echo >> /etc/hosts في الـworkflow).
النتيجة:
deploy.how2claude.* في DNS العام في أيّ مكانkamal deploy / app exec / app logs بالعمل، لأنّ /etc/hosts يُستشار قبل DNSملحق قاعدة البيانات يحتاج إلى التغيير نفسه:
accessories:
db:
image: postgres:17
host: deploy.how2claude # ← الـhostname نفسه
هذا هو الجزء الذي أردتُ تدوينه.
بعد أن أنجزنا إصلاح how2claude، كنت سأنتقل إلى سياق آخر. أوقفني Claude: «أنت تستخدم Kamal أيضاً في smarts وpickful، أليس كذلك؟ دعني أتفقّدهما».
تفقّدهما.
deploy.smarts.md — المشكلة نفسها، سجلّ A عامdeploy.pickful.ai (production)، وdeploy.pickful.xyz (alpha)، فضلاً عن deploy-test1.pickful.ai ميت منذ زمن، وdeploy.staging.yml يُشير إلى نطاق متقاعد، blockgeek.comكان pickful الأكثر تشابكاً بين الثلاثة لأنّ فيه عدّة destinations (production / alpha / test2) وحملاً تاريخياً. كَنَس Claude بالمناسبة destinations staging وtest1 الميتة — لم يكن لها سبب للوجود، مجرّد ضوضاء.
الـcommits النهائية:
smarts 6482472 2026-04-27 16:20:31 Use private deploy.smarts alias instead of public deploy.smarts.md (#38)
how2claude ccc0344 2026-04-27 16:20:38 Use private deploy.how2claude alias instead of public deploy.how2claude.com (#13)
pickful e6bf9af 2026-04-27 16:20:45 Deploy config hygiene + privatize hostnames (#118)
أربع عشرة ثانية. صحيح أنّ هذا فقط وقت إفراغ GitHub لقائمة الدمج — أمّا العمل الفعلي فقد توزّع على الساعتين السابقتين. لكن الشكل العملي كان: اكتشاف واحد، وإصلاح في ثلاثة مستودعات.
لو كنت أعمل وحدي، فالنسخة الواقعية ستكون: أصلحه في how2claude، أكتب TODO يقول "افعل الشيء نفسه في smarts وpickful"، ثم أُشاهد ذلك الـTODO راقداً في القائمة ثلاثة أشهر. عشتُ هذا التسلسل بحذافيره مرّات عديدة.
إن كنت تنشر باستخدام Kamal أو Capistrano أو أيّة أداة نشر تعتمد على SSH:
config/deploy*.yml وgrep عن host: وservers:. أدرج كلّ hostname يظهر.dig +short على كلّ منها. كلّ ما يُعيد IP المصدر بدلاً من لا شيء يكون مُسرَّباً.deploy.<project>)، وضع الـIP في /etc/hosts.echo "$IP deploy.<project>" | sudo tee -a /etc/hosts.إن كان فريقك يدير عدّة مشاريع تتشارك النمط البنيوي نفسه، فاكتب grep من سطر واحد يمسح كلّ deploy*.yml في كلّ المستودعات؛ هذا أوثق من فحصها واحداً واحداً.
لم يخفق Cloudflare، ولم يخفق Kamal — كلتا الأداتين كانتا تؤدّيان عملهما تماماً.
الدرس هو: القيم الافتراضية تُضلّلك بهدوء. deploy.<your-domain>.com يبدو كاسم داخلي، لكنّ DNS لا يعرف مفهوم "الداخلي" — سجلّ A هو سجلّ A، ومتى نُشر فهو عام. قد يمنحك الاسم وهم الخصوصية، وأيقونة السحابة الرمادية الصغيرة في لوحة Cloudflare ليست سوى الصيغة البصرية لذلك الوهم نفسه: تجلس هناك بهدوء، بلا تحذير أحمر، لكنّها في الحقيقة تخبرك "هذا السجلّ لا يمرّ بي".
دع Claude يقرأ إعدادات النشر لديك مرّة واحدة. هو لا يشاركك ذلك الوهم.