Insiden keamanan seperti eksploitasi multi-tenant yang diungkap oleh Eaton Works (kasus jnj-webapp-hacks) menunjukkan risiko fatal dari kegagalan validasi konteks tenant. Penyerang dapat melompat antar batas tenant hanya dengan memanipulasi parameter ID request akibat kontrol otorisasi yang hanya mengandalkan logika aplikasi.
Spektrum Arsitektur Multi-Tenancy: Isolasi vs Biaya
Memilih arsitektur multi-tenant adalah kompromi langsung antara batas keamanan (security boundary), kompleksitas operasional, dan efisiensi resource. Tiga pola umum:
- Logical Separation (Shared Database, Shared Schema): Data seluruh tenant disimpan di tabel yang sama dengan pembeda kolom
tenant_id. Overhead biaya sangat rendah, pemanfaatan pooling database optimal, namun risiko Broken Object Level Authorization (BOLA/IDOR) paling tinggi jika aplikasi gagal memfiltertenant_id. - Physical / Schema Isolation (Separate Schema / Separate Database): Setiap tenant memiliki skema atau instance database independen. Isolasi data sangat kuat dan blast radius kebocoran data minim. Kelemahannya: biaya infrastruktur membengkak (idle resources), migrasi skema kompleks, serta limitasi connection pool ke database engine.
- API Gateway Context Routing: Permintaan routing di layer ingress berdasarkan subdomain, header JWT, atau path (misal:
tenant.domain.com). Gateway memvalidasi identitas tenant dan menyuntikkan konteks ke downstream service, sering dikombinasikan dengan arsitektur logical atau database-per-tenant.
Matriks Trade-off Arsitektur
| Model | Biaya Infra | Kompleksitas Migrasi | Ketahanan BOLA/IDOR | Pilihan Terbaik Saat |
|---|---|---|---|---|
| Shared DB + Row Isolation | Paling Rendah | Rendah (Single migration) | Rendah (Tergantung filter kode) | SaaS B2B/B2C skala masif, budget ketat |
| Separate Schema | Sedang | Sedang (Loop skema per tenant) | Sedang-Tinggi | Enterprise compliance ringan, volume sedang |
| Separate Database | Paling Tinggi | Tinggi (Provisioning, fleet management) | Sangat Tinggi (Hardware/Process boundary) | Fintech, Healthcare, strict regulatory SLA |
Mitigasi di Layer Database: PostgreSQL Row-Level Security (RLS)
Mencegah bug aplikasi membocorkan data tenant lain dilakukan dengan memindahkan penegakan isolasi ke database engine menggunakan Postgres RLS. Koneksi aplikasi menetapkan variabel session tenant sebelum mengeksekusi query.
-- 1. Buat tabel dengan kolom tenant_id
CREATE TABLE organizations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL
);
CREATE TABLE documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES organizations(id),
title TEXT NOT NULL,
content TEXT NOT NULL
);
-- Index tenant_id untuk performa filtering RLS
CREATE INDEX idx_documents_tenant_id ON documents(tenant_id);
-- 2. Aktifkan Row-Level Security
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
ALTER TABLE documents FORCE ROW LEVEL SECURITY; -- Berlaku juga untuk table owner jika bukan superuser
-- 3. Definisikan Security Policy berbasis session variable
CREATE POLICY tenant_isolation_policy ON documents
AS RESTRICTIVE
FOR ALL
USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::UUID)
WITH CHECK (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::UUID);
Jika variabel app.current_tenant_id kosong atau tidak sesuai, database mengembalikan zero rows pada SELECT dan menolak operasi INSERT/UPDATE.
Enforcement Konteks Tenant di Layer Backend (Go)
Aplikasi harus memvalidasi identitas tenant dari klaim token autentikasi (JWT) yang terverifikasi, bukan dari input payload request atau URL parameter yang rentan manipulasi client.
package main
import (
"context"
"database/sql"
"net/http"
"github.com/google/uuid"
)
type contextKey string
const tenantContextKey contextKey = "tenantID"
// TenantMiddleware memvalidasi klaim tenant dari request context terotentikasi
func TenantMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Asumsi: Authn middleware sebelumnya telah memverifikasi JWT dan menyimpan klaim
tenantHeader := r.Header.Get("X-Tenant-ID") // Atau ambil langsung dari claims jwt.StandardClaims
tenantID, err := uuid.Parse(tenantHeader)
if err != nil {
http.Error(w, "Tenant ID tidak valid atau hilang", http.StatusForbidden)
return
}
ctx := context.WithValue(r.Context(), tenantContextKey, tenantID.String())
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// ExecuteTenantQuery membungkus query dalam transaksi dengan runtime setting postgres
func ExecuteTenantQuery(ctx context.Context, db *sql.DB, query string, args ...any) (*sql.Rows, error) {
tenantID, ok := ctx.Value(tenantContextKey).(string)
if !ok || tenantID == "" {
return nil, sql.ErrNoRows
}
tx, err := db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true})
if err != nil {
return nil, err
}
defer tx.Rollback()
// Set session parameter khusus di transaksi ini (LOCAL memastikan reset saat tx berakhir)
_, err = tx.ExecContext(ctx, "SELECT set_config('app.current_tenant_id', $1, true)", tenantID)
if err != nil {
return nil, err
}
rows, err := tx.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
return rows, tx.Commit()
}
Prinsip Mencegah Celah Akses Lintas Tenant
- Strict Boundary di API Gateway: Validasi keterikatan user ID dan tenant ID pada entry point. Hindari endpoint yang menerima
tenant_idlangsung dari path parameter tanpa verifikasi hak akses pada JWT token. - Defense in Depth: Gabungkan RLS di level DB dengan validasi di application code. Jangan jadikan ORM query filter (
WHERE tenant_id = x) sebagai satu-satunya garis pertahanan. - Connection Pooling Hygiene: Saat memakai session-level variables di Postgres dengan connection pooling (seperti PgBouncer dalam mode transaction), pastikan selalu memakai
set_config(..., true)dengan parameter ketiga bernilaitrue(local to transaction) agar konteks tenant tidak bocor ke request berikutnya.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!