Pertahanan di chapter-chapter sebelumnya bekerja dengan asumsi yang jarang diucapkan: role yang menjalankan query tidak punya hak lebih dari yang dibutuhkannya. Prepared statement mengunci struktur query — tetapi bug tetap bisa lolos, fungsi lama tetap bisa lupa parameter, dan saat itu terjadi, kerusakan yang terjadi persis sebesar hak yang dipegang role. Chapter penutup section ini membangun asumsi itu secara sadar lewat prinsip least privilege: setiap pelaku — aplikasi, pelaporan, admin migrasi — hanya menerima hak paling kecil yang tetap menyelesaikan pekerjaannya, tidak satu privilege lebih. Kita akan merancang tiga role untuk satu aplikasi, membuktikan batas-batasnya dijalankan oleh server, memisahkan urusan privilege dari kepemilikan, lalu menutup dengan pola transisi yang bersih dan daftar periksa pengerasan untuk server produksi.
Prinsip: Hak Paling Kecil yang Menyelesaikan Pekerjaan #
Alasan prinsip ini berdiri paling awal di daftar pengerasan bukan doktrin, melainkan matematika kegagalan: probabilitas aplikasi dibobol mungkin kecil, tetapi dampaknya dikalikan hak aksesnya. Chapter SQL Injection
sudah memperlihatkan versi mini dari perkalian itu — role baca yang mencoba DROP TABLE tertahan oleh must be owner of table. Least privilege memperluas pola yang sama ke seluruh permukaan: aplikasi utama tidak butuh DELETE jika aturan bisnisnya penghapusan lunak; laporan mingguan tidak butuh hak tulis; migrasi skema tidak butuh hak permanen. Setiap privilege yang tidak dibagikan adalah serangan yang gagal bahkan sebelum mencapai bug.
Tiga Role untuk Satu Aplikasi #
Pola yang paling sering dipakai tim membagi aplikasi menjadi tiga role kelompok: app_baca_um untuk modul pelaporan, app_tulis_um untuk modul transaksi, dan app_ddl_um untuk migrasi skema yang dijalankan terbatas. File pembuka membuat ketiganya beserta haknya — perhatikan bahwa hanya dua role pertama yang menerima USAGE pada schema; role DDL sengaja tidak diberi apa-apa di sini karena kepemilikannya akan dibahas terpisah:
-- Tiga role untuk satu aplikasi: baca, tulis, dan DDL dipisah.
DROP ROLE IF EXISTS app_baca_um;
DROP ROLE IF EXISTS app_tulis_um;
DROP ROLE IF EXISTS app_ddl_um;
-- Role kelompok tanpa LOGIN: aplikasi akan memakainya lewat
-- GRANT ke role login, atau langsung dijadikan role login
-- saat dipasang di server produksi.
CREATE ROLE app_baca_um NOLOGIN;
CREATE ROLE app_tulis_um NOLOGIN;
CREATE ROLE app_ddl_um NOLOGIN;
CREATE TABLE produk (
id integer PRIMARY KEY,
nama text NOT NULL,
harga numeric(10,2) NOT NULL
);
INSERT INTO produk (id, nama, harga) VALUES
(1, 'kopi tubruk', 25000.00),
(2, 'teh melati', 18000.00),
(3, 'gula aren', 32000.00);
-- USAGE pada schema adalah pintu masuk; tanpanya hak atas
-- tabel tidak pernah terjangkau.
GRANT USAGE ON SCHEMA public TO app_baca_um, app_tulis_um;
GRANT SELECT ON produk TO app_baca_um;
GRANT SELECT, INSERT, UPDATE ON produk TO app_tulis_um;
-- Rekap PG15: CREATE pada schema public sudah tidak lagi milik
-- PUBLIC -- hanya pg_database_owner (pemilik database).
SELECT nspname, nspacl
FROM pg_namespace
WHERE nspname = 'public';
SELECT has_schema_privilege('app_tulis_um', 'public', 'CREATE') AS tulis_create,
has_schema_privilege('app_tulis_um', 'public', 'USAGE') AS tulis_usage;Output yang Dihasilkan:
DROP ROLE
DROP ROLE
DROP ROLE
CREATE ROLE
CREATE ROLE
CREATE ROLE
CREATE TABLE
INSERT 0 3
GRANT
GRANT
GRANT
nspname | nspacl
---------+--------------------------------------------------------------------------------------------------------------------------------
public | {pg_database_owner=UC/pg_database_owner,=U/pg_database_owner,app_baca_um=U/pg_database_owner,app_tulis_um=U/pg_database_owner}
(1 row)
tulis_create | tulis_usage
--------------+-------------
f | t
(1 row)Tiga keputusan desain di file itu layak dibedah:
NOLOGINpada ketiga role — role ini adalah kelompok hak, bukan akun koneksi. Aplikasi produksi nanti memakai role login yang diberi keanggotaan grup (GRANT app_tulis_um TO app_web), sehingga hak bisa dinaikkan-turunkan tanpa menyentuh konfigurasi aplikasi.USAGEsebelum hak tabel —USAGEpada schema adalah pintu masuk; tanpanyaSELECTpadaproduktidak pernah terjangkau, sebagaimana dilatih di chapter Schema Privileges .- Rekap PG15 pada
nspacl— ACL schemapublickini hanya memberiU(USAGE) untuk umum danUCuntukpg_database_owner; kolomtulis_createbernilaifmembuktikan role aplikasi tidak bisa membuat objek di schema public — perubahan bawaan sejak PostgreSQL 15 yang menutup lubang lama.
Menguji Batas: Baca dan Tulis #
Batas least privilege bukan dokumentasi — server menegakkannya pada setiap perintah. Kita uji dari balik SET ROLE, pola yang dipakai sepanjang section ini:
-- Role baca: SELECT berhasil, INSERT ditolak.
SET ROLE app_baca_um;
SELECT count(*) AS produk_terbaca FROM produk;
-- Aplikasi pelaporan tidak butuh hak tulis -- dan server
-- memastikan hal itu secara harfiah.
INSERT INTO produk (id, nama, harga) VALUES (4, 'roti sobek', 12000.00);Output yang Dihasilkan:
psql:uji-baca.sql:8: ERROR: permission denied for table produk-- Role tulis: INSERT dan UPDATE berhasil, DELETE ditolak.
SET ROLE app_tulis_um;
INSERT INTO produk (id, nama, harga) VALUES (4, 'roti sobek', 12000.00);
UPDATE produk SET harga = 26000.00 WHERE id = 1;
SELECT id, nama, harga FROM produk ORDER BY id;
-- DELETE memang tidak diberikan: korupsi data oleh bug aplikasi
-- tidak bisa berakhir pada penghapusan massal.
DELETE FROM produk WHERE id = 4;Output yang Dihasilkan:
psql:uji-tulis.sql:12: ERROR: permission denied for table produkDua file itu memperlihatkan pola penolakan yang sama dua tingkat. Role baca berhasil menjalankan SELECT (tiga produk) lalu ditolak mentah pada INSERT: permission denied for table produk. Role tulis berhasil menjalankan INSERT dan UPDATE — query penutupnya memperlihatkan roti sobek masuk dan kopi tubruk naik harga — tetapi berhenti pada DELETE dengan error yang sama: hak itu memang tidak pernah dibagikan. Memotong DELETE dari role aplikasi adalah keputusan yang murah dan berdampak besar: bug penghapusan massal yang sesekali menghantala industri tidak akan menemukan apa pun untuk dihapus.
Privilege Bukan Kepemilikan #
Pemilik objek berdiri di kelas tersendiri: bisa ALTER, bisa DROP, bisa mengatur ulang GRANT — hak yang tidak bisa diberikan lewat GRANT biasa sekalipun. Konsekuensinya dua arah dan sama pentingnya untuk keamanan:
-- Privilege bukan kepemilikan: pemilik bisa DROP tabelnya,
-- pemegang privilege lengkap sekalipun tidak.
CREATE TABLE catatan_app (
id integer PRIMARY KEY,
catatan text NOT NULL
);
ALTER TABLE catatan_app OWNER TO app_ddl_um;
-- app_ddl_um adalah pemilik: DROP berhasil tanpa satu GRANT pun.
SET ROLE app_ddl_um;
DROP TABLE catatan_app;
RESET ROLE;
SELECT to_regclass('catatan_app') AS objek_tersisa;
-- Kontras: app_tulis_um memegang SELECT, INSERT, UPDATE
-- pada produk -- tetapi bukan pemiliknya.
SET ROLE app_tulis_um;
DROP TABLE produk;Output yang Dihasilkan:
psql:ownership.sql:23: ERROR: must be owner of table produkBagian pertama membuktikan sisi pemilik: app_ddl_um menjatuhkan catatan_app tanpa satu GRANT pun atas tabel itu — kepemilikan membawa seluruh haknya sendiri, dan objek_tersisa yang bernilai NULL mengonfirmasi tabelnya lenyap. Bagian kedua adalah kebalikannya: app_tulis_um yang memegang tiga privilege atas produk ditolak dengan must be owner of table produk. Desain yang sehat karena itu memisahkan kepemilikan dari operasional: tabel dimiliki role DDL (atau role deploy khusus), aplikasi hanya menerima privilege DML — persis struktur yang kita pasang di file pembuka.
DEFAULT PRIVILEGES untuk Tim #
Tabel baru dibuat setiap sprint, dan GRANT manual untuk setiap tabel adalah pekerjaan yang pasti terlewat. Solusinya sudah dikenal dari chapter Schema Privileges
: ALTER DEFAULT PRIVILEGES memasang grant yang berlaku otomatis untuk objek-objek baru buatan pemilik tertentu:
-- DEFAULT PRIVILEGES: tabel baru buatan postgres otomatis
-- memberi SELECT kepada role baca.
ALTER DEFAULT PRIVILEGES
FOR ROLE postgres
IN SCHEMA public
GRANT SELECT ON TABLES TO app_baca_um;
CREATE TABLE log_harga (
id integer PRIMARY KEY,
produk_id integer NOT NULL,
harga_baru numeric(10,2) NOT NULL
);
-- Audit hak efektif: tabel lama (GRANT manual) dan tabel baru
-- (DEFAULT PRIVILEGES) keduanya terbaca oleh app_baca_um.
SELECT grantee, table_name, privilege_type
FROM information_schema.role_table_grants
WHERE table_name IN ('produk', 'log_harga')
AND grantee LIKE 'app_%'
ORDER BY grantee, table_name, privilege_type;
SET ROLE app_baca_um;
SELECT count(*) AS log_terbaca FROM log_harga;
RESET ROLE;Output yang Dihasilkan:
ALTER DEFAULT PRIVILEGES
CREATE TABLE
grantee | table_name | privilege_type
--------------+------------+----------------
app_baca_um | log_harga | SELECT
app_baca_um | produk | SELECT
app_tulis_um | produk | INSERT
app_tulis_um | produk | SELECT
app_tulis_um | produk | UPDATE
(5 rows)
SET
log_terbaca
-------------
0
(1 row)
RESETQuery audit di tengah file adalah alat yang layak dijadikan rutinitas: information_schema.role_table_grants mendaftar hak efektif per tabel dan per penerima, dan dengan ORDER BY tiga kolom hasilnya deterministik — cocok dibandingkan antar-lingkungan atau dipasang di pipeline audit. Hasilnya di sini: log_harga — yang dibuat setelah aturan default — menerima app_baca_um SELECT otomatis di samping grant manual produk; dan dari balik SET ROLE, SELECT count(*) atas tabel baru itu berhasil tanpa satu GRANT tambahan. Perhatikan pula FOR ROLE postgres pada aturannya: default privileges terikat pemilik yang membuat objek, jadi tim dengan banyak pemilik perlu satu aturan per pemilik — atau satu peran deploy tunggal yang memang menjadi sumber semua objek produksi.
Transisi dan Pembersihan #
Role berhenti dipakai — kontrak selesai, layanan dihentikan — dan kebersihan cluster menuntut objek tanggungannya diselesaikan lebih dulu, sebagaimana dilatih di chapter Membuat dan Menghapus Role . File penutup menjalankan alur lengkapnya untuk ketiga role latihan:
-- Transisi kepemilikan dan pembersihan tuntas seluruh role latihan.
CREATE TABLE arsip_um (
id integer PRIMARY KEY
);
ALTER TABLE arsip_um OWNER TO app_ddl_um;
-- Alur pembersih peran DDL: kepemilikan dipindahkan dulu,
-- sisa hak dilepas, baru role boleh dihapus.
REASSIGN OWNED BY app_ddl_um TO postgres;
DROP OWNED BY app_ddl_um;
DROP ROLE app_ddl_um;
DROP OWNED BY app_baca_um, app_tulis_um;
DROP ROLE app_baca_um;
DROP ROLE app_tulis_um;
SELECT count(*) AS sisa_role_app
FROM pg_roles
WHERE rolname LIKE 'app_%_um';
-- Tabel arsip tetap hidup di bawah pemilik barunya.
SELECT tableowner
FROM pg_tables
WHERE tablename = 'arsip_um';Output yang Dihasilkan:
CREATE TABLE
ALTER TABLE
REASSIGN OWNED
DROP OWNED
DROP ROLE
DROP OWNED
DROP ROLE
DROP ROLE
sisa_role_app
---------------
0
(1 row)
tableowner
------------
postgres
(1 row)Urutan tiga langkah untuk app_ddl_um adalah intinya: REASSIGN OWNED memindahkan arsip_um ke postgres sehingga tabel tetap hidup, DROP OWNED melepas sisa privilege yang menempel, dan baru kemudian DROP ROLE berhasil. Dua role lain cukup DROP OWNED + DROP ROLE karena tidak memiliki objek. Dua query penutup memverifikasi hasil akhirnya: sisa_role_app bernilai 0 dan tableowner menunjukkan postgres — cluster kembali bersih tanpa kehilangan satu baris data. Untuk role yang meninggalkan jejak di beberapa database, jalankan REASSIGN OWNED dan DROP OWNED di setiap database itu sebelum DROP ROLE.
Daftar Periksa Pengerasan Produksi #
| Periksa | Bentuk minimal yang sehat |
|---|---|
| Role khusus per aplikasi | Satu role login per aplikasi/modul; tanpa berbagi akun lintas tim |
| Tanpa superuser untuk aplikasi | Aplikasi dan laporan terhubung sebagai role biasa, bukan pemilik cluster |
| Privilege bertingkat | SELECT untuk pelaporan; SELECT/INSERT/UPDATE untuk transaksi; DELETE hanya bila aturan bisnis menuntut |
| Kepemilikan terpisah dari operasional | Objek dimiliki role deploy/DDL; aplikasi hanya pemegang privilege |
USAGE schema dievaluasi | Role aplikasi tidak memegang CREATE di schema bersama |
| Default privileges terpasang | Tabel baru otomatis memberi hak sesuai peran; audit lewat information_schema.role_table_grants |
| RLS untuk data lintas tenant | Chapter Policy RLS diterapkan bila satu tabel memuat data banyak tenant |
| Authentication dan jalur | scram-sha-256 di pg_hba.conf, hostssl untuk TCP, sslmode=verify-full di aplikasi |
| Prepared statement di seluruh jalur | Semua query berparameter; escaping hanya untuk bagian struktural |
| Siklus hidup role diurus | REASSIGN OWNED/DROP OWNED/DROP ROLE setiap kali layanan pensiun |
Daftar itu merangkum seluruh section dalam sepuluh baris — role dari chapter pertama, privilege dan RLS di tengah, authentication dan TLS sebagai gerbang, prepared statement sebagai isi, dan least privilege sebagai fondasi. Dengan fondasi selesai, pengelolaan berpindah ke wilayah yang berbeda: menjaga server tetap hidup dan sehat dari hari ke hari. Section berikutnya membuka persis dari situ — konfigurasi server, log, dan pemeliharaan rutin sehari-hari seorang administrator PostgreSQL.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
default-privileges.sql
Unduh berkas-- DEFAULT PRIVILEGES: tabel baru buatan postgres otomatis
-- memberi SELECT kepada role baca.
ALTER DEFAULT PRIVILEGES
FOR ROLE postgres
IN SCHEMA public
GRANT SELECT ON TABLES TO app_baca_um;
CREATE TABLE log_harga (
id integer PRIMARY KEY,
produk_id integer NOT NULL,
harga_baru numeric(10,2) NOT NULL
);
-- Audit hak efektif: tabel lama (GRANT manual) dan tabel baru
-- (DEFAULT PRIVILEGES) keduanya terbaca oleh app_baca_um.
SELECT grantee, table_name, privilege_type
FROM information_schema.role_table_grants
WHERE table_name IN ('produk', 'log_harga')
AND grantee LIKE 'app_%'
ORDER BY grantee, table_name, privilege_type;
SET ROLE app_baca_um;
SELECT count(*) AS log_terbaca FROM log_harga;
RESET ROLE;
ownership.sql
Unduh berkas-- Privilege bukan kepemilikan: pemilik bisa DROP tabelnya,
-- pemegang privilege lengkap sekalipun tidak.
CREATE TABLE catatan_app (
id integer PRIMARY KEY,
catatan text NOT NULL
);
ALTER TABLE catatan_app OWNER TO app_ddl_um;
-- app_ddl_um adalah pemilik: DROP berhasil tanpa satu GRANT pun.
SET ROLE app_ddl_um;
DROP TABLE catatan_app;
RESET ROLE;
SELECT to_regclass('catatan_app') AS objek_tersisa;
-- Kontras: app_tulis_um memegang SELECT, INSERT, UPDATE
-- pada produk -- tetapi bukan pemiliknya.
SET ROLE app_tulis_um;
DROP TABLE produk;
tiga-role.sql
Unduh berkas-- Tiga role untuk satu aplikasi: baca, tulis, dan DDL dipisah.
DROP ROLE IF EXISTS app_baca_um;
DROP ROLE IF EXISTS app_tulis_um;
DROP ROLE IF EXISTS app_ddl_um;
-- Role kelompok tanpa LOGIN: aplikasi akan memakainya lewat
-- GRANT ke role login, atau langsung dijadikan role login
-- saat dipasang di server produksi.
CREATE ROLE app_baca_um NOLOGIN;
CREATE ROLE app_tulis_um NOLOGIN;
CREATE ROLE app_ddl_um NOLOGIN;
CREATE TABLE produk (
id integer PRIMARY KEY,
nama text NOT NULL,
harga numeric(10,2) NOT NULL
);
INSERT INTO produk (id, nama, harga) VALUES
(1, 'kopi tubruk', 25000.00),
(2, 'teh melati', 18000.00),
(3, 'gula aren', 32000.00);
-- USAGE pada schema adalah pintu masuk; tanpanya hak atas
-- tabel tidak pernah terjangkau.
GRANT USAGE ON SCHEMA public TO app_baca_um, app_tulis_um;
GRANT SELECT ON produk TO app_baca_um;
GRANT SELECT, INSERT, UPDATE ON produk TO app_tulis_um;
-- Rekap PG15: CREATE pada schema public sudah tidak lagi milik
-- PUBLIC -- hanya pg_database_owner (pemilik database).
SELECT nspname, nspacl
FROM pg_namespace
WHERE nspname = 'public';
SELECT has_schema_privilege('app_tulis_um', 'public', 'CREATE') AS tulis_create,
has_schema_privilege('app_tulis_um', 'public', 'USAGE') AS tulis_usage;
transisi-bersih.sql
Unduh berkas-- Transisi kepemilikan dan pembersihan tuntas seluruh role latihan.
CREATE TABLE arsip_um (
id integer PRIMARY KEY
);
ALTER TABLE arsip_um OWNER TO app_ddl_um;
-- Alur pembersih peran DDL: kepemilikan dipindahkan dulu,
-- sisa hak dilepas, baru role boleh dihapus.
REASSIGN OWNED BY app_ddl_um TO postgres;
DROP OWNED BY app_ddl_um;
DROP ROLE app_ddl_um;
DROP OWNED BY app_baca_um, app_tulis_um;
DROP ROLE app_baca_um;
DROP ROLE app_tulis_um;
SELECT count(*) AS sisa_role_app
FROM pg_roles
WHERE rolname LIKE 'app_%_um';
-- Tabel arsip tetap hidup di bawah pemilik barunya.
SELECT tableowner
FROM pg_tables
WHERE tablename = 'arsip_um';
uji-baca.sql
Unduh berkas-- Role baca: SELECT berhasil, INSERT ditolak.
SET ROLE app_baca_um;
SELECT count(*) AS produk_terbaca FROM produk;
-- Aplikasi pelaporan tidak butuh hak tulis -- dan server
-- memastikan hal itu secara harfiah.
INSERT INTO produk (id, nama, harga) VALUES (4, 'roti sobek', 12000.00);
uji-tulis.sql
Unduh berkas-- Role tulis: INSERT dan UPDATE berhasil, DELETE ditolak.
SET ROLE app_tulis_um;
INSERT INTO produk (id, nama, harga) VALUES (4, 'roti sobek', 12000.00);
UPDATE produk SET harga = 26000.00 WHERE id = 1;
SELECT id, nama, harga FROM produk ORDER BY id;
-- DELETE memang tidak diberikan: korupsi data oleh bug aplikasi
-- tidak bisa berakhir pada penghapusan massal.
DELETE FROM produk WHERE id = 4;