feat: TLS — إغلاق المنافذ المكشوفة + الثقة بالبروكسي + دليل الإصدار
النطاق: tripz-api.intaleqapp.com (قرار المالك) — مملوك وجاهز، ومحايد عن اسم العلامة الذي لم يُحسم فلا يحتاج تغييراً لاحقاً. ثغرتان كانتا ستُبطلان TLS من أصله: - API كان منشوراً على 0.0.0.0:4010 → صار 127.0.0.1. بلا هذا يبقى http://IP:4010 مفتوحاً فيتجاوز TLS كلياً وتصير الشهادة زينة - **Postgres كان مكشوفاً على 0.0.0.0:55432** — القاعدة على الإنترنت مباشرة → صار 127.0.0.1. المنفذ غير القياسي إخفاء لا أمان. الوصول من الماك عبر نفق SSH (docs/20 §4) trust proxy = 1: خلف Nginx كان req.ip يصير 127.0.0.1 للجميع فيمتلئ سجل التدقيق المالي (I7) بعنوان البروكسي بدل السائق — أثرٌ بلا قيمة عند النزاع. قفزة واحدة فقط: الثقة المفتوحة تسمح للعميل بتزوير X-Forwarded-For. docs/20: DNS → موقع reverse proxy → شهادة → **إعداد ترقية WebSocket** (بدونه يسقط التتبّع الحيّ) + مهلة 3600s للنبضات طويلة العمر + فحص تحقّق أهمّه أن http://IP:4010 يجب أن **يفشل**. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
1605754722
commit
d2a34f753c
@@ -2,7 +2,8 @@
|
||||
# ملاحظة: السيرفر مشترك — كل شيء معزول ببادئة tripz و Redis DB غير الافتراضي (راجع docs/14)
|
||||
|
||||
NODE_ENV=development
|
||||
API_PORT=4010 # منفذ غير شائع لتفادي التصادم على السيرفر المشترك
|
||||
API_PORT=4010 # يُنشر على 127.0.0.1 فقط — Nginx (CloudPanel) ينهي الـTLS. راجع docs/20
|
||||
DB_EXPOSE_PORT=55432 # 127.0.0.1 فقط. للوصول من الماك: نفق SSH (docs/20 §4)
|
||||
|
||||
# ---- PostgreSQL (قاعدة مستقلة + بادئة جداول) ----
|
||||
DB_HOST=postgres
|
||||
|
||||
@@ -16,7 +16,10 @@ services:
|
||||
volumes:
|
||||
- tripz-pgdata:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "${DB_EXPOSE_PORT:-55432}:5432" # منفذ مضيف غير قياسي (عزل)
|
||||
# 127.0.0.1 فقط — كانت مكشوفة على 0.0.0.0 أي أن القاعدة كانت على
|
||||
# الإنترنت مباشرة. المنفذ غير القياسي ليس أماناً، هو إخفاء.
|
||||
# للوصول من الماك: نفق SSH (راجع docs/20).
|
||||
- "127.0.0.1:${DB_EXPOSE_PORT:-55432}:5432"
|
||||
networks: [tripz-net]
|
||||
|
||||
redis:
|
||||
@@ -50,7 +53,10 @@ services:
|
||||
command: ["node", "dist/main.js"]
|
||||
depends_on: [postgres, redis]
|
||||
ports:
|
||||
- "${API_PORT:-4010}:4010"
|
||||
# 127.0.0.1 فقط: لا يصل إليه إلا Nginx المحلي (CloudPanel) الذي ينهي
|
||||
# الـTLS. بلا هذا يبقى http://IP:4010 مفتوحاً فيتجاوز الـTLS كله
|
||||
# ويصير الشهادة زينة (docs/20).
|
||||
- "127.0.0.1:${API_PORT:-4010}:4010"
|
||||
volumes:
|
||||
- tripz-storage:/app/storage
|
||||
networks: [tripz-net]
|
||||
|
||||
+8
-1
@@ -3,13 +3,14 @@ import { NestFactory } from '@nestjs/core';
|
||||
import { ValidationPipe, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import { AppModule } from './app.module';
|
||||
import { RedisIoAdapter } from './realtime/redis-io.adapter';
|
||||
|
||||
async function bootstrap() {
|
||||
// rawBody: التوقيع يُحسب على الجسم **كما وصل** (docs/17 — I6). إعادة تسلسل
|
||||
// JSON قد تغيّر ترتيب المفاتيح أو المسافات فيفشل توقيع سليم.
|
||||
const app = await NestFactory.create(AppModule, { rawBody: true });
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule, { rawBody: true });
|
||||
const cfg = app.get(ConfigService);
|
||||
|
||||
// مُحوّل Socket.IO عبر Redis — يفعّل التوسّع الأفقي (عدة نسخ)
|
||||
@@ -17,6 +18,12 @@ async function bootstrap() {
|
||||
await redisIoAdapter.connectToRedis();
|
||||
app.useWebSocketAdapter(redisIoAdapter);
|
||||
|
||||
// خلف بروكسي (Nginx ينهي الـTLS): بلا هذا يصير req.ip = 127.0.0.1 للجميع،
|
||||
// فيمتلئ سجل التدقيق المالي (docs/17 — I7) بعنوان البروكسي بدل عنوان
|
||||
// السائق — أي أثرٌ بلا قيمة عند النزاع. `1` = ثقة بقفزة واحدة فقط؛ الثقة
|
||||
// المفتوحة تسمح للعميل بتزوير X-Forwarded-For.
|
||||
app.set('trust proxy', 1);
|
||||
|
||||
app.setGlobalPrefix('api');
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({ whitelist: true, transform: true }),
|
||||
|
||||
@@ -176,10 +176,14 @@
|
||||
| I3 | **جدول لكل طريقة دفع** | ⏳ لم يُنفَّذ — إعادة هيكلة مخطط كاملة (`tripz_pay_payments` عام حالياً). يُنفَّذ مع أول ربط بوابة حقيقية، لا قبله. |
|
||||
| I8 | **webhook SMS + Gemini** | ⏳ لم يُنفَّذ — ميزة كاملة (`raw_sms_log` + استخراج + تسوية). تُبنى عند دخول السوق السوري فعلياً. |
|
||||
|
||||
### 🔴 أخطر ثغرة مالية قائمة — ليست في الكود
|
||||
**الـAPI يعمل على `http` بلا TLS** (`194.163.173.157:4010`، والتطبيق يفعّل `usesCleartextTraffic=true`). من يلتقط الشبكة يسرق توكن أي سائق ويسحب أرباحه — ولا OTP ولا HMAC ولا تدقيق يمنع ذلك تماماً.
|
||||
- توقيع I6 **يخفّف** الضرر (التوكن المسروق وحده لا يكفي: المفتاح لا يمرّ في الشبكة بعد الدخول) لكنه **ترقيع لا بديل**.
|
||||
- **الإصلاح الحقيقي: TLS** (نطاق + شهادة عبر CloudPanel/Let's Encrypt) ثم `usesCleartextTraffic=false`. **يسبق أي عمل مدفوعات آخر.**
|
||||
### 🔴 TLS — أخطر ثغرة مالية كانت قائمة. **المستند: [20-tls](20-tls.md)**
|
||||
الـAPI كان على `http` بلا TLS: من يلتقط الشبكة يسرق توكن أي سائق ويسحب أرباحه، ولا OTP ولا HMAC ولا تدقيق يمنع ذلك.
|
||||
**النطاق المعتمد: `tripz-api.intaleqapp.com`.** الجزء البرمجي ✅ (المنافذ + trust proxy)؛ الباقي خطوات سيرفر يدوية في docs/20.
|
||||
|
||||
وأثناء العمل ظهرت ثغرتان تُبطلان TLS من أصله لو تُركتا:
|
||||
- **`4010` كان مكشوفاً على `0.0.0.0`** → صار `127.0.0.1`. بلا هذا يبقى `http://IP:4010` مفتوحاً فيتجاوز TLS كلياً والشهادة زينة.
|
||||
- **Postgres كان مكشوفاً على `0.0.0.0:55432`** — أي القاعدة على الإنترنت مباشرة → صار `127.0.0.1` (الوصول من الماك عبر نفق SSH).
|
||||
- **`trust proxy = 1`**: خلف Nginx كان `req.ip` سيصير `127.0.0.1` للجميع فيمتلئ سجل التدقيق المالي (I7) بعنوان البروكسي بدل السائق — أثرٌ بلا قيمة عند النزاع. والقفزة الواحدة مقصودة: الثقة المفتوحة تسمح بتزوير `X-Forwarded-For`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# 20 — TLS: إنهاء الطبقة المشفّرة أمام الـAPI
|
||||
|
||||
> **النطاق المعتمد: `tripz-api.intaleqapp.com`** (قرار المالك 2026-07-17).
|
||||
> السبب في اختيار نطاق فرعي على `intaleqapp.com`: مملوك وجاهز بلا انتظار DNS، ومحايد عن اسم العلامة التجارية الذي لم يُحسم — فلا يحتاج تغييراً لاحقاً.
|
||||
|
||||
---
|
||||
|
||||
## 1. لماذا هذا يسبق كل عمل مدفوعات
|
||||
الـAPI كان يعمل على `http://194.163.173.157:4010`. من يلتقط الشبكة (واي-فاي عام) يقرأ توكن أي سائق ويسحب أرباحه. **لا OTP ولا HMAC ولا سجل تدقيق يمنع ذلك** — كلها تُبنى فوق قناة مكشوفة. TLS هو الأساس، لا طبقة إضافية.
|
||||
|
||||
## 2. ثغرتان أُصلحتا في نفس الشغل (بلا هذا يصير TLS زينة)
|
||||
| كان | صار | لماذا |
|
||||
|-----|-----|--------|
|
||||
| `4010:4010` أي `0.0.0.0` | `127.0.0.1:4010` | بلا هذا يبقى `http://IP:4010` مفتوحاً **فيتجاوز TLS كلياً**. الشهادة على النطاق لا تُغلق المنفذ. |
|
||||
| `55432:5432` أي `0.0.0.0` | `127.0.0.1:55432` | **Postgres كان على الإنترنت مباشرة.** المنفذ غير القياسي إخفاء لا أمان. |
|
||||
|
||||
بعد هذا لا يصل للـAPI إلا Nginx المحلي، ولا تصل القاعدة إلا من داخل السيرفر.
|
||||
|
||||
## 3. الخطوات على السيرفر
|
||||
|
||||
### 3.1 DNS
|
||||
سجل `A` لـ`tripz-api.intaleqapp.com` → `194.163.173.157`. تحقّق قبل المتابعة:
|
||||
```bash
|
||||
dig +short tripz-api.intaleqapp.com # يجب أن يطبع 194.163.173.157
|
||||
```
|
||||
> Let's Encrypt يفشل إن لم ينتشر الـDNS بعد. لا تُصدر الشهادة قبل أن يطبع الأمر العنوان الصحيح.
|
||||
|
||||
### 3.2 موقع Reverse Proxy في CloudPanel
|
||||
CloudPanel → **Add Site** → **Create Reverse Proxy**:
|
||||
- Domain: `tripz-api.intaleqapp.com`
|
||||
- Reverse Proxy URL: `http://127.0.0.1:4010`
|
||||
|
||||
### 3.3 الشهادة
|
||||
CloudPanel → الموقع → **SSL/TLS** → **Actions → New Let's Encrypt Certificate**.
|
||||
CloudPanel يجدّدها تلقائياً.
|
||||
|
||||
### 3.4 دعم WebSocket (إلزامي — بدونه يسقط التتبّع الحيّ)
|
||||
Socket.IO يحتاج ترقية الاتصال. في **Vhost Editor** للموقع، تأكّد أن كتلة `location /` تحوي:
|
||||
```nginx
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# نبضات المواقع والسوكت طويلة العمر — الافتراضي 60s يقطعها
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
```
|
||||
`X-Forwarded-For` **ضروري**: بدونه يسجّل التدقيق المالي (docs/17 — I7) عنوان البروكسي لكل سائق، فيصير الأثر بلا قيمة. الباك إند يثق بقفزة واحدة فقط (`trust proxy = 1`) — الثقة المفتوحة تسمح للعميل بتزوير العنوان.
|
||||
|
||||
### 3.5 النشر
|
||||
```bash
|
||||
cd /home/tripz-llc && git pull origin main
|
||||
cd backend && docker compose up -d --build
|
||||
docker image prune -f
|
||||
```
|
||||
|
||||
### 3.6 التحقق
|
||||
```bash
|
||||
curl -I https://tripz-api.intaleqapp.com/api/health # 200 عبر TLS
|
||||
curl -I http://194.163.173.157:4010/api/health # يجب أن **يفشل** الآن
|
||||
docker compose exec api printenv | grep -c PLATFORM_SECRET # 1
|
||||
```
|
||||
**الفحص الثاني هو الأهم**: نجاحه يعني أن المنفذ ما زال مكشوفاً وأن TLS يُتجاوَز.
|
||||
|
||||
## 4. الوصول للقاعدة بعد الإغلاق
|
||||
لم تعد `55432` مفتوحة. من الماك:
|
||||
```bash
|
||||
ssh -L 55432:127.0.0.1:55432 root@194.163.173.157
|
||||
# ثم اتصل بـ localhost:55432 كالمعتاد
|
||||
```
|
||||
سكربتات التحقق لا تتأثر — تعمل داخل شبكة `tripz-net` وتنادي `http://tripz-api:4010` مباشرة.
|
||||
|
||||
## 5. بعد نجاح TLS — خطوات تابعة
|
||||
| البند | التفصيل |
|
||||
|-------|---------|
|
||||
| **فلاتر: BASE_URL** | `--dart-define BASE_URL=https://tripz-api.intaleqapp.com/api` |
|
||||
| **فلاتر: إغلاق النصّ الصريح** | `usesCleartextTraffic=false` في `AndroidManifest` — يمنع أي رجوع صامت لـhttp |
|
||||
| **تفعيل التوقيع** | `PAYMENTS_REQUIRE_SIGNATURE=true` بعد أن يوقّع فلاتر (docs/17 — I6) |
|
||||
| **HSTS** | بعد التأكد من استقرار TLS — يمنع الرجوع لـhttp |
|
||||
| **بقية `intaleqapp.com`** | `map-saas` و`nabeh` على https أصلاً ✅ |
|
||||
|
||||
## 6. ما لا يحلّه TLS
|
||||
- **لا يحمي من تطبيق مُفكَّك**: TLS يحمي القناة لا الطرف. الحماية من الطرف هي الاستحقاقات على السيرفر ([19-entitlements-licensing](19-entitlements-licensing.md)).
|
||||
- **لا يمنع pinning bypass**: من يملك الجهاز يرى حركته. لذلك تبقى القاعدة: **السيرفر لا يثق بالعميل أبداً**.
|
||||
|
||||
---
|
||||
← ذو صلة: [15-deploy-flow](15-deploy-flow.md) · [14-server-conventions](14-server-conventions.md) · [17-backend-backlog](17-backend-backlog.md)
|
||||
Reference in New Issue
Block a user