Multi user
Unix Multiuser
Sebagian besar dari kita, apalagi para programer dan sysadmin Web
Unix, pasti sudah cukup familiar dengan software ini. MySQL—yang pernah
dibahas di edisi 4 mwmag—merupakan server RDBMS open source terpopuler,
terutama untuk aplikasi Web. Artikel kali ini akan membahas isu-isu
keamanan seputar menjalankan daemon MySQL di lingkungan multiuser,
seperti di server shared hosting. Dalam lingkungan multiuser ada
beberapa hal yang perlu diperhatikan, terutama yang berkaitan dengan
isolasi user yang satu dengan user lain. Meskipun ada pula sebagian
poin-poin yang diterangkan—seperti pengaturan permission MySQL—yang
dapat diterapkan untuk Windows, namun jelas fokus artikel ini adalah
pada lingkungan Unix/Linux.
Kita langsung mulai saja dengan tip yang pertama. Jika Anda punya
budget cukup, pasanglah daemon MySQL (mysqld) di mesin terpisah. Cara
ini adalah termasuk yang paling aman, karena mesin terpisah ini dapat
kita taruh di belakang firewall sehingga tidak menerima koneksi dari
Internet melainkan dari mesin-mesin tertentu saja di jaringan lokal.
Juga, tiap user yang menggunakan database tidak perlu kita beri akses
shell. Semua port dapat kita tutup kecuali port default mysqld saja
(3306) yang kita buka. Tidak ada yang bisa menyentuh langsung file-file
database maupun log kecuali user
mysql di mesin database tersebut, dan juga admin mesin tersebut.
Meskipun MySQL merupakan database multiuser, tapi daemon
mysqld hanya berjalan sebagai satu user Unix saja (defaultnya adalah
mysql).
Tidak ada wrapping seperti halnya pada Apache + suexec/cgiwrap. User
Unix ini memiliki akses ke semua database di direktori data (defaultnya
adalah
/var/lib/mysql). Tentu saja MySQL memiliki sistem access
privilege yang cukup mendetil untuk mengatur klien mana yang dapat
mengakses tabel atau database mana. Namun bug pada kode mysqld atau
miskonfigurasi sistem privilegenya bisa saja mengakibatkan sebuah
database berisi data rahasia terbuka bagi user MySQL lain. Misalnya
lewat
LOAD DATA INFILE atau
LOAD LOCAL DATA INFILE maupun lewat
SELECT INTO OUTFILE, yang memungkinkan penyerang berpotensi menimpa file database milik user lain.
Jika Anda mempunyai sebuah database yang datanya sensitif atau harus
benar-benar private, Anda bisa dapat menjalankan mysqld terpisah. Tiap
mysqld berjalan sebagai user Unix yang berbeda, sehingga satu mysqld
tidak dapat mengganggu mysqld yang lainnya. Bug pada mysqld yang satu
tidak dapat mengganggu mysqld lainnya, karena masing-masing tidak bisa
menyentuh direktori data daemon lain.
Contoh berikut adalah user
steven yang menjalankan sendiri daemon MySQL pribadi, berjalan sebagai user
steven dan port 3366. Daemon MySQL utama, sebagai user
mysql, tidak dapat menyentuh database.
$ mkdir /home/steven/mysql
$ mysql_install_db --datadir=/home/steven/mysql
$ /usr/sbin/mysqld --datadir=/home/steven/mysql --port=3366 \
--socket=/home/steven/mysql/mysql.sock &
Anda dapat mengganti port 3366 dengan port kesukaan sendiri. Sebelum
memakai, Anda perlu memberi password pada user MySQL root tentunya:
$ mysqladmin -u root -S /home/steven/mysql/mysql.sock password RAHASIA
Ganti
RAHASIA dengan password sebenarnya. Selanjutnya nanti sewaktu melakukan koneksi ke mysqld ini dengan command line client
mysql misalnya, Anda perlu menyebutkan opsi
-h (host) dan
-P (port) atau
-S (socket). Misalnya, koneksi via soket Unix:
$ mysql -u root -S /home/steven/mysql/mysql.sock
Atau koneksi via TCP:
$ mysql -u root -h 127.0.0.1 -P 3366
Tapi, lagi-lagi, jika jumlah user banyak dan masing-masing ingin
menjalankan mysqld-nya masing-masing, tentu saja overheadnya besar.
Sebagai seorang admin database, mau tidak mau hal yang satu ini harus
dikuasai baik-baik. Sistem Access Privilege adalah cara utama MySQL
dalam membatasi user dan host mana saja yang bisa melakukan koneksi,
menyimpan password masing-masing user, dan membatasi kemampuan user
dalam memanipulasi database (apakah hanya bisa melakukan
SELECT saja, atau
SELECT dan
UPDATE,
dst). Apa artinya daemon MySQL yang terisolasi dari mesin maupun daemon
lain apabila ia dengan cerobohnya menerima koneksi dari sembarang user
dan host?
Sistem privilege MySQL cukup mendetil dan menurut saya lebih simpel
tapi mendetil daripada sistem privilege PostgreSQL, meskipun dari segi
otentikasi sistem PostgreSQL sebetulnya lebih fleksibel. Pada dasarnya
seluruh informasi pengaturan privilege disimpan dalam sebuah database
khusus bernama
mysql. Penjelasan mendetil mengenai di luar
cakupan artikel ini, tapi sebagai tip beberapa hal berikut ini sebaiknya
diikuti. Pertama, sebaiknya tiap user diberi password. Kedua, tabel
user di database
mysql ini sangat sensitif karena berisi password, jadi jangan biarkan user manapun memiliki privilege
SELECT terhadap. Ketiga, batasi privilege
FILE maupun
PROCESS,
jangan berikan kecuali benar-benar perlu. Keempat, jangan biarkan satu
user database melihat database lain kecuali benar-benar perlu.
Dalam server shared hosting yang saya kelola, demi kesederhanaan,
tiap user database hanya boleh mengakses satu database, dan daftar
privilegenya seperti ini: Entri di tabel
user:
('localhost', 'USER', password('PASSWORD'), 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N', 'N'). Entri di tabel
db:
('localhost', 'DATABASE', 'USER', 'Y', 'Y', 'Y', 'Y', 'Y', 'Y', 'N', 'Y', 'Y', 'Y'). Artinya, si user
USER hanya dapat melihat database
DATABASE, hanya bisa melakukan koneksi dari mesin lokal melalui soket Unix, dan tidak memiliki privilege
GRANT. Privilege
FILE
diberikan, tapi berhubung tiap skrip CGI diwrap, maka setidaknya akan
lebih sulit bagi seorang penyerang untuk menciptakan file sebagai user
Unix lain.
Dalam sebuah server shared hosting misalnya, umumnya database diakses
dari skrip CGI/PHP/ASP di server yang sama. Jika kita ingin agar hanya
user atau skrip dari mesin lokal saja yang dapat melakukan koneksi ke
daemon MySQL maka kita dapat mematikan opsi listening di soket TCP.
Seperti kita ketahui, MySQL mendengarkan koneksi di soket Unix
(defaultnya di
/var/lib/mysql/mysql.sock) dan di port TCP
(defaultnya di port 3306). Soket Unix hanya untuk klien lokal, sementara
soket TCP dapat digunakan untuk koneksi jaringan.
Untuk membuat mysqld tidak mendengarkan di soket TCP sama sekali, taruh baris berikut di
/etc/my.cnf:
[mysqld]
skip-networking
lalu restart mysqld. Atau tambahkan
--skip-networking saat menjalankan
mysqld.
Setelah memakai opsi ini, bahkan koneksi ke
127.0.0.1 pun akan ditolak dengan pesan
Connection Refused. Karena memang tidak ada soket TCP yang mendengarkan. Jadi untuk melakukan koneksi ke daemon MySQL kita harus menggunakan host
localhost agar soket Unixlah yang dipakai sebagai metode koneksi.
Keuntungan menggunakan soket Unix, selain sedikit lebih cepat (karena
tidak ada overhead protokol TCP/IP), isolasi filesystem pun berlaku
sebagai lapisan pelindung. Database milik user
steven di contoh
Tip 2 tadi misalnya, tidak dapat dicapai sama sekali baik oleh klien di
mesin lain maupun oleh user Unix lain di mesin yang sama, karena
defaultnya lokasi soket Unix di
/home/steven/mysql/mysql.sock ini tertutup dari user lain. Sehingga database ini bersifat pribadi dan hanya bisa diakses oleh user
steven sendiri maupun oleh skrip CGI/PHP yang berjalan sebagai user
steven. Daemon MySQL utama, di lain pihak, merupakan daemon yang terbuka bagi semua user, karena
/var/lib/mysql/mysql.sock secara default dapat diakses oleh semua user.
Tentu saja, kadang-kadang database MySQL ingin diakses selain dari
localhost
(misalnya, ingin diakses dari klien GUI di komputer Windows). Dalam
kasus ini, mau tidak mau kita harus membuka lagi soket TCP dan kembali
mengandalkan sistem privilege MySQL dalam membatasi host.
Dalam lingkungan multiuser, tiap user semestinya diberi database
masing-masing dan database default yang diberikan oleh distribusi MySQL,
yaitu
test, biasanya tidak ada gunanya. Ada baiknya database
ini dihapus saja sehingga tidak sembarang user yang bisa mengakses
database ini dan lalu memenuhi disk atau melakukan serangan DOS
misalnya. Sebab defaultnya adalah semua user database bisa melihat
database
test. Untuk menghapus database
test, berikan perintah ini dari klien command line
mysql:
mysql> DELETE FROM db WHERE db='test' OR db='test_%';
mysql> FLUSH PRIVILEGES;
Chroot adalah fasilitas yang disediakan di banyak sistem operasi Unix
untuk membuat filesystem root “virtual” bagi program, misalnya di bawah
/home/steven/root, sehingga program menganggap path tersebut adalah
/ dan tidak bisa melihat path di atasnya (
/home/steven maupun
/home maupun
/
asli). Chroot banyak bermanfaat untuk memenjarakan program sehingga
tidak bisa menyentuh file-file di luar direktori yang sudah kita
tetapkan sebagai penjaranya.
Tip ini cukup berhubungan dengan tip 2 (tiap user memperoleh mysqld masing-masing). Di lingkungan virtual server pun
chroot sering dilakukan terhadap tiap-tiap daemon agar masing-masing tidak dapat melihat file milik daemon lain.
Chroot saat ini harus dilakukan sebagai root karena merupakan operasi
privileged. Menyambung contoh di tip 2, berikut ini cara menjalankan
mysqld milik user
steven dan memenjarakannya di
/home/steven/mysql.
mysqld memiliki opsi
--chroot sehingga kita tidak perlu mengkopi
/usr/sbin/mysqld-nya itu sendiri, namun terakhir saya coba masih bermasalah, sehingga kita akan melakukan chroot menggunakan
/usr/sbin/chroot. Contoh ini untuk RedHat Linux (saya memakai versi 7.2) dan distribusi RPM binary dari MySQL AB (saya memakai 3.23.51).
$ chdir /home/steven
$ mkdir -p fakeroot/{var/lib,etc,tmp,lib,usr/sbin,usr/share}
$ mv mysql fakeroot/var/lib/ # direktori data yg sudah dibuat di tip 2
$ echo -e "root:*:0:0:::\nnobody:*:99:99:::\nsteven:*:500:500:::" \
> fakeroot/etc/passwd
$ echo -e "root::0:\nsteven::500:" > fakeroot/etc/group
$ cp -a /lib/{ld-*,libc.so*,libc-*.so,libnsl*,libnss_files*,libtermcap*} \
fakeroot/lib/
$ cp -a /usr/sbin/mysqld fakeroot/usr/sbin/
$ cp -a /usr/share/mysql fakeroot/usr/share/
# /usr/sbin/chroot /home/steven/fakeroot /usr/sbin/mysqld \
--user=steven \
--datadir=/var/lib/mysql \
--port=3366 \
--socket=/var/lib/mysql/mysql.sock
Untuk instalasi seperti di atas, dibutuhkan overhead ruang disk
sebesar sekitar 12MB untuk mengkopi library Linux dan file-file lain
yang diperlukan ke bawah
/home/steven/fakeroot (karena jika
tidak, setelah dilakukan chroot maka mysqld tidak akan dapat mencari
library yang dibutuhkan). Kita juga memerlukan
/etc/passwd dan
/etc/group
“dummy”. Ganti UID dan GID 500 sesuai UID dan GID user yang akan
menjalankan mysqld. Setelah semua file-file siap, kita memanggil mysqld.
Perhatikan bahwa opsi-opsi path di
--datadir dan
--socket
adalah relatif terhadap fakeroot. Program-program yang berada di bawah
chroot yang sama akan melihat soket dan direktori data MySQL milik
steven ini di
/var/lib/mysql, tapi sebetulnya—seperti yang akan dilihat program-program lain yang berada di luar chroot—lokasi asli ada di
/home/steven/fakeroot/var/lib/mysql.
Chroot berguna terutama jika Anda tidak mempercayai MySQL versi
development atau beta (mis: versi 4.0.2 saat ini). Chroot juga
bermanfaat dalam menutup kemungkinan perintah SQL
LOAD DATA INFILE dan
SELECT INTO OUTFILE
mengintip dan merusak file-file yang tidak diinginkan. Chroot juga
perlu Anda pertimbangkan bila sebuah server memuat beberapa user yang
benar-benar tidak saling mempercayai satu sama lain. Dalam kenyataan
sehari-hari, saya jarang menggunakan chroot.
LOAD DATA LOCAL INFILE adalah fasilitas agar klien bisa membaca sebuah file dari filesystem dan memasukkannya ke tabel database. Sejak 3.23.49,
LOAD DATA LOCAL INFILE
sudah dimatikan secara default. Jika ada skrip CGI atau ASP di mesin
shared hosting Anda yang tidak diwrap, maka ada potensi perintah SQL
tersebut dipakai untuk membaca file-file sebagai user
nobody misalnya (dan mengambil source code skrip-skrip milik user lain lewat memuatnya ke tabel database lalu nanti ke file melalui
SELECT INTO OUTFILE atau lewat skrip yang melakukan
SELECT
dan mencetaknya ke file). Jika Anda cukup yakin bahwa semua skrip
terwrap, maka barulah Anda bisa mengaktifkan perintah SQL ini dengan
memberikan opsi:
[mysql]
local-infile
di
/etc/my.cnf. Skrip pun harus memberikan perintah
mysql_options() dulu—saat ini belum didukung oleh PHP—sebelum bisa
memberikan perintah SQL
LOAD DATA LOCAL INFILE.
Jika koneksi MySQL Anda hanya lokal via soket Unix, Anda cukup aman
di poin ini. Tapi jika daemon MySQL Anda harus menerima klien (atau
melakukan replikasi) via koneksi TCP dari Internet, maka ada potensi
pencurian data melalui sniffing paket. Password dan data-data sensitif
bisa melayang dari hop ke hop dan siap disadap. Ada baiknya Anda
mempertimbangkan mensetup SSL untuk koneksi MySQL.
MySQL baru mendukung koneksi SSL natif mulai 4.0. Karena itu, kita bisa menggunakan
stunnel
untuk mengenkripsi saluran komunikasi. Prinsip stunnel adalah, di sisi
klien dan server kita memasang gerbang stunnel masuk agar terbentuk
“terowongan”. Masing-masing klien dan server asli berkomunikasi secara
lokal (via localhost) dengan stunnel. Sementara barulah stunnel di kedua
sisi saling melakukan pembicaraan SSL. Berikut sebuah contoh. Di sisi
klien (mis, beralamat di
klien.com):
# /usr/sbin/stunnel -P/tmp/ -c -d 3306 -r server.com:3307
Di sisi server (yang beralamat mis di
server.com):
# cd /usr/share/ssl/certs
# openssl req -new -x509 -days 365 -nodes -out stunnel.pem \
-keyout stunnel.pem
# /usr/sbin/stunnel -P/tmp/ -p stunnel.pem -d 3307 -r localhost:3306
Lalu dari
klien.com lakukan koneksi MySQL ke server
server.com seperti biasa.