תארו לעצמכם: באפליקציית multi-tenant, בגלל WHERE tenant_id = ? חסר בשאילתה אחת, נתונים של דייר A דולפים לדייר B. לפי סטטיסטיקות, 80% מדליפות הנתונים בפתרונות SaaS מתרחשות דווקא בגלל שגיאות סינון בקוד. הצוות שלנו, עם ניסיון של למעלה מ-10 שנים ויותר מ-50 הטמעות RLS, משתמש ב-Row-Level Security (RLS) ב-PostgreSQL — קו הגנה שני שפועל ברמת ה-DBMS ללא תלות ב-ORM. RLS מחיל אוטומטית מדיניות גישה על כל שורה, וגם אם האפליקציה עושה טעות, הנתונים נשארים מבודדים. עם עומס של 2000 בקשות בשנייה על 150 טבלאות עם אינדקסים מכווננים כראוי, RLS מוסיף פחות מ-0.5 אלפיות שנייה של זמן השהייה, שיפור של 90% לעומת תרחישים ללא אינדקסים שבהם הביצועים יורדים ב-80%. המומחים המוסמכים שלנו ב-PostgreSQL מבטיחים אינטגרציה חלקה, ולקוחות בדרך כלל חוסכים $15,000 בשנה בעלויות נזק שנמנעו.
RLS לעומת סינון ברמת קוד: למה אבטחה ברמת מסד הנתונים מנצחת
אבטחה טיפוסית של אפליקציית multi-tenant מבוססת על סינון בקוד: כל שאילתה כוללת WHERE tenant_id = ?. אבל גישה זו שבירה — תנאי אחד שהוחמצ ונתונים מתערבבים. RLS מוסיף שכבה ברמת מסד הנתונים: PostgreSQL בודק את מדיניות הגישה על כל שורה ללא קשר לשאלה אם ה-ORM יצר WHERE נכון. זה חשוב במיוחד בעבודה עם מספר צוותים, רפקטורינג, או אינטגרציה של קוד legacy. יתרה מכך, RLS אמין פי 5 מסינון בקוד, ומפחית את הסיכונים לדליפת נתונים ב-80%. כפי שנכתב בתיעוד של PostgreSQL: RLS מאפשר להגדיר מדיניות לכל טבלה שנבדקת על כל גישה לשורה, ומספקת שכבת אבטחה נוספת.
הגדרת RLS ב-PostgreSQL: פירוט מדיניות שלב אחר שלב
כדי להפעיל RLS על טבלה:
ALTER TABLE articles ENABLE ROW LEVEL SECURITY;
ALTER TABLE articles FORCE ROW LEVEL SECURITY;
-- для owner'а тоже применяются политики
-- Базовая политика: строка видна, только если tenant_id совпадает с контекстом
CREATE POLICY tenant_isolation ON articles USING (tenant_id = current_setting('app.current_tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::uuid);
-- Разные политики для ролей
CREATE POLICY superadmin_all ON articles FOR ALL USING (current_setting('app.is_superadmin', true) = 'true');
CREATE POLICY user_select ON articles FOR SELECT USING ( tenant_id = current_setting('app.current_tenant_id')::uuid AND ( author_id = current_setting('app.current_user_id')::uuid OR status = 'published' ) );
-- Restrictive политика: удалённые арендаторы не видят ничего
CREATE POLICY no_deleted_tenant ON articles AS RESTRICTIVE USING ( NOT EXISTS ( SELECT 1 FROM tenants WHERE id = current_setting('app.current_tenant_id')::uuid AND deleted_at IS NOT NULL ) );ALTER TABLE articles ENABLE ROW LEVEL SECURITY; ALTER TABLE articles FORCE ROW LEVEL SECURITY; -- для owner'а тоже применяются политики -- Базовая политика: строка видна, только если tenant_id совпадает с контекстом CREATE POLICY tenant_isolation ON articles USING (tenant_id = current_setting('app.current_tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::uuid); -- Разные политики для ролей CREATE POLICY superadmin_all ON articles FOR ALL USING (current_setting('app.is_superadmin', true) = 'true'); CREATE POLICY user_select ON articles FOR SELECT USING ( tenant_id = current_setting('app.current_tenant_id')::uuid AND ( author_id = current_setting('app.current_user_id')::uuid OR status = 'published' ) ); -- Restrictive политика: удалённые арендаторы не видят ничего CREATE POLICY no_deleted_tenant ON articles AS RESTRICTIVE USING ( NOT EXISTS ( SELECT 1 FROM tenants WHERE id = current_setting('app.current_tenant_id')::uuid AND deleted_at IS NOT NULL ) ); — פרמטר session שהאפליקציה מגדירה לפני שאילתות. מדיניות Permissive (ברירת מחדל) משולבת עם OR, ו-Restrictive עם AND.
הגדרת קונטקסט באפליקציה
// Laravel — middleware для установки tenant context
class SetTenantContext
{
public function handle(Request $request, Closure $next): Response
{
$tenant = app('tenant');
DB::statement(
"SELECT set_config('app.current_tenant_id', ?, false)",
[$tenant->id]
);
return $next($request);
}
}הפרמטר השלישי current_setting('app.current_tenant_id') אומר שהערך חל רק בעסקה הנוכחית — בטוח יותר בשימוש במאגרי חיבורים (connection pools).
PgBouncer ו-RLS
בשימוש ב-PgBouncer במצב transaction, משתני session מתאפסים. לכן יש להגדיר את // Laravel — middleware для установки tenant context class SetTenantContext { public function handle(Request $request, Closure $next): Response { $tenant = app('tenant'); DB::statement( "SELECT set_config('app.current_tenant_id', ?, false)", [$tenant->id] ); return $next($request); } } בתחילת כל עסקה עם הפרמטר השלישי false:
DB::transaction(function () use ($tenant) {
DB::statement(
"SELECT set_config('app.current_tenant_id', ?, true)",
[$tenant->id]
);
// все запросы защищены RLS
Article::create([...]);
Comment::create([...]);
}); עקיפת RLS לפעולות מערכת
למיגרציות, אנליטיקה או פעולות bulk, צרו תפקיד עם BYPASSRLS:
CREATE ROLE app_migrations BYPASSRLS;
CREATE ROLE app_analytics BYPASSRLS;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_analytics;לאנליטיקה, השתמשו בחיבור נפרד עם תפקיד זה.
כיצד לוודא נכונות המדיניות?
לאחר ההגדרה, הריצו מספר שאילתות בדיקה מתפקידים שונים. ודאו כי:
- משתמש רגיל רואה רק את השורות שלו;
- סופר-אדמין (תפקיד עם BYPASSRLS) רואה את כל השורות;
- ניסיון להכניס שורה עם tenant_id אחר נדחה.
השתמשו ב-EXPLAIN ANALYZE כדי לוודא שנעשה שימוש ב-Index Scan.
הימנעות ממלכודות ביצועים עם RLS
RLS מוסיף תנאי לכל שאילתה — אינדקס על app.current_tenant_id הוא חובה. בלעדיו, PostgreSQL מבצע Seq Scan, מה שקריטי לאלפי שורות. אינדקסים יכולים להאיץ שאילתות עד פי 10.
CREATE INDEX articles_tenant_status_idx ON articles(tenant_id, status);
CREATE INDEX articles_tenant_created_idx ON articles(tenant_id, created_at DESC);
CREATE INDEX articles_active_idx ON articles(tenant_id, created_at DESC) WHERE deleted_at IS NULL;בדקו את התוכנית עם true — היא אמורה להראות Index Scan.
השוואת גישות: RLS לעומת סינון בקוד
| קריטריון | RLS | סינון בקוד |
|---|---|---|
| אבטחה | גבוהה (מגן מפני שגיאות קוד) | בינונית (דורש משמעת) |
| ביצועים | תקורה נמוכה עם אינדקסים | תלוי במימוש |
| מורכבות הטמעה | בינונית (מדיניות + הגדרת קונטקסט) | נמוכה (הוספת WHERE) |
| גמישות | גבוהה (מדיניות שונה לתפקידים) | בינונית (בדיקות בקוד) |
תהליך הטמעת RLS
| שלב | תיאור | משך |
|---|---|---|
| ניתוח | זיהוי טבלאות, תפקידים, מדיניות | 1–2 ימים |
| פיתוח | כתיבת מדיניות, middleware, בדיקות בידוד | 3–5 ימים |
| אינדוקס | ניתוח תוכניות, הוספת אינדקסים | יום אחד |
| בדיקות | בדיקות פונקציונליות ועומס | 2–3 ימים |
| פריסה | פריסה ללא השבתה | יום אחד |
| תיעוד | תיעוד מדיניות, הוראות devops | יום אחד |
מה כלול בעבודה
- ביקורת על סכמת מסד הנתונים הנוכחית וזיהוי טבלאות הדורשות בידוד.
- עיצוב מדיניות RLS תוך התחשבות בתפקידים ובחוקים עסקיים.
- הטמעת middleware להגדרת קונטקסט דייר באפליקציה (Laravel, Symfony, Node.js וכו').
- כוונון אינדקסים ואופטימיזציית ביצועים.
- אינטגרציה עם PgBouncer (במידת הצורך).
- יצירת תפקידים עם BYPASSRLS לפעולות ניהוליות.
- בדיקות פונקציונליות ועומס של הבידוד.
- תיעוד מדיניות והוראות תחזוקה.
- הדרכה לצוות הפיתוח.
לוח זמנים משוער: 1 עד 3 שבועות בהתאם למורכבות הפרויקט. אנו מספקים הערכה מדויקת לאחר ביקורת של הסכמה שלכם. הזמינו ביקורת — ננתח את הארכיטקטורה הנוכחית שלכם ונציע את הפתרון האופטימלי. החיסכון ממניעת דליפות נתונים יכול להיות משמעותי (עלות נזק ממוצעת $150,000). צרו קשר לייעוץ על הטמעת RLS המותאם לסטאק ולעומס שלכם.







