Tutorial Hardening PostgreSQL: Implementasi Row Level Security & pgaudit
Poin Kunci Artikel Ini:
- Dalam pola ini, data milik seluruh tenant disimpan dalam tabel yang sama dan dibedakan melalui kolom penanda seperti .
- Setiap sintaks query SQL yang dibangun oleh aplikasi wajib menyertakan klausul penyaringan seperti .
- Kelemahan fatal metode ini terletak pada faktor kelalaian manusia (human error).
Ancaman Kebocoran Data Multi-Tenant dan Ketiadaan Audit Log
Arsitektur aplikasi modern berjenis Software-as-a-Service (SaaS) umumnya mengadopsi model shared database, shared schema demi efisiensi infrastruktur dan kemudahan pemeliharaan. Dalam pola ini, data milik seluruh tenant disimpan dalam tabel yang sama dan dibedakan melalui kolom penanda seperti tenant_id. Pendekatan ini hemat sumber daya, namun menghadirkan risiko keamanan tingkat tinggi yang dikenal sebagai cross-tenant data leakage atau kebocoran data antar tenant.
Secara historis, sebagian besar pengembang menggantungkan isolasi tenant sepenuhnya pada logika aplikasi backend. Setiap sintaks query SQL yang dibangun oleh aplikasi wajib menyertakan klausul penyaringan seperti WHERE tenant_id = 'tenant_a'. Kelemahan fatal metode ini terletak pada faktor kelalaian manusia (human error). Bug pada penulisan ORM, kegagalan parameter parsing, atau celah SQL Injection (SQLi) dapat mengeksekusi query tanpa klausul WHERE, sehingga mengekspos seluruh data milik tenant lain kepada entitas yang tidak berhak.
Masalah mendasar kedua pada instalasi standar PostgreSQL adalah keterbatasan fungsi pencatatan log bawaan (default logging). Secara default, engine PostgreSQL hanya mencatat error koneksi, query yang mengalami batas waktu (timeout), atau statement yang memicu kegagalan sistem. Engine tidak mencatat aktivitas eksekusi query harian yang berhasil seperti SELECT, UPDATE, INSERT, atau DELETE pada objek-objek sensitif.
Ketiadaan jejak audit granular ini menimbulkan titik buta (blind spot) dalam analisis keamanan jaringan. Ketika terjadi insiden pencurian data atau eskalasi hak akses internal (insider threat), tim SecOps dan Database Administrator (DBA) tidak memiliki sarana forensik untuk mengidentifikasi akun mana yang mengambil data, kapan transaksi diubah, atau query mana yang digunakan untuk mengekstraksi informasi. Hal ini berimbas langsung pada kegagalan pemenuhan standar kepatuhan regulasi industri seperti ISO/IEC 27001, SOC 2 Type II, HIPAA, serta Regulasi Perlindungan Data Pribadi (UU PDP / GDPR).
Implementasi Row Level Security (RLS) PostgreSQL
Row Level Security (RLS) adalah fitur bawaan engine PostgreSQL yang diperkenalkan sejak versi 9.5. RLS mengintersepsi seluruh query di tingkat parser dan rewriter engine database. Engine mengevaluasi aturan akses (policy) secara otomatis pada tiap baris (row) tabel sebelum mengembalikan hasil ke aplikasi. Dengan RLS, meskipun aplikasi secara tidak sengaja mengirimkan query SELECT * FROM transactions; tanpa filter WHERE, database hanya mengembalikan baris data yang berhak diakses oleh konteks sesi pengguna yang aktif.
Langkah 1: Membuat Database, Role Aplikasi, dan Tabel Schema
Langkah awal dimulai dengan membuat konteks database terpisah, role non-superuser untuk koneksi aplikasi backend, dan tabel sampel transaksi. Hindari penggunaan akun postgres atau superuser lain untuk operasional harian aplikasi.
-- Login ke psql sebagai superuser postgres
CREATE DATABASE tenant_db;
\c tenant_db
-- Buat role aplikasi non-superuser tanpa hak akses membuat database/role
CREATE ROLE app_user WITH LOGIN PASSWORD 'PasswordSuperAman123!';
-- Buat tabel transaksi multi-tenant
CREATE TABLE transactions (
id SERIAL PRIMARY KEY,
tenant_id VARCHAR(50) NOT NULL,
amount NUMERIC(12, 2) NOT NULL,
description TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Berikan hak akses manipulasi data pada tabel dan sequence ke role app_user
GRANT ALL ON TABLE transactions TO app_user;
GRANT USAGE, SELECT ON SEQUENCE transactions_id_seq TO app_user;
Langkah 2: Mengisi Data Sampel Multi-Tenant
Eksekusi sintaks berikut untuk memasukkan sampel data dari dua tenant berbeda, yaitu tenant_a dan tenant_b:
INSERT INTO transactions (tenant_id, amount, description) VALUES
('tenant_a', 150000.00, 'Pembayaran Tagihan Layanan Tenant A'),
('tenant_a', 300000.00, 'Pembelian Lisensi Tahunan Tenant A'),
('tenant_b', 500000.00, 'Pembayaran Invoice Konsultasi Tenant B');
Langkah 3: Mengaktifkan RLS dan Menyusun Policy Isolasi
Secara default, RLS pada tabel baru berstatus nonaktif. Fitur harus diaktifkan secara eksplisit menggunakan instruksi ALTER TABLE. Selanjutnya, buat security policy yang memanfaatkan custom session variable (app.current_tenant_id).
-- Aktifkan mekanisme RLS pada tabel transactions
ALTER TABLE transactions ENABLE ROW LEVEL SECURITY;
-- Buat policy isolasi tenant untuk operasi SELECT, INSERT, UPDATE, dan DELETE
CREATE POLICY tenant_isolation_policy ON transactions
FOR ALL
TO app_user
USING (tenant_id = current_setting('app.current_tenant_id', true))
WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true));
Penjelasan teknis klausa policy:
- FOR ALL: Menerapkan policy pada seluruh operasi manipulasi data (
SELECT,INSERT,UPDATE,DELETE). - TO app_user: Membatasi penerapan policy hanya untuk role
app_user. - USING: Menentukan baris data yang dapat dibaca (read) atau diubah (update/delete). Fungsi
current_setting('app.current_tenant_id', true)membaca variabel sesi yang ditetapkan oleh koneksi aplikasi. - WITH CHECK: Memvalidasi data baru yang dimasukkan (
INSERT) atau diperbarui (UPDATE). Jikatenant_idpada data baru tidak cocok dengan variabel sesi aktif, operasi dibatalkan dengan error penolakan.
Langkah 4: Pengujian dan Verifikasi Isolasi Data
Uji isolasi data dengan melakukan simulasi koneksi sebagai pengguna aplikasi app_user, lalu atur variabel sesi sebelum menjalankan query.
-- Pindah koneksi sebagai app_user
\c tenant_db app_user
-- Konfigurasi identitas tenant pada sesi aktif
SET LOCAL app.current_tenant_id = 'tenant_a';
-- Query seluruh data tanpa klausa WHERE manual
SELECT * FROM transactions;
-- Hasil: Hanya mencantumkan 2 baris milik tenant_a
-- Uji coba pelanggaran policy (mencoba memasukkan data tenant_b saat sesi tenant_a)
INSERT INTO transactions (tenant_id, amount, description)
VALUES ('tenant_b', 900000.00, 'Percobaan Bypass Data Tenant B');
-- Hasil: ERROR: new row violates row-level security policy for table "transactions"
Konfigurasi Extension pgaudit di Linux
Meskipun RLS berhasil mengisolasi data, pemantauan dan akuntabilitas aktivitas database tetap memerlukan audit logging yang komprehensif. Extension pgaudit (PostgreSQL Audit Extension) menyediakan pencatatan audit log terperinci yang memenuhi persyaratan standar keamanan publik dan privat, dengan mencatat statement SQL terkonfigurasi ke log standar OS.
Langkah 1: Instalasi Package pgaudit di Operating System
Sesuaikan proses instalasi paket pgaudit dengan versi PostgreSQL dan distribusi Linux yang digunakan.
Untuk Ubuntu / Debian (PostgreSQL 15):
sudo apt-get update
sudo apt-get install -y postgresql-15-pgaudit
Untuk RHEL / Rocky Linux / AlmaLinux (PostgreSQL 15):
sudo dnf install -y pgaudit15_15
Langkah 2: Konfigurasi Parameter postgresql.conf
Modifikasi file konfigurasi utama database agar modul pgaudit dimuat ke memori saat engine booting.
sudo nano /etc/postgresql/15/main/postgresql.conf
Tambahkan atau perbarui blok konfigurasi berikut pada file postgresql.conf:
# Muat shared library pgaudit saat startup
shared_preload_libraries = 'pgaudit'
# Set kelas aktivitas yang dicatat dalam log
pgaudit.log = 'read, write, ddl, role'
# Hemat ruang disk dengan tidak mencatat skema katalog internal
pgaudit.log_catalog = off
# Catat parameter query (misal nilai variabel pada prepared statement)
pgaudit.log_parameter = on
# Catat nama relasi/tabel yang terlibat dalam statement
pgaudit.log_relation = on
# Catat statement berulang secara lengkap
pgaudit.log_statement_once = off
# Konfigurasi format file log PostgreSQL
logging_collector = on
log_directory = 'log'
log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'
log_line_prefix = '%m [%p] %q%u@%d '
Langkah 3: Restart Service PostgreSQL
Parameter shared_preload_libraries membutuhkan restart penuh pada service database agar engine dapat mengunggah pustaka biner pgaudit.
sudo systemctl restart postgresql
Langkah 4: Aktivasi Extension pada Database Target
Setelah service berjalan kembali, aktifkan extension pgaudit di dalam database target menggunakan hak akses superuser.
-- Login sebagai superuser
psql -d tenant_db -U postgres
-- Buat extension pgaudit
CREATE EXTENSION pgaudit;
Langkah 5: Verifikasi Output Audit Log
Jalankan query baca atau tulis pada database untuk menguji pencatatan log audit:
-- Jalankan query audit test
SELECT * FROM transactions WHERE tenant_id = 'tenant_a';
Pantau output berkas log di sistem operasi Linux melalui perintah tail:
sudo tail -f /var/log/postgresql/postgresql-15-main.log | grep AUDIT
Format log audit yang dihasilkan akan menampilkan detail terperinci seperti berikut:
2026-03-30 10:15:22.123 WIB [12345] app_user@tenant_db AUDIT: SESSION,1,1,READ,SELECT,TABLE,public.transactions,"SELECT * FROM transactions WHERE tenant_id = 'tenant_a';",<not logged>
Mencegah Bypass Policy dan Rotasi Audit Log
Implementasi standar RLS dan pgaudit memerlukan penyesuaian tingkat lanjut (hardening) untuk menutupi celah keamanan sekunder dan menjaga kestabilitas ruang penyimpanan disk.
Mencegah Superuser dan Table Owner Bypass Policy RLS
Secara default pada PostgreSQL, pengguna bertipe superuser dan pemilik tabel (table owner) memiliki hak bawaan untuk melewati (bypass) seluruh batasan RLS. Jika kredensial table owner digunakan oleh aplikasi backend, aturan RLS tidak akan berefek.
Untuk memaksakan agar pemilik tabel juga wajib tunduk pada aturan RLS, manfaatkan instruksi FORCE ROW LEVEL SECURITY:
-- Paksa pemilik tabel untuk mematuhi aturan RLS
ALTER TABLE transactions FORCE ROW LEVEL SECURITY;
Mitigasi Kebocoran Variabel Sesi pada Connection Pooling
Dalam arsitektur modern yang menggunakan middleware connection pooler seperti PgBouncer dengan transaction pooling mode, penggunaan SET LOCAL app.current_tenant_id sangat krusial. Perintah SET LOCAL memastikan variabel sesi hanya berlaku di dalam batas transaksi yang sedang berjalan (BEGIN ... COMMIT) dan otomatis dibersihkan saat transaksi selesai. Jangan gunakan SET biasa tanpa LOCAL, karena nilai variabel dapat tertinggal di koneksi fisik pendukung dan mencemari permintaan tenant lain yang meminjam koneksi sama.
Mengatur Rotasi Log Audit dengan Utilities Logrotate
Aktivasi pgaudit.log = 'read, write' pada database berintensitas tinggi akan menghasilkan volume file log yang sangat besar. Tanpa pengelolaan yang tepat, partisi disk OS dapat penuh dan memicu penghentian mendadak pada engine PostgreSQL.
Buat berkas konfigurasi logrotate khusus untuk log PostgreSQL di Linux:
sudo nano /etc/logrotate.d/postgresql-audit
Masukkan spesifikasi rotasi berkas log sebagai berikut:
/var/log/postgresql/*.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
create 0640 postgres postgres
postrotate
systemctl reload postgresql > /dev/null 2>&1 || true
endscript
}
Penyelesaian Masalah (Troubleshooting) Permasalahan RLS
Terdapat dua kendala umum saat pengimplementasian RLS di lingkungan produksi:
- Masalah 1: Query Mengembalikan 0 Row (Hasil Kosong). Penyebab utama adalah variabel sesi
app.current_tenant_idbelum diinisialisasi sebelum query dieksekusi. Jika fungsicurrent_setting('app.current_tenant_id')dipanggil tanpa argumen keduatrue, PostgreSQL akan melemparkan runtime error. Menggunakancurrent_setting('app.current_tenant_id', true)mengembalikan nilaiNULLjika belum diisi, sehingga perbandingantenant_id = NULLmengevaluasi keFALSEdan aman dari kebocoran data. - Masalah 2: Error Permission Denied pada Sequence. Saat melakukan
INSERTpada tabel berkolomSERIALatauBIGSERIAL, pengguna butuh akses ke sequence generator. Jika hak akses ini terlewat, eksekusi queryINSERTditolak. Solusi: jalankan perintahGRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_user;.
Checklist Security Hardening PostgreSQL
Gunakan daftar periksa berikut untuk memastikan seluruh parameter keamanan database telah dikonfigurasi dengan benar sebelum diterapkan ke lingkungan produksi:
| Fokus Keamanan | Tindakan Hardening | Sintaks / Parameter Verifikasi |
|---|---|---|
| Isolasi Multi-tenant | Aktifkan RLS dan paksa penguncian owner | ALTER TABLE ... FORCE ROW LEVEL SECURITY; |
| Hak Akses Pengguna | Terapkan Principle of Least Privilege | Role aplikasi non-superuser & tanpa CREATEDB |
| Audit Logging Query | Aktifkan pgaudit pada shared libraries | shared_preload_libraries = 'pgaudit' |
| Manajemen Disk Storage | Konfigurasi Logrotate & matikan log catalog | pgaudit.log_catalog = off & retensi 30 hari |
| Isolasi Connection Pooler | Gunakan pembatasan transaksi lokal | SET LOCAL app.current_tenant_id = '...'; |
Kesimpulan
Strategi hardening pada database PostgreSQL memerlukan kombinasi antara mekanisme pencegahan aktif dan pemantauan pasif. Penggunaan Row Level Security (RLS) memberikan proteksi defensif di layer engine yang menjamin isolasi data multi-tenant tidak bergantung pada kerapian kueri aplikasi. Sementara itu, integrasi extension pgaudit memberikan visibilitas penuh terhadap setiap aktivitas transaksi data, memungkinkan identifikasi potensi anomali secara real-time sekaligus memenuhi standar audit kepatuhan industri.
Kombinasi kedua teknologi ini membentuk fondasi defense-in-depth yang kokoh pada tingkatan arsitektur data. Selalu lakukan uji validasi policy RLS dan simulasi failover logrotate di lingkungan staging sebelum mendistribusikan konfigurasi ini ke kluster produksi.


