لنفرض أن لديك تطبيقاً على السيرفر خلف ال Reverse Proxy، بنطاق وشهادة TLS وصفحة دخول بكلمة مرور قوية، فسوف يبدو لك أن العمل قد انتهى، فالاتصال مشفر ولا يدخل إلا من يعرف كلمة المرور. ولكن افتح سجل الطلبات Access Log بعد يوم واحد من النشر، وسوف تجد آلاف الطلبات من عناوين لا تعرفها: محاولات على /wp-login.php وأنت لا تستخدم WordPress أصلاً، وطلبات تبحث عن /.env و/.git/config، وسلسلة محاولات دخول على صفحة /login بأسماء مستخدمين شائعة، وهذه كلها برامج آلية Bots تفحص الإنترنت كله دون أن تعرف عنك شيئاً.
وكلمة المرور تحمي بوابة واحدة فقط، أما الطلبات التي لا تمر بها فلا تحميها، كملف إعداد نسيته في مجلد عام، أو ثغرة Vulnerability في التطبيق نفسه قبل صفحة الدخول، أو صفحة تعرض داخل إطار iframe في موقع مزيف، أو برنامج يجرب ملايين كلمات المرور دون أن يوقفه أحد. لذلك فالحل ليس كلمة مرور أطول، وإنما طبقات حماية Defense in Depth يضعها ال Reverse Proxy أمام كل تطبيق، بحيث إذا تجاوز المهاجم طبقة وجد أمامه أخرى.
وفي هذا المقال سوف نتناول:
- لماذا لا تكفي كلمة المرور، وما الطبقات التي يمكن أن يضيفها ال Reverse Proxy دون تعديل سطر واحد في كود التطبيق.
- ترويسات الأمان Security Headers: ما تمنعه كل ترويسة، وكيف تضيفها في Nginx Proxy Manager وTraefik وCaddy، مع أخطاء شائعة تجعلها تختفي دون أن تنتبه.
- الحد من معدل الطلبات Rate Limiting ضد تخمين كلمات المرور، وقصر لوحات الإدارة على عناوين محددة أو على شبكة VPN.
- الحظر التلقائي مع fail2ban وCrowdSec، وجدار حماية تطبيقات الويب Web Application Firewall مع OWASP Core Rule Set، ومقارنته بما يقدمه Cloudflare مجاناً.
- إخفاء المعلومات التي لا يحتاجها الزائر، وأين يقع Forward Auth بين هذه الطبقات، ثم قائمة تحقق تطبقها على كل تطبيق جديد.
والأمثلة كلها مختبرة على Nginx Proxy Manager 2.16 وTraefik 3.7 وCaddy 2.11، مع ملاحظات عن HAProxy حيث يلزم، وإذا لم تختر أداتك بعد فابدأ بمقال أي Reverse Proxy يناسبك؟.
لماذا لا تكفي كلمة المرور وحدها؟
الحل الأول الذي يخطر على البال لحماية أي تطبيق هو كلمة مرور قوية، وهذا صحيح كخطوة أولى، ولكنه يفشل في ثلاث حالات على الأقل. الأولى أن صفحة الدخول نفسها تستقبل عدداً غير محدود من المحاولات، فإذا كان أحد المستخدمين يستخدم كلمة مرور ضعيفة أو مسربة من موقع آخر فسوف يجدها برنامج التخمين Brute Force عاجلاً أو آجلاً. والثانية أن التطبيق فيه كود يعمل قبل صفحة الدخول، كالملفات الثابتة وواجهة ال API وصفحة إعادة تعيين كلمة المرور، وأي ثغرة في هذا الكود لا تحتاج إلى كلمة مرور أصلاً. والثالثة أن المتصفح نفسه يمكن أن يستخدم ضد المستخدم، كأن تعرض صفحتك داخل إطار في موقع آخر ليضغط المستخدم على زر لا يراه.
لذلك سوف نبني الحماية في طبقات، وكل طبقة تغطي ما لا تغطيه غيرها:
| الطبقة | ما تمنعه | أين نضعها |
|---|---|---|
| ترويسات الأمان Security Headers | هجمات تمر عبر المتصفح، مثل عرض الصفحة في إطار أو تنفيذ سكربت محقون | كل تطبيق |
| الحد من معدل الطلبات Rate Limiting | تخمين كلمات المرور والطلبات المتكررة | صفحات الدخول وواجهات API |
| قوائم العناوين المسموحة IP Allow List | وصول أي شخص من الإنترنت إلى لوحات الإدارة | اللوحات والأدوات الداخلية |
| الحظر التلقائي Automatic Banning | عنوان يكرر محاولات فاشلة أو يفحص المسارات | السيرفر كله |
| جدار حماية تطبيقات الويب WAF | أنماط الهجوم المعروفة مثل SQL Injection وXSS | التطبيقات المكشوفة للعموم |
| إخفاء المعلومات | كشف الإصدارات والملفات الحساسة ورسائل الأخطاء | كل تطبيق |
وقد يتساءل البعض: لماذا نضع هذه الطبقات في ال Reverse Proxy وليس في كل تطبيق؟ والإجابة أن ال Reverse Proxy هو الباب الوحيد الذي تمر منه كل الطلبات إذا كانت تطبيقاتك لا تنشر أي منفذ على السيرفر كما شرحنا في دليل شبكات Docker وأفضل الممارسات، وبالتالي تكتب الإعداد مرة واحدة في مكان واحد، وتحمي به تطبيقات لا تملك كودها ولا تستطيع تعديله. ولاحظ أن هذه الطبقات لا تغني عن تحديث التطبيقات نفسها، والسبب أن أي ثغرة في التطبيق تبقى موجودة خلفها، وكل ما تفعله الطبقات أنها تصعب الوصول إليها.
ترويسات الأمان: تعليمات يرسلها السيرفر إلى المتصفح
ترويسة الأمان Security Header سطر يضيفه السيرفر إلى الاستجابة Response فيغير به سلوك المتصفح مع الصفحة، كأن يرفض فتحها عبر HTTP أو عرضها داخل إطار أو تشغيل سكربت لا يعرفه، فهي لا تحمي السيرفر من المهاجم مباشرة، وإنما تحمي مستخدميك من أن يستخدم المتصفح ضدهم، لذلك هي رخيصة ويجب أن تكون على كل تطبيق، ويكفيك هنا ما تحتاجه هذه الطبقة من إعداد، أما شرح كل ترويسة وقيمها ومخاطرها بالتفصيل فتجده في دليل ترويسات الأمان بالتفصيل: HSTS وCSP، وفي دليل OWASP لترويسات HTTP.
HSTS: لا تقبل HTTP مع هذا النطاق مرة أخرى
الترويسة Strict-Transport-Security واختصاراً HSTS تطلب من المتصفح أن يحول كل طلب لهذا النطاق إلى HTTPS بنفسه مدة max-age بالثواني، فلا يخرج الطلب الأول عبر HTTP حيث يمكن اعتراضه:
Strict-Transport-Security: max-age=31536000; includeSubDomainsولا تضف includeSubDomains إلا إذا كانت كل نطاقاتك الفرعية تعمل عبر HTTPS، ولا تضف preload إلا بقرار مدروس، والسبب أن الخروج من القائمة المسبقة يستغرق شهوراً، وابدأ بمدة قصيرة ثم ارفعها على مراحل.
max-age=63072000; preload دائماً، ويضيف includeSubDomains إذا فعلت HSTS Subdomains، فيصبح النطاق مستوفياً لشروط الإدراج في القائمة المسبقة، لذلك لا تفعل الخيارين معاً على النطاق الرئيسي قبل أن تراجع كل نطاقاتك الفرعية.Content-Security-Policy: من أين يحمل المتصفح السكربتات؟
الترويسة Content-Security-Policy واختصاراً CSP قائمة بالمصادر التي يسمح للصفحة أن تحمل منها السكربتات والصور والاتصالات، فيرفض المتصفح السكربت المحقون في هجمة XSS، والسياسة التالية تسمح بنفس النطاق فقط:
Content-Security-Policy: default-src 'self'; frame-ancestors 'none'ولأنها مرتبطة بالتطبيق نفسه فقد تكسر جزءاً منه، لذلك نبدأ بالصيغة Content-Security-Policy-Report-Only التي تسجل المخالفات دون أن تمنع شيئاً، ولا نضيف سياسة ثانية إذا كان التطبيق يرسل سياسته الخاصة.
X-Frame-Options وframe-ancestors: لا تعرض صفحتي في إطار
ضد هجمة Clickjacking نضع X-Frame-Options: DENY مع التوجيه frame-ancestors داخل CSP، والسبب أن frame-ancestors لا تمنع شيئاً ما دامت السياسة بالصيغة Report-Only.
ثلاث ترويسات صغيرة وواحدة لا تضفها
X-Content-Type-Options: nosniff: لا يخمن المتصفح نوع الملف من محتواه.Referrer-Policy: strict-origin-when-cross-origin: لا يرسل إلى المواقع الأخرى إلا اسم النطاق.Permissions-Policy: القيمةcamera=()تمنع أي صفحة من طلب الكاميرا، فلا تضعها على تطبيقات الاجتماعات المرئية.X-XSS-Protection: مهجورة، والمتصفحات الحديثة أزالت الفلتر الذي تتحكم فيه، فلا تضفها.
إضافة الترويسات في Nginx Proxy Manager
في NPM تكتب الترويسات في تبويب Advanced الخاص بال Proxy Host، والحل الذي تجده في أغلب الشروحات هو add_header:
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;ولكنه لا يعمل في NPM، والسبب أن التوجيه add_header لا يورث إلى كتلة location التي فيها add_header خاص بها، وNPM يضع في كل location السطر add_header X-Served-By $host;، فتلغى ترويساتك بصمت. والحل التوجيه more_set_headers من وحدة headers-more المدمجة في OpenResty:
more_set_headers "X-Content-Type-Options: nosniff";
more_set_headers "X-Frame-Options: DENY";
more_set_headers "Referrer-Policy: strict-origin-when-cross-origin";
more_set_headers "Permissions-Policy: camera=(), microphone=(), geolocation=()";
more_set_headers "Content-Security-Policy-Report-Only: default-src 'self'; frame-ancestors 'none'";
more_clear_headers Server;احفظ، ثم اختبر من السيرفر نفسه:
curl -s -I -H 'Host: app.example.com' http://127.0.0.1/والمخرج سوف يكون كما يلي:
HTTP/1.1 200 OK
Date: Sun, 04 Oct 2026 08:14:33 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 280
Connection: keep-alive
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Content-Security-Policy-Report-Only: default-src 'self'; frame-ancestors 'none'
X-Served-By: app.example.comفي الإعداد أعلاه لاحظ التالي:
- اختفت
Server: openrestyبفضلmore_clear_headers Server، أماX-Served-Byفيضيفها NPM ولا تكشف إلا اسم النطاق. - لا توجد
Strict-Transport-Securityلأننا اختبرنا عبر HTTP، وتفاصيل كتابتها بقيمتك بدلاً من خيار التبويب في الدليل المفصل المذكور أعلاه. - ولأن
more_set_headersتعمل على مستوىserver، فهي تظهر أيضاً على استجابات الأخطاء مثل403و429التي سوف نراها في الأقسام التالية.
إضافة الترويسات في Traefik
في Traefik نعرف ال Middleware من نوع headers مرة واحدة في ملف داخل مجلد الإعداد الديناميكي Dynamic Configuration، أي /opt/traefik/dynamic/security.yml:
http:
middlewares:
security-headers:
headers:
stsSeconds: 31536000
stsIncludeSubdomains: true
contentTypeNosniff: true
frameDeny: true
referrerPolicy: "strict-origin-when-cross-origin"
permissionsPolicy: "camera=(), microphone=(), geolocation=()"
contentSecurityPolicyReportOnly: "default-src 'self'; frame-ancestors 'none'"ونربطه بال Router بالسطر traefik.http.routers.app.middlewares=security-headers@file، ثم نختبر:
curl -s -I -H 'Host: app.example.com' http://127.0.0.1/والمخرج سوف يكون كما يلي:
HTTP/1.1 200 OK
Content-Length: 345
Content-Security-Policy-Report-Only: default-src 'self'; frame-ancestors 'none'
Content-Type: text/plain; charset=utf-8
Date: Sun, 04 Oct 2026 08:10:27 GMT
Permissions-Policy: camera=(), microphone=(), geolocation=()
Referrer-Policy: strict-origin-when-cross-origin
X-Content-Type-Options: nosniff
X-Frame-Options: DENYولا تظهر Strict-Transport-Security هنا لأن Traefik يضيفها على طلبات HTTPS فقط، وهذا هو السلوك الصحيح.
إضافة الترويسات في Caddy
في Caddy نستخدم التوجيه header داخل مقتطف Snippet نستورده في كل موقع:
(security) {
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "strict-origin-when-cross-origin"
Permissions-Policy "camera=(), microphone=(), geolocation=()"
Content-Security-Policy-Report-Only "default-src 'self'; frame-ancestors 'none'"
-Server
-Via
}
}
app.example.com {
import security
reverse_proxy whoami:80
handle_errors {
import security
respond "{err.status_code} {err.status_text}"
}
}في الإعداد أعلاه لاحظ التالي:
- العلامة
-قبل اسم الترويسة تعني حذفها، فلا يعلن السيرفر اسمه ولا اسم ال Proxy. - الكتلة
handle_errorsتجعل الأخطاء التي يولدها Caddy نفسه، كالرمز429في قسم الحد من معدل الطلبات، تحمل نفس الترويسات بدلاً من أن تخرج ومعهاServer: Caddyفقط.
أما في HAProxy فتضيف الترويسات بالتوجيه http-response set-header وتحذفها بالتوجيه http-response del-header داخل ال frontend، مثل http-response set-header X-Frame-Options DENY وhttp-response del-header Server، وتجد هيكل الملف كاملاً في دليل HAProxy.
الحد من معدل الطلبات Rate Limiting ضد تخمين كلمات المرور
برنامج التخمين يرسل مئات المحاولات في الدقيقة إلى صفحة الدخول، والإنسان الحقيقي لا يحتاج إلى أكثر من محاولتين أو ثلاث، وهذا الفرق هو ما يستغله الحد من معدل الطلبات، فتحدد عدد الطلبات المسموح بها لكل عنوان IP خلال مدة معينة، وما زاد عنها يرد عليه ال Reverse Proxy بالرمز 429 Too Many Requests دون أن يصل إلى التطبيق. ولاحظ أننا نضع الحد على مسار الدخول وحده وليس على التطبيق كله، والسبب أن صفحة واحدة في تطبيق حديث قد تطلب عشرات الملفات خلال ثانية، فإذا وضعت حداً منخفضاً على كل المسارات فسوف يحظر المستخدمون الحقيقيون أنفسهم.
وقبل أن تبدأ تأكد أن ال Reverse Proxy يرى عنوان الزائر الحقيقي، فإذا كان أمامه Cloudflare أو Load Balancer ولم تضبط الثقة بالترويسات، فسوف يرى كل الطلبات قادمة من عناوين قليلة، فيحظر الجميع عندما يتجاوز أحدهم الحد، وتجد الحل لكل أداة في دليل تطبيقك خلف Reverse Proxy.
في Nginx Proxy Manager
يعتمد Nginx على وحدة limit_req، وهي تحتاج إلى سطرين: الأول limit_req_zone الذي يعرف المنطقة Zone ومعدلها ويجب أن يكون في كتلة http، والثاني limit_req الذي يطبقها على مسار معين. وتبويب Advanced يكتب داخل كتلة server فقط، لذلك نضع السطر الأول في ملف من الملفات المخصصة التي يقرؤها NPM وهو /data/nginx/custom/http_top.conf، أي داخل ال Volume npm_data:
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_req_status 429;وبعد إنشاء الملف أعد تشغيل NPM مرة واحدة بالأمر docker compose restart npm حتى يقرأه، ثم أضف في تبويب Advanced الخاص بال Proxy Host، تحت سطور الترويسات، الكتلة التالية:
location = /login {
limit_req zone=login burst=5 nodelay;
include conf.d/include/proxy.conf;
}في الإعداد أعلاه لاحظ التالي:
- المفتاح
$binary_remote_addrهو عنوان الزائر بصيغة مختصرة، والمساحة10mتكفي لنحو 160 ألف عنوان كما يبين التوثيق. - المعدل
5r/mيعني خمسة طلبات في الدقيقة، أي طلباً كل 12 ثانية، وburst=5يسمح بخمسة طلبات إضافية دفعة واحدة، وnodelayيمررها مباشرة بدلاً من تأخيرها. - القيمة الافتراضية لرمز الرفض في Nginx هي
503، والسطرlimit_req_status 429يجعلها429، وهو الرمز الصحيح الذي تفهمه الأدوات وبرامج المراقبة. - السطر
include conf.d/include/proxy.confيجعل هذه الكتلة تمرر الطلب إلى التطبيق بنفس طريقة الكتلة الافتراضية التي يولدها NPM، فلا تحتاج إلى كتابةproxy_passبنفسك. - اكتب المسار الذي يرسل إليه نموذج الدخول فعلاً، وتعرفه من تبويب Network في أدوات المطور، فهو
/user/loginفي Gitea مثلاً و/api/v4/users/loginفي Mattermost.
والآن نرسل ثمانية طلبات متتالية ونطبع رمز كل منها:
for i in $(seq 1 8); do curl -s -o /dev/null -w '%{http_code} ' -H 'Host: app.example.com' http://127.0.0.1/login; done; echoوالمخرج سوف يكون كما يلي:
200 200 200 200 200 200 429 429مرت ستة طلبات، أي الطلب الأول ثم الخمسة التي يسمح بها burst، وبعدها رفض NPM كل طلب بالرمز 429 إلى أن يمر وقت كاف، ويسمح بطلب جديد كل 12 ثانية.
في Traefik
يوفر Traefik ال Middleware من نوع rateLimit، ونضيفه إلى نفس الملف security.yml ثم نربطه ب Router خاص بمسار الدخول:
http:
middlewares:
login-limit:
rateLimit:
average: 5
period: 1m
burst: 5 labels:
- "traefik.http.routers.app-login.rule=Host(`app.example.com`) && Path(`/login`)"
- "traefik.http.routers.app-login.entrypoints=websecure"
- "traefik.http.routers.app-login.middlewares=security-headers@file,login-limit@file"والقاعدة Host(...) && Path(...) أطول من قاعدة ال Router الرئيسي للتطبيق، وTraefik يعطي القاعدة الأطول أولوية أعلى افتراضياً، فيمر طلب /login عبر هذا ال Router وحده. وعند إرسال ثمانية طلبات كما في المثال السابق سوف يكون المخرج كما يلي:
200 200 200 200 200 429 429 429وإذا كان أمام Traefik جهاز Proxy آخر، فأضف إلى ال Middleware الخيار sourceCriterion.ipStrategy.depth حتى يقرأ العنوان الصحيح من X-Forwarded-For، ولاحظ أن العداد يحفظ في ذاكرة كل نسخة من Traefik وحدها، فإذا شغلت أكثر من نسخة فيمكنك ربطها ب Redis كما يشرح التوثيق.
في Caddy
هنا يختلف الأمر، فلا يوجد حد لمعدل الطلبات في نسخة Caddy القياسية التي في ال Image الرسمي، وإنما في وحدة إضافية Plugin هي caddy-ratelimit، وهي من كاتب Caddy الأصلي ولكنها ليست جزءاً من المشروع الرسمي كما ينبه مستودعها. ولإضافتها تبني Image خاصاً بك عبر الأداة xcaddy كما يشرح توثيق ال Image الرسمي، والملف /opt/caddy/Dockerfile سوف يكون كما يلي:
FROM caddy:2.11.4-builder AS builder
RUN xcaddy build --with github.com/mholt/caddy-ratelimit
FROM caddy:2.11.4
COPY --from=builder /usr/bin/caddy /usr/bin/caddyبعد ذلك غير في compose.yaml السطر image: caddy:2.11.4 إلى build: . مع image: caddy-custom:2.11.4، ثم ابن وشغل بالأمر docker compose up -d --build، وتأكد أن الوحدة موجودة:
docker compose exec caddy caddy list-modules | grep ratehttp.handlers.rate_limitالآن أضف إلى موقع التطبيق في ال Caddyfile الكتلة التالية:
rate_limit {
zone login {
match {
path /login
}
key {remote_host}
events 5
window 1m
}
}والمعنى خمسة طلبات لكل عنوان خلال دقيقة على المسار /login، وعند تجاوزها يرد Caddy بالرمز 429 مع الترويسة Retry-After التي تخبر العميل بعدد الثواني المتبقية. وإذا أرسلت ثمانية طلبات ثم طلباً تاسعاً تعرض استجابته كاملة:
curl -s -i --resolve app.example.com:443:127.0.0.1 https://app.example.com/loginوالمخرج سوف يكون كما يلي:
HTTP/1.1 429 Too Many Requests
Content-Security-Policy-Report-Only: default-src 'self'; frame-ancestors 'none'
Content-Type: text/plain; charset=utf-8
Permissions-Policy: camera=(), microphone=(), geolocation=()
Referrer-Policy: strict-origin-when-cross-origin
Retry-After: 28
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
...
429 Too Many Requestsولاحظ أن ترويسات الأمان موجودة في استجابة الخطأ، وهذا بفضل الكتلة handle_errors التي أضفناها في قسم الترويسات، وبدونها سوف ترى استجابة فارغة فيها Server: Caddy فقط. وتذكر أن ال Image الخاص يعني أنك مسؤول عن إعادة بنائه مع كل إصدار جديد من Caddy، فإذا لم ترغب في ذلك فاترك الحد لطبقة أمام Caddy مثل Cloudflare، أو للتطبيق نفسه إن كان يدعمه.
وفي HAProxy
يعتمد HAProxy على جداول Stick Tables التي تحفظ عدادات لكل عنوان، كما يشرح توثيق HAProxy عن ضبط حركة المرور، والسطور التالية داخل ال frontend تعطي نفس النتيجة، أي خمسة طلبات تمر ثم 429:
acl login_path path /login
stick-table type ip size 100k expire 1m store http_req_rate(1m)
http-request track-sc0 src if login_path
http-request deny deny_status 429 if login_path { sc_http_req_rate(0) gt 5 }وقد يتساءل البعض: إذا كان المهاجم يملك آلاف العناوين، كما في شبكات البوتات Botnets، فما فائدة حد لكل عنوان؟ والإجابة أن الحد يوقف الهجوم الرخيص من سيرفر واحد، وهو الأكثر شيوعاً، ويرفع كلفة الهجوم الموزع كثيراً، أما الحماية من الهجوم الموزع على كلمات المرور فهي التحقق بخطوتين Two-Factor Authentication، وسوف نعود إليها في قسم Forward Auth.
قصر لوحات الإدارة على عناوين محددة
لوحة الإدارة Admin Panel وأدوات المراقبة لا يحتاجها إلا فريقك، فلماذا يراها كل من في الإنترنت؟ والحل أن تسمح لعناوين أو شبكات محددة فقط، كشبكة المكتب أو شبكة ال VPN، وأي عنوان آخر يرد عليه ال Reverse Proxy بالرمز 403 Forbidden قبل أن يرى صفحة الدخول أصلاً، وبالتالي لا يستطيع تخمين كلمة المرور، ولا استغلال ثغرة في صفحة الدخول نفسها.
في Nginx Proxy Manager تستخدم قائمة الوصول Access List من تبويب Rules كما في دليل NPM، وهي تطبق على ال Proxy Host كله. وإذا أردت أن تحمي مساراً واحداً مثل /admin وتترك باقي التطبيق للعموم، فأضف في تبويب Advanced الكتلة التالية التي تستخدم وحدة access في Nginx:
location /admin {
allow 10.8.0.0/24;
allow 192.168.1.0/24;
deny all;
include conf.d/include/proxy.conf;
}location / في تبويب Advanced، فإن قائمة الوصول المختارة في تبويب Details لا تطبق إطلاقاً كما شرحنا في دليل حماية أي تطبيق باستخدام authentik Forward Auth، وهناك تجد طريقة Custom Locations التي تجمع بين الاثنين. لذلك اختبر دائماً من عنوان غير مسموح بعد أي تعديل، ولا تعتمد على ما تعرضه الواجهة.في Traefik تستخدم ال Middleware من نوع ipAllowList على Router خاص بالمسار:
http:
middlewares:
vpn-only:
ipAllowList:
sourceRange:
- "10.8.0.0/24"
- "192.168.1.0/24" labels:
- "traefik.http.routers.app-admin.rule=Host(`app.example.com`) && PathPrefix(`/admin`)"
- "traefik.http.routers.app-admin.entrypoints=websecure"
- "traefik.http.routers.app-admin.middlewares=security-headers@file,vpn-only@file"في Caddy تستخدم المطابق remote_ip مع not، فتطابق كل طلب إلى /admin من خارج الشبكات المسموحة وترد عليه بالرمز 403:
@admin {
path /admin /admin/*
not remote_ip 10.8.0.0/24 192.168.1.0/24
}
respond @admin 403وفي HAProxy يكفي سطران: acl from_vpn src 10.8.0.0/24 192.168.1.0/24 ثم http-request deny deny_status 403 if { path_beg /admin } !from_vpn. والآن اختبر من عنوان خارج هذه الشبكات:
curl -s -i -H 'Host: app.example.com' http://127.0.0.1/admin | head -n 1HTTP/1.1 403 Forbiddenوالنتيجة واحدة في الأدوات الأربع. وتذكر أن قائمة العناوين لا تعني شيئاً إذا كان ال Reverse Proxy يرى عنواناً خاطئاً، فخلف Cloudflare يرى عناوين Cloudflare، وإذا وثق بترويسة يرسلها العميل فيستطيع أي شخص أن يكتب فيها عنواناً مسموحاً، لذلك راجع قسم انتحال العنوان في دليل تطبيقك خلف Reverse Proxy قبل أن تعتمد على هذه القوائم.
حظر الدول أم شبكة VPN؟
الخيار الذي يطلبه كثيرون هو حظر الدول Geo-blocking، أي أن تسمح لعناوين بلدك فقط، ويدعمه NPM عبر وحدة geoip2 مع قاعدة بيانات عناوين مثل GeoLite2 من MaxMind، كما يدعمه Cloudflare في القواعد المخصصة Custom Rules. وهو مفيد لتقليل الضجيج في السجلات، ولكنه حماية ضعيفة، والسبب أن المهاجم يستطيع استئجار سيرفر أو VPN في بلدك خلال دقائق، وأن موظفك المسافر سوف يحظر.
والحل الأنسب للوحات الإدارة هو ألا تكون على الإنترنت أصلاً، وإنما خلف شبكة خاصة VPN كما في دليل شبكة خاصة مع WireGuard وwg-easy، ثم تسمح في القائمة لشبكة ال VPN وحدها مثل 10.8.0.0/24 في الأمثلة أعلاه. وبهذا الشكل فإن من لا يملك مفتاح WireGuard لا يرى اللوحة، ومن يملكه يدخل من أي مكان في العالم.
الحظر التلقائي: fail2ban وCrowdSec
الحد من معدل الطلبات يبطئ المهاجم ولا يوقفه، فبعد كل دقيقة يحصل على خمس محاولات جديدة، وطوال هذا الوقت يستطيع أن يفحص مسارات أخرى. والطبقة التالية هي أن تقرأ السجلات Logs، فإذا تكرر من عنوان معين سلوك مريب كالرفض المتكرر بالرموز 401 و403 و429، حظرته على مستوى جدار الحماية Firewall لمدة معينة، فلا يصل إلى أي خدمة على السيرفر.
fail2ban على سجلات ال Reverse Proxy
في دليل تأمين خادم VPS من أول دخول ثبتنا fail2ban لحماية SSH، والآن سوف نوسعه ليقرأ سجلات NPM. يكتب NPM سجل كل مضيف في /data/logs/proxy-host-N_access.log، والسطر فيه بالشكل التالي:
[04/Oct/2026:08:14:36 +0000] - - 429 - HEAD http app.example.com "/login" [Client 192.168.96.1] [Length 0] [Gzip -] [Sent-to whoami] "curl/8.12.1" "-"لذلك نكتب مرشحاً Filter يطابق الأسطر التي رمزها 401 أو 403 أو 429 ويستخرج العنوان من [Client ...]، والملف /etc/fail2ban/filter.d/npm-abuse.conf سوف يكون كما يلي:
[Definition]
failregex = ^\S* \S+ \S+ (?:401|403|429) - \S+ \S+ \S+ "[^"]*" \[Client <HOST>\]
ignoreregex =ولاحظ أن التعبير يبدأ ب ^\S* وليس بنص التاريخ، والسبب أن fail2ban يحذف التاريخ من السطر قبل أن يطبق المرشح، فإذا كتبت نمط التاريخ بنفسك فلن يطابق أي سطر. وقبل أن تفعل المرشح اختبره على السجل الحقيقي بالأداة fail2ban-regex، ويحفظ NPM سجلاته في ال Volume npm_data:
sudo fail2ban-regex /var/lib/docker/volumes/npm_data/_data/logs/proxy-host-1_access.log /etc/fail2ban/filter.d/npm-abuse.confوالمخرج سوف يكون كما يلي:
Results
=======
Failregex: 10 total
|- #) [# of hits] regular expression
| 1) [10] ^\S* \S+ \S+ (?:401|403|429) - \S+ \S+ \S+ "[^"]*" \[Client <HOST>\]
`-
...
Lines: 28 lines, 0 ignored, 10 matched, 18 missedومن الأسطر التي لم تطابق تتأكد أن طلبات 200 العادية لا تحسب على أحد. الآن أضف السجن Jail في /etc/fail2ban/jail.d/npm.local:
[npm-abuse]
enabled = true
filter = npm-abuse
logpath = /var/lib/docker/volumes/npm_data/_data/logs/proxy-host-*_access.log
backend = auto
port = http,https
banaction = iptables-multiport
chain = DOCKER-USER
maxretry = 10
findtime = 10m
bantime = 1hsudo fail2ban-client -t
sudo systemctl reload fail2ban
sudo fail2ban-client status npm-abuseفي الإعداد أعلاه لاحظ التالي:
- السطر
chain = DOCKER-USERهو أهم سطر هنا، والسبب أن الطلبات إلى Container ينشر منفذه لا تمر بسلسلةINPUTالتي يضع فيها fail2ban قواعده افتراضياً، وإنما بسلسلةFORWARD، وتوثيق Docker يحدد السلسلةDOCKER-USERلقواعدك، فبدون هذا السطر سوف يظهر العنوان محظوراً فيfail2ban-client statusويستمر في الوصول إلى NPM. - السطر
backend = autoيجعل هذا السجن يقرأ الملفات، والسبب أن إعداد دليل تأمين VPS جعلbackend = systemdافتراضياً لكل السجون، وسجلات NPM ليست في journald. - عشر محاولات مرفوضة خلال عشر دقائق تعني حظراً لساعة، وارفع
maxretryإذا كانت لديك قوائم وصول يصطدم بها موظفوك كثيراً.
CrowdSec: الحظر الجماعي
يعمل CrowdSec بنفس فكرة fail2ban، أي قراءة السجلات واكتشاف السلوك المريب، ولكنه يقسم العمل إلى جزأين: محرك يقرأ السجلات ويتخذ القرارات ويحفظها في واجهة محلية Local API، ومكونات الحظر Remediation Components التي يسميها المشروع Bouncers، وهي التي تنفذ القرار في جدار الحماية أو داخل ال Reverse Proxy نفسه. والفرق الأهم أن المستخدمين يشاركون العناوين المحظورة، فتحصل على قائمة بعناوين هاجمت غيرك قبل أن تصل إليك.
ولكل أداة مجموعة جاهزة Collection تعرف صيغة سجلاتها والسيناريوهات Scenarios التي تكتشفها، مثل الفحص عن المسارات وتخمين كلمات المرور، فلسجلات NPM تثبت المجموعة crowdsecurity/nginx-proxy-manager وتخبر CrowdSec بمكان السجلات بالنوع nginx-proxy-manager، ولTraefik وCaddy مجموعتان مماثلتان هما crowdsecurity/traefik وcrowdsecurity/caddy. أما مكون الحظر فيختلف بحسب الأداة، وهذا ما يجب أن تنتبه له:
- Nginx Proxy Manager: لا يوجد مكون حظر داخل ال Image الرسمي، لذلك الحل الأبسط هو مكون جدار الحماية على السيرفر نفسه، بالحزمة
crowdsec-firewall-bouncer-iptables، مع إضافةDOCKER-USERإلىiptables_chainsفي إعداده للسبب نفسه الذي شرحناه مع fail2ban. وهناك نسخة معدلة Fork من NPM اسمها NPMplus تدمج CrowdSec داخلها، ولكنها مشروع مستقل له جدول إصداراته. - Traefik: المكون المعتمد في توثيق CrowdSec هو الإضافة crowdsec-bouncer-traefik-plugin، وهي من تطوير المجتمع، وتعمل ك Middleware ترد على العنوان المحظور بالرمز
403. - Caddy: الوحدة caddy-crowdsec-bouncer، وتحتاج إلى بناء Image خاص بالأداة xcaddy كما فعلنا مع caddy-ratelimit.
- HAProxy: مكون HAProxy SPOA الذي يتصل ب HAProxy عبر بروتوكول SPOE.
| fail2ban | CrowdSec | |
|---|---|---|
| نقاط القوة | خفيف، وموجود في مستودعات كل التوزيعات، وتكتب المرشح بنفسك فتعرف بالضبط ما يحظره | سيناريوهات جاهزة لعشرات التطبيقات، وقائمة عناوين مشتركة، ومكونات حظر داخل ال Reverse Proxy |
| نقاط الضعف | كل مرشح تكتبه وتختبره بنفسك، ولا يعرف شيئاً عن عنوان هاجم غيرك | أجزاء أكثر تثبتها وتحدثها، ويحتاج حساباً في لوحة CrowdSec للاستفادة الكاملة من القوائم |
والاختيار بينهما يعتمد على حجم ما تديره، فسيرفر واحد بثلاثة تطبيقات يكفيه fail2ban بمرشح واحد، أما إذا كانت لديك عدة سيرفرات أو تطبيقات مكشوفة للعموم فإن CrowdSec يوفر عليك كتابة المرشحات ومتابعتها.
جدار حماية تطبيقات الويب WAF
الطبقات السابقة تنظر إلى من يرسل الطلب وكم مرة أرسله، أما جدار حماية تطبيقات الويب Web Application Firewall واختصاراً WAF فينظر إلى محتوى الطلب نفسه، فيقرأ الرابط والترويسات وجسم الطلب Body ويبحث فيها عن أنماط الهجوم المعروفة، مثل حقن SQL واختصاراً SQL Injection، وحقن السكربتات XSS، والوصول إلى ملفات النظام بمسارات مثل ../../etc/passwd، وحقن الأوامر Command Injection. والمحرك الأشهر هو ModSecurity الذي انتقل إلى OWASP بعد أن أنهت Trustwave رعايته في يوليو 2024، وبديله المكتوب بلغة Go هو Coraza، والمحركان لا يعرفان الهجمات بأنفسهما، وإنما يقرآن قواعد Rules، والمجموعة المعتمدة منها هي OWASP Core Rule Set واختصاراً CRS.
وتعمل CRS بنظام النقاط Anomaly Scoring، فكل قاعدة تطابق تضيف نقاطاً إلى الطلب، وإذا تجاوز المجموع الحد، وهو 5 افتراضياً، يرفض الطلب بالرمز 403. ومستوى التشدد Paranoia Level من 1 إلى 4، والمستوى 1 هو الافتراضي وأقلها إنذارات خاطئة، وتجد تفاصيل كل مستوى في توثيق CRS.
ModSecurity مع CRS أمام تطبيقك
أسهل طريقة لتجربة WAF هي ال Image الرسمي owasp/modsecurity-crs، وهو Nginx فيه ModSecurity وCRS جاهزين، فتضعه بين ال Reverse Proxy والتطبيق على نفس شبكة proxy، وتوجه ال Proxy Host إليه بدلاً من التطبيق:
services:
waf:
image: owasp/modsecurity-crs:4.29.0-nginx-alpine-202609301109
container_name: waf
restart: unless-stopped
environment:
BACKEND: http://whoami:80
BLOCKING_PARANOIA: 1
ANOMALY_INBOUND: 5
ANOMALY_OUTBOUND: 4
volumes:
- ./REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf:/etc/modsecurity.d/owasp-crs/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf:ro
networks:
- proxy
networks:
proxy:
external: trueفي الإعداد أعلاه لاحظ التالي:
BACKENDهو التطبيق الذي يحميه WAF، وفي NPM يصبح Forward Hostname هوwafوالمنفذ8080، وهو المنفذ الذي يستمع عليه ال Image لأنه يعمل بمستخدم دون صلاحيات root.- الملف
REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.confهو مكان الاستثناءات التي سوف نحتاجها بعد قليل، فأنشئه فارغاً الآن بالأمرtouch. - إذا أردت أن تبدأ بالمراقبة فقط دون منع، فأضف
MODSEC_RULE_ENGINE: DetectionOnly، فيسجل WAF ما كان سوف يمنعه ولا يمنع شيئاً، وهذه أفضل بداية مع أي تطبيق حقيقي.
والآن نرسل خمسة طلبات: طلباً عادياً، ثم محاولة SQL Injection، ثم XSS، ثم محاولة قراءة ملف من النظام، ثم طلباً بواجهة أداة الفحص sqlmap:
curl -s -o /dev/null -w '%{http_code}\n' "https://app.example.com/"
curl -s -o /dev/null -w '%{http_code}\n' "https://app.example.com/products?id=1%27%20OR%20%271%27=%271"
curl -s -o /dev/null -w '%{http_code}\n' "https://app.example.com/search?q=%3Cscript%3Ealert(1)%3C/script%3E"
curl -s -o /dev/null -w '%{http_code}\n' "https://app.example.com/download?file=../../../../etc/passwd"
curl -s -o /dev/null -w '%{http_code}\n' -A 'sqlmap/1.8' "https://app.example.com/"والمخرج سوف يكون كما يلي:
200
403
403
403
403وفي سجل ال Container تجد سبب كل رفض، فمحاولة SQL Injection مثلاً طابقت القاعدة 942100 بالرسالة SQL Injection Attack Detected via libinjection، ثم رفضتها القاعدة 949110 بالرسالة Inbound Anomaly Score Exceeded لأن مجموع النقاط تجاوز الحد. ولاحظ أن الطلب لم يصل إلى التطبيق أصلاً، فحتى لو كان في التطبيق ثغرة SQL Injection فعلية فلن يصلها هذا الطلب.
الإنذارات الخاطئة False Positives: الوجه الآخر للـ WAF
لنأخذ مثالاً: لديك قاعدة معرفة Wiki داخلية يكتب فيها فريقك شروحات تقنية، فيحفظ أحدهم صفحة فيها السطر التالي:
curl -s -o /dev/null -w '%{http_code}\n' -X POST "https://app.example.com/wiki/save" --data-urlencode 'content=Run this on the server: cat /etc/passwd | grep deploy'403رفض WAF طلباً مشروعاً تماماً، والسبب أن النص يحتوي ما يحتويه الهجوم بالضبط، فطابق القواعد 930120 (OS File Access Attempt) و932160 و932235 و932260 الخاصة بحقن الأوامر، وكذلك الصفحة التي تشرح وسم <script> سوف ترفض. وهذا هو الإنذار الخاطئ False Positive، وهو ما يجعل كثيرين يعطلون WAF بعد أسبوع من تفعيله. والحل ليس رفع الحد ولا تعطيل القواعد، وإنما استثناء محدد: هذا الحقل، في هذا المسار، من هذه الفئة من القواعد، كما يشرح توثيق CRS عن ضبط الإنذارات الخاطئة. والملف REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf سوف يكون كما يلي:
SecRule REQUEST_URI "@beginsWith /wiki/save" \
"id:1000,phase:1,pass,nolog,\
ctl:ruleRemoveTargetByTag=attack-rce;ARGS:content,\
ctl:ruleRemoveTargetByTag=attack-lfi;ARGS:content,\
ctl:ruleRemoveTargetByTag=attack-xss;ARGS:content"أعد تشغيل ال Container بالأمر docker compose restart waf، ثم أرسل نفس النص مرة إلى صفحة الويكي ومرة إلى مسار آخر:
200
403في الإعداد أعلاه لاحظ التالي:
- الاستثناء يزيل الحقل
contentوحده من قواعد ثلاث فئات Tags، هي حقن الأوامرattack-rceوقراءة الملفاتattack-lfiوXSS، وفي المسار/wiki/saveوحده، أما باقي الحقول وباقي القواعد وباقي المسارات فما زالت محمية، ولهذا رفض المسار الآخر نفس النص. - رقم القاعدة
id:1000يجب أن يكون فريداً، والمجال من 1 إلى 99999 مخصص للقواعد المحلية في CRS. - لا تبدأ بالاستثناءات الواسعة مثل تعطيل قاعدة كاملة بالأمر
SecRuleRemoveById، والسبب أنك تزيل الحماية عن التطبيق كله لتصلح صفحة واحدة.
وكلفة WAF ليست في الإنذارات وحدها، فكل طلب يمر على مئات التعبيرات النمطية Regular Expressions، وفحص جسم الطلبات الكبيرة يستهلك المعالج CPU والذاكرة، وكلما رفعت مستوى التشدد زادت الكلفة والإنذارات معاً. لذلك ضعه أمام التطبيقات المكشوفة للعموم، كالموقع ونماذج الاتصال وواجهات API العامة، أما الأدوات الداخلية خلف VPN فلا تحتاجه في الغالب.
وماذا عن NPM وTraefik وCaddy؟
- Nginx Proxy Manager: لا يحتوي ال Image الرسمي على ModSecurity، لذلك الطريقة الأنسب هي Container ال WAF أعلاه بين NPM والتطبيق، أو مكون AppSec في CrowdSec الذي يطبق قواعد CRS عبر NPMplus.
- Caddy: الوحدة coraza-caddy من مشروع Coraza نفسه، وتحتاج إلى بناء Image خاص بالأمر
xcaddy build --with github.com/corazawaf/coraza-caddy/v2، وفيها قواعد CRS مدمجة تفعلها بالخيارload_owasp_crs، مع السطرorder coraza_waf firstفي الإعدادات العامة. - Traefik: الإضافة coraza-http-wasm-traefik تعمل عبر WebAssembly، ويصفها مستودعها بأنها ما زالت في مرحلة مبكرة، أما ال Middleware الأسرع فمتاح في Traefik Hub التجاري فقط، لذلك فال Container المستقل أعلاه هو الخيار العملي مع Traefik أيضاً.
أم تترك المهمة لـ Cloudflare؟
إذا كان نطاقك على Cloudflare مع تفعيل ال Proxy، فالخطة المجانية فيها مجموعة القواعد المجانية Cloudflare Free Managed Ruleset التي تغطي الثغرات الأوسع انتشاراً، وخمس قواعد مخصصة Custom Rules تستطيع أن تحظر بها دولة أو مساراً، وقاعدة واحدة للحد من معدل الطلبات بفترة عد ثابتة 10 ثوان. أما مجموعة Cloudflare OWASP Core Ruleset فهي متاحة من خطة Pro فما فوق.
ونقطة القوة هنا أن الهجوم يتوقف قبل أن يصل إلى سيرفرك ويستهلك موارده، ولا توجد عندك قواعد تحدثها، أما نقطة الضعف فهي أن هذه الحماية كلها تسقط إذا وصل المهاجم إلى العنوان الحقيقي للسيرفر مباشرة، لذلك إذا اعتمدت على Cloudflare فاقصر المنفذين 80 و443 على عناوين Cloudflare وحدها في جدار الحماية، أو استخدم Cloudflare Tunnel كما في دليل تشغيل خادمك من إنترنت المنزل. والطبقتان لا تتعارضان، فقواعد Cloudflare العامة في الخارج، والاستثناءات الدقيقة الخاصة بتطبيقك في الداخل.
لا تعط المهاجم معلومات مجانية
أول ما تفعله أداة الفحص هو جمع المعلومات: ما اسم السيرفر وإصداره، وهل يوجد ملف .env أو مجلد .git في المسار العام، وماذا تقول رسالة الخطأ. وكل معلومة من هذه توفر على المهاجم وقتاً، فإذا عرف أنك تستخدم إصداراً معيناً بحث عن ثغراته مباشرة.
اسم السيرفر وإصداره: يرسل NPM الترويسة Server: openresty دون رقم الإصدار، لأن server_tokens off مفعل في إعداده، وحذفناها كاملة بالسطر more_clear_headers Server. وCaddy يرسل Server: Caddy وVia وحذفناهما في المقتطف، أما Traefik فلا يضيف ترويسة Server من عنده، ولكن التطبيق نفسه قد يرسل ترويسات مثل X-Powered-By: Express أو X-Powered-By: PHP/8.4، وتحذفها بنفس الطريقة، أي more_clear_headers X-Powered-By في NPM، و-X-Powered-By في Caddy، والقيمة الفارغة في customResponseHeaders في Traefik.
الملفات الحساسة: ملف .env فيه كلمات مرور قاعدة البيانات ومفاتيح ال API، ومجلد .git فيه تاريخ الكود كله، وكلاهما لا يجب أن يكون في المجلد العام أصلاً، ولكن إذا وصل أحدهما إليه بالخطأ فهذه القواعد تمنعه، وتمنع أي مسار يبدأ بنقطة باستثناء /.well-known/ الذي تحتاجه شهادات Let's Encrypt:
# Nginx Proxy Manager: تبويب Advanced
location ~ /\.(?!well-known) {
return 404;
}# Traefik: Router بأولوية أعلى، وMiddleware لا يسمح لأي عنوان من الخارج
http:
routers:
app-hidden:
rule: 'Host(`app.example.com`) && PathRegexp(`^/\.(env|git|htaccess)`)'
entryPoints: [websecure]
middlewares: [deny-all]
service: whoami
middlewares:
deny-all:
ipAllowList:
sourceRange:
- "127.0.0.1/32"# Caddy
@hidden path /.env /.env.* /.git /.git/* /.htaccess /.DS_Store
respond @hidden 404وفي HAProxy: http-request deny deny_status 404 if { path_reg ^/\.(env|git|htaccess) }. والآن اختبر:
curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: app.example.com' http://127.0.0.1/.env
curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: app.example.com' http://127.0.0.1/.git/configوالمخرج في NPM وCaddy وHAProxy سوف يكون كما يلي:
404
404أما Traefik فيرد بالرمز 403، والسبب أنه لا يملك في نسخته المفتوحة Middleware يرد بصفحة ثابتة، فنستخدم ipAllowList بعنوان لا يأتي منه أي طلب خارجي. ولاحظ أننا نفضل 404 حيث أمكن، لأن 403 يخبر الفاحص أن الملف موجود وممنوع، أما 404 فلا يخبره بشيء.
وقد يتساءل البعض: أليس خيار Block Common Exploits في NPM كافياً لهذا؟ والإجابة لا، فهذا الخيار قائمة قصيرة من الأنماط في الروابط وسلاسل الاستعلام Query Strings، ولا يمنع /.env، وطلب مثل /?id=1%20union%20select%201 يمر منه ويصل إلى التطبيق، لذلك أبقه مفعلاً، ولكن لا تعتبره WAF.
صفحات الأخطاء: رسالة الخطأ التفصيلية Stack Trace تكشف مسارات الملفات وأسماء المكتبات وإصداراتها وأحياناً أجزاء من الاستعلامات، لذلك تأكد أن وضع التطوير Debug Mode معطل في كل تطبيق إنتاج، مثل APP_DEBUG=false في Laravel وDEBUG = False في Django. وفي NPM افتح Settings ثم Default Site، وهو ما يرد به NPM على أي طلب باسم نطاق غير معرف، كمن يفحص عنوان IP السيرفر مباشرة، واختر 404 Page أو No Response (444) بدلاً من صفحة الترحيب الافتراضية التي تخبر الفاحص باسم الأداة.
أين يقع Forward Auth بين هذه الطبقات؟
كل الطبقات السابقة لا تعرف من هو المستخدم، فهي تحكم على الطلب من عنوانه ومعدله ومحتواه، أما Forward Auth فيضيف الطبقة التي تعرفه، فلا يصل أي طلب إلى التطبيق قبل أن يسجل المستخدم دخوله في authentik بحسابه الشخصي مع التحقق بخطوتين، وعندها لا يرى المهاجم إلا صفحة دخول authentik، وهي صفحة واحدة تحدثها وتراقبها، بدلاً من عشر صفحات دخول في عشرة تطبيقات. وتجد الإعداد كاملاً في دليل حماية أي تطبيق باستخدام authentik Forward Auth.
ولكن Forward Auth لا يلغي الطبقات الأخرى، وإنما يقف بينها، فالترتيب الذي نوصي به للأداة الداخلية هو: VPN أو قائمة عناوين، ثم Forward Auth، ثم التطبيق. أما التطبيق المكشوف للعموم الذي لا يمكن وضعه خلف تسجيل دخول، كموقع الشركة أو نموذج الاتصال، فطبقاته هي ترويسات الأمان والحد من معدل الطلبات وWAF والحظر التلقائي. ولاحظ أيضاً أن صفحة دخول authentik نفسها تحتاج إلى حد لمعدل الطلبات، فهي الآن الباب الذي يطرقه الجميع.
قائمة تحقق لكل تطبيق جديد
قبل أن تشير بنطاق أي تطبيق جديد إلى السيرفر، مر على هذه القائمة:
- التطبيق لا ينشر أي منفذ على السيرفر، ويصل إليه ال Reverse Proxy عبر شبكة Docker فقط.
- هل يحتاجه أحد من خارج الفريق؟ إذا كانت الإجابة لا، فضعه خلف VPN أو قائمة عناوين، ثم Forward Auth.
- HTTPS مع Force SSL، ثم HSTS بعد أن تتأكد أن الشهادة تتجدد، ودون
preloadإلا بقرار مدروس. - ترويسات الأمان:
X-Content-Type-OptionsوX-Frame-OptionsوReferrer-PolicyوPermissions-Policy، وCSP بصيغة Report-Only إذا لم يرسلها التطبيق نفسه، ثم اختبرها بالأمرcurl -I. - حد لمعدل الطلبات على مسار الدخول ومسار إعادة تعيين كلمة المرور، واختبره حتى ترى
429. - مسار
/adminأو لوحة الإدارة مقصورة على شبكة ال VPN، واختبرها من عنوان آخر حتى ترى403. - الطلبات إلى
/.envو/.git/configترد ب404، والترويستانServerوX-Powered-Byمحذوفتان. - وضع التطوير Debug Mode معطل، والتطبيق يرى عنوان الزائر الحقيقي.
- سجل المضيف يقرؤه fail2ban أو CrowdSec، والحظر يطبق على السلسلة
DOCKER-USER. - إذا كان التطبيق للعموم، فضع أمامه WAF بوضع DetectionOnly لأسبوع، وأضف الاستثناءات اللازمة، ثم فعل المنع.
الخلاصة
وصلنا لنهاية الموضوع، وأهم ما فيه:
- كلمة المرور تحمي بوابة واحدة، والحماية الحقيقية طبقات يضعها ال Reverse Proxy أمام كل تطبيق، فإذا سقطت طبقة بقيت الأخرى.
- في NPM لا تستخدم
add_headerفي تبويب Advanced، والسبب أن كلlocationفيهadd_headerخاص به يلغي ترويساتك، واستخدمmore_set_headersبدلاً منه، وفي Caddy أضفhandle_errorsحتى تحمل الأخطاء نفس الترويسات. - خيار HSTS في NPM يرسل
preloadدائماً، فلا تجمعه مع HSTS Subdomains على النطاق الرئيسي قبل أن تراجع كل نطاقاتك الفرعية. - ضع الحد من معدل الطلبات على مسار الدخول وحده، وتأكد قبله أن ال Reverse Proxy يرى عنوان الزائر الحقيقي، وتذكر أن Caddy يحتاج إلى وحدة إضافية لذلك.
- لوحات الإدارة مكانها خلف VPN، وحظر الدول يقلل الضجيج ولا يحمي.
- الحظر التلقائي على سجلات ال Reverse Proxy يجب أن يكون في السلسلة
DOCKER-USER، وإلا ظهر العنوان محظوراً وهو ما زال يصل. - WAF مع CRS يوقف الهجمات المعروفة، ولكن ابدأ بوضع DetectionOnly وعالج الإنذارات الخاطئة باستثناءات ضيقة على الحقل والمسار.
سجل التحديثات
- أكتوبر 2026: كتابة الدليل واختبار الإعدادات على Nginx Proxy Manager 2.16.0 وTraefik 3.7.13 وCaddy 2.11.4 مع caddy-ratelimit وHAProxy 3.4.6 وOWASP CRS 4.29.0.