Aplikasi nyaris selalu membutuhkan kredensial untuk berbicara dengan sistem lain: password database, API key payment gateway, kredensial SMTP, hingga kunci penandatanganan webhook. Nilai-nilai rahasia tersebut — yang kita sebut secret — adalah pembuka pintu ke sistem berbahaya bila jatuh ke tangan yang salah. Chapter ini membahas mengapa secret tidak boleh ditulis langsung di dalam kode atau dikomit ke git, lalu membangun kebiasaan pengelolaan yang aman: variabel lingkungan, berkas .env, rotasi berkala, dan langkah cepat ketika kebocoran terlanjur terjadi.
Apa Itu Secret #
Secret adalah nilai rahasia yang aplikasi gunakan untuk membuktikan identitasnya ke sistem lain. Berbeda dengan data biasa, secret tidak boleh muncul di log, di pesan error, apalagi di repositori. Beberapa contoh yang paling sering Anda temui:
| Jenis secret | Contoh wujudnya | Akibat bila bocor |
|---|---|---|
| API key layanan pihak ketiga | sk_... milik payment gateway | tagihan dan penyalahgunaan layanan |
| Password database | kredensial user MySQL/PostgreSQL | kebocoran sekaligus modifikasi data |
| Kredensial SMTP | password akun email pengirim | pengiriman spam atas nama domain Anda |
| Signing key | secret penandatangan token JWT/webhook | pemalsuan token yang dianggap sah |
Ciri umum semua secret: nilainya panjang, acak, dan hanya bisa diverifikasi — bukan ditebak. Karena itu satu-satunya pertahanan adalah menjaga kerahasiaannya; sekali bocor, tidak ada “menebak balik” yang bisa dilakukan.
Mengapa Secret Tidak Boleh Berada di Kode #
Menuliskan secret langsung di kode terasa praktis saat pertama kali, tetapi keputusan tersebut menyebar ke banyak tempat yang tidak bisa Anda kendalikan lagi. Riwayat git menyimpan setiap versi berkas selamanya — menhapus berkas pada commit terbaru tidak menghapus commit lama. Setiap orang yang pernah melakukan clone, setiap fork, setiap tampilan log CI, dan setiap tangkapan layar berbagi kode menjadi salinan secret tersebut.
Bentuk rentan yang paling klasik tampak seperti ini:
<?php
// RENTAN — jangan ditiru
$passwordDatabase = "r4hAsi4-S3k4l1";
$apiKeyPayment = "sk_uji_51H8xQzK9mNoPq";Kedua nilai tersebut melekat pada riwayat repositori selamanya, dan siapa pun yang membaca berkas ini — termasuk calon kontributor baru — langsung memegang kredensial produksi. Versi amannya memindahkan nilai keluar dari kode dan membacanya saat aplikasi berjalan:
<?php
// AMAN — nilai dibaca dari lingkungan saat runtime
$passwordDatabase = getenv("DB_PASSWORD");
$apiKeyPayment = getenv("PAYMENT_API_KEY");Dengan pola ini, berkas kode bisa dibagikan, direview, dan dikomit dengan bebas karena kode hanya memuat nama variabel lingkungan, bukan nilainya. Nilai aslinya hidup di luar repositori — di server, di CI, atau di mesin masing-masing pengembang.
Mendeteksi Secret dengan Pemindaian Sederhana #
Mengandalkan ketelitian mata mudah bocor: satu baris db_password=... yang terlewat saat commit sudah cukup untuk insiden. Pemindaian otomatis tidak perlu rumit — pola regex sederhana sudah menangkap banyak kasus umum. Mari kita bangun detektor mini yang memeriksa isi sebuah berkas konfigurasi:
<?php
function deteksiSecret(string $isiBerkas): array
{
$pola = [
"AWS access key" => "/\bAKIA[0-9A-Z]{16}\b/",
"private key" => "/-----BEGIN [A-Z ]*PRIVATE KEY-----/",
"kredensial hardcode" => "/(?i)(?:^|[\s_.-])(password|passwd|secret|api[_-]?key|apikey|token)\s*=\s*\S{8,}$/",
];
$temuan = [];
foreach (explode("\n", $isiBerkas) as $nomor => $baris) {
foreach ($pola as $jenis => $regex) {
if (preg_match($regex, $baris) === 1) {
$temuan[] = sprintf("baris %d: %s", $nomor + 1, $jenis);
}
}
}
return $temuan;
}
// Semua nilai di bawah ini PALSU, khusus untuk latihan.
$contohConfig = <<<'CONFIG'
app_name=Toko Budi
db_host=127.0.0.1
db_user=budi
db_password=r4hAsi4-S3k4l1
payment_api_key=sk_uji_51H8xQzK9mNoPq
aws_access_key=AKIAIOSFODNN7EXAMPLE
CONFIG;
foreach (deteksiSecret($contohConfig) as $temuan) {
echo "- $temuan\n";
}Output yang Dihasilkan:
- baris 4: kredensial hardcode
- baris 5: kredensial hardcode
- baris 6: AWS access keyMari kita bedah detektor di atas baris per baris:
- Array
$polamemetakan tiga jenis secret ke polanya. AWS access key selalu berawalanAKIAdiikuti 16 karakter alfanumerik kapital; private key selalu diawali baris-----BEGIN ... PRIVATE KEY-----. - Pola ketiga mencari penugasan ke kunci berbahaya seperti
password,secret, atauapi_key. Prefiks(?:^|[\s_.-])membuat pola cocok baik di awal baris maupun setelah garis bawah seperti padadb_password, dan\S{8,}$memastikan nilainya cukup panjang agar tidak melaporkandb_host=127.0.0.1. - Loop ganda memecah berkas menjadi baris, lalu menguji setiap pola terhadap setiap baris.
$nomor + 1mengubah indeks array (mulai dari 0) menjadi nomor baris yang manusiawi.
Variabel Lingkungan sebagai Saluran Resmi #
Variabel lingkungan (environment variable) adalah mekanisme bawaan sistem operasi untuk meneruskan konfigurasi ke proses tanpa menyentuh berkas kode. PHP membacanya lewat getenv(), dan untuk latihan kita bisa mengisinya lewat putenv():
<?php
putenv("APP_ENV=production");
putenv("PAYMENT_API_KEY=YOUR_API_KEY");
echo "APP_ENV : " . getenv("APP_ENV") . "\n";
echo "PAYMENT_API_KEY : " . getenv("PAYMENT_API_KEY") . "\n";
echo "TOKEN_TIDAK_ADA : " . var_export(getenv("TOKEN_TIDAK_ADA"), true) . "\n";Output yang Dihasilkan:
APP_ENV : production
PAYMENT_API_KEY : YOUR_API_KEY
TOKEN_TIDAK_ADA : falsePerhatikan baris terakhir: getenv() mengembalikan false — bukan null — ketika variabel tidak ada, sehingga pemeriksaan ?? tidak akan “menangkap” nilai hilang. Gunakan perbandingan eksplisit getenv("X") === false saat ketiadaan variabel adalah kondisi error.
PHP juga menyediakan superglobal $_ENV, tetapi keduanya tidak selalu sinkron — putenv() hanya mengisi penyimpanan yang dibaca getenv():
<?php
putenv("LATIHAN_MODE=demo");
var_dump(getenv("LATIHAN_MODE"));
var_dump(isset($_ENV["LATIHAN_MODE"]));Output yang Dihasilkan:
string(4) "demo"
bool(false)Hasil bool(false) pada baris kedua menunjukkan bahwa penugasan lewat putenv() tidak otomatis mengisi array $_ENV. Di produksi, variabel lingkungan biasanya diset oleh web server, container, atau proses CI sebelum PHP berjalan — keduanya terisi dari sumber yang sama. Praktik yang aman adalah memilih satu kanal pembacaan dan menggunakannya secara konsisten di seluruh aplikasi; detail penerapannya di berbagai lingkungan dibahas pada chapter environment variables
.
Berkas .env dan .gitignore #
Untuk pengembangan lokal, kebiasaan yang umum adalah menyimpan secret di berkas .env pada akar proyek, lalu mendaftarkan berkas tersebut ke .gitignore agar tidak pernah dikomit. Berikut latihan lengkapnya — membuat berkas, mem-parsing isinya, lalu membaca kembali:
<?php
function parseEnv(string $path): array
{
$hasil = [];
foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $baris) {
$baris = trim($baris);
if ($baris === "" || str_starts_with($baris, "#")) {
continue;
}
[$kunci, $nilai] = explode("=", $baris, 2);
$hasil[trim($kunci)] = trim($nilai);
}
return $hasil;
}
$dir = sys_get_temp_dir() . "/toko-budi-env";
if (!is_dir($dir)) {
mkdir($dir, 0700, true);
}
file_put_contents("$dir/.env", "APP_ENV=production\nDB_PASSWORD=YOUR_DATABASE_PASSWORD\n");
file_put_contents("$dir/.env.example", "APP_ENV=\nDB_PASSWORD=\n");
file_put_contents("$dir/.gitignore", ".env\n");
$env = parseEnv("$dir/.env");
echo "APP_ENV : " . $env["APP_ENV"] . "\n";
echo "DB_PASSWORD : " . $env["DB_PASSWORD"] . "\n";
echo ".gitignore : " . trim(file_get_contents("$dir/.gitignore")) . "\n";
echo "Kunci .env.example: " . implode(", ", array_keys(parseEnv("$dir/.env.example"))) . "\n";Output yang Dihasilkan:
APP_ENV : production
DB_PASSWORD : YOUR_DATABASE_PASSWORD
.gitignore : .env
Kunci .env.example: APP_ENV, DB_PASSWORDBeberapa keputusan penting tersembunyi di latihan tersebut. Direktori latihan dibuat dengan mode 0700 sehingga hanya pemilik akun yang bisa membacanya. Fungsi parseEnv() melewati baris kosong dan komentar berawalan #, lalu memecah tiap baris pada tanda = pertama — batas 2 pada explode() menjaga nilai yang mengandung = tetap utuh. Terakhir, berkas .env.example sengaja berisi kunci tanpa nilai: berkas inilah yang boleh dikomit sebagai dokumentasi kunci yang dibutuhkan aplikasi, sehingga anggota tim baru tinggal menyalinnya menjadi .env dan mengisi nilai miliknya sendiri.
Dalam proyek nyata Anda tidak perlu menulis parser sendiri — pustaka seperti vlucas/phpdotenv dan framework populer (Laravel, Symfony) sudah memuat .env otomatis ke variabel lingkungan saat aplikasi booting. Prinsipnya sama persis dengan latihan di atas: berkas di luar git, nilai naik ke variabel lingkungan, kode hanya membaca nama variabelnya.
Vault dan Secret Manager #
Ketika tim dan jumlah lingkungan bertumbuh, berkas .env mulai menemui batas: nilainya tersebar di banyak mesin, tidak ada jejak siapa membaca apa, dan rotasi harus dilakukan manual satu per satu. Solusi tingkat lanjut adalah vault atau secret manager — layanan terpusat seperti HashiCorp Vault, AWS Secrets Manager, Google Secret Manager, atau Azure Key Vault. Aplikasi berbicara ke vault saat booting, membuktikan identitasnya (misalnya lewat credential mesin), lalu menerima secret yang dibutuhkan.
Manfaat utamanya bukan sekadar penyimpanan, melainkan kendali: akses per aplikasi bisa dibatasi, setiap pembacaan tercatat di audit log, dan rotasi bisa diotomatisasi sehingga secret berumur pendek. Konsep penyiapan vault di sisi infrastruktur dibahas pada chapter managing secrets .
Rotasi Secret #
Rotasi adalah kebiasaan mengganti secret secara berkala, dengan asumsi bahwa nilai lama mungkin sudah menyebar lebih luas dari yang kita ketahui. Secret yang dirotasi setiap 90 hari jauh lebih murah dilindungi daripada secret yang sama dipakai bertahun-tahun. Mari kita tulis pemeriksa sederhana berbasis tanggal:
<?php
function perluRotasi(DateTimeImmutable $rotasiTerakhir, int $intervalHari, DateTimeImmutable $hariIni): bool
{
$batasWaktu = $rotasiTerakhir->add(new DateInterval("P{$intervalHari}D"));
return $hariIni > $batasWaktu;
}
$zona = new DateTimeZone("Asia/Jakarta");
$rotasiTerakhir = new DateTimeImmutable("2025-09-01", $zona);
$hariIni = new DateTimeImmutable("2026-01-15", $zona);
var_dump(perluRotasi($rotasiTerakhir, 90, $hariIni));
var_dump(perluRotasi($hariIni, 90, $hariIni));Output yang Dihasilkan:
bool(true)
bool(false)Fungsi perluRotasi() menghitung tenggat dengan menambahkan DateInterval sebesar interval rotasi, lalu membandingkannya dengan tanggal hari ini. Pemanggilan pertama mengembalikan true karena jeda sejak 1 September 2025 sudah melewati 90 hari, sedangkan pemanggilan kedua false karena baru saja dirotasi. Jadwalkan pemeriksaan seperti ini di tugas terjadwal agar rotasi tidak mengandalkan ingatan orang.
Ketika Secret Terlanjur Dikomit #
Kebocoran adalah skenario yang harus diasumsikan bisa terjadi, sehingga responsnya perlu dihafal sebagai prosedur. Urutan langkahnya penting — banyak tim melakukan kebalikannya dan kehilangan waktu berharga:
- Cabut secret tersebut segera di sumbernya: ganti API key di dasbor penyedia layanan, ganti password database, atau nonaktifkan akun yang bocor. Perlakukan secret yang tercommit sebagai sudah dicuri, karena repositori bisa saja telah di-clone pihak lain.
- Perbarui nilai baru di semua lingkungan — server produksi, staging, mesin pengembang, dan konfigurasi CI — lalu verifikasi aplikasi berjalan dengan nilai baru.
- Bersihkan riwayat git dengan alat seperti
git filter-repoatau BFG Repo-Cleaner. Langkah ini menghapus secret dari riwayat repositori, tetapi tidak bisa menarik kembali clone yang sudah beredar — karena itulah langkah 1 selalu menjadi prioritas. - Tutup celah masuknya: pastikan
.envberada di.gitignore, pasang pemindai secret sebagai pre-commit hook di mesin setiap pengembang, dan jalankan pemindaian yang sama sebagai tahapan CI pada setiap push agar kebocoran tertahan sebelum menyentuh branch utama.
Detektor mini pada chapter ini adalah bibit dari tahapan CI tersebut: jalankan fungsi deteksiSecret() terhadap diff setiap pull request, dan gagalkan pipeline ketika ditemukan temuan.
Ringkasan #
Secret adalah kredensial yang melindungi sistem lain, sehingga tempatnya di luar kode dan di luar git — dibaca lewat variabel lingkungan saat aplikasi berjalan, didokumentasikan lewat .env.example, dan dirotasi secara berkala. Ketika kebocoran terjadi, urutan responsnya adalah mencabut, memperbarui, membersihkan riwayat, lalu menutup celahnya dengan pemindaian otomatis. Aplikasi Anda kini berjalan dengan kredensial yang terkelola; kebiasaan aman berikutnya yang perlu dibangun adalah menjaga keamanan kode pihak ketiga yang aplikasi Anda pakai — pembahasan itu kita mulai pada chapter Dependency Security
.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
contoh.php
Unduh berkas<?php
// contoh.php — ringkasi praktik Secrets Management dalam satu program utuh.
// Seluruh nilai rahasia pada berkas ini PALSU dan hanya untuk latihan.
function deteksiSecret(string $isiBerkas): array
{
$pola = [
"AWS access key" => "/\bAKIA[0-9A-Z]{16}\b/",
"private key" => "/-----BEGIN [A-Z ]*PRIVATE KEY-----/",
"kredensial hardcode" => "/(?i)(?:^|[\s_.-])(password|passwd|secret|api[_-]?key|apikey|token)\s*=\s*\S{8,}$/",
];
$temuan = [];
foreach (explode("\n", $isiBerkas) as $nomor => $baris) {
foreach ($pola as $jenis => $regex) {
if (preg_match($regex, $baris) === 1) {
$temuan[] = sprintf("baris %d: %s", $nomor + 1, $jenis);
}
}
}
return $temuan;
}
function parseEnv(string $path): array
{
$hasil = [];
foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $baris) {
$baris = trim($baris);
if ($baris === "" || str_starts_with($baris, "#")) {
continue;
}
[$kunci, $nilai] = explode("=", $baris, 2);
$hasil[trim($kunci)] = trim($nilai);
}
return $hasil;
}
// Latihan 1: pindai berkas konfigurasi tiruan.
$contohConfig = <<<'CONFIG'
app_name=Toko Budi
db_host=127.0.0.1
db_user=budi
db_password=r4hAsi4-S3k4l1
payment_api_key=sk_uji_51H8xQzK9mNoPq
aws_access_key=AKIAIOSFODNN7EXAMPLE
CONFIG;
echo "=== Hasil pemindaian ===\n";
foreach (deteksiSecret($contohConfig) as $temuan) {
echo "- $temuan\n";
}
// Latihan 2: buat berkas .env + .gitignore, lalu baca kembali.
$dir = sys_get_temp_dir() . "/toko-budi-env";
if (!is_dir($dir)) {
mkdir($dir, 0700, true);
}
file_put_contents("$dir/.env", "APP_ENV=production\nDB_PASSWORD=YOUR_DATABASE_PASSWORD\n");
file_put_contents("$dir/.gitignore", ".env\n");
$env = parseEnv("$dir/.env");
echo "=== Isi .env ===\n";
echo "APP_ENV : " . $env["APP_ENV"] . "\n";
echo "DB_PASSWORD : " . $env["DB_PASSWORD"] . "\n";
echo ".gitignore : " . trim(file_get_contents("$dir/.gitignore")) . "\n";
// Latihan 3: cek jadwal rotasi.
$zona = new DateTimeZone("Asia/Jakarta");
$rotasiTerakhir = new DateTimeImmutable("2025-09-01", $zona);
$hariIni = new DateTimeImmutable("2026-01-15", $zona);
$perlu = $hariIni > $rotasiTerakhir->add(new DateInterval("P90D"));
echo "=== Rotasi ===\n";
echo "Perlu rotasi (interval 90 hari): " . ($perlu ? "ya" : "tidak") . "\n";