> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# حماية تطبيقات الويب على مستوى HTTP: طبقات أمان أمام كل تطبيق في الـ Reverse Proxy
- URL: https://arabroot.io/articles/حماية-تطبيقات-الويب-على-مستوى-http/
- Published: 2026-09-30T15:25:00.000Z
- Updated: 2026-10-05T10:18:24.000Z
- Description: كلمة المرور وحدها لا تحمي تطبيقاً مكشوفاً على الإنترنت، لذلك نبني في هذا الدليل أمام كل تطبيق طبقات حماية في الـ Reverse Proxy نفسه، بإعدادات مختبرة على Nginx Proxy Manager وTraefik وCaddy.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, الشبكات والوصول, الأمن والدخول الموحد, الاستضافة الذاتية

لنفرض أن لديك تطبيقاً على السيرفر خلف ال 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](https://arabroot.io/articles/nginx-proxy-manager-%D9%86%D8%B7%D8%A7%D9%82%D8%A7%D8%AA-%D9%88%D8%B4%D9%87%D8%A7%D8%AF%D8%A7%D8%AA-tls/) و[Traefik](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-traefik-reverse-proxy/) و[Caddy](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-caddy-reverse-proxy/)، مع أخطاء شائعة تجعلها تختفي دون أن تنتبه.
- الحد من معدل الطلبات 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](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-haproxy-load-balancer/) حيث يلزم، وإذا لم تختر أداتك بعد فابدأ بمقال [أي Reverse Proxy يناسبك؟](https://arabroot.io/articles/%D9%85%D9%82%D8%A7%D8%B1%D9%86%D8%A9-nginx-proxy-manager-%D9%88-traefik-%D9%88-haproxy/).

## لماذا لا تكفي كلمة المرور وحدها؟

الحل الأول الذي يخطر على البال لحماية أي تطبيق هو كلمة مرور قوية، وهذا صحيح كخطوة أولى، ولكنه يفشل في ثلاث حالات على الأقل. الأولى أن صفحة الدخول نفسها تستقبل عدداً غير محدود من المحاولات، فإذا كان أحد المستخدمين يستخدم كلمة مرور ضعيفة أو مسربة من موقع آخر فسوف يجدها برنامج التخمين Brute Force عاجلاً أو آجلاً. والثانية أن التطبيق فيه كود يعمل قبل صفحة الدخول، كالملفات الثابتة وواجهة ال API وصفحة إعادة تعيين كلمة المرور، وأي ثغرة في هذا الكود لا تحتاج إلى كلمة مرور أصلاً. والثالثة أن المتصفح نفسه يمكن أن يستخدم ضد المستخدم، كأن تعرض صفحتك داخل إطار في موقع آخر ليضغط المستخدم على زر لا يراه.

لذلك سوف نبني الحماية في طبقات، وكل طبقة تغطي ما لا تغطيه غيرها:

| الطبقة                                | ما تمنعه                                                           | أين نضعها                 |
| ------------------------------------- | ------------------------------------------------------------------ | ------------------------- |
| ترويسات الأمان Security Headers       | هجمات تمر عبر المتصفح، مثل عرض الصفحة في إطار أو تنفيذ سكربت محقون | كل تطبيق                  |
| الحد من معدل الطلبات Rate Limiting    | تخمين كلمات المرور والطلبات المتكررة                               | صفحات الدخول وواجهات API  |
| قوائم العناوين المسموحة IP Allow List | وصول أي شخص من الإنترنت إلى لوحات الإدارة                          | اللوحات والأدوات الداخلية |
| الحظر التلقائي Automatic Banning      | عنوان يكرر محاولات فاشلة أو يفحص المسارات                          | السيرفر كله               |
| جدار حماية تطبيقات الويب WAF          | أنماط الهجوم المعروفة مثل SQL Injection وXSS                       | التطبيقات المكشوفة للعموم |
| إخفاء المعلومات                       | كشف الإصدارات والملفات الحساسة ورسائل الأخطاء                      | كل تطبيق                  |

وقد يتساءل البعض: لماذا نضع هذه الطبقات في ال Reverse Proxy وليس في كل تطبيق؟ والإجابة أن ال Reverse Proxy هو الباب الوحيد الذي تمر منه كل الطلبات إذا كانت تطبيقاتك لا تنشر أي منفذ على السيرفر كما شرحنا في دليل [شبكات Docker وأفضل الممارسات](https://arabroot.io/articles/%D8%B4%D8%A8%D9%83%D8%A7%D8%AA-docker-%D9%88%D8%A3%D9%81%D8%B6%D9%84-%D8%A7%D9%84%D9%85%D9%85%D8%A7%D8%B1%D8%B3%D8%A7%D8%AA/)، وبالتالي تكتب الإعداد مرة واحدة في مكان واحد، وتحمي به تطبيقات لا تملك كودها ولا تستطيع تعديله. ولاحظ أن هذه الطبقات لا تغني عن تحديث التطبيقات نفسها، والسبب أن أي ثغرة في التطبيق تبقى موجودة خلفها، وكل ما تفعله الطبقات أنها تصعب الوصول إليها.

## ترويسات الأمان: تعليمات يرسلها السيرفر إلى المتصفح

ترويسة الأمان Security Header سطر يضيفه السيرفر إلى الاستجابة Response فيغير به سلوك المتصفح مع الصفحة، كأن يرفض فتحها عبر HTTP أو عرضها داخل إطار أو تشغيل سكربت لا يعرفه، فهي لا تحمي السيرفر من المهاجم مباشرة، وإنما تحمي مستخدميك من أن يستخدم المتصفح ضدهم، لذلك هي رخيصة ويجب أن تكون على كل تطبيق، ويكفيك هنا ما تحتاجه هذه الطبقة من إعداد، أما شرح كل ترويسة وقيمها ومخاطرها بالتفصيل فتجده في دليل [ترويسات الأمان بالتفصيل: HSTS وCSP](https://arabroot.io/articles/%D8%AA%D8%B1%D9%88%D9%8A%D8%B3%D8%A7%D8%AA-%D8%A7%D9%84%D8%A3%D9%85%D8%A7%D9%86-hsts-%D9%88-csp/)، وفي [دليل OWASP لترويسات HTTP](https://cheatsheetseries.owasp.org/cheatsheets/HTTP%5FHeaders%5FCheat%5FSheet.html?ref=arabroot.io).

### HSTS: لا تقبل HTTP مع هذا النطاق مرة أخرى

الترويسة [Strict-Transport-Security](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Strict-Transport-Security?ref=arabroot.io) واختصاراً HSTS تطلب من المتصفح أن يحول كل طلب لهذا النطاق إلى HTTPS بنفسه مدة `max-age` بالثواني، فلا يخرج الطلب الأول عبر HTTP حيث يمكن اعتراضه:

```http
Strict-Transport-Security: max-age=31536000; includeSubDomains
```

ولا تضف `includeSubDomains` إلا إذا كانت كل نطاقاتك الفرعية تعمل عبر HTTPS، ولا تضف `preload` إلا بقرار مدروس، والسبب أن الخروج من [القائمة المسبقة](https://hstspreload.org/?ref=arabroot.io) يستغرق شهوراً، وابدأ بمدة قصيرة ثم ارفعها على مراحل.

⚠️

خيار HSTS في تبويب SSL في Nginx Proxy Manager يرسل `max-age=63072000; preload` دائماً، ويضيف `includeSubDomains` إذا فعلت HSTS Subdomains، فيصبح النطاق مستوفياً لشروط الإدراج في القائمة المسبقة، لذلك لا تفعل الخيارين معاً على النطاق الرئيسي قبل أن تراجع كل نطاقاتك الفرعية.

### Content-Security-Policy: من أين يحمل المتصفح السكربتات؟

الترويسة [Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy?ref=arabroot.io) واختصاراً CSP قائمة بالمصادر التي يسمح للصفحة أن تحمل منها السكربتات والصور والاتصالات، فيرفض المتصفح السكربت المحقون في هجمة XSS، والسياسة التالية تسمح بنفس النطاق فقط:

```http
Content-Security-Policy: default-src 'self'; frame-ancestors 'none'
```

ولأنها مرتبطة بالتطبيق نفسه فقد تكسر جزءاً منه، لذلك نبدأ بالصيغة [Content-Security-Policy-Report-Only](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy-Report-Only?ref=arabroot.io) التي تسجل المخالفات دون أن تمنع شيئاً، ولا نضيف سياسة ثانية إذا كان التطبيق يرسل سياسته الخاصة.

### X-Frame-Options وframe-ancestors: لا تعرض صفحتي في إطار

ضد هجمة Clickjacking نضع `X-Frame-Options: DENY` مع التوجيه [frame-ancestors](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/frame-ancestors?ref=arabroot.io) داخل CSP، والسبب أن `frame-ancestors` لا تمنع شيئاً ما دامت السياسة بالصيغة Report-Only.

### ثلاث ترويسات صغيرة وواحدة لا تضفها

- [X-Content-Type-Options: nosniff](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Content-Type-Options?ref=arabroot.io): لا يخمن المتصفح نوع الملف من محتواه.
- [Referrer-Policy: strict-origin-when-cross-origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Referrer-Policy?ref=arabroot.io): لا يرسل إلى المواقع الأخرى إلا اسم النطاق.
- [Permissions-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Permissions-Policy?ref=arabroot.io): القيمة `camera=()` تمنع أي صفحة من طلب الكاميرا، فلا تضعها على تطبيقات الاجتماعات المرئية.
- [X-XSS-Protection](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-XSS-Protection?ref=arabroot.io): مهجورة، والمتصفحات الحديثة أزالت الفلتر الذي تتحكم فيه، فلا تضفها.

### إضافة الترويسات في Nginx Proxy Manager

في NPM تكتب الترويسات في تبويب Advanced الخاص بال Proxy Host، والحل الذي تجده في أغلب الشروحات هو `add_header`:

```nginx
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
```

ولكنه لا يعمل في NPM، والسبب أن [التوجيه add\_header لا يورث](https://nginx.org/en/docs/http/ngx%5Fhttp%5Fheaders%5Fmodule.html?ref=arabroot.io#add%5Fheader) إلى كتلة `location` التي فيها `add_header` خاص بها، وNPM يضع في كل `location` السطر `add_header X-Served-By $host;`، فتلغى ترويساتك بصمت. والحل التوجيه `more_set_headers` من وحدة [headers-more](https://github.com/openresty/headers-more-nginx-module?ref=arabroot.io) المدمجة في OpenResty:

```nginx
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;
```

احفظ، ثم اختبر من السيرفر نفسه:

```bash
curl -s -I -H 'Host: app.example.com' http://127.0.0.1/
```

والمخرج سوف يكون كما يلي:

```http
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](https://doc.traefik.io/traefik/reference/routing-configuration/http/middlewares/headers/?ref=arabroot.io) مرة واحدة في ملف داخل مجلد الإعداد الديناميكي Dynamic Configuration، أي `/opt/traefik/dynamic/security.yml`:

```yaml
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`، ثم نختبر:

```bash
curl -s -I -H 'Host: app.example.com' http://127.0.0.1/
```

والمخرج سوف يكون كما يلي:

```http
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](https://caddyserver.com/docs/caddyfile/directives/header?ref=arabroot.io) داخل مقتطف Snippet نستورده في كل موقع:

```nginx
(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](https://caddyserver.com/docs/caddyfile/directives/handle%5Ferrors?ref=arabroot.io) تجعل الأخطاء التي يولدها 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](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-haproxy-load-balancer/).

## الحد من معدل الطلبات Rate Limiting ضد تخمين كلمات المرور

برنامج التخمين يرسل مئات المحاولات في الدقيقة إلى صفحة الدخول، والإنسان الحقيقي لا يحتاج إلى أكثر من محاولتين أو ثلاث، وهذا الفرق هو ما يستغله الحد من معدل الطلبات، فتحدد عدد الطلبات المسموح بها لكل عنوان IP خلال مدة معينة، وما زاد عنها يرد عليه ال Reverse Proxy بالرمز `429 Too Many Requests` دون أن يصل إلى التطبيق. ولاحظ أننا نضع الحد على مسار الدخول وحده وليس على التطبيق كله، والسبب أن صفحة واحدة في تطبيق حديث قد تطلب عشرات الملفات خلال ثانية، فإذا وضعت حداً منخفضاً على كل المسارات فسوف يحظر المستخدمون الحقيقيون أنفسهم.

وقبل أن تبدأ تأكد أن ال Reverse Proxy يرى عنوان الزائر الحقيقي، فإذا كان أمامه Cloudflare أو Load Balancer ولم تضبط الثقة بالترويسات، فسوف يرى كل الطلبات قادمة من عناوين قليلة، فيحظر الجميع عندما يتجاوز أحدهم الحد، وتجد الحل لكل أداة في دليل [تطبيقك خلف Reverse Proxy](https://arabroot.io/articles/%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D9%83-%D8%AE%D9%84%D9%81-reverse-proxy/).

### في Nginx Proxy Manager

يعتمد Nginx على وحدة [limit\_req](https://nginx.org/en/docs/http/ngx%5Fhttp%5Flimit%5Freq%5Fmodule.html?ref=arabroot.io)، وهي تحتاج إلى سطرين: الأول `limit_req_zone` الذي يعرف المنطقة Zone ومعدلها ويجب أن يكون في كتلة `http`، والثاني `limit_req` الذي يطبقها على مسار معين. وتبويب Advanced يكتب داخل كتلة `server` فقط، لذلك نضع السطر الأول في ملف من [الملفات المخصصة التي يقرؤها NPM](https://nginxproxymanager.com/advanced-config/?ref=arabroot.io#custom-nginx-configurations) وهو `/data/nginx/custom/http_top.conf`، أي داخل ال Volume `npm_data`:

```nginx
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_req_status 429;
```

وبعد إنشاء الملف أعد تشغيل NPM مرة واحدة بالأمر `docker compose restart npm` حتى يقرأه، ثم أضف في تبويب Advanced الخاص بال Proxy Host، تحت سطور الترويسات، الكتلة التالية:

```nginx
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.

والآن نرسل ثمانية طلبات متتالية ونطبع رمز كل منها:

```bash
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
```

والمخرج سوف يكون كما يلي:

```bash
200 200 200 200 200 200 429 429
```

مرت ستة طلبات، أي الطلب الأول ثم الخمسة التي يسمح بها `burst`، وبعدها رفض NPM كل طلب بالرمز `429` إلى أن يمر وقت كاف، ويسمح بطلب جديد كل 12 ثانية.

### في Traefik

يوفر Traefik [ال Middleware من نوع rateLimit](https://doc.traefik.io/traefik/reference/routing-configuration/http/middlewares/ratelimit/?ref=arabroot.io)، ونضيفه إلى نفس الملف `security.yml` ثم نربطه ب Router خاص بمسار الدخول:

```yaml
http:
  middlewares:
    login-limit:
      rateLimit:
        average: 5
        period: 1m
        burst: 5
```

```yaml
    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 يعطي القاعدة الأطول [أولوية أعلى](https://doc.traefik.io/traefik/reference/routing-configuration/http/routing/rules-and-priority/?ref=arabroot.io) افتراضياً، فيمر طلب `/login` عبر هذا ال Router وحده. وعند إرسال ثمانية طلبات كما في المثال السابق سوف يكون المخرج كما يلي:

```bash
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](https://github.com/mholt/caddy-ratelimit?ref=arabroot.io)، وهي من كاتب Caddy الأصلي ولكنها ليست جزءاً من المشروع الرسمي كما ينبه مستودعها. ولإضافتها تبني Image خاصاً بك عبر الأداة xcaddy كما يشرح [توثيق ال Image الرسمي](https://hub.docker.com/%5F/caddy?ref=arabroot.io)، والملف `/opt/caddy/Dockerfile` سوف يكون كما يلي:

```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`، وتأكد أن الوحدة موجودة:

```bash
docker compose exec caddy caddy list-modules | grep rate
```

```bash
http.handlers.rate_limit
```

الآن أضف إلى موقع التطبيق في ال Caddyfile الكتلة التالية:

```nginx
	rate_limit {
		zone login {
			match {
				path /login
			}
			key {remote_host}
			events 5
			window 1m
		}
	}
```

والمعنى خمسة طلبات لكل عنوان خلال دقيقة على المسار `/login`، وعند تجاوزها يرد Caddy بالرمز `429` مع الترويسة `Retry-After` التي تخبر العميل بعدد الثواني المتبقية. وإذا أرسلت ثمانية طلبات ثم طلباً تاسعاً تعرض استجابته كاملة:

```bash
curl -s -i --resolve app.example.com:443:127.0.0.1 https://app.example.com/login
```

والمخرج سوف يكون كما يلي:

```http
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 عن ضبط حركة المرور](https://www.haproxy.com/documentation/haproxy-configuration-tutorials/security/traffic-policing/?ref=arabroot.io)، والسطور التالية داخل ال frontend تعطي نفس النتيجة، أي خمسة طلبات تمر ثم `429`:

```ini
    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](https://arabroot.io/articles/nginx-proxy-manager-%D9%86%D8%B7%D8%A7%D9%82%D8%A7%D8%AA-%D9%88%D8%B4%D9%87%D8%A7%D8%AF%D8%A7%D8%AA-tls/)، وهي تطبق على ال Proxy Host كله. وإذا أردت أن تحمي مساراً واحداً مثل `/admin` وتترك باقي التطبيق للعموم، فأضف في تبويب Advanced الكتلة التالية التي تستخدم [وحدة access في Nginx](https://nginx.org/en/docs/http/ngx%5Fhttp%5Faccess%5Fmodule.html?ref=arabroot.io):

```nginx
location /admin {
    allow 10.8.0.0/24;
    allow 192.168.1.0/24;
    deny all;
    include conf.d/include/proxy.conf;
}
```

⚠️

إذا كنت تستخدم Forward Auth مع authentik واستبدلت كتلة `location /` في تبويب Advanced، فإن قائمة الوصول المختارة في تبويب Details لا تطبق إطلاقاً كما شرحنا في دليل [حماية أي تطبيق باستخدام authentik Forward Auth](https://arabroot.io/articles/%D8%AD%D9%85%D8%A7%D9%8A%D8%A9-%D8%A3%D9%8A-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-%D8%A8%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-authentik-forward-auth/)، وهناك تجد طريقة Custom Locations التي تجمع بين الاثنين. لذلك اختبر دائماً من عنوان غير مسموح بعد أي تعديل، ولا تعتمد على ما تعرضه الواجهة.

**في Traefik** تستخدم [ال Middleware من نوع ipAllowList](https://doc.traefik.io/traefik/reference/routing-configuration/http/middlewares/ipallowlist/?ref=arabroot.io) على Router خاص بالمسار:

```yaml
http:
  middlewares:
    vpn-only:
      ipAllowList:
        sourceRange:
          - "10.8.0.0/24"
          - "192.168.1.0/24"
```

```yaml
    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](https://caddyserver.com/docs/caddyfile/matchers?ref=arabroot.io#remote-ip) مع `not`، فتطابق كل طلب إلى `/admin` من خارج الشبكات المسموحة وترد عليه بالرمز `403`:

```nginx
	@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`. والآن اختبر من عنوان خارج هذه الشبكات:

```bash
curl -s -i -H 'Host: app.example.com' http://127.0.0.1/admin | head -n 1
```

```http
HTTP/1.1 403 Forbidden
```

والنتيجة واحدة في الأدوات الأربع. وتذكر أن قائمة العناوين لا تعني شيئاً إذا كان ال Reverse Proxy يرى عنواناً خاطئاً، فخلف Cloudflare يرى عناوين Cloudflare، وإذا وثق بترويسة يرسلها العميل فيستطيع أي شخص أن يكتب فيها عنواناً مسموحاً، لذلك راجع قسم انتحال العنوان في دليل [تطبيقك خلف Reverse Proxy](https://arabroot.io/articles/%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D9%83-%D8%AE%D9%84%D9%81-reverse-proxy/) قبل أن تعتمد على هذه القوائم.

### حظر الدول أم شبكة VPN؟

الخيار الذي يطلبه كثيرون هو حظر الدول Geo-blocking، أي أن تسمح لعناوين بلدك فقط، ويدعمه NPM عبر [وحدة geoip2](https://nginxproxymanager.com/advanced-config/?ref=arabroot.io#enabling-the-geoip2-module) مع قاعدة بيانات عناوين مثل GeoLite2 من MaxMind، كما يدعمه Cloudflare في القواعد المخصصة Custom Rules. وهو مفيد لتقليل الضجيج في السجلات، ولكنه حماية ضعيفة، والسبب أن المهاجم يستطيع استئجار سيرفر أو VPN في بلدك خلال دقائق، وأن موظفك المسافر سوف يحظر.

والحل الأنسب للوحات الإدارة هو ألا تكون على الإنترنت أصلاً، وإنما خلف شبكة خاصة VPN كما في دليل [شبكة خاصة مع WireGuard وwg-easy](https://arabroot.io/articles/%D8%B4%D8%A8%D9%83%D8%A9-%D8%AE%D8%A7%D8%B5%D8%A9-%D9%85%D8%B9-wireguard-%D9%88-wg-easy/)، ثم تسمح في القائمة لشبكة ال VPN وحدها مثل `10.8.0.0/24` في الأمثلة أعلاه. وبهذا الشكل فإن من لا يملك مفتاح WireGuard لا يرى اللوحة، ومن يملكه يدخل من أي مكان في العالم.

## الحظر التلقائي: fail2ban وCrowdSec

الحد من معدل الطلبات يبطئ المهاجم ولا يوقفه، فبعد كل دقيقة يحصل على خمس محاولات جديدة، وطوال هذا الوقت يستطيع أن يفحص مسارات أخرى. والطبقة التالية هي أن تقرأ السجلات Logs، فإذا تكرر من عنوان معين سلوك مريب كالرفض المتكرر بالرموز `401` و`403` و`429`، حظرته على مستوى جدار الحماية Firewall لمدة معينة، فلا يصل إلى أي خدمة على السيرفر.

### fail2ban على سجلات ال Reverse Proxy

في دليل [تأمين خادم VPS من أول دخول](https://arabroot.io/articles/%D8%AA%D8%A3%D9%85%D9%8A%D9%86-%D8%AE%D8%A7%D8%AF%D9%85-vps-%D9%85%D9%86-%D8%A3%D9%88%D9%84-%D8%AF%D8%AE%D9%88%D9%84/) ثبتنا [fail2ban](https://github.com/fail2ban/fail2ban?ref=arabroot.io) لحماية SSH، والآن سوف نوسعه ليقرأ سجلات NPM. يكتب NPM سجل كل مضيف في `/data/logs/proxy-host-N_access.log`، والسطر فيه بالشكل التالي:

```bash
[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` سوف يكون كما يلي:

```ini
[Definition]
failregex = ^\S* \S+ \S+ (?:401|403|429) - \S+ \S+ \S+ "[^"]*" \[Client <HOST>\]
ignoreregex =
```

ولاحظ أن التعبير يبدأ ب `^\S*` وليس بنص التاريخ، والسبب أن fail2ban يحذف التاريخ من السطر قبل أن يطبق المرشح، فإذا كتبت نمط التاريخ بنفسك فلن يطابق أي سطر. وقبل أن تفعل المرشح اختبره على السجل الحقيقي بالأداة `fail2ban-regex`، ويحفظ NPM سجلاته في ال Volume `npm_data`:

```bash
sudo fail2ban-regex /var/lib/docker/volumes/npm_data/_data/logs/proxy-host-1_access.log /etc/fail2ban/filter.d/npm-abuse.conf
```

والمخرج سوف يكون كما يلي:

```bash
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`:

```ini
[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   = 1h
```

```bash
sudo fail2ban-client -t
sudo systemctl reload fail2ban
sudo fail2ban-client status npm-abuse
```

في الإعداد أعلاه لاحظ التالي:

- السطر `chain = DOCKER-USER` هو أهم سطر هنا، والسبب أن الطلبات إلى Container ينشر منفذه لا تمر بسلسلة `INPUT` التي يضع فيها fail2ban قواعده افتراضياً، وإنما بسلسلة `FORWARD`، و[توثيق Docker](https://docs.docker.com/engine/network/packet-filtering-firewalls/?ref=arabroot.io) يحدد السلسلة `DOCKER-USER` لقواعدك، فبدون هذا السطر سوف يظهر العنوان محظوراً في `fail2ban-client status` ويستمر في الوصول إلى NPM.
- السطر `backend = auto` يجعل هذا السجن يقرأ الملفات، والسبب أن إعداد دليل تأمين VPS جعل `backend = systemd` افتراضياً لكل السجون، وسجلات NPM ليست في journald.
- عشر محاولات مرفوضة خلال عشر دقائق تعني حظراً لساعة، وارفع `maxretry` إذا كانت لديك قوائم وصول يصطدم بها موظفوك كثيراً.

### CrowdSec: الحظر الجماعي

يعمل [CrowdSec](https://docs.crowdsec.net/u/getting%5Fstarted/intro?ref=arabroot.io) بنفس فكرة fail2ban، أي قراءة السجلات واكتشاف السلوك المريب، ولكنه يقسم العمل إلى جزأين: محرك يقرأ السجلات ويتخذ القرارات ويحفظها في واجهة محلية Local API، ومكونات الحظر Remediation Components التي يسميها المشروع Bouncers، وهي التي تنفذ القرار في جدار الحماية أو داخل ال Reverse Proxy نفسه. والفرق الأهم أن المستخدمين يشاركون العناوين المحظورة، فتحصل على قائمة بعناوين هاجمت غيرك قبل أن تصل إليك.

ولكل أداة مجموعة جاهزة Collection تعرف صيغة سجلاتها والسيناريوهات Scenarios التي تكتشفها، مثل الفحص عن المسارات وتخمين كلمات المرور، فلسجلات NPM تثبت المجموعة [crowdsecurity/nginx-proxy-manager](https://app.crowdsec.net/hub/author/crowdsecurity/collections/nginx-proxy-manager?ref=arabroot.io) وتخبر CrowdSec بمكان السجلات بالنوع `nginx-proxy-manager`، ولTraefik وCaddy مجموعتان مماثلتان هما [crowdsecurity/traefik](https://app.crowdsec.net/hub/author/crowdsecurity/collections/traefik?ref=arabroot.io) و[crowdsecurity/caddy](https://app.crowdsec.net/hub/author/crowdsecurity/collections/caddy?ref=arabroot.io). أما مكون الحظر فيختلف بحسب الأداة، وهذا ما يجب أن تنتبه له:

- **Nginx Proxy Manager**: لا يوجد مكون حظر داخل ال Image الرسمي، لذلك الحل الأبسط هو [مكون جدار الحماية](https://docs.crowdsec.net/u/bouncers/firewall?ref=arabroot.io) على السيرفر نفسه، بالحزمة `crowdsec-firewall-bouncer-iptables`، مع إضافة `DOCKER-USER` إلى `iptables_chains` في إعداده للسبب نفسه الذي شرحناه مع fail2ban. وهناك نسخة معدلة Fork من NPM اسمها [NPMplus](https://github.com/ZoeyVid/NPMplus?ref=arabroot.io) تدمج CrowdSec داخلها، ولكنها مشروع مستقل له جدول إصداراته.
- **Traefik**: المكون المعتمد في [توثيق CrowdSec](https://docs.crowdsec.net/u/bouncers/traefik?ref=arabroot.io) هو الإضافة [crowdsec-bouncer-traefik-plugin](https://plugins.traefik.io/plugins/6335346ca4caa9ddeffda116/crowdsec-bouncer-traefik-plugin?ref=arabroot.io)، وهي من تطوير المجتمع، وتعمل ك Middleware ترد على العنوان المحظور بالرمز `403`.
- **Caddy**: الوحدة [caddy-crowdsec-bouncer](https://github.com/hslatman/caddy-crowdsec-bouncer?ref=arabroot.io)، وتحتاج إلى بناء Image خاص بالأداة xcaddy كما فعلنا مع caddy-ratelimit.
- **HAProxy**: مكون [HAProxy SPOA](https://docs.crowdsec.net/u/bouncers/haproxy%5Fspoa?ref=arabroot.io) الذي يتصل ب HAProxy عبر بروتوكول SPOE.

|            | fail2ban                                                                        | CrowdSec                                                                                   |
| ---------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| نقاط القوة | خفيف، وموجود في مستودعات كل التوزيعات، وتكتب المرشح بنفسك فتعرف بالضبط ما يحظره | سيناريوهات جاهزة لعشرات التطبيقات، وقائمة عناوين مشتركة، ومكونات حظر داخل ال Reverse Proxy |
| نقاط الضعف | كل مرشح تكتبه وتختبره بنفسك، ولا يعرف شيئاً عن عنوان هاجم غيرك                  | أجزاء أكثر تثبتها وتحدثها، ويحتاج حساباً في لوحة CrowdSec للاستفادة الكاملة من القوائم     |

والاختيار بينهما يعتمد على حجم ما تديره، فسيرفر واحد بثلاثة تطبيقات يكفيه fail2ban بمرشح واحد، أما إذا كانت لديك عدة سيرفرات أو تطبيقات مكشوفة للعموم فإن CrowdSec يوفر عليك كتابة المرشحات ومتابعتها.

## جدار حماية تطبيقات الويب WAF

الطبقات السابقة تنظر إلى من يرسل الطلب وكم مرة أرسله، أما جدار حماية تطبيقات الويب Web Application Firewall واختصاراً WAF فينظر إلى محتوى الطلب نفسه، فيقرأ الرابط والترويسات وجسم الطلب Body ويبحث فيها عن أنماط الهجوم المعروفة، مثل حقن SQL واختصاراً SQL Injection، وحقن السكربتات XSS، والوصول إلى ملفات النظام بمسارات مثل `../../etc/passwd`، وحقن الأوامر Command Injection. والمحرك الأشهر هو [ModSecurity](https://github.com/owasp-modsecurity/ModSecurity?ref=arabroot.io) الذي انتقل إلى OWASP بعد أن أنهت Trustwave رعايته في يوليو 2024، وبديله المكتوب بلغة Go هو Coraza، والمحركان لا يعرفان الهجمات بأنفسهما، وإنما يقرآن قواعد Rules، والمجموعة المعتمدة منها هي [OWASP Core Rule Set](https://coreruleset.org/?ref=arabroot.io) واختصاراً CRS.

وتعمل CRS بنظام النقاط Anomaly Scoring، فكل قاعدة تطابق تضيف نقاطاً إلى الطلب، وإذا تجاوز المجموع الحد، وهو 5 افتراضياً، يرفض الطلب بالرمز `403`. ومستوى التشدد Paranoia Level من 1 إلى 4، والمستوى 1 هو الافتراضي وأقلها إنذارات خاطئة، وتجد تفاصيل كل مستوى في [توثيق CRS](https://coreruleset.org/docs/concepts/paranoia%5Flevels/?ref=arabroot.io).

### ModSecurity مع CRS أمام تطبيقك

أسهل طريقة لتجربة WAF هي ال Image الرسمي [owasp/modsecurity-crs](https://hub.docker.com/r/owasp/modsecurity-crs?ref=arabroot.io)، وهو Nginx فيه ModSecurity وCRS جاهزين، فتضعه بين ال Reverse Proxy والتطبيق على نفس شبكة `proxy`، وتوجه ال Proxy Host إليه بدلاً من التطبيق:

```yaml
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:

```bash
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/"
```

والمخرج سوف يكون كما يلي:

```bash
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 داخلية يكتب فيها فريقك شروحات تقنية، فيحفظ أحدهم صفحة فيها السطر التالي:

```bash
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'
```

```bash
403
```

رفض WAF طلباً مشروعاً تماماً، والسبب أن النص يحتوي ما يحتويه الهجوم بالضبط، فطابق القواعد `930120` (OS File Access Attempt) و`932160` و`932235` و`932260` الخاصة بحقن الأوامر، وكذلك الصفحة التي تشرح وسم `<script>` سوف ترفض. وهذا هو الإنذار الخاطئ False Positive، وهو ما يجعل كثيرين يعطلون WAF بعد أسبوع من تفعيله. والحل ليس رفع الحد ولا تعطيل القواعد، وإنما استثناء محدد: هذا الحقل، في هذا المسار، من هذه الفئة من القواعد، كما يشرح [توثيق CRS عن ضبط الإنذارات الخاطئة](https://coreruleset.org/docs/concepts/false%5Fpositives%5Ftuning/?ref=arabroot.io). والملف `REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf` سوف يكون كما يلي:

```ini
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`، ثم أرسل نفس النص مرة إلى صفحة الويكي ومرة إلى مسار آخر:

```bash
200
403
```

في الإعداد أعلاه لاحظ التالي:

- الاستثناء يزيل الحقل `content` وحده من قواعد ثلاث فئات Tags، هي حقن الأوامر `attack-rce` وقراءة الملفات `attack-lfi` وXSS، وفي المسار `/wiki/save` وحده، أما باقي الحقول وباقي القواعد وباقي المسارات فما زالت محمية، ولهذا رفض المسار الآخر نفس النص.
- رقم القاعدة `id:1000` يجب أن يكون فريداً، والمجال من 1 إلى 99999 مخصص للقواعد المحلية في CRS.
- لا تبدأ بالاستثناءات الواسعة مثل تعطيل قاعدة كاملة بالأمر `SecRuleRemoveById`، والسبب أنك تزيل الحماية عن التطبيق كله لتصلح صفحة واحدة.

وكلفة WAF ليست في الإنذارات وحدها، فكل طلب يمر على مئات التعبيرات النمطية Regular Expressions، وفحص جسم الطلبات الكبيرة يستهلك المعالج CPU والذاكرة، وكلما رفعت مستوى التشدد زادت الكلفة والإنذارات معاً. لذلك ضعه أمام التطبيقات المكشوفة للعموم، كالموقع ونماذج الاتصال وواجهات API العامة، أما الأدوات الداخلية خلف VPN فلا تحتاجه في الغالب.

📌

في مارس 2019 دخلت مهاجمة إلى بيانات بنك Capital One المخزنة على AWS مستغلة خطأ في إعداد جدار حماية تطبيقات الويب WAF، كما جاء في [بيان وزارة العدل الأمريكية](https://www.justice.gov/usao-wdwa/pr/seattle-tech-worker-arrested-data-theft-involving-large-financial-services-company?ref=arabroot.io)، فوصلت إلى بيانات نحو 100 مليون شخص في الولايات المتحدة و6 ملايين في كندا بحسب [صفحة البنك عن الحادثة](https://www.capitalone.com/digital/facts2019/?ref=arabroot.io)، ولم يكتشف البنك ما حدث إلا في يوليو بعد أن أبلغه باحث أمني. والدرس أن WAF ليس جداراً يحميك بمجرد تشغيله، وإنما برنامج آخر على حافة شبكتك، وإعداده نفسه جزء من سطح الهجوم Attack Surface، لذلك راجع إعداده وصلاحياته كما تراجع أي تطبيق آخر.

### وماذا عن NPM وTraefik وCaddy؟

- **Nginx Proxy Manager**: لا يحتوي ال Image الرسمي على ModSecurity، لذلك الطريقة الأنسب هي Container ال WAF أعلاه بين NPM والتطبيق، أو مكون AppSec في CrowdSec الذي يطبق قواعد CRS عبر NPMplus.
- **Caddy**: الوحدة [coraza-caddy](https://github.com/corazawaf/coraza-caddy?ref=arabroot.io) من مشروع Coraza نفسه، وتحتاج إلى بناء Image خاص بالأمر `xcaddy build --with github.com/corazawaf/coraza-caddy/v2`، وفيها قواعد CRS مدمجة تفعلها بالخيار `load_owasp_crs`، مع السطر `order coraza_waf first` في الإعدادات العامة.
- **Traefik**: الإضافة [coraza-http-wasm-traefik](https://github.com/jcchavezs/coraza-http-wasm-traefik?ref=arabroot.io) تعمل عبر WebAssembly، ويصفها مستودعها بأنها ما زالت في مرحلة مبكرة، أما [ال Middleware الأسرع](https://doc.traefik.io/traefik/reference/routing-configuration/http/middlewares/waf/?ref=arabroot.io) فمتاح في Traefik Hub التجاري فقط، لذلك فال Container المستقل أعلاه هو الخيار العملي مع Traefik أيضاً.

### أم تترك المهمة لـ Cloudflare؟

إذا كان نطاقك على Cloudflare مع تفعيل ال Proxy، فالخطة المجانية فيها [مجموعة القواعد المجانية Cloudflare Free Managed Ruleset](https://developers.cloudflare.com/waf/managed-rules/?ref=arabroot.io) التي تغطي الثغرات الأوسع انتشاراً، و[خمس قواعد مخصصة Custom Rules](https://developers.cloudflare.com/waf/custom-rules/?ref=arabroot.io) تستطيع أن تحظر بها دولة أو مساراً، و[قاعدة واحدة للحد من معدل الطلبات](https://developers.cloudflare.com/waf/rate-limiting-rules/?ref=arabroot.io) بفترة عد ثابتة 10 ثوان. أما مجموعة Cloudflare OWASP Core Ruleset فهي متاحة من خطة Pro فما فوق.

ونقطة القوة هنا أن الهجوم يتوقف قبل أن يصل إلى سيرفرك ويستهلك موارده، ولا توجد عندك قواعد تحدثها، أما نقطة الضعف فهي أن هذه الحماية كلها تسقط إذا وصل المهاجم إلى العنوان الحقيقي للسيرفر مباشرة، لذلك إذا اعتمدت على Cloudflare فاقصر المنفذين 80 و443 على [عناوين Cloudflare](https://www.cloudflare.com/ips/?ref=arabroot.io) وحدها في جدار الحماية، أو استخدم Cloudflare Tunnel كما في دليل [تشغيل خادمك من إنترنت المنزل](https://arabroot.io/articles/%D8%AA%D8%B4%D8%BA%D9%8A%D9%84-%D8%AE%D8%A7%D8%AF%D9%85%D9%83-%D9%85%D9%86-%D8%A5%D9%86%D8%AA%D8%B1%D9%86%D8%AA-%D8%A7%D9%84%D9%85%D9%86%D8%B2%D9%84/). والطبقتان لا تتعارضان، فقواعد 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
# Nginx Proxy Manager: تبويب Advanced
location ~ /\.(?!well-known) {
    return 404;
}
```

```yaml
# 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"
```

```nginx
# 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) }`. والآن اختبر:

```bash
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 سوف يكون كما يلي:

```bash
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](https://arabroot.io/articles/%D8%AD%D9%85%D8%A7%D9%8A%D8%A9-%D8%A3%D9%8A-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82-%D8%A8%D8%A7%D8%B3%D8%AA%D8%AE%D8%AF%D8%A7%D9%85-authentik-forward-auth/).

ولكن Forward Auth لا يلغي الطبقات الأخرى، وإنما يقف بينها، فالترتيب الذي نوصي به للأداة الداخلية هو: VPN أو قائمة عناوين، ثم Forward Auth، ثم التطبيق. أما التطبيق المكشوف للعموم الذي لا يمكن وضعه خلف تسجيل دخول، كموقع الشركة أو نموذج الاتصال، فطبقاته هي ترويسات الأمان والحد من معدل الطلبات وWAF والحظر التلقائي. ولاحظ أيضاً أن صفحة دخول authentik نفسها تحتاج إلى حد لمعدل الطلبات، فهي الآن الباب الذي يطرقه الجميع.

## قائمة تحقق لكل تطبيق جديد

قبل أن تشير بنطاق أي تطبيق جديد إلى السيرفر، مر على هذه القائمة:

1. التطبيق لا ينشر أي منفذ على السيرفر، ويصل إليه ال Reverse Proxy عبر شبكة Docker فقط.
2. هل يحتاجه أحد من خارج الفريق؟ إذا كانت الإجابة لا، فضعه خلف VPN أو قائمة عناوين، ثم Forward Auth.
3. HTTPS مع Force SSL، ثم HSTS بعد أن تتأكد أن الشهادة تتجدد، ودون `preload` إلا بقرار مدروس.
4. ترويسات الأمان: `X-Content-Type-Options` و`X-Frame-Options` و`Referrer-Policy` و`Permissions-Policy`، وCSP بصيغة Report-Only إذا لم يرسلها التطبيق نفسه، ثم اختبرها بالأمر `curl -I`.
5. حد لمعدل الطلبات على مسار الدخول ومسار إعادة تعيين كلمة المرور، واختبره حتى ترى `429`.
6. مسار `/admin` أو لوحة الإدارة مقصورة على شبكة ال VPN، واختبرها من عنوان آخر حتى ترى `403`.
7. الطلبات إلى `/.env` و`/.git/config` ترد ب `404`، والترويستان `Server` و`X-Powered-By` محذوفتان.
8. وضع التطوير Debug Mode معطل، والتطبيق يرى عنوان الزائر الحقيقي.
9. سجل المضيف يقرؤه fail2ban أو CrowdSec، والحظر يطبق على السلسلة `DOCKER-USER`.
10. إذا كان التطبيق للعموم، فضع أمامه 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.