الأدلة التقنية

حماية تطبيقات الويب على مستوى HTTP: طبقات أمان أمام كل تطبيق في الـ Reverse Proxy

كلمة المرور وحدها لا تحمي تطبيقاً مكشوفاً على الإنترنت، لذلك نبني في هذا الدليل أمام كل تطبيق طبقات حماية في الـ Reverse Proxy نفسه، بإعدادات مختبرة على Nginx Proxy Manager وTraefik وCaddy.

حماية تطبيقات الويب على مستوى HTTP: طبقات أمان أمام كل تطبيق في الـ Reverse Proxy

لنفرض أن لديك تطبيقاً على السيرفر خلف ال 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 إلا بقرار مدروس، والسبب أن الخروج من القائمة المسبقة يستغرق شهوراً، وابدأ بمدة قصيرة ثم ارفعها على مراحل.

⚠️
خيار HSTS في تبويب SSL في Nginx Proxy Manager يرسل 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.

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

إضافة الترويسات في 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 rate
http.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;
}
⚠️
إذا كنت تستخدم Forward Auth مع authentik واستبدلت كتلة 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 1
HTTP/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   = 1h
sudo 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.
fail2banCrowdSec
نقاط القوةخفيف، وموجود في مستودعات كل التوزيعات، وتكتب المرشح بنفسك فتعرف بالضبط ما يحظرهسيناريوهات جاهزة لعشرات التطبيقات، وقائمة عناوين مشتركة، ومكونات حظر داخل ال 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 فلا تحتاجه في الغالب.

📌
في مارس 2019 دخلت مهاجمة إلى بيانات بنك Capital One المخزنة على AWS مستغلة خطأ في إعداد جدار حماية تطبيقات الويب WAF، كما جاء في بيان وزارة العدل الأمريكية، فوصلت إلى بيانات نحو 100 مليون شخص في الولايات المتحدة و6 ملايين في كندا بحسب صفحة البنك عن الحادثة، ولم يكتشف البنك ما حدث إلا في يوليو بعد أن أبلغه باحث أمني. والدرس أن WAF ليس جداراً يحميك بمجرد تشغيله، وإنما برنامج آخر على حافة شبكتك، وإعداده نفسه جزء من سطح الهجوم Attack Surface، لذلك راجع إعداده وصلاحياته كما تراجع أي تطبيق آخر.

وماذا عن 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 نفسها تحتاج إلى حد لمعدل الطلبات، فهي الآن الباب الذي يطرقه الجميع.

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

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

  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.
نشرة عرب رووت | ArabRoot

معرفة تستحق مكاناً في بريدك.

مقالات مختارة وأدوات مفيدة وأفكار لمشروعك القادم، في رسالة واحدة كل أسبوع.

يمكنك إلغاء الاشتراك متى شئت. الخصوصية

تم استلام طلبك. افتح بريدك واضغط رابط التأكيد لإتمام الاشتراك.