Server bisa punya akun-akun rapi dengan hak minim, tetapi satu kata sandi lemah membuat semuanya percuma: budi_analyst dengan sandi budi123 akan ditebak dalam hitungan menit. Penjaga kualitas kata sandi di MySQL versi 8 adalah komponen validate_password — dan berbeda dari versi-versi lama, komponen ini tidak terpasang secara default, sehingga server baru menerimam kata sandi apa pun, termasuk yang kosong ala chapter sebelumnya. Pada chapter ini kita memasang komponen tersebut secara nyata, menyaksikan ERROR 1819 menolak kata sandi lemah, lalu melengkapi kebijakan dengan tiga alat lain: masa berlaku sandi (PASSWORD EXPIRE), sandi ganda untuk masa transisi (RETAIN CURRENT PASSWORD), dan penguncian otomatis akun yang terlalu sering gagal login (FAILED_LOGIN_ATTEMPTS).

Memeriksa dan Memasang validate_password #

Status komponen tercermin dari variabel-variabel validate_password.* — bila tidak ada satu pun, komponen belum terpasang. Kita periksa dulu kondisi awal server:

sql
SHOW VARIABLES LIKE 'validate_password%';

Output yang Dihasilkan:

text
Empty set (0.00 sec)

Hasil kosong membenarkan: server masih menerima kata sandi apa pun. Di MySQL 8, alat ini berstatus komponen (bukan plugin lama), dipasang dengan satu statement administrator:

sql
INSTALL COMPONENT 'file://component_validate_password';

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

Pemasangan langsung efektif — tanpa restart server — dan pendaftaran komponen tersimpan di tabel mysql.component sehingga tetap terpasang setelah server dinyalakan ulang. Sekarang variabel-variabelnya hidup:

sql
SHOW VARIABLES LIKE 'validate_password%';

Output yang Dihasilkan:

text
+-------------------------------------------------+--------+
| Variable_name                                   | Value  |
+-------------------------------------------------+--------+
| validate_password.changed_characters_percentage | 0      |
| validate_password.check_user_name               | ON     |
| validate_password.dictionary_file               |        |
| validate_password.length                        | 8      |
| validate_password.mixed_case_count              | 1      |
| validate_password.number_count                  | 1      |
| validate_password.policy                        | MEDIUM |
| validate_password.special_char_count            | 1      |
+-------------------------------------------------+--------+
8 rows in set (0.00 sec)

Delapan variabel tampil dengan kebijakan default MEDIUM. Artinya, sebuah kata sandi minimal 8 karakter (length), memuat huruf kecil dan huruf besar (mixed_case_count), dan memuat angka (number_count); check_user_name turut melarang nama user muncul di dalam sandi, dan special_char_count baru diperhitungkan pada kebijakan STRONG yang juga memakai kamus kata terlarang dari dictionary_file.

ERROR 1819: Kata Sandi Lemah Ditolak #

Dengan komponen aktif, uji pertama kita sengaja mengirim sandi lemah abc123 — terlalu pendek, tanpa huruf besar:

sql
-- @error
CREATE USER 'u_lemah'@'localhost' IDENTIFIED BY 'abc123';

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

ERROR 1819 (HY000): Your password does not satisfy the current policy requirements
Bye

Server menolak dengan ERROR 1819 (HY000): Your password does not satisfy the current policy requirements — dan penting dicatat: akun u_lemah tidak dibuat sama sekali, karena penolakan terjadi sebelum baris akun ditulis. Kata sandi yang sama dengan chapter-chapter sebelumnya, SandiKuat2026!, lolos semua persyaratan MEDIUM: 15 karakter, huruf besar-kecil, angka, bahkan karakter khusus.

Sandi Ganda dan Masa Berlaku #

Rotasi kata sandi di server produksi punya masalah klasik: begitu sandi diganti, puluhan koneksi aplikasi yang masih memakai sandi lama langsung terputus. MySQL menjembataninya dengan RETAIN CURRENT PASSWORD — sandi lama tetap berlaku sebagai sandi kedua selama masa transisi. Kita siapkan akun uji lengkap dengan aturan penguncian, lalu aktifkan sandi gandanya:

sql
DROP USER IF EXISTS 'u_uji'@'localhost';
CREATE USER 'u_uji'@'localhost' IDENTIFIED BY 'SandiKuat2026!'
  FAILED_LOGIN_ATTEMPTS 3 PASSWORD_LOCK_TIME 2;

SELECT User_attributes -> '$.Password_locking' AS aturan_penguncian
FROM mysql.user WHERE user = 'u_uji';

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

+------------------------------------------------------------+
| aturan_penguncian                                          |
+------------------------------------------------------------+
| {"failed_login_attempts": 3, "password_lock_time_days": 2} |
+------------------------------------------------------------+
1 row in set (0.00 sec)

Kolom User_attributes (kolom JSON baru di mysql.user) merekam aturan penguncian kita: maksimal 3 percobaan gagal beruntun, lalu akun terkunci 2 hari. Lanjut ke sandi ganda:

sql
ALTER USER 'u_uji'@'localhost' IDENTIFIED BY 'SandiBaru2026!' RETAIN CURRENT PASSWORD;

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

Kedua kata sandi kini sama-sama sah. Kita buktikan dari terminal dengan dua koneksi terpisah — pertama sandi lama, lalu sandi baru:

bash
-- @test-bash
MYSQL_PWD='SandiKuat2026!' mysql -h 127.0.0.1 -P 3306 -u u_uji -t -e "SELECT 'sandi_lama' AS yang_dipakai;"
MYSQL_PWD='SandiBaru2026!' mysql -h 127.0.0.1 -P 3306 -u u_uji -t -e "SELECT 'sandi_baru' AS yang_dipakai;"

Output yang Dihasilkan:

text
+--------------+
| yang_dipakai |
+--------------+
| sandi_lama   |
+--------------+
+--------------+
| yang_dipakai |
+--------------+
| sandi_baru   |
+--------------+

Kedua login sukses — sandi lama tetap hidup untuk koneksi-koneksi lama, sementara sandi baru siap disebar bertahap. Setelah transisi selesai, ALTER USER ... IDENTIFIED BY '...' biasa (tanpa RETAIN) menghapus sandi lama.

Berikutnya, masa berlaku: PASSWORD EXPIRE menandai sandi kedaluwarsa sehingga pemilik akun dipaksa menggantinya pada login berikutnya. Eksekusi dan pemeriksaannya dari terminal:

bash
-- @test-bash
mysql -h 127.0.0.1 -P 3306 -u root -t -e "ALTER USER 'u_uji'@'localhost' PASSWORD EXPIRE; SELECT user, password_expired FROM mysql.user WHERE user = 'u_uji';"

Output yang Dihasilkan:

text
+-------+------------------+
| user  | password_expired |
+-------+------------------+
| u_uji | Y                |
+-------+------------------+

Kolom password_expired bernilai Y. Koneksi berikutnya tetap boleh masuk, tetapi berjalan dalam sandbox: setiap perintah selain penggantian sandi ditolak. Klien interaktif meminta sandi baru secara otomatis; klien batch perlu opsi --connect-expired-password agar bisa masuk dan melihat penolakannya:

bash
-- @test-bash
MYSQL_PWD='SandiBaru2026!' mysql -h 127.0.0.1 -P 3306 -u u_uji --connect-expired-password -e "SELECT 1;"

Output yang Dihasilkan:

text
ERROR 1820 (HY000) at line 1: You must reset your password using ALTER USER statement before executing this statement.

ERROR 1820 (HY000): You must reset your password using ALTER USER statement before executing this statement — akun terkunci rapat pada satu tugas: ganti sandi. Pola ini dipasangkan dengan opsi PASSWORD EXPIRE INTERVAL 90 DAY pada CREATE USER/ALTER USER untuk kedaluwarsa otomatis tiap 90 hari, atau PASSWORD EXPIRE NEVER untuk menonaktifkannya.

Mengunci Akun yang Diserang #

Aturan FAILED_LOGIN_ATTEMPTS 3 PASSWORD_LOCK_TIME 2 yang tadi kita pasang kini diuji. Tiga percobaan beruntun dengan sandi salah — meniru serangan tebakan sandi:

bash
-- @test-bash
for i in 1 2 3; do echo "Percobaan salah-sandi ke-$i:"; MYSQL_PWD='SandiSalahTotal' mysql -h 127.0.0.1 -P 3306 -u u_uji -e "SELECT 1;" 2>&1; done

Output yang Dihasilkan:

text
Percobaan salah-sandi ke-1:
ERROR 1045 (28000): Access denied for user 'u_uji'@'localhost' (using password: YES)
Percobaan salah-sandi ke-2:
ERROR 1045 (28000): Access denied for user 'u_uji'@'localhost' (using password: YES)
Percobaan salah-sandi ke-3:
ERROR 3955 (HY000): Access denied for user 'u_uji'@'localhost'. Account is blocked for 2 day(s) (2 day(s) remaining) due to 3 consecutive failed logins.

Mari kita bedah urutannya. Dua percobaan pertama ditolak biasa dengan ERROR 1045: Access denied. Pada percobaan ketiga, aturan terpicu: server menjawab ERROR 3955 (HY000): Access denied ... Account is blocked for 2 day(s) (2 day(s) remaining) due to 3 consecutive failed logins. Sejak momen itu, sandi yang benar pun akan ditolak selama dua hari — koneksi sah harus menunggu atau meminta administrator melepas kuncinya. Penghitung gagal beruntun kembali nol setelah login sukses, sehingga salah ketik sesekali oleh pemilik akun tidak akan mengunci apa pun; yang terkunci hanyalah pola gagal terus-menerus, ciri khas upaya menebak sandi.

Kebijakan Organisasi #

Setiap variabel komponen bisa diketatkan sesuai aturan organisasi — misalnya menaikkan validate_password.policy ke STRONG (menambah persyaratan karakter khusus dan pemeriksaan kamus), memperpanjang length, atau mengisi dictionary_file dengan daftar kata terlarang internal (nama perusahaan, merek, tahun berjalan). Untuk server yang dikelola tim, perubahan permanen paling praktis lewat SET PERSIST validate_password.policy = 'STRONG' — nilai tersimpan di mysqld-auto.cnf dan bertahan lintas restart, sehingga kebijakan tidak hilang diam-diam saat server dinyalakan ulang.

Membersihkan Latihan #

Akun uji kita jatuhkan dan komponen kita copot kembali — server kembali pada kondisi awal yang menerima segala sandi:

bash
-- @test-bash
mysql -h 127.0.0.1 -P 3306 -u root -e "DROP USER IF EXISTS 'u_uji'@'localhost'; DROP USER IF EXISTS 'u_lemah'@'localhost'; UNINSTALL COMPONENT 'file://component_validate_password';"

Referensi #

Kata sandi kini punya penjaga kualitas, masa berlaku, dan mekanisme ganti sandi yang manusiawi. Namun kebijakan sandi hanya menyulitkan penyerang dari luar — bila penyerang sudah menyelinap ke dalam aplikasi, pertahanan berikutnya adalah memastikan akun aplikasi itu sendiri sesempit mungkin. Chapter berikutnya merangkum semuanya dalam satu prinsip: least privilege, hak paling sedikit yang masih cukup.